ssl-test 1.6.0 → 2.0.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 +8 -0
- data/README.md +33 -4
- data/lib/ssl-test/crl.rb +20 -9
- data/lib/ssl-test/memory_store.rb +81 -0
- data/lib/ssl-test/ocsp.rb +15 -10
- data/lib/ssl-test.rb +26 -15
- data/spec/cache_backends_spec.rb +103 -0
- data/spec/memory_store_spec.rb +79 -0
- data/spec/ssl-test_spec.rb +37 -38
- data/ssl-test.gemspec +6 -0
- metadata +49 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 886440c38c24dd0ad30e2fff8552fe89aa924010e8f9218032d030eceb29a422
|
|
4
|
+
data.tar.gz: b854a7f800c8aa3364ed78ebf29b4ec731e3904b7c33b682a39383a5096742d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 895f1193c0924d93b0bc4c29dea5d9de8b38b047f25c83273d13146c3bffa0a201676e320d6f4a6f373f231f87fdf0fda7bbef5a8983e40d489c8179097ed8dc
|
|
7
|
+
data.tar.gz: c1d2d3cd0014d31e01456dd5f0d1e58a59e820bb77f8e15a2755a27421a107b63d6cc653f115a7e50f19b475f40b1961bbdccab3b92181f1a5f812f68a668604
|
data/.github/workflows/ruby.yml
CHANGED
|
@@ -3,6 +3,14 @@ on: [push]
|
|
|
3
3
|
jobs:
|
|
4
4
|
specs:
|
|
5
5
|
runs-on: ubuntu-22.04
|
|
6
|
+
services:
|
|
7
|
+
redis:
|
|
8
|
+
image: redis
|
|
9
|
+
ports: ['6379:6379']
|
|
10
|
+
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
|
11
|
+
memcached:
|
|
12
|
+
image: memcached
|
|
13
|
+
ports: ['11211:11211']
|
|
6
14
|
steps:
|
|
7
15
|
- uses: actions/checkout@v2
|
|
8
16
|
- name: Set up Ruby
|
data/README.md
CHANGED
|
@@ -100,16 +100,42 @@ After that it fetches the [CRL](https://en.wikipedia.org/wiki/Certificate_revoca
|
|
|
100
100
|
|
|
101
101
|
### Caching
|
|
102
102
|
|
|
103
|
-
OCSP and CRL responses are cached
|
|
103
|
+
OCSP and CRL responses are cached, which makes subsequent testing faster and more robust (avoids network error and throttling).
|
|
104
104
|
|
|
105
105
|
About the caching duration:
|
|
106
106
|
- OCSP responses are cached until their "next_update" indicated inside the repsonse
|
|
107
107
|
- OCSP errors are cached for 5 minutes
|
|
108
108
|
- CRL responses are cached for 1 hour
|
|
109
109
|
|
|
110
|
-
CRL responses can be big so when they expires they are re-validated with the server using HTTP caching headers when available (`Etag` & `Last-Modified`) to avoid downloading the list again if it didn't change.
|
|
110
|
+
CRL responses can be big so when they expires they are re-validated with the server using HTTP caching headers when available (`Etag` & `Last-Modified`) to avoid downloading the list again if it didn't change. The cached body is therefore kept in the backend for a longer retention period (~4 days, refreshed on each use) so it's still around to revalidate against; unused lists are dropped after that.
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
#### Cache backend
|
|
113
|
+
|
|
114
|
+
The cache backend is configurable. By default SSLTest uses a simple in-process store (`SSLTest::MemoryStore`). To share the cache across processes and get compression, assign any object implementing the `Rails.cache`-style API (`read`, `write(key, value, expires_in:)`, `delete`):
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
SSLTest.cache = Rails.cache # shared + compressed (e.g. memcache via Dalli)
|
|
118
|
+
SSLTest.cache = SSLTest::MemoryStore.new # the default in-process store
|
|
119
|
+
SSLTest.cache = MyCustomStore.new # anything responding to read/write/delete
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The default in-process store is per-process and unbounded, so be careful about memory usage if you try to validate millions of certificates in a row (the OCSP cache is keyed by certificate serial). Using a shared store like `Rails.cache` with memcache avoids this and shares the cache across processes.
|
|
123
|
+
|
|
124
|
+
If you want a bounded/compressed in-process cache without pulling in `Rails.cache`, the API is intentionally compatible with `ActiveSupport::Cache::MemoryStore`, which you can drop in directly:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
require "active_support/cache"
|
|
128
|
+
SSLTest.cache = ActiveSupport::Cache::MemoryStore.new(size: 64.megabytes, compress: true)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
(It auto-prunes when it exceeds `size`, unlike the built-in store. Note the introspection helpers below are specific to `SSLTest::MemoryStore`.)
|
|
132
|
+
|
|
133
|
+
> **Using memcached (Dalli)?** CRL lists can be large (commonly several MB, up to ~20MB for busy CAs). memcached rejects values over its max item size — 1MB by default — which Dalli surfaces as `Dalli::ValueOverMaxSize` (logged and skipped by `ActiveSupport::Cache::MemCacheStore`, so the test still passes but the list isn't cached and gets re-downloaded every time). To actually cache big CRLs, raise the limit on **both** sides to at least 64MB:
|
|
134
|
+
>
|
|
135
|
+
> - memcached server: start it with `-I 64m`
|
|
136
|
+
> - Dalli client: `Dalli::Client.new(servers, value_max_bytes: 64 * 1024 * 1024)`, or via Rails: `config.cache_store = :mem_cache_store, servers, { value_max_bytes: 64.megabytes }`
|
|
137
|
+
|
|
138
|
+
You can check the size of the **built-in** store with `SSLTest.cache.size`, which returns:
|
|
113
139
|
|
|
114
140
|
```ruby
|
|
115
141
|
{
|
|
@@ -125,7 +151,9 @@ You can check the size of the cache with `SSLTest.cache_size`, which returns:
|
|
|
125
151
|
}
|
|
126
152
|
```
|
|
127
153
|
|
|
128
|
-
You can also flush
|
|
154
|
+
You can also flush it using `SSLTest.cache.clear` if you want (not recommended).
|
|
155
|
+
|
|
156
|
+
`size` is specific to the built-in `MemoryStore`; other backends won't respond to it. (The module-level `SSLTest.cache_size` and `SSLTest.flush_cache` from previous versions were **removed in 2.0** — use `SSLTest.cache.size` / `SSLTest.cache.clear` instead.)
|
|
129
157
|
|
|
130
158
|
### Logging
|
|
131
159
|
|
|
@@ -167,6 +195,7 @@ But also **revoked certs** like most browsers (not handled by `curl`)
|
|
|
167
195
|
|
|
168
196
|
See also github releases: https://github.com/jarthod/ssl-test/releases
|
|
169
197
|
|
|
198
|
+
* 2.0.0 - 2026-06-16: 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)
|
|
170
199
|
* 1.6.0 - 2026-06-16: Check revocation with CRL first and fall back to OCSP (was OCSP first) to reduce revocation detection delay
|
|
171
200
|
* 1.5.0 - 2025-11-28: Add support for local certificates testing and HTTP proxies (#8), changed `#test` method into `#test_url` and `#test_cert` (`#test` remains as an alias for `#test_url` for backward-compatibility)
|
|
172
201
|
* 1.4.1 - 2022-10-24: Add support for "tcps://" scheme
|
data/lib/ssl-test/crl.rb
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
module SSLTest
|
|
2
2
|
module CRL
|
|
3
3
|
CRL_CACHE_DURATION = 3600 # 1 hour
|
|
4
|
+
# How long a CRL entry is kept in the backend before being dropped if it's no
|
|
5
|
+
# longer used. This is much longer than CRL_CACHE_DURATION so the cached body
|
|
6
|
+
# and caching headers survive past the revalidation window (for cheap 304s),
|
|
7
|
+
# but bounded so unused lists don't pile up forever in a shared/long-lived
|
|
8
|
+
# backend (e.g. memcache). It's refreshed on every fetch (200/304), so
|
|
9
|
+
# actively-used entries never expire from this.
|
|
10
|
+
CRL_CACHE_RETENTION = 100 * CRL_CACHE_DURATION # ~4 days
|
|
4
11
|
|
|
5
12
|
# A note about caching:
|
|
6
13
|
# I choose to only cache the raw HTTP body here (and not the parsed list or better a hash
|
|
@@ -54,10 +61,14 @@ module SSLTest
|
|
|
54
61
|
def follow_crl_redirects(uri, open_timeout: 5, read_timeout: 5, redirection_limit: 5, proxy_host: nil, proxy_port: nil)
|
|
55
62
|
return [nil, "Too many redirections (> #{redirection_limit})"] if redirection_limit == 0
|
|
56
63
|
|
|
57
|
-
# Return file from cache if not expired
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
# Return file from cache if not expired.
|
|
65
|
+
# CRL entries are kept in the backend for CRL_CACHE_RETENTION (much longer
|
|
66
|
+
# than CRL_CACHE_DURATION) so the cached body + caching headers survive past
|
|
67
|
+
# the freshness window and can be revalidated cheaply with a conditional
|
|
68
|
+
# request (304). We track our own freshness window with the :expires field.
|
|
69
|
+
cache_key = "#{CACHE_NAMESPACE}/crl/#{uri}"
|
|
70
|
+
cache_entry = cache.read(cache_key)
|
|
71
|
+
return [cache_entry[:body], nil] if cache_entry && cache_entry[:expires] > Time.now
|
|
61
72
|
|
|
62
73
|
@logger&.debug { "SSLTest + CRL: fetch URI #{uri}" }
|
|
63
74
|
path = uri.path == "" ? "/" : uri.path
|
|
@@ -67,9 +78,9 @@ module SSLTest
|
|
|
67
78
|
|
|
68
79
|
req = Net::HTTP::Get.new(path)
|
|
69
80
|
# Include conditional caching headers from cache to save bandwidth if list didn't change (304)
|
|
70
|
-
if etag = cache_entry&.
|
|
81
|
+
if etag = cache_entry&.[](:etag)
|
|
71
82
|
req["If-None-Match"] = etag
|
|
72
|
-
elsif last_mod = cache_entry&.
|
|
83
|
+
elsif last_mod = cache_entry&.[](:last_mod)
|
|
73
84
|
req["If-Modified-Since"] = last_mod
|
|
74
85
|
end
|
|
75
86
|
http_response = http.request(req)
|
|
@@ -77,19 +88,19 @@ module SSLTest
|
|
|
77
88
|
when Net::HTTPNotModified
|
|
78
89
|
# No changes, bump cache expiration time and return cached body
|
|
79
90
|
@logger&.debug { "SSLTest + CRL: 304 Not Modified" }
|
|
80
|
-
|
|
91
|
+
cache.write(cache_key, cache_entry.merge(expires: Time.now + CRL_CACHE_DURATION), expires_in: CRL_CACHE_RETENTION)
|
|
81
92
|
[cache_entry[:body], nil]
|
|
82
93
|
when Net::HTTPSuccess
|
|
83
94
|
# Success, update (or add to) cache and return frech body
|
|
84
95
|
@logger&.debug { "SSLTest + CRL: 200 OK (#{http_response.body.bytesize} bytes)" }
|
|
85
96
|
@logger&.warn { "SSLTest + CRL: Warning: massive file size" } if http_response.body.bytesize > 1024**2 # 1MB
|
|
86
97
|
@logger&.warn { "SSLTest + CRL: Warning: no caching headers on #{uri}" } unless http_response["Etag"] or http_response["Last-Modified"]
|
|
87
|
-
|
|
98
|
+
cache.write(cache_key, {
|
|
88
99
|
body: http_response.body,
|
|
89
100
|
expires: Time.now + CRL_CACHE_DURATION,
|
|
90
101
|
etag: http_response["Etag"],
|
|
91
102
|
last_mod: http_response["Last-Modified"]
|
|
92
|
-
}
|
|
103
|
+
}, expires_in: CRL_CACHE_RETENTION)
|
|
93
104
|
[http_response.body, nil]
|
|
94
105
|
when Net::HTTPRedirection
|
|
95
106
|
follow_crl_redirects(URI(http_response["location"]), open_timeout: open_timeout, read_timeout: read_timeout, proxy_host: proxy_host, proxy_port: proxy_port, redirection_limit: redirection_limit - 1)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module SSLTest
|
|
2
|
+
# A tiny in-process cache store used as the default backend. It mirrors the
|
|
3
|
+
# small subset of the ActiveSupport::Cache / Rails.cache API that SSLTest relies
|
|
4
|
+
# on (read/write/delete) so they're interchangeable (assign Rails.cache via
|
|
5
|
+
# SSLTest.cache= to share/compress across processes). Access is guarded by a
|
|
6
|
+
# Mutex because SSLTest is typically used from threaded servers (e.g. Puma).
|
|
7
|
+
#
|
|
8
|
+
# Unlike a shared/compressed backend (memcache via Dalli), this store is
|
|
9
|
+
# per-process, uncompressed and unbounded, so be careful about memory usage if
|
|
10
|
+
# you validate millions of certificates in a row (the OCSP cache is keyed by
|
|
11
|
+
# certificate serial). For those workloads, configure SSLTest.cache to a shared
|
|
12
|
+
# store instead.
|
|
13
|
+
class MemoryStore
|
|
14
|
+
def initialize
|
|
15
|
+
@data = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def read(key)
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
entry = @data[key]
|
|
22
|
+
next nil unless entry
|
|
23
|
+
if entry[:expires_at] && entry[:expires_at] <= Time.now
|
|
24
|
+
@data.delete(key)
|
|
25
|
+
next nil
|
|
26
|
+
end
|
|
27
|
+
entry[:value]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write(key, value, expires_in: nil)
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
@data[key] = { value: value, expires_at: expires_in && Time.now + expires_in }
|
|
34
|
+
end
|
|
35
|
+
value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def delete(key)
|
|
39
|
+
@mutex.synchronize { @data.delete(key) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def clear
|
|
43
|
+
@mutex.synchronize { @data.clear }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Yields [key, value] for every entry that hasn't expired.
|
|
47
|
+
def each
|
|
48
|
+
return enum_for(:each) unless block_given?
|
|
49
|
+
now = Time.now
|
|
50
|
+
@mutex.synchronize { @data.dup }.each do |key, entry|
|
|
51
|
+
next if entry[:expires_at] && entry[:expires_at] <= now
|
|
52
|
+
yield key, entry[:value]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns a breakdown of the cached SSLTest entries (CRL lists and OCSP
|
|
57
|
+
# responses/errors) with approximate byte sizes, mainly useful for monitoring
|
|
58
|
+
# memory usage. Specific to ssl-test's key namespace.
|
|
59
|
+
def size
|
|
60
|
+
crl_lists = ocsp_responses = ocsp_errors = 0
|
|
61
|
+
crl_bytes = ocsp_bytes = 0
|
|
62
|
+
each do |key, value|
|
|
63
|
+
case key
|
|
64
|
+
when %r{\A#{CACHE_NAMESPACE}/crl/}
|
|
65
|
+
crl_lists += 1
|
|
66
|
+
crl_bytes += ObjectSize.size(value)
|
|
67
|
+
when %r{\A#{CACHE_NAMESPACE}/ocsp-error/}
|
|
68
|
+
ocsp_errors += 1
|
|
69
|
+
ocsp_bytes += ObjectSize.size(value)
|
|
70
|
+
when %r{\A#{CACHE_NAMESPACE}/ocsp/}
|
|
71
|
+
ocsp_responses += 1
|
|
72
|
+
ocsp_bytes += ObjectSize.size(value)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
{
|
|
76
|
+
crl: { lists: crl_lists, bytes: crl_bytes },
|
|
77
|
+
ocsp: { responses: ocsp_responses, errors: ocsp_errors, bytes: ocsp_bytes }
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/ssl-test/ocsp.rb
CHANGED
|
@@ -5,15 +5,18 @@ module SSLTest
|
|
|
5
5
|
private
|
|
6
6
|
|
|
7
7
|
def test_ocsp_revocation cert, issuer:, chain:, **options
|
|
8
|
-
@ocsp_response_cache ||= {}
|
|
9
|
-
@ocsp_request_error_cache ||= {}
|
|
10
|
-
|
|
11
8
|
unicity_key = "#{cert.issuer}/#{cert.serial}"
|
|
9
|
+
error_cache_key = "#{CACHE_NAMESPACE}/ocsp-error/#{unicity_key}"
|
|
10
|
+
response_cache_key = "#{CACHE_NAMESPACE}/ocsp/#{unicity_key}"
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
# Expiry is handled by the cache backend (expires_in), so a cached value is
|
|
13
|
+
# always still valid: a present error means we recently failed, a present
|
|
14
|
+
# response means it hasn't reached its next_update yet.
|
|
15
|
+
cached_error = cache.read(error_cache_key)
|
|
16
|
+
return cached_error if cached_error
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
ocsp_response = cache.read(response_cache_key)
|
|
19
|
+
if ocsp_response.nil?
|
|
17
20
|
authority_info_access = cert.extensions.find do |extension|
|
|
18
21
|
extension.oid == "authorityInfoAccess"
|
|
19
22
|
end
|
|
@@ -57,11 +60,13 @@ module SSLTest
|
|
|
57
60
|
|
|
58
61
|
return ocsp_soft_fail_return("Serial check failed (URI: #{ocsp_uri})", unicity_key) unless response_certificate_id.serial == certificate_id.serial
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
ocsp_response = { status: status, reason: reason, revocation_time: revocation_time }
|
|
64
|
+
# Cache until the response's next_update. If it's already past (or
|
|
65
|
+
# missing), skip caching and just use the fresh result for this call.
|
|
66
|
+
ttl = next_update && next_update - Time.now
|
|
67
|
+
cache.write(response_cache_key, ocsp_response, expires_in: ttl) if ttl && ttl > 0
|
|
61
68
|
end
|
|
62
69
|
|
|
63
|
-
ocsp_response = @ocsp_response_cache[unicity_key]
|
|
64
|
-
|
|
65
70
|
return [true, revocation_reason_to_string(ocsp_response[:reason]), ocsp_response[:revocation_time]] if ocsp_response[:status] == OpenSSL::OCSP::V_CERTSTATUS_REVOKED
|
|
66
71
|
:ocsp_ok
|
|
67
72
|
end
|
|
@@ -135,7 +140,7 @@ module SSLTest
|
|
|
135
140
|
|
|
136
141
|
def ocsp_soft_fail_return(reason, unicity_key = nil)
|
|
137
142
|
error = [false, reason, nil]
|
|
138
|
-
|
|
143
|
+
cache.write("#{CACHE_NAMESPACE}/ocsp-error/#{unicity_key}", error, expires_in: ERROR_CACHE_DURATION) if unicity_key
|
|
139
144
|
error
|
|
140
145
|
end
|
|
141
146
|
end
|
data/lib/ssl-test.rb
CHANGED
|
@@ -3,6 +3,7 @@ require "net/https"
|
|
|
3
3
|
require "openssl"
|
|
4
4
|
require "uri"
|
|
5
5
|
require "ssl-test/object_size"
|
|
6
|
+
require "ssl-test/memory_store"
|
|
6
7
|
require "ssl-test/ocsp"
|
|
7
8
|
require "ssl-test/crl"
|
|
8
9
|
|
|
@@ -10,7 +11,11 @@ module SSLTest
|
|
|
10
11
|
extend OCSP
|
|
11
12
|
extend CRL
|
|
12
13
|
|
|
13
|
-
VERSION = -"
|
|
14
|
+
VERSION = -"2.0.0"
|
|
15
|
+
|
|
16
|
+
# Prefix for all cache keys so SSLTest entries coexist cleanly inside a shared
|
|
17
|
+
# cache (e.g. Rails.cache).
|
|
18
|
+
CACHE_NAMESPACE = -"ssl-test"
|
|
14
19
|
|
|
15
20
|
class << self
|
|
16
21
|
def test_url url, open_timeout: 5, read_timeout: 5, proxy_host: nil, proxy_port: nil, redirection_limit: 5
|
|
@@ -79,24 +84,30 @@ module SSLTest
|
|
|
79
84
|
end
|
|
80
85
|
end
|
|
81
86
|
|
|
87
|
+
# The cache backend used to store CRL and OCSP responses. Defaults to an
|
|
88
|
+
# in-process MemoryStore. To share the cache across processes (and get
|
|
89
|
+
# compression), assign Rails.cache (or any object responding to the
|
|
90
|
+
# Rails.cache-style API: read/write/delete), e.g. `SSLTest.cache = Rails.cache`.
|
|
91
|
+
def cache
|
|
92
|
+
@cache ||= MemoryStore.new
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def cache= store
|
|
96
|
+
@cache = store
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Removed in 2.0: introspection now lives on the cache store. With the
|
|
100
|
+
# built-in MemoryStore use SSLTest.cache.size; other backends (e.g. memcache)
|
|
101
|
+
# can't be enumerated.
|
|
82
102
|
def cache_size
|
|
83
|
-
|
|
84
|
-
crl: {
|
|
85
|
-
lists: @crl_response_cache&.size || 0,
|
|
86
|
-
bytes: ObjectSize.size(@crl_response_cache)
|
|
87
|
-
},
|
|
88
|
-
ocsp: {
|
|
89
|
-
responses: @ocsp_response_cache&.size || 0,
|
|
90
|
-
errors: @ocsp_request_error_cache&.size || 0,
|
|
91
|
-
bytes: ObjectSize.size(@ocsp_response_cache) + ObjectSize.size(@ocsp_request_error_cache)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
103
|
+
raise NoMethodError, "SSLTest.cache_size was removed in 2.0; use SSLTest.cache.size instead (available on the built-in SSLTest::MemoryStore)."
|
|
94
104
|
end
|
|
95
105
|
|
|
106
|
+
# Removed in 2.0: clearing now lives on the cache store. With the built-in
|
|
107
|
+
# MemoryStore use SSLTest.cache.clear (note: calling clear on a shared backend
|
|
108
|
+
# like Rails.cache would wipe unrelated entries).
|
|
96
109
|
def flush_cache
|
|
97
|
-
|
|
98
|
-
@ocsp_response_cache = {}
|
|
99
|
-
@ocsp_request_error_cache = {}
|
|
110
|
+
raise NoMethodError, "SSLTest.flush_cache was removed in 2.0; use SSLTest.cache.clear instead."
|
|
100
111
|
end
|
|
101
112
|
|
|
102
113
|
def logger= logger
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "ssl-test"
|
|
2
|
+
require "active_support"
|
|
3
|
+
require "active_support/cache"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
# Verifies the cache backends people are likely to plug into SSLTest.cache (the
|
|
7
|
+
# classic Rails/ActiveSupport stores) satisfy the read / write / expiration
|
|
8
|
+
# contract SSLTest relies on, including (de)serialization of the value shapes it
|
|
9
|
+
# stores: Hashes containing Strings (incl. binary CRL bodies), Times, Integers
|
|
10
|
+
# and nils, plus Arrays (OCSP errors).
|
|
11
|
+
#
|
|
12
|
+
# Stores backed by an external server (MemCacheStore, RedisCacheStore) or an
|
|
13
|
+
# extra gem are skipped when unavailable, so the suite stays green locally; CI
|
|
14
|
+
# provides the servers (see .github/workflows/ruby.yml) so they actually run.
|
|
15
|
+
describe "ActiveSupport cache backend compatibility" do
|
|
16
|
+
# Representative of what SSLTest caches: a CRL entry (binary body + Time) and
|
|
17
|
+
# an OCSP error entry (an Array). Fixed Time so serialization round-trips are
|
|
18
|
+
# deterministic.
|
|
19
|
+
let(:crl_entry) do
|
|
20
|
+
{ body: ("\x30\x82\x01\x02".b * 50), expires: Time.utc(2030, 1, 1, 12), etag: 'W/"abc123"', last_mod: nil }
|
|
21
|
+
end
|
|
22
|
+
let(:ocsp_error) { [false, "Request failed (URI: http://ocsp.example.com)", nil] }
|
|
23
|
+
|
|
24
|
+
# Stores that actually persist values (NullStore intentionally doesn't).
|
|
25
|
+
CACHING_STORES = %w[MemoryStore FileStore MemCacheStore RedisCacheStore]
|
|
26
|
+
|
|
27
|
+
def build_store(name)
|
|
28
|
+
case name
|
|
29
|
+
when "MemoryStore"
|
|
30
|
+
ActiveSupport::Cache::MemoryStore.new
|
|
31
|
+
when "FileStore"
|
|
32
|
+
ActiveSupport::Cache::FileStore.new(Dir.mktmpdir("ssl-test-cache"))
|
|
33
|
+
when "NullStore"
|
|
34
|
+
ActiveSupport::Cache::NullStore.new
|
|
35
|
+
when "MemCacheStore"
|
|
36
|
+
require "dalli"
|
|
37
|
+
ActiveSupport::Cache::MemCacheStore.new(ENV.fetch("MEMCACHE_SERVERS", "127.0.0.1:11211"))
|
|
38
|
+
when "RedisCacheStore"
|
|
39
|
+
require "redis"
|
|
40
|
+
ActiveSupport::Cache::RedisCacheStore.new(url: ENV.fetch("REDIS_URL", "redis://127.0.0.1:6379/15"))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
around do |example|
|
|
45
|
+
previous = SSLTest.cache
|
|
46
|
+
example.run
|
|
47
|
+
ensure
|
|
48
|
+
SSLTest.cache = previous
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
(CACHING_STORES + %w[NullStore]).each do |name|
|
|
52
|
+
context name do
|
|
53
|
+
before do
|
|
54
|
+
begin
|
|
55
|
+
SSLTest.cache = build_store(name)
|
|
56
|
+
rescue LoadError => e
|
|
57
|
+
skip "#{name} unavailable: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# For server-backed stores, ActiveSupport silently treats a missing
|
|
61
|
+
# server as a cache miss; probe so we skip (rather than fail) when the
|
|
62
|
+
# server isn't running.
|
|
63
|
+
if CACHING_STORES.include?(name)
|
|
64
|
+
SSLTest.cache.write("ssl-test/probe", "ok", expires_in: 60)
|
|
65
|
+
skip "#{name} server not reachable" unless SSLTest.cache.read("ssl-test/probe") == "ok"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if name == "NullStore"
|
|
70
|
+
it "acts as a no-op (the gem still works, just without caching)" do
|
|
71
|
+
SSLTest.cache.write("ssl-test/crl/x", crl_entry, expires_in: nil)
|
|
72
|
+
expect(SSLTest.cache.read("ssl-test/crl/x")).to be_nil
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
it "round-trips a CRL entry (binary body + Time serialization)" do
|
|
76
|
+
SSLTest.cache.write("ssl-test/crl/x", crl_entry, expires_in: 100 * 3600)
|
|
77
|
+
expect(SSLTest.cache.read("ssl-test/crl/x")).to eq(crl_entry)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "round-trips an OCSP error entry (Array serialization)" do
|
|
81
|
+
SSLTest.cache.write("ssl-test/ocsp-error/y", ocsp_error, expires_in: 300)
|
|
82
|
+
expect(SSLTest.cache.read("ssl-test/ocsp-error/y")).to eq(ocsp_error)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "returns nil for a missing key" do
|
|
86
|
+
expect(SSLTest.cache.read("ssl-test/ocsp/missing")).to be_nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "persists entries written with no expiry (expires_in: nil)" do
|
|
90
|
+
SSLTest.cache.write("ssl-test/crl/persist", crl_entry, expires_in: nil)
|
|
91
|
+
expect(SSLTest.cache.read("ssl-test/crl/persist")).to eq(crl_entry)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "honors expires_in" do
|
|
95
|
+
SSLTest.cache.write("ssl-test/ocsp/z", { status: 0 }, expires_in: 0.1)
|
|
96
|
+
expect(SSLTest.cache.read("ssl-test/ocsp/z")).to eq({ status: 0 })
|
|
97
|
+
sleep 0.2
|
|
98
|
+
expect(SSLTest.cache.read("ssl-test/ocsp/z")).to be_nil
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require "ssl-test"
|
|
2
|
+
|
|
3
|
+
describe SSLTest::MemoryStore do
|
|
4
|
+
subject(:store) { described_class.new }
|
|
5
|
+
|
|
6
|
+
it "round-trips written values" do
|
|
7
|
+
store.write("k", "v")
|
|
8
|
+
expect(store.read("k")).to eq("v")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "returns nil for missing keys" do
|
|
12
|
+
expect(store.read("missing")).to be_nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "expires entries after expires_in" do
|
|
16
|
+
store.write("k", "v", expires_in: -1) # already expired
|
|
17
|
+
expect(store.read("k")).to be_nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "keeps entries with no expiry" do
|
|
21
|
+
store.write("k", "v", expires_in: nil)
|
|
22
|
+
expect(store.read("k")).to eq("v")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "deletes and clears entries" do
|
|
26
|
+
store.write("a", 1)
|
|
27
|
+
store.write("b", 2)
|
|
28
|
+
store.delete("a")
|
|
29
|
+
expect(store.read("a")).to be_nil
|
|
30
|
+
expect(store.read("b")).to eq(2)
|
|
31
|
+
store.clear
|
|
32
|
+
expect(store.read("b")).to be_nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "iterates non-expired entries with #each" do
|
|
36
|
+
store.write("live", 1)
|
|
37
|
+
store.write("dead", 2, expires_in: -1)
|
|
38
|
+
expect(store.each.to_a).to eq([["live", 1]])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "#size reports a CRL/OCSP breakdown" do
|
|
42
|
+
store.write("ssl-test/crl/http://example.com/x.crl", "body")
|
|
43
|
+
store.write("ssl-test/ocsp/issuer/1", { status: 0 })
|
|
44
|
+
store.write("ssl-test/ocsp-error/issuer/2", [false, "err", nil])
|
|
45
|
+
store.write("unrelated/key", "ignored")
|
|
46
|
+
expect(store.size).to match({
|
|
47
|
+
crl: { lists: 1, bytes: be > 0 },
|
|
48
|
+
ocsp: { responses: 1, errors: 1, bytes: be > 0 }
|
|
49
|
+
})
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# #size as reported through the default store after real CRL/OCSP fetches.
|
|
54
|
+
describe "SSLTest.cache.size" do
|
|
55
|
+
before { SSLTest.cache.clear }
|
|
56
|
+
|
|
57
|
+
it "returns 0 by default" do
|
|
58
|
+
expect(SSLTest.cache.size).to eq({
|
|
59
|
+
crl: { bytes: 0, lists: 0 },
|
|
60
|
+
ocsp: { bytes: 0, errors: 0, responses: 0 }
|
|
61
|
+
})
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "returns CRL cache size properly" do
|
|
65
|
+
SSLTest.send(:follow_crl_redirects, URI("http://crl.certigna.fr/certigna.crl")) # 1.1k
|
|
66
|
+
SSLTest.send(:follow_crl_redirects, URI("http://crl3.digicert.com/DigiCertTLSHybridECCSHA3842020CA1-1.crl")) # 26k
|
|
67
|
+
expect(SSLTest.cache.size[:crl][:lists]).to eq(2)
|
|
68
|
+
expect(SSLTest.cache.size[:crl][:bytes]).to be > 2000
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "returns OCSP cache size properly" do
|
|
72
|
+
SSLTest.test("https://github.com")
|
|
73
|
+
expect(SSLTest.cache.size[:ocsp][:responses]).to eq(1)
|
|
74
|
+
expect(SSLTest.cache.size[:ocsp][:errors]).to eq(0)
|
|
75
|
+
expect(SSLTest.cache.size[:ocsp][:bytes]).to be > 0
|
|
76
|
+
expect(SSLTest.cache.size[:crl][:lists]).to eq(1)
|
|
77
|
+
expect(SSLTest.cache.size[:crl][:bytes]).to be > 100
|
|
78
|
+
end
|
|
79
|
+
end
|
data/spec/ssl-test_spec.rb
CHANGED
|
@@ -15,12 +15,11 @@ RSpec.configure do |config|
|
|
|
15
15
|
# one, and examples tagged `:retry` are re-run a few times (via rspec-retry) so
|
|
16
16
|
# transient network blips don't fail the suite.
|
|
17
17
|
config.verbose_retry = true
|
|
18
|
-
config.display_try_failure_messages = true
|
|
19
18
|
config.default_sleep_interval = 1
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
describe SSLTest do
|
|
23
|
-
before { SSLTest.
|
|
22
|
+
before { SSLTest.cache.clear }
|
|
24
23
|
|
|
25
24
|
let(:proxy_thread) { nil }
|
|
26
25
|
|
|
@@ -186,7 +185,7 @@ describe SSLTest do
|
|
|
186
185
|
expect(valid).to eq(true)
|
|
187
186
|
expect(cert).to be_a OpenSSL::X509::Certificate
|
|
188
187
|
# make sure both were used
|
|
189
|
-
expect(SSLTest.
|
|
188
|
+
expect(SSLTest.cache.size).to match({
|
|
190
189
|
crl: hash_including(lists: 1),
|
|
191
190
|
ocsp: hash_including(responses: 1, errors: 0)
|
|
192
191
|
})
|
|
@@ -237,35 +236,8 @@ describe SSLTest do
|
|
|
237
236
|
end
|
|
238
237
|
end
|
|
239
238
|
|
|
240
|
-
describe '.cache_size' do
|
|
241
|
-
before { SSLTest.flush_cache }
|
|
242
|
-
|
|
243
|
-
it "returns 0 by default" do
|
|
244
|
-
expect(SSLTest.cache_size).to eq({
|
|
245
|
-
crl: { bytes: 0, lists: 0 },
|
|
246
|
-
ocsp: { bytes: 0, errors: 0, responses: 0 }
|
|
247
|
-
})
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
it "returns CRL cache size properly" do
|
|
251
|
-
SSLTest.send(:follow_crl_redirects, URI("http://crl.certigna.fr/certigna.crl")) # 1.1k
|
|
252
|
-
SSLTest.send(:follow_crl_redirects, URI("http://crl3.digicert.com/DigiCertTLSHybridECCSHA3842020CA1-1.crl")) # 26k
|
|
253
|
-
expect(SSLTest.cache_size[:crl][:lists]).to eq(2)
|
|
254
|
-
expect(SSLTest.cache_size[:crl][:bytes]).to be > 2000
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
it "returns OCSP cache size properly" do
|
|
258
|
-
SSLTest.test("https://github.com")
|
|
259
|
-
expect(SSLTest.cache_size[:ocsp][:responses]).to eq(1)
|
|
260
|
-
expect(SSLTest.cache_size[:ocsp][:errors]).to eq(0)
|
|
261
|
-
expect(SSLTest.cache_size[:ocsp][:bytes]).to be > 150
|
|
262
|
-
expect(SSLTest.cache_size[:crl][:lists]).to eq(1)
|
|
263
|
-
expect(SSLTest.cache_size[:crl][:bytes]).to be > 500
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
|
|
267
239
|
describe '.follow_crl_redirects' do
|
|
268
|
-
before { SSLTest.
|
|
240
|
+
before { SSLTest.cache.clear }
|
|
269
241
|
# 19MB: http://crl3.digicert.com/ssca-sha2-g6.crl
|
|
270
242
|
it "fetch CRL list and updates cache" do
|
|
271
243
|
uri = URI("http://crl.certigna.fr/certigna.crl")
|
|
@@ -274,11 +246,11 @@ describe SSLTest do
|
|
|
274
246
|
expect(error).to be_nil
|
|
275
247
|
|
|
276
248
|
# Check cache status
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
expect(
|
|
280
|
-
expect(
|
|
281
|
-
expect(
|
|
249
|
+
cache_key = "ssl-test/crl/#{uri}"
|
|
250
|
+
entry = SSLTest.cache.read(cache_key)
|
|
251
|
+
expect(entry).not_to be_nil
|
|
252
|
+
expect(entry.keys).to match_array [:body, :expires, :etag, :last_mod]
|
|
253
|
+
expect(entry[:expires]).to be > (Time.now + 3590)
|
|
282
254
|
|
|
283
255
|
# Make sure we return value from cache
|
|
284
256
|
body2, error2 = nil, nil
|
|
@@ -287,7 +259,7 @@ describe SSLTest do
|
|
|
287
259
|
expect(body2).to be(body) # using cache
|
|
288
260
|
|
|
289
261
|
# Make sure we return cached value in case of 304
|
|
290
|
-
cache
|
|
262
|
+
SSLTest.cache.write(cache_key, entry.merge(expires: Time.now), expires_in: nil) # cache is now expired
|
|
291
263
|
body2, error2 = nil, nil
|
|
292
264
|
time = Benchmark.realtime { body2, error2 = SSLTest.send(:follow_crl_redirects, uri) }
|
|
293
265
|
expect(time).to be > 0.001 # a request is made
|
|
@@ -295,6 +267,33 @@ describe SSLTest do
|
|
|
295
267
|
end
|
|
296
268
|
end
|
|
297
269
|
|
|
270
|
+
describe '.cache' do
|
|
271
|
+
# Restore the default in-process store after tests that swap the backend so
|
|
272
|
+
# global state doesn't leak between examples.
|
|
273
|
+
after { SSLTest.cache = SSLTest::MemoryStore.new }
|
|
274
|
+
|
|
275
|
+
it "defaults to an in-process MemoryStore" do
|
|
276
|
+
SSLTest.instance_variable_set(:@cache, nil) # reset memoized default
|
|
277
|
+
expect(SSLTest.cache).to be_a SSLTest::MemoryStore
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "uses the configured backend for CRL and OCSP" do
|
|
281
|
+
store = SSLTest::MemoryStore.new
|
|
282
|
+
SSLTest.cache = store
|
|
283
|
+
expect(store).to receive(:write).at_least(:once).and_call_original
|
|
284
|
+
expect(store).to receive(:read).at_least(:once).and_call_original
|
|
285
|
+
SSLTest.test("https://github.com")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it "cache_size (removed in 2.0) raises pointing to cache.size" do
|
|
289
|
+
expect { SSLTest.cache_size }.to raise_error(NoMethodError, /SSLTest\.cache\.size/)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it "flush_cache (removed in 2.0) raises pointing to cache.clear" do
|
|
293
|
+
expect { SSLTest.flush_cache }.to raise_error(NoMethodError, /SSLTest\.cache\.clear/)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
298
297
|
describe '.test_cert' do
|
|
299
298
|
it "returns no error on valid SNI website" do
|
|
300
299
|
cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'fixtures/www_mycs_com_client.pem')))
|
|
@@ -439,7 +438,7 @@ describe SSLTest do
|
|
|
439
438
|
expect(valid).to eq(true)
|
|
440
439
|
expect(cert).to eq(cert)
|
|
441
440
|
# make sure both were used
|
|
442
|
-
expect(SSLTest.
|
|
441
|
+
expect(SSLTest.cache.size).to match({
|
|
443
442
|
crl: hash_including(lists: 1),
|
|
444
443
|
ocsp: hash_including(responses: 1, errors: 0)
|
|
445
444
|
})
|
data/ssl-test.gemspec
CHANGED
|
@@ -22,4 +22,10 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
spec.add_development_dependency "rspec"
|
|
23
23
|
spec.add_development_dependency "rspec-retry"
|
|
24
24
|
spec.add_development_dependency "webrick"
|
|
25
|
+
# Used to verify SSLTest.cache works with the classic Rails/ActiveSupport
|
|
26
|
+
# cache stores (MemoryStore, FileStore, NullStore, MemCacheStore via dalli,
|
|
27
|
+
# RedisCacheStore via redis).
|
|
28
|
+
spec.add_development_dependency "activesupport"
|
|
29
|
+
spec.add_development_dependency "dalli"
|
|
30
|
+
spec.add_development_dependency "redis"
|
|
25
31
|
end
|
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:
|
|
4
|
+
version: 2.0.0
|
|
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-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bundler
|
|
@@ -79,6 +79,48 @@ dependencies:
|
|
|
79
79
|
- - ">="
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: activesupport
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: dalli
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: redis
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
82
124
|
email:
|
|
83
125
|
- jobs@adrienjarthon.com
|
|
84
126
|
executables: []
|
|
@@ -94,8 +136,10 @@ files:
|
|
|
94
136
|
- Rakefile
|
|
95
137
|
- lib/ssl-test.rb
|
|
96
138
|
- lib/ssl-test/crl.rb
|
|
139
|
+
- lib/ssl-test/memory_store.rb
|
|
97
140
|
- lib/ssl-test/object_size.rb
|
|
98
141
|
- lib/ssl-test/ocsp.rb
|
|
142
|
+
- spec/cache_backends_spec.rb
|
|
99
143
|
- spec/fixtures/digicert_com_ca_bundle.pem
|
|
100
144
|
- spec/fixtures/digicert_com_client.pem
|
|
101
145
|
- spec/fixtures/expired_cert_ca_bundle.pem
|
|
@@ -116,6 +160,7 @@ files:
|
|
|
116
160
|
- spec/fixtures/www_github_com_client.pem
|
|
117
161
|
- spec/fixtures/www_mycs_com_ca_bundle.pem
|
|
118
162
|
- spec/fixtures/www_mycs_com_client.pem
|
|
163
|
+
- spec/memory_store_spec.rb
|
|
119
164
|
- spec/ssl-test_spec.rb
|
|
120
165
|
- ssl-test.gemspec
|
|
121
166
|
homepage: https://github.com/jarthod/ssl-test
|
|
@@ -140,6 +185,7 @@ rubygems_version: 3.6.2
|
|
|
140
185
|
specification_version: 4
|
|
141
186
|
summary: Test website SSL certificate validity
|
|
142
187
|
test_files:
|
|
188
|
+
- spec/cache_backends_spec.rb
|
|
143
189
|
- spec/fixtures/digicert_com_ca_bundle.pem
|
|
144
190
|
- spec/fixtures/digicert_com_client.pem
|
|
145
191
|
- spec/fixtures/expired_cert_ca_bundle.pem
|
|
@@ -160,4 +206,5 @@ test_files:
|
|
|
160
206
|
- spec/fixtures/www_github_com_client.pem
|
|
161
207
|
- spec/fixtures/www_mycs_com_ca_bundle.pem
|
|
162
208
|
- spec/fixtures/www_mycs_com_client.pem
|
|
209
|
+
- spec/memory_store_spec.rb
|
|
163
210
|
- spec/ssl-test_spec.rb
|