sslcheck 0.9.0

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