ssl-test 1.0.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e444097317630bf487a1e1c4c55baa00de963656
4
- data.tar.gz: e82c53db8ae5371c3b6a77b5d7ac7b04064c09a4
2
+ SHA256:
3
+ metadata.gz: 3192c4c66dc0345089108a47311eacba0e6b22ee1794896a9006bc2ff0fc7fce
4
+ data.tar.gz: 4306f6cc249d078ab07700ae5e42eac40d8387c6daefb41a1e43a43e169e6f29
5
5
  SHA512:
6
- metadata.gz: da985b40c54ed631ddcc262b376b478660e2a294d66e42d910cfeb1663dc75bfd4ec491c572f2d04646738741607ad788eedf65872d084ef32a46dcb8c1c6b62
7
- data.tar.gz: 40831c9f53c00a65f3f76866f8b4cf967faf3b1a289441094187dacdd948398733fa1665671c34e04e802b4d4e28dcc420a08b1840770827ce4307279e2cdc05
6
+ metadata.gz: 3ba176fda3fda4cf82f89c24a335fdb2e0ccdb7a735a228b2aae452afe7ebbc5b56cb6232d03a306315d9f14174a5f3ad8a383480b7180cee2ff7815e8471dfe
7
+ data.tar.gz: 7e7cf7ad82a36a541de9b65ad3bc30d4bf65577538249e6bf3980494d1829511c026b711571571c7b7ada5d60b70b4101776e3574734bb9fb4497db4e2fdb199
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.4.3
5
+ - 2.5.0
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # SSLTest
1
+ # SSLTest [![Build Status](https://travis-ci.com/jarthod/ssl-test.svg?branch=master)](https://travis-ci.com/jarthod/ssl-test)
2
2
 
3
- A small ruby gem to help you test a website's SSL certificate.
3
+ A small ruby gem (with no dependencies) to help you test a website's SSL certificate.
4
4
 
5
5
  ```ruby
6
6
  gem 'ssl-test'
@@ -47,9 +47,82 @@ cert # => nil
47
47
  ```
48
48
  Default timeout values are 5 seconds each (open and read)
49
49
 
50
+ Revoked certificates are detected using [OCSP](https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol) endpoint by default:
51
+ ```ruby
52
+ valid, error, cert = SSLTest.test "https://revoked.badssl.com"
53
+ valid # => false
54
+ error # => "SSL certificate revoked: The certificate was revoked for an unknown reason (revocation date: 2019-10-07 20:30:39 UTC)"
55
+ cert # => #<OpenSSL::X509::Certificate...>
56
+ ```
57
+
58
+ If the OCSP endpoint is missing, invalid or unreachable the certificate revocation will be tested using [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list).
59
+
60
+ If both OCSP and CRL tests are impossible, the certificate will still be considered valid but with an error message:
61
+ ```ruby
62
+ valid, error, cert = SSLTest.test "https://sitewithnoOCSPorCRL.com"
63
+ valid # => true
64
+ error # => "Revocation test couldn't be performed: OCSP: Missing OCSP URI in authorityInfoAccess extension, CRL: Missing crlDistributionPoints extension"
65
+ cert # => #<OpenSSL::X509::Certificate...>
66
+ ```
67
+
50
68
  ## How it works
51
69
 
52
- SSLTester simply performs a HEAD request using ruby `net/https` library and verifies the SSL status. It also hooks into the validation process to intercept the raw certificate for you.
70
+ SSLTester connects as an HTTPS client (without issuing any requests) and then closes the connection. It does so using ruby `net/https` library and verifies the SSL status. It also hooks into the validation process to intercept the raw certificate for you.
71
+
72
+ After that it queries the [OCSP](https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol) endpoint to verify if the certificate has been revoked. If OCSP is not available it'll fetch the [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list) instead. It does this for every certificates in the chain (except the root which is trusted by your Operating System). It is possible the first one will be validated with OCSP and the intermediate with CRL depending on what they offer.
73
+
74
+ ### Caching
75
+
76
+ OCSP and CRL responses are cached in memory, which makes subsequent testing faster and more robust (avoids network error and throttling) but be careful about memory usage if you try to validate millions of certificates in a row.
77
+
78
+ About the caching duration:
79
+ - OCSP responses are cached until their "next_update" indicated inside the repsonse
80
+ - OCSP errors are cached for 5 minutes
81
+ - CRL responses are cached for 1 hour
82
+
83
+ CRL responses can be big so when they expires they are re-validated with the server using HTTP caching headers when available (`Etag` & `Last-Modified`) to avoid downloading the list again if it didn't change.
84
+
85
+ You can check the size of the cache with `SSLTest.cache_size`, which returns:
86
+
87
+ ```ruby
88
+ {
89
+ crl: {
90
+ lists: 5,
91
+ bytes: 5123456
92
+ },
93
+ ocsp: {
94
+ responses: 350,
95
+ errors: 2,
96
+ bytes: 45876
97
+ }
98
+ }
99
+ ```
100
+
101
+ You can also flush the cache using `SSLTest.flush_cache` if you want (not recommended)
102
+
103
+ ### Logging
104
+
105
+ You can enable logging by setting `SSLTest.logger`, for example:
106
+
107
+ ```ruby
108
+ SSLTest.logger = Rails.logger
109
+ ```
110
+
111
+ SSLTest will log various messages depending on the log level you specify, example:
112
+
113
+ ```
114
+ INFO -- : SSLTest https://www.anonymisation.gov.pf started
115
+ DEBUG -- : SSLTest + test_chain_revocation: www.anonymisation.gov.pf
116
+ DEBUG -- : SSLTest + OCSP: fetch URI http://servicesca.ocsp.certigna.fr
117
+ DEBUG -- : SSLTest + OCSP: 200 OK (4661 bytes)
118
+ DEBUG -- : SSLTest + OCSP: ocsp_ok
119
+ DEBUG -- : SSLTest + test_chain_revocation: Certigna Services CA
120
+ DEBUG -- : SSLTest + OCSP: [false, "Missing OCSP URI in authorityInfoAccess extension", nil]
121
+ DEBUG -- : SSLTest + CRL: fetch URI http://crl.certigna.fr/certigna.crl
122
+ DEBUG -- : SSLTest + CRL: 200 OK (1152 bytes)
123
+ DEBUG -- : SSLTest + CRL: crl_ok
124
+ INFO -- : SSLTest https://www.anonymisation.gov.pf finished: revoked=false
125
+ ```
53
126
 
