sslcheck 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +177 -0
- data/Rakefile +14 -0
- data/acceptance/acceptance_helper.rb +1 -0
- data/acceptance/checking_certificates_spec.rb +23 -0
- data/acceptance/client_spec.rb +67 -0
- data/blinky_tests +3 -0
- data/lib/sslcheck.rb +20 -0
- data/lib/sslcheck/certificate.rb +121 -0
- data/lib/sslcheck/certificate_client.rb +10 -0
- data/lib/sslcheck/check.rb +65 -0
- data/lib/sslcheck/client.rb +68 -0
- data/lib/sslcheck/generic_error.rb +15 -0
- data/lib/sslcheck/parser.rb +96 -0
- data/lib/sslcheck/validator.rb +111 -0
- data/lib/sslcheck/validators/ca_bundle.rb +23 -0
- data/lib/sslcheck/validators/common_name.rb +27 -0
- data/lib/sslcheck/validators/errors.rb +15 -0
- data/lib/sslcheck/validators/expiration_date.rb +10 -0
- data/lib/sslcheck/validators/generic_validator.rb +15 -0
- data/lib/sslcheck/validators/issue_date.rb +10 -0
- data/lib/sslcheck/version.rb +3 -0
- data/run_acceptance_on_ci +7 -0
- data/sentinal +10 -0
- data/spec/ca_bundle_validator_spec.rb +24 -0
- data/spec/cert_fixtures.rb +814 -0
- data/spec/certificate_spec.rb +134 -0
- data/spec/check_spec.rb +172 -0
- data/spec/common_name_validator_spec.rb +40 -0
- data/spec/expiration_date_validator_spec.rb +36 -0
- data/spec/issue_date_validator_spec.rb +36 -0
- data/spec/parser_spec.rb +0 -0
- data/spec/response_spec.rb +13 -0
- data/spec/spec_helper.rb +100 -0
- data/spec/validator_spec.rb +84 -0
- data/sslcheck.gemspec +26 -0
- metadata +165 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
module SSLCheck
|
2
|
+
class Check
|
3
|
+
attr_accessor :peer_cert, :ca_bundle, :host_name
|
4
|
+
def initialize(client=nil, validator=nil)
|
5
|
+
@client = client || Client.new
|
6
|
+
@validator = validator || Validator.new
|
7
|
+
@errors = []
|
8
|
+
@checked = false
|
9
|
+
end
|
10
|
+
|
11
|
+
def check(url)
|
12
|
+
fetch(url)
|
13
|
+
validate if no_errors?
|
14
|
+
@checked = true
|
15
|
+
@url = url
|
16
|
+
return self
|
17
|
+
end
|
18
|
+
|
19
|
+
def errors
|
20
|
+
@errors
|
21
|
+
end
|
22
|
+
|
23
|
+
def failed?
|
24
|
+
return false if no_errors?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid?
|
29
|
+
return true if no_errors? && checked?
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def checked?
|
34
|
+
return true if @checked
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
def url
|
39
|
+
@url
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def no_errors?
|
45
|
+
@errors.empty?
|
46
|
+
end
|
47
|
+
|
48
|
+
def fetch(url)
|
49
|
+
response = @client.get(url)
|
50
|
+
self.peer_cert = response.peer_cert
|
51
|
+
self.ca_bundle = response.ca_bundle
|
52
|
+
self.host_name = response.host_name
|
53
|
+
|
54
|
+
response.errors.each do |error|
|
55
|
+
@errors << error
|
56
|
+
end
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate
|
61
|
+
@validator.validate(host_name, peer_cert, ca_bundle)
|
62
|
+
true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
|
5
|
+
module SSLCheck
|
6
|
+
class Client
|
7
|
+
class Response
|
8
|
+
attr_accessor :host_name, :errors
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
self.errors = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def raw_peer_cert=(peer_cert)
|
15
|
+
@raw_peer_cert = peer_cert
|
16
|
+
end
|
17
|
+
|
18
|
+
def raw_peer_cert_chain=(peer_cert_chain)
|
19
|
+
@raw_peer_cert_chain = peer_cert_chain
|
20
|
+
end
|
21
|
+
|
22
|
+
def peer_cert
|
23
|
+
Certificate.new(@raw_peer_cert)
|
24
|
+
end
|
25
|
+
|
26
|
+
def ca_bundle
|
27
|
+
@raw_peer_cert_chain.map{|ca_cert| Certificate.new(ca_cert) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@response = Response.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def get(url)
|
36
|
+
begin
|
37
|
+
uri = determine_uri(url)
|
38
|
+
|
39
|
+
sock = TCPSocket.new(uri.host, 443)
|
40
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
41
|
+
ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
42
|
+
|
43
|
+
@socket = OpenSSL::SSL::SSLSocket.new(sock, ctx).tap do |socket|
|
44
|
+
socket.sync_close = true
|
45
|
+
socket.connect
|
46
|
+
@response.host_name = uri.host
|
47
|
+
@response.raw_peer_cert = OpenSSL::X509::Certificate.new(socket.peer_cert)
|
48
|
+
@response.raw_peer_cert_chain = socket.peer_cert_chain
|
49
|
+
end
|
50
|
+
|
51
|
+
@socket.sysclose
|
52
|
+
rescue URI::InvalidURIError
|
53
|
+
@response.errors << SSLCheck::Errors::Connection::InvalidURI.new({:name => "Invalid URI Error", :type => :invalid_uri, :message => "The URI, #{url}, is not a valid URI."})
|
54
|
+
rescue OpenSSL::SSL::SSLError
|
55
|
+
@response.errors << SSLCheck::Errors::Connection::SSLVerify.new({:name => "OpenSSL Verification Error", :type => :openssl_error, :message => "There was a peer verification error."})
|
56
|
+
end
|
57
|
+
|
58
|
+
@response
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def determine_uri(url)
|
63
|
+
return URI.parse(url) if url.match(/^https\:\/\//)
|
64
|
+
return URI.parse(url.gsub("http","https")) if url.match(/^http\:\/\//)
|
65
|
+
return URI.parse("https://#{url}") if url.match(/^https\:\/\//).nil?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SSLCheck
|
2
|
+
module Errors
|
3
|
+
class GenericError
|
4
|
+
attr_accessor :name, :type, :message
|
5
|
+
def initialize(opts={})
|
6
|
+
self.name = opts[:name]
|
7
|
+
self.type = opts[:type]
|
8
|
+
self.message = opts[:message]
|
9
|
+
end
|
10
|
+
def to_s
|
11
|
+
"[#{self.name}] #{self.message}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module SSLCheck
|
2
|
+
class Parser
|
3
|
+
class SSLNotConfigured < StandardError; end
|
4
|
+
|
5
|
+
def initialize(raw, url=nil)
|
6
|
+
@raw = raw
|
7
|
+
@url = url
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse
|
11
|
+
raise SSLNotConfigured if connection_refused?
|
12
|
+
{
|
13
|
+
"raw" => @raw,
|
14
|
+
"valid_certificate" => valid_certificate?,
|
15
|
+
"issued_by" => issued_by,
|
16
|
+
"issued_at" => issued_at,
|
17
|
+
"expires_at" => expires_at,
|
18
|
+
"organizational_unit" => organizational_unit,
|
19
|
+
"common_name" => common_name,
|
20
|
+
"issuer_country" => issuer_country,
|
21
|
+
"issuer_state" => issuer_state,
|
22
|
+
"issuer_locality" => issuer_locality,
|
23
|
+
"issuer_organization" => issuer_organization,
|
24
|
+
"issuer_common_name" => issuer_common_name,
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def certs
|
29
|
+
@raw.scan(/((?<=-----BEGIN CERTIFICATE-----)(?:\S+|\s(?!-----END CERTIFICATE-----))+(?=\s-----END CERTIFICATE-----))/)
|
30
|
+
.flatten
|
31
|
+
.map{|cert| "-----BEGIN CERTIFICATE-----\n#{cert.strip}\n-----END CERTIFICATE-----\n" }
|
32
|
+
end
|
33
|
+
|
34
|
+
def certificate
|
35
|
+
Certificate.new certs.first
|
36
|
+
end
|
37
|
+
|
38
|
+
def ca_bundle
|
39
|
+
Certificate.new certs[1..certs.size].join("\n")
|
40
|
+
end
|
41
|
+
|
42
|
+
def url
|
43
|
+
@url
|
44
|
+
end
|
45
|
+
|
46
|
+
def issued_by
|
47
|
+
certificate.issued_by
|
48
|
+
end
|
49
|
+
|
50
|
+
def issued_at
|
51
|
+
certificate.not_before
|
52
|
+
end
|
53
|
+
|
54
|
+
def expires_at
|
55
|
+
certificate.not_after
|
56
|
+
end
|
57
|
+
|
58
|
+
def organizational_unit
|
59
|
+
certificate.organizational_unit
|
60
|
+
end
|
61
|
+
|
62
|
+
def common_name
|
63
|
+
certificate.common_name
|
64
|
+
end
|
65
|
+
|
66
|
+
def issuer_country
|
67
|
+
certificate.issuer_country
|
68
|
+
end
|
69
|
+
|
70
|
+
def issuer_state
|
71
|
+
certificate.issuer_state
|
72
|
+
end
|
73
|
+
|
74
|
+
def issuer_locality
|
75
|
+
certificate.issuer_locality
|
76
|
+
end
|
77
|
+
|
78
|
+
def issuer_organization
|
79
|
+
certificate.issuer_organization
|
80
|
+
end
|
81
|
+
|
82
|
+
def issuer_common_name
|
83
|
+
certificate.issuer_common_name
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
private
|
88
|
+
def connection_refused?
|
89
|
+
@raw.match("connect: Connection refused")
|
90
|
+
end
|
91
|
+
|
92
|
+
def valid_certificate?
|
93
|
+
Validator.new(self).validate
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module SSLCheck
|
4
|
+
class Validator
|
5
|
+
class CommonNameMissingError < ArgumentError;end
|
6
|
+
class PeerCertificateMissingError < ArgumentError;end
|
7
|
+
class CABundleMissingError < ArgumentError;end
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@valid = false
|
11
|
+
@errors = []
|
12
|
+
@warnings = []
|
13
|
+
@common_name = nil
|
14
|
+
@peer_cert = nil
|
15
|
+
@ca_bundle = []
|
16
|
+
@validated = false
|
17
|
+
@default_validators = [
|
18
|
+
Validators::CommonName,
|
19
|
+
Validators::IssueDate,
|
20
|
+
Validators::ExpirationDate,
|
21
|
+
Validators::CABundle,
|
22
|
+
]
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate(common_name=nil, peer_cert=nil, ca_bundle=[], validators=[])
|
26
|
+
raise CommonNameMissingError if common_name.nil? || common_name.empty?
|
27
|
+
raise PeerCertificateMissingError if peer_cert.nil?
|
28
|
+
raise CABundleMissingError if ca_bundle.nil? || ca_bundle.empty?
|
29
|
+
@common_name = common_name
|
30
|
+
@peer_cert = peer_cert
|
31
|
+
|
32
|
+
run_validations(validators)
|
33
|
+
end
|
34
|
+
|
35
|
+
def valid?
|
36
|
+
@validated && errors.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def errors
|
40
|
+
@errors.compact
|
41
|
+
end
|
42
|
+
|
43
|
+
def warnings
|
44
|
+
[]
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def run_validations(validators)
|
49
|
+
validators = @default_validators if validators.empty?
|
50
|
+
validators.each do |validator|
|
51
|
+
@errors << validator.new(@common_name, @peer_cert, @ca_bundle).validate
|
52
|
+
end
|
53
|
+
@validated = true
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# class InvalidCertificate < StandardError;end
|
60
|
+
# class InvalidCommonName < StandardError;end
|
61
|
+
# class InvalidDates < StandardError;end
|
62
|
+
# class MissingCACertificate < StandardError;end
|
63
|
+
|
64
|
+
# def initialize(parser=nil)
|
65
|
+
# @parser = parser
|
66
|
+
# @url = parser.url
|
67
|
+
# end
|
68
|
+
|
69
|
+
# def validate
|
70
|
+
# raise InvalidCertificate unless validate_certificates
|
71
|
+
# raise InvalidCommonName, "expected #{@url} but got #{certificate.common_name}" unless validate_common_name
|
72
|
+
# raise InvalidDates, "Issued On: #{certificate.not_before}, Expires On: #{certificate.not_after}" unless validate_dates
|
73
|
+
# true
|
74
|
+
# end
|
75
|
+
|
76
|
+
# def validate_certificates
|
77
|
+
# certificate.verify(ca_bundle)
|
78
|
+
# end
|
79
|
+
|
80
|
+
# def validate_common_name
|
81
|
+
# matching_wildcard_domain || certificate.common_name.downcase == @url.downcase
|
82
|
+
# end
|
83
|
+
|
84
|
+
# def validate_expiration_date
|
85
|
+
# !certificate.expired?
|
86
|
+
# end
|
87
|
+
|
88
|
+
# def validate_issue_date
|
89
|
+
# certificate.issued?
|
90
|
+
# end
|
91
|
+
|
92
|
+
# def validate_dates
|
93
|
+
# validate_expiration_date && validate_issue_date
|
94
|
+
# end
|
95
|
+
|
96
|
+
# private
|
97
|
+
# def certificate
|
98
|
+
# @parser.certificate
|
99
|
+
# end
|
100
|
+
|
101
|
+
# def matching_wildcard_domain
|
102
|
+
# true if (certificate.common_name.match(/\*\./) && @url.include?(certificate.common_name.gsub(/\*\./,'')))
|
103
|
+
# end
|
104
|
+
|
105
|
+
# def ca_bundle
|
106
|
+
# begin
|
107
|
+
# @parser.ca_bundle
|
108
|
+
# rescue OpenSSL::X509::CertificateError => e
|
109
|
+
# raise MissingCACertificate
|
110
|
+
# end
|
111
|
+
# end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SSLCheck
|
2
|
+
module Validators
|
3
|
+
class CABundle < GenericValidator
|
4
|
+
def validate
|
5
|
+
return nil if verified_certificate?
|
6
|
+
SSLCheck::Errors::Validation::CABundleVerification.new({:name => "Certificate Authority Verification", :message => "The Certificate could not be verified using the supplied Certificate Authority (CA) Bundle."})
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def verified_certificate?
|
11
|
+
return false if @ca_bundle.empty?
|
12
|
+
|
13
|
+
store = OpenSSL::X509::Store.new
|
14
|
+
|
15
|
+
@ca_bundle.each do |ca_cert|
|
16
|
+
store.add_cert ca_cert.to_x509
|
17
|
+
end
|
18
|
+
|
19
|
+
store.verify(@peer_cert.to_x509)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module SSLCheck
|
2
|
+
module Validators
|
3
|
+
class CommonName < GenericValidator
|
4
|
+
def validate
|
5
|
+
return nil if common_name_matches?
|
6
|
+
SSLCheck::Errors::Validation::CommonNameMismatch.new({:name => "Common Name Mismatch", :message => "This certificate is not valid for #{@common_name}."})
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def common_name_matches?
|
11
|
+
matching_wildcard_domain || alternate_common_name_match || direct_common_name_match
|
12
|
+
end
|
13
|
+
|
14
|
+
def matching_wildcard_domain
|
15
|
+
true if (@peer_cert.common_name.match(/\*\./) && @common_name.include?(@peer_cert.common_name.gsub(/\*\./,'')))
|
16
|
+
end
|
17
|
+
|
18
|
+
def direct_common_name_match
|
19
|
+
@peer_cert.common_name.downcase == @common_name.downcase
|
20
|
+
end
|
21
|
+
|
22
|
+
def alternate_common_name_match
|
23
|
+
@peer_cert.alternate_common_names.include?(@common_name.downcase)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SSLCheck
|
2
|
+
module Errors
|
3
|
+
module Connection
|
4
|
+
class InvalidURI < GenericError; end
|
5
|
+
class SSLVerify < GenericError; end
|
6
|
+
end
|
7
|
+
|
8
|
+
module Validation
|
9
|
+
class CommonNameMismatch < GenericError;end
|
10
|
+
class NotYetIssued < GenericError;end
|
11
|
+
class CertificateExpired < GenericError;end
|
12
|
+
class CABundleVerification < GenericError;end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module SSLCheck
|
2
|
+
module Validators
|
3
|
+
class ExpirationDate < GenericValidator
|
4
|
+
def validate(clock=DateTime)
|
5
|
+
return nil if clock.now < @peer_cert.not_after
|
6
|
+
SSLCheck::Errors::Validation::CertificateExpired.new({:name => "Certifiate Expired", :message => "This certificate expired on #{@peer_cert.not_after}."})
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|