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.
- 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
|