-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/pure ruby ssl implementation for root certificate issuer check (
#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
Showing
4 changed files
with
160 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |