tr-fetch 0.9.1 → 0.9.3
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/README.md +37 -10
- package/check.js +9 -3
- package/crl.js +121 -32
- package/download.js +6 -6
- package/index.js +1 -0
- package/options.js +10 -1
- package/package.json +1 -1
- package/pkiutils.js +58 -4
- package/tr-curl/options.js +32 -3
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ const response = await trFetch(url, {
|
|
|
28
28
|
trFetchDebug: false,
|
|
29
29
|
trFetchCrlPolicy: {
|
|
30
30
|
disabled: false,
|
|
31
|
+
maxCrlBytes: 16777216,
|
|
31
32
|
missingCrlDistributionPoint: 'ignore',
|
|
32
33
|
unreachableCrlDistributionPoint: 'reject',
|
|
33
34
|
invalidCrl: 'reject',
|
|
@@ -65,6 +66,23 @@ continues. `reject` rejects the fetch promise before sending the HTTP request
|
|
|
65
66
|
on that connection. These policies apply only to revocation checks; they cannot
|
|
66
67
|
relax normal TLS verification.
|
|
67
68
|
|
|
69
|
+
### CRL size limit
|
|
70
|
+
|
|
71
|
+
`trFetchCrlPolicy.maxCrlBytes` is the largest CRL accepted, in bytes. It
|
|
72
|
+
defaults to `16777216` (16 MiB) and must be a positive safe integer; there is
|
|
73
|
+
no value for unlimited. Some public CAs publish much larger CRLs, so raise it
|
|
74
|
+
explicitly when needed:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
await trFetch('https://example.com/', { trFetchCrlPolicy: { maxCrlBytes: 64 * 1024 * 1024 } });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A larger download is an unreachable distribution point; larger
|
|
81
|
+
`trFetchCrlOverride` data is an invalid CRL. A cached CRL above the caller's
|
|
82
|
+
limit is not used, even if an earlier caller with a higher limit cached it.
|
|
83
|
+
Revoked entries are scanned in place, so memory use stays close to the CRL's
|
|
84
|
+
size.
|
|
85
|
+
|
|
68
86
|
### Disabling a check
|
|
69
87
|
|
|
70
88
|
Both policy objects accept `disabled`. It defaults to `false`; `undefined` and
|
|
@@ -305,12 +323,9 @@ CRL retrieval:
|
|
|
305
323
|
for HTTPS downloads. Download failures use `unreachableCrlDistributionPoint`.
|
|
306
324
|
- Does not copy application cookies, authorization headers or request bodies
|
|
307
325
|
into CRL requests.
|
|
308
|
-
- Allows at most five redirects,
|
|
326
|
+
- Allows at most five redirects, `maxCrlBytes` of decoded response data and ten seconds
|
|
309
327
|
per download including redirects and body reading. Caller cancellation also
|
|
310
328
|
cancels CRL retrieval. At most 32 distribution URIs are tried per certificate.
|
|
311
|
-
- Parses at most 1,000,000 ASN.1 nodes per CRL, enough for several hundred
|
|
312
|
-
thousand revoked entries. This bounds the memory that a hostile CRL can use
|
|
313
|
-
(about 0.6 GB); larger CRLs are invalid.
|
|
314
329
|
- Allows private-network HTTP(S) endpoints, including loopback, for private PKI.
|
|
315
330
|
It does not perform filesystem or LDAP lookup.
|
|
316
331
|
- Does not recursively check the CRL download server's CRLs or OCSP status.
|
|
@@ -416,15 +431,27 @@ tr-curl --cacert private-ca.pem --crlfile current.crl https://internal.example/
|
|
|
416
431
|
| Failure and limits | `-f/--fail`, `--fail-with-body`, `--fail-early`, `--max-filesize`, `-m/--max-time` |
|
|
417
432
|
| Messages | `-v/--verbose`, `-s/--silent`, `-S/--show-error`, `--no-progress-meter`, `-#/--progress-bar`, `-h/--help`, `-V/--version` |
|
|
418
433
|
| TLS | `-k/--insecure`, `--cacert`, `--crlfile`, `-1/--tlsv1`, `--tlsv1.0` … `--tlsv1.3`, `--tls-max`, `--ciphers`, `--tls13-ciphers` |
|
|
419
|
-
| trFetch | `--
|
|
434
|
+
| trFetch | `--tr-fetch-max-crl-bytes`, `--tr-fetch-crl-url`, `--tr-fetch-ocsp-url`, `--tr-fetch-options` |
|
|
420
435
|
|
|
421
436
|
`--verbose` prints the request and response headers, prefixed with `>` and
|
|
422
437
|
`<` like curl, and also enables `trFetchDebug`, so the revocation diagnostics
|
|
423
|
-
appear on stderr. `--
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
438
|
+
appear on stderr. `--cacert` replaces Node's default CA certificates for the
|
|
439
|
+
process.
|
|
440
|
+
|
|
441
|
+
The `--tr-fetch-*` options set trFetch options directly:
|
|
442
|
+
|
|
443
|
+
| Option | trFetch option |
|
|
444
|
+
|---|---|
|
|
445
|
+
| `--tr-fetch-max-crl-bytes <bytes>` | `trFetchCrlPolicy.maxCrlBytes`, a positive integer |
|
|
446
|
+
| `--tr-fetch-crl-url <url>` | `trFetchCrlDistributionPointOverride` |
|
|
447
|
+
| `--tr-fetch-ocsp-url <url>` | `trFetchOcspUriOverride` |
|
|
448
|
+
| `--crlfile <file>` | `trFetchCrlOverride`, read from the file |
|
|
449
|
+
|
|
450
|
+
`--tr-fetch-options` takes a JSON object of any trFetch options, for example
|
|
451
|
+
`'{"trFetchCrlCheckDepth":"full-chain"}'`. It can be repeated; later objects
|
|
452
|
+
replace earlier keys. The options above take precedence over the same
|
|
453
|
+
settings in it, whatever their order, and `--tr-fetch-max-crl-bytes` is merged
|
|
454
|
+
into its `trFetchCrlPolicy` rather than replacing it.
|
|
428
455
|
|
|
429
456
|
`--insecure` cannot be implemented through trFetch, which never relaxes TLS
|
|
430
457
|
verification. With `-k`, tr-curl uses plain fetch with an unverified TLS
|
package/check.js
CHANGED
|
@@ -92,8 +92,14 @@ async function checkCertificate(peer, hostname, options, signal, override, debug
|
|
|
92
92
|
const url = networkUrl(source);
|
|
93
93
|
key = issuerId + ':' + url.href;
|
|
94
94
|
crl = cache.get(key, options.cacheSize, options.cacheTTL, Date.now(), debug);
|
|
95
|
+
// A CRL cached under a higher limit must not bypass this one.
|
|
96
|
+
if (crl && (crl.size > options.policy.maxCrlBytes)) {
|
|
97
|
+
debug?.('CRL cache entry not used', { source: debugUrl(url), reason: 'exceeds maxCrlBytes',
|
|
98
|
+
bytes: crl.size, maxCrlBytes: options.policy.maxCrlBytes });
|
|
99
|
+
crl = undefined;
|
|
100
|
+
}
|
|
95
101
|
if (! crl) {
|
|
96
|
-
bytes = await downloadCrl(url, signal, debug);
|
|
102
|
+
bytes = await downloadCrl(url, signal, debug, options.policy.maxCrlBytes);
|
|
97
103
|
}
|
|
98
104
|
} catch (cause) {
|
|
99
105
|
signal?.throwIfAborted();
|
|
@@ -105,9 +111,9 @@ async function checkCertificate(peer, hostname, options, signal, override, debug
|
|
|
105
111
|
let result;
|
|
106
112
|
try {
|
|
107
113
|
if (! crl) {
|
|
108
|
-
crl = parseCrl(bytes);
|
|
114
|
+
crl = parseCrl(bytes, options.policy.maxCrlBytes);
|
|
109
115
|
debug?.('CRL parsed', { source: (source === undefined) ? 'trFetchCrlOverride' : debugUrl(source),
|
|
110
|
-
revokedEntries: crl.
|
|
116
|
+
revokedEntries: crl.revokedCount });
|
|
111
117
|
}
|
|
112
118
|
result = await validateCrl(crl, certificate, issuer, point.urls);
|
|
113
119
|
} catch (cause) {
|
package/crl.js
CHANGED
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const asn1 = require('asn1js');
|
|
4
4
|
const pki = require('pkijs');
|
|
5
|
-
const { cryptoEngine, parseDer,
|
|
5
|
+
const { cryptoEngine, parseDer, derElement, derHeader, oidString, extensionsById, extensionValue,
|
|
6
|
+
parseCertificate } = require('./pkiutils');
|
|
7
|
+
const { DEFAULT_MAX_CRL_BYTES } = require('./options');
|
|
6
8
|
|
|
7
|
-
const
|
|
8
|
-
// A revoked entry takes three to seven ASN.1 nodes, so this allows CRLs of
|
|
9
|
-
// several hundred thousand entries. Unlike the byte limit, it also bounds the
|
|
10
|
-
// memory used by hostile input made of minimal nodes (about 0.6 GB).
|
|
11
|
-
const MAX_CRL_NODES = 1000000;
|
|
9
|
+
const TIME_TAGS = [ 0x17, 0x18 ];
|
|
12
10
|
|
|
13
11
|
function distributionPoints(certificate) {
|
|
14
12
|
const extension = extensionsById(certificate.extensions).get('2.5.29.31');
|
|
@@ -28,10 +26,11 @@ function distributionPoints(certificate) {
|
|
|
28
26
|
});
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
function parseCrl(bytes) {
|
|
32
|
-
if (bytes.length >
|
|
33
|
-
throw new Error(
|
|
29
|
+
function parseCrl(bytes, maxBytes = DEFAULT_MAX_CRL_BYTES) {
|
|
30
|
+
if (bytes.length > maxBytes) {
|
|
31
|
+
throw new Error(`CRL exceeds maxCrlBytes (${maxBytes} bytes)`);
|
|
34
32
|
}
|
|
33
|
+
const size = bytes.length;
|
|
35
34
|
if (bytes.toString('ascii', 0, 32).trimStart().startsWith('-----BEGIN')) {
|
|
36
35
|
const match = /^\s*-----BEGIN X509 CRL-----\s*([A-Za-z0-9+/=\r\n\t ]+)\s*-----END X509 CRL-----\s*$/.exec(bytes.toString('ascii'));
|
|
37
36
|
if (! match) {
|
|
@@ -43,7 +42,110 @@ function parseCrl(bytes) {
|
|
|
43
42
|
throw new Error('Malformed base64 in PEM CRL');
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
|
-
|
|
45
|
+
const outer = derElement(bytes, 0);
|
|
46
|
+
if (outer.end !== bytes.length) {
|
|
47
|
+
throw new Error('Malformed ASN.1 data: trailing bytes');
|
|
48
|
+
}
|
|
49
|
+
const tbs = derElement(bytes, outer.start, outer.end);
|
|
50
|
+
if ((outer.tag !== 0x30) || (tbs.tag !== 0x30)) {
|
|
51
|
+
throw new Error('Malformed CRL structure');
|
|
52
|
+
}
|
|
53
|
+
const fields = [];
|
|
54
|
+
for (let offset = tbs.start; offset < tbs.end; offset = fields.at(-1).end) {
|
|
55
|
+
fields.push(derElement(bytes, offset, tbs.end));
|
|
56
|
+
}
|
|
57
|
+
// version, signature, issuer, thisUpdate, nextUpdate, revokedCertificates
|
|
58
|
+
let index = (fields[0]?.tag === 0x02) ? 3 : 2;
|
|
59
|
+
index += TIME_TAGS.includes(fields[index + 1]?.tag) ? 2 : 1;
|
|
60
|
+
const entries = (fields[index]?.tag === 0x30) ? fields[index] : undefined;
|
|
61
|
+
// Decoding every revoked entry into an ASN.1 object tree takes hundreds of
|
|
62
|
+
// bytes of memory per encoded byte, so large CRLs could exhaust the heap.
|
|
63
|
+
// PKI.js decodes the rest; the entries are scanned in place.
|
|
64
|
+
const headerFields = fields.filter(x => x !== entries).map(x => bytes.subarray(x.offset, x.end));
|
|
65
|
+
const headerLength = headerFields.reduce((sum, x) => sum + x.length, 0);
|
|
66
|
+
const trailer = bytes.subarray(tbs.end, outer.end);
|
|
67
|
+
const tbsHeader = derHeader(0x30, headerLength);
|
|
68
|
+
const crl = parseDer(Buffer.concat([ derHeader(0x30, tbsHeader.length + headerLength + trailer.length),
|
|
69
|
+
tbsHeader, ...headerFields, trailer ]), pki.CertificateRevocationList);
|
|
70
|
+
const scan = scanEntries(bytes, entries);
|
|
71
|
+
return { crl, size, bytes, tbs: bytes.subarray(tbs.offset, tbs.end), entries,
|
|
72
|
+
revokedCount: scan.count, entryProblem: scan.problem };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function checkEntryExtensions(bytes, list) {
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
let problem;
|
|
78
|
+
for (let offset = list.start; offset < list.end;) {
|
|
79
|
+
const extension = derElement(bytes, offset, list.end);
|
|
80
|
+
offset = extension.end;
|
|
81
|
+
const id = derElement(bytes, extension.start, extension.end);
|
|
82
|
+
let value = derElement(bytes, id.end, extension.end);
|
|
83
|
+
let critical = false;
|
|
84
|
+
if (value.tag === 0x01) {
|
|
85
|
+
critical = (value.end > value.start) && (bytes[value.start] !== 0);
|
|
86
|
+
value = derElement(bytes, value.end, extension.end);
|
|
87
|
+
}
|
|
88
|
+
if ((extension.tag !== 0x30) || (id.tag !== 0x06) || (value.tag !== 0x04) || (value.end !== extension.end)) {
|
|
89
|
+
throw new Error('Malformed CRL entry extension');
|
|
90
|
+
}
|
|
91
|
+
const extnID = oidString(bytes.subarray(id.start, id.end));
|
|
92
|
+
if (seen.has(extnID)) {
|
|
93
|
+
problem ??= `Duplicate extension ${extnID}`;
|
|
94
|
+
}
|
|
95
|
+
seen.add(extnID);
|
|
96
|
+
if (extnID === '2.5.29.29') {
|
|
97
|
+
problem ??= 'Indirect CRL certificateIssuer entries are unsupported';
|
|
98
|
+
} else if (critical) {
|
|
99
|
+
problem ??= `Unsupported critical CRL entry extension ${extnID}`;
|
|
100
|
+
} else if (extnID === '2.5.29.21') {
|
|
101
|
+
const reason = derElement(bytes, value.start, value.end);
|
|
102
|
+
let code = 0;
|
|
103
|
+
for (let i = reason.start; i < reason.end; i++) {
|
|
104
|
+
code = (code * 256) + bytes[i];
|
|
105
|
+
}
|
|
106
|
+
if ((reason.tag !== 0x0a) || (reason.end !== value.end) || (reason.end === reason.start) || (code === 8)) {
|
|
107
|
+
problem ??= 'Invalid revocation reason in a complete CRL';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return problem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Walk the revoked entries without decoding them into objects. Structural
|
|
115
|
+
// errors throw; the first unsupported entry is reported for use after the CRL
|
|
116
|
+
// is authenticated. With a serial, also report whether it is listed.
|
|
117
|
+
function scanEntries(bytes, entries, serial) {
|
|
118
|
+
let count = 0;
|
|
119
|
+
let problem;
|
|
120
|
+
let revoked = false;
|
|
121
|
+
for (let offset = entries?.start; offset < entries?.end;) {
|
|
122
|
+
const entry = derElement(bytes, offset, entries.end);
|
|
123
|
+
offset = entry.end;
|
|
124
|
+
const number = derElement(bytes, entry.start, entry.end);
|
|
125
|
+
const date = derElement(bytes, number.end, entry.end);
|
|
126
|
+
let next = date.end;
|
|
127
|
+
if ((entry.tag !== 0x30) || (number.tag !== 0x02) || ! TIME_TAGS.includes(date.tag)) {
|
|
128
|
+
throw new Error('Malformed revoked certificate entry');
|
|
129
|
+
}
|
|
130
|
+
if (next < entry.end) {
|
|
131
|
+
const extensions = derElement(bytes, next, entry.end);
|
|
132
|
+
if (extensions.tag !== 0x30) {
|
|
133
|
+
throw new Error('Malformed revoked certificate entry');
|
|
134
|
+
}
|
|
135
|
+
if (serial === undefined) {
|
|
136
|
+
problem ??= checkEntryExtensions(bytes, extensions);
|
|
137
|
+
}
|
|
138
|
+
next = extensions.end;
|
|
139
|
+
}
|
|
140
|
+
if (next !== entry.end) {
|
|
141
|
+
throw new Error('Malformed revoked certificate entry');
|
|
142
|
+
}
|
|
143
|
+
count++;
|
|
144
|
+
if ((serial !== undefined) && bytes.subarray(number.start, number.end).equals(serial)) {
|
|
145
|
+
revoked = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { count, problem, revoked };
|
|
47
149
|
}
|
|
48
150
|
|
|
49
151
|
function checkValidity(crl, now = Date.now()) {
|
|
@@ -95,7 +197,8 @@ function checkScope(crl, certificate, urls) {
|
|
|
95
197
|
return extensions;
|
|
96
198
|
}
|
|
97
199
|
|
|
98
|
-
async function validateCrl(
|
|
200
|
+
async function validateCrl(parsed, certificate, issuer, urls, now = Date.now()) {
|
|
201
|
+
const crl = parsed.crl;
|
|
99
202
|
const nextUpdate = checkValidity(crl, now);
|
|
100
203
|
const extensions = checkScope(crl, certificate, urls);
|
|
101
204
|
if (! crl.issuer.isEqual(certificate.issuer) || ! crl.issuer.isEqual(issuer.subject)) {
|
|
@@ -138,31 +241,17 @@ async function validateCrl(crl, certificate, issuer, urls, now = Date.now()) {
|
|
|
138
241
|
if (crlNumber && ! (extensionValue(crlNumber) instanceof asn1.Integer)) {
|
|
139
242
|
throw new Error('Malformed CRL number');
|
|
140
243
|
}
|
|
141
|
-
|
|
244
|
+
// The signature covers the original TBS bytes, including the entries.
|
|
245
|
+
if (! await cryptoEngine.verifyWithPublicKey(parsed.tbs, crl.signatureValue, issuer.subjectPublicKeyInfo, crl.signatureAlgorithm)) {
|
|
142
246
|
throw new Error('CRL signature verification failed');
|
|
143
247
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
for (const extension of extensionsById(entry.crlEntryExtensions?.extensions).values()) {
|
|
147
|
-
if (extension.extnID === '2.5.29.29') {
|
|
148
|
-
throw new Error('Indirect CRL certificateIssuer entries are unsupported');
|
|
149
|
-
}
|
|
150
|
-
if (extension.critical) {
|
|
151
|
-
throw new Error(`Unsupported critical CRL entry extension ${extension.extnID}`);
|
|
152
|
-
}
|
|
153
|
-
if (extension.extnID === '2.5.29.21') {
|
|
154
|
-
const reason = extensionValue(extension);
|
|
155
|
-
if (! (reason instanceof asn1.Enumerated) || (reason.valueBlock.valueDec === 8)) {
|
|
156
|
-
throw new Error('Invalid revocation reason in a complete CRL');
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (entry.userCertificate.isEqual(certificate.serialNumber)) {
|
|
161
|
-
revoked = true;
|
|
162
|
-
}
|
|
248
|
+
if (parsed.entryProblem) {
|
|
249
|
+
throw new Error(parsed.entryProblem);
|
|
163
250
|
}
|
|
251
|
+
const serial = Buffer.from(certificate.serialNumber.valueBlock.valueHexView);
|
|
252
|
+
const { revoked } = scanEntries(parsed.bytes, parsed.entries, serial);
|
|
164
253
|
checkValidity(crl);
|
|
165
254
|
return { revoked, nextUpdate };
|
|
166
255
|
}
|
|
167
256
|
|
|
168
|
-
module.exports = {
|
|
257
|
+
module.exports = { parseCertificate, distributionPoints, parseCrl, validateCrl };
|
package/download.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { assertVerifiedResponse, createAgent } = require('./transport');
|
|
4
|
-
const { MAX_CRL_BYTES } = require('./crl');
|
|
5
4
|
const { debugUrl } = require('./debug');
|
|
6
5
|
|
|
7
6
|
function networkUrl(value, kind = 'CRL distribution point') {
|
|
@@ -21,9 +20,9 @@ function networkUrl(value, kind = 'CRL distribution point') {
|
|
|
21
20
|
return url;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
async function download(value, signal, body, debug) {
|
|
23
|
+
async function download(value, signal, body, debug, maxCrlBytes) {
|
|
25
24
|
const kind = (body === undefined) ? 'CRL' : 'OCSP';
|
|
26
|
-
const maxBytes = (body === undefined) ?
|
|
25
|
+
const maxBytes = (body === undefined) ? maxCrlBytes : 1024 * 1024;
|
|
27
26
|
let url = networkUrl(value, kind);
|
|
28
27
|
const timeout = AbortSignal.timeout(10000);
|
|
29
28
|
const downloadSignal = signal ? AbortSignal.any([ signal, timeout ]) : timeout;
|
|
@@ -75,7 +74,8 @@ async function download(value, signal, body, debug) {
|
|
|
75
74
|
for await (const chunk of response.body) {
|
|
76
75
|
length += chunk.byteLength;
|
|
77
76
|
if (length > maxBytes) {
|
|
78
|
-
throw new Error(
|
|
77
|
+
throw new Error((body === undefined) ? `CRL download exceeds maxCrlBytes (${maxBytes} bytes)` :
|
|
78
|
+
'OCSP download exceeds the 1 MiB size limit');
|
|
79
79
|
}
|
|
80
80
|
chunks.push(chunk);
|
|
81
81
|
}
|
|
@@ -89,8 +89,8 @@ async function download(value, signal, body, debug) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function downloadCrl(value, signal, debug) {
|
|
93
|
-
return download(value, signal, undefined, debug);
|
|
92
|
+
function downloadCrl(value, signal, debug, maxBytes) {
|
|
93
|
+
return download(value, signal, undefined, debug, maxBytes);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function downloadOcsp(value, request, signal, debug) {
|
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ async function trFetch(input, options) {
|
|
|
18
18
|
url: debugUrl(request.url),
|
|
19
19
|
crl: config.policy.disabled ? 'disabled' : 'enabled',
|
|
20
20
|
crlDepth: (config.checkDepth === Infinity) ? 'full-chain' : config.checkDepth,
|
|
21
|
+
maxCrlBytes: config.policy.maxCrlBytes,
|
|
21
22
|
ocsp: config.ocspPolicy.disabled ? 'disabled' : 'enabled',
|
|
22
23
|
ocspDepth: (config.ocspCheckDepth === Infinity) ? 'full-chain' : config.ocspCheckDepth
|
|
23
24
|
});
|
package/options.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const DEFAULT_MAX_CRL_BYTES = 16 * 1024 * 1024;
|
|
4
|
+
|
|
3
5
|
const DEFAULT_POLICY = {
|
|
4
6
|
disabled: false,
|
|
7
|
+
maxCrlBytes: DEFAULT_MAX_CRL_BYTES,
|
|
5
8
|
missingCrlDistributionPoint: 'ignore',
|
|
6
9
|
unreachableCrlDistributionPoint: 'reject',
|
|
7
10
|
invalidCrl: 'reject',
|
|
@@ -54,6 +57,12 @@ function parsePolicy(value, defaults, name) {
|
|
|
54
57
|
throw new TypeError(`${name}.disabled must be a boolean, null or undefined`);
|
|
55
58
|
}
|
|
56
59
|
policy.disabled = setting ?? false;
|
|
60
|
+
} else if (key === 'maxCrlBytes') {
|
|
61
|
+
// Deliberately no value for unlimited; any limit must be explicit.
|
|
62
|
+
if (! Number.isSafeInteger(setting) || (setting <= 0)) {
|
|
63
|
+
throw new TypeError(`${name}.maxCrlBytes must be a positive safe integer (bytes)`);
|
|
64
|
+
}
|
|
65
|
+
policy.maxCrlBytes = setting;
|
|
57
66
|
} else {
|
|
58
67
|
if (! [ 'ignore', 'warn', 'reject' ].includes(setting)) {
|
|
59
68
|
throw new TypeError(`${name}.${key} must be ignore, warn or reject`);
|
|
@@ -143,4 +152,4 @@ function splitOptions(options) {
|
|
|
143
152
|
ocspPolicy, ocspUri, ocspCheckDepth, debugEnabled };
|
|
144
153
|
}
|
|
145
154
|
|
|
146
|
-
module.exports = { splitOptions };
|
|
155
|
+
module.exports = { DEFAULT_MAX_CRL_BYTES, splitOptions };
|
package/package.json
CHANGED
package/pkiutils.js
CHANGED
|
@@ -6,9 +6,8 @@ const { webcrypto } = require('node:crypto');
|
|
|
6
6
|
|
|
7
7
|
const cryptoEngine = new pki.CryptoEngine({ name: 'tr-fetch', crypto: webcrypto });
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const decoded = asn1.fromBER(bytes, limits);
|
|
9
|
+
function parseDer(bytes, Type) {
|
|
10
|
+
const decoded = asn1.fromBER(bytes);
|
|
12
11
|
if (decoded.result.error) {
|
|
13
12
|
throw new Error(`Malformed ASN.1 data: ${decoded.result.error}`);
|
|
14
13
|
}
|
|
@@ -18,6 +17,61 @@ function parseDer(bytes, Type, limits) {
|
|
|
18
17
|
return new Type({ schema: decoded.result });
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
// A minimal DER element reader for data too large to decode into an object
|
|
21
|
+
// tree: single-byte tags and definite lengths only, always within bounds.
|
|
22
|
+
function derElement(bytes, offset, end = bytes.length) {
|
|
23
|
+
if ((offset + 2) > end) {
|
|
24
|
+
throw new Error('Malformed ASN.1 data: truncated element');
|
|
25
|
+
}
|
|
26
|
+
const tag = bytes[offset];
|
|
27
|
+
if ((tag & 0x1f) === 0x1f) {
|
|
28
|
+
throw new Error('Malformed ASN.1 data: unsupported tag');
|
|
29
|
+
}
|
|
30
|
+
let start = offset + 2;
|
|
31
|
+
let length = bytes[offset + 1];
|
|
32
|
+
if (length & 0x80) {
|
|
33
|
+
const count = length & 0x7f;
|
|
34
|
+
if ((count === 0) || (count > 4) || ((start + count) > end)) {
|
|
35
|
+
throw new Error('Malformed ASN.1 data: unsupported length encoding');
|
|
36
|
+
}
|
|
37
|
+
length = 0;
|
|
38
|
+
for (let i = 0; i < count; i++) {
|
|
39
|
+
length = (length * 256) + bytes[start + i];
|
|
40
|
+
}
|
|
41
|
+
start += count;
|
|
42
|
+
}
|
|
43
|
+
if ((start + length) > end) {
|
|
44
|
+
throw new Error('Malformed ASN.1 data: element exceeds its container');
|
|
45
|
+
}
|
|
46
|
+
return { tag, offset, start, end: start + length };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function derHeader(tag, length) {
|
|
50
|
+
if (length < 0x80) {
|
|
51
|
+
return Buffer.from([ tag, length ]);
|
|
52
|
+
}
|
|
53
|
+
const bytes = [];
|
|
54
|
+
for (let value = length; value > 0; value = Math.floor(value / 256)) {
|
|
55
|
+
bytes.unshift(value % 256);
|
|
56
|
+
}
|
|
57
|
+
return Buffer.from([ tag, 0x80 | bytes.length, ...bytes ]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function oidString(bytes) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
let value = 0;
|
|
63
|
+
for (const byte of bytes) {
|
|
64
|
+
value = (value * 128) + (byte & 0x7f);
|
|
65
|
+
if (! (byte & 0x80)) {
|
|
66
|
+
parts.push(value);
|
|
67
|
+
value = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const first = parts.shift() ?? 0;
|
|
71
|
+
const top = Math.min(2, Math.floor(first / 40));
|
|
72
|
+
return [ top, first - (top * 40), ...parts ].join('.');
|
|
73
|
+
}
|
|
74
|
+
|
|
21
75
|
function extensionsById(extensions = []) {
|
|
22
76
|
const result = new Map();
|
|
23
77
|
for (const extension of extensions) {
|
|
@@ -45,4 +99,4 @@ function parseCertificate(bytes) {
|
|
|
45
99
|
return parseDer(bytes, pki.Certificate);
|
|
46
100
|
}
|
|
47
101
|
|
|
48
|
-
module.exports = { cryptoEngine, parseDer, extensionsById, extensionValue, parseCertificate };
|
|
102
|
+
module.exports = { cryptoEngine, parseDer, derElement, derHeader, oidString, extensionsById, extensionValue, parseCertificate };
|
package/tr-curl/options.js
CHANGED
|
@@ -26,6 +26,14 @@ function tlsMaxCb(value) {
|
|
|
26
26
|
return (value === 'default') ? value : TLS_VERSIONS[value];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function positiveBytesCb(value) {
|
|
30
|
+
return (/^[1-9]\d*$/.test(value) && Number.isSafeInteger(Number(value))) ? Number(value) : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function networkUrlCb(value) {
|
|
34
|
+
return (URL.canParse(value) && [ 'http:', 'https:' ].includes(new URL(value).protocol)) ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
function jsonObjectCb(value) {
|
|
30
38
|
try {
|
|
31
39
|
const parsed = JSON.parse(value);
|
|
@@ -96,7 +104,10 @@ function optionDefinitions() {
|
|
|
96
104
|
flag(undefined, 'tlsv1.3', 'Use TLSv1.3 or greater'),
|
|
97
105
|
arg(undefined, 'tls-max', '<version> Maximum TLS version: 1.0, 1.1, 1.2, 1.3 or default', tlsMaxCb),
|
|
98
106
|
arg(undefined, 'tls13-ciphers', '<list> TLS 1.3 cipher suites to use'),
|
|
99
|
-
arg(undefined, '
|
|
107
|
+
arg(undefined, 'tr-fetch-max-crl-bytes', '<bytes> Largest CRL accepted (default 16777216)', positiveBytesCb),
|
|
108
|
+
arg(undefined, 'tr-fetch-crl-url', '<url> Fetch the server certificate\'s CRL from this URL instead', networkUrlCb),
|
|
109
|
+
arg(undefined, 'tr-fetch-ocsp-url', '<url> Query this OCSP responder for the server certificate instead', networkUrlCb),
|
|
110
|
+
arg(undefined, 'tr-fetch-options', '<json> Extra trFetch options as a JSON object', jsonObjectCb, true),
|
|
100
111
|
arg('u', 'user', '<user:password> Server user and password (basic authentication)'),
|
|
101
112
|
arg(undefined, 'url', '<url> URL to work with', undefined, true),
|
|
102
113
|
flag('v', 'verbose', 'Make the operation more talkative, including trFetch debug output', true),
|
|
@@ -123,12 +134,30 @@ function parseArguments(argv) {
|
|
|
123
134
|
if (value('output').length && value('remote-name')) {
|
|
124
135
|
throw new UsageError('--output and --remote-name cannot be used together');
|
|
125
136
|
}
|
|
126
|
-
const trFetchOptions = Object.assign({}, ...value('
|
|
137
|
+
const trFetchOptions = Object.assign({}, ...value('tr-fetch-options'));
|
|
127
138
|
for (const key of Object.keys(trFetchOptions)) {
|
|
128
139
|
if (! /^trFetch/.test(key)) {
|
|
129
|
-
throw new UsageError(`--
|
|
140
|
+
throw new UsageError(`--tr-fetch-options accepts only trFetch options, not ${JSON.stringify(key)}`);
|
|
130
141
|
}
|
|
131
142
|
}
|
|
143
|
+
// Dedicated options take precedence over the same settings given in
|
|
144
|
+
// --tr-fetch-options, regardless of their order.
|
|
145
|
+
if (value('tr-fetch-max-crl-bytes') !== undefined) {
|
|
146
|
+
const policy = trFetchOptions.trFetchCrlPolicy;
|
|
147
|
+
// Leave a malformed policy for trFetch to reject.
|
|
148
|
+
if ((policy === undefined) || (policy && (typeof(policy) === 'object') && ! Array.isArray(policy))) {
|
|
149
|
+
trFetchOptions.trFetchCrlPolicy = { ...policy, maxCrlBytes: value('tr-fetch-max-crl-bytes') };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (value('tr-fetch-crl-url') !== undefined) {
|
|
153
|
+
if (value('crlfile') !== undefined) {
|
|
154
|
+
throw new UsageError('--tr-fetch-crl-url and --crlfile cannot be used together');
|
|
155
|
+
}
|
|
156
|
+
trFetchOptions.trFetchCrlDistributionPointOverride = value('tr-fetch-crl-url');
|
|
157
|
+
}
|
|
158
|
+
if (value('tr-fetch-ocsp-url') !== undefined) {
|
|
159
|
+
trFetchOptions.trFetchOcspUriOverride = value('tr-fetch-ocsp-url');
|
|
160
|
+
}
|
|
132
161
|
const data = [ 'data', 'data-ascii', 'data-binary', 'data-raw', 'data-urlencode', 'json' ]
|
|
133
162
|
.flatMap(value).sort((a, b) => a.seq - b.seq);
|
|
134
163
|
if (value('head') && data.length && ! value('get')) {
|