Skip to content

Commit 270e2cd

Browse files
author
Jef Spaleta
authored
Feature/pure ruby ssl implementation for root certificate issuer check (sensu-plugins#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 sensu-plugins#70, unrelated to this new feature * Update logic for validating issuer name format options. Using mixin libraries internal validation for allowed values.
1 parent b1d3a6e commit 270e2cd

File tree

4 files changed

+160
-1
lines changed

4 files changed

+160
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This CHANGELOG follows the format listed [here](https://github.com/sensu-plugins
1111
- 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
1212
- Require latest sensu-plugin for [Sensu Go support](https://github.com/sensu-plugins/sensu-plugin#sensu-go-enablement)
1313
- New option to treat anchor argument as a regexp
14+
- New Check plugin `check-ssl-root-issuer.rb` with alternative logic for trust anchor verification.
1415

1516
### Changed
1617
- `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

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ The Sensu assets packaged from this repository are built against the Sensu Ruby
2020
* bin/check-ssl-hsts-preload.rb
2121
* bin/check-ssl-hsts-preloadable.rb
2222
* bin/check-ssl-qualys.rb
23+
* bin/check-ssl-root-issuer.rb
2324

2425
## Usage
2526

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

28-
Check that a specific website is chained to a specific root certificate (Let's Encrypt for instance).
29+
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.
2930

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

60+
### `bin/check-ssl-root-issuer.rb`
61+
62+
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.
63+
64+
```
65+
./bin/check-ssl-root-issuer.rb -u example.com -a "CN=DST Root CA X3,O=Digital Signature Trust Co."
66+
```
5967

6068
## Installation
6169

bin/check-ssl-root-issuer.rb

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#! /usr/bin/env ruby
2+
#
3+
# check-ssl-root-issuer
4+
#
5+
# DESCRIPTION:
6+
# Check that a certificate is chained to a specific root certificate issuer
7+
#
8+
# OUTPUT:
9+
# plain text
10+
#
11+
# PLATFORMS:
12+
# Linux
13+
#
14+
# DEPENDENCIES:
15+
# gem: sensu-plugin
16+
#
17+
# USAGE:
18+
#
19+
# Check that a specific website is chained to a specific root certificate
20+
# ./check-ssl-root-issuer.rb \
21+
# -u https://example.com \
22+
# -i "CN=DST Root CA X3,O=Digital Signature Trust Co."
23+
#
24+
# LICENSE:
25+
# Copyright Jef Spaleta (jspaleta@gmail.com) 2020
26+
# Released under the same terms as Sensu (the MIT license); see LICENSE
27+
# for details.
28+
#
29+
30+
require 'sensu-plugin/check/cli'
31+
require 'openssl'
32+
require 'uri'
33+
require 'net/http'
34+
require 'net/https'
35+
36+
#
37+
# Check root certificate has specified issuer name
38+
#
39+
class CheckSSLRootIssuer < Sensu::Plugin::Check::CLI
40+
option :url,
41+
description: 'Url to check: Ex "https://google.com"',
42+
short: '-u',
43+
long: '--url URL',
44+
required: true
45+
46+
option :issuer,
47+
description: 'An X509 certificate issuer name, RFC2253 format Ex: "CN=DST Root CA X3,O=Digital Signature Trust Co."',
48+
short: '-i',
49+
long: '--issuer ISSUER_NAME',
50+
required: true
51+
52+
option :regexp,
53+
description: 'Treat the issuer name as a regexp',
54+
short: '-r',
55+
long: '--regexp',
56+
default: false,
57+
boolean: true,
58+
required: false
59+
60+
option :format,
61+
description: 'optional issuer name format.',
62+
short: '-f',
63+
long: '--format FORMAT_VAL',
64+
default: 'RFC2253',
65+
in: %w('RFC2253', 'ONELINE', 'COMPAT'),
66+
required: false
67+
68+
def cert_name_format
69+
# Note: because format argument is pre-validated by mixin 'in' logic eval is safe to use
70+
eval "OpenSSL::X509::Name::#{config[:format]}" # rubocop:disable Lint/Eval
71+
end
72+
73+
def validate_issuer(cert)
74+
issuer = cert.issuer.to_s(cert_name_format)
75+
if config[:regexp]
76+
issuer_regexp = Regexp.new(config[:issuer].to_s)
77+
issuer =~ issuer_regexp
78+
else
79+
issuer == config[:issuer].to_s
80+
end
81+
end
82+
83+
def find_root_cert(uri)
84+
root_cert = nil
85+
http = Net::HTTP.new(uri.host, uri.port)
86+
http.open_timeout = 10
87+
http.read_timeout = 10
88+
http.use_ssl = true
89+
http.cert_store = OpenSSL::X509::Store.new
90+
http.cert_store.set_default_paths
91+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
92+
93+
http.verify_callback = lambda { |verify_ok, store_context|
94+
root_cert = store_context.current_cert unless root_cert
95+
unless verify_ok
96+
@failed_cert = store_context.current_cert
97+
@failed_cert_reason = [store_context.error, store_context.error_string] if store_context.error != 0
98+
end
99+
verify_ok
100+
}
101+
http.start {}
102+
root_cert
103+
end
104+
105+
# Do the actual work and massage some data
106+
107+
def run
108+
@fail_cert = nil
109+
@failed_cert_reason = 'Unknown'
110+
uri = URI.parse(config[:url])
111+
critical "url protocol must be https, you specified #{url}" if uri.scheme != 'https'
112+
root_cert = find_root_cert(uri)
113+
if @failed_cert
114+
msg = "Certificate verification failed.\n Reason: #{@failed_cert_reason}"
115+
critical msg
116+
end
117+
118+
if validate_issuer(root_cert)
119+
msg = 'Root certificate in chain has expected issuer name'
120+
ok msg
121+
else
122+
msg = "Root certificate issuer did not match expected name.\nFound: \"#{root_cert.issuer.to_s(config[:issuer_format])}\""
123+
critical msg
124+
end
125+
end
126+
end

test/check-ssl-root-issuer.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require_relative '../bin/check-ssl-anchor.rb'
2+
3+
describe CheckSSLRootIssuer do
4+
before(:all) do
5+
# Ensure the check isn't run when exiting (which is the default)
6+
CheckSSLRootIssuer.class_variable_set(:@@autorun, nil)
7+
end
8+
9+
let(:check) do
10+
CheckSSLRootIssuer.new ['-u', 'https://philporada.com', '-i', '"CN=DST Root CA X3,O=Digital Signature Trust Co."']
11+
end
12+
13+
it 'should pass check if the root issuer matches what the users -i flag' do
14+
expect(check).to receive(:ok).and_raise SystemExit
15+
expect { check.run }.to raise_error SystemExit
16+
end
17+
18+
it 'should pass check if the root issuer matches what the users -i flag' do
19+
check.config[:anchor] = 'testdata'
20+
check.config[:regexp] = false
21+
expect(check).to receive(:critical).and_raise SystemExit
22+
expect { check.run }.to raise_error SystemExit
23+
end
24+
end

0 commit comments

Comments
 (0)