tr-fetch 0.9.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.
- package/LICENSE +21 -0
- package/README.md +473 -0
- package/bin/tr-curl.js +11 -0
- package/cache.js +70 -0
- package/check.js +205 -0
- package/crl.js +164 -0
- package/debug.js +33 -0
- package/download.js +100 -0
- package/errors.js +42 -0
- package/index.js +57 -0
- package/ocsp.js +265 -0
- package/options.js +146 -0
- package/package.json +66 -0
- package/pkiutils.js +44 -0
- package/tr-curl/main.js +633 -0
- package/tr-curl/options.js +193 -0
- package/tr-curl/progress.js +136 -0
- package/transport.js +150 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Timo J. Rinne
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# tr-fetch
|
|
2
|
+
|
|
3
|
+
Node.js `fetch()` with certificate revocation list (CRL) and OCSP checking.
|
|
4
|
+
Plain JavaScript, CommonJS, no build step. Requires Node.js 26 or later.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
const trFetch = require('tr-fetch');
|
|
8
|
+
|
|
9
|
+
const response = await trFetch('https://example.com/');
|
|
10
|
+
console.log(await response.text());
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
ES module default imports work too:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import trFetch from 'tr-fetch';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The function calls Node's system `fetch()` and returns its native `Response`.
|
|
20
|
+
It accepts the usual string, `URL` or `Request` input and fetch options,
|
|
21
|
+
including streaming bodies, redirects and abort signals.
|
|
22
|
+
|
|
23
|
+
## Options
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
const response = await trFetch(url, {
|
|
27
|
+
method: 'GET',
|
|
28
|
+
trFetchDebug: false,
|
|
29
|
+
trFetchCrlPolicy: {
|
|
30
|
+
disabled: false,
|
|
31
|
+
missingCrlDistributionPoint: 'ignore',
|
|
32
|
+
unreachableCrlDistributionPoint: 'reject',
|
|
33
|
+
invalidCrl: 'reject',
|
|
34
|
+
revokedCertificate: 'reject'
|
|
35
|
+
},
|
|
36
|
+
trFetchCrlCacheSize: 32,
|
|
37
|
+
trFetchCrlCacheTTL: 1800,
|
|
38
|
+
trFetchCrlCheckDepth: 0,
|
|
39
|
+
trFetchOcspPolicy: {
|
|
40
|
+
disabled: false,
|
|
41
|
+
missingOcspUri: 'ignore',
|
|
42
|
+
unreachableOcspUri: 'reject',
|
|
43
|
+
rejectedCertificate: 'reject'
|
|
44
|
+
},
|
|
45
|
+
trFetchOcspCheckDepth: 0
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
These are the defaults. Policy objects may specify only the properties to
|
|
50
|
+
change. Both checks are enabled by default and operate independently. Each
|
|
51
|
+
failure policy accepts `'ignore'`, `'warn'` or `'reject'`.
|
|
52
|
+
|
|
53
|
+
| Policy | Condition |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `missingCrlDistributionPoint` | The checked certificate has no CRL distribution points extension and no applicable override. |
|
|
56
|
+
| `unreachableCrlDistributionPoint` | No usable network location, a forbidden URI scheme, download failure, non-200 response, timeout, excessive redirects or download size. |
|
|
57
|
+
| `invalidCrl` | Malformed data, wrong issuer, bad signature, invalid signing permission, invalid dates, expired CRL, mismatched scope or unsupported CRL features. |
|
|
58
|
+
| `revokedCertificate` | The certificate serial is present in a successfully authenticated, applicable CRL. |
|
|
59
|
+
| `missingOcspUri` | No OCSP access location is advertised in the certificate's Authority Information Access extension, and no applicable override is set. |
|
|
60
|
+
| `unreachableOcspUri` | Forbidden URI scheme, download failure, non-200 HTTP response, timeout, excessive redirects or response size. |
|
|
61
|
+
| `rejectedCertificate` | OCSP reports revoked or unknown; or the response is malformed, unsuccessful, stale, incorrectly signed, unauthorized, mismatched or otherwise unsupported. |
|
|
62
|
+
|
|
63
|
+
`ignore` continues without a warning. `warn` emits a Node.js process warning and
|
|
64
|
+
continues. `reject` rejects the fetch promise before sending the HTTP request
|
|
65
|
+
on that connection. These policies apply only to revocation checks; they cannot
|
|
66
|
+
relax normal TLS verification.
|
|
67
|
+
|
|
68
|
+
### Disabling a check
|
|
69
|
+
|
|
70
|
+
Both policy objects accept `disabled`. It defaults to `false`; `undefined` and
|
|
71
|
+
`null` also mean `false`. Only boolean `true` disables the corresponding check.
|
|
72
|
+
Other types reject with `TypeError`.
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
await trFetch(url, {
|
|
76
|
+
trFetchCrlPolicy: { disabled: true },
|
|
77
|
+
trFetchOcspPolicy: { disabled: true }
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A disabled check performs no certificate revocation inspection, cache access,
|
|
82
|
+
responder lookup or warning emission. Its depth and overrides are not used.
|
|
83
|
+
Options are still validated. Disabling either check leaves the other enabled;
|
|
84
|
+
disabling both retains mandatory normal TLS trust and hostname verification.
|
|
85
|
+
|
|
86
|
+
Unknown `trFetch...` options, unknown policy keys and invalid option values
|
|
87
|
+
reject with `TypeError`. Custom options are removed before the request reaches
|
|
88
|
+
system fetch. The caller's options are not modified.
|
|
89
|
+
|
|
90
|
+
### Debug output
|
|
91
|
+
|
|
92
|
+
Set `trFetchDebug: true` to write verification diagnostics to **stderr**:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
await trFetch('https://example.com/', { trFetchDebug: true });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The default is `false` (also when omitted or `undefined`); other values,
|
|
99
|
+
including `null`, reject with `TypeError`. This option is removed before
|
|
100
|
+
calling system fetch.
|
|
101
|
+
|
|
102
|
+
Each line starts with `[trFetch debug #N]`, followed by an event and JSON details.
|
|
103
|
+
The ID groups events from the same fetch, including redirects, when calls run
|
|
104
|
+
concurrently. Certificate events identify the hostname, serial number, and
|
|
105
|
+
`leaf` at depth `0` or `intermediate CA` at depth `1` and above.
|
|
106
|
+
|
|
107
|
+
Diagnostics cover discovered CRL distribution points and OCSP URIs, overrides,
|
|
108
|
+
downloads and redirects, parsing, authenticated serial/status results, and
|
|
109
|
+
policy actions (`ignore`, `warn`, `reject`, or `continue` for successful checks).
|
|
110
|
+
Cache events report hits, misses, storage, bypass, expiration by TTL or CRL
|
|
111
|
+
validity, and LRU purges. Expiration and purge events appear when cache access
|
|
112
|
+
actually removes entries; there is no background expiration timer.
|
|
113
|
+
|
|
114
|
+
For example, a successful OCSP result includes:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
[trFetch debug #1] OCSP check completed {"hostname":"example.com","certificate":"leaf","depth":0,"serial":"2A","source":"https://ca.example/ocsp","result":"good","authenticated":true,"policy":"rejectedCertificate","configuredAction":"reject","action":"continue","nextUpdate":1790500000000}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
An unusable candidate is logged as a detected condition; its failure policy is
|
|
121
|
+
applied only if no usable alternative exists. Parsing alone does not indicate
|
|
122
|
+
authentication. Disabled checks and excluded trust anchors are identified.
|
|
123
|
+
Debugging does not change verification, caching, warnings or rejection behavior,
|
|
124
|
+
and enabling it for one fetch does not enable it for other calls.
|
|
125
|
+
|
|
126
|
+
Logged URLs omit credentials, query strings and fragments. Request headers,
|
|
127
|
+
request/response bodies and raw certificate/CRL data are not logged. Hostnames,
|
|
128
|
+
URL paths and certificate serial numbers remain visible.
|
|
129
|
+
|
|
130
|
+
### Check depth
|
|
131
|
+
|
|
132
|
+
`trFetchCrlCheckDepth` and `trFetchOcspCheckDepth` are independent top-level
|
|
133
|
+
options with the same values:
|
|
134
|
+
|
|
135
|
+
| Value | Certificates checked |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `0` or `'leaf'` (default) | The server certificate. |
|
|
138
|
+
| `1` | The server certificate and its first intermediate CA. |
|
|
139
|
+
| `n` | The server certificate and up to `n` intermediate CAs. |
|
|
140
|
+
| `'full-chain'` | All certificates exposed by the verified TLS chain, excluding its trust anchor. |
|
|
141
|
+
|
|
142
|
+
Numeric depths must be nonnegative safe integers. The trust anchor is excluded
|
|
143
|
+
at every depth; trust in it is established by Node's CA configuration. The
|
|
144
|
+
server certificate is checked whenever that check is enabled, including when
|
|
145
|
+
explicitly trusted and self-signed. Chain traversal has a defensive limit of
|
|
146
|
+
32 certificates.
|
|
147
|
+
|
|
148
|
+
The same policies apply at every selected depth. Each intermediate uses its
|
|
149
|
+
own distribution points or OCSP URIs. Overrides apply only to the server
|
|
150
|
+
certificate. A successful check does not override a rejection from the other
|
|
151
|
+
enabled check.
|
|
152
|
+
|
|
153
|
+
### Overrides
|
|
154
|
+
|
|
155
|
+
Set **one** of these options:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
await trFetch(url, {
|
|
159
|
+
trFetchCrlDistributionPointOverride: 'https://ca.example.com/current.crl'
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await trFetch(url, {
|
|
163
|
+
trFetchCrlOverride: crlBytes
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`trFetchCrlDistributionPointOverride` accepts an absolute URL string or `URL`.
|
|
168
|
+
`trFetchCrlOverride` accepts a PEM string, `Buffer`, `Uint8Array` or
|
|
169
|
+
`ArrayBuffer` containing one complete PEM or DER CRL. It is data, never a file
|
|
170
|
+
path. Supplying both options rejects with `TypeError`.
|
|
171
|
+
|
|
172
|
+
Either override bypasses the server certificate's advertised distribution
|
|
173
|
+
points. Override CRLs still require a valid signature from the actual issuer,
|
|
174
|
+
appropriate signing permission, valid dates and applicable scope. A directly
|
|
175
|
+
supplied CRL with a named issuing distribution point cannot establish a match
|
|
176
|
+
to an effective network distribution point and is rejected as `invalidCrl`.
|
|
177
|
+
Direct override data is copied and is not put in the download cache.
|
|
178
|
+
|
|
179
|
+
`trFetchOcspUriOverride` independently accepts an absolute URL string or `URL`:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
await trFetch(url, {
|
|
183
|
+
trFetchOcspUriOverride: 'https://ca.example.com/ocsp'
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
It replaces the leaf certificate's advertised OCSP URIs. It can be used with
|
|
188
|
+
either CRL override. Responses from the forced URI still require authentication
|
|
189
|
+
and a matching certificate identifier. Intermediates use their own OCSP URIs.
|
|
190
|
+
|
|
191
|
+
### Cache
|
|
192
|
+
|
|
193
|
+
Fetched and validated CRLs share an in-memory LRU cache within the loaded
|
|
194
|
+
module. Cache keys include the issuer certificate and distribution URL.
|
|
195
|
+
Certificate decisions and failed lookups are never cached. A cached CRL is
|
|
196
|
+
validated against the current certificate and current time on every use.
|
|
197
|
+
|
|
198
|
+
- `trFetchCrlCacheSize` defaults to `32` entries. Any integer **0 or less**
|
|
199
|
+
disables caching for that call.
|
|
200
|
+
- `trFetchCrlCacheTTL` defaults to `1800` **seconds**. `0` disables caching;
|
|
201
|
+
`-1` removes the TTL limit. Other values must be nonnegative safe integers.
|
|
202
|
+
- Entries expire at the earlier of their original TTL deadline and CRL
|
|
203
|
+
`nextUpdate`. Reading an entry does not extend its lifetime.
|
|
204
|
+
- A later caller's shorter TTL also limits the age of an existing entry.
|
|
205
|
+
A longer TTL cannot extend an entry's original expiry.
|
|
206
|
+
- The calling request's size limit is applied whenever the shared cache is
|
|
207
|
+
accessed or updated. Reducing the size evicts the least recently used entries.
|
|
208
|
+
- Expired entries are removed lazily on cache access. They are never reused.
|
|
209
|
+
|
|
210
|
+
With TTL `-1`, CRLs still expire at `nextUpdate` and can be evicted by LRU.
|
|
211
|
+
Concurrent misses can download the same CRL independently; cancellation and
|
|
212
|
+
policy decisions remain local to each request.
|
|
213
|
+
|
|
214
|
+
OCSP responses are not cached. Every checked certificate gets a fresh OCSP
|
|
215
|
+
request; CRL cache options affect only CRLs.
|
|
216
|
+
|
|
217
|
+
## Errors and warnings
|
|
218
|
+
|
|
219
|
+
CRL rejections are `trFetch.TrFetchCrlError` instances; OCSP rejections are
|
|
220
|
+
`trFetch.TrFetchOcspError` instances. Both are surfaced directly
|
|
221
|
+
instead of being hidden beneath fetch's generic `TypeError: fetch failed`.
|
|
222
|
+
|
|
223
|
+
| `code` | Policy |
|
|
224
|
+
|---|---|
|
|
225
|
+
| `TR_FETCH_CRL_MISSING_DISTRIBUTION_POINT` | `missingCrlDistributionPoint` |
|
|
226
|
+
| `TR_FETCH_CRL_UNREACHABLE_DISTRIBUTION_POINT` | `unreachableCrlDistributionPoint` |
|
|
227
|
+
| `TR_FETCH_CRL_INVALID` | `invalidCrl` |
|
|
228
|
+
| `TR_FETCH_CERTIFICATE_REVOKED` | `revokedCertificate` |
|
|
229
|
+
| `TR_FETCH_OCSP_MISSING_URI` | `missingOcspUri` |
|
|
230
|
+
| `TR_FETCH_OCSP_UNREACHABLE_URI` | `unreachableOcspUri` |
|
|
231
|
+
| `TR_FETCH_OCSP_CERTIFICATE_REJECTED` | `rejectedCertificate` |
|
|
232
|
+
|
|
233
|
+
Messages explain the specific failure. Certificate errors include `policyKey`,
|
|
234
|
+
`hostname`, `serialNumber`, `fingerprint256`, and, when applicable,
|
|
235
|
+
`distributionPoint` or `ocspUri`, and `cause`. OCSP rejection details include
|
|
236
|
+
`ocspStatus`: `'revoked'`, `'unknown'` or `'invalid-response'`.
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
try {
|
|
240
|
+
await trFetch(url);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if ((error instanceof trFetch.TrFetchCrlError) || (error instanceof trFetch.TrFetchOcspError)) {
|
|
243
|
+
console.error(error.code, error.message);
|
|
244
|
+
} else {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
process.on('warning', function(warning) {
|
|
250
|
+
if ([ 'TrFetchCrlWarning', 'TrFetchOcspWarning' ].includes(warning.name)) {
|
|
251
|
+
console.error(warning.code, warning.message);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Warnings use the same codes and context, with the names `TrFetchCrlWarning`
|
|
257
|
+
and `TrFetchOcspWarning`.
|
|
258
|
+
Normal TLS/network failures keep native fetch's error behavior. Cancellation
|
|
259
|
+
preserves the caller's abort reason.
|
|
260
|
+
|
|
261
|
+
## Connection and revocation behavior
|
|
262
|
+
|
|
263
|
+
An Undici connector performs ordinary Node TLS verification with
|
|
264
|
+
`rejectUnauthorized: true`. It then performs the enabled checks before handing
|
|
265
|
+
that **same socket** to system fetch. There is no separate TLS probe and no second,
|
|
266
|
+
unchecked connection carrying the application request. HTTPS redirect
|
|
267
|
+
destinations go through the same checks. HTTP requests have no certificate
|
|
268
|
+
to check; standard fetch redirect behavior otherwise applies.
|
|
269
|
+
|
|
270
|
+
Each fetch call owns an agent. Request sockets and TLS sessions are not reused;
|
|
271
|
+
redirects also establish checked connections. This avoids stale revocation
|
|
272
|
+
decisions on pooled connections, with extra handshake overhead. Response bodies
|
|
273
|
+
remain streamable while the agent closes gracefully after their consumption
|
|
274
|
+
or cancellation.
|
|
275
|
+
|
|
276
|
+
The wrapper retains Node's default CA trust, certificate validity, hostname
|
|
277
|
+
verification and TLS settings. It does not install a global dispatcher or
|
|
278
|
+
change TLS verification settings. Its Undici dependency may initialize the
|
|
279
|
+
normal global dispatcher if one has not yet been created. Explicit custom
|
|
280
|
+
dispatchers, custom global dispatchers, and unrecognized dispatcher
|
|
281
|
+
configurations are rejected: silently replacing custom trust, pinning or proxy
|
|
282
|
+
settings could weaken the application's security. Proxy agents and custom
|
|
283
|
+
per-agent TLS configuration are therefore unsupported. Default-dispatcher
|
|
284
|
+
recognition inspects Undici configuration and fails closed on unknown layouts.
|
|
285
|
+
|
|
286
|
+
System fetch drives the package's Undici agent through the dispatcher API of
|
|
287
|
+
the Undici version bundled with Node.js. Their major versions must match;
|
|
288
|
+
otherwise trFetch rejects with `TypeError` before connecting.
|
|
289
|
+
|
|
290
|
+
trFetch calls `globalThis.fetch`. Wrappers that pass the `dispatcher` option
|
|
291
|
+
on, such as most instrumentation, keep working. If a replacement drops it, the
|
|
292
|
+
request is sent over an unchecked connection. trFetch detects this and fails
|
|
293
|
+
closed: an HTTP(S) response is accepted only if both the request URL and the
|
|
294
|
+
final response URL were reached over connections that its own agent opened and
|
|
295
|
+
verified. Otherwise the fetch rejects with `TypeError`, and a CRL or OCSP
|
|
296
|
+
download counts as unreachable. The request may already have been sent by then,
|
|
297
|
+
so do not replace system fetch with an implementation that ignores
|
|
298
|
+
`dispatcher`.
|
|
299
|
+
|
|
300
|
+
CRL retrieval:
|
|
301
|
+
|
|
302
|
+
- Accepts only `http:` and `https:` URLs. Files, local paths, LDAP, LDAPS, FTP,
|
|
303
|
+
data URLs and URLs containing credentials are unreachable distribution points.
|
|
304
|
+
- Validates every redirect target and uses normal, mandatory TLS verification
|
|
305
|
+
for HTTPS downloads. Download failures use `unreachableCrlDistributionPoint`.
|
|
306
|
+
- Does not copy application cookies, authorization headers or request bodies
|
|
307
|
+
into CRL requests.
|
|
308
|
+
- Allows at most five redirects, 16 MiB of decoded response data and ten seconds
|
|
309
|
+
per download including redirects and body reading. Caller cancellation also
|
|
310
|
+
cancels CRL retrieval. At most 32 distribution URIs are tried per certificate.
|
|
311
|
+
- Allows private-network HTTP(S) endpoints, including loopback, for private PKI.
|
|
312
|
+
It does not perform filesystem or LDAP lookup.
|
|
313
|
+
- Does not recursively check the CRL download server's CRLs or OCSP status.
|
|
314
|
+
Download HTTPS trust is verified normally, and CRL contents must independently
|
|
315
|
+
authenticate under the checked certificate's issuer.
|
|
316
|
+
|
|
317
|
+
Alternative distribution URIs are tried until a valid, applicable complete CRL
|
|
318
|
+
is obtained. Only when all alternatives fail are their failure policies applied.
|
|
319
|
+
A successfully authenticated CRL listing the certificate immediately invokes
|
|
320
|
+
the revocation policy; it does not trigger a search for a different answer.
|
|
321
|
+
|
|
322
|
+
Supported CRLs are direct, complete CRLs with SHA-256, SHA-384 or SHA-512
|
|
323
|
+
signatures supported by PKIjs/Web Crypto, including RSA and ECDSA. Validation
|
|
324
|
+
checks issuer binding, signature algorithm consistency, `cRLSign` key usage
|
|
325
|
+
when present, authority key identifiers when available, dates and scope.
|
|
326
|
+
`nextUpdate` is required. Matching named issuing distribution points and
|
|
327
|
+
user/CA certificate scopes are supported.
|
|
328
|
+
|
|
329
|
+
Delta CRLs, indirect CRLs, reason-limited distribution points or CRLs,
|
|
330
|
+
attribute-certificate CRLs, unsupported critical extensions and weak or
|
|
331
|
+
unsupported signature algorithms use `invalidCrl`. This implementation does
|
|
332
|
+
not merge delta CRLs.
|
|
333
|
+
|
|
334
|
+
### OCSP behavior
|
|
335
|
+
|
|
336
|
+
The library reads OCSP URIs from the certificate's Authority Information Access
|
|
337
|
+
extension and sends a DER request using HTTP POST with
|
|
338
|
+
`Content-Type: application/ocsp-request`. Requests contain the certificate's
|
|
339
|
+
serial and issuer hashes, with a fresh 32-byte nonce. SHA-1 is used for the
|
|
340
|
+
standard issuer identifier; response signatures must use supported SHA-256,
|
|
341
|
+
SHA-384 or SHA-512 algorithms.
|
|
342
|
+
|
|
343
|
+
Only HTTP(S) responder URIs are accepted. Each lookup has a ten-second deadline
|
|
344
|
+
and a 1 MiB response limit. HTTP 307/308 redirects preserve the POST body, with
|
|
345
|
+
at most five redirects; other redirect codes use `unreachableOcspUri`. Every
|
|
346
|
+
redirect URI is validated. Caller cancellation interrupts the lookup. Up to
|
|
347
|
+
32 advertised URIs are tried per certificate. Requests contain no application
|
|
348
|
+
authorization headers, cookies or body. Private-network HTTP(S) is allowed.
|
|
349
|
+
|
|
350
|
+
A successful response must contain exactly one matching certificate identifier
|
|
351
|
+
and a signature authorized by that certificate's actual issuer. An issuer may
|
|
352
|
+
sign directly, or delegate to a certificate it issued directly. A delegated
|
|
353
|
+
responder must have OCSP signing extended key usage, digital-signature key usage
|
|
354
|
+
when present, current certificate validity, and `id-pkix-ocsp-nocheck`. Delegates
|
|
355
|
+
without no-check are currently unsupported because their own revocation status
|
|
356
|
+
would require a separate validation path. Weak delegate certificate signatures
|
|
357
|
+
and RSA keys shorter than 2048 bits are rejected.
|
|
358
|
+
|
|
359
|
+
Response `producedAt` and `thisUpdate` must not be in the future. `nextUpdate`,
|
|
360
|
+
when present, must not have passed. When it is absent, the response is accepted
|
|
361
|
+
for at most five minutes after `thisUpdate`. Signer certificate expiry also
|
|
362
|
+
bounds validity. Unknown critical extensions and duplicate or mismatched
|
|
363
|
+
certificate identifiers reject. If a responder returns a nonce, it must match
|
|
364
|
+
the request; responders that omit it remain subject to the validity-time checks.
|
|
365
|
+
|
|
366
|
+
Unusable alternatives are tried until an authenticated status is obtained.
|
|
367
|
+
An authenticated revoked or unknown status immediately invokes
|
|
368
|
+
`rejectedCertificate`; the client does not seek a different answer elsewhere.
|
|
369
|
+
Malformed, unsuccessful or unauthenticated responses also use that policy when
|
|
370
|
+
no usable alternative exists, with a distinct explanatory message.
|
|
371
|
+
|
|
372
|
+
OCSP responder HTTPS connections receive normal TLS verification. Their own
|
|
373
|
+
revocation endpoints are not recursively queried. This is active OCSP lookup;
|
|
374
|
+
TLS-stapled responses are not consumed.
|
|
375
|
+
|
|
376
|
+
The transport uses Node's documented
|
|
377
|
+
[fetch dispatcher interface](https://nodejs.org/api/globals.html#custom-dispatcher)
|
|
378
|
+
and [TLS certificate inspection](https://nodejs.org/api/tls.html#tlssocketgetpeercertificatedetailed).
|
|
379
|
+
CRL format and scope rules follow the applicable parts of
|
|
380
|
+
[RFC 5280](https://www.rfc-editor.org/rfc/rfc5280). OCSP verification follows
|
|
381
|
+
the applicable parts of [RFC 6960](https://www.rfc-editor.org/rfc/rfc6960).
|
|
382
|
+
|
|
383
|
+
## tr-curl
|
|
384
|
+
|
|
385
|
+
The package installs `tr-curl`, a small `curl` replacement built on trFetch.
|
|
386
|
+
It is mainly a test tool: every HTTPS connection gets trFetch's CRL and OCSP
|
|
387
|
+
checks. Only `http:` and `https:` URLs are supported; a URL without a scheme
|
|
388
|
+
defaults to `http://`.
|
|
389
|
+
|
|
390
|
+
```sh
|
|
391
|
+
npx tr-curl -v https://example.com/
|
|
392
|
+
tr-curl -fsSL -o page.html https://example.com/
|
|
393
|
+
tr-curl --json '{"a":1}' -u user:password https://api.example.com/items
|
|
394
|
+
tr-curl --cacert private-ca.pem --crlfile current.crl https://internal.example/
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`tr-curl --help` lists the options. Their meaning follows curl:
|
|
398
|
+
|
|
399
|
+
| Area | Options |
|
|
400
|
+
|---|---|
|
|
401
|
+
| Request | `-X/--request`, `-H/--header`, `-A/--user-agent`, `-e/--referer`, `-r/--range`, `--etag-compare`, `-G/--get`, `-I/--head` |
|
|
402
|
+
| Data | `-d/--data`, `--data-ascii`, `--data-binary`, `--data-raw`, `--data-urlencode`, `--json` |
|
|
403
|
+
| Authentication | `-u/--user`, `--oauth2-bearer`, credentials in the URL |
|
|
404
|
+
| Redirects | `-L/--location`, `--location-trusted`, `--max-redirs` |
|
|
405
|
+
| Output | `-o/--output`, `-O/--remote-name`, `--remote-name-all`, `--output-dir`, `--create-dirs`, `--remove-on-error`, `-i/--include` |
|
|
406
|
+
| Failure and limits | `-f/--fail`, `--fail-with-body`, `--fail-early`, `--max-filesize`, `-m/--max-time` |
|
|
407
|
+
| Messages | `-v/--verbose`, `-s/--silent`, `-S/--show-error`, `--no-progress-meter`, `-#/--progress-bar`, `-h/--help`, `-V/--version` |
|
|
408
|
+
| TLS | `-k/--insecure`, `--cacert`, `--crlfile`, `-1/--tlsv1`, `--tlsv1.0` … `--tlsv1.3`, `--tls-max`, `--ciphers`, `--tls13-ciphers` |
|
|
409
|
+
| trFetch | `--trfetch-options <json>` |
|
|
410
|
+
|
|
411
|
+
`--verbose` prints the request and response headers, prefixed with `>` and
|
|
412
|
+
`<` like curl, and also enables `trFetchDebug`, so the revocation diagnostics
|
|
413
|
+
appear on stderr. `--trfetch-options` takes a JSON object of trFetch options,
|
|
414
|
+
for example `'{"trFetchCrlCheckDepth":"full-chain"}'`. The option can be
|
|
415
|
+
repeated, and later objects override earlier keys. `--crlfile` sets
|
|
416
|
+
`trFetchCrlOverride`. `--cacert` replaces Node's default CA certificates for
|
|
417
|
+
the process.
|
|
418
|
+
|
|
419
|
+
`--insecure` cannot be implemented through trFetch, which never relaxes TLS
|
|
420
|
+
verification. With `-k`, tr-curl uses plain fetch with an unverified TLS
|
|
421
|
+
connection instead, so no CRL or OCSP checks are made.
|
|
422
|
+
|
|
423
|
+
Exit codes follow curl, for example: 1 unsupported protocol, 2 usage error,
|
|
424
|
+
3 malformed URL, 6 unresolvable host, 7 connection failure, 18 partial
|
|
425
|
+
transfer, 22 HTTP error with `--fail`, 23 write error, 28 timeout, 35 TLS
|
|
426
|
+
handshake failure, 47 too many redirects, 60 certificate verification failure
|
|
427
|
+
or CRL rejection, 63 maximum file size exceeded, and 91 OCSP rejection.
|
|
428
|
+
|
|
429
|
+
Differences from curl:
|
|
430
|
+
|
|
431
|
+
- Options are parsed with [Optist](https://www.npmjs.com/package/optist).
|
|
432
|
+
`--name=value` is accepted, short options that take an argument cannot be
|
|
433
|
+
combined with other short options (`-o file`, not `-ofile`), and single-value
|
|
434
|
+
options may be given only once. Options may follow URLs, and `--` ends
|
|
435
|
+
option parsing.
|
|
436
|
+
- The `-o` and `-O` options cannot be mixed. `-o` names pair with URLs in
|
|
437
|
+
order, and `-O` applies to as many URLs as it is given.
|
|
438
|
+
- Requests use HTTP/1.1 through fetch. Fetch sets `Host`, `Connection` and
|
|
439
|
+
`Content-Length` itself, adds `Accept-Language`, `Sec-Fetch-Mode` and
|
|
440
|
+
`Accept-Encoding`, and decodes compressed responses. `-H "Name:"` removes
|
|
441
|
+
only headers that tr-curl itself would add. Response header names appear in
|
|
442
|
+
lowercase.
|
|
443
|
+
- TLS-SRP (`--tlsuser`, `--tlspassword`, `--tlsauthtype`) and other TLS
|
|
444
|
+
backend features that Node.js does not provide are not supported. URL
|
|
445
|
+
globbing, cookies, proxies, uploads and non-HTTP protocols are not
|
|
446
|
+
implemented either.
|
|
447
|
+
|
|
448
|
+
## Development
|
|
449
|
+
|
|
450
|
+
```sh
|
|
451
|
+
npm install
|
|
452
|
+
npm test
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Enable fetch diagnostics during tests with:
|
|
456
|
+
|
|
457
|
+
```sh
|
|
458
|
+
TR_FETCH_TEST_DEBUG=yes npm test
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
`TR_FETCH_TEST_DEBUG` accepts `y`, `yes`, `true` or `1`, case-insensitively.
|
|
462
|
+
Other values (or an unset variable) leave test debugging off. This sets
|
|
463
|
+
`trFetchDebug: true` by default for test fetch calls; explicit options in tests
|
|
464
|
+
that verify debugging behavior still take precedence. The variable affects
|
|
465
|
+
only the test suite, not the library's runtime defaults.
|
|
466
|
+
|
|
467
|
+
Tests use Node's built-in test runner and generate temporary signing keys,
|
|
468
|
+
certificates, CRLs and OCSP responses in memory. Integration tests run local
|
|
469
|
+
HTTP/HTTPS servers; they do not require public network access.
|
|
470
|
+
|
|
471
|
+
Runtime dependencies: `undici` for the transport adapter, `pkijs` and `asn1js`
|
|
472
|
+
for X.509/CRL/OCSP parsing and signature verification, and `optist` for
|
|
473
|
+
tr-curl's command line. No compiler or external OpenSSL executable is required.
|
package/bin/tr-curl.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { main } = require('../tr-curl/main');
|
|
5
|
+
|
|
6
|
+
main(process.argv.slice(2)).then(function(code) {
|
|
7
|
+
process.exitCode = code;
|
|
8
|
+
}, function(error) {
|
|
9
|
+
process.stderr.write(`tr-curl: ${error?.stack ?? error}\n`);
|
|
10
|
+
process.exitCode = 2;
|
|
11
|
+
});
|
package/cache.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { debugUrl } = require('./debug');
|
|
4
|
+
|
|
5
|
+
function cacheEntry(key) {
|
|
6
|
+
const separator = key.indexOf(':');
|
|
7
|
+
return { issuer: key.slice(0, separator), source: debugUrl(key.slice(separator + 1)) };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Cache verified CRLs, never a policy decision or a failed lookup. A caller's
|
|
11
|
+
// shorter TTL also applies to entries populated by an earlier caller.
|
|
12
|
+
class CrlCache {
|
|
13
|
+
#entries = new Map();
|
|
14
|
+
|
|
15
|
+
prune(size, now = Date.now(), debug) {
|
|
16
|
+
for (const [key, entry] of this.#entries) {
|
|
17
|
+
if (now >= entry.expiresAt) {
|
|
18
|
+
this.#entries.delete(key);
|
|
19
|
+
debug?.('CRL cache expired', { ...cacheEntry(key),
|
|
20
|
+
reason: (entry.expiresAt === entry.nextUpdate) ? 'CRL nextUpdate' : 'TTL', expiresAt: entry.expiresAt });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
while (this.#entries.size > Math.max(0, size)) {
|
|
24
|
+
const key = this.#entries.keys().next().value;
|
|
25
|
+
this.#entries.delete(key);
|
|
26
|
+
debug?.('CRL cache purged', { ...cacheEntry(key), reason: (size <= 0) ? 'cache disabled' : 'LRU capacity', size });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get(key, size, ttl, now = Date.now(), debug) {
|
|
31
|
+
this.prune(size, now, debug);
|
|
32
|
+
if ((size <= 0) || (ttl === 0)) {
|
|
33
|
+
debug?.('CRL cache bypassed', { ...cacheEntry(key), reason: 'cache disabled', size, ttl });
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const entry = this.#entries.get(key);
|
|
37
|
+
if (! entry) {
|
|
38
|
+
debug?.('CRL cache miss', cacheEntry(key));
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if ((ttl !== -1) && ((now - entry.fetchedAt) >= (ttl * 1000))) {
|
|
42
|
+
this.#entries.delete(key);
|
|
43
|
+
debug?.('CRL cache expired', { ...cacheEntry(key), reason: 'caller TTL', ttl, ageSeconds: (now - entry.fetchedAt) / 1000 });
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
this.#entries.delete(key);
|
|
47
|
+
this.#entries.set(key, entry);
|
|
48
|
+
debug?.('CRL cache hit', { ...cacheEntry(key), ageSeconds: (now - entry.fetchedAt) / 1000, expiresAt: entry.expiresAt });
|
|
49
|
+
return entry.value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
set(key, value, nextUpdate, fetchedAt, size, ttl, now = Date.now(), debug) {
|
|
53
|
+
this.prune(size, now, debug);
|
|
54
|
+
if ((size <= 0) || (ttl === 0)) {
|
|
55
|
+
debug?.('CRL cache store skipped', { ...cacheEntry(key), reason: 'cache disabled', size, ttl });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const expiresAt = Math.min(nextUpdate, (ttl === -1) ? Infinity : fetchedAt + (ttl * 1000));
|
|
59
|
+
if (expiresAt <= now) {
|
|
60
|
+
debug?.('CRL cache store skipped', { ...cacheEntry(key), reason: 'already expired', expiresAt });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.#entries.delete(key);
|
|
64
|
+
this.#entries.set(key, { value, fetchedAt, expiresAt, nextUpdate });
|
|
65
|
+
debug?.('CRL cache stored', { ...cacheEntry(key), expiresAt, nextUpdate, ttl, size });
|
|
66
|
+
this.prune(size, now, debug);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = CrlCache;
|