tr-fetch 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/check.js +9 -3
- package/crl.js +121 -28
- package/download.js +6 -6
- package/index.js +1 -0
- package/options.js +10 -1
- package/package.json +1 -1
- package/pkiutils.js +61 -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,7 +323,7 @@ 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
329
|
- Allows private-network HTTP(S) endpoints, including loopback, for private PKI.
|
|
@@ -388,7 +406,14 @@ checks. Only `http:` and `https:` URLs are supported; a URL without a scheme
|
|
|
388
406
|
defaults to `http://`.
|
|
389
407
|
|
|
390
408
|
```sh
|
|
391
|
-
npx tr-curl -v https://example.com/
|
|
409
|
+
npx -p tr-fetch tr-curl -v https://example.com/
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The command is in the `tr-fetch` package, so `npx` needs `-p tr-fetch`.
|
|
413
|
+
After `npm install -g tr-fetch`, or inside a project that depends on it,
|
|
414
|
+
`tr-curl` can be run directly:
|
|
415
|
+
|
|
416
|
+
```sh
|
|
392
417
|
tr-curl -fsSL -o page.html https://example.com/
|
|
393
418
|
tr-curl --json '{"a":1}' -u user:password https://api.example.com/items
|
|
394
419
|
tr-curl --cacert private-ca.pem --crlfile current.crl https://internal.example/
|
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,9 +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
|
|
9
|
+
const TIME_TAGS = [ 0x17, 0x18 ];
|
|
8
10
|
|
|
9
11
|
function distributionPoints(certificate) {
|
|
10
12
|
const extension = extensionsById(certificate.extensions).get('2.5.29.31');
|
|
@@ -24,10 +26,11 @@ function distributionPoints(certificate) {
|
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
function parseCrl(bytes) {
|
|
28
|
-
if (bytes.length >
|
|
29
|
-
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)`);
|
|
30
32
|
}
|
|
33
|
+
const size = bytes.length;
|
|
31
34
|
if (bytes.toString('ascii', 0, 32).trimStart().startsWith('-----BEGIN')) {
|
|
32
35
|
const match = /^\s*-----BEGIN X509 CRL-----\s*([A-Za-z0-9+/=\r\n\t ]+)\s*-----END X509 CRL-----\s*$/.exec(bytes.toString('ascii'));
|
|
33
36
|
if (! match) {
|
|
@@ -39,7 +42,110 @@ function parseCrl(bytes) {
|
|
|
39
42
|
throw new Error('Malformed base64 in PEM CRL');
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
|
-
|
|
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 };
|
|
43
149
|
}
|
|
44
150
|
|
|
45
151
|
function checkValidity(crl, now = Date.now()) {
|
|
@@ -91,7 +197,8 @@ function checkScope(crl, certificate, urls) {
|
|
|
91
197
|
return extensions;
|
|
92
198
|
}
|
|
93
199
|
|
|
94
|
-
async function validateCrl(
|
|
200
|
+
async function validateCrl(parsed, certificate, issuer, urls, now = Date.now()) {
|
|
201
|
+
const crl = parsed.crl;
|
|
95
202
|
const nextUpdate = checkValidity(crl, now);
|
|
96
203
|
const extensions = checkScope(crl, certificate, urls);
|
|
97
204
|
if (! crl.issuer.isEqual(certificate.issuer) || ! crl.issuer.isEqual(issuer.subject)) {
|
|
@@ -134,31 +241,17 @@ async function validateCrl(crl, certificate, issuer, urls, now = Date.now()) {
|
|
|
134
241
|
if (crlNumber && ! (extensionValue(crlNumber) instanceof asn1.Integer)) {
|
|
135
242
|
throw new Error('Malformed CRL number');
|
|
136
243
|
}
|
|
137
|
-
|
|
244
|
+
// The signature covers the original TBS bytes, including the entries.
|
|
245
|
+
if (! await cryptoEngine.verifyWithPublicKey(parsed.tbs, crl.signatureValue, issuer.subjectPublicKeyInfo, crl.signatureAlgorithm)) {
|
|
138
246
|
throw new Error('CRL signature verification failed');
|
|
139
247
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for (const extension of extensionsById(entry.crlEntryExtensions?.extensions).values()) {
|
|
143
|
-
if (extension.extnID === '2.5.29.29') {
|
|
144
|
-
throw new Error('Indirect CRL certificateIssuer entries are unsupported');
|
|
145
|
-
}
|
|
146
|
-
if (extension.critical) {
|
|
147
|
-
throw new Error(`Unsupported critical CRL entry extension ${extension.extnID}`);
|
|
148
|
-
}
|
|
149
|
-
if (extension.extnID === '2.5.29.21') {
|
|
150
|
-
const reason = extensionValue(extension);
|
|
151
|
-
if (! (reason instanceof asn1.Enumerated) || (reason.valueBlock.valueDec === 8)) {
|
|
152
|
-
throw new Error('Invalid revocation reason in a complete CRL');
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (entry.userCertificate.isEqual(certificate.serialNumber)) {
|
|
157
|
-
revoked = true;
|
|
158
|
-
}
|
|
248
|
+
if (parsed.entryProblem) {
|
|
249
|
+
throw new Error(parsed.entryProblem);
|
|
159
250
|
}
|
|
251
|
+
const serial = Buffer.from(certificate.serialNumber.valueBlock.valueHexView);
|
|
252
|
+
const { revoked } = scanEntries(parsed.bytes, parsed.entries, serial);
|
|
160
253
|
checkValidity(crl);
|
|
161
254
|
return { revoked, nextUpdate };
|
|
162
255
|
}
|
|
163
256
|
|
|
164
|
-
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
|
@@ -8,12 +8,70 @@ const cryptoEngine = new pki.CryptoEngine({ name: 'tr-fetch', crypto: webcrypto
|
|
|
8
8
|
|
|
9
9
|
function parseDer(bytes, Type) {
|
|
10
10
|
const decoded = asn1.fromBER(bytes);
|
|
11
|
-
if (
|
|
12
|
-
throw new Error(
|
|
11
|
+
if (decoded.result.error) {
|
|
12
|
+
throw new Error(`Malformed ASN.1 data: ${decoded.result.error}`);
|
|
13
|
+
}
|
|
14
|
+
if (decoded.offset !== bytes.length) {
|
|
15
|
+
throw new Error('Malformed ASN.1 data: trailing bytes');
|
|
13
16
|
}
|
|
14
17
|
return new Type({ schema: decoded.result });
|
|
15
18
|
}
|
|
16
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
|
+
|
|
17
75
|
function extensionsById(extensions = []) {
|
|
18
76
|
const result = new Map();
|
|
19
77
|
for (const extension of extensions) {
|
|
@@ -41,4 +99,4 @@ function parseCertificate(bytes) {
|
|
|
41
99
|
return parseDer(bytes, pki.Certificate);
|
|
42
100
|
}
|
|
43
101
|
|
|
44
|
-
module.exports = { cryptoEngine, parseDer, extensionsById, extensionValue, parseCertificate };
|
|
102
|
+
module.exports = { cryptoEngine, parseDer, derElement, derHeader, oidString, extensionsById, extensionValue, parseCertificate };
|