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.
@@ -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
- describe '.test' do
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
- pending "Expired for the moment"
19
- valid, error, cert = SSLTest.test("https://1000-sans.badssl.com/")
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
- it "returns no error when no CN" do
26
- pending "Expired for the moment https://github.com/chromium/badssl.com/issues/447"
27
- valid, error, cert = SSLTest.test("https://no-common-name.badssl.com/")
28
- expect(error).to be_nil
29
- expect(valid).to eq(true)
30
- expect(cert).to be_a OpenSSL::X509::Certificate
31
- end
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.badssl.com/")
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.badssl.com/")
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.badssl.com/")
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 eq ("SSL certificate test failed: Failed to open TCP connection to pijoinlrfgind.com:443 (getaddrinfo: Name or service not known)")
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 eq ("SSL certificate test failed: Failed to open TCP connection to updown.io:443 (Connection timed out - user specified timeout)")
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(:follow_ocsp_redirects).and_raise(ArgumentError.new("test"))
91
- valid, error, cert = SSLTest.test("https://updown.io")
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
- expect(SSLTest).not_to receive(:follow_crl_redirects)
100
- valid, error, cert = SSLTest.test("https://revoked.badssl.com/")
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
- expect(SSLTest).to receive(:test_ocsp_revocation).once.and_return([false, "skip OCSP", nil])
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: Unknown reason (revocation date: 2021-10-27 21:38:48 UTC)")
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 eq ("Revocation test couldn't be performed: OCSP: Request failed (URI: http://ocsp.digicert.com): Too many redirections (> 0), CRL: Request failed (URI: http://crl3.digicert.com/DigiCertTLSHybridECCSHA3842020CA1-1.crl): Too many redirections (> 0)")
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 fallback to see error message
124
- expect(SSLTest).to receive(:test_crl_revocation).once.and_return([false, "skip CRL", nil])
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://www.demarches-simplifiees.fr")
127
- expect(error).to eq ("Revocation test couldn't be performed: OCSP: Missing OCSP URI in authorityInfoAccess extension, CRL: skip CRL")
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
- # Disable OCSP
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://meta.updown.io")
147
- expect(error).to eq ("Revocation test couldn't be performed: OCSP: skip OCSP, CRL: Missing crlDistributionPoints extension")
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 (Let's Encrypt R3 intermediate)" do
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://meta.updown.io/")
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 > 27_000
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://updown.io")
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 1152
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
- end
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.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: 2022-10-24 00:00:00.000000000 Z
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
- description:
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.3.7
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