sslcheck 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|