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,595 @@
1
+ // Cost profile, measured on the target runtime, recorded because it is counter-intuitive enough
2
+ // that it has already sent one optimisation effort at the wrong target.
3
+ //
4
+ // Validating a chain costs ~3.5 ms for an all-ECDSA chain and ~0.8 ms for an RSA one. Almost all
5
+ // of that is signature verification, not parsing: parseCertificate over a whole chain is ~158 us,
6
+ // while a single ECDSA P-384 verify is ~665-816 us — 12x a P-256 verify (~56 us) and ~27x an
7
+ // RSA-2048 verify (~25 us). A typical EC chain carries two P-384 links, so two operations account
8
+ // for most of the total.
9
+ //
10
+ // This is not a runtime defect and it is not worth reporting as one: the same measurement in Node
11
+ // gives RSA 26 us, P-256 53 us, P-384 371 us, so P-384 is intrinsically expensive and this runtime
12
+ // is only ~2x slower at it. (An earlier investigation reported Node verifying all three in ~16 us
13
+ // and concluded the runtime had a pathological P-384 implementation; that Node measurement did not
14
+ // reproduce, and a P-384 verify faster than an RSA-2048 verify is not a plausible result.)
15
+ //
16
+ // The consequences that matter: there is no headroom here, because a signature check cannot be
17
+ // skipped and hand-rolling P-384 in JavaScript would be both slower and a new security-critical
18
+ // implementation this package exists to avoid. The cost is paid once per CONNECTION, so reuse is
19
+ // what amortises it — a pooled request is ~0.88 ms against ~9-12 ms for a new EC connection. And
20
+ // an origin whose certificate chain is RSA or P-256 validates several times cheaper here than one
21
+ // using P-384, which is worth knowing if you control the origin.
22
+
23
+ // RFC 5280 s6.1 certification path building and validation.
24
+ //
25
+ // The chain arrives leaf-first, but nothing else about it is trusted: real servers ship
26
+ // intermediates out of order, append irrelevant certificates, and include their own root. The
27
+ // path is therefore rebuilt here from the leaf up — issuer matched to subject by exact DN bytes,
28
+ // disambiguated by key identifiers, terminated at a caller-supplied trust anchor — and only the
29
+ // certificates on that rebuilt path are judged.
30
+ //
31
+ // A trust anchor is a (name, public key, constraints) triple per RFC 5280 s6.1.1, not a
32
+ // certificate: the anchor's own self-signature proves nothing (anyone can self-sign any name)
33
+ // and is never verified. For the same reason an anchor's notBefore/notAfter are recorded but not
34
+ // enforced — expiring a root out from under otherwise-valid chains is exactly the failure that
35
+ // took down half the internet when AddTrust expired; root lifetime is store-curation policy, not
36
+ // path validity.
37
+ //
38
+ // Deliberately not implemented, and why that is safe or announced rather than silent:
39
+ // * Policy processing (certificatePolicies / policyConstraints / inhibitAnyPolicy): with no
40
+ // required policy set, RFC 5280 policy processing cannot fail a path. policyConstraints and
41
+ // inhibitAnyPolicy — which would change that — are always critical and are NOT in
42
+ // KNOWN_EXTENSIONS, so a path carrying them is rejected, never quietly mis-validated.
43
+ // * Revocation FETCHING (CRL downloads, responder queries): unreachable from a metered edge
44
+ // runtime mid-handshake, and a privacy leak besides. Revocation is instead checked from a
45
+ // stapled OCSP response when the handshake carries one — see src/trust/ocsp.js for the
46
+ // verification and src/trust/index.js for the policy on absence.
47
+
48
+ import { CertificateError, ConfigError, codes } from '../errors.js';
49
+ import { equal, toHex } from '../util/bytes.js';
50
+ import { SIG_SCHEME, SIG_SCHEME_PARAMS } from '../tls/constants.js';
51
+ import {
52
+ OID, parseCertificate, parseNameConstraints, parseSubjectPublicKeyInfo, resolveSignatureScheme,
53
+ } from './x509.js';
54
+ import {
55
+ TAG,
56
+ readAll,
57
+ expectTlv,
58
+ children,
59
+ readInteger,
60
+ ecdsaDerToRaw as derEcdsaToRaw,
61
+ } from './der.js';
62
+ import { matchesIdentity, dnsWithinSubtree, ipWithinSubtree } from './name.js';
63
+
64
+ const constraintError = (message, detail) =>
65
+ new CertificateError(codes.CERT_CONSTRAINT, message, detail);
66
+
67
+ // ------------------------------------------------------------------ anchors
68
+
69
+ /**
70
+ * The RFC 5280 s6.1.1 trust-anchor triple, normalised. Validity bounds are recorded but never
71
+ * enforced — see the module comment for why expiring a root is store policy, not path policy.
72
+ * @typedef {object} TrustAnchor
73
+ * @property {Uint8Array} subjectBytes exact subject DN DER
74
+ * @property {string} subjectText
75
+ * @property {Uint8Array} spkiDer
76
+ * @property {Uint8Array | null} subjectKeyIdentifier
77
+ * @property {import('./x509.js').NameConstraints | null} nameConstraints
78
+ * @property {number | null} notBefore
79
+ * @property {number | null} notAfter
80
+ */
81
+
82
+ /**
83
+ * Anything normalizeAnchor accepts: DER, a parsed certificate, or an anchor-shaped record.
84
+ * The record form also admits `nameConstraintsBytes` (raw extnValue), which is how the bundled
85
+ * store defers parsing to the one anchor a handshake actually lands on.
86
+ * @typedef {object} AnchorRecord
87
+ * @property {Uint8Array} subjectBytes
88
+ * @property {Uint8Array} spkiDer
89
+ * @property {string} [subjectText]
90
+ * @property {Uint8Array | null} [subjectKeyIdentifier]
91
+ * @property {import('./x509.js').NameConstraints | null} [nameConstraints]
92
+ * @property {Uint8Array | null} [nameConstraintsBytes]
93
+ * @property {number | null} [notBefore]
94
+ * @property {number | null} [notAfter]
95
+ */
96
+
97
+ /** @typedef {Uint8Array | import('./x509.js').Certificate | AnchorRecord} AnchorLike */
98
+
99
+ /**
100
+ * An indexed anchor lookup: everything path building needs from a root store. The bundled
101
+ * store implements it with a subject-hash index so a handshake touches one anchor, not all.
102
+ * @typedef {{ forIssuer: (subjectDn: Uint8Array) => AnchorLike[] | Promise<AnchorLike[]> }}
103
+ * AnchorSource
104
+ */
105
+
106
+ /**
107
+ * Strip a parsed certificate down to the RFC 5280 s6.1.1 trust-anchor triple. Used for the
108
+ * `mode:'anchors'` knob and by the root-store generator, so both feed path validation through
109
+ * the identical shape.
110
+ * @param {import('./x509.js').Certificate} cert
111
+ * @returns {TrustAnchor}
112
+ */
113
+ export function anchorFromCertificate(cert) {
114
+ return Object.freeze({
115
+ subjectBytes: cert.subject.bytes,
116
+ subjectText: cert.subject.text,
117
+ spkiDer: cert.spki.spkiDer,
118
+ subjectKeyIdentifier: cert.subjectKeyIdentifier,
119
+ nameConstraints: cert.nameConstraints,
120
+ notBefore: cert.notBefore,
121
+ notAfter: cert.notAfter,
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Accept an anchor object (from roots.js or anchorFromCertificate), DER, or a parsed cert.
127
+ * @param {AnchorLike} a
128
+ * @param {number} [index]
129
+ * @returns {TrustAnchor}
130
+ */
131
+ function normalizeAnchor(a, index) {
132
+ if (a instanceof Uint8Array) return anchorFromCertificate(parseCertificate(a));
133
+ if (a && a.tbsBytes) return anchorFromCertificate(a);
134
+ if (a && a.subjectBytes instanceof Uint8Array && a.spkiDer instanceof Uint8Array) {
135
+ return {
136
+ subjectBytes: a.subjectBytes,
137
+ subjectText: a.subjectText ?? '<anchor>',
138
+ spkiDer: a.spkiDer,
139
+ subjectKeyIdentifier: a.subjectKeyIdentifier ?? null,
140
+ // roots.js stores the raw extension value and defers parsing to the one anchor a
141
+ // handshake actually lands on.
142
+ nameConstraints: a.nameConstraints ??
143
+ (a.nameConstraintsBytes ? parseNameConstraints(a.nameConstraintsBytes) : null),
144
+ notBefore: a.notBefore ?? null,
145
+ notAfter: a.notAfter ?? null,
146
+ };
147
+ }
148
+ throw new ConfigError(codes.CONFIG_INVALID,
149
+ `trust anchor at index ${index} is neither DER, a parsed certificate, nor an anchor object`);
150
+ }
151
+
152
+ /**
153
+ * Normalise `anchors` into a lookup source. An array is scanned; an object with `forIssuer` is
154
+ * used as-is — the bundled root store implements it with a subject-hash index so a handshake
155
+ * touches one anchor, not the whole store.
156
+ */
157
+ function toAnchorSource(anchors) {
158
+ if (anchors && typeof anchors.forIssuer === 'function') return anchors;
159
+ if (!Array.isArray(anchors)) {
160
+ throw new ConfigError(codes.CONFIG_INVALID,
161
+ 'anchors must be an array or an anchor source with forIssuer()');
162
+ }
163
+ const normalized = anchors.map(normalizeAnchor);
164
+ return {
165
+ forIssuer: async (dnBytes) => normalized.filter((a) => equal(a.subjectBytes, dnBytes)),
166
+ };
167
+ }
168
+
169
+ // ------------------------------------------------------------------ signature verification
170
+
171
+ const CURVE_TO_SCHEME = {
172
+ [OID.secp256r1]: SIG_SCHEME.ecdsa_secp256r1_sha256,
173
+ [OID.secp384r1]: SIG_SCHEME.ecdsa_secp384r1_sha384,
174
+ [OID.secp521r1]: SIG_SCHEME.ecdsa_secp521r1_sha512,
175
+ };
176
+
177
+ /**
178
+ * ECDSA-Sig-Value to the fixed-width r||s form WebCrypto verifies. The conversion itself lives in
179
+ * der.js because the TLS CertificateVerify path needs exactly the same one, and two copies of a
180
+ * signature parser is two chances to be subtly different.
181
+ */
182
+ function ecdsaDerToRaw(sig, orderLen, subject) {
183
+ return derEcdsaToRaw(sig, orderLen, (why) =>
184
+ new CertificateError(codes.CERT_SIGNATURE_INVALID,
185
+ `ECDSA signature on "${subject}" is malformed: ${why}`, { subject }));
186
+ }
187
+
188
+ /**
189
+ * Verify `cert`'s signature over its original to-be-signed bytes with the signer's public key.
190
+ *
191
+ * The scheme comes from resolveSignatureScheme (which is where MD5/SHA-1 die, before any
192
+ * cryptography runs). For ECDSA the curve belongs to the issuer's key, not the OID, so the
193
+ * WebCrypto import parameters are chosen from the issuer's SPKI and only the hash from the OID —
194
+ * both looked up in SIG_SCHEME_PARAMS rather than re-declared here.
195
+ *
196
+ * Exported (as verifySignedObject) for the OCSP checker: a BasicOCSPResponse is signed exactly
197
+ * like a certificate — an AlgorithmIdentifier, a BIT STRING over the original DER of a TBS
198
+ * element — and two implementations of "check an X.509-style signature" is two chances for one
199
+ * of them to be subtly the weaker. `cert` is therefore the structural subset both callers can
200
+ * supply: `{ tbsBytes, signature, signatureAlgorithm, subject: { text } }`, which a parsed
201
+ * Certificate satisfies as-is and the OCSP checker fakes up from response fields.
202
+ *
203
+ * @param {{ tbsBytes: Uint8Array, signature: Uint8Array,
204
+ * signatureAlgorithm: import('./x509.js').AlgorithmId,
205
+ * subject: { text: string } }} cert what was signed, certificate-shaped
206
+ * @param {Uint8Array} issuerSpkiDer the signer's SubjectPublicKeyInfo, DER
207
+ * @param {string} issuerText the signer's name, for error messages
208
+ * @returns {Promise<void>} every failure throws a typed CertificateError
209
+ */
210
+ export async function verifySignedObject(cert, issuerSpkiDer, issuerText) {
211
+ const scheme = resolveSignatureScheme(cert); // throws CERT_SIGNATURE_WEAK / _UNSUPPORTED
212
+ const issuerSpki = parseSubjectPublicKeyInfo(issuerSpkiDer);
213
+ const mismatch = (why) =>
214
+ new CertificateError(codes.CERT_SIGNATURE_INVALID,
215
+ `signature on "${cert.subject.text}" (${scheme.name}) cannot be by issuer "${issuerText}": ${why}`,
216
+ { subject: cert.subject.text, issuer: issuerText, scheme: scheme.name });
217
+
218
+ let importParams;
219
+ let verifyParams;
220
+ let sigBytes = cert.signature;
221
+ if (scheme.kind === 'ecdsa') {
222
+ if (issuerSpki.algorithmOid !== OID.ecPublicKey) throw mismatch('issuer key is not an EC key');
223
+ const schemeId = CURVE_TO_SCHEME[issuerSpki.curveOid];
224
+ if (!schemeId) {
225
+ throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
226
+ `issuer "${issuerText}" uses EC curve ${issuerSpki.curveOid}, which is not supported`,
227
+ { curveOid: issuerSpki.curveOid });
228
+ }
229
+ const table = SIG_SCHEME_PARAMS[schemeId];
230
+ importParams = table.import;
231
+ verifyParams = { name: 'ECDSA', hash: scheme.hash };
232
+ sigBytes = ecdsaDerToRaw(cert.signature, table.curveOrderLen, cert.subject.text);
233
+ } else if (scheme.kind === 'rsa-pkcs1') {
234
+ // An RSASSA-PSS-restricted key must never validate PKCS#1 v1.5 signatures (RFC 4055 s1.2) —
235
+ // accepting cross-protocol use of one key is a known signature-confusion primitive.
236
+ if (issuerSpki.algorithmOid !== OID.rsaEncryption) {
237
+ throw mismatch('issuer key is not an rsaEncryption key');
238
+ }
239
+ const table = SIG_SCHEME_PARAMS[scheme.scheme];
240
+ importParams = table.import;
241
+ verifyParams = table.verify;
242
+ } else if (scheme.kind === 'rsa-pss') {
243
+ if (issuerSpki.algorithmOid !== OID.rsaEncryption && issuerSpki.algorithmOid !== OID.rsassaPss) {
244
+ throw mismatch('issuer key is not an RSA key');
245
+ }
246
+ const table = SIG_SCHEME_PARAMS[scheme.scheme];
247
+ importParams = table.import;
248
+ verifyParams = table.verify;
249
+ } else if (scheme.kind === 'ed25519') {
250
+ if (issuerSpki.algorithmOid !== OID.ed25519) throw mismatch('issuer key is not an Ed25519 key');
251
+ importParams = SIG_SCHEME_PARAMS[SIG_SCHEME.ed25519].import;
252
+ verifyParams = SIG_SCHEME_PARAMS[SIG_SCHEME.ed25519].verify;
253
+ } else {
254
+ throw mismatch(`unhandled scheme kind ${scheme.kind}`);
255
+ }
256
+
257
+ let ok = false;
258
+ try {
259
+ const key = await crypto.subtle.importKey('spki', issuerSpkiDer, importParams, false, ['verify']);
260
+ ok = await crypto.subtle.verify(verifyParams, key, sigBytes, cert.tbsBytes);
261
+ } catch (e) {
262
+ throw mismatch(`WebCrypto refused the key or signature (${e?.message ?? e})`);
263
+ }
264
+ if (!ok) {
265
+ throw new CertificateError(codes.CERT_SIGNATURE_INVALID,
266
+ `signature on "${cert.subject.text}" by "${issuerText}" (${scheme.name}) did not verify`,
267
+ { subject: cert.subject.text, issuer: issuerText, scheme: scheme.name });
268
+ }
269
+ }
270
+
271
+ // ------------------------------------------------------------------ path building
272
+
273
+ /** Key-identifier compatibility: a constraint only when both sides actually carry one. */
274
+ const kidCompatible = (childAki, parentSki) => !childAki || !parentSki || equal(childAki, parentSki);
275
+
276
+ /**
277
+ * Build the path leaf-to-anchor. Anchors are preferred at every step, so a self-signed (or
278
+ * cross-signed) root the server helpfully included is simply never reached. Where several
279
+ * same-named candidates exist (cross-signs, key rollovers), signatures disambiguate; a lone
280
+ * candidate is accepted structurally and verified in the validation pass, where failure produces
281
+ * the precise CERT_SIGNATURE_INVALID rather than a vague "no path".
282
+ */
283
+ async function buildPath(certs, anchorSource, maxPathLength) {
284
+ const leaf = certs[0];
285
+ const path = [leaf];
286
+ const used = new Set([0]);
287
+ let current = leaf;
288
+ let sigFailure = null;
289
+
290
+ for (;;) {
291
+ if (path.length > maxPathLength) {
292
+ throw constraintError(
293
+ `certification path exceeded ${maxPathLength} certificates without reaching a trust anchor`,
294
+ { maxPathLength });
295
+ }
296
+
297
+ // 1) Trust anchors for the current issuer name. The anchor link is verified eagerly: it both
298
+ // selects among same-named anchors and lets a failed anchor fall back to a supplied
299
+ // cross-sign, which is how root rollovers actually deploy. Every anchor a source returns is
300
+ // re-normalised here — the bundled store hands back packed records whose name constraints
301
+ // are still raw bytes, and consuming those un-normalised would silently drop the
302
+ // constraints (found the hard way by the generator's integration test).
303
+ const anchorCandidates = (await anchorSource.forIssuer(current.issuer.bytes))
304
+ .map(normalizeAnchor)
305
+ .filter((a) => kidCompatible(current.authorityKeyIdentifier, a.subjectKeyIdentifier ?? null));
306
+ for (const anchor of anchorCandidates) {
307
+ try {
308
+ await verifySignedObject(current, anchor.spkiDer, anchor.subjectText ?? '<anchor>');
309
+ return { path, anchor, topVerified: true };
310
+ } catch (e) {
311
+ // Only "this particular key did not make this signature" keeps the search going; a weak
312
+ // or unsupported algorithm is a property of the child and no other parent can fix it.
313
+ if (e.code !== codes.CERT_SIGNATURE_INVALID) throw e;
314
+ sigFailure = e;
315
+ }
316
+ }
317
+
318
+ // 2) Supplied certificates.
319
+ const candidates = [];
320
+ for (let i = 0; i < certs.length; i++) {
321
+ if (used.has(i)) continue;
322
+ const c = certs[i];
323
+ if (!equal(c.subject.bytes, current.issuer.bytes)) continue;
324
+ if (!kidCompatible(current.authorityKeyIdentifier, c.subjectKeyIdentifier)) continue;
325
+ if (equal(c.der, current.der)) continue; // a duplicate of current can only build a loop
326
+ candidates.push(i);
327
+ }
328
+ let chosen = -1;
329
+ if (candidates.length === 1) {
330
+ chosen = candidates[0];
331
+ } else if (candidates.length > 1) {
332
+ for (const i of candidates) {
333
+ try {
334
+ await verifySignedObject(current, certs[i].spki.spkiDer, certs[i].subject.text);
335
+ chosen = i;
336
+ break;
337
+ } catch (e) {
338
+ if (e.code !== codes.CERT_SIGNATURE_INVALID) throw e;
339
+ sigFailure = e;
340
+ }
341
+ }
342
+ }
343
+ if (chosen === -1) {
344
+ // Dead end. Prefer the most specific story: a candidate existed but its key did not make
345
+ // the signature; the chain ends at an untrusted self-signed cert; or the issuer is simply
346
+ // absent from both the chain and the store.
347
+ if (sigFailure) throw sigFailure;
348
+ if (current.isSelfIssued) {
349
+ throw new CertificateError(codes.CERT_UNTRUSTED_ROOT,
350
+ `certification path ends at self-signed "${current.subject.text}", which is not a trust anchor`,
351
+ { subject: current.subject.text });
352
+ }
353
+ throw new CertificateError(codes.CERT_CHAIN_INCOMPLETE,
354
+ `no certificate for issuer "${current.issuer.text}" of "${current.subject.text}" was ` +
355
+ 'supplied, and no trust anchor has that subject',
356
+ { subject: current.subject.text, issuer: current.issuer.text });
357
+ }
358
+ used.add(chosen);
359
+ path.push(certs[chosen]);
360
+ current = certs[chosen];
361
+ }
362
+ }
363
+
364
+ // ------------------------------------------------------------------ name-constraint state
365
+
366
+ /**
367
+ * Constraint state as layers: each certificate that imposes permittedSubtrees adds one layer per
368
+ * name type, and a name is acceptable only if EVERY layer of its type covers it. Keeping layers
369
+ * instead of computing intersections is exactly RFC 5280's intersection semantics without the
370
+ * subtlety of intersecting suffix sets. Exclusions are a flat union — one hit anywhere rejects.
371
+ */
372
+ function makeConstraintState(anchor) {
373
+ const state = {
374
+ dnsPermittedLayers: [], // {bases: string[], by: string}[]
375
+ ipPermittedLayers: [], // {bases: {addr, mask}[], by: string}[]
376
+ dnsExcluded: [], // {base: string, by: string}
377
+ ipExcluded: [], // {addr, mask, by}
378
+ };
379
+ if (anchor.nameConstraints) {
380
+ addConstraints(state, anchor.nameConstraints, anchor.subjectText ?? '<anchor>', true);
381
+ }
382
+ return state;
383
+ }
384
+
385
+ function addConstraints(state, nc, by, critical) {
386
+ const collect = (subtrees, which) => {
387
+ const dns = [];
388
+ const ip = [];
389
+ for (const t of subtrees) {
390
+ if (t.type === 'dns') dns.push(t.value);
391
+ else if (t.type === 'ip') ip.push({ addr: t.addr, mask: t.mask });
392
+ else if (critical) {
393
+ // RFC 5280 s4.2.1.10: constraint forms we cannot enforce (directoryName, rfc822Name,
394
+ // URI, otherName, or out-of-spec minimum/maximum) in a CRITICAL extension must reject
395
+ // the path — silently ignoring a constraint the CA insisted on is failing open.
396
+ throw constraintError(
397
+ `"${by}" imposes a ${which} name constraint of an unsupported type in a critical ` +
398
+ 'nameConstraints extension; refusing to ignore it',
399
+ { by, which });
400
+ }
401
+ }
402
+ return { dns, ip };
403
+ };
404
+ if (nc.permitted) {
405
+ const { dns, ip } = collect(nc.permitted, 'permitted');
406
+ if (dns.length) state.dnsPermittedLayers.push({ bases: dns, by });
407
+ if (ip.length) state.ipPermittedLayers.push({ bases: ip, by });
408
+ }
409
+ if (nc.excluded) {
410
+ const { dns, ip } = collect(nc.excluded, 'excluded');
411
+ for (const base of dns) state.dnsExcluded.push({ base, by });
412
+ for (const e of ip) state.ipExcluded.push({ ...e, by });
413
+ }
414
+ }
415
+
416
+ /** Check every SAN name of `cert` against the accumulated constraint state. */
417
+ function checkConstraints(state, cert) {
418
+ const subject = cert.subject.text;
419
+ for (const name of cert.subjectAltNames.dns) {
420
+ for (const layer of state.dnsPermittedLayers) {
421
+ if (!layer.bases.some((b) => dnsWithinSubtree(name, b))) {
422
+ throw constraintError(
423
+ `dNSName "${name}" of "${subject}" is outside the permitted subtrees imposed by ` +
424
+ `"${layer.by}" (permitted: ${layer.bases.join(', ')})`,
425
+ { name, subject, by: layer.by });
426
+ }
427
+ }
428
+ for (const ex of state.dnsExcluded) {
429
+ if (dnsWithinSubtree(name, ex.base)) {
430
+ throw constraintError(
431
+ `dNSName "${name}" of "${subject}" is inside the subtree "${ex.base}" excluded by "${ex.by}"`,
432
+ { name, subject, by: ex.by });
433
+ }
434
+ }
435
+ }
436
+ for (const ip of cert.subjectAltNames.ip) {
437
+ const shown = toHex(ip);
438
+ for (const layer of state.ipPermittedLayers) {
439
+ if (!layer.bases.some((b) => ipWithinSubtree(ip, b.addr, b.mask))) {
440
+ throw constraintError(
441
+ `iPAddress ${shown} of "${subject}" is outside the permitted subtrees imposed by "${layer.by}"`,
442
+ { subject, by: layer.by });
443
+ }
444
+ }
445
+ for (const ex of state.ipExcluded) {
446
+ if (ipWithinSubtree(ip, ex.addr, ex.mask)) {
447
+ throw constraintError(
448
+ `iPAddress ${shown} of "${subject}" is inside a subtree excluded by "${ex.by}"`,
449
+ { subject, by: ex.by });
450
+ }
451
+ }
452
+ }
453
+ }
454
+
455
+ // ------------------------------------------------------------------ validation
456
+
457
+ /**
458
+ * Build and validate a certification path. Every failure throws a typed CertificateError;
459
+ * there is no boolean to forget to check.
460
+ *
461
+ * @param {object} opts
462
+ * @param {Array<Uint8Array | import('./x509.js').Certificate>} opts.chain DER (or
463
+ * already-parsed) certificates, leaf first, in whatever order and with whatever extras the
464
+ * server chose to send
465
+ * @param {AnchorLike[] | AnchorSource} opts.anchors trust anchors, or an indexed anchor source
466
+ * @param {string | null} [opts.hostname] identity to require of the leaf; omit to skip
467
+ * (index.js never omits)
468
+ * @param {number} [opts.now] epoch ms
469
+ * @param {number} [opts.maxPathLength] hard cap on path certificates — this runs on a metered
470
+ * runtime and a pathological chain must cost O(cap), not O(chain²)
471
+ * @returns {Promise<{ leaf: import('./x509.js').Certificate,
472
+ * path: import('./x509.js').Certificate[], anchor: TrustAnchor }>} parsed leaf, the
473
+ * validated path (leaf first), and the anchor that terminated it
474
+ */
475
+ export async function validatePath(
476
+ { chain, anchors, hostname = null, now = Date.now(), maxPathLength = 10 },
477
+ ) {
478
+ if (!Array.isArray(chain) || chain.length === 0) {
479
+ throw new CertificateError(codes.CERT_CHAIN_INCOMPLETE, 'the peer supplied no certificates');
480
+ }
481
+ const certs = chain.map((c, i) => {
482
+ if (c instanceof Uint8Array) return parseCertificate(c);
483
+ if (c && c.tbsBytes) return c;
484
+ throw new ConfigError(codes.CONFIG_INVALID, `chain[${i}] is neither DER nor a parsed certificate`);
485
+ });
486
+ const anchorSource = toAnchorSource(anchors);
487
+ const { path, anchor, topVerified } = await buildPath(certs, anchorSource, maxPathLength);
488
+
489
+ const state = makeConstraintState(anchor);
490
+ // RFC 5280 s6.1.4 (l): the working constraint starts at the path length; each non-self-issued
491
+ // intermediate consumes one slot, and any certificate may lower — never raise — the remainder.
492
+ let pathLenRemaining = { value: maxPathLength, by: null };
493
+
494
+ // Process from the certificate under the anchor down to the leaf, as s6.1.3/6.1.4 do: state
495
+ // (name constraints, path length) flows downward.
496
+ for (let j = path.length - 1; j >= 0; j--) {
497
+ const cert = path[j];
498
+ const isLeaf = j === 0;
499
+ const subject = cert.subject.text;
500
+
501
+ // RFC 5280 s6.1: an unrecognised critical extension anywhere on the path is a hard stop.
502
+ // This check runs before any use of the certificate — an extension we cannot read may change
503
+ // the meaning of everything we can.
504
+ if (cert.unknownCriticalExtensions.length > 0) {
505
+ throw constraintError(
506
+ `certificate "${subject}" carries unrecognised critical extension(s) ` +
507
+ `${cert.unknownCriticalExtensions.join(', ')}; RFC 5280 s6.1 requires rejection`,
508
+ { subject, oids: [...cert.unknownCriticalExtensions] });
509
+ }
510
+
511
+ const parent = j === path.length - 1 ? null : path[j + 1];
512
+ if (!(topVerified && parent === null)) {
513
+ await verifySignedObject(
514
+ cert,
515
+ parent ? parent.spki.spkiDer : anchor.spkiDer,
516
+ parent ? parent.subject.text : (anchor.subjectText ?? '<anchor>'),
517
+ );
518
+ }
519
+
520
+ if (now < cert.notBefore) {
521
+ throw new CertificateError(codes.CERT_NOT_YET_VALID,
522
+ `certificate "${subject}" is not valid until ${new Date(cert.notBefore).toISOString()} ` +
523
+ `(now ${new Date(now).toISOString()})`,
524
+ { subject, notBefore: cert.notBefore, now });
525
+ }
526
+ if (now > cert.notAfter) {
527
+ throw new CertificateError(codes.CERT_EXPIRED,
528
+ `certificate "${subject}" expired ${new Date(cert.notAfter).toISOString()} ` +
529
+ `(now ${new Date(now).toISOString()})`,
530
+ { subject, notAfter: cert.notAfter, now });
531
+ }
532
+
533
+ // Name constraints bind every certificate below the imposer. Self-issued intermediates are
534
+ // exempt (s6.1.3 (b)) — they re-certify the same CA, not a new name — but the leaf never is.
535
+ if (isLeaf || !cert.isSelfIssued) checkConstraints(state, cert);
536
+
537
+ if (!isLeaf) {
538
+ // s6.1.4 (k): every intermediate must be a v3 CA. A certificate without basicConstraints
539
+ // (v1 certs included) never gets to issue — "it is old" is not a capability grant.
540
+ if (!cert.basicConstraints.present || !cert.basicConstraints.ca) {
541
+ throw constraintError(
542
+ `certificate "${subject}" signed others but ` +
543
+ (cert.basicConstraints.present
544
+ ? 'its basicConstraints do not assert cA'
545
+ : 'has no basicConstraints extension'),
546
+ { subject });
547
+ }
548
+ // s6.1.4 (n): a CA that carries keyUsage must include keyCertSign.
549
+ if (cert.keyUsage && !cert.keyUsage.keyCertSign) {
550
+ throw constraintError(
551
+ `certificate "${subject}" signed others but its keyUsage lacks keyCertSign`, { subject });
552
+ }
553
+ // s6.1.4 (l)/(m): path length accounting. Self-issued intermediates do not consume a slot.
554
+ if (!cert.isSelfIssued) {
555
+ if (pathLenRemaining.value <= 0) {
556
+ throw constraintError(
557
+ `pathLenConstraint ${pathLenRemaining.limit} imposed by "${pathLenRemaining.by}" ` +
558
+ `does not allow "${subject}" to appear as a further intermediate`,
559
+ { subject, by: pathLenRemaining.by });
560
+ }
561
+ pathLenRemaining = { ...pathLenRemaining, value: pathLenRemaining.value - 1 };
562
+ }
563
+ const pl = cert.basicConstraints.pathLenConstraint;
564
+ if (pl !== null && pl < pathLenRemaining.value) {
565
+ pathLenRemaining = { value: pl, limit: pl, by: subject };
566
+ }
567
+ if (cert.nameConstraints) {
568
+ addConstraints(state, cert.nameConstraints, subject,
569
+ cert.extensions.get(OID.nameConstraints)?.critical ?? false);
570
+ }
571
+ } else {
572
+ // End-entity role checks. A leaf that is also a CA is legal (s6.1 has no prohibition);
573
+ // what matters is that its stated purposes include TLS server authentication.
574
+ if (cert.extendedKeyUsage &&
575
+ !cert.extendedKeyUsage.includes(OID.serverAuth) &&
576
+ !cert.extendedKeyUsage.includes(OID.anyExtendedKeyUsage)) {
577
+ throw constraintError(
578
+ `certificate "${subject}" has an extendedKeyUsage (${cert.extendedKeyUsage.join(', ')}) ` +
579
+ 'that does not include serverAuth or anyExtendedKeyUsage',
580
+ { subject, eku: [...cert.extendedKeyUsage] });
581
+ }
582
+ // Every key-exchange this package negotiates (TLS 1.3, TLS 1.2 ECDHE) authenticates the
583
+ // server with a handshake signature, which keyUsage must permit when present.
584
+ if (cert.keyUsage && !cert.keyUsage.digitalSignature) {
585
+ throw constraintError(
586
+ `certificate "${subject}" has a keyUsage without digitalSignature, which every ` +
587
+ 'supported key exchange requires',
588
+ { subject });
589
+ }
590
+ if (hostname !== null) matchesIdentity(cert, hostname);
591
+ }
592
+ }
593
+
594
+ return { leaf: path[0], path, anchor };
595
+ }