ssl-test 1.3.0 → 1.3.1

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