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.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/lib/ssl-test.rb +22 -14
- data/test/ssl-test_test.rb +45 -45
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96222036799eb67b9c2f8356d8d838b9d35ea04df8622a43b4eee4ac48cb1b1c
|
4
|
+
data.tar.gz: 06e7cc81b323295170afd44cd1009f8153ea84c662e6f80e5303d3ce0b267327
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 101d9e53a4ce393be7445f3a7b05fc89836a7d72a6ba16eda8b157f94c63f7652f75b9aa70b883d86ec570c4d3f0e4cf77379d441665c8d5c53073838dc82966
|
7
|
+
data.tar.gz: f16565a7be834ac9af6a8cefa0c39a820bab5cd40eecde444ac4bb48854b29510c9dfca83acc808972f6f63e71680c98a34beb356b96fd1c39e990cdc6673fc9
|
data/README.md
CHANGED
data/lib/ssl-test.rb
CHANGED
@@ -4,7 +4,8 @@ require "openssl"
|
|
4
4
|
require "uri"
|
5
5
|
|
6
6
|
module SSLTest
|
7
|
-
VERSION = "1.3.
|
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("
|
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("
|
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("
|
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("
|
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,
|
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("
|
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]
|
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)
|
data/test/ssl-test_test.rb
CHANGED
@@ -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
|
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.
|
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:
|
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.
|
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-
|
11
|
+
date: 2020-04-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|