tunnelfetch 1.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.
Files changed (96) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +617 -0
  3. package/README.zh-CN.md +470 -0
  4. package/package.json +74 -0
  5. package/src/client/cookies.js +429 -0
  6. package/src/client/decode.js +346 -0
  7. package/src/client/redirect.js +249 -0
  8. package/src/client.js +704 -0
  9. package/src/errors.js +181 -0
  10. package/src/http1/chunked.js +289 -0
  11. package/src/http1/index.js +10 -0
  12. package/src/http1/request.js +143 -0
  13. package/src/http1/response.js +493 -0
  14. package/src/http2/connection.js +1170 -0
  15. package/src/http2/constants.js +129 -0
  16. package/src/http2/frames.js +291 -0
  17. package/src/http2/hpack.js +420 -0
  18. package/src/http2/huffman.js +203 -0
  19. package/src/http2/index.js +21 -0
  20. package/src/index.js +46 -0
  21. package/src/pool.js +256 -0
  22. package/src/proxy/direct.js +62 -0
  23. package/src/proxy/http-connect.js +206 -0
  24. package/src/proxy/index.js +197 -0
  25. package/src/proxy/socks5.js +344 -0
  26. package/src/tls/aead.js +263 -0
  27. package/src/tls/connect.js +407 -0
  28. package/src/tls/constants.js +334 -0
  29. package/src/tls/extensions.js +376 -0
  30. package/src/tls/handshake-messages.js +901 -0
  31. package/src/tls/handshake.js +568 -0
  32. package/src/tls/handshake12.js +507 -0
  33. package/src/tls/index.js +44 -0
  34. package/src/tls/keyschedule.js +473 -0
  35. package/src/tls/record.js +872 -0
  36. package/src/tls/tickets.js +145 -0
  37. package/src/tls/transcript.js +101 -0
  38. package/src/tls/wire.js +224 -0
  39. package/src/transport.js +296 -0
  40. package/src/trust/der.js +551 -0
  41. package/src/trust/index.js +375 -0
  42. package/src/trust/name.js +235 -0
  43. package/src/trust/ocsp.js +759 -0
  44. package/src/trust/path.js +595 -0
  45. package/src/trust/roots.js +454 -0
  46. package/src/trust/x509.js +902 -0
  47. package/src/util/bytes.js +470 -0
  48. package/src/util/deadline.js +266 -0
  49. package/src/warmup-fixture.js +85 -0
  50. package/src/warmup.js +243 -0
  51. package/types/client/cookies.d.ts +159 -0
  52. package/types/client/decode.d.ts +54 -0
  53. package/types/client/redirect.d.ts +96 -0
  54. package/types/client.d.ts +323 -0
  55. package/types/errors.d.ts +141 -0
  56. package/types/http1/chunked.d.ts +48 -0
  57. package/types/http1/index.d.ts +3 -0
  58. package/types/http1/request.d.ts +44 -0
  59. package/types/http1/response.d.ts +183 -0
  60. package/types/http2/connection.d.ts +282 -0
  61. package/types/http2/constants.d.ts +95 -0
  62. package/types/http2/frames.d.ts +116 -0
  63. package/types/http2/hpack.d.ts +99 -0
  64. package/types/http2/huffman.d.ts +21 -0
  65. package/types/http2/index.d.ts +5 -0
  66. package/types/index.d.ts +17 -0
  67. package/types/pool.d.ts +135 -0
  68. package/types/proxy/direct.d.ts +26 -0
  69. package/types/proxy/http-connect.d.ts +37 -0
  70. package/types/proxy/index.d.ts +62 -0
  71. package/types/proxy/socks5.d.ts +47 -0
  72. package/types/tls/aead.d.ts +67 -0
  73. package/types/tls/connect.d.ts +280 -0
  74. package/types/tls/constants.d.ts +275 -0
  75. package/types/tls/extensions.d.ts +195 -0
  76. package/types/tls/handshake-messages.d.ts +430 -0
  77. package/types/tls/handshake.d.ts +90 -0
  78. package/types/tls/handshake12.d.ts +35 -0
  79. package/types/tls/index.d.ts +9 -0
  80. package/types/tls/keyschedule.d.ts +272 -0
  81. package/types/tls/record.d.ts +361 -0
  82. package/types/tls/tickets.d.ts +66 -0
  83. package/types/tls/transcript.d.ts +52 -0
  84. package/types/tls/wire.d.ts +106 -0
  85. package/types/transport.d.ts +222 -0
  86. package/types/trust/der.d.ts +239 -0
  87. package/types/trust/index.d.ts +194 -0
  88. package/types/trust/name.d.ts +33 -0
  89. package/types/trust/ocsp.d.ts +138 -0
  90. package/types/trust/path.d.ts +139 -0
  91. package/types/trust/roots.d.ts +36 -0
  92. package/types/trust/x509.d.ts +401 -0
  93. package/types/util/bytes.d.ts +183 -0
  94. package/types/util/deadline.d.ts +133 -0
  95. package/types/warmup-fixture.d.ts +11 -0
  96. package/types/warmup.d.ts +45 -0
