ssl-test 2.0.1 → 2.1.2
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 +9 -8
- data/by_serial_bench.rb +65 -0
- data/crl_bench.rb +122 -0
- data/lib/ssl-test/crl.rb +17 -22
- data/lib/ssl-test.rb +5 -4
- data/spec/memory_store_spec.rb +4 -2
- data/spec/ssl-test_spec.rb +61 -58
- data/ssl-test.gemspec +2 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36325440b8b451a3a2820ab4c597ed765a9d7787ff613ea36aeea671892a9882
|
|
4
|
+
data.tar.gz: 794733e1186c7a03f9b04823a165637cf7c559554e84b478d7b3fb57fe2e9e0e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 682c775642e4dbae339d37afe3c059f2e34511c942d199eb09b3090a73f65840e09c0df904d2790893752cc7ad4db2f2c7cbdf68f17765cbee4015967d665282
|
|
7
|
+
data.tar.gz: 9bf3c4f4d6113d2884fd33041310a22cb5b6610aebb5e44b62a3cffa3396e56e6e7b6091dab907dcbf0fdec6d2ccdb00eac5f60a5018045ace13d334f0b1d581
|
data/README.md
CHANGED
|
@@ -59,7 +59,7 @@ error # => nil
|
|
|
59
59
|
cert # => #<OpenSSL::X509::Certificate...>
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Revoked certificates are detected using [
|
|
62
|
+
Revoked certificates are detected using [OCSP](https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol) by default:
|
|
63
63
|
|
|
64
64
|
```ruby
|
|
65
65
|
valid, error, cert = SSLTest.test_url "https://revoked.badssl.com"
|
|
@@ -68,13 +68,13 @@ error # => "SSL certificate revoked: Key Compromise (revocation date: 2019-10-07
|
|
|
68
68
|
cert # => #<OpenSSL::X509::Certificate...>
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
If the
|
|
71
|
+
If the OCSP endpoint is missing, invalid or unreachable the certificate revocation will be tested using the [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list).
|
|
72
72
|
|
|
73
|
-
You can swap the order if you'd rather check
|
|
73
|
+
You can swap the order if you'd rather check CRL first and only fall back to OCSP on error (the default is OCSP first, since 2.1, because CRLs can be large and checking them is significantly more memory- and CPU-intensive):
|
|
74
74
|
|
|
75
75
|
```ruby
|
|
76
|
-
SSLTest.revocation_order = %i[ocsp
|
|
77
|
-
SSLTest.revocation_order = %i[crl
|
|
76
|
+
SSLTest.revocation_order = %i[crl ocsp] # CRL first, OCSP fallback
|
|
77
|
+
SSLTest.revocation_order = %i[ocsp crl] # the default: OCSP first, CRL fallback
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
If both CRL and OCSP tests are impossible, the certificate will still be considered valid but with an error message:
|
|
@@ -103,7 +103,7 @@ This check will pass for self-signed certificates if the certificate is signed b
|
|
|
103
103
|
|
|
104
104
|
SSLTester connects as an HTTPS client (without issuing any requests) and then closes the connection. It does so using ruby `net/https` library and verifies the SSL status. It also hooks into the validation process to intercept the raw certificate for you.
|
|
105
105
|
|
|
106
|
-
After that it
|
|
106
|
+
After that it queries the [OCSP](https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol) endpoint to verify if the certificate has been revoked. If the OCSP endpoint is not available it'll fetch the [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list) instead. It does this for every certificates in the chain (except the root which is trusted by your Operating System). It is possible the first one will be validated with OCSP and the intermediate with CRL depending on what they offer.
|
|
107
107
|
|
|
108
108
|
### Caching
|
|
109
109
|
|
|
@@ -200,8 +200,9 @@ But also **revoked certs** like most browsers (not handled by `curl`)
|
|
|
200
200
|
|
|
201
201
|
## Changelog
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
* 2.1.2 - 2026-06-23: Add support for faster `CRL#by_serial` method I just added to OpenSSL (https://github.com/ruby/openssl/pull/1065)
|
|
204
|
+
* 2.1.1 - 2026-06-21: Refactor CRL code to use newer OpenSSL `crl_uris` helper
|
|
205
|
+
* 2.1.0 - 2026-06-20: Check revocation with OCSP first and fall back to CRL (was CRL first since 1.6) because CRLs can be large and checking them is significantly more memory- and CPU-intensive. Set `SSLTest.revocation_order = %i[crl ocsp]` to restore the previous order.
|
|
205
206
|
* 2.0.1 - 2026-06-19: Speed up and shrink the memory use of CRL checks with a fast path that scans the raw CRL for the certificate's serial before parsing, avoiding instantiating the entire revocation list (>1M Ruby objects for busy CAs) when the cert isn't revoked. Send both `If-None-Match` and `If-Modified-Since` on CRL revalidation so CDN-backed CAs that don't honor their own ETag (e.g. DigiCert) still return a `304` instead of re-downloading the whole list.
|
|
206
207
|
* 2.0.0 - 2026-06-16: Make the revocation check order configurable via `SSLTest.revocation_order` (`%i[crl ocsp]` by default, set `%i[ocsp crl]` to check OCSP first). Make the cache backend configurable. The default stays an in-process `SSLTest::MemoryStore`, but you can now assign any object responding to the `Rails.cache`-style API (`read`/`write`/`delete`) with `SSLTest.cache = Rails.cache` to share responses across processes and get compression (e.g. memcache via Dalli — see the memcached note in the Caching section about raising the max value size for large CRLs). **Breaking:** the module-level `SSLTest.cache_size` and `SSLTest.flush_cache` were removed — use `SSLTest.cache.size` and `SSLTest.cache.clear` instead (these only work with the built-in `MemoryStore`; shared backends like `Rails.cache` can't be enumerated and shouldn't be wholesale-cleared)
|
|
207
208
|
* 1.6.0 - 2026-06-16: Check revocation with CRL first and fall back to OCSP (was OCSP first) to reduce revocation detection delay
|
data/by_serial_bench.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# Needs the patched openssl that defines #by_serial; point OPENSSL_LIB at the dev
|
|
4
|
+
# build (defaults to ../openssl/lib). gem install benchmark-ips benchmark-memory
|
|
5
|
+
#
|
|
6
|
+
# ruby by_serial_bench.rb [CRL_URL]
|
|
7
|
+
|
|
8
|
+
dev = ENV["OPENSSL_LIB"] || File.expand_path("../openssl/lib", __dir__)
|
|
9
|
+
$LOAD_PATH.unshift(dev) if File.exist?(File.join(dev, "openssl.so"))
|
|
10
|
+
|
|
11
|
+
require "openssl"
|
|
12
|
+
require "net/http"
|
|
13
|
+
require "uri"
|
|
14
|
+
require "tmpdir"
|
|
15
|
+
require "benchmark/ips"
|
|
16
|
+
require "benchmark/memory"
|
|
17
|
+
|
|
18
|
+
abort "loaded openssl has no #by_serial (set OPENSSL_LIB to the patched build)" unless
|
|
19
|
+
OpenSSL::X509::CRL.method_defined?(:by_serial)
|
|
20
|
+
|
|
21
|
+
url = ARGV[0] || "http://c.cf-i.ssl.com/ae801ed1c55bb579d79208b0d772acfb8cc3a208.crl" # big CRL example
|
|
22
|
+
cache = File.join(Dir.tmpdir, "by_serial_bench_#{File.basename(URI(url).path)}")
|
|
23
|
+
body = File.exist?(cache) ? File.binread(cache) :
|
|
24
|
+
(warn("downloading #{url} ..."); d = Net::HTTP.get(URI(url)); File.binwrite(cache, d); d)
|
|
25
|
+
|
|
26
|
+
crl = OpenSSL::X509::CRL.new(body)
|
|
27
|
+
entries = crl.revoked.size
|
|
28
|
+
absent = 0xDEAD_BEEF_CAFE_F00D
|
|
29
|
+
present = crl.revoked[entries / 2].serial # middle of the list for median performance
|
|
30
|
+
|
|
31
|
+
puts "#{OpenSSL::OPENSSL_LIBRARY_VERSION} — #{url}"
|
|
32
|
+
puts "#{(body.bytesize / 1e6).round(1)} MB DER, #{entries} revoked entries"
|
|
33
|
+
|
|
34
|
+
[["not revoked", absent], ["revoked", present]].each do |label, serial|
|
|
35
|
+
abort "mismatch for #{label}" unless
|
|
36
|
+
crl.by_serial(serial) == crl.revoked.find { |r| r.serial == serial }
|
|
37
|
+
puts "\n=== #{label} serial ==="
|
|
38
|
+
|
|
39
|
+
puts "\n-- warm: lookup on a parsed CRL --"
|
|
40
|
+
@list = nil
|
|
41
|
+
Benchmark.ips do |x|
|
|
42
|
+
x.config(warmup: 1, time: 3)
|
|
43
|
+
x.report("revoked.find") { (@list ||= crl.revoked).find { |r| r.serial == serial } }
|
|
44
|
+
x.report("by_serial") { crl.by_serial(serial) }
|
|
45
|
+
x.compare!
|
|
46
|
+
end
|
|
47
|
+
Benchmark.memory do |x|
|
|
48
|
+
x.report("revoked.find") { crl.revoked.find { |r| r.serial == serial } }
|
|
49
|
+
x.report("by_serial") { crl.by_serial(serial) }
|
|
50
|
+
x.compare!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
puts "\n-- cold: parse a fresh CRL + one lookup --"
|
|
54
|
+
Benchmark.ips do |x|
|
|
55
|
+
x.config(warmup: 0, time: 3)
|
|
56
|
+
x.report("revoked.find") { OpenSSL::X509::CRL.new(body).revoked.find { |r| r.serial == serial } }
|
|
57
|
+
x.report("by_serial") { OpenSSL::X509::CRL.new(body).by_serial(serial) }
|
|
58
|
+
x.compare!
|
|
59
|
+
end
|
|
60
|
+
Benchmark.memory do |x|
|
|
61
|
+
x.report("revoked.find") { OpenSSL::X509::CRL.new(body).revoked.find { |r| r.serial == serial } }
|
|
62
|
+
x.report("by_serial") { OpenSSL::X509::CRL.new(body).by_serial(serial) }
|
|
63
|
+
x.compare!
|
|
64
|
+
end
|
|
65
|
+
end
|
data/crl_bench.rb
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
#
|
|
4
|
+
# Benchmark: checking whether a single serial is revoked in a CRL.
|
|
5
|
+
# A) OpenSSL::X509::CRL#revoked.find { ... } (only API available today)
|
|
6
|
+
# B) X509_CRL_get0_by_serial() (proposed, via Fiddle)
|
|
7
|
+
#
|
|
8
|
+
# Each approach runs in its own forked process so the reported peak RSS reflects
|
|
9
|
+
# only that approach. Requires MRI on Linux (uses fork + /proc; falls back to
|
|
10
|
+
# Process.getrusage on macOS/BSD).
|
|
11
|
+
#
|
|
12
|
+
# ruby crl_bench.rb [CRL_URL]
|
|
13
|
+
|
|
14
|
+
require "openssl"
|
|
15
|
+
require "fiddle"
|
|
16
|
+
require "benchmark"
|
|
17
|
+
require "net/http"
|
|
18
|
+
require "uri"
|
|
19
|
+
require "tmpdir"
|
|
20
|
+
|
|
21
|
+
CRL_URL = ARGV[0] || "http://c.cf-i.ssl.com/ae801ed1c55bb579d79208b0d772acfb8cc3a208.crl"
|
|
22
|
+
|
|
23
|
+
# --- fetch the CRL (cached in /tmp so reruns are offline) ----------------------
|
|
24
|
+
cache = File.join(Dir.tmpdir, "crl_bench_#{File.basename(URI(CRL_URL).path)}")
|
|
25
|
+
body = if File.exist?(cache)
|
|
26
|
+
File.binread(cache)
|
|
27
|
+
else
|
|
28
|
+
warn "downloading #{CRL_URL} ..."
|
|
29
|
+
data = Net::HTTP.get_response(URI(CRL_URL)).body
|
|
30
|
+
File.binwrite(cache, data)
|
|
31
|
+
data
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# --- libcrypto bindings (symbols are already in-process via require "openssl") -
|
|
35
|
+
def sym(name)
|
|
36
|
+
Fiddle::Handle::DEFAULT[name]
|
|
37
|
+
rescue Fiddle::DLError
|
|
38
|
+
@lib ||= %w[libcrypto.so libcrypto.so.3 libcrypto.dylib libcrypto.so.1.1]
|
|
39
|
+
.lazy.map { |n| begin; Fiddle.dlopen(n); rescue Fiddle::DLError; nil; end }.find(&:itself)
|
|
40
|
+
@lib[name]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
P, L, I = Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_INT
|
|
44
|
+
D2I_CRL = Fiddle::Function.new(sym("d2i_X509_CRL"), [P, P, L], P)
|
|
45
|
+
D2I_INT = Fiddle::Function.new(sym("d2i_ASN1_INTEGER"), [P, P, L], P)
|
|
46
|
+
GET0 = Fiddle::Function.new(sym("X509_CRL_get0_by_serial"),[P, P, P], I)
|
|
47
|
+
|
|
48
|
+
# d2i_*(a, **pp, len) advances *pp; wrap a DER String into a C object pointer.
|
|
49
|
+
def d2i(fn, der)
|
|
50
|
+
buf = Fiddle::Pointer[der]
|
|
51
|
+
obj = fn.call(nil, Fiddle::Pointer[[buf.to_i].pack("q")], der.bytesize)
|
|
52
|
+
raise "d2i failed" if obj.null?
|
|
53
|
+
obj
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Peak resident set size in MB (high-water mark for this process).
|
|
57
|
+
def peak_mb
|
|
58
|
+
if File.exist?("/proc/self/status") # Linux
|
|
59
|
+
File.foreach("/proc/self/status") { |l| return l.split[1].to_f / 1024 if l.start_with?("VmHWM:") }
|
|
60
|
+
end
|
|
61
|
+
if Process.respond_to?(:getrusage) # macOS / BSD
|
|
62
|
+
m = Process.getrusage(:SELF).maxrss
|
|
63
|
+
return RUBY_PLATFORM.include?("darwin") ? m / 1024.0 / 1024 : m / 1024.0
|
|
64
|
+
end
|
|
65
|
+
0.0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Run the block in a fresh process; peak RSS then reflects only that work.
|
|
69
|
+
def isolate
|
|
70
|
+
rd, wr = IO.pipe
|
|
71
|
+
pid = fork { rd.close; wr.write(Marshal.dump(yield)); wr.close; exit! }
|
|
72
|
+
wr.close
|
|
73
|
+
out = rd.read; rd.close; Process.wait(pid)
|
|
74
|
+
Marshal.load(out)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Approach A: the only thing the gem can do today.
|
|
78
|
+
def bench_revoked(body, serial)
|
|
79
|
+
bn = OpenSSL::BN.new(serial)
|
|
80
|
+
crl = nil; tp = Benchmark.realtime { crl = OpenSSL::X509::CRL.new(body) }
|
|
81
|
+
hit = nil; entries = nil
|
|
82
|
+
tl = Benchmark.realtime { list = crl.revoked; entries = list.size; hit = list.find { |r| r.serial == bn } }
|
|
83
|
+
{ entries: entries, parse_ms: tp * 1000, lookup_ms: tl * 1000, rss_mb: peak_mb, revoked: !hit.nil? }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Approach B: X509_CRL_get0_by_serial (0 = not found, 1/2 = found).
|
|
87
|
+
def bench_get0(body, serial)
|
|
88
|
+
crl = nil; tp = Benchmark.realtime { crl = d2i(D2I_CRL, body) }
|
|
89
|
+
rc = nil
|
|
90
|
+
tl = Benchmark.realtime do
|
|
91
|
+
asn1 = d2i(D2I_INT, OpenSSL::ASN1::Integer.new(serial).to_der)
|
|
92
|
+
rc = GET0.call(crl, Fiddle::NULL, asn1)
|
|
93
|
+
end
|
|
94
|
+
{ parse_ms: tp * 1000, lookup_ms: tl * 1000, rss_mb: peak_mb, revoked: rc != 0 }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
absent = 0xDEAD_BEEF_CAFE_F00D # ~certainly not in the CRL
|
|
98
|
+
present = isolate { OpenSSL::X509::CRL.new(body).revoked.first&.serial&.to_i } # a real revoked serial
|
|
99
|
+
|
|
100
|
+
puts "CRL: #{CRL_URL}"
|
|
101
|
+
puts "DER: #{body.bytesize} bytes (#{(body.bytesize / 1e6).round(1)} MB)"
|
|
102
|
+
puts "Ruby #{RUBY_VERSION}, openssl gem #{OpenSSL::VERSION}, #{OpenSSL::OPENSSL_LIBRARY_VERSION}"
|
|
103
|
+
puts
|
|
104
|
+
|
|
105
|
+
a = isolate { bench_revoked(body, absent) }
|
|
106
|
+
b = isolate { bench_get0(body, absent) }
|
|
107
|
+
|
|
108
|
+
puts "Lookup of an absent serial (#{a[:entries]} entries in the CRL):"
|
|
109
|
+
printf " %-26s parse %7.1f ms | lookup %8.1f ms | peak RSS %7.1f MB\n",
|
|
110
|
+
"A #revoked.find", a[:parse_ms], a[:lookup_ms], a[:rss_mb]
|
|
111
|
+
printf " %-26s parse %7.1f ms | lookup %8.1f ms | peak RSS %7.1f MB\n",
|
|
112
|
+
"B get0_by_serial", b[:parse_ms], b[:lookup_ms], b[:rss_mb]
|
|
113
|
+
printf " => %.1fx faster lookup, %.1fx less peak RSS\n",
|
|
114
|
+
a[:lookup_ms] / b[:lookup_ms], a[:rss_mb] / b[:rss_mb]
|
|
115
|
+
puts
|
|
116
|
+
|
|
117
|
+
# correctness: both must agree on an absent and a present serial
|
|
118
|
+
ok_absent = (a[:revoked] == false) && (b[:revoked] == false)
|
|
119
|
+
ok_present = present && isolate { bench_revoked(body, present)[:revoked] } &&
|
|
120
|
+
isolate { bench_get0(body, present)[:revoked] }
|
|
121
|
+
puts "correctness: absent serial -> both 'not revoked' (#{ok_absent ? 'PASS' : 'FAIL'}); " \
|
|
122
|
+
"present serial -> both 'revoked' (#{ok_present ? 'PASS' : 'FAIL'})"
|
data/lib/ssl-test/crl.rb
CHANGED
|
@@ -12,35 +12,22 @@ module SSLTest
|
|
|
12
12
|
# A note about caching:
|
|
13
13
|
# I choose to only cache the raw HTTP body here (and not the parsed list or better a hash
|
|
14
14
|
# indexed by certificat serial). This is not CPU efficient because it means every time we
|
|
15
|
-
# need to check a cert from a cached CRL we need to
|
|
15
|
+
# need to check a cert from a cached CRL we need to read it again, optionally instantiate the list
|
|
16
16
|
# of Revoked certs and then iterate to find it (there's no API to find one cert without
|
|
17
|
-
# generting the list
|
|
17
|
+
# generting the list yet: https://github.com/ruby/openssl/pull/1065).
|
|
18
18
|
# I did this because of memory efficiency, because for big 20MB CRL list (so taking 20MB
|
|
19
19
|
# in memory cache), the parsed version takes more than 100M, the list of Revoked certs 120MB,
|
|
20
20
|
# and building a hash with serial, time and reason takes even more.
|
|
21
21
|
# So doing this would be MUCH faster in terms of CPU for subsequent tests on the same CRL
|
|
22
22
|
# but would take a LOT of memory.
|
|
23
|
-
# Note: we now check CRL first for every cert in the chain (leaf included), so leaf
|
|
24
|
-
# CRLs are fetched and cached too. These can be large for busy CAs, which makes the
|
|
25
|
-
# memory tradeoff above (caching the raw body rather than the parsed list) even more relevant.
|
|
26
23
|
|
|
27
24
|
private
|
|
28
25
|
|
|
29
26
|
def test_crl_revocation cert, issuer:, chain:, **options
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
return [false, "Missing crlDistributionPoints extension", nil] unless crl_distribution_points
|
|
35
|
-
|
|
36
|
-
# OpenSSL 2.2+ may simplify this: https://github.com/ruby/openssl/commit/ea702a106d3d8136c48f244593de95666be0edf9
|
|
37
|
-
crl = crl_distribution_points.value.split("\n").find do |description|
|
|
38
|
-
description.match?(/URI:/)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
return [false, "Missing CRL URI in crlDistributionPoints extension", nil] unless crl
|
|
27
|
+
crl = cert.crl_uris&.first
|
|
28
|
+
return [false, "Missing crlDistributionPoints extension", nil] if crl.nil?
|
|
42
29
|
|
|
43
|
-
crl_uri = URI(crl
|
|
30
|
+
crl_uri = URI(crl)
|
|
44
31
|
http_response, crl_request_error = follow_crl_redirects(crl_uri, **options)
|
|
45
32
|
return [false, "Request failed (URI: #{crl_uri}): #{crl_request_error}", nil] unless http_response
|
|
46
33
|
|
|
@@ -53,10 +40,18 @@ module SSLTest
|
|
|
53
40
|
serial_der = OpenSSL::ASN1::Integer.new(cert.serial).to_der
|
|
54
41
|
return :crl_ok unless response.to_der.include?(serial_der)
|
|
55
42
|
|
|
56
|
-
# The serial's bytes appear (a real hit, or a rare collision):
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
revoked =
|
|
43
|
+
# The serial's bytes appear (a real hit, or a rare collision): confirm
|
|
44
|
+
# authoritatively and pull the reason/date. This only runs for the few
|
|
45
|
+
# certs that look revoked, so we can afford the lookup here.
|
|
46
|
+
revoked =
|
|
47
|
+
if response.respond_to?(:by_serial)
|
|
48
|
+
# O(log n) C-level lookup, no full-list materialisation. Only on recent
|
|
49
|
+
# openssl gems (https://github.com/ruby/openssl/pull/1065).
|
|
50
|
+
response.by_serial(cert.serial)
|
|
51
|
+
else
|
|
52
|
+
response.revoked.find { |r| r.serial == cert.serial }
|
|
53
|
+
end
|
|
54
|
+
|
|
60
55
|
if revoked
|
|
61
56
|
reason = revoked.extensions.find {|e| e.oid == "CRLReason"}&.value
|
|
62
57
|
return [true, reason || "Unknown reason", revoked.time]
|
data/lib/ssl-test.rb
CHANGED
|
@@ -11,7 +11,7 @@ module SSLTest
|
|
|
11
11
|
extend OCSP
|
|
12
12
|
extend CRL
|
|
13
13
|
|
|
14
|
-
VERSION = -"2.
|
|
14
|
+
VERSION = -"2.1.2"
|
|
15
15
|
|
|
16
16
|
# Prefix for all cache keys so SSLTest entries coexist cleanly inside a shared
|
|
17
17
|
# cache (e.g. Rails.cache).
|
|
@@ -117,10 +117,11 @@ module SSLTest
|
|
|
117
117
|
# The order in which revocation check methods are tried for each certificate.
|
|
118
118
|
# The first method to return a conclusive answer (ok or revoked) wins; the
|
|
119
119
|
# next is only tried when the previous one errors out (missing endpoint,
|
|
120
|
-
# network error, etc.). Defaults to
|
|
121
|
-
#
|
|
120
|
+
# network error, etc.). Defaults to OCSP first, since CRLs can be large and
|
|
121
|
+
# checking them is significantly more memory- and CPU-intensive. Set to
|
|
122
|
+
# %i[crl ocsp] to check CRL first (e.g. to reduce revocation propagation delay).
|
|
122
123
|
def revocation_order
|
|
123
|
-
@revocation_order ||= %i[crl
|
|
124
|
+
@revocation_order ||= %i[ocsp crl]
|
|
124
125
|
end
|
|
125
126
|
|
|
126
127
|
def revocation_order= order
|
data/spec/memory_store_spec.rb
CHANGED
|
@@ -52,7 +52,7 @@ describe SSLTest::MemoryStore do
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# #size as reported through the default store after real CRL/OCSP fetches.
|
|
55
|
-
describe "SSLTest.cache.size", retry:
|
|
55
|
+
describe "SSLTest.cache.size", retry: 2 do # examples hit live CRL/OCSP endpoints
|
|
56
56
|
before { SSLTest.cache.clear }
|
|
57
57
|
|
|
58
58
|
it "returns 0 by default" do
|
|
@@ -70,7 +70,9 @@ describe "SSLTest.cache.size", retry: 5 do # examples hit live CRL/OCSP endpoint
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
it "returns OCSP cache size properly" do
|
|
73
|
-
|
|
73
|
+
# Google's leaf is checked via OCSP and its intermediate (no OCSP URI) via CRL,
|
|
74
|
+
# so this populates both the OCSP and CRL caches under the default OCSP-first order.
|
|
75
|
+
SSLTest.test("https://google.com")
|
|
74
76
|
expect(SSLTest.cache.size[:ocsp][:responses]).to eq(1)
|
|
75
77
|
expect(SSLTest.cache.size[:ocsp][:errors]).to eq(0)
|
|
76
78
|
expect(SSLTest.cache.size[:ocsp][:bytes]).to be > 0
|
data/spec/ssl-test_spec.rb
CHANGED
|
@@ -12,9 +12,10 @@ RSpec.configure do |config|
|
|
|
12
12
|
# The error/revocation examples below hit several public TLS test endpoints
|
|
13
13
|
# (badssl.com, testserver.host, ssl.com) which intermittently reset connections
|
|
14
14
|
# under load. They're spread across a few providers to avoid hammering a single
|
|
15
|
-
# one, and the network-hitting describe blocks are tagged `retry:
|
|
15
|
+
# one, and the network-hitting describe blocks are tagged `retry: 2` (via
|
|
16
16
|
# rspec-retry) so transient network blips don't fail the suite.
|
|
17
17
|
config.verbose_retry = true
|
|
18
|
+
config.display_try_failure_messages = true
|
|
18
19
|
config.default_sleep_interval = 1
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -26,7 +27,7 @@ describe SSLTest do
|
|
|
26
27
|
|
|
27
28
|
after(:each) { proxy_thread&.kill }
|
|
28
29
|
|
|
29
|
-
describe '.test_url', retry:
|
|
30
|
+
describe '.test_url', retry: 2 do # examples hit live TLS/CRL/OCSP endpoints
|
|
30
31
|
it "returns no error on valid SNI website" do
|
|
31
32
|
valid, error, cert = SSLTest.test("https://www.mycs.com")
|
|
32
33
|
expect(error).to be_nil
|
|
@@ -66,7 +67,7 @@ describe SSLTest do
|
|
|
66
67
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
67
68
|
end
|
|
68
69
|
|
|
69
|
-
it "returns error on incomplete chain" do
|
|
70
|
+
it "returns error on incomplete chain", retry: 5 do
|
|
70
71
|
valid, error, cert = SSLTest.test("https://incomplete-chain.badssl.com/")
|
|
71
72
|
expect(error).to eq ("error code 20: unable to get local issuer certificate")
|
|
72
73
|
expect(valid).to eq(false)
|
|
@@ -81,8 +82,8 @@ describe SSLTest do
|
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
it "returns error on invalid host" do
|
|
84
|
-
valid, error, cert = SSLTest.test("https://
|
|
85
|
-
expect(error).to
|
|
85
|
+
valid, error, cert = SSLTest.test("https://db3.updn.io/")
|
|
86
|
+
expect(error).to eq('error code 62: hostname mismatch')
|
|
86
87
|
expect(valid).to eq(false)
|
|
87
88
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
88
89
|
end
|
|
@@ -111,7 +112,7 @@ describe SSLTest do
|
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
it "reports revocation exceptions" do
|
|
114
|
-
expect(SSLTest).to receive(:
|
|
115
|
+
expect(SSLTest).to receive(:follow_ocsp_redirects).and_raise(ArgumentError.new("test"))
|
|
115
116
|
valid, error, cert = SSLTest.test("https://digicert.com")
|
|
116
117
|
expect(error).to eq ("SSL certificate test failed: test")
|
|
117
118
|
expect(valid).to be_nil
|
|
@@ -119,9 +120,9 @@ describe SSLTest do
|
|
|
119
120
|
end
|
|
120
121
|
|
|
121
122
|
it "returns error on revoked cert (OCSP)" do
|
|
122
|
-
#
|
|
123
|
-
expect(SSLTest).to receive(:test_crl_revocation).once.and_return([false, "skip CRL", nil])
|
|
123
|
+
# OCSP is tried first and detects the revocation, so CRL is never used
|
|
124
124
|
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
125
|
+
expect(SSLTest).not_to receive(:follow_crl_redirects)
|
|
125
126
|
valid, error, cert = SSLTest.test("https://revoked-rsa-dv.ssl.com/")
|
|
126
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)")
|
|
127
128
|
expect(valid).to eq(false)
|
|
@@ -129,43 +130,45 @@ describe SSLTest do
|
|
|
129
130
|
end
|
|
130
131
|
|
|
131
132
|
it "returns error on revoked cert (CRL)" do
|
|
132
|
-
#
|
|
133
|
+
# Disable OCSP (tried first) so the CRL performs the revocation check
|
|
134
|
+
expect(SSLTest).to receive(:test_ocsp_revocation).once.and_return([false, "skip OCSP", nil])
|
|
133
135
|
expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
|
|
134
|
-
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
# On a serial byte-match we confirm via #by_serial (recent openssl) or, as a
|
|
137
|
+
# fallback, #revoked, to extract the reason/date.
|
|
138
|
+
confirm = OpenSSL::X509::CRL.method_defined?(:by_serial) ? :by_serial : :revoked
|
|
139
|
+
expect_any_instance_of(OpenSSL::X509::CRL).to receive(confirm).and_call_original
|
|
140
|
+
valid, error, cert = SSLTest.test("https://revoked.testserver.host/")
|
|
141
|
+
expect(error).to match(/SSL certificate revoked: Unknown reason \(revocation date:/)
|
|
139
142
|
expect(valid).to eq(false)
|
|
140
143
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
141
144
|
end
|
|
142
145
|
|
|
143
146
|
it "stops following redirection after the limit for the revoked certs check" do
|
|
144
147
|
valid, error, cert = SSLTest.test("https://github.com/", redirection_limit: 0)
|
|
145
|
-
expect(error).to include("Revocation test couldn't be performed:
|
|
146
|
-
expect(error).to include("OCSP: Request failed")
|
|
148
|
+
expect(error).to include("Revocation test couldn't be performed: OCSP: Request failed")
|
|
147
149
|
expect(error).to include("Too many redirections (> 0)")
|
|
148
150
|
expect(valid).to eq(true)
|
|
149
151
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
150
152
|
end
|
|
151
153
|
|
|
152
154
|
it "warns when the OCSP URI is missing" do
|
|
153
|
-
# Disable CRL
|
|
154
|
-
expect(SSLTest).to receive(:test_crl_revocation).
|
|
155
|
+
# Disable CRL fallback to see the OCSP error message
|
|
156
|
+
expect(SSLTest).to receive(:test_crl_revocation).once.and_return([false, "skip CRL", nil])
|
|
155
157
|
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
156
158
|
valid, error, cert = SSLTest.test("https://google.com")
|
|
157
|
-
expect(error).to eq ("Revocation test couldn't be performed:
|
|
159
|
+
expect(error).to eq ("Revocation test couldn't be performed: OCSP: Missing OCSP URI in authorityInfoAccess extension, CRL: skip CRL")
|
|
158
160
|
expect(valid).to eq(true)
|
|
159
161
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
160
162
|
end
|
|
161
163
|
|
|
162
164
|
it "works with CRL only" do
|
|
163
|
-
# CRL
|
|
165
|
+
# Disable OCSP so the CRL performs the revocation check for both certs
|
|
166
|
+
expect(SSLTest).to receive(:test_ocsp_revocation).twice.and_return([false, "skip OCSP", nil])
|
|
164
167
|
expect(SSLTest).to receive(:follow_crl_redirects).twice.and_call_original
|
|
165
|
-
expect(SSLTest).not_to receive(:test_ocsp_revocation)
|
|
166
168
|
# Both certs are absent from their CRL, so the serial byte-search short-circuits
|
|
167
|
-
# and we never
|
|
169
|
+
# and we never reach a per-serial lookup (let alone materialise the revoked list).
|
|
168
170
|
expect_any_instance_of(OpenSSL::X509::CRL).not_to receive(:revoked)
|
|
171
|
+
expect_any_instance_of(OpenSSL::X509::CRL).not_to receive(:by_serial) if OpenSSL::X509::CRL.method_defined?(:by_serial)
|
|
169
172
|
valid, error, cert = SSLTest.test("https://www.demarches-simplifiees.fr")
|
|
170
173
|
expect(error).to be_nil
|
|
171
174
|
expect(valid).to eq(true)
|
|
@@ -177,15 +180,15 @@ describe SSLTest do
|
|
|
177
180
|
expect(SSLTest).to receive(:test_ocsp_revocation).once.and_return([false, "skip OCSP", nil])
|
|
178
181
|
expect(SSLTest).not_to receive(:follow_crl_redirects)
|
|
179
182
|
valid, error, cert = SSLTest.test("https://github.com")
|
|
180
|
-
expect(error).to eq ("Revocation test couldn't be performed: CRL: Missing crlDistributionPoints extension
|
|
183
|
+
expect(error).to eq ("Revocation test couldn't be performed: OCSP: skip OCSP, CRL: Missing crlDistributionPoints extension")
|
|
181
184
|
expect(valid).to eq(true)
|
|
182
185
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
183
186
|
end
|
|
184
187
|
|
|
185
|
-
it "works with OCSP for first cert and CRL for intermediate (
|
|
188
|
+
it "works with OCSP for first cert and CRL for intermediate (Google)" do
|
|
186
189
|
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
187
190
|
expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
|
|
188
|
-
valid, error, cert = SSLTest.test("https://
|
|
191
|
+
valid, error, cert = SSLTest.test("https://google.com")
|
|
189
192
|
expect(error).to be_nil
|
|
190
193
|
expect(valid).to eq(true)
|
|
191
194
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
@@ -226,7 +229,7 @@ describe SSLTest do
|
|
|
226
229
|
expect(valid).to eq(true)
|
|
227
230
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
228
231
|
|
|
229
|
-
expect($proxy).to have_received(:do_GET).
|
|
232
|
+
expect($proxy).to have_received(:do_GET).thrice
|
|
230
233
|
end
|
|
231
234
|
end
|
|
232
235
|
|
|
@@ -241,7 +244,7 @@ describe SSLTest do
|
|
|
241
244
|
end
|
|
242
245
|
end
|
|
243
246
|
|
|
244
|
-
describe '.follow_crl_redirects', retry:
|
|
247
|
+
describe '.follow_crl_redirects', retry: 2 do # fetches a live CRL
|
|
245
248
|
before { SSLTest.cache.clear }
|
|
246
249
|
# 19MB: http://crl3.digicert.com/ssca-sha2-g6.crl
|
|
247
250
|
it "fetch CRL list and updates cache" do
|
|
@@ -272,7 +275,7 @@ describe SSLTest do
|
|
|
272
275
|
end
|
|
273
276
|
end
|
|
274
277
|
|
|
275
|
-
describe '.cache', retry:
|
|
278
|
+
describe '.cache', retry: 2 do # some examples hit live CRL/OCSP endpoints
|
|
276
279
|
# Restore the default in-process store after tests that swap the backend so
|
|
277
280
|
# global state doesn't leak between examples.
|
|
278
281
|
after { SSLTest.cache = SSLTest::MemoryStore.new }
|
|
@@ -299,7 +302,7 @@ describe SSLTest do
|
|
|
299
302
|
end
|
|
300
303
|
end
|
|
301
304
|
|
|
302
|
-
describe '.test_cert', retry:
|
|
305
|
+
describe '.test_cert', retry: 2 do # revocation checks hit live CRL/OCSP endpoints
|
|
303
306
|
it "returns no error on valid SNI website" do
|
|
304
307
|
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/www_mycs_com_client.pem')))
|
|
305
308
|
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/www_mycs_com_ca_bundle.pem')))
|
|
@@ -342,7 +345,7 @@ describe SSLTest do
|
|
|
342
345
|
it "reports revocation exceptions" do
|
|
343
346
|
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/digicert_com_client.pem')))
|
|
344
347
|
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/digicert_com_ca_bundle.pem')))
|
|
345
|
-
expect(SSLTest).to receive(:
|
|
348
|
+
expect(SSLTest).to receive(:follow_ocsp_redirects).and_raise(ArgumentError.new("test"))
|
|
346
349
|
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
347
350
|
expect(error).to eq("SSL certificate test failed: test")
|
|
348
351
|
expect(valid).to be_nil
|
|
@@ -353,9 +356,9 @@ describe SSLTest do
|
|
|
353
356
|
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/revoked_rsa_dv_client.pem')))
|
|
354
357
|
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/revoked_rsa_dv_ca_bundle.pem')))
|
|
355
358
|
|
|
356
|
-
#
|
|
357
|
-
expect(SSLTest).to receive(:test_crl_revocation).once.and_return([false, "skip CRL", nil])
|
|
359
|
+
# OCSP is tried first and detects the revocation, so CRL is never used
|
|
358
360
|
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
361
|
+
expect(SSLTest).not_to receive(:follow_crl_redirects)
|
|
359
362
|
|
|
360
363
|
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
361
364
|
expect(error).to eq ("SSL certificate revoked: The certificate was revoked for an unknown reason (revocation date: 2025-06-09 15:07:39 UTC)")
|
|
@@ -367,9 +370,12 @@ describe SSLTest do
|
|
|
367
370
|
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/revoked_badssl_client.pem')))
|
|
368
371
|
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/revoked_badssl_ca_bundle.pem')))
|
|
369
372
|
|
|
370
|
-
#
|
|
373
|
+
# Disable OCSP (tried first) so the CRL performs the revocation check
|
|
374
|
+
expect(SSLTest).to receive(:test_ocsp_revocation).once.and_return([false, "skip OCSP", nil])
|
|
371
375
|
expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
|
|
372
|
-
|
|
376
|
+
# On a serial byte-match we confirm via #by_serial (recent openssl) or #revoked
|
|
377
|
+
confirm = OpenSSL::X509::CRL.method_defined?(:by_serial) ? :by_serial : :revoked
|
|
378
|
+
expect_any_instance_of(OpenSSL::X509::CRL).to receive(confirm).and_call_original
|
|
373
379
|
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
374
380
|
expect(error).to eq ("SSL certificate revoked: Key Compromise (revocation date: 2026-05-12 21:01:31 UTC)")
|
|
375
381
|
expect(valid).to eq(false)
|
|
@@ -381,8 +387,7 @@ describe SSLTest do
|
|
|
381
387
|
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/www_github_com_ca_bundle.pem')))
|
|
382
388
|
|
|
383
389
|
valid, error, cert = SSLTest.test_cert(cert, ca_bundle, redirection_limit: 0)
|
|
384
|
-
expect(error).to include("Revocation test couldn't be performed:
|
|
385
|
-
expect(error).to include("OCSP: Request failed")
|
|
390
|
+
expect(error).to include("Revocation test couldn't be performed: OCSP: Request failed")
|
|
386
391
|
expect(error).to include("Too many redirections (> 0)")
|
|
387
392
|
expect(valid).to eq(true)
|
|
388
393
|
expect(cert).to eq(cert)
|
|
@@ -392,12 +397,12 @@ describe SSLTest do
|
|
|
392
397
|
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/google_com_client.pem')))
|
|
393
398
|
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/google_com_ca_bundle.pem')))
|
|
394
399
|
|
|
395
|
-
# Disable CRL
|
|
396
|
-
expect(SSLTest).to receive(:test_crl_revocation).
|
|
400
|
+
# Disable CRL fallback to see the OCSP error message
|
|
401
|
+
expect(SSLTest).to receive(:test_crl_revocation).once.and_return([false, "skip CRL", nil])
|
|
397
402
|
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
398
403
|
|
|
399
404
|
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
400
|
-
expect(error).to eq ("Revocation test couldn't be performed:
|
|
405
|
+
expect(error).to eq ("Revocation test couldn't be performed: OCSP: Missing OCSP URI in authorityInfoAccess extension, CRL: skip CRL")
|
|
401
406
|
expect(valid).to eq(true)
|
|
402
407
|
expect(cert).to eq(cert)
|
|
403
408
|
end
|
|
@@ -406,9 +411,9 @@ describe SSLTest do
|
|
|
406
411
|
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/www_demarches-simplifiees_fr_client.pem')))
|
|
407
412
|
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/www_demarches-simplifiees_fr_ca_bundle.pem')))
|
|
408
413
|
|
|
409
|
-
# CRL
|
|
414
|
+
# Disable OCSP so the CRL performs the revocation check for both certs
|
|
415
|
+
expect(SSLTest).to receive(:test_ocsp_revocation).twice.and_return([false, "skip OCSP", nil])
|
|
410
416
|
expect(SSLTest).to receive(:follow_crl_redirects).twice.and_call_original
|
|
411
|
-
expect(SSLTest).not_to receive(:test_ocsp_revocation)
|
|
412
417
|
|
|
413
418
|
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
414
419
|
expect(error).to be_nil
|
|
@@ -425,18 +430,18 @@ describe SSLTest do
|
|
|
425
430
|
expect(SSLTest).not_to receive(:follow_crl_redirects)
|
|
426
431
|
|
|
427
432
|
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
428
|
-
expect(error).to eq ("Revocation test couldn't be performed: CRL: Missing crlDistributionPoints extension
|
|
433
|
+
expect(error).to eq ("Revocation test couldn't be performed: OCSP: skip OCSP, CRL: Missing crlDistributionPoints extension")
|
|
429
434
|
expect(valid).to eq(true)
|
|
430
435
|
expect(cert).to eq(cert)
|
|
431
436
|
|
|
432
437
|
end
|
|
433
438
|
|
|
434
|
-
it "works with OCSP for first cert and CRL for intermediate (
|
|
439
|
+
it "works with OCSP for first cert and CRL for intermediate (Google)" do
|
|
435
440
|
expect(SSLTest).to receive(:follow_ocsp_redirects).once.and_call_original
|
|
436
441
|
expect(SSLTest).to receive(:follow_crl_redirects).once.and_call_original
|
|
437
442
|
|
|
438
|
-
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/
|
|
439
|
-
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/
|
|
443
|
+
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/google_com_client.pem')))
|
|
444
|
+
ca_bundle = OpenSSL::X509::Certificate.load(File.read(File.join(__dir__, 'fixtures/google_com_ca_bundle.pem')))
|
|
440
445
|
|
|
441
446
|
valid, error, cert = SSLTest.test_cert(cert, ca_bundle)
|
|
442
447
|
expect(error).to be_nil
|
|
@@ -475,8 +480,7 @@ describe SSLTest do
|
|
|
475
480
|
expect(valid).to eq(true)
|
|
476
481
|
expect(cert).to eq(cert)
|
|
477
482
|
|
|
478
|
-
|
|
479
|
-
expect($proxy).to have_received(:do_GET).twice
|
|
483
|
+
expect($proxy).to have_received(:do_GET).once
|
|
480
484
|
end
|
|
481
485
|
end
|
|
482
486
|
|
|
@@ -495,14 +499,14 @@ describe SSLTest do
|
|
|
495
499
|
end
|
|
496
500
|
|
|
497
501
|
describe '.revocation_order' do # no network: dispatch logic is stubbed
|
|
498
|
-
after { SSLTest.revocation_order = %i[crl
|
|
502
|
+
after { SSLTest.revocation_order = %i[ocsp crl] } # reset to the default
|
|
499
503
|
|
|
500
504
|
let(:cert) { OpenSSL::X509::Certificate.new }
|
|
501
505
|
let(:issuer) { OpenSSL::X509::Certificate.new }
|
|
502
506
|
let(:chain) { [cert, issuer] }
|
|
503
507
|
|
|
504
|
-
it "defaults to
|
|
505
|
-
expect(SSLTest.revocation_order).to eq(%i[crl
|
|
508
|
+
it "defaults to OCSP first" do
|
|
509
|
+
expect(SSLTest.revocation_order).to eq(%i[ocsp crl])
|
|
506
510
|
end
|
|
507
511
|
|
|
508
512
|
it "validates the value" do
|
|
@@ -510,23 +514,22 @@ describe SSLTest do
|
|
|
510
514
|
expect { SSLTest.revocation_order = %i[ocsp bogus] }.to raise_error(ArgumentError)
|
|
511
515
|
end
|
|
512
516
|
|
|
513
|
-
it "checks
|
|
514
|
-
expect(SSLTest).to receive(:
|
|
515
|
-
expect(SSLTest).to receive(:
|
|
517
|
+
it "checks OCSP first by default, falling back to CRL on error" do
|
|
518
|
+
expect(SSLTest).to receive(:test_ocsp_revocation).ordered.and_return([false, "OCSP boom", nil])
|
|
519
|
+
expect(SSLTest).to receive(:test_crl_revocation).ordered.and_return(:crl_ok)
|
|
516
520
|
result = SSLTest.send(:test_chain_revocation, chain)
|
|
517
521
|
expect(result).to eq([false, nil, nil])
|
|
518
522
|
end
|
|
519
523
|
|
|
520
|
-
it "checks
|
|
521
|
-
SSLTest.revocation_order = %i[ocsp
|
|
522
|
-
expect(SSLTest).to receive(:
|
|
523
|
-
expect(SSLTest).to receive(:
|
|
524
|
+
it "checks CRL first when configured, falling back to OCSP on error" do
|
|
525
|
+
SSLTest.revocation_order = %i[crl ocsp]
|
|
526
|
+
expect(SSLTest).to receive(:test_crl_revocation).ordered.and_return([false, "CRL boom", nil])
|
|
527
|
+
expect(SSLTest).to receive(:test_ocsp_revocation).ordered.and_return(:ocsp_ok)
|
|
524
528
|
result = SSLTest.send(:test_chain_revocation, chain)
|
|
525
529
|
expect(result).to eq([false, nil, nil])
|
|
526
530
|
end
|
|
527
531
|
|
|
528
532
|
it "does not try the second method when the first one passes" do
|
|
529
|
-
SSLTest.revocation_order = %i[ocsp crl]
|
|
530
533
|
expect(SSLTest).to receive(:test_ocsp_revocation).and_return(:ocsp_ok)
|
|
531
534
|
expect(SSLTest).not_to receive(:test_crl_revocation)
|
|
532
535
|
expect(SSLTest.send(:test_chain_revocation, chain)).to eq([false, nil, nil])
|
data/ssl-test.gemspec
CHANGED
|
@@ -12,6 +12,8 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.homepage = "https://github.com/jarthod/ssl-test"
|
|
13
13
|
spec.license = "MIT"
|
|
14
14
|
|
|
15
|
+
spec.required_ruby_version = ">= 3.1"
|
|
16
|
+
|
|
15
17
|
spec.files = `git ls-files -z`.split("\x0")
|
|
16
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
17
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ssl-test
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adrien Rey-Jarthon
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-06-
|
|
10
|
+
date: 2026-06-23 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bundler
|
|
@@ -134,6 +134,8 @@ files:
|
|
|
134
134
|
- LICENSE.txt
|
|
135
135
|
- README.md
|
|
136
136
|
- Rakefile
|
|
137
|
+
- by_serial_bench.rb
|
|
138
|
+
- crl_bench.rb
|
|
137
139
|
- lib/ssl-test.rb
|
|
138
140
|
- lib/ssl-test/crl.rb
|
|
139
141
|
- lib/ssl-test/memory_store.rb
|
|
@@ -174,7 +176,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
174
176
|
requirements:
|
|
175
177
|
- - ">="
|
|
176
178
|
- !ruby/object:Gem::Version
|
|
177
|
-
version: '
|
|
179
|
+
version: '3.1'
|
|
178
180
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
181
|
requirements:
|
|
180
182
|
- - ">="
|