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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +177 -0
  8. data/Rakefile +14 -0
  9. data/acceptance/acceptance_helper.rb +1 -0
  10. data/acceptance/checking_certificates_spec.rb +23 -0
  11. data/acceptance/client_spec.rb +67 -0
  12. data/blinky_tests +3 -0
  13. data/lib/sslcheck.rb +20 -0
  14. data/lib/sslcheck/certificate.rb +121 -0
  15. data/lib/sslcheck/certificate_client.rb +10 -0
  16. data/lib/sslcheck/check.rb +65 -0
  17. data/lib/sslcheck/client.rb +68 -0
  18. data/lib/sslcheck/generic_error.rb +15 -0
  19. data/lib/sslcheck/parser.rb +96 -0
  20. data/lib/sslcheck/validator.rb +111 -0
  21. data/lib/sslcheck/validators/ca_bundle.rb +23 -0
  22. data/lib/sslcheck/validators/common_name.rb +27 -0
  23. data/lib/sslcheck/validators/errors.rb +15 -0
  24. data/lib/sslcheck/validators/expiration_date.rb +10 -0
  25. data/lib/sslcheck/validators/generic_validator.rb +15 -0
  26. data/lib/sslcheck/validators/issue_date.rb +10 -0
  27. data/lib/sslcheck/version.rb +3 -0
  28. data/run_acceptance_on_ci +7 -0
  29. data/sentinal +10 -0
  30. data/spec/ca_bundle_validator_spec.rb +24 -0
  31. data/spec/cert_fixtures.rb +814 -0
  32. data/spec/certificate_spec.rb +134 -0
  33. data/spec/check_spec.rb +172 -0
  34. data/spec/common_name_validator_spec.rb +40 -0
  35. data/spec/expiration_date_validator_spec.rb +36 -0
  36. data/spec/issue_date_validator_spec.rb +36 -0
  37. data/spec/parser_spec.rb +0 -0
  38. data/spec/response_spec.rb +13 -0
  39. data/spec/spec_helper.rb +100 -0
  40. data/spec/validator_spec.rb +84 -0
  41. data/sslcheck.gemspec +26 -0
  42. metadata +165 -0
@@ -0,0 +1,10 @@
1
+ require 'uri'
2
+
3
+ module SSLCheck
4
+ class CertificateClient
5
+ def fetch(url)
6
+ uri = URI.parse(url)
7
+ `bash -c '(sleep 5; kill $$) & exec openssl s_client -showcerts -connect #{uri.to_s}:443 < /dev/null'`
8
+ end
9
+ end
10
+ end
@@ -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