Skip to content

Feature/pure ruby ssl implementation for root certificate issuer check #71

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This CHANGELOG follows the format listed [here](https://github.com/sensu-plugins
- Travis build automation to generate Sensu Asset tarballs that can be used n conjunction with Sensu provided ruby runtime assets and the Bonsai Asset Index
- Require latest sensu-plugin for [Sensu Go support](https://github.com/sensu-plugins/sensu-plugin#sensu-go-enablement)
- New option to treat anchor argument as a regexp
- New Check plugin `check-ssl-root-issuer.rb` with alternative logic for trust anchor verification.

### Changed
- `check-ssl-anchor.rb` uses regexp to test for present of certificates in cert chain that works with both openssl 1.0 and 1.1 formatting
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ The Sensu assets packaged from this repository are built against the Sensu Ruby
* bin/check-ssl-hsts-preload.rb
* bin/check-ssl-hsts-preloadable.rb
* bin/check-ssl-qualys.rb
* bin/check-ssl-root-issuer.rb

## Usage

### `bin/check-ssl-anchor.rb`

Check that a specific website is chained to a specific root certificate (Let's Encrypt for instance).
Check that a specific website is chained to a specific root certificate (Let's Encrypt for instance). Requires the `openssl` commandline tool to be available on the system.

```
./bin/check-ssl-anchor.rb -u example.com -a "i:/O=Digital Signature Trust Co./CN=DST Root CA X3"
Expand Down Expand Up @@ -56,6 +57,13 @@ Checks the ssllabs qualysis api for grade of your server, this check can be quit
./bin/check-ssl-qualys.rb -d google.com
```

### `bin/check-ssl-root-issuer.rb`

Check that a specific website is chained to a specific root certificate issuer. This is a pure Ruby implementation, does not require the openssl cmdline client tool to be installed.

```
./bin/check-ssl-root-issuer.rb -u example.com -a "CN=DST Root CA X3,O=Digital Signature Trust Co."
```

## Installation

Expand Down
126 changes: 126 additions & 0 deletions bin/check-ssl-root-issuer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#! /usr/bin/env ruby
#
# check-ssl-root-issuer
#
# DESCRIPTION:
# Check that a certificate is chained to a specific root certificate issuer
#
# OUTPUT:
# plain text
#
# PLATFORMS:
# Linux
#
# DEPENDENCIES:
# gem: sensu-plugin
#
# USAGE:
#
# Check that a specific website is chained to a specific root certificate
# ./check-ssl-root-issuer.rb \
# -u https://example.com \
# -i "CN=DST Root CA X3,O=Digital Signature Trust Co."
#
# LICENSE:
# Copyright Jef Spaleta (jspaleta@gmail.com) 2020
# Released under the same terms as Sensu (the MIT license); see LICENSE
# for details.
#

require 'sensu-plugin/check/cli'
require 'openssl'
require 'uri'
require 'net/http'
require 'net/https'

#
# Check root certificate has specified issuer name
#
class CheckSSLRootIssuer < Sensu::Plugin::Check::CLI
option :url,
description: 'Url to check: Ex "https://google.com"',
short: '-u',
long: '--url URL',
required: true

option :issuer,
description: 'An X509 certificate issuer name, RFC2253 format Ex: "CN=DST Root CA X3,O=Digital Signature Trust Co."',
short: '-i',
long: '--issuer ISSUER_NAME',
required: true

option :regexp,
description: 'Treat the issuer name as a regexp',
short: '-r',
long: '--regexp',
default: false,
boolean: true,
required: false

option :format,
description: 'optional issuer name format.',
short: '-f',
long: '--format FORMAT_VAL',
default: 'RFC2253',
in: %w('RFC2253', 'ONELINE', 'COMPAT'),
required: false

def cert_name_format
# Note: because format argument is pre-validated by mixin 'in' logic eval is safe to use
eval "OpenSSL::X509::Name::#{config[:format]}" # rubocop:disable Lint/Eval
end

def validate_issuer(cert)
issuer = cert.issuer.to_s(cert_name_format)
if config[:regexp]
issuer_regexp = Regexp.new(config[:issuer].to_s)
issuer =~ issuer_regexp
else
issuer == config[:issuer].to_s
end
end

def find_root_cert(uri)
root_cert = nil
http = Net::HTTP.new(uri.host, uri.port)
http.open_timeout = 10
http.read_timeout = 10
http.use_ssl = true
http.cert_store = OpenSSL::X509::Store.new
http.cert_store.set_default_paths
http.verify_mode = OpenSSL::SSL::VERIFY_PEER

http.verify_callback = lambda { |verify_ok, store_context|
root_cert = store_context.current_cert unless root_cert
unless verify_ok
@failed_cert = store_context.current_cert
@failed_cert_reason = [store_context.error, store_context.error_string] if store_context.error != 0
end
verify_ok
}
http.start {}
root_cert
end

# Do the actual work and massage some data

def run
@fail_cert = nil
@failed_cert_reason = 'Unknown'
uri = URI.parse(config[:url])
critical "url protocol must be https, you specified #{url}" if uri.scheme != 'https'
root_cert = find_root_cert(uri)
if @failed_cert
msg = "Certificate verification failed.\n Reason: #{@failed_cert_reason}"
critical msg
end

if validate_issuer(root_cert)
msg = 'Root certificate in chain has expected issuer name'
ok msg
else
msg = "Root certificate issuer did not match expected name.\nFound: \"#{root_cert.issuer.to_s(config[:issuer_format])}\""
critical msg
end
end
end
24 changes: 24 additions & 0 deletions test/check-ssl-root-issuer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require_relative '../bin/check-ssl-anchor.rb'

describe CheckSSLRootIssuer do
before(:all) do
# Ensure the check isn't run when exiting (which is the default)
CheckSSLRootIssuer.class_variable_set(:@@autorun, nil)
end

let(:check) do
CheckSSLRootIssuer.new ['-u', 'https://philporada.com', '-i', '"CN=DST Root CA X3,O=Digital Signature Trust Co."']
end

it 'should pass check if the root issuer matches what the users -i flag' do
expect(check).to receive(:ok).and_raise SystemExit
expect { check.run }.to raise_error SystemExit
end

it 'should pass check if the root issuer matches what the users -i flag' do
check.config[:anchor] = 'testdata'
check.config[:regexp] = false
expect(check).to receive(:critical).and_raise SystemExit
expect { check.run }.to raise_error SystemExit
end
end