@@ -0,0 +1,902 @@
1
+ // X.509 v3 certificate parsing (RFC 5280 s4), on top of the DER reader.
2
+ //
3
+ // Parsed from the raw bytes, deliberately not via any runtime helper: the runtime's certificate
4
+ // object was measured on the target edge and silently lacks pathLenConstraint, the signature
5
+ // algorithm, and real key-usage bits. A security check that reads `undefined` and shrugs is the
6
+ // failure mode this package exists to close, so every field consumed by path validation comes
7
+ // from our own DER walk of the original bytes.
8
+ //
9
+ // parseCertificate() is a parser, not a judge: it records what the certificate says (including
10
+ // weak algorithms and unknown critical extensions) and throws only on malformed or self-
11
+ // contradictory encodings. Judgement lives in path.js, which consults resolveSignatureScheme()
12
+ // and unknownCriticalExtensions for exactly the certificates whose signatures actually anchor
13
+ // trust — a server may ship a stale SHA-1 self-signed root alongside a modern chain, and that
14
+ // root's own signature is never consumed, so it must not be able to break the connection.
15
+
16
+ import { CertificateError, codes } from '../errors.js';
17
+ import { equal, toHex } from '../util/bytes.js';
18
+ import { SIG_SCHEME } from '../tls/constants.js';
19
+ import {
20
+ TAG, CLS, readTlv, readAll, readSequence, children, content, element, expectTlv,
21
+ readInteger, readOid, readBitString, readBoolean, readTime, readString, parseError, tagName,
22
+ } from './der.js';
23
+
24
+ export const OID = /** @type {const} */ ({
25
+ // public key algorithms
26
+ rsaEncryption: '1.2.840.113549.1.1.1',
27
+ rsassaPss: '1.2.840.113549.1.1.10',
28
+ ecPublicKey: '1.2.840.10045.2.1',
29
+ ed25519: '1.3.101.112',
30
+ ed448: '1.3.101.113',
31
+ // named curves
32
+ secp256r1: '1.2.840.10045.3.1.7',
33
+ secp384r1: '1.3.132.0.34',
34
+ secp521r1: '1.3.132.0.35',
35
+ // signature algorithms
36
+ sha256WithRsa: '1.2.840.113549.1.1.11',
37
+ sha384WithRsa: '1.2.840.113549.1.1.12',
38
+ sha512WithRsa: '1.2.840.113549.1.1.13',
39
+ ecdsaWithSha256: '1.2.840.10045.4.3.2',
40
+ ecdsaWithSha384: '1.2.840.10045.4.3.3',
41
+ ecdsaWithSha512: '1.2.840.10045.4.3.4',
42
+ // digests (referenced by RSA-PSS parameters)
43
+ sha1: '1.3.14.3.2.26',
44
+ sha256: '2.16.840.1.101.3.4.2.1',
45
+ sha384: '2.16.840.1.101.3.4.2.2',
46
+ sha512: '2.16.840.1.101.3.4.2.3',
47
+ mgf1: '1.2.840.113549.1.1.8',
48
+ // extensions
49
+ subjectKeyIdentifier: '2.5.29.14',
50
+ keyUsage: '2.5.29.15',
51
+ subjectAltName: '2.5.29.17',
52
+ issuerAltName: '2.5.29.18',
53
+ basicConstraints: '2.5.29.19',
54
+ nameConstraints: '2.5.29.30',
55
+ crlDistributionPoints: '2.5.29.31',
56
+ certificatePolicies: '2.5.29.32',
57
+ authorityKeyIdentifier: '2.5.29.35',
58
+ extendedKeyUsage: '2.5.29.37',
59
+ freshestCrl: '2.5.29.46',
60
+ authorityInfoAccess: '1.3.6.1.5.5.7.1.1',
61
+ subjectInfoAccess: '1.3.6.1.5.5.7.1.11',
62
+ sctList: '1.3.6.1.4.1.11129.2.4.2',
63
+ // extended key usage members
64
+ serverAuth: '1.3.6.1.5.5.7.3.1',
65
+ clientAuth: '1.3.6.1.5.5.7.3.2',
66
+ ocspSigning: '1.3.6.1.5.5.7.3.9',
67
+ anyExtendedKeyUsage: '2.5.29.37.0',
68
+ // OCSP (RFC 6960)
69
+ ocspBasic: '1.3.6.1.5.5.7.48.1.1',
70
+ ocspNonce: '1.3.6.1.5.5.7.48.1.2',
71
+ ocspNocheck: '1.3.6.1.5.5.7.48.1.5',
72
+ });
73
+
74
+ /** Signature algorithms rejected outright: collisions are practical or near-practical. */
75
+ const WEAK_SIG_OIDS = {
76
+ '1.2.840.113549.1.1.2': 'md2WithRSAEncryption',
77
+ '1.2.840.113549.1.1.3': 'md4WithRSAEncryption',
78
+ '1.2.840.113549.1.1.4': 'md5WithRSAEncryption',
79
+ '1.2.840.113549.1.1.5': 'sha1WithRSAEncryption',
80
+ '1.3.14.3.2.29': 'sha1WithRSAEncryption (OIW)',
81
+ '1.2.840.10045.4.1': 'ecdsa-with-SHA1',
82
+ '1.2.840.10040.4.3': 'dsa-with-sha1',
83
+ };
84
+
85
+ /**
86
+ * Extensions this validator understands, or has deliberately judged safe to leave unprocessed
87
+ * even when marked critical. Everything else that is critical causes rejection in path.js
88
+ * (RFC 5280 s6.1: a relying party MUST reject on unrecognised critical extensions — ignoring
89
+ * them is the classic fail-open).
90
+ *
91
+ * certificatePolicies is listed because with no required policy set, RFC 5280 policy processing
92
+ * cannot fail; policyConstraints / inhibitAnyPolicy are deliberately NOT listed, because they
93
+ * make policy processing mandatory and we do not implement it — they are always critical, so
94
+ * their presence in a path rejects it.
95
+ */
96
+ export const KNOWN_EXTENSIONS = new Set([
97
+ OID.subjectKeyIdentifier, OID.keyUsage, OID.subjectAltName, OID.issuerAltName,
98
+ OID.basicConstraints, OID.nameConstraints, OID.crlDistributionPoints, OID.certificatePolicies,
99
+ OID.authorityKeyIdentifier, OID.extendedKeyUsage, OID.freshestCrl,
100
+ OID.authorityInfoAccess, OID.subjectInfoAccess, OID.sctList,
101
+ ]);
102
+
103
+ // ------------------------------------------------------------------ names
104
+
105
+ const ATTR_NAME = {
106
+ '2.5.4.3': 'CN', '2.5.4.4': 'SN', '2.5.4.5': 'serialNumber', '2.5.4.6': 'C', '2.5.4.7': 'L',
107
+ '2.5.4.8': 'ST', '2.5.4.9': 'street', '2.5.4.10': 'O', '2.5.4.11': 'OU', '2.5.4.15': 'businessCategory',
108
+ '2.5.4.17': 'postalCode', '2.5.4.42': 'GN', '2.5.4.97': 'organizationIdentifier',
109
+ '1.2.840.113549.1.9.1': 'emailAddress', '0.9.2342.19200300.100.1.1': 'UID',
110
+ '0.9.2342.19200300.100.1.25': 'DC',
111
+ };
112
+
113
+ /**
114
+ * A parsed X.500 Name. `bytes` is the exact DER of the whole Name — the canonical identity for
115
+ * every comparison; `rdns` and `text` exist for constraint checks and log lines respectively.
116
+ * @typedef {object} DistinguishedName
117
+ * @property {Uint8Array} bytes
118
+ * @property {Array<Array<{ oid: string, value: string }>>} rdns one array per RDN, in order;
119
+ * non-string attribute values are rendered as '#hex'
120
+ * @property {string} text human-readable 'CN=..., O=...' form
121
+ */
122
+
123
+ /**
124
+ * Name ::= RDNSequence ::= SEQUENCE OF RelativeDistinguishedName (SET OF AttributeTypeAndValue).
125
+ *
126
+ * `bytes` is the exact DER of the whole Name and is the canonical form used for all issuer ==
127
+ * subject comparisons. RFC 5280 s7.1 also allows caseIgnore/whitespace-folded matching, but a CA
128
+ * that spells its own name two different ways between certificates breaks every deployed
129
+ * validator that matters (they compare bytes too), and a lax comparator is one more place to
130
+ * confuse two names. Exact bytes, fail closed.
131
+ *
132
+ * @param {Uint8Array} bytes
133
+ * @param {import('./der.js').Tlv} tlv
134
+ * @returns {DistinguishedName}
135
+ */
136
+ export function parseName(bytes, tlv) {
137
+ expectTlv(tlv, { tag: TAG.SEQUENCE, constructed: true }, 'Name');
138
+ const rdns = [];
139
+ const parts = [];
140
+ for (const rdn of children(bytes, tlv, 'Name')) {
141
+ expectTlv(rdn, { tag: TAG.SET, constructed: true }, 'RelativeDistinguishedName');
142
+ const avas = [];
143
+ const kids = children(bytes, rdn, 'RelativeDistinguishedName');
144
+ if (kids.length === 0) throw parseError(rdn.start, 'empty RelativeDistinguishedName');
145
+ for (const ava of kids) {
146
+ expectTlv(ava, { tag: TAG.SEQUENCE, constructed: true }, 'AttributeTypeAndValue');
147
+ const [typeTlv, valueTlv, ...extra] = children(bytes, ava, 'AttributeTypeAndValue');
148
+ if (!typeTlv || !valueTlv || extra.length) {
149
+ throw parseError(ava.start, 'AttributeTypeAndValue must be exactly { type, value }');
150
+ }
151
+ const oid = readOid(bytes, typeTlv, 'attribute type');
152
+ let value = null;
153
+ try {
154
+ value = readString(bytes, valueTlv, `attribute ${oid}`);
155
+ } catch {
156
+ // Non-string attribute values are legal (rare); render as hex rather than fail a
157
+ // certificate over an attribute we only ever display.
158
+ value = `#${toHex(content(bytes, valueTlv))}`;
159
+ }
160
+ avas.push({ oid, value });
161
+ parts.push(`${ATTR_NAME[oid] ?? oid}=${value}`);
162
+ }
163
+ rdns.push(avas);
164
+ }
165
+ return { bytes: element(bytes, tlv), rdns, text: parts.join(', ') || '<empty name>' };
166
+ }
167
+
168
+ // ------------------------------------------------------------------ GeneralName
169
+
170
+ /**
171
+ * GeneralName (RFC 5280 s4.2.1.6), used by subjectAltName and nameConstraints. `ipLens` differs
172
+ * by context: SAN carries bare addresses (4/16), name constraints carry address+mask (8/32).
173
+ */
174
+ function parseGeneralName(bytes, tlv, ipLens, what) {
175
+ if (tlv.cls !== CLS.CONTEXT) {
176
+ throw parseError(tlv.start,
177
+ `expected context-tagged GeneralName in ${what}, got ${tagName(tlv.cls, tlv.tag)}`);
178
+ }
179
+ const c = content(bytes, tlv);
180
+ switch (tlv.tag) {
181
+ case 1: // rfc822Name, IA5String
182
+ case 2: // dNSName, IA5String
183
+ case 6: { // uniformResourceIdentifier, IA5String
184
+ let s = '';
185
+ for (let i = 0; i < c.byteLength; i++) {
186
+ const b = c[i];
187
+ // An embedded NUL is the null-prefix attack ("example.com\0.evil.test"): parsers that
188
+ // stop at NUL and parsers that do not will disagree about this name. Never tolerated.
189
+ if (b === 0x00) throw parseError(tlv.contentStart + i, `NUL byte in ${what} GeneralName`);
190
+ if (b > 0x7f) {
191
+ throw parseError(tlv.contentStart + i, `non-ASCII byte 0x${b.toString(16)} in ${what} IA5String`);
192
+ }
193
+ s += String.fromCharCode(b);
194
+ }
195
+ return { type: tlv.tag === 1 ? 'email' : tlv.tag === 2 ? 'dns' : 'uri', value: s };
196
+ }
197
+ case 7: { // iPAddress, OCTET STRING
198
+ if (!ipLens.includes(c.byteLength)) {
199
+ throw parseError(
200
+ tlv.start,
201
+ `iPAddress in ${what} must be ${ipLens.join(' or ')} bytes, got ${c.byteLength}`,
202
+ );
203
+ }
204
+ return { type: 'ip', bytes: c };
205
+ }
206
+ case 4: // directoryName (EXPLICIT Name)
207
+ return { type: 'dirName', bytes: c };
208
+ default:
209
+ // otherName [0], x400Address [3], ediPartyName [5], registeredID [8]: recorded so that
210
+ // name-constraint processing can refuse to ignore them when the extension is critical.
211
+ return { type: 'other', tag: tlv.tag };
212
+ }
213
+ }
214
+
215
+ // ------------------------------------------------------------------ extensions
216
+
217
+ function parseBasicConstraints(valueBytes) {
218
+ const seq = readSequence(valueBytes, 0, 'BasicConstraints');
219
+ if (seq.end !== valueBytes.byteLength) throw parseError(seq.end, 'trailing bytes in BasicConstraints');
220
+ const kids = children(valueBytes, seq, 'BasicConstraints');
221
+ let i = 0;
222
+ let ca = false;
223
+ if (i < kids.length && kids[i].tag === TAG.BOOLEAN && kids[i].cls === CLS.UNIVERSAL) {
224
+ ca = readBoolean(valueBytes, kids[i], 'cA');
225
+ // DER forbids encoding a DEFAULT value; cA FALSE spelled out is a second encoding of the
226
+ // same meaning, which is exactly the ambiguity class this parser exists to refuse.
227
+ if (!ca) throw parseError(kids[i].start, 'cA FALSE must be omitted in DER');
228
+ i++;
229
+ }
230
+ let pathLenConstraint = null;
231
+ if (i < kids.length) {
232
+ const { value, negative } = readInteger(valueBytes, kids[i], 'pathLenConstraint');
233
+ if (negative || value === null) {
234
+ throw parseError(kids[i].start, 'pathLenConstraint must be a small non-negative integer');
235
+ }
236
+ if (!ca) {
237
+ // RFC 5280 s4.2.1.9: pathLenConstraint is only meaningful with cA=TRUE. A constraint on a
238
+ // non-CA is self-contradictory; guessing which half to believe would be failing open.
239
+ throw parseError(kids[i].start, 'pathLenConstraint present without cA TRUE');
240
+ }
241
+ pathLenConstraint = value;
242
+ i++;
243
+ }
244
+ if (i !== kids.length) throw parseError(kids[i].start, 'unexpected extra field in BasicConstraints');
245
+ return { present: true, ca, pathLenConstraint };
246
+ }
247
+
248
+ const KEY_USAGE_BITS = [
249
+ 'digitalSignature', 'nonRepudiation', 'keyEncipherment', 'dataEncipherment',
250
+ 'keyAgreement', 'keyCertSign', 'cRLSign', 'encipherOnly', 'decipherOnly',
251
+ ];
252
+
253
+ /**
254
+ * The nine RFC 5280 s4.2.1.3 bits, each explicit so a validator reads `false`, never
255
+ * `undefined` — an absent bit and an unset bit must be indistinguishable.
256
+ * @typedef {object} KeyUsage
257
+ * @property {boolean} digitalSignature
258
+ * @property {boolean} nonRepudiation
259
+ * @property {boolean} keyEncipherment
260
+ * @property {boolean} dataEncipherment
261
+ * @property {boolean} keyAgreement
262
+ * @property {boolean} keyCertSign
263
+ * @property {boolean} cRLSign
264
+ * @property {boolean} encipherOnly
265
+ * @property {boolean} decipherOnly
266
+ */
267
+
268
+ /** @returns {KeyUsage} */
269
+ function parseKeyUsage(valueBytes) {
270
+ const tlv = readAll(valueBytes, 'KeyUsage');
271
+ const { bytes: bits, unusedBits } = readBitString(valueBytes, tlv, 'KeyUsage');
272
+ const usage = {};
273
+ let any = false;
274
+ for (let i = 0; i < KEY_USAGE_BITS.length; i++) {
275
+ const byte = i >> 3;
276
+ const set =
277
+ byte < bits.byteLength &&
278
+ (byte < bits.byteLength - 1 || (7 - (i & 7)) >= unusedBits) &&
279
+ (bits[byte] & (0x80 >> (i & 7))) !== 0;
280
+ usage[KEY_USAGE_BITS[i]] = set;
281
+ any = any || set;
282
+ }
283
+ // RFC 5280 s4.2.1.3: when the extension appears, at least one bit MUST be set. An all-zero
284
+ // keyUsage asserts "this key may do nothing", which cannot coexist with using the key.
285
+ if (!any) throw parseError(tlv.start, 'KeyUsage extension with no bits set');
286
+ return Object.freeze(usage);
287
+ }
288
+
289
+ function parseExtendedKeyUsage(valueBytes) {
290
+ const seq = readSequence(valueBytes, 0, 'ExtKeyUsageSyntax');
291
+ if (seq.end !== valueBytes.byteLength) throw parseError(seq.end, 'trailing bytes in ExtendedKeyUsage');
292
+ const kids = children(valueBytes, seq, 'ExtKeyUsageSyntax');
293
+ if (kids.length === 0) throw parseError(seq.start, 'empty ExtendedKeyUsage');
294
+ return Object.freeze(kids.map((k) => readOid(valueBytes, k, 'KeyPurposeId')));
295
+ }
296
+
297
+ /**
298
+ * The SAN entries identity matching consults. `present` distinguishes "no SAN extension"
299
+ * (matches nothing, by policy) from "SAN with no entries of this type".
300
+ * @typedef {object} SubjectAltNames
301
+ * @property {boolean} present
302
+ * @property {ReadonlyArray<string>} dns
303
+ * @property {ReadonlyArray<Uint8Array>} ip raw 4- or 16-byte addresses
304
+ * @property {ReadonlyArray<string>} uri
305
+ * @property {ReadonlyArray<string>} email
306
+ */
307
+
308
+ /** @returns {SubjectAltNames} */
309
+ function parseSubjectAltName(valueBytes) {
310
+ const seq = readSequence(valueBytes, 0, 'GeneralNames');
311
+ if (seq.end !== valueBytes.byteLength) throw parseError(seq.end, 'trailing bytes in SubjectAltName');
312
+ const kids = children(valueBytes, seq, 'GeneralNames');
313
+ // RFC 5280 s4.2.1.6: if present, the SAN sequence MUST contain at least one entry.
314
+ if (kids.length === 0) throw parseError(seq.start, 'empty SubjectAltName');
315
+ const dns = [];
316
+ const ip = [];
317
+ const uri = [];
318
+ const email = [];
319
+ for (const k of kids) {
320
+ const gn = parseGeneralName(valueBytes, k, [4, 16], 'subjectAltName');
321
+ if (gn.type === 'dns') dns.push(gn.value);
322
+ else if (gn.type === 'ip') ip.push(gn.bytes);
323
+ else if (gn.type === 'uri') uri.push(gn.value);
324
+ else if (gn.type === 'email') email.push(gn.value);
325
+ // dirName / other entries are legal; nothing in this package matches on them.
326
+ }
327
+ return Object.freeze({ present: true, dns: Object.freeze(dns), ip: Object.freeze(ip),
328
+ uri: Object.freeze(uri), email: Object.freeze(email) });
329
+ }
330
+
331
+ function parseSubjectKeyIdentifier(valueBytes) {
332
+ const tlv = readAll(valueBytes, 'SubjectKeyIdentifier');
333
+ expectTlv(tlv, { tag: TAG.OCTET_STRING, constructed: false }, 'SubjectKeyIdentifier');
334
+ return content(valueBytes, tlv);
335
+ }
336
+
337
+ function parseAuthorityKeyIdentifier(valueBytes) {
338
+ const seq = readSequence(valueBytes, 0, 'AuthorityKeyIdentifier');
339
+ if (seq.end !== valueBytes.byteLength) {
340
+ throw parseError(seq.end, 'trailing bytes in AuthorityKeyIdentifier');
341
+ }
342
+ let keyId = null;
343
+ for (const k of children(valueBytes, seq, 'AuthorityKeyIdentifier')) {
344
+ if (k.cls !== CLS.CONTEXT) {
345
+ throw parseError(k.start, 'AuthorityKeyIdentifier fields must be context-tagged');
346
+ }
347
+ if (k.tag === 0) keyId = content(valueBytes, k); // [0] IMPLICIT KeyIdentifier
348
+ // [1] authorityCertIssuer / [2] authorityCertSerialNumber: parsed past, not used — chain
349
+ // building matches on DN plus keyIdentifier, which is what CAs actually populate.
350
+ }
351
+ return keyId;
352
+ }
353
+
354
+ /**
355
+ * One GeneralSubtree, reduced to what constraint enforcement can act on. 'other' entries are
356
+ * forms this validator cannot enforce; path.js rejects the path when a critical extension
357
+ * carries one, which is why they are preserved rather than dropped.
358
+ * @typedef {{ type: 'dns', value: string } | { type: 'email', value: string }
359
+ * | { type: 'uri', value: string } | { type: 'ip', addr: Uint8Array, mask: Uint8Array }
360
+ * | { type: 'other', tag: number }} NameConstraintSubtree
361
+ */
362
+
363
+ /**
364
+ * @typedef {object} NameConstraints
365
+ * @property {ReadonlyArray<NameConstraintSubtree> | null} permitted
366
+ * @property {ReadonlyArray<NameConstraintSubtree> | null} excluded
367
+ */
368
+
369
+ /**
370
+ * NameConstraints (RFC 5280 s4.2.1.10). Subtrees we cannot enforce are preserved as
371
+ * `{type:'other'}` entries so path.js can refuse to ignore them when the extension is critical.
372
+ * A GeneralSubtree with minimum != 0 or maximum present is demoted to unsupported for the same
373
+ * reason: RFC 5280 forbids them, and enforcing a constraint we cannot interpret is worse than
374
+ * rejecting.
375
+ *
376
+ * @param {Uint8Array} valueBytes the extnValue content
377
+ * @returns {NameConstraints}
378
+ */
379
+ export function parseNameConstraints(valueBytes) {
380
+ const seq = readSequence(valueBytes, 0, 'NameConstraints');
381
+ if (seq.end !== valueBytes.byteLength) throw parseError(seq.end, 'trailing bytes in NameConstraints');
382
+ const kids = children(valueBytes, seq, 'NameConstraints');
383
+ if (kids.length === 0) {
384
+ throw parseError(seq.start, 'NameConstraints with neither permitted nor excluded subtrees');
385
+ }
386
+ const out = { permitted: null, excluded: null };
387
+ for (const k of kids) {
388
+ if (k.cls !== CLS.CONTEXT || (k.tag !== 0 && k.tag !== 1) || !k.constructed) {
389
+ throw parseError(k.start, 'NameConstraints fields must be [0] or [1] GeneralSubtrees');
390
+ }
391
+ const which = k.tag === 0 ? 'permitted' : 'excluded';
392
+ if (out[which] !== null) throw parseError(k.start, `duplicate ${which}Subtrees`);
393
+ const subtrees = [];
394
+ const trees = children(valueBytes, k, 'GeneralSubtrees');
395
+ if (trees.length === 0) throw parseError(k.start, `empty ${which}Subtrees`);
396
+ for (const t of trees) {
397
+ expectTlv(t, { tag: TAG.SEQUENCE, constructed: true }, 'GeneralSubtree');
398
+ const fields = children(valueBytes, t, 'GeneralSubtree');
399
+ if (fields.length === 0) throw parseError(t.start, 'empty GeneralSubtree');
400
+ const gn = parseGeneralName(valueBytes, fields[0], [8, 32], 'nameConstraints');
401
+ const boundsOk = fields.length === 1; // minimum defaults to 0, maximum must be absent
402
+ if (!boundsOk || gn.type === 'dirName') {
403
+ subtrees.push({ type: 'other', tag: fields[0].tag });
404
+ } else if (gn.type === 'ip') {
405
+ const half = gn.bytes.byteLength / 2;
406
+ subtrees.push({ type: 'ip', addr: gn.bytes.subarray(0, half), mask: gn.bytes.subarray(half) });
407
+ } else {
408
+ subtrees.push(gn); // dns / email / uri / other
409
+ }
410
+ }
411
+ out[which] = Object.freeze(subtrees);
412
+ }
413
+ return Object.freeze(out);
414
+ }
415
+
416
+ // ------------------------------------------------------------------ algorithm identifiers
417
+
418
+ /**
419
+ * AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY OPTIONAL }.
420
+ *
421
+ * Exported for the OCSP checker, which meets the same structure in BasicOCSPResponse and CertID
422
+ * and must read it with the same strictness rather than a second, slightly different walk.
423
+ *
424
+ * @param {Uint8Array} bytes
425
+ * @param {import('./der.js').Tlv} tlv
426
+ * @param {string} what
427
+ * @returns {AlgorithmId}
428
+ */
429
+ export function parseAlgorithmIdentifier(bytes, tlv, what) {
430
+ expectTlv(tlv, { tag: TAG.SEQUENCE, constructed: true }, what);
431
+ const kids = children(bytes, tlv, what);
432
+ if (kids.length < 1 || kids.length > 2) {
433
+ throw parseError(tlv.start, `${what} must be { algorithm, parameters? }`);
434
+ }
435
+ const oid = readOid(bytes, kids[0], `${what} algorithm`);
436
+ const params = kids.length === 2 ? kids[1] : null;
437
+ return {
438
+ oid,
439
+ paramsTlv: params,
440
+ paramsBytes: params ? element(bytes, params) : null,
441
+ bytes: element(bytes, tlv),
442
+ };
443
+ }
444
+
445
+ /**
446
+ * RSASSA-PSS-params (RFC 4055). All fields are EXPLICIT-tagged with SHA-1 defaults, so an absent
447
+ * field *means* SHA-1 — which is why "assume the modern default" is not an option here and every
448
+ * field is resolved before the algorithm is accepted.
449
+ */
450
+ function parsePssParams(algo) {
451
+ const empty = { hashOid: OID.sha1, mgfHashOid: OID.sha1, saltLength: 20, trailer: 1 };
452
+ if (!algo.paramsTlv) return empty;
453
+ const bytes = algo.paramsBytes;
454
+ const seq = readAll(bytes, 'RSASSA-PSS-params');
455
+ expectTlv(seq, { tag: TAG.SEQUENCE, constructed: true }, 'RSASSA-PSS-params');
456
+ const out = { ...empty };
457
+ for (const field of children(bytes, seq, 'RSASSA-PSS-params')) {
458
+ if (field.cls !== CLS.CONTEXT || !field.constructed) {
459
+ throw parseError(field.start, 'RSASSA-PSS-params fields must be explicit context tags');
460
+ }
461
+ const inner = readTlv(bytes, field.contentStart);
462
+ if (inner.end !== field.contentEnd) throw parseError(inner.end, 'trailing bytes in PSS parameter');
463
+ switch (field.tag) {
464
+ case 0:
465
+ out.hashOid = parseAlgorithmIdentifier(bytes, inner, 'PSS hashAlgorithm').oid;
466
+ break;
467
+ case 1: {
468
+ const mgf = parseAlgorithmIdentifier(bytes, inner, 'PSS maskGenAlgorithm');
469
+ if (mgf.oid !== OID.mgf1 || !mgf.paramsTlv) {
470
+ throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
471
+ `RSA-PSS mask generation function ${mgf.oid} is not MGF1, refusing`, { oid: mgf.oid });
472
+ }
473
+ out.mgfHashOid = parseAlgorithmIdentifier(bytes, mgf.paramsTlv, 'MGF1 hash').oid;
474
+ break;
475
+ }
476
+ case 2: {
477
+ const { value, negative } = readInteger(bytes, inner, 'PSS saltLength');
478
+ if (negative || value === null) throw parseError(inner.start, 'PSS saltLength out of range');
479
+ out.saltLength = value;
480
+ break;
481
+ }
482
+ case 3: {
483
+ const { value } = readInteger(bytes, inner, 'PSS trailerField');
484
+ out.trailer = value;
485
+ break;
486
+ }
487
+ default:
488
+ throw parseError(field.start, `unknown RSASSA-PSS-params field [${field.tag}]`);
489
+ }
490
+ }
491
+ return out;
492
+ }
493
+
494
+ const PSS_BY_HASH = {
495
+ [OID.sha256]: { scheme: SIG_SCHEME.rsa_pss_rsae_sha256, hash: 'SHA-256', saltLength: 32 },
496
+ [OID.sha384]: { scheme: SIG_SCHEME.rsa_pss_rsae_sha384, hash: 'SHA-384', saltLength: 48 },
497
+ [OID.sha512]: { scheme: SIG_SCHEME.rsa_pss_rsae_sha512, hash: 'SHA-512', saltLength: 64 },
498
+ };
499
+
500
+ const DIRECT_SCHEMES = {
501
+ [OID.sha256WithRsa]: { kind: 'rsa-pkcs1', scheme: SIG_SCHEME.rsa_pkcs1_sha256, name: 'rsa_pkcs1_sha256' },
502
+ [OID.sha384WithRsa]: { kind: 'rsa-pkcs1', scheme: SIG_SCHEME.rsa_pkcs1_sha384, name: 'rsa_pkcs1_sha384' },
503
+ [OID.sha512WithRsa]: { kind: 'rsa-pkcs1', scheme: SIG_SCHEME.rsa_pkcs1_sha512, name: 'rsa_pkcs1_sha512' },
504
+ [OID.ed25519]: { kind: 'ed25519', scheme: SIG_SCHEME.ed25519, name: 'ed25519' },
505
+ };
506
+
507
+ const ECDSA_HASH = {
508
+ [OID.ecdsaWithSha256]: 'SHA-256',
509
+ [OID.ecdsaWithSha384]: 'SHA-384',
510
+ [OID.ecdsaWithSha512]: 'SHA-512',
511
+ };
512
+
513
+ /**
514
+ * How to verify one certificate signature. `scheme` indexes SIG_SCHEME_PARAMS where the OID
515
+ * fully determines it; for ECDSA only the hash is known here and path.js completes the plan
516
+ * from the issuer's curve.
517
+ * @typedef {object} SignaturePlan
518
+ * @property {'rsa-pkcs1' | 'rsa-pss' | 'ecdsa' | 'ed25519'} kind
519
+ * @property {number} [scheme]
520
+ * @property {'SHA-256' | 'SHA-384' | 'SHA-512'} [hash] weaker hashes died in the OID check
521
+ * @property {string} name for error messages
522
+ */
523
+
524
+ /**
525
+ * Map a certificate's signature algorithm to a verification plan, or throw.
526
+ *
527
+ * Called by path.js exactly when a certificate's signature is about to anchor trust, and by the
528
+ * OCSP checker for a response's own signature — the parameter is therefore the structural subset
529
+ * both can supply, and a full Certificate qualifies as-is. Weak algorithms (MD2/MD4/MD5, SHA-1)
530
+ * are rejected here by OID, before any cryptography runs — some runtimes' verifiers still accept
531
+ * SHA-1 and this one must provably not be among them. ECDSA returns only the hash: in X.509
532
+ * (unlike TLS) the curve belongs to the issuer's key, so path.js completes the plan from the
533
+ * issuer's SPKI.
534
+ *
535
+ * @param {{ signatureAlgorithm: AlgorithmId, subject: { text: string } }} cert
536
+ * @returns {SignaturePlan}
537
+ */
538
+ export function resolveSignatureScheme(cert) {
539
+ const { oid } = cert.signatureAlgorithm;
540
+ const weak = WEAK_SIG_OIDS[oid];
541
+ if (weak) {
542
+ throw new CertificateError(codes.CERT_SIGNATURE_WEAK,
543
+ `certificate "${cert.subject.text}" is signed with ${weak} (${oid}); ` +
544
+ 'MD-family and SHA-1 signatures are refused',
545
+ { oid, algorithm: weak, subject: cert.subject.text });
546
+ }
547
+ const direct = DIRECT_SCHEMES[oid];
548
+ if (direct) return direct;
549
+ const ecdsaHash = ECDSA_HASH[oid];
550
+ if (ecdsaHash) return { kind: 'ecdsa', hash: ecdsaHash, name: `ecdsa-with-${ecdsaHash}` };
551
+ if (oid === OID.rsassaPss) {
552
+ const p = parsePssParams(cert.signatureAlgorithm);
553
+ if (p.hashOid === OID.sha1 || p.mgfHashOid === OID.sha1) {
554
+ throw new CertificateError(codes.CERT_SIGNATURE_WEAK,
555
+ `certificate "${cert.subject.text}" uses RSA-PSS with SHA-1 ` +
556
+ `(hash ${p.hashOid}, MGF1 hash ${p.mgfHashOid}); SHA-1 signatures are refused`,
557
+ { oid, subject: cert.subject.text });
558
+ }
559
+ const entry = PSS_BY_HASH[p.hashOid];
560
+ if (!entry) {
561
+ throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
562
+ `RSA-PSS hash ${p.hashOid} on "${cert.subject.text}" is not supported`, { oid: p.hashOid });
563
+ }
564
+ // RFC 4055 consistency: MGF1 must run the same hash, the salt must be one hash-length, and
565
+ // the trailer must be 0xBC. Every real PSS certificate satisfies this; the exceptions are
566
+ // parameter-confusion experiments we refuse rather than approximate.
567
+ if (p.mgfHashOid !== p.hashOid) {
568
+ throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
569
+ `RSA-PSS with MGF1 hash ${p.mgfHashOid} != message hash ${p.hashOid}, refusing`,
570
+ { hash: p.hashOid, mgfHash: p.mgfHashOid });
571
+ }
572
+ if (p.saltLength !== entry.saltLength) {
573
+ throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
574
+ `RSA-PSS salt length ${p.saltLength} != hash length ${entry.saltLength}, refusing`,
575
+ { saltLength: p.saltLength, expected: entry.saltLength });
576
+ }
577
+ if (p.trailer !== 1) {
578
+ throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
579
+ `RSA-PSS trailer field ${p.trailer} != 1, refusing`, { trailer: p.trailer });
580
+ }
581
+ return { kind: 'rsa-pss', scheme: entry.scheme, hash: entry.hash, name: `rsa_pss_${entry.hash}` };
582
+ }
583
+ throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
584
+ `signature algorithm ${oid} on "${cert.subject.text}" is not supported`,
585
+ { oid, subject: cert.subject.text });
586
+ }
587
+
588
+ // ------------------------------------------------------------------ SPKI
589
+
590
+ /**
591
+ * A parsed SubjectPublicKeyInfo. `spkiDer` is the exact original element — the bytes WebCrypto
592
+ * imports and the bytes SPKI pinning hashes, so it must never be a re-encoding.
593
+ * @typedef {object} Spki
594
+ * @property {string} algorithmOid
595
+ * @property {string | null} curveOid named curve, EC keys only
596
+ * @property {Uint8Array} keyBytes the subjectPublicKey payload
597
+ * @property {Uint8Array} spkiDer
598
+ */
599
+
600
+ /**
601
+ * @param {Uint8Array} bytes
602
+ * @param {import('./der.js').Tlv} tlv
603
+ * @returns {Spki}
604
+ */
605
+ function parseSpki(bytes, tlv) {
606
+ expectTlv(tlv, { tag: TAG.SEQUENCE, constructed: true }, 'SubjectPublicKeyInfo');
607
+ const kids = children(bytes, tlv, 'SubjectPublicKeyInfo');
608
+ if (kids.length !== 2) {
609
+ throw parseError(tlv.start, 'SubjectPublicKeyInfo must be { algorithm, subjectPublicKey }');
610
+ }
611
+ const algo = parseAlgorithmIdentifier(bytes, kids[0], 'SPKI algorithm');
612
+ const { unusedBits, bytes: keyBytes } = readBitString(bytes, kids[1], 'subjectPublicKey');
613
+ // Every key type this package can use (RSA, EC points, Ed25519) is a whole number of octets.
614
+ if (unusedBits !== 0) throw parseError(kids[1].start, `subjectPublicKey has ${unusedBits} unused bits`);
615
+ let curveOid = null;
616
+ if (algo.oid === OID.ecPublicKey) {
617
+ if (!algo.paramsTlv || algo.paramsTlv.tag !== TAG.OID) {
618
+ // RFC 5480 allows explicit curve parameters; no public CA uses them, and accepting an
619
+ // attacker-described curve is a known validation trap. Named curves only.
620
+ throw parseError(kids[0].start, 'EC key without a named curve (explicit parameters are refused)');
621
+ }
622
+ curveOid = readOid(bytes, algo.paramsTlv, 'EC named curve');
623
+ }
624
+ return {
625
+ algorithmOid: algo.oid,
626
+ curveOid,
627
+ keyBytes,
628
+ spkiDer: element(bytes, tlv),
629
+ };
630
+ }
631
+
632
+ /**
633
+ * Parse a bare SubjectPublicKeyInfo element (as stored for trust anchors, which persist only the
634
+ * SPKI rather than a whole certificate). Same walk as inside a certificate.
635
+ * @param {Uint8Array} spkiDer
636
+ * @returns {Spki}
637
+ */
638
+ export function parseSubjectPublicKeyInfo(spkiDer) {
639
+ return parseSpki(spkiDer, readAll(spkiDer, 'SubjectPublicKeyInfo'));
640
+ }
641
+
642
+ // ------------------------------------------------------------------ certificate
643
+
644
+ /**
645
+ * The certificate's signatureAlgorithm, with parameters kept both raw and as a Tlv because
646
+ * RSA-PSS resolution has to re-walk them.
647
+ * @typedef {object} AlgorithmId
648
+ * @property {string} oid
649
+ * @property {Uint8Array | null} paramsBytes
650
+ * @property {import('./der.js').Tlv | null} paramsTlv
651
+ * @property {Uint8Array} bytes the whole AlgorithmIdentifier element
652
+ */
653
+
654
+ /**
655
+ * A fully parsed certificate. Frozen; every byte field is a subarray of the original `der`.
656
+ * This is the complete shape behind the trimmed `ParsedCertificate` documented on the public
657
+ * verifyChain surface.
658
+ * @typedef {object} Certificate
659
+ * @property {Uint8Array} der the original bytes, never re-encoded
660
+ * @property {Uint8Array} tbsBytes exact TBSCertificate slice the signature covers
661
+ * @property {number} version 1, 2 or 3
662
+ * @property {string} serialNumber hex of the INTEGER content bytes
663
+ * @property {boolean} serialNegative negative serials are misissuance but must still parse
664
+ * @property {AlgorithmId} signatureAlgorithm
665
+ * @property {Uint8Array} signature
666
+ * @property {DistinguishedName} issuer
667
+ * @property {DistinguishedName} subject
668
+ * @property {number} notBefore epoch ms
669
+ * @property {number} notAfter epoch ms
670
+ * @property {Spki} spki
671
+ * @property {Map<string, { critical: boolean, valueBytes: Uint8Array }>} extensions by OID
672
+ * @property {{ present: boolean, ca: boolean, pathLenConstraint: number | null }} basicConstraints
673
+ * @property {KeyUsage | null} keyUsage null when the extension is absent
674
+ * @property {ReadonlyArray<string> | null} extendedKeyUsage KeyPurposeId OIDs
675
+ * @property {SubjectAltNames} subjectAltNames
676
+ * @property {Uint8Array | null} subjectKeyIdentifier
677
+ * @property {Uint8Array | null} authorityKeyIdentifier keyIdentifier field only
678
+ * @property {NameConstraints | null} nameConstraints
679
+ * @property {ReadonlyArray<string>} unknownCriticalExtensions OIDs path.js must reject on
680
+ * @property {boolean} isSelfIssued subject DER equals issuer DER
681
+ */
682
+
683
+ /**
684
+ * Parse one DER certificate into a frozen, fully-walked structure. Throws CertificateError
685
+ * (CERT_PARSE) on malformed or self-contradictory encodings; judgement about what the
686
+ * certificate MAY do lives in path.js.
687
+ *
688
+ * `tbsBytes` is the exact original slice of the TBSCertificate element — signature verification
689
+ * happens over these bytes and never over anything re-encoded.
690
+ *
691
+ * @param {Uint8Array} der
692
+ * @returns {Certificate}
693
+ */
694
+ export function parseCertificate(der) {
695
+ if (!(der instanceof Uint8Array)) {
696
+ throw new CertificateError(codes.CERT_PARSE, 'certificate must be a Uint8Array of DER');
697
+ }
698
+ const cert = readAll(der, 'Certificate');
699
+ expectTlv(cert, { tag: TAG.SEQUENCE, constructed: true }, 'Certificate');
700
+ const [tbsTlv, sigAlgTlv, sigTlv, ...extraTop] = children(der, cert, 'Certificate');
701
+ if (!tbsTlv || !sigAlgTlv || !sigTlv || extraTop.length) {
702
+ throw parseError(cert.start,
703
+ 'Certificate must be { tbsCertificate, signatureAlgorithm, signatureValue }');
704
+ }
705
+ expectTlv(tbsTlv, { tag: TAG.SEQUENCE, constructed: true }, 'TBSCertificate');
706
+ const outerAlg = parseAlgorithmIdentifier(der, sigAlgTlv, 'signatureAlgorithm');
707
+ const { bytes: signature } = readBitString(der, sigTlv, 'signatureValue');
708
+
709
+ const fields = children(der, tbsTlv, 'TBSCertificate');
710
+ let i = 0;
711
+ const next = (what) => {
712
+ if (i >= fields.length) throw parseError(tbsTlv.end, `TBSCertificate ends before ${what}`);
713
+ return fields[i++];
714
+ };
715
+
716
+ // version [0] EXPLICIT INTEGER DEFAULT v1(0)
717
+ let version = 1;
718
+ if (fields[0] && fields[0].cls === CLS.CONTEXT && fields[0].tag === 0) {
719
+ const wrapper = next('version');
720
+ if (!wrapper.constructed) throw parseError(wrapper.start, 'version [0] must be constructed (EXPLICIT)');
721
+ const inner = readTlv(der, wrapper.contentStart);
722
+ if (inner.end !== wrapper.contentEnd) throw parseError(inner.end, 'trailing bytes in version');
723
+ const { value, negative } = readInteger(der, inner, 'version');
724
+ if (negative || value === null || value > 2) {
725
+ throw parseError(inner.start, `unsupported certificate version ${negative ? 'negative' : value}`);
726
+ }
727
+ // DER forbids encoding the DEFAULT: version v1 must be expressed by omission.
728
+ if (value === 0) throw parseError(inner.start, 'version v1 must be omitted, not encoded');
729
+ version = value + 1;
730
+ }
731
+
732
+ const serialTlv = next('serialNumber');
733
+ const serial = readInteger(der, serialTlv, 'serialNumber');
734
+ const innerAlg = parseAlgorithmIdentifier(der, next('signature'), 'TBSCertificate signature');
735
+
736
+ // RFC 5280 s4.1.1.2: outer signatureAlgorithm MUST equal the TBS signature field. A mismatch
737
+ // means someone re-wrapped a signed body under a different algorithm claim — the textbook
738
+ // algorithm-substitution forgery — so the comparison is over full DER bytes, not just OIDs.
739
+ if (!equal(outerAlg.bytes, innerAlg.bytes)) {
740
+ throw new CertificateError(codes.CERT_PARSE,
741
+ `signatureAlgorithm mismatch: outer ${outerAlg.oid} vs tbsCertificate ${innerAlg.oid}` +
742
+ (outerAlg.oid === innerAlg.oid ? ' (same OID, different parameters)' : ''),
743
+ { outer: outerAlg.oid, inner: innerAlg.oid });
744
+ }
745
+
746
+ const issuer = parseName(der, next('issuer'));
747
+ const validityTlv = next('validity');
748
+ expectTlv(validityTlv, { tag: TAG.SEQUENCE, constructed: true }, 'Validity');
749
+ const times = children(der, validityTlv, 'Validity');
750
+ if (times.length !== 2) throw parseError(validityTlv.start, 'Validity must be { notBefore, notAfter }');
751
+ // RFC 5280 wants UTCTime through 2049 and GeneralizedTime after; both encodings name an
752
+ // unambiguous instant, so a cert that picked the wrong one is accepted — rejecting it would
753
+ // add availability risk and zero security.
754
+ const notBefore = readTime(der, times[0], 'notBefore');
755
+ const notAfter = readTime(der, times[1], 'notAfter');
756
+ if (notAfter < notBefore) {
757
+ throw parseError(validityTlv.start,
758
+ `notAfter precedes notBefore (${new Date(notAfter).toISOString()} < ` +
759
+ `${new Date(notBefore).toISOString()})`);
760
+ }
761
+ const subject = parseName(der, next('subject'));
762
+ const spki = parseSpki(der, next('subjectPublicKeyInfo'));
763
+
764
+ // issuerUniqueID [1] / subjectUniqueID [2] IMPLICIT: legal in v2/v3, obsolete, skipped.
765
+ while (i < fields.length && fields[i].cls === CLS.CONTEXT &&
766
+ (fields[i].tag === 1 || fields[i].tag === 2)) {
767
+ if (version === 1) {
768
+ throw parseError(fields[i].start, 'unique identifiers are not allowed in v1 certificates');
769
+ }
770
+ i++;
771
+ }
772
+
773
+ const extensions = new Map();
774
+ let basicConstraints = Object.freeze({ present: false, ca: false, pathLenConstraint: null });
775
+ let keyUsage = null;
776
+ let extendedKeyUsage = null;
777
+ let subjectAltNames = Object.freeze({ present: false, dns: Object.freeze([]), ip: Object.freeze([]),
778
+ uri: Object.freeze([]), email: Object.freeze([]) });
779
+ let subjectKeyIdentifier = null;
780
+ let authorityKeyIdentifier = null;
781
+ let nameConstraints = null;
782
+ const unknownCriticalExtensions = [];
783
+
784
+ if (i < fields.length) {
785
+ const wrapper = fields[i++];
786
+ if (wrapper.cls !== CLS.CONTEXT || wrapper.tag !== 3 || !wrapper.constructed) {
787
+ throw parseError(wrapper.start, `unexpected TBSCertificate field ${tagName(wrapper.cls, wrapper.tag)}`);
788
+ }
789
+ if (version !== 3) {
790
+ throw parseError(wrapper.start, `extensions are not allowed in v${version} certificates`);
791
+ }
792
+ const listTlv = readTlv(der, wrapper.contentStart);
793
+ if (listTlv.end !== wrapper.contentEnd) throw parseError(listTlv.end, 'trailing bytes in extensions');
794
+ expectTlv(listTlv, { tag: TAG.SEQUENCE, constructed: true }, 'Extensions');
795
+ const extTlvs = children(der, listTlv, 'Extensions');
796
+ if (extTlvs.length === 0) throw parseError(listTlv.start, 'empty Extensions sequence');
797
+ for (const extTlv of extTlvs) {
798
+ expectTlv(extTlv, { tag: TAG.SEQUENCE, constructed: true }, 'Extension');
799
+ const parts = children(der, extTlv, 'Extension');
800
+ if (parts.length < 2 || parts.length > 3) {
801
+ throw parseError(extTlv.start, 'Extension must be { extnID, critical?, extnValue }');
802
+ }
803
+ const extnId = readOid(der, parts[0], 'extnID');
804
+ let critical = false;
805
+ let valueIdx = 1;
806
+ if (parts.length === 3) {
807
+ critical = readBoolean(der, parts[1], 'critical');
808
+ // DER: critical FALSE is the DEFAULT and must be omitted.
809
+ if (!critical) throw parseError(parts[1].start, 'critical FALSE must be omitted in DER');
810
+ valueIdx = 2;
811
+ }
812
+ expectTlv(parts[valueIdx], { tag: TAG.OCTET_STRING, constructed: false }, 'extnValue');
813
+ const valueBytes = content(der, parts[valueIdx]);
814
+ // RFC 5280 s4.2: at most one instance of each extension. Two copies of keyUsage is two
815
+ // policies for one key; whichever a validator reads, the other was the lie.
816
+ if (extensions.has(extnId)) {
817
+ throw parseError(extTlv.start, `duplicate extension ${extnId}`);
818
+ }
819
+ extensions.set(extnId, Object.freeze({ critical, valueBytes }));
820
+
821
+ switch (extnId) {
822
+ case OID.basicConstraints: basicConstraints = Object.freeze(parseBasicConstraints(valueBytes)); break;
823
+ case OID.keyUsage: keyUsage = parseKeyUsage(valueBytes); break;
824
+ case OID.extendedKeyUsage: extendedKeyUsage = parseExtendedKeyUsage(valueBytes); break;
825
+ case OID.subjectAltName: subjectAltNames = parseSubjectAltName(valueBytes); break;
826
+ case OID.subjectKeyIdentifier:
827
+ subjectKeyIdentifier = parseSubjectKeyIdentifier(valueBytes);
828
+ break;
829
+ case OID.authorityKeyIdentifier:
830
+ authorityKeyIdentifier = parseAuthorityKeyIdentifier(valueBytes);
831
+ break;
832
+ case OID.nameConstraints: nameConstraints = parseNameConstraints(valueBytes); break;
833
+ default:
834
+ if (critical && !KNOWN_EXTENSIONS.has(extnId)) unknownCriticalExtensions.push(extnId);
835
+ }
836
+ }
837
+ }
838
+ if (i !== fields.length) {
839
+ throw parseError(fields[i].start, 'unexpected trailing TBSCertificate field');
840
+ }
841
+
842
+ return Object.freeze({
843
+ der,
844
+ tbsBytes: element(der, tbsTlv),
845
+ version,
846
+ serialNumber: toHex(serial.bytes),
847
+ serialNegative: serial.negative,
848
+ signatureAlgorithm: Object.freeze({
849
+ oid: outerAlg.oid,
850
+ paramsBytes: outerAlg.paramsBytes,
851
+ paramsTlv: outerAlg.paramsTlv,
852
+ bytes: outerAlg.bytes,
853
+ }),
854
+ signature,
855
+ issuer: Object.freeze(issuer),
856
+ subject: Object.freeze(subject),
857
+ notBefore,
858
+ notAfter,
859
+ spki: Object.freeze(spki),
860
+ extensions,
861
+ basicConstraints,
862
+ keyUsage,
863
+ extendedKeyUsage,
864
+ subjectAltNames,
865
+ subjectKeyIdentifier,
866
+ authorityKeyIdentifier,
867
+ nameConstraints,
868
+ unknownCriticalExtensions: Object.freeze(unknownCriticalExtensions),
869
+ isSelfIssued: equal(subject.bytes, issuer.bytes),
870
+ });
871
+ }
872
+
873
+ // ------------------------------------------------------------------ PEM
874
+
875
+ const PEM_RE = /-----BEGIN CERTIFICATE-----([A-Za-z0-9+/=\s]+?)-----END CERTIFICATE-----/g;
876
+
877
+ /**
878
+ * Extract every CERTIFICATE block from PEM text as DER. Used for user-supplied trust anchors;
879
+ * TLS itself always delivers DER. Throws CERT_PARSE on bad base64 or when no block is found.
880
+ * @param {string} text
881
+ * @returns {Uint8Array[]}
882
+ */
883
+ export function decodePem(text) {
884
+ const out = [];
885
+ for (const match of String(text).matchAll(PEM_RE)) {
886
+ const b64 = match[1].replace(/\s+/g, '');
887
+ let bin;
888
+ try {
889
+ bin = atob(b64);
890
+ } catch {
891
+ throw new CertificateError(codes.CERT_PARSE,
892
+ `PEM certificate block ${out.length + 1} is not valid base64`);
893
+ }
894
+ const der = new Uint8Array(bin.length);
895
+ for (let j = 0; j < bin.length; j++) der[j] = bin.charCodeAt(j);
896
+ out.push(der);
897
+ }
898
+ if (out.length === 0) {
899
+ throw new CertificateError(codes.CERT_PARSE, 'no CERTIFICATE blocks found in PEM input');
900
+ }
901
+ return out;
902
+ }