Skip to content

Commit

Permalink
Feature/pure ruby ssl implementation for root certificate issuer check (
Browse files Browse the repository at this point in the history
#71)

* Add option to treat anchor as a regexp. Fix parsing of openssl client output to work with both openssl 1.0 and openssl 1.1 formatting

* updates to make travis and rubocop happy

* Add pure ruby implementation of check-ssl-root-issuer.rb as alternative to check-ssl-anchor.rb

* make rubocop happy

* add test for check-ssl-root-issuer

* update changelog and README with new plugin information

* remove files changed in PR #70, unrelated to this new feature

* Update logic for validating issuer name format options. Using mixin libraries internal validation for allowed values.
  • Loading branch information
Jef Spaleta authored Jun 28, 2020
1 parent d497555 commit c2bc9d5
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 1 deletion.
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

0 comments on commit c2bc9d5

Please sign in to comment.