54
127
  ### What kind of errors will SSLTest detect
55
128
 
@@ -58,19 +131,22 @@ Pretty much the same errors `curl` will:
58
131
  - Incomplete certificate chain (missing intermediary)
59
132
  - Self signed certificates
60
133
  - Valid certs used with incorect hostname
134
+ - Untrusted root (if your system is up-to-date)
135
+ - And more...
61
136
 
62
- ### GOTCHA: errors SSLTest will NOT detect
63
-
64
- There is a spefic kind or error this code will **NOT** detect: *revoked certificates*. This is much more complex to handle because it needs an up to date database of revoked certs to check with. This is implemented in most modern browsers but the results vary greatly (chrome ignores this for example).
137
+ But also **revoked certs** like most browsers (not handled by `curl`)
65
138
 
66
- Here is an example of website with a revoked certificate: https://revoked.grc.com/
139
+ ## Changelog
67
140
 
68
- Any contribution to add this feature is greatly appreciated :)
141
+ * 1.4.0 - 2021-01-16: Implemented CRL as fallback to OCSP + expose cache metrics + add logger support
142
+ * 1.3.1 - 2020-04-25: Improved caching of failed OCSP responses (#5)
143
+ * 1.3.0 - 2020-04-25: Added revoked cert detection using OCSP (#3)
69
144
 
70
145
  ## Contributing
71
146
 
72
147
  1. Fork it ( https://github.com/[my-github-username]/ssl-test/fork )
73
148
  2. Create your feature branch (`git checkout -b my-new-feature`)
74
- 3. Commit your changes (`git commit -am 'Add some feature'`)
75
- 4. Push to the branch (`git push origin my-new-feature`)
76
- 5. Create a new Pull Request
149
+ 3. Make sure the tests are passing (`rspec`)
150
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
151
+ 5. Push to the branch (`git push origin my-new-feature`)
152
+ 6. Create a new Pull Request
data/Rakefile CHANGED
@@ -1,13 +1,11 @@
1
1
  require "bundler/gem_tasks"
2
- require "rake/testtask"
2
+ require "rspec/core/rake_task"
3
3
 
4
- Rake::TestTask.new do |t|
5
- t.pattern = "test/*_test.rb"
6
- end
4
+ RSpec::Core::RakeTask.new(:spec)
7
5
 
8
6
  desc "Open an irb session preloaded with ssl-test"
9
7
  task :console do
10
8
  sh "irb -rubygems -I lib -r ssl_test.rb"
11
9
  end
12
10
 
13
- task default: :test
11
+ task default: :spec
@@ -1,34 +1,133 @@
1
+ require "net/http"
1
2
  require "net/https"
3
+ require "openssl"
4
+ require "uri"
5
+ require "ssl-test/object_size"
6
+ require "ssl-test/ocsp"
7
+ require "ssl-test/crl"
2
8
 
3
9
  module SSLTest
4
- VERSION = "1.0.0"
5
-
6
- def self.test url, open_timeout: 5, read_timeout: 5
7
- uri = URI.parse(url)
8
- return if uri.scheme != 'https'
9
- cert = failed_cert_reason = nil
10
-
11
- http = Net::HTTP.new(uri.host, uri.port)
12
- http.open_timeout = open_timeout
13
- http.read_timeout = read_timeout
14
- http.use_ssl = true
15
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
16
- http.verify_callback = -> (verify_ok, store_context) {
17
- cert = store_context.current_cert
18
- failed_cert_reason = [store_context.error, store_context.error_string] if !verify_ok
19
- verify_ok
20
- }
21
-
22
- req = Net::HTTP::Head.new('/')
23
- begin
24
- res = http.start { http.request(req) }
25
- return [true, nil, cert]
26
- rescue OpenSSL::SSL::SSLError => e
27
- error = e.message
28
- error = "error code %d: %s" % failed_cert_reason if failed_cert_reason
29
- return [false, error, cert]
30
- rescue => e
31
- return [nil, "SSL certificate test failed: #{e.message}"]
10
+ extend OCSP
11
+ extend CRL
12
+
13
+ VERSION = -"1.4.0"
14
+
15
+ class << self
16
+ def test url, open_timeout: 5, read_timeout: 5, redirection_limit: 5
17
+ uri = URI.parse(url)
18
+ return if uri.scheme != 'https'
19
+ cert = failed_cert_reason = chain = nil
20
+
21
+ @logger&.info { "SSLTest #{url} started" }
22
+ http = Net::HTTP.new(uri.host, uri.port)
23
+ http.open_timeout = open_timeout
24
+ http.read_timeout = read_timeout
25
+ http.use_ssl = true
26
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
27
+ http.verify_callback = -> (verify_ok, store_context) {
28
+ cert = store_context.current_cert
29
+ chain = store_context.chain
30
+ failed_cert_reason = [store_context.error, store_context.error_string] if store_context.error != 0
31
+ verify_ok
32
+ }
33
+
34
+ begin
35
+ http.start { }
36
+ revoked, message, revocation_date = test_chain_revocation(chain, open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit)
37
+ @logger&.info { "SSLTest #{url} finished: revoked=#{revoked} #{message}" }
38
+ return [false, "SSL certificate revoked: #{message} (revocation date: #{revocation_date})", cert] if revoked
39
+ return [true, "Revocation test couldn't be performed: #{message}", cert] if message
40
+ return [true, nil, cert]
41
+ rescue OpenSSL::SSL::SSLError => e
42
+ error = e.message
43
+ error = "error code %d: %s" % failed_cert_reason if failed_cert_reason
44
+ if error =~ /certificate verify failed/
45
+ domains = cert_domains(cert)
46
+ if matching_domains(domains, uri.host).none?
47
+ error = "hostname \"#{uri.host}\" does not match the server certificate (#{domains.join(', ')})"
48
+ end
49
+ end
50
+ @logger&.info { "SSLTest #{url} finished: #{error}" }
51
+ return [false, error, cert]
52
+ rescue => e
53
+ @logger&.error { "SSLTest #{url} failed: #{e.message}" }
54
+ return [nil, "SSL certificate test failed: #{e.message}", cert]
55
+ end
56
+ end
57
+
58
+ def cache_size
59
+ {
60
+ crl: {
61
+ lists: @crl_response_cache&.size || 0,
62
+ bytes: ObjectSize.size(@crl_response_cache)
63
+ },
64
+ ocsp: {
65
+ responses: @ocsp_response_cache&.size || 0,
66
+ errors: @ocsp_request_error_cache&.size || 0,
67
+ bytes: ObjectSize.size(@ocsp_response_cache) + ObjectSize.size(@ocsp_request_error_cache)
68
+ }
69
+ }
70
+ end
71
+
72
+ def flush_cache
73
+ @crl_response_cache = {}
74
+ @ocsp_response_cache = {}
75
+ @ocsp_request_error_cache = {}
76
+ end
77
+
78
+ def logger= logger
79
+ @logger = logger
80
+ end
81
+
82
+ private
83
+
84
+ # https://docs.ruby-lang.org/en/2.2.0/OpenSSL/OCSP.html
85
+ # https://stackoverflow.com/questions/16244084/how-to-programmatically-check-if-a-certificate-has-been-revoked#answer-16257470
86
+ # Returns an array with [certificate_revoked?, error_reason, revocation_date]
87
+ def test_chain_revocation chain, **options
88
+ # Test each certificates in the chain except the last one (root cert),
89
+ # which can only be revoked by removing it from the OS.
90
+ chain[0..-2].each_with_index do |cert, i|
91
+ @logger&.debug { "SSLTest + test_chain_revocation: #{cert_field_to_hash(cert.subject)['CN']}" }
92
+
93
+ # Try with OCSP first
94
+ ocsp_result = test_ocsp_revocation(cert, issuer: chain[i + 1], chain: chain, **options)
95
+ @logger&.debug { "SSLTest + OCSP: #{ocsp_result}" }
96
+ next if ocsp_result == :ocsp_ok # passed, go to next cert
97
+ return ocsp_result if ocsp_result[0] == true # revoked
98
+
99
+ # Otherwise it means there was an error so let's try with CRL instead
100
+ crl_result = test_crl_revocation(cert, issuer: chain[i + 1], chain: chain, **options)
101
+ @logger&.debug { "SSLTest + CRL: #{crl_result}" }
102
+ next if crl_result == :crl_ok # passed, go to next cert
103
+ return crl_result if crl_result[0] == true # revoked
104
+
105
+ # If both method failed, return a soft fail with a combination of both error messages
106
+ return [false, "OCSP: #{ocsp_result[1]}, CRL: #{crl_result[1]}", nil]
107
+ end
108
+
109
+ # If all test passed, the certificate is not revoked
110
+ [false, nil, nil]
111
+ end
112
+
113
+ def cert_field_to_hash field
114
+ field.to_a.each.with_object({}) do |v, h|
115
+ v = v.to_a
116
+ h[v[0]] = v[1].encode('UTF-8', undef: :replace, invalid: :replace)
117
+ end
118
+ end
119
+
120
+ def cert_domains cert
121
+ (Array(cert_field_to_hash(cert.subject)['CN']) +
122
+ cert_field_to_hash(cert.extensions)['subjectAltName'].split(/\s*,\s*/))
123
+ .compact
124
+ .map {|s| s.gsub(/^DNS:/, '') }
125
+ .uniq
126
+ end
127
+
128
+ def matching_domains domains, hostname
129
+ domains.map {|s| Regexp.new("\A#{Regexp.escape(s).gsub('\*', '[^.]+')}\z") }
130
+ .select {|domain| domain.match?(hostname) }
32
131
  end
33
132
  end
34
133
  end
@@ -0,0 +1,102 @@
1
+ module SSLTest
2
+ module CRL
3
+ CRL_CACHE_DURATION = 3600 # 1 hour
4
+
5
+ # A note about caching:
6
+ # I choose to only cache the raw HTTP body here (and not the parsed list or better a hash
7
+ # indexed by certificat serial). This is not CPU efficient because it means every time we
8
+ # need to check a cert from a cached CRL we need to parse it again, instantiate the list
9
+ # of Revoked certs and then iterate to find it (there's no API to find one cert without
10
+ # generting the list unfortuantely).
11
+ # I did this because of memory efficiency, because for big 20MB CRL list (so taking 20MB
12
+ # in memory cache), the parsed version takes more than 100M, the list of Revoked certs 120MB,
13
+ # and building a hash with serial, time and reason takes even more.
14
+ # So doing this would be MUCH faster in terms of CPU for subsequent tests on the same CRL
15
+ # but would take a LOT of memory.
16
+ # Also I expect most providers to support OCSP for first level cert (a lot of revokation),
17
+ # which means we should have to use CRL mostly for intermediaries with much smaller CRL.
18
+ # That's what Let's Encrypt is doing with their R3 intermediate for example.
19
+
20
+ private
21
+
22
+ def test_crl_revocation cert, issuer:, chain:, **options
23
+ crl_distribution_points = cert.extensions.find do |extension|
24
+ extension.oid == "crlDistributionPoints"
25
+ end
26
+
27
+ return [false, "Missing crlDistributionPoints extension", nil] unless crl_distribution_points
28
+
29
+ # OpenSSL 2.2+ may simplify this: https://github.com/ruby/openssl/commit/ea702a106d3d8136c48f244593de95666be0edf9
30
+ crl = crl_distribution_points.value.split("\n").find do |description|
31
+ description.match?(/URI:/)
32
+ end
33
+
34
+ return [false, "Missing CRL URI in crlDistributionPoints extension", nil] unless crl
35
+
36
+ crl_uri = URI(crl[/URI:(.*)/, 1])
37
+ http_response, crl_request_error = follow_crl_redirects(crl_uri, **options)
38
+ return [false, "Request failed (URI: #{crl_uri}): #{crl_request_error}", nil] unless http_response
39
+
40
+ response = OpenSSL::X509::CRL.new http_response
41
+ return [false, "Signature verification failed (URI: #{crl_uri})", nil] unless response.verify(issuer.public_key)
42
+
43
+ revoked = response.revoked.find { |r| r.serial == cert.serial }
44
+ if revoked
45
+ reason = revoked.extensions.find {|e| e.oid == "CRLReason"}&.value
46
+ return [true, reason || "Unknown reason", revoked.time]
47
+ else
48
+ end
49
+
50
+ :crl_ok
51
+ end
52
+
53
+ # Returns an array with [response, error_message]
54
+ def follow_crl_redirects(uri, open_timeout: 5, read_timeout: 5, redirection_limit: 5)
55
+ return [nil, "Too many redirections (> #{redirection_limit})"] if redirection_limit == 0
56
+
57
+ # Return file from cache if not expired
58
+ @crl_response_cache ||= {}
59
+ cache_entry = @crl_response_cache[uri]
60
+ return [cache_entry[:body], nil] if cache_entry && cache_entry.fetch(:expires) > Time.now
61
+
62
+ @logger&.debug { "SSLTest + CRL: fetch URI #{uri}" }
63
+ path = uri.path == "" ? "/" : uri.path
64
+ http = Net::HTTP.new(uri.hostname, uri.port)
65
+ http.open_timeout = open_timeout
66
+ http.read_timeout = read_timeout
67
+
68
+ req = Net::HTTP::Get.new(path)
69
+ # Include conditional caching headers from cache to save bandwidth if list didn't change (304)
70
+ if etag = cache_entry&.fetch(:etag)
71
+ req["If-None-Match"] = etag
72
+ elsif last_mod = cache_entry&.fetch(:last_mod)
73
+ req["If-Modified-Since"] = last_mod
74
+ end
75
+ http_response = http.request(req)
76
+ case http_response
77
+ when Net::HTTPNotModified
78
+ # No changes, bump cache expiration time and return cached body
79
+ @logger&.debug { "SSLTest + CRL: 304 Not Modified" }
80
+ @crl_response_cache[uri][:expires] = Time.now + CRL_CACHE_DURATION
81
+ [cache_entry[:body], nil]
82
+ when Net::HTTPSuccess
83
+ # Success, update (or add to) cache and return frech body
84
+ @logger&.debug { "SSLTest + CRL: 200 OK (#{http_response.body.bytesize} bytes)" }
85
+ @logger&.warn { "SSLTest + CRL: Warning: massive file size" } if http_response.body.bytesize > 1024**2 # 1MB
86
+ @logger&.warn { "SSLTest + CRL: Warning: no caching headers on #{uri}" } unless http_response["Etag"] or http_response["Last-Modified"]
87
+ @crl_response_cache[uri] = {
88
+ body: http_response.body,
89
+ expires: Time.now + CRL_CACHE_DURATION,
90
+ etag: http_response["Etag"],
91
+ last_mod: http_response["Last-Modified"]
92
+ }
93
+ [http_response.body, nil]
94
+ when Net::HTTPRedirection
95
+ follow_crl_redirects(URI(http_response["location"]), open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit - 1)
96
+ else
97
+ @logger&.debug { "SSLTest + CRL: Error: #{http_response.class}" }
98
+ [nil, "Wrong response type (#{http_response.class})"]
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,25 @@
1
+ require 'objspace'
2
+
3
+ module ObjectSize
4
+ def self.size(obj)
5
+ case obj
6
+ when String
7
+ obj.bytesize
8
+ when Integer
9
+ obj.size
10
+ when Hash
11
+ sum = 0
12
+ obj.each do |key, val|
13
+ sum += size(key)
14
+ sum += size(val)
15
+ end
16
+ sum
17
+ when Array
18
+ obj.reduce(0) do |sum, val|
19
+ sum + size(val)
20
+ end
21
+ else
22
+ ObjectSpace.memsize_of(obj)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,142 @@
1
+ module SSLTest
2
+ module OCSP
3
+ ERROR_CACHE_DURATION = 5 * 60 # 5 minutes
4
+
5
+ private
6
+
7
+ def test_ocsp_revocation cert, issuer:, chain:, **options
8
+ @ocsp_response_cache ||= {}
9
+ @ocsp_request_error_cache ||= {}
10
+
11
+ unicity_key = "#{cert.issuer}/#{cert.serial}"
12
+
13
+ current_request_error_cache = @ocsp_request_error_cache[unicity_key]
14
+ return current_request_error_cache[:error] if current_request_error_cache && Time.now <= current_request_error_cache[:expires]
15
+
16
+ if @ocsp_response_cache[unicity_key].nil? || @ocsp_response_cache[unicity_key][:next_update] <= Time.now
17
+ authority_info_access = cert.extensions.find do |extension|
18
+ extension.oid == "authorityInfoAccess"
19
+ end
20
+
21
+ return ocsp_soft_fail_return("Missing authorityInfoAccess extension") unless authority_info_access
22
+
23
+ # OpenSSL 2.2+ may simplify this: https://github.com/ruby/openssl/commit/ea702a106d3d8136c48f244593de95666be0edf9
24
+ ocsp = authority_info_access.value.split("\n").find do |description|
25
+ description.start_with?("OCSP")
26
+ end
27
+
28
+ return ocsp_soft_fail_return("Missing OCSP URI in authorityInfoAccess extension") unless ocsp
29
+
30
+ digest = OpenSSL::Digest::SHA1.new
31
+ certificate_id = OpenSSL::OCSP::CertificateId.new(cert, issuer, digest)
32
+
33
+ request = OpenSSL::OCSP::Request.new
34
+ request.add_certid certificate_id
35
+ request.add_nonce
36
+
37
+ ocsp_uri = URI(ocsp[/URI:(.*)/, 1])
38
+ http_response, ocsp_request_error = follow_ocsp_redirects(ocsp_uri, request.to_der, **options)
39
+ return ocsp_soft_fail_return("Request failed (URI: #{ocsp_uri}): #{ocsp_request_error}", unicity_key) unless http_response
40
+
41
+ response = OpenSSL::OCSP::Response.new http_response
42
+ # https://ruby-doc.org/stdlib-2.6.3/libdoc/openssl/rdoc/OpenSSL/OCSP.html#constants-list
43
+ return ocsp_soft_fail_return("Unsuccessful response (URI: #{ocsp_uri}): #{ocsp_response_status_to_string(response.status)}", unicity_key) unless response.status == OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
44
+ basic_response = response.basic
45
+
46
+ # Check the response signature
47
+ store = OpenSSL::X509::Store.new
48
+ store.set_default_paths
49
+ # https://ruby-doc.org/stdlib-2.4.0/libdoc/openssl/rdoc/OpenSSL/OCSP/BasicResponse.html#method-i-verify
50
+ return ocsp_soft_fail_return("Signature verification failed (URI: #{ocsp_uri})", unicity_key) unless basic_response.verify(chain, store)
51
+
52
+ # https://ruby-doc.org/stdlib-2.4.0/libdoc/openssl/rdoc/OpenSSL/OCSP/Request.html#method-i-check_nonce
53
+ return ocsp_soft_fail_return("Nonce check failed (URI: #{ocsp_uri})", unicity_key) unless request.check_nonce(basic_response) != 0
54
+
55
+ # https://ruby-doc.org/stdlib-2.3.0/libdoc/openssl/rdoc/OpenSSL/OCSP/BasicResponse.html#method-i-status
56
+ response_certificate_id, status, reason, revocation_time, _this_update, next_update, _extensions = basic_response.status.first
57
+
58
+ return ocsp_soft_fail_return("Serial check failed (URI: #{ocsp_uri})", unicity_key) unless response_certificate_id.serial == certificate_id.serial
59
+
60
+ @ocsp_response_cache[unicity_key] = { status: status, reason: reason, revocation_time: revocation_time, next_update: next_update }
61
+ end
62
+
63
+ ocsp_response = @ocsp_response_cache[unicity_key]
64
+
65
+ return [true, revocation_reason_to_string(ocsp_response[:reason]), ocsp_response[:revocation_time]] if ocsp_response[:status] == OpenSSL::OCSP::V_CERTSTATUS_REVOKED
66
+ :ocsp_ok
67
+ end
68
+
69
+ # Returns an array with [response, error_message]
70
+ def follow_ocsp_redirects(uri, data, open_timeout: 5, read_timeout: 5, redirection_limit: 5)
71
+ return [nil, "Too many redirections (> #{redirection_limit})"] if redirection_limit == 0
72
+
73
+ @logger&.debug { "SSLTest + OCSP: fetch URI #{uri}" }
74
+ path = uri.path == "" ? "/" : uri.path
75
+ http = Net::HTTP.new(uri.hostname, uri.port)
76
+ http.open_timeout = open_timeout
77
+ http.read_timeout = read_timeout
78
+
79
+ http_response = http.post(path, data, "content-type" => "application/ocsp-request")
80
+ case http_response
81
+ when Net::HTTPSuccess
82
+ @logger&.debug { "SSLTest + OCSP: 200 OK (#{http_response.body.bytesize} bytes)" }
83
+ [http_response.body, nil]
84
+ when Net::HTTPRedirection
85
+ follow_ocsp_redirects(URI(http_response["location"]), data, open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit - 1)
86
+ else
87
+ @logger&.debug { "SSLTest + OCSP: Error: #{http_response.class}" }
88
+ [nil, "Wrong response type (#{http_response.class})"]
89
+ end
90
+ end
91
+
92
+ # https://ruby-doc.org/stdlib-2.6.3/libdoc/openssl/rdoc/OpenSSL/OCSP.html#constants-list
93
+ def ocsp_response_status_to_string(response_status)
94
+ case response_status
95
+ when OpenSSL::OCSP::RESPONSE_STATUS_INTERNALERROR
96
+ "Internal error in issuer"
97
+ when OpenSSL::OCSP::RESPONSE_STATUS_MALFORMEDREQUEST
98
+ "Illegal confirmation request"
99
+ when OpenSSL::OCSP::RESPONSE_STATUS_SIGREQUIRED
100
+ "You must sign the request and resubmit"
101
+ when OpenSSL::OCSP::RESPONSE_STATUS_TRYLATER
102
+ "Try again later"
103
+ when OpenSSL::OCSP::RESPONSE_STATUS_UNAUTHORIZED
104
+ "Your request is unauthorized"
105
+ else
106
+ "Unknown reason"
107
+ end
108
+ end
109
+
110
+ def revocation_reason_to_string(revocation_reason)
111
+ # https://ruby-doc.org/stdlib-2.4.0/libdoc/openssl/rdoc/OpenSSL/OCSP.html#constants-list
112
+ case revocation_reason
113
+ when OpenSSL::OCSP::REVOKED_STATUS_AFFILIATIONCHANGED
114
+ "The certificate subject's name or other information changed"
115
+ when OpenSSL::OCSP::REVOKED_STATUS_CACOMPROMISE
116
+ "This CA certificate was revoked due to a key compromise"
117
+ when OpenSSL::OCSP::REVOKED_STATUS_CERTIFICATEHOLD
118
+ "The certificate is on hold"
119
+ when OpenSSL::OCSP::REVOKED_STATUS_CESSATIONOFOPERATION
120
+ "The certificate is no longer needed"
121
+ when OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE
122
+ "The certificate was revoked due to a key compromise"
123
+ when OpenSSL::OCSP::REVOKED_STATUS_NOSTATUS
124
+ "The certificate was revoked for an unknown reason"
125
+ when OpenSSL::OCSP::REVOKED_STATUS_REMOVEFROMCRL
126
+ "The certificate was previously on hold and should now be removed from the CRL"
127
+ when OpenSSL::OCSP::REVOKED_STATUS_SUPERSEDED
128
+ "The certificate was superseded by a new certificate"
129
+ when OpenSSL::OCSP::REVOKED_STATUS_UNSPECIFIED
130
+ "The certificate was revoked for an unspecified reason"
131
+ else
132
+ "Unknown reason"
133
+ end
134
+ end
135
+
136
+ def ocsp_soft_fail_return(reason, unicity_key = nil)
137
+ error = [false, reason, nil]
138
+ @ocsp_request_error_cache[unicity_key] = { error: error, expires: Time.now + ERROR_CACHE_DURATION } if unicity_key
139
+ error
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,226 @@
1
+ require "ssl-test"
2
+ require "benchmark"
3
+
4
+ # Uncomment for debug logging:
5
+ # require "logger"
6
+ # SSLTest.logger = Logger.new(STDOUT)
7
+
8
+ describe SSLTest do
9
+ describe '.test' do
10
+ it "returns no error on valid SNI website" do
11
+ valid, error, cert = SSLTest.test("https://www.mycs.com")
12
+ expect(error).to be_nil
13
+ expect(valid).to eq(true)
14
+ expect(cert).to be_a OpenSSL::X509::Certificate
15
+ end
16
+
17
+ it "returns no error on valid SAN" do
18
+ valid, error, cert = SSLTest.test("https://1000-sans.badssl.com/")
19
+ expect(error).to be_nil
20
+ expect(valid).to eq(true)
21
+ expect(cert).to be_a OpenSSL::X509::Certificate
22
+ end
23
+
24
+ it "returns no error when no CN" do
25
+ skip "Expired for the moment https://github.com/chromium/badssl.com/issues/447"
26
+ valid, error, cert = SSLTest.test("https://no-common-name.badssl.com/")
27
+ expect(error).to be_nil
28
+ expect(valid).to eq(true)
29
+ expect(cert).to be_a OpenSSL::X509::Certificate
30
+ end
31
+
32
+ it "works with websites blocking http requests" do
33
+ valid, error, cert = SSLTest.test("https://obyava.ua")
34
+ expect(error).to be_nil
35
+ expect(valid).to eq(true)
36
+ expect(cert).to be_a OpenSSL::X509::Certificate
37
+ end
38
+
39
+ it "returns error on self signed certificate" do
40
+ valid, error, cert = SSLTest.test("https://self-signed.badssl.com/")
41
+ expect(error).to eq ("error code 18: self signed certificate")
42
+ expect(valid).to eq(false)
43
+ expect(cert).to be_a OpenSSL::X509::Certificate
44
+ end
45
+
46
+ it "returns error on incomplete chain" do
47
+ valid, error, cert = SSLTest.test("https://incomplete-chain.badssl.com/")
48
+ expect(error).to eq ("error code 20: unable to get local issuer certificate")
49
+ expect(valid).to eq(false)
50
+ expect(cert).to be_a OpenSSL::X509::Certificate
51
+ end
52
+
53
+ it "returns error on untrusted root" do
54
+ valid, error, cert = SSLTest.test("https://untrusted-root.badssl.com/")
55
+ expect(error).to eq ("error code 19: self signed certificate in certificate chain")
56
+ expect(valid).to eq(false)
57
+ expect(cert).to be_a OpenSSL::X509::Certificate
58
+ end
59
+
60
+ it "returns error on invalid host" do
61
+ valid, error, cert = SSLTest.test("https://wrong.host.badssl.com/")
62
+ expect(error).to include('hostname "wrong.host.badssl.com" does not match the server certificate')
63
+ expect(valid).to eq(false)
64
+ expect(cert).to be_a OpenSSL::X509::Certificate
65
+ end
66
+
67
+ it "returns error on expired cert" do
68
+ valid, error, cert = SSLTest.test("https://expired.badssl.com/")
69
+ expect(error).to eq ("error code 10: certificate has expired")
70
+ expect(valid).to eq(false)
71
+ expect(cert).to be_a OpenSSL::X509::Certificate
72
+ end
73
+
74
+ it "returns undetermined state on unhandled error" do
75
+ valid, error, cert = SSLTest.test("https://pijoinlrfgind.com")
76
+ expect(error).to eq ("SSL certificate test failed: Failed to open TCP connection to pijoinlrfgind.com:443 (getaddrinfo: Name or service not known)")
77
+ expect(valid).to be_nil
78
+ expect(cert).to be_nil
79
+ end
80
+
81
+ it "stops on timeouts" do
82
+ valid, error, cert = SSLTest.test("https://updown.io", open_timeout: 0)
83
+ expect(error).to eq ("SSL certificate test failed: Net::OpenTimeout")
84
+ expect(valid).to be_nil
85
+ expect(cert).to be_nil
86
+ end
87
+
88
+ it "reports revocation exceptions" do
89
+ expect(SSLTest).to receive(:follow_ocsp_redirects).and_raise(ArgumentError.new("test"))
90
+ valid, error, cert = SSLTest.test("https://updown.io")
91
+ expect(error).to eq ("SSL certificate test failed: test")
92
+ expect(valid).to be_nil
93
+ expect(cert).to be_a OpenSSL::X509::Certificate
94
+ end
95
+
96
+ it "returns error on revoked cert (OCSP)" do
97
+ expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
98
+ expect(SSLTest).not_to receive(:follow_crl_redirects)
99
+ valid, error, cert = SSLTest.test("https://revoked.badssl.com/")
100
+ expect(error).to eq ("SSL certificate revoked: The certificate was revoked for an unknown reason (revocation date: 2019-10-07 20:30:39 UTC)")
101
+ expect(valid).to eq(false)
102
+ expect(cert).to be_a OpenSSL::X509::Certificate
103
+ end
104
+
105
+ it "returns error on revoked cert (CRL)" do
106
+ expect(SSLTest).to receive(:test_ocsp_revocation).once.and_return([false, "skip OCSP", nil])
107
+ expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
108
+ valid, error, cert = SSLTest.test("https://revoked.badssl.com/")
109
+ expect(error).to eq ("SSL certificate revoked: Unknown reason (revocation date: 2019-10-07 20:30:39 UTC)")
110
+ expect(valid).to eq(false)
111
+ expect(cert).to be_a OpenSSL::X509::Certificate
112
+ end
113
+
114
+ it "stops following redirection after the limit for the revoked certs check" do
115
+ valid, error, cert = SSLTest.test("https://github.com/", redirection_limit: 0)
116
+ expect(error).to eq ("Revocation test couldn't be performed: OCSP: Request failed (URI: http://ocsp.digicert.com): Too many redirections (> 0), CRL: Request failed (URI: http://crl3.digicert.com/sha2-ha-server-g6.crl): Too many redirections (> 0)")
117
+ expect(valid).to eq(true)
118
+ expect(cert).to be_a OpenSSL::X509::Certificate
119
+ end
120
+
121
+ it "warns when the OCSP URI is missing" do
122
+ # Disable CRL fallback to see error message
123
+ expect(SSLTest).to receive(:test_crl_revocation).once.and_return([false, "skip CRL", nil])
124
+ expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
125
+ valid, error, cert = SSLTest.test("https://www.demarches-simplifiees.fr")
126
+ expect(error).to eq ("Revocation test couldn't be performed: OCSP: Missing OCSP URI in authorityInfoAccess extension, CRL: skip CRL")
127
+ expect(valid).to eq(true)
128
+ expect(cert).to be_a OpenSSL::X509::Certificate
129
+ end
130
+
131
+ it "works with CRL only" do
132
+ # Disable OCSP
133
+ expect(SSLTest).to receive(:test_ocsp_revocation).twice.and_return([false, "skip OCSP", nil])
134
+ expect(SSLTest).to receive(:follow_crl_redirects).twice.and_call_original
135
+ valid, error, cert = SSLTest.test("https://www.demarches-simplifiees.fr")
136
+ expect(error).to be_nil
137
+ expect(valid).to eq(true)
138
+ expect(cert).to be_a OpenSSL::X509::Certificate
139
+ end
140
+
141
+ it "warns when the CRL URI is missing" do
142
+ # Disable OCSP to see error message
143
+ expect(SSLTest).to receive(:test_ocsp_revocation).once.and_return([false, "skip OCSP", nil])
144
+ expect(SSLTest).not_to receive(:follow_crl_redirects)
145
+ valid, error, cert = SSLTest.test("https://meta.updown.io")
146
+ expect(error).to eq ("Revocation test couldn't be performed: OCSP: skip OCSP, CRL: Missing crlDistributionPoints extension")
147
+ expect(valid).to eq(true)
148
+ expect(cert).to be_a OpenSSL::X509::Certificate
149
+ end
150
+
151
+ it "works with OCSP for first cert and CRL for intermediate (Let's Encrypt R3 intermediate)" do
152
+ expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
153
+ expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
154
+ valid, error, cert = SSLTest.test("https://meta.updown.io/")
155
+ expect(error).to be_nil
156
+ expect(valid).to eq(true)
157
+ expect(cert).to be_a OpenSSL::X509::Certificate
158
+ end
159
+
160
+ it "works with OCSP for first cert and CRL for intermediate (Certigna Services CA)" do
161
+ expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
162
+ expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
163
+ # Similar chain: https://www.demarches-simplifiees.fr
164
+ valid, error, cert = SSLTest.test("https://www.anonymisation.gov.pf")
165
+ expect(error).to be_nil
166
+ expect(valid).to eq(true)
167
+ expect(cert).to be_a OpenSSL::X509::Certificate
168
+ end
169
+ end
170
+
171
+ describe '.cache_size' do
172
+ before { SSLTest.flush_cache }
173
+
174
+ it "returns 0 by default" do
175
+ expect(SSLTest.cache_size).to eq({
176
+ crl: { bytes: 0, lists: 0 },
177
+ ocsp: { bytes: 0, errors: 0, responses: 0 }
178
+ })
179
+ end
180
+
181
+ it "returns CRL cache size properly" do
182
+ SSLTest.send(:follow_crl_redirects, URI("http://crl.certigna.fr/certigna.crl")) # 1.3k
183
+ SSLTest.send(:follow_crl_redirects, URI("http://crl3.digicert.com/ssca-sha2-g6.crl")) # 19M
184
+ expect(SSLTest.cache_size[:crl][:lists]).to eq(2)
185
+ expect(SSLTest.cache_size[:crl][:bytes]).to be > 19_000_000
186
+ end
187
+
188
+ it "returns OCSP cache size properly" do
189
+ SSLTest.test("https://updown.io")
190
+ expect(SSLTest.cache_size[:ocsp][:responses]).to eq(2)
191
+ expect(SSLTest.cache_size[:ocsp][:errors]).to eq(0)
192
+ expect(SSLTest.cache_size[:ocsp][:bytes]).to be > 200
193
+ end
194
+ end
195
+
196
+ describe '.follow_crl_redirects' do
197
+ before { SSLTest.flush_cache }
198
+ # 19MB: http://crl3.digicert.com/ssca-sha2-g6.crl
199
+ it "fetch CRL list and updates cache" do
200
+ uri = URI("http://crl.certigna.fr/certigna.crl")
201
+ body, error = SSLTest.send(:follow_crl_redirects, uri)
202
+ expect(body.bytesize).to equal 1152
203
+ expect(error).to be_nil
204
+
205
+ # Check cache status
206
+ cache = SSLTest.instance_variable_get('@crl_response_cache')
207
+ expect(cache.size).to equal 1
208
+ expect(cache.keys).to match_array [uri]
209
+ expect(cache[uri].keys).to match_array [:body, :expires, :etag, :last_mod]
210
+ expect(cache[uri][:expires]).to be > (Time.now + 3590)
211
+
212
+ # Make sure we return value from cache
213
+ body2, error2 = nil, nil
214
+ time = Benchmark.realtime { body2, error2 = SSLTest.send(:follow_crl_redirects, uri) }
215
+ expect(time).to be < 0.001 # no request
216
+ expect(body2).to be(body) # using cache
217
+
218
+ # Make sure we return cached value in case of 304
219
+ cache[uri][:expires] = Time.now # cache is now expired
220
+ body2, error2 = nil, nil
221
+ time = Benchmark.realtime { body2, error2 = SSLTest.send(:follow_crl_redirects, uri) }
222
+ expect(time).to be > 0.001 # a request is made
223
+ expect(body2).to be(body) # but we're still using cache because it's a 304
224
+ end
225
+ end
226
+ end
@@ -6,7 +6,7 @@ require 'ssl-test'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "ssl-test"
8
8
  spec.version = SSLTest::VERSION
9
- spec.authors = ["Adrien Jarthon"]
9
+ spec.authors = ["Adrien Rey-Jarthon"]
10
10
  spec.email = ["jobs@adrienjarthon.com"]
11
11
  spec.summary = %q{Test website SSL certificate validity}
12
12
  spec.homepage = "https://github.com/jarthod/ssl-test"
@@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_development_dependency "bundler", "~> 1.7"
21
- spec.add_development_dependency "rake", "~> 10.0"
20
+ spec.add_development_dependency "bundler", ">= 1.7"
21
+ spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "rspec"
22
23
  end
metadata CHANGED
@@ -1,43 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ssl-test
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
- - Adrien Jarthon
7
+ - Adrien Rey-Jarthon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-01 00:00:00.000000000 Z
11
+ date: 2021-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.7'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.7'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  description:
42
56
  email:
43
57
  - jobs@adrienjarthon.com
@@ -46,13 +60,17 @@ extensions: []
46
60
  extra_rdoc_files: []
47
61
  files:
48
62
  - ".gitignore"
63
+ - ".travis.yml"
49
64
  - Gemfile
50
65
  - LICENSE.txt
51
66
  - README.md
52
67
  - Rakefile
53
68
  - lib/ssl-test.rb
69
+ - lib/ssl-test/crl.rb
70
+ - lib/ssl-test/object_size.rb
71
+ - lib/ssl-test/ocsp.rb
72
+ - spec/ssl-test_spec.rb
54
73
  - ssl-test.gemspec
55
- - test/ssl-test_test.rb
56
74
  homepage: https://github.com/jarthod/ssl-test
57
75
  licenses:
58
76
  - MIT
@@ -72,11 +90,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
90
  - !ruby/object:Gem::Version
73
91
  version: '0'
74
92
  requirements: []
75
- rubyforge_project:
76
- rubygems_version: 2.4.5.1
93
+ rubygems_version: 3.1.2
77
94
  signing_key:
78
95
  specification_version: 4
79
96
  summary: Test website SSL certificate validity
80
97
  test_files:
81
- - test/ssl-test_test.rb
82
- has_rdoc:
98
+ - spec/ssl-test_spec.rb
@@ -1,57 +0,0 @@
1
- require "ssl-test"
2
- require "minitest/autorun"
3
-
4
- describe SSLTest do
5
-
6
- describe '.test' do
7
- it "returns no error on valid SNI website" do
8
- valid, error, cert = SSLTest.test("https://www.mycs.com")
9
- valid.must_equal true
10
- error.must_be_nil
11
- cert.must_be_instance_of OpenSSL::X509::Certificate
12
- end
13
-
14
- it "returns error on self signed certificate" do
15
- valid, error, cert = SSLTest.test("https://kernelcoffee.org")
16
- valid.must_equal false
17
- error.must_equal "error code 18: self signed certificate"
18
- cert.must_be_instance_of OpenSSL::X509::Certificate
19
- end
20
-
21
- it "returns error on invalid host" do
22
- valid, error, cert = SSLTest.test("https://staging.updown.io")
23
- valid.must_equal false
24
- error.must_equal 'hostname "staging.updown.io" does not match the server certificate'
25
- cert.must_be_instance_of OpenSSL::X509::Certificate
26
- end
27
-
28
- it "returns error on expired cert" do
29
- valid, error, cert = SSLTest.test("https://testssl-expire.disig.sk")
30
- valid.must_equal false
31
- error.must_equal "error code 10: certificate has expired"
32
- cert.must_be_instance_of OpenSSL::X509::Certificate
33
- end
34
-
35
- it "returns undetermined state on unhandled error" do
36
- valid, error, cert = SSLTest.test("https://pijoinlrfgind.com")
37
- valid.must_be_nil
38
- error.must_equal "SSL certificate test failed: getaddrinfo: Name or service not known"
39
- cert.must_be_nil
40
- end
41
-
42
- it "stops on timeouts" do
43
- valid, error, cert = SSLTest.test("https://approachio.com", open_timeout: 1)
44
- valid.must_be_nil
45
- error.must_equal "SSL certificate test failed: execution expired"
46
- cert.must_be_nil
47
- end
48
-
49
- # Not implemented yet
50
- # it "returns error on revoked cert" do
51
- # valid, error, cert = SSLTest.test("https://revoked.grc.com")
52
- # valid.must_equal false
53
- # error.must_equal "error code XX: certificate has been revoked"
54
- # cert.must_be_instance_of OpenSSL::X509::Certificate
55
- # end
56
- end
57
- end