ssl-test 1.3.0 → 1.3.1

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 (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/lib/ssl-test.rb +22 -14
  4. data/test/ssl-test_test.rb +45 -45
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e0ef293b9ca10c582a1965de46c2e60ee83abc9b21bed73f25f1714ba2765e5
4
- data.tar.gz: 539b4d8d4cd53eaf33e3c795b9567ae7409be393888e7be1ee22c03536d83219
3
+ metadata.gz: 96222036799eb67b9c2f8356d8d838b9d35ea04df8622a43b4eee4ac48cb1b1c
4
+ data.tar.gz: 06e7cc81b323295170afd44cd1009f8153ea84c662e6f80e5303d3ce0b267327
5
5
  SHA512:
6
- metadata.gz: 4430b6b22196a66d67a82a34ea3ffbf826c85bd2be832fed688ad29bcfe1a0c04663b4bd00ec4d32d62e4fc502fd802d5ee573d094fde2e469de932971882be3
7
- data.tar.gz: f72393d3d6f44e086f960707d3f7ae17588f728a121a1ed46929a5d0f5418ee7a109bb5975566cfa9f34d8b6adfb06d77e24df5c4881e72028a38a5b8b382129
6
+ metadata.gz: 101d9e53a4ce393be7445f3a7b05fc89836a7d72a6ba16eda8b157f94c63f7652f75b9aa70b883d86ec570c4d3f0e4cf77379d441665c8d5c53073838dc82966
7
+ data.tar.gz: f16565a7be834ac9af6a8cefa0c39a820bab5cd40eecde444ac4bb48854b29510c9dfca83acc808972f6f63e71680c98a34beb356b96fd1c39e990cdc6673fc9
data/README.md CHANGED
@@ -83,6 +83,7 @@ But also **revoked certs** like most browsers (not handled by `curl`)
83
83
 
84
84
  ## Changelog
85
85
 
86
+ * 1.3.1 - 2020-04-25: Improved caching of failed OCSP responses (#5)
86
87
  * 1.3.0 - 2020-04-25: Added revoked cert detection using OCSP (#3)
87
88
 
88
89
  ## Contributing
@@ -4,7 +4,8 @@ require "openssl"
4
4
  require "uri"
5
5
 
6
6
  module SSLTest
7
- VERSION = "1.3.0".freeze
7
+ VERSION = "1.3.1".freeze
8
+ OCSP_REQUEST_ERROR_CACHE_DURATION = 5 * 60
8
9
 
9
10
  def self.test url, open_timeout: 5, read_timeout: 5, redirection_limit: 5
10
11
  uri = URI.parse(url)
@@ -50,11 +51,15 @@ module SSLTest
50
51
  # Returns an array with [ocsp_check_failed, certificate_revoked, error_reason, revocation_date]
51
52
  def self.test_ocsp_revocation chain, open_timeout: 5, read_timeout: 5, redirection_limit: 5
52
53
  @ocsp_response_cache ||= {}
54
+ @ocsp_request_error_cache ||= {}
53
55
  chain[0..-2].each_with_index do |cert, i|
54
56
  # https://tools.ietf.org/html/rfc5280#section-4.1.2.2
55
57
  # The serial number [...] MUST be unique for each certificate issued by a given CA (i.e., the issuer name and serial number identify a unique certificate)
56
58
  unicity_key = "#{cert.issuer}/#{cert.serial}"
57
59
 
60
+ current_request_error_cache = @ocsp_request_error_cache[unicity_key]
61
+ return current_request_error_cache[:error] if current_request_error_cache && Time.now <= current_request_error_cache[:cache_until]
62
+
58
63
  if @ocsp_response_cache[unicity_key].nil? || @ocsp_response_cache[unicity_key][:next_update] <= Time.now
59
64
  issuer = chain[i + 1]
60
65
 
@@ -83,27 +88,27 @@ module SSLTest
83
88
  return ocsp_soft_fail_return("Missing OCSP URI in authorityInfoAccess extension") unless ocsp
84
89
 
85
90
  ocsp_uri = URI(ocsp[/URI:(.*)/, 1])
86
- http_response = follow_ocsp_redirects(ocsp_uri, request.to_der, open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit)
87
- return ocsp_soft_fail_return("OCSP response request failed") unless http_response
91
+ http_response, ocsp_request_error = follow_ocsp_redirects(ocsp_uri, request.to_der, open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit)
92
+ return ocsp_soft_fail_return("Request failed (URI: #{ocsp_uri}): #{ocsp_request_error}", unicity_key) unless http_response
88
93
 
89
94
  response = OpenSSL::OCSP::Response.new http_response.body
90
95
  # https://ruby-doc.org/stdlib-2.6.3/libdoc/openssl/rdoc/OpenSSL/OCSP.html#constants-list
91
- return ocsp_soft_fail_return("OCSP response failed: #{ocsp_response_status_to_string(response.status)}") unless response.status == OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
96
+ 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
92
97
  basic_response = response.basic
93
98
 
94
99
  # Check the response signature
95
100
  store = OpenSSL::X509::Store.new
96
101
  store.set_default_paths
97
102
  # https://ruby-doc.org/stdlib-2.4.0/libdoc/openssl/rdoc/OpenSSL/OCSP/BasicResponse.html#method-i-verify
98
- return ocsp_soft_fail_return("OCSP response signature verification failed") unless basic_response.verify(chain, store)
103
+ return ocsp_soft_fail_return("Signature verification failed (URI: #{ocsp_uri})", unicity_key) unless basic_response.verify(chain, store)
99
104
 
100
105
  # https://ruby-doc.org/stdlib-2.4.0/libdoc/openssl/rdoc/OpenSSL/OCSP/Request.html#method-i-check_nonce
101
- return ocsp_soft_fail_return("OCSP response nonce check failed") unless request.check_nonce(basic_response) != 0
106
+ return ocsp_soft_fail_return("Nonce check failed (URI: #{ocsp_uri})", unicity_key) unless request.check_nonce(basic_response) != 0
102
107
 
103
108
  # https://ruby-doc.org/stdlib-2.3.0/libdoc/openssl/rdoc/OpenSSL/OCSP/BasicResponse.html#method-i-status
104
- response_certificate_id, status, reason, revocation_time, this_update, next_update, _extensions = basic_response.status.first
109
+ response_certificate_id, status, reason, revocation_time, _this_update, next_update, _extensions = basic_response.status.first
105
110
 
106
- return ocsp_soft_fail_return("OCSP response serial check failed") unless response_certificate_id.serial == certificate_id.serial
111
+ return ocsp_soft_fail_return("Serial check failed (URI: #{ocsp_uri})", unicity_key) unless response_certificate_id.serial == certificate_id.serial
107
112
 
108
113
  @ocsp_response_cache[unicity_key] = { status: status, reason: reason, revocation_time: revocation_time, next_update: next_update }
109
114
  end
@@ -140,8 +145,9 @@ module SSLTest
140
145
  .select {|domain| domain.match?(hostname) }
141
146
  end
142
147
 
148
+ # Returns an array with [response, error_message]
143
149
  def follow_ocsp_redirects(uri, data, open_timeout: 5, read_timeout: 5, redirection_limit: 5)
144
- return nil if redirection_limit == 0
150
+ return [nil, "Too many redirections (> #{redirection_limit})"] if redirection_limit == 0
145
151
 
146
152
  path = uri.path == "" ? "/" : uri.path
147
153
  http = Net::HTTP.new(uri.hostname, uri.port)
@@ -151,11 +157,11 @@ module SSLTest
151
157
  http_response = http.post(path, data, "content-type" => "application/ocsp-request")
152
158
  case http_response
153
159
  when Net::HTTPSuccess
154
- http_response
160
+ [http_response, nil]
155
161
  when Net::HTTPRedirection
156
- follow_ocsp_redirects(URI(http_response["location"]), data, open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit -1)
162
+ follow_ocsp_redirects(URI(http_response["location"]), data, open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit - 1)
157
163
  else
158
- nil
164
+ [nil, "Wrong response type (#{http_response.class})"]
159
165
  end
160
166
  end
161
167
 
@@ -177,8 +183,10 @@ module SSLTest
177
183
  end
178
184
  end
179
185
 
180
- def ocsp_soft_fail_return(reason)
181
- [false, false, reason, nil].freeze
186
+ def ocsp_soft_fail_return(reason, unicity_key = nil)
187
+ error = [false, false, reason, nil]
188
+ @ocsp_request_error_cache[unicity_key] = { error: error, cache_until: Time.now + OCSP_REQUEST_ERROR_CACHE_DURATION } if unicity_key
189
+ error
182
190
  end
183
191
 
184
192
  def revocation_reason_to_string(revocation_reason)
@@ -6,107 +6,107 @@ describe SSLTest do
6
6
  describe '.test' do
7
7
  it "returns no error on valid SNI website" do
8
8
  valid, error, cert = SSLTest.test("https://www.mycs.com")
9
- error.must_be_nil
10
- valid.must_equal true
11
- cert.must_be_instance_of OpenSSL::X509::Certificate
9
+ _(error).must_be_nil
10
+ _(valid).must_equal true
11
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
12
12
  end
13
13
 
14
14
  it "returns no error on valid SAN" do
15
15
  valid, error, cert = SSLTest.test("https://1000-sans.badssl.com/")
16
- error.must_be_nil
17
- valid.must_equal true
18
- cert.must_be_instance_of OpenSSL::X509::Certificate
16
+ _(error).must_be_nil
17
+ _(valid).must_equal true
18
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
19
19
  end
20
20
 
21
21
  it "returns no error when no CN" do
22
22
  valid, error, cert = SSLTest.test("https://no-common-name.badssl.com/")
23
- error.must_be_nil
24
- valid.must_equal true
25
- cert.must_be_instance_of OpenSSL::X509::Certificate
23
+ _(error).must_be_nil
24
+ _(valid).must_equal true
25
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
26
26
  end
27
27
 
28
28
  it "works with websites blocking http requests" do
29
29
  valid, error, cert = SSLTest.test("https://obyava.ua")
30
- error.must_be_nil
31
- valid.must_equal true
32
- cert.must_be_instance_of OpenSSL::X509::Certificate
30
+ _(error).must_be_nil
31
+ _(valid).must_equal true
32
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
33
33
  end
34
34
 
35
35
  it "returns error on self signed certificate" do
36
36
  valid, error, cert = SSLTest.test("https://self-signed.badssl.com/")
37
- error.must_equal "error code 18: self signed certificate"
38
- valid.must_equal false
39
- cert.must_be_instance_of OpenSSL::X509::Certificate
37
+ _(error).must_equal "error code 18: self signed certificate"
38
+ _(valid).must_equal false
39
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
40
40
  end
41
41
 
42
42
  it "returns error on incomplete chain" do
43
43
  valid, error, cert = SSLTest.test("https://incomplete-chain.badssl.com/")
44
- error.must_equal "error code 20: unable to get local issuer certificate"
45
- valid.must_equal false
46
- cert.must_be_instance_of OpenSSL::X509::Certificate
44
+ _(error).must_equal "error code 20: unable to get local issuer certificate"
45
+ _(valid).must_equal false
46
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
47
47
  end
48
48
 
49
49
  it "returns error on untrusted root" do
50
50
  valid, error, cert = SSLTest.test("https://untrusted-root.badssl.com/")
51
- error.must_equal "error code 20: unable to get local issuer certificate"
52
- valid.must_equal false
53
- cert.must_be_instance_of OpenSSL::X509::Certificate
51
+ _(error).must_equal "error code 19: self signed certificate in certificate chain"
52
+ _(valid).must_equal false
53
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
54
54
  end
55
55
 
56
56
  it "returns error on invalid host" do
57
57
  valid, error, cert = SSLTest.test("https://wrong.host.badssl.com/")
58
- error.must_equal 'hostname "wrong.host.badssl.com" does not match the server certificate'
59
- valid.must_equal false
60
- cert.must_be_instance_of OpenSSL::X509::Certificate
58
+ assert error.include?('hostname "wrong.host.badssl.com" does not match the server certificate')
59
+ _(valid).must_equal false
60
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
61
61
  end
62
62
 
63
63
  it "returns error on expired cert" do
64
64
  valid, error, cert = SSLTest.test("https://expired.badssl.com/")
65
- error.must_equal "error code 10: certificate has expired"
66
- valid.must_equal false
67
- cert.must_be_instance_of OpenSSL::X509::Certificate
65
+ _(error).must_equal "error code 10: certificate has expired"
66
+ _(valid).must_equal false
67
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
68
68
  end
69
69
 
70
70
  it "returns undetermined state on unhandled error" do
71
71
  valid, error, cert = SSLTest.test("https://pijoinlrfgind.com")
72
- error.must_equal "SSL certificate test failed: Failed to open TCP connection to pijoinlrfgind.com:443 (getaddrinfo: Name or service not known)"
73
- valid.must_be_nil
74
- cert.must_be_nil
72
+ _(error).must_equal "SSL certificate test failed: Failed to open TCP connection to pijoinlrfgind.com:443 (getaddrinfo: Name or service not known)"
73
+ _(valid).must_be_nil
74
+ _(cert).must_be_nil
75
75
  end
76
76
 
77
77
  it "stops on timeouts" do
78
78
  valid, error, cert = SSLTest.test("https://updown.io", open_timeout: 0)
79
- error.must_equal "SSL certificate test failed: Net::OpenTimeout"
80
- valid.must_be_nil
81
- cert.must_be_nil
79
+ _(error).must_equal "SSL certificate test failed: Net::OpenTimeout"
80
+ _(valid).must_be_nil
81
+ _(cert).must_be_nil
82
82
  end
83
83
 
84
84
  it "returns error on revoked cert" do
85
85
  valid, error, cert = SSLTest.test("https://revoked.badssl.com/")
86
- error.must_equal "SSL certificate revoked: The certificate was revoked for an unknown reason (revocation date: 2019-10-07 20:30:39 UTC)"
87
- valid.must_equal false
88
- cert.must_be_instance_of OpenSSL::X509::Certificate
86
+ _(error).must_equal "SSL certificate revoked: The certificate was revoked for an unknown reason (revocation date: 2019-10-07 20:30:39 UTC)"
87
+ _(valid).must_equal false
88
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
89
89
  end
90
90
 
91
91
  it "stops following redirection after the limit for the revoked certs check" do
92
92
  valid, error, cert = SSLTest.test("https://github.com/", redirection_limit: 0)
93
- error.must_equal "OCSP test couldn't be performed: OCSP response request failed"
94
- valid.must_equal true
95
- cert.must_be_instance_of OpenSSL::X509::Certificate
93
+ _(error).must_equal "OCSP test couldn't be performed: Request failed (URI: http://ocsp.digicert.com): Too many redirections (> 0)"
94
+ _(valid).must_equal true
95
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
96
96
  end
97
97
 
98
98
  it "warns when the OCSP URI is missing" do
99
99
  valid, error, cert = SSLTest.test("https://www.demarches-simplifiees.fr")
100
- error.must_equal "OCSP test couldn't be performed: Missing OCSP URI in authorityInfoAccess extension"
101
- valid.must_equal true
102
- cert.must_be_instance_of OpenSSL::X509::Certificate
100
+ _(error).must_equal "OCSP test couldn't be performed: Missing OCSP URI in authorityInfoAccess extension"
101
+ _(valid).must_equal true
102
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
103
103
  end
104
104
 
105
105
  it "warns when the authorityInfoAccess extension is missing" do
106
106
  valid, error, cert = SSLTest.test("https://www.anonymisation.gov.pf")
107
- error.must_equal "OCSP test couldn't be performed: Missing authorityInfoAccess extension"
108
- valid.must_equal true
109
- cert.must_be_instance_of OpenSSL::X509::Certificate
107
+ _(error).must_equal "OCSP test couldn't be performed: Missing authorityInfoAccess extension"
108
+ _(valid).must_equal true
109
+ _(cert).must_be_instance_of OpenSSL::X509::Certificate
110
110
  end
111
111
  end
112
112
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ssl-test
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrien Jarthon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-25 00:00:00.000000000 Z
11
+ date: 2020-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler