ssl-test 1.4.1 → 1.6.0
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/.github/workflows/ruby.yml +3 -5
- data/README.md +46 -17
- data/lib/ssl-test/crl.rb +6 -6
- data/lib/ssl-test/ocsp.rb +3 -3
- data/lib/ssl-test.rb +75 -29
- data/spec/fixtures/digicert_com_ca_bundle.pem +92 -0
- data/spec/fixtures/digicert_com_client.pem +40 -0
- data/spec/fixtures/expired_cert_ca_bundle.pem +100 -0
- data/spec/fixtures/expired_cert_client.pem +31 -0
- data/spec/fixtures/google_com_ca_bundle.pem +108 -0
- data/spec/fixtures/google_com_client.pem +48 -0
- data/spec/fixtures/incomplete_chain_ca_bundle.pem +29 -0
- data/spec/fixtures/incomplete_chain_client.pem +29 -0
- data/spec/fixtures/revoked_badssl_ca_bundle.pem +79 -0
- data/spec/fixtures/revoked_badssl_client.pem +22 -0
- data/spec/fixtures/revoked_rsa_dv_ca_bundle.pem +114 -0
- data/spec/fixtures/revoked_rsa_dv_client.pem +41 -0
- data/spec/fixtures/self_signed_ca_bundle.pem +21 -0
- data/spec/fixtures/self_signed_client.pem +21 -0
- data/spec/fixtures/www_demarches-simplifiees_fr_ca_bundle.pem +132 -0
- data/spec/fixtures/www_demarches-simplifiees_fr_client.pem +56 -0
- data/spec/fixtures/www_github_com_ca_bundle.pem +59 -0
- data/spec/fixtures/www_github_com_client.pem +24 -0
- data/spec/fixtures/www_mycs_com_ca_bundle.pem +79 -0
- data/spec/fixtures/www_mycs_com_client.pem +33 -0
- data/spec/ssl-test_spec.rb +309 -53
- data/ssl-test.gemspec +2 -0
- metadata +71 -7
data/spec/ssl-test_spec.rb
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
require "ssl-test"
|
|
2
2
|
require "benchmark"
|
|
3
|
+
require 'webrick'
|
|
4
|
+
require 'webrick/httpproxy'
|
|
5
|
+
require 'rspec/retry'
|
|
3
6
|
|
|
4
7
|
# Uncomment for debug logging:
|
|
5
8
|
# require "logger"
|
|
6
9
|
# SSLTest.logger = Logger.new(STDOUT)
|
|
7
10
|
|
|
11
|
+
RSpec.configure do |config|
|
|
12
|
+
# The error/revocation examples below hit several public TLS test endpoints
|
|
13
|
+
# (badssl.com, testserver.host, ssl.com) which intermittently reset connections
|
|
14
|
+
# under load. They're spread across a few providers to avoid hammering a single
|
|
15
|
+
# one, and examples tagged `:retry` are re-run a few times (via rspec-retry) so
|
|
16
|
+
# transient network blips don't fail the suite.
|
|
17
|
+
config.verbose_retry = true
|
|
18
|
+
config.display_try_failure_messages = true
|
|
19
|
+
config.default_sleep_interval = 1
|
|
20
|
+
end
|
|
21
|
+
|
|
8
22
|
describe SSLTest do
|
|
9
|
-
|
|
23
|
+
before { SSLTest.flush_cache }
|
|
24
|
+
|
|
25
|
+
let(:proxy_thread) { nil }
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
after(:each) { proxy_thread&.kill }
|
|
29
|
+
|
|
30
|
+
describe '.test_url' do
|
|
10
31
|
it "returns no error on valid SNI website" do
|
|
11
32
|
valid, error, cert = SSLTest.test("https://www.mycs.com")
|
|
12
33
|
expect(error).to be_nil
|
|
@@ -15,20 +36,22 @@ describe SSLTest do
|
|
|
15
36
|
end
|
|
16
37
|
|
|
17
38
|
it "returns no error on valid SAN" do
|
|
18
|
-
|
|
19
|
-
valid, error, cert = SSLTest.test("https://
|
|
39
|
+
# CN is updown.io, www.updown.io is an Alternative Name
|
|
40
|
+
valid, error, cert = SSLTest.test("https://www.updown.io/")
|
|
20
41
|
expect(error).to be_nil
|
|
21
42
|
expect(valid).to eq(true)
|
|
22
43
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
23
44
|
end
|
|
24
45
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
# Disabled: unlikely to be repaired anytime soon: https://github.com/chromium/badssl.com/issues/447
|
|
47
|
+
# Couldn't find a good alternative
|
|
48
|
+
# it "returns no error when no CN" do
|
|
49
|
+
# pending "Expired for the moment https://github.com/chromium/badssl.com/issues/447"
|
|
50
|
+
# valid, error, cert = SSLTest.test("https://no-common-name.badssl.com/")
|
|
51
|
+
# expect(error).to be_nil
|
|
52
|
+
# expect(valid).to eq(true)
|
|
53
|
+
# expect(cert).to be_a OpenSSL::X509::Certificate
|
|
54
|
+
# end
|
|
32
55
|
|
|
33
56
|
it "works with websites blocking http requests" do
|
|
34
57
|
valid, error, cert = SSLTest.test("https://obyava.ua")
|
|
@@ -37,36 +60,36 @@ describe SSLTest do
|
|
|
37
60
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
38
61
|
end
|
|
39
62
|
|
|
40
|
-
it "returns error on self signed certificate" do
|
|
41
|
-
valid, error, cert = SSLTest.test("https://self-signed.
|
|
63
|
+
it "returns error on self signed certificate", :retry => 5 do
|
|
64
|
+
valid, error, cert = SSLTest.test("https://self-signed.testserver.host/")
|
|
42
65
|
expect(error).to eq ("error code 18: self-signed certificate")
|
|
43
66
|
expect(valid).to eq(false)
|
|
44
67
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
45
68
|
end
|
|
46
69
|
|
|
47
|
-
it "returns error on incomplete chain" do
|
|
70
|
+
it "returns error on incomplete chain", :retry => 5 do
|
|
48
71
|
valid, error, cert = SSLTest.test("https://incomplete-chain.badssl.com/")
|
|
49
72
|
expect(error).to eq ("error code 20: unable to get local issuer certificate")
|
|
50
73
|
expect(valid).to eq(false)
|
|
51
74
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
52
75
|
end
|
|
53
76
|
|
|
54
|
-
it "returns error on untrusted root" do
|
|
55
|
-
valid, error, cert = SSLTest.test("https://untrusted-root.
|
|
77
|
+
it "returns error on untrusted root", :retry => 5 do
|
|
78
|
+
valid, error, cert = SSLTest.test("https://untrusted-root.testserver.host/")
|
|
56
79
|
expect(error).to eq ("error code 19: self-signed certificate in certificate chain")
|
|
57
80
|
expect(valid).to eq(false)
|
|
58
81
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
59
82
|
end
|
|
60
83
|
|
|
61
|
-
it "returns error on invalid host" do
|
|
84
|
+
it "returns error on invalid host", :retry => 5 do
|
|
62
85
|
valid, error, cert = SSLTest.test("https://wrong.host.badssl.com/")
|
|
63
86
|
expect(error).to include('error code 62: hostname mismatch')
|
|
64
87
|
expect(valid).to eq(false)
|
|
65
88
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
66
89
|
end
|
|
67
90
|
|
|
68
|
-
it "returns error on expired cert" do
|
|
69
|
-
valid, error, cert = SSLTest.test("https://expired.
|
|
91
|
+
it "returns error on expired cert", :retry => 5 do
|
|
92
|
+
valid, error, cert = SSLTest.test("https://expired-rsa-dv.ssl.com/")
|
|
70
93
|
expect(error).to eq ("error code 10: certificate has expired")
|
|
71
94
|
expect(valid).to eq(false)
|
|
72
95
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
@@ -74,65 +97,71 @@ describe SSLTest do
|
|
|
74
97
|
|
|
75
98
|
it "returns undetermined state on unhandled error" do
|
|
76
99
|
valid, error, cert = SSLTest.test("https://pijoinlrfgind.com")
|
|
77
|
-
expect(error).to
|
|
100
|
+
expect(error).to include("SSL certificate test failed: Failed to open TCP connection to pijoinlrfgind.com:443")
|
|
101
|
+
expect(error).to match(/name.*not known/i)
|
|
78
102
|
expect(valid).to be_nil
|
|
79
103
|
expect(cert).to be_nil
|
|
80
104
|
end
|
|
81
105
|
|
|
82
106
|
it "stops on timeouts" do
|
|
83
107
|
valid, error, cert = SSLTest.test("https://updown.io", open_timeout: 0)
|
|
84
|
-
expect(error).to
|
|
108
|
+
expect(error).to include("SSL certificate test failed")
|
|
109
|
+
expect(error).to match(/timeout/i)
|
|
85
110
|
expect(valid).to be_nil
|
|
86
111
|
expect(cert).to be_nil
|
|
87
112
|
end
|
|
88
113
|
|
|
89
114
|
it "reports revocation exceptions" do
|
|
90
|
-
expect(SSLTest).to receive(:
|
|
91
|
-
valid, error, cert = SSLTest.test("https://
|
|
115
|
+
expect(SSLTest).to receive(:follow_crl_redirects).and_raise(ArgumentError.new("test"))
|
|
116
|
+
valid, error, cert = SSLTest.test("https://digicert.com")
|
|
92
117
|
expect(error).to eq ("SSL certificate test failed: test")
|
|
93
118
|
expect(valid).to be_nil
|
|
94
119
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
95
120
|
end
|
|
96
121
|
|
|
97
122
|
it "returns error on revoked cert (OCSP)" do
|
|
123
|
+
# CRL is tried first; disable it so OCSP performs the revocation check
|
|
124
|
+
expect(SSLTest).to receive(:test_crl_revocation).once.and_return([false, "skip CRL", nil])
|
|
98
125
|
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
expect(error).to eq ("SSL certificate revoked: The certificate was revoked for an unknown reason (revocation date: 2021-10-27 21:38:48 UTC)")
|
|
126
|
+
valid, error, cert = SSLTest.test("https://revoked-rsa-dv.ssl.com/")
|
|
127
|
+
expect(error).to eq ("SSL certificate revoked: The certificate was revoked for an unspecified reason (revocation date: 2026-06-09 14:37:38 UTC)")
|
|
102
128
|
expect(valid).to eq(false)
|
|
103
129
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
104
130
|
end
|
|
105
131
|
|
|
106
|
-
it "returns error on revoked cert (CRL)" do
|
|
107
|
-
|
|
132
|
+
it "returns error on revoked cert (CRL)", :retry => 5 do
|
|
133
|
+
# CRL is tried first and detects the revocation, so OCSP is never used
|
|
108
134
|
expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
|
|
135
|
+
expect(SSLTest).not_to receive(:test_ocsp_revocation)
|
|
109
136
|
valid, error, cert = SSLTest.test("https://revoked.badssl.com/")
|
|
110
|
-
expect(error).to eq ("SSL certificate revoked:
|
|
137
|
+
expect(error).to eq ("SSL certificate revoked: Key Compromise (revocation date: 2026-05-12 21:01:31 UTC)")
|
|
111
138
|
expect(valid).to eq(false)
|
|
112
139
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
113
140
|
end
|
|
114
141
|
|
|
115
142
|
it "stops following redirection after the limit for the revoked certs check" do
|
|
116
143
|
valid, error, cert = SSLTest.test("https://github.com/", redirection_limit: 0)
|
|
117
|
-
expect(error).to
|
|
144
|
+
expect(error).to include("Revocation test couldn't be performed: CRL: Missing crlDistributionPoints extension")
|
|
145
|
+
expect(error).to include("OCSP: Request failed")
|
|
146
|
+
expect(error).to include("Too many redirections (> 0)")
|
|
118
147
|
expect(valid).to eq(true)
|
|
119
148
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
120
149
|
end
|
|
121
150
|
|
|
122
151
|
it "warns when the OCSP URI is missing" do
|
|
123
|
-
# Disable CRL
|
|
124
|
-
expect(SSLTest).to receive(:test_crl_revocation).
|
|
152
|
+
# Disable CRL (tried first) to see the OCSP error message
|
|
153
|
+
expect(SSLTest).to receive(:test_crl_revocation).twice.and_return([false, "skip CRL", nil])
|
|
125
154
|
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
126
|
-
valid, error, cert = SSLTest.test("https://
|
|
127
|
-
expect(error).to eq ("Revocation test couldn't be performed: OCSP: Missing OCSP URI in authorityInfoAccess extension
|
|
155
|
+
valid, error, cert = SSLTest.test("https://google.com")
|
|
156
|
+
expect(error).to eq ("Revocation test couldn't be performed: CRL: skip CRL, OCSP: Missing OCSP URI in authorityInfoAccess extension")
|
|
128
157
|
expect(valid).to eq(true)
|
|
129
158
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
130
159
|
end
|
|
131
160
|
|
|
132
161
|
it "works with CRL only" do
|
|
133
|
-
#
|
|
134
|
-
expect(SSLTest).to receive(:test_ocsp_revocation).twice.and_return([false, "skip OCSP", nil])
|
|
162
|
+
# CRL is tried first and succeeds for both certs, so OCSP is never used
|
|
135
163
|
expect(SSLTest).to receive(:follow_crl_redirects).twice.and_call_original
|
|
164
|
+
expect(SSLTest).not_to receive(:test_ocsp_revocation)
|
|
136
165
|
valid, error, cert = SSLTest.test("https://www.demarches-simplifiees.fr")
|
|
137
166
|
expect(error).to be_nil
|
|
138
167
|
expect(valid).to eq(true)
|
|
@@ -143,29 +172,24 @@ describe SSLTest do
|
|
|
143
172
|
# Disable OCSP to see error message
|
|
144
173
|
expect(SSLTest).to receive(:test_ocsp_revocation).once.and_return([false, "skip OCSP", nil])
|
|
145
174
|
expect(SSLTest).not_to receive(:follow_crl_redirects)
|
|
146
|
-
valid, error, cert = SSLTest.test("https://
|
|
147
|
-
expect(error).to eq ("Revocation test couldn't be performed:
|
|
175
|
+
valid, error, cert = SSLTest.test("https://github.com")
|
|
176
|
+
expect(error).to eq ("Revocation test couldn't be performed: CRL: Missing crlDistributionPoints extension, OCSP: skip OCSP")
|
|
148
177
|
expect(valid).to eq(true)
|
|
149
178
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
150
179
|
end
|
|
151
180
|
|
|
152
|
-
it "works with OCSP for first cert and CRL for intermediate (
|
|
181
|
+
it "works with OCSP for first cert and CRL for intermediate (GitHub)" do
|
|
153
182
|
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
154
183
|
expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
|
|
155
|
-
valid, error, cert = SSLTest.test("https://
|
|
156
|
-
expect(error).to be_nil
|
|
157
|
-
expect(valid).to eq(true)
|
|
158
|
-
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
it "works with OCSP for first cert and CRL for intermediate (Certigna Services CA)" do
|
|
162
|
-
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
163
|
-
expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
|
|
164
|
-
# Similar chain: https://www.demarches-simplifiees.fr
|
|
165
|
-
valid, error, cert = SSLTest.test("https://www.anonymisation.gov.pf")
|
|
184
|
+
valid, error, cert = SSLTest.test("https://github.com")
|
|
166
185
|
expect(error).to be_nil
|
|
167
186
|
expect(valid).to eq(true)
|
|
168
187
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
188
|
+
# make sure both were used
|
|
189
|
+
expect(SSLTest.cache_size).to match({
|
|
190
|
+
crl: hash_including(lists: 1),
|
|
191
|
+
ocsp: hash_including(responses: 1, errors: 0)
|
|
192
|
+
})
|
|
169
193
|
end
|
|
170
194
|
|
|
171
195
|
it "accepts tcps scheme" do
|
|
@@ -174,6 +198,43 @@ describe SSLTest do
|
|
|
174
198
|
expect(valid).to eq(true)
|
|
175
199
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
176
200
|
end
|
|
201
|
+
|
|
202
|
+
context 'when specifying a proxy' do
|
|
203
|
+
context 'when the proxy is active' do
|
|
204
|
+
let(:proxy_thread) do
|
|
205
|
+
thread = Thread.new do
|
|
206
|
+
dev_null = WEBrick::Log::new("/dev/null", 7)
|
|
207
|
+
$proxy = WEBrick::HTTPProxyServer.new Port: 8080, :Logger => dev_null, :AccessLog => []
|
|
208
|
+
$proxy.start
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
sleep 0.1 # wait for the proxy to start!
|
|
212
|
+
allow($proxy).to receive(:do_GET).and_call_original
|
|
213
|
+
|
|
214
|
+
thread
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'uses the provided http proxy' do
|
|
218
|
+
proxy_thread
|
|
219
|
+
|
|
220
|
+
valid, error, cert = SSLTest.test("https://updown.io", proxy_host: '127.0.0.1', proxy_port: 8080)
|
|
221
|
+
expect(error).to be_nil
|
|
222
|
+
expect(valid).to eq(true)
|
|
223
|
+
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
224
|
+
|
|
225
|
+
expect($proxy).to have_received(:do_GET).twice
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
context 'when the proxy is not reachable' do
|
|
230
|
+
it 'returns a http error' do
|
|
231
|
+
valid, error, cert = SSLTest.test("https://updown.io", proxy_host: '127.0.0.1', proxy_port: 55000)
|
|
232
|
+
expect(error).to include('(Connection refused - connect(2) for "127.0.0.1" port 55000)')
|
|
233
|
+
expect(valid).to be_nil
|
|
234
|
+
expect(cert).to be_nil
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
177
238
|
end
|
|
178
239
|
|
|
179
240
|
describe '.cache_size' do
|
|
@@ -190,11 +251,11 @@ describe SSLTest do
|
|
|
190
251
|
SSLTest.send(:follow_crl_redirects, URI("http://crl.certigna.fr/certigna.crl")) # 1.1k
|
|
191
252
|
SSLTest.send(:follow_crl_redirects, URI("http://crl3.digicert.com/DigiCertTLSHybridECCSHA3842020CA1-1.crl")) # 26k
|
|
192
253
|
expect(SSLTest.cache_size[:crl][:lists]).to eq(2)
|
|
193
|
-
expect(SSLTest.cache_size[:crl][:bytes]).to be >
|
|
254
|
+
expect(SSLTest.cache_size[:crl][:bytes]).to be > 2000
|
|
194
255
|
end
|
|
195
256
|
|
|
196
257
|
it "returns OCSP cache size properly" do
|
|
197
|
-
SSLTest.test("https://
|
|
258
|
+
SSLTest.test("https://github.com")
|
|
198
259
|
expect(SSLTest.cache_size[:ocsp][:responses]).to eq(1)
|
|
199
260
|
expect(SSLTest.cache_size[:ocsp][:errors]).to eq(0)
|
|
200
261
|
expect(SSLTest.cache_size[:ocsp][:bytes]).to be > 150
|
|
@@ -209,7 +270,7 @@ describe SSLTest do
|
|
|
209
270
|
it "fetch CRL list and updates cache" do
|
|
210
271
|
uri = URI("http://crl.certigna.fr/certigna.crl")
|
|
211
272
|
body, error = SSLTest.send(:follow_crl_redirects, uri)
|
|
212
|
-
expect(body.bytesize).to equal
|
|
273
|
+
expect(body.bytesize).to equal 1417
|
|
213
274
|
expect(error).to be_nil
|
|
214
275
|
|
|
215
276
|
# Check cache status
|
|
@@ -233,4 +294,199 @@ describe SSLTest do
|
|
|
233
294
|
expect(body2).to be(body) # but we're still using cache because it's a 304
|
|
234
295
|
end
|
|
235
296
|
end
|
|
236
|
-
|
|
297
|
+
|
|
298
|
+
describe '.test_cert' do
|
|
299
|
+
it "returns no error on valid SNI website" do
|
|
300
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/www_mycs_com_client.pem')))
|
|
301
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/www_mycs_com_ca_bundle.pem')))
|
|
302
|
+
|
|
303
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
304
|
+
expect(error).to be_nil
|
|
305
|
+
expect(valid).to eq(true)
|
|
306
|
+
expect(cert).to eq(cert)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it "returns no error on self signed certificates" do
|
|
310
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/self_signed_client.pem')))
|
|
311
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/self_signed_ca_bundle.pem')))
|
|
312
|
+
|
|
313
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
314
|
+
expect(error).to be_nil
|
|
315
|
+
expect(valid).to eq(true)
|
|
316
|
+
expect(cert).to eq(cert)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it "returns error on expired cert" do
|
|
320
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/expired_cert_client.pem')))
|
|
321
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/expired_cert_ca_bundle.pem')))
|
|
322
|
+
|
|
323
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
324
|
+
expect(error).to eq ("error code 10: certificate has expired")
|
|
325
|
+
expect(valid).to eq(false)
|
|
326
|
+
expect(cert).to eq(cert)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
it "returns error on incomplete chain" do
|
|
330
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/incomplete_chain_client.pem')))
|
|
331
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/incomplete_chain_ca_bundle.pem')))
|
|
332
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
333
|
+
expect(error).to eq ("error code 20: unable to get local issuer certificate")
|
|
334
|
+
expect(valid).to eq(false)
|
|
335
|
+
expect(cert).to eq(cert)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it "reports revocation exceptions" do
|
|
339
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/digicert_com_client.pem')))
|
|
340
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/digicert_com_ca_bundle.pem')))
|
|
341
|
+
expect(SSLTest).to receive(:follow_crl_redirects).and_raise(ArgumentError.new("test"))
|
|
342
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
343
|
+
expect(error).to eq("SSL certificate test failed: test")
|
|
344
|
+
expect(valid).to be_nil
|
|
345
|
+
expect(cert).to eq(cert)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it "returns error on revoked cert (OCSP)" do
|
|
349
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/revoked_rsa_dv_client.pem')))
|
|
350
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/revoked_rsa_dv_ca_bundle.pem')))
|
|
351
|
+
|
|
352
|
+
# CRL is tried first; disable it so OCSP performs the revocation check
|
|
353
|
+
expect(SSLTest).to receive(:test_crl_revocation).once.and_return([false, "skip CRL", nil])
|
|
354
|
+
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
355
|
+
|
|
356
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
357
|
+
expect(error).to eq ("SSL certificate revoked: The certificate was revoked for an unknown reason (revocation date: 2025-06-09 15:07:39 UTC)")
|
|
358
|
+
expect(valid).to eq(false)
|
|
359
|
+
expect(cert).to eq(cert)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it "returns error on revoked cert (CRL)" do
|
|
363
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/revoked_badssl_client.pem')))
|
|
364
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/revoked_badssl_ca_bundle.pem')))
|
|
365
|
+
|
|
366
|
+
# CRL is tried first and detects the revocation, so OCSP is never used
|
|
367
|
+
expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
|
|
368
|
+
expect(SSLTest).not_to receive(:test_ocsp_revocation)
|
|
369
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
370
|
+
expect(error).to eq ("SSL certificate revoked: Key Compromise (revocation date: 2026-05-12 21:01:31 UTC)")
|
|
371
|
+
expect(valid).to eq(false)
|
|
372
|
+
expect(cert).to eq(cert)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
it "stops following redirection after the limit for the revoked certs check" do
|
|
376
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/www_github_com_client.pem')))
|
|
377
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/www_github_com_ca_bundle.pem')))
|
|
378
|
+
|
|
379
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle, redirection_limit: 0)
|
|
380
|
+
expect(error).to include("Revocation test couldn't be performed: CRL: Missing crlDistributionPoints extension")
|
|
381
|
+
expect(error).to include("OCSP: Request failed")
|
|
382
|
+
expect(error).to include("Too many redirections (> 0)")
|
|
383
|
+
expect(valid).to eq(true)
|
|
384
|
+
expect(cert).to eq(cert)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it "warns when the OCSP URI is missing" do
|
|
388
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/google_com_client.pem')))
|
|
389
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/google_com_ca_bundle.pem')))
|
|
390
|
+
|
|
391
|
+
# Disable CRL (tried first) to see the OCSP error message
|
|
392
|
+
expect(SSLTest).to receive(:test_crl_revocation).twice.and_return([false, "skip CRL", nil])
|
|
393
|
+
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
394
|
+
|
|
395
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
396
|
+
expect(error).to eq ("Revocation test couldn't be performed: CRL: skip CRL, OCSP: Missing OCSP URI in authorityInfoAccess extension")
|
|
397
|
+
expect(valid).to eq(true)
|
|
398
|
+
expect(cert).to eq(cert)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
it "works with CRL only" do
|
|
402
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/www_demarches-simplifiees_fr_client.pem')))
|
|
403
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/www_demarches-simplifiees_fr_ca_bundle.pem')))
|
|
404
|
+
|
|
405
|
+
# CRL is tried first and succeeds for both certs, so OCSP is never used
|
|
406
|
+
expect(SSLTest).to receive(:follow_crl_redirects).twice.and_call_original
|
|
407
|
+
expect(SSLTest).not_to receive(:test_ocsp_revocation)
|
|
408
|
+
|
|
409
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
410
|
+
expect(error).to be_nil
|
|
411
|
+
expect(valid).to eq(true)
|
|
412
|
+
expect(cert).to eq(cert)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
it "warns when the CRL URI is missing" do
|
|
416
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/www_github_com_client.pem')))
|
|
417
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/www_github_com_ca_bundle.pem')))
|
|
418
|
+
|
|
419
|
+
# Disable OCSP to see error message
|
|
420
|
+
expect(SSLTest).to receive(:test_ocsp_revocation).once.and_return([false, "skip OCSP", nil])
|
|
421
|
+
expect(SSLTest).not_to receive(:follow_crl_redirects)
|
|
422
|
+
|
|
423
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
424
|
+
expect(error).to eq ("Revocation test couldn't be performed: CRL: Missing crlDistributionPoints extension, OCSP: skip OCSP")
|
|
425
|
+
expect(valid).to eq(true)
|
|
426
|
+
expect(cert).to eq(cert)
|
|
427
|
+
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it "works with OCSP for first cert and CRL for intermediate (GitHub)" do
|
|
431
|
+
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
432
|
+
expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
|
|
433
|
+
|
|
434
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/www_github_com_client.pem')))
|
|
435
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/www_github_com_ca_bundle.pem')))
|
|
436
|
+
|
|
437
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
438
|
+
expect(error).to be_nil
|
|
439
|
+
expect(valid).to eq(true)
|
|
440
|
+
expect(cert).to eq(cert)
|
|
441
|
+
# make sure both were used
|
|
442
|
+
expect(SSLTest.cache_size).to match({
|
|
443
|
+
crl: hash_including(lists: 1),
|
|
444
|
+
ocsp: hash_including(responses: 1, errors: 0)
|
|
445
|
+
})
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
context 'when specifying a proxy' do
|
|
449
|
+
context 'when the proxy is active' do
|
|
450
|
+
let(:proxy_thread) do
|
|
451
|
+
thread = Thread.new do
|
|
452
|
+
dev_null = WEBrick::Log::new("/dev/null", 7)
|
|
453
|
+
$proxy = WEBrick::HTTPProxyServer.new Port: 8080, :Logger => dev_null, :AccessLog => []
|
|
454
|
+
$proxy.start
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
sleep 0.1 # wait for the proxy to start!
|
|
458
|
+
allow($proxy).to receive(:do_GET).and_call_original
|
|
459
|
+
|
|
460
|
+
thread
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
it 'uses the provided http proxy' do
|
|
464
|
+
proxy_thread
|
|
465
|
+
|
|
466
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/google_com_client.pem')))
|
|
467
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/google_com_ca_bundle.pem')))
|
|
468
|
+
|
|
469
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle, proxy_host: '127.0.0.1', proxy_port: 8080)
|
|
470
|
+
expect(error).to be_nil
|
|
471
|
+
expect(valid).to eq(true)
|
|
472
|
+
expect(cert).to eq(cert)
|
|
473
|
+
|
|
474
|
+
# CRL is tried first, so both certs are checked via CRL (GET) through the proxy
|
|
475
|
+
expect($proxy).to have_received(:do_GET).twice
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
context 'when the proxy is not reachable' do
|
|
480
|
+
it 'returns a http error' do
|
|
481
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/google_com_client.pem')))
|
|
482
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/google_com_ca_bundle.pem')))
|
|
483
|
+
|
|
484
|
+
valid, error, cert = SSLTest.test_cert(cert, ca_bundle, proxy_host: '127.0.0.1', proxy_port: 55000)
|
|
485
|
+
expect(error).to include('(Connection refused - connect(2) for "127.0.0.1" port 55000)')
|
|
486
|
+
expect(valid).to be_nil
|
|
487
|
+
expect(cert).to eq(cert)
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
data/ssl-test.gemspec
CHANGED
|
@@ -20,4 +20,6 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.add_development_dependency "bundler", ">= 1.7"
|
|
21
21
|
spec.add_development_dependency "rake"
|
|
22
22
|
spec.add_development_dependency "rspec"
|
|
23
|
+
spec.add_development_dependency "rspec-retry"
|
|
24
|
+
spec.add_development_dependency "webrick"
|
|
23
25
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ssl-test
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adrien Rey-Jarthon
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 2026-06-16 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: bundler
|
|
@@ -52,7 +51,34 @@ dependencies:
|
|
|
52
51
|
- - ">="
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: '0'
|
|
55
|
-
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec-retry
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: webrick
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
56
82
|
email:
|
|
57
83
|
- jobs@adrienjarthon.com
|
|
58
84
|
executables: []
|
|
@@ -70,13 +96,32 @@ files:
|
|
|
70
96
|
- lib/ssl-test/crl.rb
|
|
71
97
|
- lib/ssl-test/object_size.rb
|
|
72
98
|
- lib/ssl-test/ocsp.rb
|
|
99
|
+
- spec/fixtures/digicert_com_ca_bundle.pem
|
|
100
|
+
- spec/fixtures/digicert_com_client.pem
|
|
101
|
+
- spec/fixtures/expired_cert_ca_bundle.pem
|
|
102
|
+
- spec/fixtures/expired_cert_client.pem
|
|
103
|
+
- spec/fixtures/google_com_ca_bundle.pem
|
|
104
|
+
- spec/fixtures/google_com_client.pem
|
|
105
|
+
- spec/fixtures/incomplete_chain_ca_bundle.pem
|
|
106
|
+
- spec/fixtures/incomplete_chain_client.pem
|
|
107
|
+
- spec/fixtures/revoked_badssl_ca_bundle.pem
|
|
108
|
+
- spec/fixtures/revoked_badssl_client.pem
|
|
109
|
+
- spec/fixtures/revoked_rsa_dv_ca_bundle.pem
|
|
110
|
+
- spec/fixtures/revoked_rsa_dv_client.pem
|
|
111
|
+
- spec/fixtures/self_signed_ca_bundle.pem
|
|
112
|
+
- spec/fixtures/self_signed_client.pem
|
|
113
|
+
- spec/fixtures/www_demarches-simplifiees_fr_ca_bundle.pem
|
|
114
|
+
- spec/fixtures/www_demarches-simplifiees_fr_client.pem
|
|
115
|
+
- spec/fixtures/www_github_com_ca_bundle.pem
|
|
116
|
+
- spec/fixtures/www_github_com_client.pem
|
|
117
|
+
- spec/fixtures/www_mycs_com_ca_bundle.pem
|
|
118
|
+
- spec/fixtures/www_mycs_com_client.pem
|
|
73
119
|
- spec/ssl-test_spec.rb
|
|
74
120
|
- ssl-test.gemspec
|
|
75
121
|
homepage: https://github.com/jarthod/ssl-test
|
|
76
122
|
licenses:
|
|
77
123
|
- MIT
|
|
78
124
|
metadata: {}
|
|
79
|
-
post_install_message:
|
|
80
125
|
rdoc_options: []
|
|
81
126
|
require_paths:
|
|
82
127
|
- lib
|
|
@@ -91,9 +136,28 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
91
136
|
- !ruby/object:Gem::Version
|
|
92
137
|
version: '0'
|
|
93
138
|
requirements: []
|
|
94
|
-
rubygems_version: 3.
|
|
95
|
-
signing_key:
|
|
139
|
+
rubygems_version: 3.6.2
|
|
96
140
|
specification_version: 4
|
|
97
141
|
summary: Test website SSL certificate validity
|
|
98
142
|
test_files:
|
|
143
|
+
- spec/fixtures/digicert_com_ca_bundle.pem
|
|
144
|
+
- spec/fixtures/digicert_com_client.pem
|
|
145
|
+
- spec/fixtures/expired_cert_ca_bundle.pem
|
|
146
|
+
- spec/fixtures/expired_cert_client.pem
|
|
147
|
+
- spec/fixtures/google_com_ca_bundle.pem
|
|
148
|
+
- spec/fixtures/google_com_client.pem
|
|
149
|
+
- spec/fixtures/incomplete_chain_ca_bundle.pem
|
|
150
|
+
- spec/fixtures/incomplete_chain_client.pem
|
|
151
|
+
- spec/fixtures/revoked_badssl_ca_bundle.pem
|
|
152
|
+
- spec/fixtures/revoked_badssl_client.pem
|
|
153
|
+
- spec/fixtures/revoked_rsa_dv_ca_bundle.pem
|
|
154
|
+
- spec/fixtures/revoked_rsa_dv_client.pem
|
|
155
|
+
- spec/fixtures/self_signed_ca_bundle.pem
|
|
156
|
+
- spec/fixtures/self_signed_client.pem
|
|
157
|
+
- spec/fixtures/www_demarches-simplifiees_fr_ca_bundle.pem
|
|
158
|
+
- spec/fixtures/www_demarches-simplifiees_fr_client.pem
|
|
159
|
+
- spec/fixtures/www_github_com_ca_bundle.pem
|
|
160
|
+
- spec/fixtures/www_github_com_client.pem
|
|
161
|
+
- spec/fixtures/www_mycs_com_ca_bundle.pem
|
|
162
|
+
- spec/fixtures/www_mycs_com_client.pem
|
|
99
163
|
- spec/ssl-test_spec.rb
|