@blamejs/pki 0.6.14 → 0.6.16

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 (112) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/lib/acme.js +59 -815
  3. package/lib/asn1-der.js +7 -245
  4. package/lib/attrcert-sign.js +12 -260
  5. package/lib/attrcert-verify.js +1 -7
  6. package/lib/byte-reader.js +0 -8
  7. package/lib/byte-writer.js +0 -11
  8. package/lib/cbor-det.js +0 -54
  9. package/lib/cmc-build.js +15 -357
  10. package/lib/cmc-verify.js +6 -239
  11. package/lib/cmp-build.js +41 -317
  12. package/lib/cmp-session.js +73 -760
  13. package/lib/cmp-verify.js +32 -488
  14. package/lib/cms-compress.js +3 -32
  15. package/lib/cms-decrypt.js +29 -294
  16. package/lib/cms-encrypt.js +14 -234
  17. package/lib/cms-sign.js +29 -324
  18. package/lib/cms-verify.js +9 -500
  19. package/lib/composite-kem.js +2 -102
  20. package/lib/composite-sig.js +1 -77
  21. package/lib/constants.js +0 -249
  22. package/lib/crl-sign.js +9 -237
  23. package/lib/crl-verify.js +1 -10
  24. package/lib/crmf-sign.js +19 -198
  25. package/lib/crmf-verify.js +1 -12
  26. package/lib/csr-sign.js +3 -34
  27. package/lib/csr-verify.js +1 -12
  28. package/lib/ct.js +28 -253
  29. package/lib/edwards-point.js +10 -44
  30. package/lib/est.js +28 -764
  31. package/lib/framework-error.js +4 -182
  32. package/lib/guard-all.js +1 -58
  33. package/lib/guard-async.js +1 -36
  34. package/lib/guard-bytes.js +6 -650
  35. package/lib/guard-compress.js +5 -102
  36. package/lib/guard-crypto.js +1 -40
  37. package/lib/guard-der.js +2 -93
  38. package/lib/guard-encoding.js +2 -60
  39. package/lib/guard-header.js +1 -36
  40. package/lib/guard-identifier.js +7 -531
  41. package/lib/guard-intrinsic.js +1 -150
  42. package/lib/guard-json.js +10 -73
  43. package/lib/guard-limits.js +1 -77
  44. package/lib/guard-list.js +1 -49
  45. package/lib/guard-name.js +3 -209
  46. package/lib/guard-parsed.js +7 -335
  47. package/lib/guard-range.js +1 -71
  48. package/lib/guard-secret.js +1 -102
  49. package/lib/guard-text.js +1 -64
  50. package/lib/guard-time.js +1 -97
  51. package/lib/hpke.js +3 -105
  52. package/lib/http-digest.js +14 -142
  53. package/lib/http-retry-after.js +1 -47
  54. package/lib/http-transport.js +21 -131
  55. package/lib/inspect.js +30 -284
  56. package/lib/ip-utils.js +6 -47
  57. package/lib/jose.js +7 -142
  58. package/lib/key.js +2 -67
  59. package/lib/lint.js +9 -194
  60. package/lib/merkle.js +1 -27
  61. package/lib/mime.js +9 -82
  62. package/lib/ocsp-request-verify.js +1 -14
  63. package/lib/ocsp-verify.js +1 -82
  64. package/lib/ocsp.js +11 -169
  65. package/lib/oid.js +5 -337
  66. package/lib/path-validate.js +103 -1492
  67. package/lib/pbes2.js +2 -89
  68. package/lib/pkcs12-build.js +31 -379
  69. package/lib/pki-build.js +64 -290
  70. package/lib/rc2.js +2 -18
  71. package/lib/rfc3339.js +4 -27
  72. package/lib/scep.js +140 -286
  73. package/lib/schema-all.js +0 -122
  74. package/lib/schema-attrcert.js +4 -146
  75. package/lib/schema-c509.js +153 -802
  76. package/lib/schema-cmc.js +5 -220
  77. package/lib/schema-cmp.js +2 -223
  78. package/lib/schema-cms.js +13 -427
  79. package/lib/schema-crl.js +5 -68
  80. package/lib/schema-crmf.js +6 -167
  81. package/lib/schema-csr.js +1 -54
  82. package/lib/schema-csrattrs.js +2 -87
  83. package/lib/schema-engine.js +0 -152
  84. package/lib/schema-ocsp.js +0 -117
  85. package/lib/schema-pkcs12.js +2 -93
  86. package/lib/schema-pkcs8.js +3 -35
  87. package/lib/schema-pkix.js +24 -655
  88. package/lib/schema-smime.js +0 -54
  89. package/lib/schema-tsp.js +3 -99
  90. package/lib/schema-x509.js +4 -63
  91. package/lib/shbs.js +11 -95
  92. package/lib/sign-scheme.js +2 -97
  93. package/lib/sigstore.js +9 -169
  94. package/lib/sleep.js +2 -8
  95. package/lib/smime.js +15 -408
  96. package/lib/tls-cert-compress.js +0 -62
  97. package/lib/trust.js +19 -185
  98. package/lib/tsp-sign.js +16 -270
  99. package/lib/validator-all.js +1 -30
  100. package/lib/validator-attcert.js +2 -46
  101. package/lib/validator-cose.js +2 -153
  102. package/lib/validator-keydesc.js +1 -37
  103. package/lib/validator-sig.js +1 -46
  104. package/lib/validator-tls.js +1 -88
  105. package/lib/validator-tpm.js +19 -122
  106. package/lib/verify-seam.js +1 -24
  107. package/lib/webauthn-mds.js +3 -400
  108. package/lib/webauthn.js +12 -833
  109. package/lib/webcrypto.js +5 -485
  110. package/lib/x509-sign.js +4 -108
  111. package/package.json +1 -1
  112. package/sbom.cdx.json +6 -6
@@ -62,23 +62,12 @@ var edwardsPoint = require("./edwards-point");
62
62
 
63
63
  var PathError = errors.PathError;
64
64
  function E(code, message, cause) { return new PathError(code, message, cause); }
65
- // Every code placed into the public validate() verdict must be a path/* code.
66
- // A direct DER decode (resolveDescriptor's asn1.decode, run outside the ns-
67
- // wrapping schema engine) can throw a raw asn1/* Asn1Error; normalize any
68
- // non-path error code to the given path/* fallback so an internal domain code
69
- // never leaks into the documented verdict. The original error is kept as `error`.
70
65
  function pathCode(e, fallback) {
71
66
  return (e && typeof e.code === "string" && e.code.indexOf("path/") === 0) ? e.code : fallback;
72
67
  }
73
68
 
74
- // Membership decided by comparison rather than by a prototype method, through the shared guard.
75
- // Every use below is a RULE: whether a certificate's extended key usages cover the ones the caller
76
- // required, whether a policy is already expected at a node, whether anyPolicy is in the user's
77
- // initial set. See guard-list for what a replaced Array.prototype.indexOf did to this validator.
78
69
  var contains = guard.list.contains;
79
70
  var containsAll = guard.list.containsAll;
80
- // RFC 5280 6.1.4(a): a policyMappings extension MUST NOT map to or from anyPolicy. The scan for a
81
- // violating pair is the rule, so it routes through the guard for the same reason.
82
71
  function mapsAnyPolicy(mappings) {
83
72
  return guard.list.anyMatches(mappings, function (m) {
84
73
  return !!m && (m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy);
@@ -87,21 +76,7 @@ function mapsAnyPolicy(mappings) {
87
76
 
88
77
  var NS = pkix.makeNS("path", PathError, oid);
89
78
  var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
90
- // Validates a trust-anchor tuple's raw publicKey as a COMPLETE SubjectPublicKeyInfo (AlgorithmIdentifier +
91
- // key BIT STRING), not just a decodable first OID -- a structurally-incomplete SPKI must fail closed at the
92
- // anchor door, not slip through to a soft valid:false when key import later rejects it.
93
79
  var ANCHOR_SPKI_SCHEMA = pkix.spki(NS);
94
- // A trust anchor carries a CLOSED, producer-derived field set -- the shape pki.trust.parseCertdata documents
95
- // and path validation + trust dedup consume: name, publicKey, algorithm, parameters, purposes, distrustAfter,
96
- // subjectDer, label, mozillaCaPolicy. Normalizing a caller tuple reads each of those fields exactly once (by
97
- // property access, which follows the prototype chain, so an inherited restriction is kept) and builds a fresh
98
- // object from those reads -- publicKey pinned to a private copy, algorithm/parameters taken from the validated
99
- // SubjectPublicKeyInfo (never the caller's declared value), the validation-read metadata (name, purposes,
100
- // distrustAfter) materialized as DATA because validation reads each more than once and a live accessor could
101
- // answer inconsistently. entry's own shape is NEVER enumerated (no `in` / ownKeys / getOwnPropertyDescriptor)
102
- // and the prototype chain is never walked, so a Proxy anchor cannot use an enumeration trap to mutate state
103
- // mid-normalization and a hostile getPrototypeOf cannot be driven into a loop (Buffer.isBuffer invokes
104
- // getPrototypeOf once). A field the caller attached OUTSIDE the closed set is not carried. See toAnchor.
105
80
 
106
81
  var subtle = webcrypto.webcrypto.subtle;
107
82
 
@@ -125,82 +100,34 @@ var OID = {
125
100
  caIssuers: oid.byName("caIssuers"),
126
101
  };
127
102
 
128
- // The set of extension OIDs the validator processes. An unrecognized critical
129
- // extension outside this set fails the path (6.1.4(o), 6.1.5(e)).
130
- // extendedKeyUsage is recognized: the critical form is legal (4.2.1.12) and
131
- // appears in the wild (RFC 6960 4.2.2.2 delegated OCSP responders); its
132
- // structure is validated wherever it is critical, and key-purpose enforcement
133
- // is the caller's opt-in via opts.requiredEku (RFC 5280 6.1 defines no EKU
134
- // processing step -- the required purpose is application context).
135
103
  var PROCESSED_EXTENSIONS = {};
136
- // cRLDistributionPoints is processed: the CRL checker consults it for the
137
- // sec. 6.3.3 shard correspondence, and a critical instance (sec. 4.2.1.13 is a
138
- // SHOULD-non-critical) is structurally validated by
139
- // validateCriticalExtensionStructure via the registered decoder. freshestCRL
140
- // stays OUT: sec. 4.2.1.15 requires it non-critical and the validator does not
141
- // consult it (no delta merge), so a critical instance fails unrecognized.
142
- // qcStatements is deliberately left unprocessed: a critical QC statement (QcLimitValue reliance limit,
143
- // QcType certificate purpose) asserts qualified-certificate semantics a relying party MUST enforce, and
144
- // this validator does not enforce them (nor expose a handler to). Marking it processed would let a caller
145
- // treat a certificate as valid outside its asserted critical QC constraints, so a critical qcStatements
146
- // fails as an unrecognized-critical extension (RFC 5280 sec. 6.1.4); a non-critical instance is
147
- // informational and does not affect the verdict. The extension is still decoded for pki.inspect / lint.
148
104
  [OID.basicConstraints, OID.keyUsage, OID.nameConstraints, OID.certificatePolicies,
149
105
  OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName,
150
106
  OID.extKeyUsage, OID.cRLDistributionPoints].
151
107
  forEach(function (o) { PROCESSED_EXTENSIONS[o] = true; });
152
- // Frozen after seeding: this exact object is both consulted by the critical-extension check here and
153
- // exported (for pki.lint to stay consistent). A caller must not be able to add an OID, because that
154
- // would make an attacker's critical, decoder-less extension pass as "processed" and skip both the
155
- // unrecognized-critical check and structural validation. Freezing makes any such write a no-op.
156
108
  Object.freeze(PROCESSED_EXTENSIONS);
157
109
 
158
- // ---- signature verify bridge (NEW 6) ---------------------------------------
159
110
 
160
- // Signature-algorithm OID -> the WebCrypto verify descriptor + how to import
161
- // the issuer SPKI. Keyed via oid.byName so no dotted-decimal OID literal
162
- // appears in source (the registry owns arc<->name). The algorithm is a property
163
- // of the CERTIFICATE and the issuer key, never of a message-selected field
164
- // (CVE-2015-9235).
165
111
  var SIG_ALGS = {};
166
- // `params` is the REQUIRED AlgorithmIdentifier parameters shape: "null" (a DER
167
- // NULL must be present -- RSASSA-PKCS1-v1_5, RFC 4055 sec. 5) or "absent" (parameters
168
- // must be omitted -- ECDSA/EdDSA/ML-DSA, RFC 5758/8410). A cert deviating from
169
- // its algorithm's required shape is malformed and rejected before verify.
170
- // `sameKeyOid` marks the one-shot families whose public-key algorithm OID equals
171
- // the signature algorithm OID: EdDSA, ML-DSA, SLH-DSA. For these, Node's
172
- // WebCrypto imports an SPKI of another type under the requested name and verifies
173
- // with the real key (unlike RSA/ECDSA, where a mismatched SPKI is refused at
174
- // import), so the issuer-key <-> signature-algorithm consistency (RFC 9814 sec. 4)
175
- // must be checked structurally: the SPKI OID must equal the signature OID.
176
112
  function _sig(name, verify, imp, params, ecdsa, sameKeyOid) {
177
113
  var entry = { verify: verify, imp: imp, params: params };
178
114
  if (ecdsa) entry.ecdsa = true;
179
115
  if (sameKeyOid) entry.sameKeyOid = true;
180
- // EdDSA descriptors carry the Edwards curve id (6 = Ed25519, 7 = Ed448) so the verify path
181
- // validates the issuer point through the shared gate without re-branching on the algorithm name.
182
116
  if (verify.name === "Ed25519") entry.eddsa = 6;
183
117
  else if (verify.name === "Ed448") entry.eddsa = 7;
184
118
  SIG_ALGS[oid.byName(name)] = entry;
185
119
  }
186
- // RSASSA-PKCS1-v1_5 -- parameters MUST be NULL.
187
120
  _sig("sha256WithRSAEncryption", { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, "null");
188
121
  _sig("sha384WithRSAEncryption", { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, "null");
189
122
  _sig("sha512WithRSAEncryption", { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, "null");
190
- // ECDSA (hash in the OID; the curve comes from the imported key) -- params absent.
191
123
  _sig("ecdsaWithSHA256", { name: "ECDSA", hash: "SHA-256" }, { name: "ECDSA" }, "absent", true);
192
124
  _sig("ecdsaWithSHA384", { name: "ECDSA", hash: "SHA-384" }, { name: "ECDSA" }, "absent", true);
193
125
  _sig("ecdsaWithSHA512", { name: "ECDSA", hash: "SHA-512" }, { name: "ECDSA" }, "absent", true);
194
- // EdDSA (one-shot, no hash parameter) -- params absent; key OID == sig OID.
195
126
  _sig("Ed25519", { name: "Ed25519" }, { name: "Ed25519" }, "absent", false, true);
196
127
  _sig("Ed448", { name: "Ed448" }, { name: "Ed448" }, "absent", false, true);
197
- // ML-DSA (FIPS 204) -- params absent; key OID == sig OID.
198
128
  _sig("id-ml-dsa-44", { name: "ML-DSA-44" }, { name: "ML-DSA-44" }, "absent", false, true);
199
129
  _sig("id-ml-dsa-65", { name: "ML-DSA-65" }, { name: "ML-DSA-65" }, "absent", false, true);
200
130
  _sig("id-ml-dsa-87", { name: "ML-DSA-87" }, { name: "ML-DSA-87" }, "absent", false, true);
201
- // SLH-DSA (FIPS 205) -- params absent; key OID == sig OID. The twelve pure sets;
202
- // the RFC 9909 sec. 3 OID name maps to the WebCrypto set name by id-slh-dsa-<set> ->
203
- // SLH-DSA-<SET> (the webcrypto SLH_DSA_NODE keys). One-shot verify like ML-DSA.
204
131
  ["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
205
132
  "shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
206
133
  ].forEach(function (set) {
@@ -208,65 +135,36 @@ _sig("id-ml-dsa-87", { name: "ML-DSA-87" }, { name: "ML-DSA-87" }, "absent", fal
208
135
  _sig("id-slh-dsa-" + set, { name: wc }, { name: wc }, "absent", false, true);
209
136
  });
210
137
 
211
- // RSASSA-PSS resolves its hash + salt from the AlgorithmIdentifier parameters.
212
138
  var OID_RSA_PSS = oid.byName("rsassaPss");
213
139
  var OID_MGF1 = oid.byName("mgf1");
214
- // SHA-1 is deliberately ABSENT -- a SHA-1 signature (PKCS#1 or PSS) is rejected,
215
- // matching the no-sha1WithRSAEncryption posture (SHAttered chosen-prefix).
216
140
  var HASH_BY_OID = {};
217
141
  HASH_BY_OID[oid.byName("sha256")] = "SHA-256";
218
142
  HASH_BY_OID[oid.byName("sha384")] = "SHA-384";
219
143
  HASH_BY_OID[oid.byName("sha512")] = "SHA-512";
220
144
 
221
- // The order-aware ECDSA DER->P1363 converter + its CURVE_FIELD_BYTES / CURVE_ORDER tables now
222
- // live in validator-sig.js (validator.sig.ecdsaDerToP1363), shared with the composite engine.
223
145
 
224
- // The algorithm OID of an AlgorithmIdentifier SEQUENCE { algorithm OID,
225
- // parameters OPTIONAL }. STRICT: a universal SEQUENCE with the OID and AT MOST
226
- // one optional parameters element -- a bare [n]-wrapped OID (no SEQUENCE) or a
227
- // SEQUENCE carrying a spurious third element is malformed and must not be read
228
- // leniently as its named algorithm.
229
146
  function seqAlgOid(seq) {
230
147
  if (!seq || seq.tagClass !== "universal" || seq.tagNumber !== asn1.TAGS.SEQUENCE || !seq.children || seq.children.length < 1 || seq.children.length > 2) {
231
148
  throw E("path/unsupported-algorithm", "expected an AlgorithmIdentifier SEQUENCE { OID, parameters? }");
232
149
  }
233
150
  return asn1.read.oid(seq.children[0]);
234
151
  }
235
- // A hash AlgorithmIdentifier { OID, parameters? } whose parameters, when present,
236
- // MUST be DER NULL (RFC 4055 sec. 2.1 / RFC 5754) -- never a SEQUENCE or arbitrary
237
- // value. Used for the PSS hashAlgorithm and the MGF1 inner hash.
238
152
  function hashAlgOid(seq) {
239
153
  var o = seqAlgOid(seq);
240
154
  if (seq.children.length === 2) {
241
155
  var p = seq.children[1];
242
156
  if (p.tagClass !== "universal" || p.tagNumber !== asn1.TAGS.NULL) throw E("path/unsupported-algorithm", "hash AlgorithmIdentifier parameters must be NULL or absent (RFC 4055)");
243
- // A NULL is well-formed only with empty content (X.690 sec. 8.8.2); the tag check
244
- // alone would accept a non-empty NULL as valid parameters.
245
157
  try { asn1.read.nullValue(p); }
246
158
  catch (e) { throw E("path/unsupported-algorithm", "hash AlgorithmIdentifier NULL parameters must have empty content (RFC 4055)", e); }
247
159
  }
248
160
  return o;
249
161
  }
250
- // The hash OID inside an EXPLICIT [n] wrapper around a hash AlgorithmIdentifier.
251
162
  function explicitHashAlgOid(wrapper) {
252
- // Coverage residual -- unreachable: the sole caller (resolveRsaPss) already
253
- // asserts the EXPLICIT wrapper carries exactly one child before calling this, so
254
- // this identical inner check cannot fire; it is a local defense-in-depth backstop.
255
163
  if (!wrapper.children || wrapper.children.length !== 1) throw E("path/unsupported-algorithm", "malformed EXPLICIT hash AlgorithmIdentifier");
256
164
  return hashAlgOid(wrapper.children[0]);
257
165
  }
258
166
 
259
167
  function resolveRsaPss(paramsBytes) {
260
- // RSASSA-PSS-params ::= SEQUENCE { hashAlgorithm [0] DEFAULT sha1,
261
- // maskGenAlgorithm [1] DEFAULT mgf1SHA1, saltLength [2] INTEGER DEFAULT 20,
262
- // trailerField [3] DEFAULT 1 }. WebCrypto verifies with MGF1 keyed to the
263
- // same hash as the signature and trailerField 0xBC (1). A declared value
264
- // that deviates cannot be honored, so it is rejected; verifying it under
265
- // WebCrypto's defaults would be a signatureAlgorithm bypass.
266
- // RFC 4055 DEFAULTs are SHA-1 (hashAlgorithm and mgf1SHA1). Because SHA-1 is
267
- // rejected, an absent hashAlgorithm or maskGenAlgorithm would resolve to SHA-1
268
- // and must be REJECTED -- a supported PSS AlgorithmIdentifier must state both
269
- // explicitly, with the MGF1 hash matching the signature hash.
270
168
  var hash = null, saltLength = 20, mgfNode = null, trailer = 1;
271
169
  if (!paramsBytes) throw E("path/unsupported-algorithm", "RSASSA-PSS requires explicit parameters (the SHA-1 defaults are rejected)");
272
170
  var n = asn1.decode(paramsBytes);
@@ -276,34 +174,19 @@ function resolveRsaPss(paramsBytes) {
276
174
  var pssLastTag = -1;
277
175
  n.children.forEach(function (f) {
278
176
  if (f.tagClass !== "context") throw E("path/unsupported-algorithm", "RSASSA-PSS-params fields must be context-tagged (RFC 4055)");
279
- // Fields are the OPTIONAL [0..3], each at most once and in ascending DER
280
- // order; an unknown or repeated/out-of-order tag is malformed.
281
177
  if (f.tagNumber > 3 || f.tagNumber <= pssLastTag) throw E("path/unsupported-algorithm", "RSASSA-PSS-params has an unexpected, duplicate, or out-of-order field [" + f.tagNumber + "]");
282
178
  pssLastTag = f.tagNumber;
283
- // Every RSASSA-PSS-params field is an EXPLICIT [n] wrapper (constructed)
284
- // around exactly one value (an AlgorithmIdentifier or an INTEGER); a
285
- // primitive/childless or multi-child context field is malformed: reading
286
- // f.children[0] and ignoring the rest would accept non-DER parameters.
287
179
  if (!f.children || f.children.length !== 1) throw E("path/unsupported-algorithm", "malformed RSASSA-PSS parameter field [" + f.tagNumber + "] (an EXPLICIT wrapper carries exactly one value)");
288
180
  if (f.tagNumber === 0) {
289
181
  var h = explicitHashAlgOid(f);
290
182
  if (!HASH_BY_OID[h]) throw E("path/unsupported-algorithm", "unsupported RSASSA-PSS hash algorithm " + h);
291
183
  hash = HASH_BY_OID[h];
292
184
  } else if (f.tagNumber === 1) {
293
- mgfNode = f.children[0]; // MaskGenAlgorithm SEQUENCE { mgf1, HashAlgorithm }
185
+ mgfNode = f.children[0];
294
186
  } else if (f.tagNumber === 2) {
295
187
  var sl = asn1.read.integer(f.children[0]);
296
- // A negative saltLength must be rejected: the OpenSSL-backed verify shim
297
- // reads -1/-2/-3 as RSA_PSS_SALTLEN_DIGEST/AUTO/MAX, and AUTO (-2) accepts
298
- // a signature of ANY salt length -- defeating the salt-length binding.
299
- // The upper bound keeps the value exact through Number conversion: the
300
- // verifier binds to the salt length the certificate states, so a value
301
- // that would round is not verifiable material (no real salt exceeds the
302
- // modulus size, let alone this).
303
188
  saltLength = guard.range.uint31(sl, E, "path/unsupported-algorithm", "RSASSA-PSS saltLength");
304
189
  } else if (f.tagNumber === 3) {
305
- // Compared for equality with 1 below -- bound before conversion so an
306
- // oversized value cannot round on its way to the comparison.
307
190
  trailer = guard.range.uint31(asn1.read.integer(f.children[0]), E, "path/unsupported-algorithm", "RSASSA-PSS trailerField");
308
191
  }
309
192
  });
@@ -318,42 +201,23 @@ function resolveRsaPss(paramsBytes) {
318
201
  return { verify: { name: "RSA-PSS", saltLength: saltLength }, imp: { name: "RSA-PSS", hash: hash } };
319
202
  }
320
203
 
321
- // A DER NULL parameters field is the 2-byte 05 00.
322
204
  function isDerNull(p) { return p && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
323
205
 
324
206
  function resolveDescriptor(sigAlg) {
325
207
  if (sigAlg.oid === OID_RSA_PSS) return resolveRsaPss(sigAlg.parameters);
326
- // Composite ML-DSA: the OID-keyed registry + the parameters-absent check
327
- // (draft-ietf-lamps-pq-composite-sigs sec. 5.3) live in composite-sig.js, shared with
328
- // CMS. sameKeyOid enforces the RFC 9814 sec. 4 key<->signature OID consistency.
329
208
  var comp = compositeSig.resolveCompositeDescriptor(sigAlg, PathError, "path/unsupported-algorithm");
330
209
  if (comp) return comp;
331
210
  var d = SIG_ALGS[sigAlg.oid];
332
211
  if (!d) throw E("path/unsupported-algorithm", "no verify descriptor for signature algorithm " + (sigAlg.name || sigAlg.oid));
333
- // The signatureAlgorithm's parameters MUST match the algorithm's fixed shape:
334
- // RSASSA-PKCS1-v1_5 requires a NULL; ECDSA/EdDSA/ML-DSA require absence. A
335
- // deviating AlgorithmIdentifier is malformed and must not verify.
336
212
  var p = sigAlg.parameters;
337
213
  if (d.params === "null" && !isDerNull(p)) throw E("path/unsupported-algorithm", "signature algorithm parameters must be NULL (RFC 4055)");
338
214
  if (d.params === "absent" && p !== null && p !== undefined) throw E("path/unsupported-algorithm", "signature algorithm parameters must be absent (RFC 5758/8410)");
339
215
  return d;
340
216
  }
341
217
 
342
- // RFC 9814 sec. 4 issuer-key <-> signature-algorithm consistency (algorithm-confusion
343
- // defense). For the one-shot families whose public key shares the signature OID
344
- // (EdDSA, ML-DSA, SLH-DSA), Node's WebCrypto imports an SPKI of a DIFFERENT type
345
- // under the requested name and verifies with the real key, so an Ed25519-signed
346
- // certificate labeled SLH-DSA would otherwise validate. Enforce structurally:
347
- // the issuer SPKI's algorithm OID MUST equal the signature algorithm OID. (For
348
- // RSA/ECDSA -- different key vs signature OIDs -- WebCrypto's import already rejects
349
- // a mismatched key type, so `sameKeyOid` is not set and this is a no-op.)
350
218
  function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
351
219
  if (!d || !d.sameKeyOid) return;
352
220
  var keyOid;
353
- // The working-key SPKI is decodable on the shipped path (the anchor's is validated at toAnchor entry and
354
- // every chain SPKI is strict-parsed), so this catch is defense-in-depth -- unreachable through
355
- // pki.path.validate, kept to keep a typed verdict for any future caller that supplies raw key bytes. The
356
- // RFC 9814 sec. 4 algorithm-confusion check below stays reachable and covered.
357
221
  try { keyOid = asn1.read.oid(asn1.decode(spkiBytes).children[0].children[0]); }
358
222
  catch (e) { throw E("path/algorithm-mismatch", "cannot read the issuer public-key algorithm identifier", e); }
359
223
  if (keyOid !== sigOid) {
@@ -361,23 +225,8 @@ function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
361
225
  }
362
226
  }
363
227
 
364
- // ecdsaDerToP1363 relocated to validator-sig.js; the composite trad-ECDSA and classical ECDSA
365
- // paths call validator.sig.ecdsaDerToP1363(sig, curve, PathError, "path/bad-signature").
366
228
 
367
- // ---- composite ML-DSA signatures (draft-ietf-lamps-pq-composite-sigs) -------
368
- // The composite verify/sign engine + the COMPOSITE_ALGS OID-keyed registry live in
369
- // composite-sig.js (shared with CMS composite SignerInfo). Path validation composes it:
370
- // resolveDescriptor (above) delegates to compositeSig.resolveCompositeDescriptor, the
371
- // certificate + OCSP verify paths to compositeSig.compositeVerify, and
372
- // compositeKeyUsageCheck (below) enforces the sec. 5.2 signature-only keyUsage restriction.
373
229
 
374
- // draft-ietf-lamps-pq-composite-sigs sec. 5.2: a certificate whose SubjectPublicKeyInfo
375
- // carries a composite ML-DSA OID, if it has a keyUsage extension, MUST assert at least
376
- // one signature bit (digitalSignature / nonRepudiation / keyCertSign / cRLSign) and
377
- // MUST NOT assert any encryption or key-establishment bit. A composite ML-DSA key is a
378
- // signature-only key (ML-DSA cannot encrypt or agree, so a "dual usage" key is forbidden
379
- // even when the traditional component could encrypt). The caller invokes this only for a
380
- // composite-keyed certificate; an absent keyUsage places no restriction (RFC 5280 4.2.1.3).
381
230
  function compositeKeyUsageCheck(cert) {
382
231
  var ku;
383
232
  try { ku = decodeExt(cert, OID.keyUsage); }
@@ -395,17 +244,9 @@ function compositeKeyUsageCheck(cert) {
395
244
  return { ok: true };
396
245
  }
397
246
 
398
- // The ML-KEM SubjectPublicKeyInfo OIDs (RFC 9935 / FIPS 203). A certificate carrying one of
399
- // these keys is a KEM key-establishment certificate: it can neither sign nor agree.
400
247
  var ML_KEM_OIDS = {};
401
248
  ["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n) { ML_KEM_OIDS[oid.byName(n)] = true; });
402
249
 
403
- // RFC 9935 sec. 5: a certificate whose SubjectPublicKeyInfo carries an id-ml-kem-* OID, if it
404
- // has a keyUsage extension, MUST assert keyEncipherment as the only key usage set, since an
405
- // ML-KEM key is a key-establishment-only key (it cannot sign or agree, so no other bit is legitimate,
406
- // and an unnamed/reserved bit set alongside keyEncipherment is equally forbidden). The caller
407
- // invokes this only for an ML-KEM-keyed certificate; an absent keyUsage places no restriction
408
- // (RFC 5280 sec. 4.2.1.3). This also makes an ML-KEM "CA" (keyCertSign) an explicit reject.
409
250
  function kemKeyUsageCheck(cert) {
410
251
  var ku;
411
252
  try { ku = decodeExt(cert, OID.keyUsage); }
@@ -421,13 +262,6 @@ function kemKeyUsageCheck(cert) {
421
262
  return { ok: true };
422
263
  }
423
264
 
424
- // Import a descriptor's verification key, validating an EdDSA point before the import runs:
425
- // node/OpenSSL import a low-order (e.g. identity or all-zeroes) Ed25519/Ed448 SPKI without
426
- // complaint and such a key verifies a forged signature. Both the certificate path and the
427
- // revocation (CRL / OCSP-response) path import through this single seam, so neither can skip
428
- // the point gate; a low-order issuer / responder key fails the caller closed (a rejected
429
- // promise the caller maps to a bad verdict), never verifying a forged chain or a forged
430
- // revocation.
431
265
  function _importVerifyKey(spkiBytes, d) {
432
266
  try {
433
267
  if (d.eddsa) edwardsPoint.validateSpki(spkiBytes, d.eddsa, PathError, "path/bad-signature");
@@ -435,19 +269,13 @@ function _importVerifyKey(spkiBytes, d) {
435
269
  return subtle.importKey("spki", spkiBytes, d.imp, false, ["verify"]);
436
270
  }
437
271
 
438
- // Verify cert.signatureValue over cert.tbsBytes with the working public key.
439
272
  function builtinVerify(state, cert) {
440
273
  var d;
441
274
  try {
442
275
  d = resolveDescriptor(cert.signatureAlgorithm);
443
276
  assertKeyMatchesSigAlg(state.workingPublicKey, cert.signatureAlgorithm.oid, d);
444
277
  } catch (e) { return Promise.resolve({ ok: false, code: pathCode(e, "path/unsupported-algorithm"), error: e }); }
445
- // The signature is an octet-aligned BIT STRING (no unused bits) for every
446
- // supported algorithm; a non-zero unused-bit count is malformed.
447
278
  if (!guard.crypto.isOctetAligned(cert.signatureValue)) return Promise.resolve({ ok: false, code: "path/bad-signature" });
448
- // A composite signature verifies its ML-DSA and traditional halves and accepts
449
- // IFF both pass -- delegated to the composite combinator (which reuses this
450
- // file's ECDSA range-check + the same import/verify seam).
451
279
  if (d.composite) return compositeSig.compositeVerify(state.workingPublicKey, cert.signatureValue.bytes, cert.tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature");
452
280
  var key;
453
281
  return _importVerifyKey(state.workingPublicKey, d).then(function (k) {
@@ -458,20 +286,11 @@ function builtinVerify(state, cert) {
458
286
  }).then(function (ok) {
459
287
  return { ok: ok === true };
460
288
  }, function (e) {
461
- // A raw OpenSSL / WebCrypto fault (wrong key type for the declared
462
- // algorithm -- the algorithm-confusion case) is a signature failure, not a
463
- // path/* verdict of its own; only a PathError code is preserved.
464
289
  return { ok: false, code: pathCode(e, "path/bad-signature"), error: e };
465
290
  });
466
291
  }
467
292
 
468
- // ---- 7.1 name comparison ---------------------------------------------------
469
293
 
470
- // RFC 5280 sec. 7.1 canonical DN / RDN comparison, via the shared name guard: the
471
- // canonical form (case-fold + internal-whitespace collapse) and the embedded
472
- // control-byte reject (CVE-2009-2408 -> path/name-chaining) live once in
473
- // guard-name, so no path-validation caller can reintroduce a raw-byte DN
474
- // comparison that treats two RFC 5280-equal names as different.
475
294
  function dnEqual(rdnsA, rdnsB) {
476
295
  return guard.name.dnEqual(rdnsA, rdnsB, E, "path/name-chaining", "distinguished name");
477
296
  }
@@ -480,7 +299,6 @@ function rdnEqual(a, b) {
480
299
  return guard.name.rdnEqual(a, b, E, "path/name-chaining", "distinguished name");
481
300
  }
482
301
 
483
- // ---- extension access ------------------------------------------------------
484
302
 
485
303
  function findExt(cert, extOid) {
486
304
  for (var i = 0; i < cert.extensions.length; i++) {
@@ -489,7 +307,6 @@ function findExt(cert, extOid) {
489
307
  return null;
490
308
  }
491
309
 
492
- // Decode an extension value, mapping the typed decoder throw to a check.
493
310
  function decodeExt(cert, extOid) {
494
311
  var ext = findExt(cert, extOid);
495
312
  if (!ext) return null;
@@ -497,12 +314,6 @@ function decodeExt(cert, extOid) {
497
314
  return { critical: ext.critical, value: dec(ext.value) };
498
315
  }
499
316
 
500
- // RFC 5280 requires several CA-scoped extensions to be marked critical:
501
- // basicConstraints (4.2.1.9), nameConstraints (4.2.1.10),
502
- // policyConstraints (4.2.1.11), inhibitAnyPolicy (4.2.1.14). A conforming
503
- // validator rejects the non-critical form -- an extension a non-supporting
504
- // relying party would ignore must not silently pass here either. Returns a
505
- // typed PathError when a PRESENT extension is not critical, else null.
506
317
  function requireCriticalExt(ext, name, checks) {
507
318
  if (ext && ext.critical !== true) {
508
319
  checks.push({ name: name, ok: false, code: "path/extension-not-critical" });
@@ -511,14 +322,7 @@ function requireCriticalExt(ext, name, checks) {
511
322
  return null;
512
323
  }
513
324
 
514
- // ---- name constraints ------------------------------------------------------
515
325
 
516
- // Split an addr-spec into [localPart, host] at its single "@". Returns null when
517
- // there is no "@" (a bare host/domain constraint), or "ambiguous" when there are
518
- // multiple "@": a conformant certificate rfc822Name is a simple addr-spec with
519
- // exactly one "@" (RFC 5280 sec. 4.2.1.6 deprecates the quoted local part, and an
520
- // addr-spec domain never contains "@"), so a multi-"@" mailbox like
521
- // "a@b"@example.com cannot be split reliably and must fail closed.
522
326
  function splitMailbox(addr) {
523
327
  var first = addr.indexOf("@");
524
328
  if (first === -1) return null;
@@ -527,24 +331,14 @@ function splitMailbox(addr) {
527
331
  }
528
332
 
529
333
  function emailMatch(constraint, mailbox) {
530
- // RFC 5280 sec. 4.2.1.10 rfc822Name: a constraint with an "@" is a full mailbox;
531
- // a leading "." is a domain matching mailboxes at a SUBDOMAIN; otherwise it is
532
- // a host matching mailboxes AT that host only. RFC 5321: the local part is
533
- // CASE-SENSITIVE (exact); only the host is compared case-insensitively.
534
334
  var mb = splitMailbox(mailbox);
535
- if (mb === "ambiguous") return "unsupported"; // multi-"@" mailbox -> fail closed
335
+ if (mb === "ambiguous") return "unsupported";
536
336
  if (constraint.indexOf("@") !== -1) {
537
- // Full-mailbox constraint: exact local part + case-insensitive host. The
538
- // host is canonicalized like dNSName/URI (strip the absolute-FQDN root dot)
539
- // so a trailing-dot mailbox cannot escape the constraint.
540
337
  var cb = splitMailbox(constraint);
541
338
  if (cb === "ambiguous" || cb === null || mb === null) return "unsupported";
542
339
  return mb[0] === cb[0] && stripTrailingDot(mb[1].toLowerCase()) === stripTrailingDot(cb[1].toLowerCase());
543
340
  }
544
- // Host/domain constraint: compare the mailbox host case-insensitively, with the
545
- // trailing FQDN root dot stripped on both sides (as hostConstraintMatch does)
546
- // so "user@evil.com." does not slip a constraint on "evil.com".
547
- if (mb === null) return "unsupported"; // no host -> cannot determine domain
341
+ if (mb === null) return "unsupported";
548
342
  var host = stripTrailingDot(mb[1].toLowerCase());
549
343
  if (host === "") return "unsupported";
550
344
  var c = stripTrailingDot(constraint.toLowerCase());
@@ -552,63 +346,44 @@ function emailMatch(constraint, mailbox) {
552
346
  return host === c;
553
347
  }
554
348
 
555
- // Strip a single trailing dot (the absolute-FQDN root label) so "evil.com."
556
- // and "evil.com" compare equal. Without that, a trailing-dot SAN would escape
557
- // a dNSName constraint.
558
349
  function stripTrailingDot(s) { return s.charAt(s.length - 1) === "." ? s.slice(0, -1) : s; }
559
350
 
560
- // Host-suffix match with the RFC 5280 sec. 4.2.1.10 leading-period rule shared by
561
- // dNSName and uniformResourceIdentifier constraints on a host.
562
351
  function hostConstraintMatch(constraint, host) {
563
352
  var c = stripTrailingDot(constraint.toLowerCase()), h = stripTrailingDot(host.toLowerCase());
564
353
  if (c === "") return true;
565
- if (c.charAt(0) === ".") return h.length > c.length && h.slice(-c.length) === c; // subdomain only
566
- return h === c || (h.length > c.length && h.slice(-(c.length + 1)) === "." + c); // host + subdomains
567
- }
568
-
569
- // Is `host` a fully qualified domain name (as a URI host constraint requires,
570
- // RFC 5280 sec. 4.2.1.10)? A dotless single label (localhost), an IPv4/IPv6 literal,
571
- // or a value carrying non-hostname characters (a scheme "://", a path "/", a
572
- // port ":") is not a FQDN and cannot be matched against a domain-suffix
573
- // constraint. Only [A-Za-z0-9.-] with at least one dot qualifies.
574
- // A code-point membership table built from a string of the allowed characters, and the shared scan that
575
- // checks every character of a non-empty string against one -- replacing the hostname-charset regexes
576
- // through one loop (no per-predicate character-class chain).
354
+ if (c.charAt(0) === ".") return h.length > c.length && h.slice(-c.length) === c;
355
+ return h === c || (h.length > c.length && h.slice(-(c.length + 1)) === "." + c);
356
+ }
357
+
577
358
  function _charTable(chars) { var t = []; for (var i = 0; i < chars.length; i++) t[_charCodeAt(chars, i)] = true; return t; }
578
- var _FQDN_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"); // /^[a-z0-9.-]+$/i
579
- var _DIGDOT_TABLE = _charTable("0123456789."); // /^[0-9.]+$/
359
+ var _FQDN_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-");
360
+ var _DIGDOT_TABLE = _charTable("0123456789.");
580
361
  function _allCharsIn(s, table) {
581
362
  if (s.length === 0) return false;
582
363
  for (var i = 0; i < s.length; i++) if (!table[_charCodeAt(s, i)]) return false;
583
364
  return true;
584
365
  }
585
- // /^[0-9]/ : begins with an ASCII digit (a dotted-OID attempt, vs an EKU / purpose name).
586
366
  function _startsWithDigit(s) { if (s.length === 0) return false; var c = _charCodeAt(s, 0); return c >= 48 && c <= 57; }
587
367
 
588
- var _SCHEME_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-"); // [a-z0-9+.-]
589
- // /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/i : the [start, end) range of the authority (up to the first / ? #
590
- // or end), or null when the input is not a scheme "//" URI. Returns a range so the caller slices once
591
- // (the slice that replaces the removed .exec), not a fresh substring here.
368
+ var _SCHEME_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-");
592
369
  function _uriAuthorityRange(uri) {
593
370
  var n = uri.length;
594
371
  if (n === 0) return null;
595
372
  var c0 = _charCodeAt(uri, 0);
596
- if (!((c0 >= 65 && c0 <= 90) || (c0 >= 97 && c0 <= 122))) return null; // scheme starts with a letter
373
+ if (!((c0 >= 65 && c0 <= 90) || (c0 >= 97 && c0 <= 122))) return null;
597
374
  var p = 1;
598
375
  while (p < n && _SCHEME_TABLE[_charCodeAt(uri, p)]) p += 1;
599
- if (_charCodeAt(uri, p) !== 58 || _charCodeAt(uri, p + 1) !== 47 || _charCodeAt(uri, p + 2) !== 47) return null; // "://"
376
+ if (_charCodeAt(uri, p) !== 58 || _charCodeAt(uri, p + 1) !== 47 || _charCodeAt(uri, p + 2) !== 47) return null;
600
377
  p += 3;
601
378
  var authStart = p;
602
- while (p < n) { var d = _charCodeAt(uri, p); if (d === 47 || d === 63 || d === 35) break; p += 1; } // [^/?#]*
379
+ while (p < n) { var d = _charCodeAt(uri, p); if (d === 47 || d === 63 || d === 35) break; p += 1; }
603
380
  return [authStart, p];
604
381
  }
605
- // /:\d+$/ : the index at which a trailing ":" + digits port starts (to slice off), or s.length when absent.
606
382
  function _portStripIndex(s) {
607
383
  var i = s.length;
608
384
  while (i > 0) { var c = _charCodeAt(s, i - 1); if (c >= 48 && c <= 57) i -= 1; else break; }
609
385
  return (i < s.length && i > 0 && _charCodeAt(s, i - 1) === 58) ? i - 1 : s.length;
610
386
  }
611
- // /^\[(.*)\]$/ -> "$1" : an IPv6 literal's surrounding "[" "]" removed, else the string unchanged.
612
387
  function _stripBrackets(s) {
613
388
  var n = s.length;
614
389
  return (n >= 2 && _charCodeAt(s, 0) === 91 && _charCodeAt(s, n - 1) === 93) ? s.slice(1, n - 1) : s;
@@ -616,25 +391,17 @@ function _stripBrackets(s) {
616
391
 
617
392
  function isFqdnHost(host) {
618
393
  var h = stripTrailingDot(host);
619
- if (h === "" || h.indexOf(".") === -1) return false; // empty or single-label (localhost)
620
- if (!_allCharsIn(h, _FQDN_TABLE)) return false; // scheme/path/port/IPv6 chars, "@", etc.
621
- if (_allCharsIn(h, _DIGDOT_TABLE)) return false; // IPv4 dotted-quad literal
394
+ if (h === "" || h.indexOf(".") === -1) return false;
395
+ if (!_allCharsIn(h, _FQDN_TABLE)) return false;
396
+ if (_allCharsIn(h, _DIGDOT_TABLE)) return false;
622
397
  return true;
623
398
  }
624
399
 
625
- // A URI constraint applies to the host part: a leading "." matches subdomains
626
- // only; a bare host matches that host exactly (not subdomains, per sec. 4.2.1.10).
627
- // Both sides must be a fully qualified domain name. A URI SAN with no host / an
628
- // IP literal, or a malformed constraint that is not an FQDN (e.g. a full URI
629
- // "http://blocked.example" where the required form is "blocked.example"), cannot
630
- // be evaluated, so it returns "unsupported" and the caller fails closed; reporting
631
- // it as a plain non-match would let the name silently escape a critical constraint.
632
400
  function uriMatch(constraint, uri) {
633
401
  var host = uriHost(uri);
634
402
  if (host === null) return "unsupported";
635
403
  if (!isFqdnHost(host)) return "unsupported";
636
404
  var c = stripTrailingDot(constraint.toLowerCase()), h = stripTrailingDot(host.toLowerCase());
637
- // Validate the constraint's own host form (strip a single leading "." domain marker).
638
405
  if (!isFqdnHost(c.charAt(0) === "." ? c.slice(1) : c)) return "unsupported";
639
406
  if (c.charAt(0) === ".") return h.length > c.length && h.slice(-c.length) === c;
640
407
  return h === c;
@@ -644,20 +411,14 @@ function uriHost(uri) {
644
411
  var r = _uriAuthorityRange(uri);
645
412
  if (r === null) return null;
646
413
  var authority = uri.slice(r[0], r[1]);
647
- // authority = [ userinfo "@" ] host [ ":" port ]. RFC 3986 userinfo does not
648
- // contain a raw "@", so a conformant authority has at most one "@". With more
649
- // than one the host cannot be determined reliably, so this fails closed
650
- // (null -> uriMatch returns "unsupported" -> the caller rejects the path);
651
- // guessing a host could slip a URI name constraint.
652
414
  var firstAt = authority.indexOf("@");
653
415
  if (firstAt !== authority.lastIndexOf("@")) return null;
654
416
  if (firstAt !== -1) authority = authority.slice(firstAt + 1);
655
417
  var host = authority.slice(0, _portStripIndex(authority));
656
- return host === "" ? null : host; // an empty authority cannot be evaluated -> unsupported
418
+ return host === "" ? null : host;
657
419
  }
658
420
 
659
421
  function ipMatch(constraint, addr) {
660
- // constraint = address+mask (8 or 32 octets); addr = 4 or 16.
661
422
  var half = constraint.length / 2;
662
423
  if (addr.length !== half) return false;
663
424
  for (var i = 0; i < half; i++) {
@@ -666,25 +427,18 @@ function ipMatch(constraint, addr) {
666
427
  return true;
667
428
  }
668
429
 
669
- // Does GeneralName-like {tagNumber, value} match a constraint base of the same
670
- // form? Returns true/false for a supported form, null when the forms differ
671
- // (not comparable), or the string "unsupported" when the forms match but this
672
- // validator does not implement that form's comparison. The caller must then fail
673
- // closed: an unenforceable constraint cannot be read as "no match".
674
430
  function nameMatchesConstraint(gnTag, gnValue, base) {
675
- if (base.tagNumber !== gnTag) return null; // different form -> not comparable
431
+ if (base.tagNumber !== gnTag) return null;
676
432
  switch (gnTag) {
677
- case 1: return emailMatch(base.value, gnValue); // rfc822Name
678
- case 2: return hostConstraintMatch(base.value, gnValue); // dNSName
679
- case 6: return uriMatch(base.value, gnValue); // uniformResourceIdentifier
680
- case 7: return ipMatch(base.value, gnValue); // iPAddress
681
- case 4: return dnStartsWith(gnValue, base.value); // directoryName
682
- default: return "unsupported"; // otherName / x400 / ediParty / registeredID
433
+ case 1: return emailMatch(base.value, gnValue);
434
+ case 2: return hostConstraintMatch(base.value, gnValue);
435
+ case 6: return uriMatch(base.value, gnValue);
436
+ case 7: return ipMatch(base.value, gnValue);
437
+ case 4: return dnStartsWith(gnValue, base.value);
438
+ default: return "unsupported";
683
439
  }
684
440
  }
685
441
 
686
- // directoryName constraint: the name DN must contain the constraint DN as an
687
- // initial RDN sequence.
688
442
  function dnStartsWith(nameDn, constraintDn) {
689
443
  if (constraintDn.rdns.length > nameDn.rdns.length) return false;
690
444
  for (var i = 0; i < constraintDn.rdns.length; i++) {
@@ -693,31 +447,16 @@ function dnStartsWith(nameDn, constraintDn) {
693
447
  return true;
694
448
  }
695
449
 
696
- // Collect the name forms a cert presents for constraint checking: its SAN
697
- // entries plus (per 4.2.1.10 / 6.1.3) an emailAddress in the subject DN as an
698
- // rfc822Name, plus the subject DN itself as a directoryName.
699
450
  function certNameForms(cert) {
700
451
  var forms = [];
701
452
  var san = decodeExt(cert, OID.subjectAltName);
702
453
  var hasRfc822San = false;
703
454
  if (san) {
704
- // Preserve every SAN entry, including a form whose value the validator does
705
- // not decode (x400Address [3] / ediPartyName [5]); dropping one would let a
706
- // critical constraint of that same unsupported form pass unenforced. The
707
- // constraint check fails such a form closed (name-constraint-unsupported).
708
455
  san.value.names.forEach(function (nm) {
709
- if (nm.tagNumber === 1) hasRfc822San = true; // an rfc822Name SAN carries the email identity
710
- // Coverage residual -- the `undefined -> null` arm is unreachable: the SAN
711
- // decoder (schema-pkix altName, decodeValue:true) routes through generalName,
712
- // which already maps an undecoded value to null, so nm.value is never undefined.
456
+ if (nm.tagNumber === 1) hasRfc822San = true;
713
457
  forms.push({ tag: nm.tagNumber, value: nm.value === undefined ? null : nm.value });
714
458
  });
715
459
  }
716
- // RFC 5280 sec. 4.2.1.10: the legacy emailAddress in the subject DN is checked as
717
- // an rfc822Name unless the SAN already carries the email identity as an
718
- // rfc822Name entry. A SAN of a different form (e.g. dNSName only) does not
719
- // cover the email, so the subject-DN email must still be constrained. Otherwise
720
- // an excluded/non-permitted mailbox would slip an rfc822Name constraint.
721
460
  if (!hasRfc822San) {
722
461
  cert.subject.rdns.forEach(function (rdn) {
723
462
  rdn.forEach(function (atv) {
@@ -729,18 +468,8 @@ function certNameForms(cert) {
729
468
  return forms;
730
469
  }
731
470
 
732
- // Check a cert's own names against the accumulated constraints. `excluded` is
733
- // a union (any match rejects). `permitted` is the intersection of every
734
- // absorbing cert's permittedSubtrees (6.1.4(g)): the permitted set is tracked
735
- // as one generation per absorbing cert, and a name of form F must match a
736
- // subtree of form F in every generation that constrains form F. A flat pool
737
- // would compute the union, letting a subordinate CA broaden what its parent
738
- // permitted (a name-constraint bypass).
739
471
  function checkNameConstraints(state, cert) {
740
472
  var forms = certNameForms(cert);
741
- // Excluded: any match -> reject. A constraint of the same form the validator
742
- // cannot compare ("unsupported") means a critical exclusion it cannot
743
- // enforce, so it fails closed on the same footing as a match.
744
473
  for (var e = 0; e < state.excludedSubtrees.length; e++) {
745
474
  var ex = state.excludedSubtrees[e];
746
475
  for (var i = 0; i < forms.length; i++) {
@@ -749,14 +478,11 @@ function checkNameConstraints(state, cert) {
749
478
  if (m === "unsupported") return { ok: false, code: "path/name-constraint-unsupported" };
750
479
  }
751
480
  }
752
- // Permitted: for each name form, every generation that constrains that form
753
- // must admit it (intersection across generations). If the only subtrees of
754
- // that form are unenforceable, the name cannot be confirmed permitted.
755
481
  for (var f = 0; f < forms.length; f++) {
756
482
  var nf = forms[f];
757
483
  for (var g = 0; g < state.permittedGenerations.length; g++) {
758
484
  var formSubtrees = state.permittedGenerations[g].filter(function (s) { return s.tag === nf.tag; });
759
- if (!formSubtrees.length) continue; // this generation does not constrain the form
485
+ if (!formSubtrees.length) continue;
760
486
  var permitted = false, unsupported = false;
761
487
  formSubtrees.forEach(function (s) {
762
488
  var r = nameMatchesConstraint(nf.tag, nf.value, { tagNumber: s.tag, value: s.base });
@@ -769,9 +495,6 @@ function checkNameConstraints(state, cert) {
769
495
  return { ok: true };
770
496
  }
771
497
 
772
- // Absorb a cert's nameConstraints (6.1.4(g)): permittedSubtrees becomes a new
773
- // GENERATION (intersection is enforced at check time by requiring a match in
774
- // every generation); excludedSubtrees union into the flat excluded pool.
775
498
  function absorbNameConstraints(state, decoded) {
776
499
  if (decoded.permittedSubtrees.length) {
777
500
  state.permittedGenerations.push(decoded.permittedSubtrees.map(function (st) {
@@ -783,15 +506,10 @@ function absorbNameConstraints(state, decoded) {
783
506
  });
784
507
  }
785
508
 
786
- // ---- certificate-policy tree ----------------------------------------------
787
509
 
788
510
  function rootNode() {
789
511
  return { depth: 0, validPolicy: OID.anyPolicy, qualifierSet: [], expectedPolicySet: [OID.anyPolicy], children: [], parent: null };
790
512
  }
791
- // A deep copy of the valid-policy tree that omits the internal `parent` back-pointer,
792
- // so the structured verdict returned to callers is acyclic (JSON.stringify-safe).
793
- // The `parent` link is an implementation detail of the 6.1.3 processing, not part
794
- // of the RFC 5280 valid_policy_tree a consumer inspects.
795
513
  function treeWithoutParent(node) {
796
514
  if (!node) return null;
797
515
  return {
@@ -804,9 +522,7 @@ function treeWithoutParent(node) {
804
522
  }
805
523
  function leavesAt(tree, depth) {
806
524
  var out = [];
807
- // Coverage residual -- unreachable: every call site guards state.validPolicyTree
808
- // truthy before calling leavesAt; the null-tree early return is a backstop.
809
- if (!tree) return out; // a pruned-empty tree has no nodes
525
+ if (!tree) return out;
810
526
  (function walk(node) {
811
527
  if (node.depth === depth) { out.push(node); return; }
812
528
  node.children.forEach(walk);
@@ -814,7 +530,6 @@ function leavesAt(tree, depth) {
814
530
  return out;
815
531
  }
816
532
  function pruneChildless(tree, depth) {
817
- // Delete depth-`depth` nodes with no children, then propagate upward.
818
533
  for (var d = depth; d > 0; d--) {
819
534
  leavesAt(tree, d).forEach(function (node) {
820
535
  if (node.children.length === 0 && node.parent) {
@@ -826,24 +541,16 @@ function pruneChildless(tree, depth) {
826
541
  }
827
542
  function treeIsEmpty(tree) { return tree.children.length === 0; }
828
543
 
829
- // ---- the state machine -----------------------------------------------------
830
544
 
831
- // Is `base` the value shape the constraint matcher compares for a GeneralName
832
- // form? Forms 0/3/5/8 (otherName / x400Address / ediPartyName / registeredID)
833
- // carry any present value -- the matcher fails them closed as unsupported.
834
545
  function isSubtreeBaseValid(tag, base) {
835
546
  switch (tag) {
836
- case 1: case 2: case 6: return typeof base === "string"; // rfc822Name / dNSName / URI
837
- case 7: return Buffer.isBuffer(base) && (base.length === 8 || base.length === 32); // iPAddress: address + mask (any BufferSource is normalized to a Buffer in checkedSubtreeSeeds)
838
- case 4: return base !== null && typeof base === "object" && !!base.rdns && intrinsic.isArray(base.rdns); // directoryName
547
+ case 1: case 2: case 6: return typeof base === "string";
548
+ case 7: return Buffer.isBuffer(base) && (base.length === 8 || base.length === 32);
549
+ case 4: return base !== null && typeof base === "object" && !!base.rdns && intrinsic.isArray(base.rdns);
839
550
  default: return base !== undefined;
840
551
  }
841
552
  }
842
553
 
843
- // Entry-point validation of a 6.1.1(b,c) user-initial subtree seed. A
844
- // mis-shaped entry (e.g. the { base: { tagNumber, value } } shape the
845
- // nameConstraints decoder emits) would never match any name, silently
846
- // disabling the constraint the caller configured -- so it throws instead.
847
554
  function checkedSubtreeSeeds(list, optName) {
848
555
  if (list === undefined || list === null) return [];
849
556
  if (!intrinsic.isArray(list)) throw E("path/bad-input", "validate: opts." + optName + " must be an array of { tag, base } subtree entries");
@@ -851,9 +558,6 @@ function checkedSubtreeSeeds(list, optName) {
851
558
  if (!st || typeof st !== "object" || !Number.isInteger(st.tag) || st.tag < 0 || st.tag > 8) {
852
559
  throw E("path/bad-input", "validate: opts." + optName + " entries must be { tag: <GeneralName tag number 0..8>, base: <that form's constraint value> }");
853
560
  }
854
- // An iPAddress base (address+mask) may arrive as any BufferSource; normalize it to a
855
- // private Buffer so the matcher can index it by byte and a caller cannot mutate it once a
856
- // microtask has passed. The name forms (string / directoryName) are validated as they stand.
857
561
  var base = (st.tag === 7 && guard.bytes.isByteSource(st.base))
858
562
  ? guard.bytes.snapshotSource(st.base, PathError, "path/bad-input", "an iPAddress subtree base")
859
563
  : st.base;
@@ -870,8 +574,6 @@ function initialize(certs, params, seeds, anchor) {
870
574
  validPolicyTree: rootNode(),
871
575
  policyNodeCount: 1,
872
576
  maxPolicyNodes: params.maxPolicyNodes !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
873
- // Each absorbing cert's permittedSubtrees is one generation; a name must be
874
- // admitted by every generation (intersection). An initial seed is generation 0.
875
577
  permittedGenerations: seeds.permitted.length ? [seeds.permitted] : [],
876
578
  excludedSubtrees: seeds.excluded,
877
579
  explicitPolicy: params.initialExplicitPolicy ? 0 : n + 1,
@@ -887,10 +589,6 @@ function initialize(certs, params, seeds, anchor) {
887
589
  };
888
590
  }
889
591
 
890
- // self-issued = subject DN equals issuer DN. dnEqual throws on a NUL/control
891
- // DN (CVE-2009-2408); a malformed-DN cert is never "self-issued" (and is failed
892
- // by the name-chaining check), so the throw is swallowed here, keeping it from
893
- // rejecting the whole validate() promise at these unwrapped call sites.
894
592
  function selfIssued(cert) {
895
593
  try { return dnEqual(cert.subject.rdns, cert.issuer.rdns); }
896
594
  catch (_e) { return false; }
@@ -904,11 +602,6 @@ function processPolicies(state, cert, i, checks) {
904
602
  if (cp && state.validPolicyTree) {
905
603
  var policies = cp.value;
906
604
  var depth = i - 1;
907
- // anyPolicy processing is active only while inhibit_anyPolicy > 0, or for a
908
- // self-issued non-final cert. What the gate covers is the (d)(2) expansion of
909
- // a cert-asserted anyPolicy and nothing more. 4.2.1.14 inhibition rests entirely on
910
- // that gate: a depth-(i-1) anyPolicy node created while processing was
911
- // active remains matchable in (d)(1)(ii).
912
605
  var anyPolicyActive = state.inhibitAnyPolicy > 0 || (i < state._n && selfIssued(cert));
913
606
  var anyPolicyPresent = false;
914
607
  var anyPolicyQualifiers = null;
@@ -922,19 +615,12 @@ function processPolicies(state, cert, i, checks) {
922
615
  }
923
616
  });
924
617
  if (!matched) {
925
- // 6.1.3(d)(1)(ii): with no expected-policy match, create the node from a
926
- // depth-(i-1) anyPolicy node. The RFC runs this step UNCONDITIONALLY
927
- // (no inhibit clause); gating it would false-reject a path whose
928
- // specific policy chains through a legitimately created anyPolicy node.
929
618
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
930
619
  if (node.validPolicy === OID.anyPolicy) addChild(state, node, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], checks);
931
620
  });
932
621
  }
933
622
  });
934
623
  if (anyPolicyPresent && anyPolicyActive) {
935
- // 6.1.3(d)(2): expand anyPolicy into unmatched expected-policy values.
936
- // Each generated child carries AP-Q -- the qualifier set of the
937
- // certificate's own anyPolicy entry ("set the qualifier_set to AP-Q").
938
624
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
939
625
  node.expectedPolicySet.forEach(function (ep) {
940
626
  var already = guard.list.anyMatches(node.children, function (ch) { return ch.validPolicy === ep; });
@@ -949,7 +635,6 @@ function processPolicies(state, cert, i, checks) {
949
635
  state.validPolicyTree = null;
950
636
  }
951
637
 
952
- // 6.1.3(f): interim check.
953
638
  if (!(state.explicitPolicy > 0 || state.validPolicyTree !== null)) {
954
639
  checks.push({ name: "policy", ok: false, code: "path/policy-required" });
955
640
  return { fatal: true, error: E("path/policy-required", "explicit policy required but the valid-policy tree is empty") };
@@ -965,16 +650,10 @@ function addChild(state, parent, validPolicy, qualifiers, expectedPolicySet, che
965
650
  void checks;
966
651
  }
967
652
 
968
- // A DER explicit NULL (05 00) as an AlgorithmIdentifier parameters field is
969
- // treated identically to omitted parameters (RFC 5280 6.1.4(e)).
970
653
  function isNullOrAbsentParams(p) {
971
654
  return p === null || p === undefined || (p.length === 2 && p[0] === 0x05 && p[1] === 0x00);
972
655
  }
973
656
 
974
- // Rebuild a SubjectPublicKeyInfo with the given AlgorithmIdentifier parameters
975
- // spliced in, so a key that inherited its domain parameters (an EC public key
976
- // whose SPKI omits the namedCurve, RFC 5280 6.1.4(f)/6.1.5(d)) becomes a
977
- // self-contained SPKI that importKey("spki", ...) can consume.
978
657
  function spliceSpkiParameters(spki, algOid, paramsBytes) {
979
658
  return asn1.build.sequence([
980
659
  asn1.build.sequence([asn1.build.oid(algOid), asn1.build.raw(paramsBytes)]),
@@ -982,10 +661,6 @@ function spliceSpkiParameters(spki, algOid, paramsBytes) {
982
661
  ]);
983
662
  }
984
663
 
985
- // RFC 5280 6.1.4(d,e,f) / 6.1.5(c,d): set working_public_key / _algorithm /
986
- // _parameters from a certificate. Present non-null parameters are copied;
987
- // NULL-or-absent parameters inherit the prior parameters iff the key algorithm
988
- // is unchanged, else clear them.
989
664
  function updateWorkingKey(state, cert) {
990
665
  var keyAlg = cert.subjectPublicKeyInfo.algorithm;
991
666
  if (!isNullOrAbsentParams(keyAlg.parameters)) {
@@ -993,9 +668,6 @@ function updateWorkingKey(state, cert) {
993
668
  } else if (keyAlg.oid !== state.workingPublicKeyAlgorithm) {
994
669
  state.workingPublicKeyParameters = null;
995
670
  }
996
- // When this cert's SPKI omits its algorithm parameters but the working set
997
- // carries inherited ones, store a reconstructed SPKI: the next signature verify
998
- // needs a complete key and would fail on the bare bytes.
999
671
  if (isNullOrAbsentParams(keyAlg.parameters) && state.workingPublicKeyParameters) {
1000
672
  state.workingPublicKey = spliceSpkiParameters(cert.subjectPublicKeyInfo, keyAlg.oid, state.workingPublicKeyParameters);
1001
673
  } else {
@@ -1004,18 +676,12 @@ function updateWorkingKey(state, cert) {
1004
676
  state.workingPublicKeyAlgorithm = keyAlg.oid;
1005
677
  }
1006
678
 
1007
- // RFC 5280 4.2.1.12 -- when the caller states required key purposes, a cert
1008
- // carrying an extendedKeyUsage must assert every one (or anyExtendedKeyUsage).
1009
- // Applied to the TARGET cert (its own purposes) AND to every intermediate CA
1010
- // (EKU chaining: an EKU on a CA constrains the purposes below it), so marking
1011
- // extKeyUsage a processed critical extension is sound. The semantic gate runs
1012
- // wherever the extension appears, never only on the leaf. A cert with no EKU is
1013
- // unconstrained. Returns true if the cert FAILS the required-purpose check.
1014
679
  function ekuPurposeFails(cert, requiredEku, checks) {
1015
680
  var eku;
1016
681
  try { eku = decodeExt(cert, OID.extKeyUsage); }
682
+ // allow:fail-open-verify -- ekuPurposeFails returns true to mean the EKU purpose FAILS (a rejecting verdict); a bad extension is recorded in checks and the purpose is treated as failed, which is fail-closed
1017
683
  catch (e) { checks.push({ name: "extendedKeyUsage", ok: false, code: pathCode(e, "path/bad-extension-value") }); return true; }
1018
- if (!eku) return false; // absent EKU: unrestricted (4.2.1.12 restricts only when present)
684
+ if (!eku) return false;
1019
685
  var purposes = eku.value;
1020
686
  var ok = contains(purposes, OID.anyExtendedKeyUsage) || containsAll(purposes, requiredEku);
1021
687
  checks.push({ name: "extendedKeyUsage", ok: ok, code: ok ? undefined : "path/eku-not-permitted" });
@@ -1025,31 +691,22 @@ function ekuPurposeFails(cert, requiredEku, checks) {
1025
691
  function prepareNext(state, cert, i, checks) {
1026
692
  var isSelfIssued = selfIssued(cert);
1027
693
 
1028
- // RFC 5280 4.2.1.12 EKU chaining -- an intermediate CA's EKU constrains the
1029
- // purposes of the certs beneath it. Enforced here so a critical EKU on an
1030
- // intermediate (now a PROCESSED extension) is not merely tolerated but honored.
1031
694
  if (state.requiredEku && ekuPurposeFails(cert, state.requiredEku, checks)) {
1032
695
  return { fatal: true, error: E("path/eku-not-permitted", "an intermediate CA extendedKeyUsage does not permit a required purpose (RFC 5280 4.2.1.12)") };
1033
696
  }
1034
697
 
1035
- // (a),(b) policy mappings.
1036
698
  var pm;
1037
699
  try { pm = decodeExt(cert, OID.policyMappings); }
1038
700
  catch (e) { checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); return { fatal: true, error: e }; }
1039
701
  if (pm) {
1040
- // The scan for a forbidden anyPolicy mapping is the rule itself (RFC 5280 6.1.4(a)), so it is a
1041
- // loop rather than a `some`: one answering false makes the forbidden mapping invisible and the
1042
- // certificate is accepted carrying it.
1043
702
  var badAny = mapsAnyPolicy(pm.value);
1044
703
  if (badAny) { checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); return { fatal: true, error: E("path/bad-policy", "policyMappings must not map to or from anyPolicy (RFC 5280 6.1.4(a))") }; }
1045
704
  if (state.validPolicyTree) applyPolicyMappings(state, pm.value, i);
1046
705
  }
1047
706
 
1048
- // (c) working issuer name; (d),(e),(f) working key + algorithm + parameters.
1049
707
  state.workingIssuerName = cert.subject;
1050
708
  updateWorkingKey(state, cert);
1051
709
 
1052
- // (g) name constraints absorb, once this cert's own names have been checked.
1053
710
  var nc;
1054
711
  try { nc = decodeExt(cert, OID.nameConstraints); }
1055
712
  catch (e) { checks.push({ name: "nameConstraints", ok: false, code: pathCode(e, "path/bad-name-constraints") }); return { fatal: true, error: e }; }
@@ -1057,14 +714,12 @@ function prepareNext(state, cert, i, checks) {
1057
714
  if (ncCritErr) return { fatal: true, error: ncCritErr };
1058
715
  if (nc) absorbNameConstraints(state, nc.value);
1059
716
 
1060
- // (h) decrement counters for a non-self-issued cert.
1061
717
  if (!isSelfIssued) {
1062
718
  if (state.explicitPolicy > 0) state.explicitPolicy--;
1063
719
  if (state.policyMapping > 0) state.policyMapping--;
1064
720
  if (state.inhibitAnyPolicy > 0) state.inhibitAnyPolicy--;
1065
721
  }
1066
722
 
1067
- // (i),(j) policy/inhibit clamps.
1068
723
  var pc;
1069
724
  try { pc = decodeExt(cert, OID.policyConstraints); }
1070
725
  catch (e) { checks.push({ name: "policyConstraints", ok: false, code: "path/bad-policy" }); return { fatal: true, error: e }; }
@@ -1081,7 +736,6 @@ function prepareNext(state, cert, i, checks) {
1081
736
  if (iapCritErr) return { fatal: true, error: iapCritErr };
1082
737
  if (iap && iap.value < state.inhibitAnyPolicy) state.inhibitAnyPolicy = iap.value;
1083
738
 
1084
- // (k) basicConstraints cA gate, the single authoritative CA check.
1085
739
  var bc;
1086
740
  try { bc = decodeExt(cert, OID.basicConstraints); }
1087
741
  catch (e) { checks.push({ name: "basicConstraints", ok: false, code: "path/bad-basic-constraints" }); return { fatal: true, error: e }; }
@@ -1089,19 +743,14 @@ function prepareNext(state, cert, i, checks) {
1089
743
  checks.push({ name: "basicConstraints", ok: false, code: "path/not-a-ca" });
1090
744
  return { fatal: true, error: E("path/not-a-ca", "intermediate certificate is not a CA (basicConstraints cA is not TRUE, RFC 5280 6.1.4(k))") };
1091
745
  }
1092
- // 4.2.1.9: a CA certificate used to validate certificate signatures MUST mark
1093
- // basicConstraints critical. A non-critical cA:TRUE is non-conforming. A
1094
- // relying party that skips non-critical extensions would not see the CA bit.
1095
746
  var bcCritErr = requireCriticalExt(bc, "basicConstraints", checks);
1096
747
  if (bcCritErr) return { fatal: true, error: bcCritErr };
1097
- // (l),(m) path length.
1098
748
  if (!isSelfIssued) {
1099
749
  if (state.maxPathLength <= 0) { checks.push({ name: "pathLength", ok: false, code: "path/path-length-exceeded" }); return { fatal: true, error: E("path/path-length-exceeded", "certification path is longer than the CA path-length constraint allows") }; }
1100
750
  state.maxPathLength--;
1101
751
  }
1102
752
  if (bc.value.pathLenConstraint !== null && bc.value.pathLenConstraint < state.maxPathLength) state.maxPathLength = bc.value.pathLenConstraint;
1103
753
 
1104
- // (n) keyUsage.keyCertSign.
1105
754
  var ku;
1106
755
  try { ku = decodeExt(cert, OID.keyUsage); }
1107
756
  catch (e) { checks.push({ name: "keyUsage", ok: false, code: "path/bad-key-usage" }); return { fatal: true, error: e }; }
@@ -1115,12 +764,7 @@ function prepareNext(state, cert, i, checks) {
1115
764
  function applyPolicyMappings(state, mappings, i) {
1116
765
  var depth = i;
1117
766
  if (state.policyMapping > 0) {
1118
- // 6.1.4(b)(1): for each depth-i node whose valid_policy is an ID-P that
1119
- // the extension maps, REPLACE its expected_policy_set with the SET of
1120
- // subjectDomainPolicy values mapped from that ID-P. Appending would retain
1121
- // the pre-mapping policy, letting a later cert satisfy the chain by
1122
- // asserting the mapped-away policy.
1123
- var mappedFrom = {}; // issuerDomainPolicy -> [subjectDomainPolicy, ...]
767
+ var mappedFrom = {};
1124
768
  mappings.forEach(function (m) { (mappedFrom[m.issuerDomainPolicy] = mappedFrom[m.issuerDomainPolicy] || []).push(m.subjectDomainPolicy); });
1125
769
  var depthI = leavesAt(state.validPolicyTree, depth);
1126
770
  var anyNodes = depthI.filter(function (nd) { return nd.validPolicy === OID.anyPolicy; });
@@ -1129,24 +773,14 @@ function applyPolicyMappings(state, mappings, i) {
1129
773
  if (idpNodes.length) {
1130
774
  idpNodes.forEach(function (nd) { nd.expectedPolicySet = mappedFrom[idp].slice(); });
1131
775
  } else {
1132
- // 6.1.4(b)(1): with no depth-i ID-P node but a depth-i anyPolicy node, generate
1133
- // the missing ID-P node under the anyPolicy node's parent with the mapped
1134
- // expected set (else an anyPolicy-only CA loses the mapping).
1135
776
  anyNodes.forEach(function (anyNode) {
1136
777
  if (anyNode.parent) addChild(state, anyNode.parent, idp, anyNode.qualifierSet[0] || null, mappedFrom[idp].slice(), []);
1137
778
  });
1138
779
  }
1139
780
  });
1140
781
  } else {
1141
- // 6.1.4(b)(2), policy_mapping == 0: delete every depth-i node whose
1142
- // valid_policy is a mapped ID-P, then prune. A prior mapping in the same
1143
- // extension may have already emptied the tree, so stop if it is gone.
1144
782
  var mappedSet = {};
1145
783
  mappings.forEach(function (m) { mappedSet[m.issuerDomainPolicy] = true; });
1146
- // Coverage residual -- unreachable today: applyPolicyMappings is invoked once per
1147
- // certificate under an `if (state.validPolicyTree)` guard and does not null the
1148
- // tree before this point. Retained as a cheap correctness backstop for a future
1149
- // per-mapping-batch refactor; do not remove.
1150
784
  if (!state.validPolicyTree) return;
1151
785
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
1152
786
  if (mappedSet[node.validPolicy] && node.parent) {
@@ -1154,30 +788,15 @@ function applyPolicyMappings(state, mappings, i) {
1154
788
  if (idx !== -1) node.parent.children.splice(idx, 1);
1155
789
  }
1156
790
  });
1157
- // Prune from the PARENT depth (i-1): a pass starting at depth i would
1158
- // delete the surviving unmapped depth-i leaves (all leaves are childless),
1159
- // wrongly emptying the tree and false-rejecting a valid path.
1160
791
  pruneChildless(state.validPolicyTree, depth - 1);
1161
792
  if (treeIsEmpty(state.validPolicyTree)) state.validPolicyTree = null;
1162
793
  }
1163
794
  }
1164
795
 
1165
- // policyMappings is semantically processed only in the prepare-for-next step
1166
- // (sec. 6.1.4(a),(b)), which does not run for the target certificate. It is also
1167
- // SHOULD-be-non-critical (sec. 4.2.1.5), so a critical policyMappings on the target
1168
- // is both anomalous and unprocessed, and must fail closed (sec. 6.1.5(f)); accepting
1169
- // it would let, e.g., a mapping to/from anyPolicy slip past the sec. 6.1.4(a) rejection
1170
- // the intermediate path applies. (nameConstraints / inhibitAnyPolicy are also
1171
- // prepare-next-only but are MUST-be-critical CA extensions, so a critical one on
1172
- // a terminal CA cert is conforming and is not treated as unprocessed here.)
1173
796
  var TARGET_UNPROCESSED_IF_CRITICAL = {};
1174
797
  TARGET_UNPROCESSED_IF_CRITICAL[OID.policyMappings] = true;
1175
798
  Object.freeze(TARGET_UNPROCESSED_IF_CRITICAL);
1176
799
 
1177
- // Membership is asked of the table's OWN properties. Freezing the table stops a write to it and
1178
- // stops nothing about the prototype chain: a name planted on Object.prototype answers for every OID
1179
- // the table does not carry, and read live this reported an unrecognized critical extension as
1180
- // processed. On a CA certificate only the first table is consulted, so the chain was accepted.
1181
800
  function unrecognizedCriticalExtension(cert, isTarget) {
1182
801
  for (var i = 0; i < cert.extensions.length; i++) {
1183
802
  var ext = cert.extensions[i];
@@ -1188,15 +807,12 @@ function unrecognizedCriticalExtension(cert, isTarget) {
1188
807
  return null;
1189
808
  }
1190
809
 
1191
- // Decode every RECOGNIZED critical extension to enforce that its extnValue is
1192
- // structurally valid -- even where the semantic gate is skipped (the leaf).
1193
- // Returns the failing typed code, or null.
1194
810
  function validateCriticalExtensionStructure(cert) {
1195
811
  for (var i = 0; i < cert.extensions.length; i++) {
1196
812
  var ext = cert.extensions[i];
1197
813
  if (!ext.critical) continue;
1198
814
  var dec = EXT_DECODERS[ext.oid];
1199
- if (!dec) continue; // unrecognized-critical handled separately
815
+ if (!dec) continue;
1200
816
  try { dec(ext.value); }
1201
817
  catch (e) { return pathCode(e, "path/bad-extension-value"); }
1202
818
  }
@@ -1270,14 +886,6 @@ function validateCriticalExtensionStructure(cert) {
1270
886
  * });
1271
887
  * res.valid; // boolean; res.results[0].checks carries the per-check codes
1272
888
  */
1273
- // Every option pki.path.validate reads. The anchor option is `trustAnchors` (plural), the same
1274
- // spelling pki.path.build / pki.cms.verify / pki.cmp.verify use, and it takes a single anchor or a
1275
- // non-empty array of them. A one-element array reproduces the single-anchor behavior exactly.
1276
- // Derived from three sources, because no one of them is complete: the `opts.*` reads in this
1277
- // function, the `params.*` reads in the RFC 5280 6.1.1 state it is passed down to, and the
1278
- // documented prose above, which is the only place `requiredEku` and the three initial-policy
1279
- // inputs appear together. A grep of one function would have shipped a list missing four real
1280
- // options and rejected callers that work today.
1281
889
  var _VALIDATE_OPTS = {
1282
890
  checkPurpose: 1, ctLogList: 1, ctPolicy: 1, historicalMode: 1, initialAnyPolicyInhibit: 1,
1283
891
  initialExcludedSubtrees: 1, initialExplicitPolicy: 1, initialPermittedSubtrees: 1,
@@ -1286,11 +894,6 @@ var _VALIDATE_OPTS = {
1286
894
  userInitialPolicySet: 1, verifier: 1
1287
895
  };
1288
896
 
1289
- // The opt-in Certificate Transparency gate (RFC 6962). When opts.ctLogList is supplied, the target's
1290
- // embedded SCTs are verified against the trusted log-list and the caller's CT policy, and the verdict is
1291
- // recorded on the target's checks. Fail-closed: a target that declares a CT policy but carries no
1292
- // SCT-list extension is refused. Reached only through opts.ctLogList, so every existing caller is
1293
- // unaffected. The SCT-list extension OID is looked up through the shared registry, never a literal.
1294
897
  var OID_SCT_LIST = oid.byName("signedCertificateTimestampList");
1295
898
  async function ctGateCheck(leaf, certs, anchor, ctLogList, ctPolicy, n, at) {
1296
899
  var exts = leaf.extensions || [], sctExt = null;
@@ -1298,8 +901,6 @@ async function ctGateCheck(leaf, certs, anchor, ctLogList, ctPolicy, n, at) {
1298
901
  if (exts[i] && (exts[i].oid === OID_SCT_LIST || exts[i].name === "signedCertificateTimestampList")) { sctExt = exts[i]; break; }
1299
902
  }
1300
903
  if (sctExt == null) return { name: "ct", ok: false, code: "path/ct-required" };
1301
- // The leaf's issuer contributes issuerKeyHash: the certificate that signed it (certs[n-2] in the
1302
- // anchor->target order), or the anchor itself when the target is validated directly beneath it.
1303
904
  var issuer = n >= 2 ? certs[n - 2] : { subjectPublicKeyInfo: { bytes: anchor.publicKey } };
1304
905
  var entry, list;
1305
906
  try {
@@ -1308,12 +909,6 @@ async function ctGateCheck(leaf, certs, anchor, ctLogList, ctPolicy, n, at) {
1308
909
  } catch (e) {
1309
910
  return { name: "ct", ok: false, code: pathCode(e, "path/ct-required") };
1310
911
  }
1311
- // verifySctList throws only for a mis-shaped caller configuration (a bad ctPolicy threshold value, a
1312
- // ctLogList that is not a parseLogList result): that is the caller's error, surfaced at the path
1313
- // boundary as path/bad-input rather than leaking the ct/* domain code of a layer they did not call.
1314
- // verifySctList throws only for a mis-shaped caller configuration (a bad ctPolicy threshold value, a
1315
- // ctLogList that is not a parseLogList result): that is the caller's error, surfaced at the path
1316
- // boundary as path/bad-input rather than leaking the ct/* domain code of a layer they did not call.
1317
912
  var verdict;
1318
913
  try {
1319
914
  verdict = await ct.verifySctList(entry, list, ctLogList,
@@ -1329,15 +924,7 @@ async function validate(path, opts) {
1329
924
  opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "validate: opts");
1330
925
  guard.identifier.assertKnownKeys(opts, _VALIDATE_OPTS, E, "path/bad-input",
1331
926
  "pki.path.validate has an unknown option (the anchor option is `trustAnchors`, a single anchor or an array). The unknown option was: ");
1332
- // A CT policy without a log-list cannot be enforced -- refuse it at entry rather than silently
1333
- // skipping the CT gate and returning a valid path the caller believes was CT-checked (fail-closed).
1334
927
  if (opts.ctPolicy != null && opts.ctLogList == null) throw E("path/bad-input", "validate: opts.ctPolicy requires opts.ctLogList (the trusted CT log-list to verify the certificate's SCTs against)");
1335
- // Validate the CT policy's KEYS at entry: a misspelled key (e.g. `minSCTs`) would otherwise read as
1336
- // undefined and silently default to the RFC floor of 1, applying a weaker policy than the caller asked
1337
- // for. The values are validated by verifySctList when the gate runs.
1338
- // Validate the whole CT configuration at entry, independent of whether the target carries an SCT
1339
- // extension, so a malformed policy or log-list is always path/bad-input -- never masked as a
1340
- // per-certificate path/ct-required just because the target happens to lack SCTs.
1341
928
  if (opts.ctLogList != null) {
1342
929
  if (typeof opts.ctLogList !== "object" || opts.ctLogList.byLogId == null) throw E("path/bad-input", "validate: opts.ctLogList must be a pki.ct.parseLogList result");
1343
930
  var ctp = guard.identifier.optionsObject(opts.ctPolicy, E, "path/bad-input", "validate: opts.ctPolicy");
@@ -1346,39 +933,17 @@ async function validate(path, opts) {
1346
933
  if (ctp.minOperators != null && (typeof ctp.minOperators !== "number" || !intrinsic.isInteger(ctp.minOperators) || ctp.minOperators < 1)) throw E("path/bad-input", "validate: opts.ctPolicy.minOperators must be a positive integer");
1347
934
  }
1348
935
  if (!intrinsic.isArray(path)) throw E("path/bad-input", "validate: path must be an array of certificates");
1349
- // Bound the per-cert asymmetric-verify work BEFORE parsing an untrusted bundle
1350
- // (the policy-tree cap guards CVE-2023-0464-style blow-up; this guards linear
1351
- // crypto amplification from an oversized path). Entry-point tier: throw.
1352
936
  var maxCerts = guard.limits.cap(opts.maxPathCerts, "validate: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
1353
937
  if (path.length > maxCerts) throw E("path/bad-input", "validate: the certification path has " + path.length + " certificates, exceeding the maxPathCerts limit (" + maxCerts + ")");
1354
- // Through the same door `build` uses, and for the same reason: every certificate this walk decides
1355
- // over is re-derived from the bytes its parser read (see coerceCert), so a rebuilt object cannot
1356
- // present a genuine signed range alongside substituted fields.
1357
938
  var certs = path.map(function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
1358
939
  var n = certs.length;
1359
940
  if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
1360
- // trustAnchors is a single anchor OR a non-empty array of them. Normalize each at the door: a parsed
1361
- // root CERTIFICATE becomes a { name, publicKey, algorithm } tuple, and a mis-shaped tuple fails closed
1362
- // with path/bad-input rather than seeding an undefined working key a self-describing SPKI could still
1363
- // validate against (a soft-verdict fail-open). With more than one anchor, select the one whose subject
1364
- // issued the top path cert (certs[0], anchor->target order); a one-element list reproduces the
1365
- // single-anchor behavior exactly, and no match falls to the first so the name-chaining check below
1366
- // fails closed (an untrusted chain is never validated against a non-matching root).
1367
941
  var rawAnchors = opts.trustAnchors;
1368
942
  var anchorList = intrinsic.isArray(rawAnchors) ? rawAnchors : [rawAnchors];
1369
943
  if (rawAnchors == null || anchorList.length === 0) throw E("path/bad-input", "validate: trustAnchors is required (a single anchor or a non-empty array of anchors)");
1370
- // With more than one anchor, validate the SAME path against each and accept if ANY yields a valid path
1371
- // (the trust-store "does this chain lead to any of my roots" question). Two anchors can share a subject
1372
- // DN with different keys (a rotated or cross-signed root), so choosing by name alone would reject a
1373
- // valid chain when the wrong same-name anchor is tried first: try each, order-independent for validity.
1374
- // Each recursion carries exactly one anchor, so it runs the single-anchor body below (no further fan-out).
1375
944
  if (anchorList.length > 1) {
1376
- // Normalize the COMPLETE array up front: toAnchor validates each tuple exactly as the single-anchor
1377
- // path does, so a malformed anchor is refused (path/bad-input) regardless of its position, never
1378
- // silently accepted because a valid anchor happened to be tried first.
1379
945
  var tuples = [];
1380
946
  for (var ti = 0; ti < anchorList.length; ti++) { tuples[ti] = toAnchor(anchorList[ti], "validate"); }
1381
- // One anchor per recursion, so each runs the single-anchor body below (no further fan-out).
1382
947
  var singleOpts = function (a) {
1383
948
  var keys = intrinsic.ownKeys(opts);
1384
949
  var o = {};
@@ -1386,10 +951,6 @@ async function validate(path, opts) {
1386
951
  o.trustAnchors = a;
1387
952
  return o;
1388
953
  };
1389
- // Only an anchor whose subject issued certs[0] can complete this path, so try each such candidate and
1390
- // skip unrelated roots: the cost does not scale with a whole trust store. Same-subject anchors with
1391
- // different keys (a rotated or cross-signed root) ALL match the terminus and are each tried, so validity
1392
- // is order-independent.
1393
954
  var lastMatched = null, anyMatched = false;
1394
955
  for (var ai = 0; ai < tuples.length; ai++) {
1395
956
  if (!dnEqual(tuples[ai].name.rdns, certs[0].issuer.rdns)) continue;
@@ -1398,28 +959,15 @@ async function validate(path, opts) {
1398
959
  if (oneRes && oneRes.valid) return oneRes;
1399
960
  lastMatched = oneRes;
1400
961
  }
1401
- // No supplied anchor's subject matched the path terminus: the chain leads to none of them. Run one to
1402
- // produce the fail-closed valid:false verdict (the name-chaining check reports it) rather than null.
1403
962
  return anyMatched ? lastMatched : validate(path, singleOpts(tuples[0]));
1404
963
  }
1405
964
  var anchor = toAnchor(anchorList[0], "validate");
1406
- // The validity-window check is always on (6.1.3(a)(2)); a missing/invalid
1407
- // check date must fail closed, never silently disable it.
1408
965
  guard.time.assertValid(opts.time, E, "path/bad-input", "validate: opts.time (the always-on validity-window check date)");
1409
- // Entry-point tier for the remaining 6.1.1 user-initial inputs: a bad value
1410
- // throws here rather than silently disabling the behavior it configures.
1411
- // Validate-if-present (the default is applied where the policy state is built);
1412
- // the shared integer cap rejects a fractional / negative / non-numeric budget.
1413
966
  guard.limits.cap(opts.maxPolicyNodes, "validate: opts.maxPolicyNodes", undefined, { E: E, code: "path/bad-input", min: 1 });
1414
- // user-initial-policy-set is a non-empty SET of policy OID strings (6.1.1(c)).
1415
- // Membership is tested with indexOf, so a raw string here would be consulted
1416
- // as a SUBSTRING match -- validated to an array of strings instead.
1417
967
  if (opts.userInitialPolicySet !== undefined) {
1418
968
  var uipsOk = intrinsic.isArray(opts.userInitialPolicySet) && opts.userInitialPolicySet.length > 0 &&
1419
969
  guard.list.allMatch(opts.userInitialPolicySet, function (p) { return typeof p === "string" && p.length > 0; });
1420
970
  if (!uipsOk) throw E("path/bad-input", "validate: opts.userInitialPolicySet must be a non-empty array of policy OID strings");
1421
- // Each entry is compared (indexOf) against canonical decoder output, so a
1422
- // non-canonical key would silently never match -- fail the typo closed here.
1423
971
  opts.userInitialPolicySet.forEach(function (p) {
1424
972
  guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.userInitialPolicySet entry " + JSON.stringify(p));
1425
973
  });
@@ -1428,9 +976,6 @@ async function validate(path, opts) {
1428
976
  permitted: checkedSubtreeSeeds(opts.initialPermittedSubtrees, "initialPermittedSubtrees"),
1429
977
  excluded: checkedSubtreeSeeds(opts.initialExcludedSubtrees, "initialExcludedSubtrees"),
1430
978
  };
1431
- // opts.requiredEku -- the key purposes the TARGET certificate must be good
1432
- // for, each a registered OID name or a dotted OID string. Resolved (and
1433
- // typo-checked) here at the entry point.
1434
979
  var purposeOpts = resolvePurposeOpts(opts);
1435
980
  var requiredEku = purposeOpts.requiredEku;
1436
981
  var checkPurpose = purposeOpts.checkPurpose;
@@ -1441,25 +986,10 @@ async function validate(path, opts) {
1441
986
  var verifier = opts.verifier || null;
1442
987
  var revocationChecker = opts.revocationChecker || null;
1443
988
  var softFail = opts.softFail === true;
1444
- // Revocation is a pluggable, opt-in step: by default a path with no checker is
1445
- // not revocation-checked. opts.requireRevocation makes the 6.1.3(a)(3)
1446
- // determination mandatory -- an absent checker (or an undetermined result)
1447
- // then fails the path closed instead of silently skipping the step.
1448
989
  var requireRevocation = opts.requireRevocation === true;
1449
990
  var failed = false;
1450
- // Whether the revocation rule RAN, and whether any certificate's answer had to be waived. The
1451
- // verdict reports the two separately because they are different claims: a caller re-reading a
1452
- // stored result needs to tell "every certificate was determined not revoked" from "the step was
1453
- // skipped" from "the step could not conclude and you had asked for that to pass".
1454
991
  var revocationRan = false, revocationWaived = false, revocationUndetermined = false;
1455
- // Which of the anchor's purpose-scoped constraints actually decided anything, reported so a
1456
- // verdict can be re-read to tell an anchor that was judged from one that carried nothing to judge.
1457
992
  var anchorDistrustApplied = false, anchorPurposeApplied = false;
1458
- // A trust anchor carrying purpose-scoped metadata, validated with no purpose to select by, is a
1459
- // configuration fault. The constraint is KEYED by purpose -- there is no way to apply
1460
- // `distrustAfter.serverAuth` without being told the validation is about serverAuth -- so without
1461
- // one the caller's stated intent would be discarded silently, and a root distrusted years ago
1462
- // would validate a current leaf. An anchor carrying no such metadata is unaffected.
1463
993
  if (!checkPurpose && _hasPurposeScopedMetadata(anchor)) {
1464
994
  throw E("path/bad-input", "validate: the trust anchor carries purpose-scoped metadata (distrustAfter / purposes), which is keyed by key purpose -- supply opts.checkPurpose to say which purpose this validation is for, or the constraint cannot be applied");
1465
995
  }
@@ -1469,7 +999,6 @@ async function validate(path, opts) {
1469
999
  var cert = certs[idx];
1470
1000
  var checks = [];
1471
1001
 
1472
- // 6.1.3(a)(1) signature.
1473
1002
  var sigRes;
1474
1003
  if (verifier) {
1475
1004
  var vv;
@@ -1488,29 +1017,18 @@ async function validate(path, opts) {
1488
1017
  checks.push({ name: "signature", ok: sigRes.ok, code: sigRes.ok ? undefined : (sigRes.code || "path/bad-signature") });
1489
1018
  if (!sigRes.ok) failed = true;
1490
1019
 
1491
- // draft-ietf-lamps-pq-composite-sigs sec. 5.2: a composite-keyed certificate's
1492
- // keyUsage must be signature-only (no dual-usage). Runs for the target AND every
1493
- // intermediate whose own subject key is composite.
1494
1020
  if (compositeSig.COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
1495
1021
  var cku = compositeKeyUsageCheck(cert);
1496
1022
  checks.push({ name: "compositeKeyUsage", ok: cku.ok, code: cku.ok ? undefined : cku.code });
1497
1023
  if (!cku.ok) failed = true;
1498
1024
  }
1499
1025
 
1500
- // RFC 9935 sec. 5: an ML-KEM-keyed certificate's keyUsage must be keyEncipherment-only.
1501
- // Runs for the target AND every intermediate whose own subject key is ML-KEM.
1502
1026
  if (ML_KEM_OIDS[cert.subjectPublicKeyInfo.algorithm.oid]) {
1503
1027
  var kku = kemKeyUsageCheck(cert);
1504
1028
  checks.push({ name: "kemKeyUsage", ok: kku.ok, code: kku.ok ? undefined : kku.code });
1505
1029
  if (!kku.ok) failed = true;
1506
1030
  }
1507
1031
 
1508
- // 6.1.3(a)(2) validity window.
1509
- //
1510
- // Instants, never the Date objects. Comparing two Dates coerces each one, and coercion runs
1511
- // `Symbol.toPrimitive` or `valueOf`, which a caller's subclass answers. opts.time was checked
1512
- // for a valid held instant at entry, and comparing the object here asked it again through a
1513
- // door the check never used -- so the value validated and the value compared were two reads.
1514
1032
  var t = guard.time.instantOf(opts.time);
1515
1033
  var vOk = true, vCode;
1516
1034
  if (t < guard.time.instantOf(cert.validity.notBefore)) { vOk = false; vCode = "path/not-yet-valid"; }
@@ -1518,15 +1036,12 @@ async function validate(path, opts) {
1518
1036
  checks.push({ name: "validity", ok: vOk, code: vCode });
1519
1037
  if (!vOk) failed = true;
1520
1038
 
1521
- // 6.1.3(a)(4) name chaining.
1522
1039
  var chainOk;
1523
1040
  try { chainOk = dnEqual(cert.issuer.rdns, state.workingIssuerName.rdns); }
1524
1041
  catch (_e) { chainOk = false; }
1525
1042
  checks.push({ name: "nameChaining", ok: chainOk === true, code: chainOk === true ? undefined : "path/name-chaining" });
1526
1043
  if (chainOk !== true) failed = true;
1527
1044
 
1528
- // 6.1.3(b,c) name constraints on this cert's own names (skip for a
1529
- // self-issued non-terminal cert).
1530
1045
  if (!(selfIssued(cert) && i !== n)) {
1531
1046
  var ncRes;
1532
1047
  try { ncRes = checkNameConstraints(state, cert); }
@@ -1535,43 +1050,20 @@ async function validate(path, opts) {
1535
1050
  if (!ncRes.ok) failed = true;
1536
1051
  }
1537
1052
 
1538
- // 6.1.3(a)(3) revocation.
1539
1053
  if (revocationChecker) {
1540
- var issuerCert = idx > 0 ? certs[idx - 1] : null; // the anchor issues cert[1]
1054
+ var issuerCert = idx > 0 ? certs[idx - 1] : null;
1541
1055
  var rv, rvError = null;
1542
- // A checker that THROWS -- or whose promise rejects -- is not a checker reporting "unknown".
1543
- // Laundering the two together made a broken checker indistinguishable from a working one that
1544
- // could not reach the responder, and under softFail both became a pass. The fault is carried
1545
- // onto the check so an operator can tell their own bug from a network condition, and it fails
1546
- // the path whatever softFail says (see the branch below).
1547
1056
  try { rv = await revocationChecker.check(cert, { workingIssuerName: state.workingIssuerName, workingPublicKey: state.workingPublicKey, workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm, issuerCert: issuerCert }, { time: opts.time, historicalMode: opts.historicalMode === true }); }
1548
1057
  catch (e) { rv = { status: "error" }; rvError = e; }
1549
- // ONLY an explicit "good" is a determined non-revocation; "revoked" fails;
1550
- // every other value ("unknown", an OCSP tryLater/unauthorized, a typo, a
1551
- // missing status) is undetermined and fails closed unless softFail.
1552
- //
1553
- // The two `ok: true` outcomes are NOT the same claim and no longer the same object. "checked,
1554
- // and it said good" and "could not check, and you waived it" read identically from a bare
1555
- // boolean, which is the whole reason a stored verdict cannot be re-read to answer whether
1556
- // revocation was ever established. Each entry now names the status it was decided on, and a
1557
- // waiver marks itself.
1558
1058
  var rvStatus = (rv && typeof rv.status === "string") ? rv.status : "unknown";
1559
1059
  if (rv && rv.status === "good") { checks.push({ name: "revocation", ok: true, status: "good" }); }
1560
1060
  else if (rv && rv.status === "revoked") { checks.push({ name: "revocation", ok: false, status: "revoked", code: "path/revoked" }); failed = true; }
1561
1061
  else if (rvError) {
1562
- // A checker that threw is a FAULT in the checker, not a status it could not reach, and
1563
- // softFail is the caller opting into an undetermined ANSWER. The built-in CRL and OCSP
1564
- // checkers return `{status:"unknown"}` for every unreachable or unverifiable condition and
1565
- // never throw, so a throw here is the caller's own bug -- waiving it would pass the
1566
- // certificate with no revocation result at all, which is the outcome softFail is asked for
1567
- // and this is not.
1568
1062
  checks.push({ name: "revocation", ok: false, status: "error", code: "path/revocation-checker-error", error: rvError });
1569
1063
  revocationUndetermined = true;
1570
1064
  failed = true;
1571
1065
  }
1572
1066
  else if (softFail) {
1573
- // No `error` slot here or below: a fault took the branch above, so anything reaching these
1574
- // two is a status the checker actually reported.
1575
1067
  checks.push({ name: "revocation", ok: true, status: rvStatus, waived: true });
1576
1068
  revocationWaived = true;
1577
1069
  } else {
@@ -1581,53 +1073,36 @@ async function validate(path, opts) {
1581
1073
  }
1582
1074
  revocationRan = true;
1583
1075
  } else if (requireRevocation) {
1584
- // No checker was supplied but the caller demands a revocation determination:
1585
- // the step cannot be performed, so fail closed (never silently skip).
1586
1076
  checks.push({ name: "revocation", ok: false, code: "path/revocation-undetermined" }); failed = true;
1587
1077
  }
1588
1078
 
1589
- // 6.1.3(d-f) policies.
1590
1079
  var polRes = processPolicies(state, cert, i, checks);
1591
1080
  if (state._capHit) { checks.push({ name: "policyTree", ok: false, code: "path/policy-tree-cap" }); failed = true; }
1592
1081
  else if (polRes.fatal) failed = true;
1593
1082
 
1594
- // empty subject requires a critical SAN (4.1.2.6).
1595
1083
  if (cert.subject.rdns.length === 0) {
1596
1084
  var san = findExt(cert, OID.subjectAltName);
1597
1085
  if (!san || !san.critical) { checks.push({ name: "emptySubject", ok: false, code: "path/empty-subject-no-critical-san" }); failed = true; }
1598
1086
  }
1599
1087
 
1600
- // 6.1.4 / 6.1.5.
1601
1088
  if (i !== n) {
1602
1089
  if (!state._capHit) {
1603
1090
  var prep = prepareNext(state, cert, i, checks);
1604
1091
  if (prep.fatal) failed = true;
1605
1092
  }
1606
1093
  } else {
1607
- // 6.1.5 wrap-up.
1608
- if (state.explicitPolicy > 0) state.explicitPolicy--; // 6.1.5(a)
1094
+ if (state.explicitPolicy > 0) state.explicitPolicy--;
1609
1095
  var lpc;
1610
1096
  try { lpc = decodeExt(cert, OID.policyConstraints); }
1611
1097
  catch (_e) { lpc = null; checks.push({ name: "policyConstraints", ok: false, code: "path/bad-policy" }); failed = true; }
1612
- // 4.2.1.11: policyConstraints MUST be critical -- apply the same check the
1613
- // intermediate path (prepareNext) uses, so a non-critical policyConstraints
1614
- // on the TARGET cert fails closed consistently.
1615
1098
  if (requireCriticalExt(lpc, "policyConstraints", checks)) failed = true;
1616
- if (lpc && lpc.value.requireExplicitPolicy === 0) state.explicitPolicy = 0; // 6.1.5(b)
1617
- // 6.1.4(a) / 4.2.1.5: policyMappings must never map to/from anyPolicy. The
1618
- // intermediate path enforces this in prepareNext; the target cert skips it,
1619
- // so apply the structural rejection here too (covers a non-critical mapping
1620
- // that the unrecognized-critical check above does not reach).
1099
+ if (lpc && lpc.value.requireExplicitPolicy === 0) state.explicitPolicy = 0;
1621
1100
  var lpm;
1622
1101
  try { lpm = decodeExt(cert, OID.policyMappings); }
1623
1102
  catch (_e) { lpm = null; checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true; }
1624
1103
  if (lpm && mapsAnyPolicy(lpm.value)) {
1625
1104
  checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true;
1626
1105
  }
1627
- // 4.2.1.10 / 4.2.1.14: nameConstraints and inhibitAnyPolicy MUST be
1628
- // critical wherever they appear -- apply to the TARGET cert the same
1629
- // check prepareNext applies to every intermediate (their semantic gates
1630
- // do not run for the target, but the criticality rule still binds).
1631
1106
  var lnc;
1632
1107
  try { lnc = decodeExt(cert, OID.nameConstraints); }
1633
1108
  catch (e) { lnc = null; checks.push({ name: "nameConstraints", ok: false, code: pathCode(e, "path/bad-name-constraints") }); failed = true; }
@@ -1636,64 +1111,33 @@ async function validate(path, opts) {
1636
1111
  try { liap = decodeExt(cert, OID.inhibitAnyPolicy); }
1637
1112
  catch (e) { liap = null; checks.push({ name: "inhibitAnyPolicy", ok: false, code: pathCode(e, "path/bad-policy") }); failed = true; }
1638
1113
  if (requireCriticalExt(liap, "inhibitAnyPolicy", checks)) failed = true;
1639
- // 4.2.1.12: when the caller states required key purposes, the target's
1640
- // extendedKeyUsage must assert every one (or anyExtendedKeyUsage -- the
1641
- // 4.2.1.12 wildcard; rejecting it is an application MAY, not the
1642
- // default). An ABSENT extension leaves the key unrestricted, so it
1643
- // satisfies any required purpose.
1644
1114
  if (requiredEku && ekuPurposeFails(cert, requiredEku, checks)) failed = true;
1645
- // Trust-anchor constraint contract (NSS / CCADB metadata; gated so a bare
1646
- // anchor or an absent checkPurpose is byte-identical to today). The anchor's
1647
- // per-purpose distrust-after date and delegator purposes apply to the
1648
- // end-entity leaf it ultimately certifies.
1649
1115
  var ta = anchor;
1650
- // A PRESENT-but-malformed distrustAfter (an Invalid Date: instanceof Date yet a
1651
- // NaN time) would make `notBefore > it` NaN-false and SILENTLY drop the distrust
1652
- // restriction -- the NaN-Date fail-open. Validate a present date fail-closed
1653
- // before the comparison; an absent (undefined/null) date is no restriction.
1654
1116
  var distrustDate = assertAnchorConstraints(ta, checkPurpose);
1655
1117
  if (distrustDate != null) {
1656
1118
  anchorDistrustApplied = true;
1657
- // STRICTLY > : a leaf whose notBefore == the distrust date stays trusted
1658
- // (Mozilla certverifier isDistrustedCertificateChain: endEntityNotBefore
1659
- // <= distrustAfterTime -> not distrusted; the end-of-day ...235959Z
1660
- // convention keeps the whole boundary day trusted).
1661
1119
  if (guard.time.instantOf(cert.validity.notBefore) > guard.time.instantOf(distrustDate)) {
1662
1120
  checks.push({ name: "distrustAfter", ok: false, code: "path/distrusted-after" }); failed = true;
1663
1121
  }
1664
1122
  }
1665
1123
  if (checkPurpose && ta.purposes) {
1666
1124
  anchorPurposeApplied = true;
1667
- // The map is the operator's RESTRICTION, so it is asked for its own entry: an inherited
1668
- // `true` would grant the purpose on every anchor whose map does not name it.
1669
1125
  if (!intrinsic.hasOwn(ta.purposes, checkPurpose) || ta.purposes[checkPurpose] !== true) {
1670
1126
  checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
1671
1127
  }
1672
1128
  }
1673
- updateWorkingKey(state, cert); // 6.1.5(c),(d) -- key AND algorithm AND parameters
1129
+ updateWorkingKey(state, cert);
1674
1130
  }
1675
1131
 
1676
- // 6.1.4(o) / 6.1.5(e) unrecognized critical extension.
1677
1132
  var unk = unrecognizedCriticalExtension(cert, i === n);
1678
1133
  if (unk) { checks.push({ name: "criticalExtensions", ok: false, code: "path/unrecognized-critical-extension" }); failed = true; }
1679
1134
 
1680
- // A RECOGNIZED critical extension must still be structurally valid even
1681
- // when its semantic gate does not run on this cert (the leaf is not subject
1682
- // to 6.1.4, so its basicConstraints/keyUsage/policy* are never read in
1683
- // prepareNext) -- a malformed critical extnValue must fail closed, not slip
1684
- // through as "recognized". Decode every recognized critical extension to
1685
- // validate it (a no-op for one already decoded above; the decoders are pure).
1686
1135
  var crit = validateCriticalExtensionStructure(cert);
1687
1136
  if (crit) { checks.push({ name: "criticalExtensionValue", ok: false, code: crit }); failed = true; }
1688
1137
 
1689
1138
  state.results.push({ index: idx, checks: checks });
1690
1139
  }
1691
1140
 
1692
- // 6.1.5(g) success condition. The tree is first INTERSECTED with the
1693
- // user-initial-policy-set (userConstrainedPolicies); success requires
1694
- // explicit_policy > 0 OR that pruned tree to be non-empty. Using the raw tree
1695
- // would accept a path whose only surviving policies are outside the user set
1696
- // when an explicit policy is required.
1697
1141
  var ucps = userConstrainedPolicies(state, n);
1698
1142
  var policyOk = state.explicitPolicy > 0 || ucps.length > 0;
1699
1143
  if (!policyOk) {
@@ -1704,7 +1148,6 @@ async function validate(path, opts) {
1704
1148
  failed = true;
1705
1149
  }
1706
1150
 
1707
- // Opt-in CT gate: verify the target's embedded SCTs and record the verdict on its checks (RFC 6962).
1708
1151
  if (opts.ctLogList != null) {
1709
1152
  var ctCheck = await ctGateCheck(certs[n - 1], certs, anchor, opts.ctLogList, opts.ctPolicy || {}, n, opts.time);
1710
1153
  var target = null;
@@ -1715,12 +1158,6 @@ async function validate(path, opts) {
1715
1158
 
1716
1159
  return {
1717
1160
  valid: !failed,
1718
- // What the revocation rule and the anchor's own trust metadata actually decided. `valid` alone
1719
- // cannot answer either, and both are questions a stored verdict is re-read to settle: was this
1720
- // certificate ever established as un-revoked, and was the anchor's distrust date consulted.
1721
- // The WEAKEST outcome on the path, not the fact that a checker ran: a certificate nobody could
1722
- // answer for leaves revocation unestablished however many others answered, and deriving the word
1723
- // from "a checker ran" put that run on the same value as one that established every answer.
1724
1161
  revocationChecked: !revocationRan ? false
1725
1162
  : revocationUndetermined ? "undetermined"
1726
1163
  : revocationWaived ? "waived" : "determined",
@@ -1735,8 +1172,6 @@ async function validate(path, opts) {
1735
1172
  workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm,
1736
1173
  workingPublicKeyParameters: state.workingPublicKeyParameters,
1737
1174
  validPolicyTree: treeWithoutParent(state.validPolicyTree),
1738
- // 6.1.5(f): the authority-constrained policy set = the leaf-depth policies
1739
- // in the valid-policy tree intersected with user-initial-policy-set.
1740
1175
  userConstrainedPolicySet: ucps,
1741
1176
  };
1742
1177
  }
@@ -1753,9 +1188,6 @@ function userConstrainedPolicies(state, n) {
1753
1188
  });
1754
1189
  var set = {};
1755
1190
  Object.keys(explicit).forEach(function (p) { if (anyUser || contains(uips, p)) set[p] = true; });
1756
- // 6.1.5(g) step 3: a depth-n anyPolicy node under a restrictive user set
1757
- // expands to each user policy (the intersection of anyPolicy with the user
1758
- // set is the user set itself).
1759
1191
  if (hasAnyLeaf) {
1760
1192
  if (anyUser) set[OID.anyPolicy] = true;
1761
1193
  else uips.forEach(function (p) { set[p] = true; });
@@ -1763,7 +1195,6 @@ function userConstrainedPolicies(state, n) {
1763
1195
  return Object.keys(set);
1764
1196
  }
1765
1197
 
1766
- // ---- the CRL revocation checker (6.3) -------------------------------------
1767
1198
 
1768
1199
  var OID_IDP = oid.byName("issuingDistributionPoint");
1769
1200
  var OID_DELTA_CRL = oid.byName("deltaCRLIndicator");
@@ -1771,41 +1202,15 @@ var OID_AUTHORITY_KEY_ID = oid.byName("authorityKeyIdentifier");
1771
1202
  var OID_CRL_NUMBER = oid.byName("cRLNumber");
1772
1203
  var OID_FRESHEST_CRL = oid.byName("freshestCRL");
1773
1204
 
1774
- // The RFC 5280 sec. 5.2.5 IssuingDistributionPoint grammar, shared with the CRL
1775
- // verbs (pkix.issuingDistributionPoint) so the scope this validator reads and the
1776
- // scope pki.crl.isRevoked refuses to answer past are read by the same rules.
1777
1205
  var IDP_SCHEMA = pkix.issuingDistributionPoint("path/bad-idp");
1778
1206
 
1779
- // RFC 5280 sec. 6.3.2(a): the legal members of reasons_mask are exactly the eight
1780
- // named ReasonFlags bits, 1..8 (`unused` bit 0 is not a reason, and `unspecified`
1781
- // has no ReasonFlags bit at all). 0x1FE is bits 1..8 set.
1782
- //
1783
- // NOTE the numbering divergence, which is the trap in this area: a ReasonFlags BIT
1784
- // is not a CRLReason VALUE. Bits 1..6 coincide with codes 1..6, but bit 7 is
1785
- // privilegeWithdrawn (code 9) and bit 8 is aACompromise (code 10). sec. 6.3.3 never
1786
- // needs a mapping between them -- the mask comes only from IDP/DP ReasonFlags and
1787
- // cert_status only from an entry's CRLReason -- so no bit<->code table exists here,
1788
- // deliberately. constants.NAMES.REASON_FLAGS and NAMES.CRL_REASON stay separate.
1789
1207
  var ALL_REASONS = 0x1FE;
1790
1208
 
1791
- // A ReasonFlags BIT STRING { unusedBits, bytes } -> a bit mask, or null when the
1792
- // encoding is not minimal DER (X.690 sec. 11.2.2 NamedBitList: trailing zero bits
1793
- // MUST be dropped). A null tells the caller the CRL's scope is unknown, which is
1794
- // the same fail-closed posture decodeIdp already takes for a malformed IDP.
1795
- //
1796
- // Bits at or above 9 are undefined by sec. 4.2.1.13. They are IGNORED rather than
1797
- // rejected: an undefined bit can never help reach all-reasons, so ignoring it can
1798
- // only withhold coverage, never grant it. Reading a bounded prefix also means a
1799
- // hostile multi-kilobyte ReasonFlags costs nothing.
1800
1209
  function reasonMaskFromBitString(bs) {
1801
1210
  if (!bs || !bs.bytes) return null;
1802
1211
  try { schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (msg) { throw E("path/bad-idp", msg); }); }
1803
1212
  catch (_e) { return null; }
1804
1213
  var mask = 0;
1805
- // From bit 1: sec. 6.3.2(a) defines the legal members as the eight NAMED reasons, and ReasonFlags
1806
- // bit 0 is -- not a reason at all. Admitting it could not grant coverage (the completeness
1807
- // test masks with ALL_REASONS) but would make a shard that asserts ONLY bit 0 look like partial
1808
- // coverage rather than none, which is the wrong verdict to report.
1809
1214
  for (var bit = 1; bit <= 8; bit++) {
1810
1215
  var byteI = bit >> 3;
1811
1216
  if (byteI >= bs.bytes.length) break;
@@ -1814,34 +1219,19 @@ function reasonMaskFromBitString(bs) {
1814
1219
  return mask;
1815
1220
  }
1816
1221
 
1817
- // RFC 5280 sec. 6.3.3(d)(1)-(4) -- the interim_reasons_mask, in ONE place.
1818
- // (1) both present -> intersection
1819
- // (2) IDP present, DP absent -> the IDP's reasons
1820
- // (3) IDP absent, DP present -> the DP's reasons
1821
- // (4) both absent -> all-reasons
1822
- // A null mask means "present but unreadable"; the caller has already marked such a
1823
- // CRL unusable, so treating it as 0 here is belt-and-braces, never a coverage grant.
1824
1222
  function interimReasonMask(idpMask, dpMask) {
1825
- if (idpMask != null && dpMask != null) return idpMask & dpMask; // (d)(1)
1826
- if (idpMask != null) return idpMask; // (d)(2)
1827
- if (dpMask != null) return dpMask; // (d)(3)
1828
- return ALL_REASONS; // (d)(4)
1223
+ if (idpMask != null && dpMask != null) return idpMask & dpMask;
1224
+ if (idpMask != null) return idpMask;
1225
+ if (dpMask != null) return dpMask;
1226
+ return ALL_REASONS;
1829
1227
  }
1830
1228
 
1831
1229
  function decodeIdp(ext) {
1832
- // Surface the scope flags the checker gates on. ANY structural or value fault
1833
- // -- non-SEQUENCE, unknown/duplicate/out-of-order field tag, a non-DER BOOLEAN,
1834
- // an encoded-FALSE default, a malformed DistributionPointName -- leaves the
1835
- // CRL's scope unknown: the CRL is unusable, never assumed unrestricted.
1836
1230
  var out = { hasDistributionPoint: false, distributionPoint: null, onlyUser: false, onlyCa: false, onlySomeReasons: null, indirect: false, onlyAttr: false, malformed: false };
1837
1231
  var m;
1838
1232
  try {
1839
1233
  m = schema.walk(IDP_SCHEMA, asn1.decode(ext.value), NS);
1840
1234
  if (m.fields.distributionPoint.present) {
1841
- // distributionPoint [0] EXPLICIT-wraps the DistributionPointName CHOICE
1842
- // (a context tag on a CHOICE-typed field is always EXPLICIT). The decoded
1843
- // name feeds the sec. 6.3.3(b)(2)(i) correspondence against the
1844
- // certificate's own DistributionPoints in the checker below.
1845
1235
  var dpnWrap = m.fields.distributionPoint.node;
1846
1236
  if (!dpnWrap.children || dpnWrap.children.length !== 1) {
1847
1237
  throw E("path/bad-idp", "IssuingDistributionPoint distributionPoint [0] must wrap exactly one DistributionPointName");
@@ -1852,8 +1242,6 @@ function decodeIdp(ext) {
1852
1242
  catch (_e) { out.malformed = true; return out; }
1853
1243
  function flag(f) {
1854
1244
  if (!f.present) return false;
1855
- // A present flag must encode DER-TRUE; a FALSE is the omitted DEFAULT
1856
- // (X.690 sec. 11.5), so mark the scope unknown and report not-set.
1857
1245
  var isSet = f.value === true;
1858
1246
  if (!isSet) out.malformed = true;
1859
1247
  return isSet;
@@ -1862,10 +1250,6 @@ function decodeIdp(ext) {
1862
1250
  out.onlyUser = flag(m.fields.onlyContainsUserCerts);
1863
1251
  out.onlyCa = flag(m.fields.onlyContainsCACerts);
1864
1252
  out.onlySomeReasons = m.fields.onlySomeReasons.present ? true : null;
1865
- // The reason BITS, not just their presence: sec. 6.3.3(d)(1)/(d)(2) need the
1866
- // value. A present-but-non-minimal ReasonFlags leaves the CRL's scope unknown,
1867
- // which is the same unusable posture every other IDP fault takes -- otherwise
1868
- // the (d)(1) intersection would be computed over an encoding DER forbids.
1869
1253
  out.onlySomeReasonsMask = null;
1870
1254
  if (m.fields.onlySomeReasons.present) {
1871
1255
  out.onlySomeReasonsMask = reasonMaskFromBitString(m.fields.onlySomeReasons.value);
@@ -1876,17 +1260,6 @@ function decodeIdp(ext) {
1876
1260
  return out;
1877
1261
  }
1878
1262
 
1879
- // RFC 5280 sec. 6.3.3(b)(2)(i): find the certificate DistributionPoint that
1880
- // CORRESPONDS to a shard CRL's IDP distribution point name. The correspondence
1881
- // rule itself -- at least one name in common, compared by BYTE-IDENTICAL DER,
1882
- // mixed name forms never corresponding -- is guard.name.dpnCorresponds, which
1883
- // is also what a CMP crlUpdate answer is held to, so one definition decides
1884
- // whether a CRL speaks for a point wherever the question is asked. Per sec.
1885
- // 6.3.3(b)(1), a DP naming a cRLIssuer participates only when that cRLIssuer
1886
- // is the certificate issuer itself: the checker only consults CRLs issued BY
1887
- // the certificate issuer and rejects indirect CRLs, so a DP delegated to
1888
- // another CRL issuer is out of play. Returns the matched DP (its `reasons`
1889
- // feeds the coarse reason-mask rule) or null.
1890
1263
  function correspondingCertDp(idpDpn, certDPs, issuerRdns) {
1891
1264
  if (!idpDpn || !certDPs) return null;
1892
1265
  for (var i = 0; i < certDPs.length; i++) {
@@ -1898,28 +1271,16 @@ function correspondingCertDp(idpDpn, certDPs, issuerRdns) {
1898
1271
  return null;
1899
1272
  }
1900
1273
 
1901
- // Does a DistributionPoint's cRLIssuer name the certificate issuer? True iff
1902
- // one of its GeneralNames is a directoryName equal to the issuer DN under the
1903
- // RFC 5280 sec. 7.1 comparison (the shared name guard, via this file's dnEqual
1904
- // wrapper). Any fault -- a DN the comparison rejects for an embedded control
1905
- // byte -- resolves false: the DP stays out of the correspondence (fail closed).
1906
1274
  function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
1907
1275
  for (var i = 0; i < cRLIssuer.names.length; i++) {
1908
1276
  var n = cRLIssuer.names[i];
1909
1277
  if (n.tagNumber !== 4 || !n.value || !n.value.rdns) continue;
1910
- // dnEqual throws only on a control-byte / malformed cRLIssuer DN; returning false excludes
1911
- // that DP from the correspondence (a malformed indirect-CRL issuer name never corresponds).
1912
1278
  try { if (dnEqual(n.value.rdns, issuerRdns)) return true; }
1913
1279
  catch (_e) { return false; }
1914
1280
  }
1915
1281
  return false;
1916
1282
  }
1917
1283
 
1918
- // The raw extnValue octets of a CRL extension, or null. sec. 6.3.3(c)(2)/(c)(3)
1919
- // compare the IDP and authorityKeyIdentifier of a delta and its base for identity;
1920
- // the comparison is over the exact encoded octets (the sec. 5.2.5 @3648
1921
- // "identical encoding" discipline already used for distributionPoint matching),
1922
- // never over a re-normalized decode.
1923
1284
  function crlExtValue(theCrl, wantOid) {
1924
1285
  for (var i = 0; i < theCrl.crlExtensions.length; i++) {
1925
1286
  if (theCrl.crlExtensions[i].oid === wantOid) return theCrl.crlExtensions[i].value;
@@ -1927,15 +1288,6 @@ function crlExtValue(theCrl, wantOid) {
1927
1288
  return null;
1928
1289
  }
1929
1290
 
1930
- // One pass over the bundle: split complete CRLs from deltas and decode, per CRL,
1931
- // the values the merge preconditions compare. A delta whose BaseCRLNumber will not
1932
- // decode as a non-negative INTEGER is unusable AS A DELTA but stays consultable for
1933
- // revocation -- dropping it wholesale would lose a revocation it lists (T1).
1934
- // RFC 5280 sec. 5.2.3 @3404 bounds a CRL number at 20 octets, and pki.crl.sign enforces that on
1935
- // emission. A number past it is non-conforming, so it does not earn the merge -- which can RELEASE
1936
- // a certificate. The value still decodes and the CRL is still consulted for revocation; only the
1937
- // new capability is withheld. Measured on the ENCODED length, matching the producer-side check
1938
- // rather than a re-derived numeric bound.
1939
1291
  function crlNumberWithinBound(n) {
1940
1292
  if (typeof n !== "bigint" || n < 0n) return false;
1941
1293
  try { return asn1.decode(asn1.build.integer(n)).content.length <= 20; }
@@ -1951,9 +1303,6 @@ function classifyCrls(parsed) {
1951
1303
  for (var dz = 0; dz < theCrl.crlExtensions.length; dz++) {
1952
1304
  if (theCrl.crlExtensions[dz].oid === OID_DELTA_CRL) { deltaCritical = theCrl.crlExtensions[dz].critical === true; break; }
1953
1305
  }
1954
- // schema-crl already decoded cRLNumber to a non-negative BigInt; the IDP,
1955
- // authorityKeyIdentifier and deltaCRLIndicator values stay RAW octets there,
1956
- // which is exactly what the byte-identical compares below need.
1957
1306
  var num = crlExtValue(theCrl, OID_CRL_NUMBER);
1958
1307
  var rec = {
1959
1308
  crl: theCrl,
@@ -1966,47 +1315,24 @@ function classifyCrls(parsed) {
1966
1315
  if (deltaRaw === null) { completes.push(rec); continue; }
1967
1316
  try {
1968
1317
  var n = asn1.read.integer(asn1.decode(deltaRaw));
1969
- // sec. 5.2.3 @3404: a CRL number may be up to 20 octets. It stays a BigInt
1970
- // through every comparison below -- narrowing through Number would collapse
1971
- // large values and make 5.2.4(c)/(d) compare the wrong things.
1972
- // sec. 5.2.4 @3447 makes deltaCRLIndicator a MUST-be-critical extension. A non-critical one
1973
- // is non-conforming, so it does not earn the NEW capability of being merged -- merging can
1974
- // RELEASE a certificate, and that must rest on a conforming indicator. It is still classified
1975
- // as a delta and still consulted for revocation, which is the shipped behavior and the
1976
- // conservative direction.
1977
1318
  if (crlNumberWithinBound(n) && deltaCritical) { rec.baseCrlNumber = n; rec.mergeable = true; }
1978
1319
  } catch (_e) {
1979
- // Unusable AS A DELTA, but still scanned for revocation. That is the conservative direction:
1980
- // the CRL is still the issuer's, signed and current (it must pass every gate before it is
1981
- // consulted), so a serial it lists is a genuine revocation and dropping it wholesale could
1982
- // lose one. Consulting it can only ever find MORE revocations, never grant coverage -- a
1983
- // delta contributes no reason mask.
1984
1320
  }
1985
1321
  deltas.push(rec);
1986
1322
  }
1987
1323
  return { completes: completes, deltas: deltas };
1988
1324
  }
1989
1325
 
1990
- // RFC 5280 sec. 6.3.3(c)(1)-(3) + sec. 5.2.4(a)-(d), as one predicate. Every
1991
- // comparison is BigInt or byte-exact: no narrowing, no canonicalization. An
1992
- // extension ABSENT on both sides matches; present on one side only does not.
1993
1326
  function deltaMergesWith(delta, complete) {
1994
1327
  if (!delta.mergeable) return false;
1995
- // Both numbers are required: 5.2.4(c)/(d) are ordering rules, and a missing
1996
- // cRLNumber on either side leaves the ordering unknowable -- fail closed.
1997
1328
  if (complete.crlNumber === null || delta.crlNumber === null) return false;
1998
- if (!dnEqualUsable(delta.crl.issuer.rdns, complete.crl.issuer.rdns)) return false; // (c)(1)
1999
- if (!sameRawExt(delta.idpRaw, complete.idpRaw)) return false; // (c)(2) / 5.2.4(b)
2000
- if (!sameRawExt(delta.akiRaw, complete.akiRaw)) return false; // (c)(3)
2001
- if (!(complete.crlNumber >= delta.baseCrlNumber)) return false; // 5.2.4(c)
2002
- if (!(complete.crlNumber < delta.crlNumber)) return false; // 5.2.4(d)
1329
+ if (!dnEqualUsable(delta.crl.issuer.rdns, complete.crl.issuer.rdns)) return false;
1330
+ if (!sameRawExt(delta.idpRaw, complete.idpRaw)) return false;
1331
+ if (!sameRawExt(delta.akiRaw, complete.akiRaw)) return false;
1332
+ if (!(complete.crlNumber >= delta.baseCrlNumber)) return false;
1333
+ if (!(complete.crlNumber < delta.crlNumber)) return false;
2003
1334
  return true;
2004
1335
  }
2005
- // dnEqual THROWS on a DN carrying an embedded NUL/control byte (CVE-2009-2408).
2006
- // For the merge preconditions a DN that cannot be compared is simply not a match:
2007
- // the delta and the base are not shown to share an issuer, so they are not merged.
2008
- // The throw is contained here rather than at the call site so the predicate itself
2009
- // stays a straight-line sequence of comparisons.
2010
1336
  function dnEqualUsable(a, b) {
2011
1337
  try { return dnEqual(a, b); }
2012
1338
  catch (_e) { return false; }
@@ -2018,10 +1344,6 @@ function sameRawExt(a, b) {
2018
1344
  return a.equals(b);
2019
1345
  }
2020
1346
 
2021
- // sec. 5.2.4 @3580 makes MULTIPLE current deltas for one scope legal and says the
2022
- // application SHOULD take the one with the latest thisUpdate. Selection, never
2023
- // rejection: treating two current deltas as a fault would refuse a conforming
2024
- // publication. Ties break on the greater cRLNumber, then first-wins.
2025
1347
  function selectDelta(candidates) {
2026
1348
  var best = null;
2027
1349
  for (var i = 0; i < candidates.length; i++) {
@@ -2079,31 +1401,14 @@ function selectDelta(candidates) {
2079
1401
  */
2080
1402
  function crlChecker(crls, opts) {
2081
1403
  var parsed = (crls || []).map(function (c, ci) {
2082
- // Re-derived from the bytes the parser read, exactly as a certificate is. A CRL's answer is a
2083
- // verdict, and its signature covers a byte range while the revocation list and the scope
2084
- // extensions are separate properties of the parsed object: keep a correctly signed CRL's
2085
- // `tbsBytes` and signature, empty `revokedCertificates`, and the signature still verifies while
2086
- // a revoked certificate reports good. The scope fields fail the same way -- an emptied
2087
- // `crlExtensions` turns a scope-restricted CRL into one that answers for everything.
2088
1404
  return guard.parsed.acceptDerived(c, "crl", crl.parse, E, "path/bad-input",
2089
1405
  "crlChecker: crls[" + ci + "]");
2090
1406
  });
2091
- // RFC 5280 sec. 6.3.1(b): use-deltas is an INPUT to the algorithm. Default ON --
2092
- // a caller holding a delta wants it used -- and turning it off never makes a
2093
- // verdict weaker, only less determined.
2094
1407
  var useDeltas = !(opts && opts.useDeltas === false);
2095
1408
  return {
2096
1409
  check: async function (cert, issuer, ctx) {
2097
1410
  var time = ctx.time;
2098
1411
  var historical = ctx.historicalMode === true;
2099
- // The cert's CA-ness gates the IDP scope flags (onlyContainsUserCerts /
2100
- // onlyContainsCACerts, RFC 5280 sec. 6.3.3(b)(2)). An UNREADABLE
2101
- // basicConstraints leaves that scope undeterminable -- guessing "not a
2102
- // CA" would consult a user-only CRL for what may be a CA certificate
2103
- // (out of the CRL's scope, so its silence proves nothing) -- so certIsCa
2104
- // stays null, BOTH scoped forms are skipped below, and only a
2105
- // full-scope CRL can speak for this certificate. The decode fault is
2106
- // carried into the undetermined verdict's reason.
2107
1412
  var certIsCa = null, certScopeFault = null;
2108
1413
  try {
2109
1414
  var bc = decodeExt(cert, OID.basicConstraints);
@@ -2111,26 +1416,12 @@ function crlChecker(crls, opts) {
2111
1416
  } catch (e) {
2112
1417
  certScopeFault = pathCode(e, "path/bad-extension-value");
2113
1418
  }
2114
- // The certificate's own cRLDistributionPoints, decoded once per check:
2115
- // the sec. 6.3.3(b)(2)(i) correspondence gate below needs the DP names.
2116
- // A decode fault leaves certDPs null -- every DP-scoped shard then stays
2117
- // revocation-only (no correspondence can be shown; fail closed) while
2118
- // the full-CRL path is unaffected (mirrors the basicConstraints
2119
- // handling above); the check never crashes on a malformed extension.
2120
1419
  var certDPs = null;
2121
1420
  var certCdpExt = findExt(cert, OID.cRLDistributionPoints);
2122
1421
  if (certCdpExt) {
2123
1422
  try { certDPs = EXT_DECODERS[OID.cRLDistributionPoints](certCdpExt.value); }
2124
1423
  catch (_e) { certDPs = null; }
2125
1424
  }
2126
- // RFC 5280 sec. 6.3.3(f): IF a keyUsage extension is present in the CRL issuer's
2127
- // certificate, the cRLSign bit must be VERIFIED set. An issuer that OMITS
2128
- // keyUsage is unconstrained (the same rule the sec. 6.1.4(n) keyCertSign gate
2129
- // applies to certificate signing), so its CRL is authoritative. The anchor
2130
- // is likewise unconstrained (issuerCert is null for the cert it directly
2131
- // issued). A PRESENT-but-unreadable keyUsage cannot be verified -- treating
2132
- // it like an absent one would let garbage keyUsage bytes authorize CRL
2133
- // signing -- so no CRL from this issuer can be authoritative.
2134
1425
  var signerAuthorized = true;
2135
1426
  if (issuer && issuer.issuerCert) {
2136
1427
  var iku;
@@ -2141,38 +1432,14 @@ function crlChecker(crls, opts) {
2141
1432
  if (iku && iku.value.cRLSign !== true) signerAuthorized = false;
2142
1433
  }
2143
1434
 
2144
- // Consult EVERY CRL issued by the cert's issuer -- a clean CRL must not
2145
- // shadow a revoking one (RFC 5280 sec. 6.3.3). A serial listed in ANY
2146
- // authoritative, in-scope, current, verified CRL is revoked; the cert is
2147
- // "good" only when the CRLs consulted together cover ALL EIGHT revocation
2148
- // reasons (sec. 6.3.3(l) + the termination rule at @5291) and none list it.
2149
- //
2150
- // Note there is deliberately no early exit once the mask is complete: the
2151
- // shipped checker scans every CRL so a clean one cannot shadow a revoking
2152
- // one, and stopping at full coverage would reintroduce exactly that.
2153
- var certStatus = null; // sec. 6.3.2(b) cert_status; null = UNREVOKED
2154
- var reasonsMask = 0; // sec. 6.3.2(a) reasons_mask; the empty set
1435
+ var certStatus = null;
1436
+ var reasonsMask = 0;
2155
1437
  var releasedByUnmergedDelta = false;
2156
- // A CURRENT, authoritative delta that merged with nothing means the local
2157
- // revocation picture is incomplete: the base it names may be newer than any
2158
- // complete CRL held here, so a clean base proves less than it appears to.
2159
- // The shipped checker blocks on exactly this, and the merge must only
2160
- // ever turn undetermined INTO good -- never make an unmerged delta weaker
2161
- // than it was before this feature existed.
2162
1438
  var sawUnmergedDelta = false;
2163
1439
  var classified = classifyCrls(parsed);
2164
1440
  var consumedDeltas = [];
2165
1441
 
2166
- // Every gate a CRL must pass before it may speak for this certificate, in
2167
- // the shipped order. Returns null when the CRL is unusable, otherwise the
2168
- // interim_reasons_mask it may contribute -- 0 meaning "consulted for
2169
- // revocation only", which is how a non-corresponding shard is honestly
2170
- // encoded (it can reveal a revocation but can never establish coverage).
2171
1442
  async function gateCrl(rec) {
2172
- // Memoized per RECORD, and the records are rebuilt by classifyCrls on every check(), so the
2173
- // cache is per-call and the checker stays re-entrant. Without it the base-by-delta pairing
2174
- // below would repeat an asynchronous public-key signature verification for every pair --
2175
- // O(completes x deltas) verifications where the shipped loop did O(crls).
2176
1443
  if (rec._gated) return rec._gate;
2177
1444
  rec._gate = await gateCrlUncached(rec);
2178
1445
  rec._gated = true;
@@ -2180,48 +1447,25 @@ function crlChecker(crls, opts) {
2180
1447
  }
2181
1448
  async function gateCrlUncached(rec) {
2182
1449
  var theCrl = rec.crl;
2183
- // dnEqual throws on a DN carrying an embedded NUL/control byte (CVE-2009-2408).
2184
- // A single malformed CRL in the bundle must NOT abort the whole check (which
2185
- // would mask a later authoritative CRL and pass under softFail) -- treat it
2186
- // as unusable and skip it, consulting the remaining CRLs.
2187
1450
  var issuerMatches;
2188
1451
  try { issuerMatches = dnEqual(theCrl.issuer.rdns, cert.issuer.rdns); }
2189
1452
  catch (_e) { return null; }
2190
1453
  if (!issuerMatches) return null;
2191
1454
  if (!signerAuthorized) return null;
2192
1455
 
2193
- // A validly-signed CRL carrying a CRITICAL extension this checker does
2194
- // not understand (anything but issuingDistributionPoint / deltaCRLIndicator)
2195
- // may change the CRL's scope or meaning -- treat it as unusable (RFC 5280
2196
- // sec. 5.2 critical-extension semantics), never authoritative.
2197
1456
  var unhandledCritical = false;
2198
1457
  for (var x = 0; x < theCrl.crlExtensions.length; x++) {
2199
1458
  var xe = theCrl.crlExtensions[x];
2200
1459
  if (xe.critical && xe.oid !== OID_IDP && xe.oid !== OID_DELTA_CRL) { unhandledCritical = true; break; }
2201
1460
  }
2202
- // RFC 5280 sec. 5.3: a critical CRL-ENTRY extension the checker cannot
2203
- // process (anything but reasonCode) makes the CRL unusable for ANY
2204
- // certificate, not just the entry that carries it.
2205
1461
  for (var ry = 0; ry < theCrl.revokedCertificates.length && !unhandledCritical; ry++) {
2206
- // Coverage residual -- the `|| []` fallback is unreachable: schema-crl sets
2207
- // crlEntryExtensions to [] when absent, so it is always an array.
2208
1462
  var ees = theCrl.revokedCertificates[ry].crlEntryExtensions || [];
2209
1463
  for (var ex = 0; ex < ees.length; ex++) {
2210
- // Key on the stable OID only -- a display name is registry-dependent
2211
- // (a custom OID could be registered as "reasonCode"), so matching by
2212
- // name would let an unhandled critical entry extension fail open.
2213
1464
  if (ees[ex].critical && ees[ex].oid !== OID_REASON_CODE) { unhandledCritical = true; break; }
2214
1465
  }
2215
1466
  }
2216
1467
  if (unhandledCritical) return null;
2217
1468
 
2218
- // sec. 6.3.3(d): the interim reason mask this CRL contributes. Its two
2219
- // inputs are the IDP's onlySomeReasons and the CORRESPONDING certificate
2220
- // DistributionPoint's reasons -- never an entry's reasonCode (sec. 5.2.5
2221
- // @3628 explicitly permits a shard entry to omit one).
2222
- // Set when the shard covers no reasons for this certificate. It is recorded rather than
2223
- // RETURNED here: the currency and signature gates below still have to run, or an expired,
2224
- // not-yet-valid or FORGED shard could be scanned for revocations and falsely revoke.
2225
1469
  var noCoverage = false;
2226
1470
  var idpMask = null, dpMask = null, sawIdp = false;
2227
1471
  var idpExtension = null;
@@ -2229,81 +1473,39 @@ function crlChecker(crls, opts) {
2229
1473
  if (idpExtension) {
2230
1474
  sawIdp = true;
2231
1475
  var idp = decodeIdp(idpExtension);
2232
- if (idp.malformed) return null; // scope unknown -> unusable
2233
- // An indirect CRL carries entries for other issuers keyed by the
2234
- // per-entry certificateIssuer attribute (not tracked here) -- matching
2235
- // by serial alone could revoke the wrong cert or falsely cover it, so
2236
- // treat an indirect CRL as unusable until certificateIssuer is honored.
1476
+ if (idp.malformed) return null;
2237
1477
  if (idp.indirect) return null;
2238
- if (idp.onlyAttr) return null; // scoped to attribute certs, not this public-key cert
2239
- if (idp.onlyCa && certIsCa !== true) return null; // out of scope (or CA-ness undeterminable)
1478
+ if (idp.onlyAttr) return null;
1479
+ if (idp.onlyCa && certIsCa !== true) return null;
2240
1480
  if (idp.onlyUser && certIsCa !== false) return null;
2241
- // The same fail-closed decision the distributionPoint correspondence rests on (sec. 5.2.5
2242
- // @3601 lets a relying party not support the IDP at all): a scope a non-supporting
2243
- // verifier would IGNORE is not a scope to build coverage on. So a non-critical IDP's
2244
- // onlySomeReasons contributes nothing -- which also preserves the shipped property that
2245
- // an onlySomeReasons shard could only ever WITHHOLD good, never establish it.
2246
1481
  if (idpExtension.critical === true) idpMask = idp.onlySomeReasonsMask;
2247
1482
  else if (idp.onlySomeReasons) noCoverage = true;
2248
1483
  if (idp.hasDistributionPoint) {
2249
- // RFC 5280 sec. 6.3.3(b)(2)(i): a partition shard speaks for this
2250
- // certificate only when the IDP's distribution point shares at
2251
- // least one IDENTICALLY-ENCODED name with one of the certificate's
2252
- // own DistributionPoints (sec. 5.2.5: "The identical encoding MUST
2253
- // be used in the distributionPoint fields of the certificate and
2254
- // the CRL"). The IDP must also be CRITICAL to be relied on for
2255
- // scope: sec. 5.2.5 describes the IDP as "a critical CRL extension"
2256
- // (descriptive phrasing, not an imperative MUST -- and @3601 lets a
2257
- // relying party not support it at all), so building coverage on a
2258
- // scope a non-supporting verifier would ignore is a deliberate
2259
- // fail-closed decision. A non-corresponding shard contributes NO
2260
- // coverage but is still consulted for revocation below: serials are
2261
- // unique per issuer, so a listed serial is a genuine revocation.
2262
1484
  var matchedDp = idpExtension.critical === true
2263
1485
  ? correspondingCertDp(idp.distributionPoint, certDPs, cert.issuer.rdns)
2264
1486
  : null;
2265
1487
  if (!matchedDp) noCoverage = true;
2266
- // sec. 6.3.3(d)(1)/(d)(3): a matched DP carrying `reasons` bounds the
2267
- // interim mask. A present-but-unreadable value cannot bound anything
2268
- // safely, so it contributes nothing rather than defaulting open.
2269
1488
  else if (matchedDp.reasons) {
2270
1489
  dpMask = reasonMaskFromBitString(matchedDp.reasons);
2271
1490
  if (dpMask === null) noCoverage = true;
2272
1491
  }
2273
1492
  }
2274
1493
  }
2275
- if (guard.time.instantOf(theCrl.thisUpdate) > guard.time.instantOf(time)) return null; // not yet valid
2276
- // A CRL with no nextUpdate has no bounded validity -- its currency
2277
- // cannot be confirmed (RFC 5280 sec. 5.1.2.5 requires nextUpdate), so a
2278
- // replayed old CRL must not read "good". Treat it as unusable.
1494
+ if (guard.time.instantOf(theCrl.thisUpdate) > guard.time.instantOf(time)) return null;
2279
1495
  if (!theCrl.nextUpdate ||
2280
- guard.time.instantOf(theCrl.nextUpdate) < guard.time.instantOf(time)) return null; // stale / no bound
1496
+ guard.time.instantOf(theCrl.nextUpdate) < guard.time.instantOf(time)) return null;
2281
1497
 
2282
1498
  var sigOk = await crlVerify.verifyCrlSignature(theCrl, issuer.workingPublicKey);
2283
- if (!sigOk) return null; // unverifiable -> not authoritative
1499
+ if (!sigOk) return null;
2284
1500
 
2285
- // sec. 6.3.3 @5295: a CRL not named by any distribution point is processed
2286
- // as though under a DP whose `reasons` and `cRLIssuer` are absent -- i.e.
2287
- // all-reasons. That is why a full-scope CRL still covers a certificate
2288
- // that happens to carry a cRLDistributionPoints extension.
2289
1501
  void sawIdp;
2290
1502
  return { interim: noCoverage ? 0 : interimReasonMask(idpMask, dpMask) };
2291
1503
  }
2292
1504
 
2293
- // sec. 6.3.3(i)/(j): the certificate's entry on one CRL, as a CRLReason
2294
- // value, or null for UNREVOKED. sec. 6.3.3(i)(2): an entry with no
2295
- // reasonCode extension is `unspecified` (0), which is a revocation.
2296
1505
  function scanCrl(theCrl) {
2297
1506
  for (var r = 0; r < theCrl.revokedCertificates.length; r++) {
2298
1507
  var entry = theCrl.revokedCertificates[r];
2299
1508
  if (entry.serialNumberHex !== cert.serialNumberHex) continue;
2300
- // A revocation is effective as of its revocationDate (RFC 5280 sec. 5.3).
2301
- // In the DEFAULT present-time validation a listed serial is revoked
2302
- // regardless of that date -- a future revocationDate is post-dating or
2303
- // clock skew and must NOT read good. Only under an EXPLICIT historical
2304
- // validation (opts.historicalMode) -- validating as of a past instant,
2305
- // e.g. a timestamped signature -- does an entry dated AFTER the
2306
- // validation time not yet apply.
2307
1509
  // allow:nan-date-comparison-unguarded -- revocationDate is codec-parsed (NaN-rejected); a NaN check time makes this FAIL CLOSED (the skip is not taken -> the entry is treated as revoked), and `time` is validated at the path.validate / crlChecker entry points.
2308
1510
  if (historical && guard.time.isDate(entry.revocationDate) &&
2309
1511
  guard.time.instantOf(entry.revocationDate) > guard.time.instantOf(time)) continue;
@@ -2313,16 +1515,6 @@ function crlChecker(crls, opts) {
2313
1515
  return null;
2314
1516
  }
2315
1517
 
2316
- // sec. 6.3.3(a)(2): a delta is obtained only when use-deltas is set AND a
2317
- // locator exists -- freshestCRL on the certificate or on the complete CRL.
2318
- // sec. 5.2.6 @3719: the locator's CONTENTS are only ever used to find a
2319
- // delta, never to validate one, so its presence is the whole gate.
2320
- // PRESENCE only, deliberately. sec. 5.2.6 @3719 says the freshestCRL contents are used to
2321
- // LOCATE a delta and never to validate one, so decoding them would be using a value the RFC
2322
- // says not to use. It is also not a security control: enabling the merge grants nothing on its
2323
- // own, because a delta still has to pass every gate -- issuer, authorization, criticality,
2324
- // scope, currency and SIGNATURE -- plus the sec. 5.2.4 merge preconditions. An attacker who
2325
- // could satisfy those already controls the issuing key.
2326
1518
  var certHasFreshest = !!findExt(cert, OID_FRESHEST_CRL);
2327
1519
  function deltaLocatorPresent(completeRec) {
2328
1520
  return certHasFreshest || crlExtValue(completeRec.crl, OID_FRESHEST_CRL) !== null;
@@ -2333,11 +1525,6 @@ function crlChecker(crls, opts) {
2333
1525
  var gate = await gateCrl(rec);
2334
1526
  if (!gate) continue;
2335
1527
 
2336
- // sec. 6.3.3(c): pair this complete CRL with a delta it may be combined
2337
- // with. The delta passes its OWN gates first -- sec. 6.3.3(h) requires
2338
- // its signature verified and (f) its issuer authorized, exactly as for a
2339
- // complete CRL -- so a stale, unverifiable or out-of-scope delta is never
2340
- // merged.
2341
1528
  var chosenDelta = null;
2342
1529
  if (useDeltas && deltaLocatorPresent(rec)) {
2343
1530
  var candidates = [];
@@ -2345,12 +1532,6 @@ function crlChecker(crls, opts) {
2345
1532
  var cand = classified.deltas[di];
2346
1533
  if (!deltaMergesWith(cand, rec)) continue;
2347
1534
  if (!(await gateCrl(cand))) continue;
2348
- // Mergeable and usable: this delta's scope IS covered by a merge, even
2349
- // if sec. 5.2.4 @3580 selection prefers a sibling with a later
2350
- // thisUpdate. Only a delta that pairs with NO complete CRL leaves the
2351
- // picture incomplete -- a losing candidate must not block a good result,
2352
- // or publishing two current deltas (which the RFC permits) would be
2353
- // worse than publishing one.
2354
1535
  cand.accounted = true;
2355
1536
  candidates.push(cand);
2356
1537
  }
@@ -2360,35 +1541,20 @@ function crlChecker(crls, opts) {
2360
1541
  var status;
2361
1542
  if (chosenDelta) {
2362
1543
  consumedDeltas.push(chosenDelta);
2363
- status = scanCrl(chosenDelta.crl); // (i) search the delta FIRST
2364
- if (status === null) status = scanCrl(rec.crl); // (j) the complete CRL only if still UNREVOKED
1544
+ status = scanCrl(chosenDelta.crl);
1545
+ if (status === null) status = scanCrl(rec.crl);
2365
1546
  } else {
2366
1547
  status = scanCrl(rec.crl);
2367
1548
  }
2368
- // (k): removeFromCRL means the certificate is no longer revoked. It is
2369
- // normalized wherever it appears -- sec. 5.3.1's "only in delta CRLs" binds
2370
- // the CA that emits it, not this consumer, and rejecting a complete CRL
2371
- // that carries one would make an unusual-but-harmless CRL unusable.
2372
1549
  if (status === 8) status = null;
2373
1550
 
2374
1551
  if (status !== null && certStatus === null) certStatus = status;
2375
- else if (status === null) reasonsMask |= gate.interim; // (l): only a clean scope covers
1552
+ else if (status === null) reasonsMask |= gate.interim;
2376
1553
  }
2377
1554
 
2378
- // An unmerged delta still speaks, under the shipped fail-closed posture: a
2379
- // serial it lists is a genuine revocation (serials are unique per issuer),
2380
- // and a removeFromCRL it carries blocks a definitive revoked without being
2381
- // able to establish good. This is what keeps the merge MONOTONIC -- it may
2382
- // turn undetermined into good or revoked, but a delta that merges with
2383
- // nothing can never erase a revocation the checker would otherwise report.
2384
1555
  for (var dj = 0; dj < classified.deltas.length; dj++) {
2385
1556
  var dRec = classified.deltas[dj];
2386
1557
  if (contains(consumedDeltas, dRec)) continue;
2387
- // A delta that was MERGEABLE with some complete CRL but lost the sec. 5.2.4 @3580 selection
2388
- // does not speak for its scope at all -- the selected delta does, and it was evaluated
2389
- // against the base. Letting a superseded delta contribute its revocation while ignoring its
2390
- // release would be incoherent: an older delta revoking a certificate that the newer one
2391
- // releases would resurrect the revocation the CA withdrew.
2392
1558
  if (dRec.accounted) continue;
2393
1559
  if (!(await gateCrl(dRec))) continue;
2394
1560
  sawUnmergedDelta = true;
@@ -2398,9 +1564,6 @@ function crlChecker(crls, opts) {
2398
1564
  if (certStatus === null) certStatus = dStatus;
2399
1565
  }
2400
1566
 
2401
- // A delta released this serial from hold but its base was not merged: a
2402
- // definitive revoked would leave a released certificate rejected, so the
2403
- // status is undetermined. This outranks a base CRL's revocation.
2404
1567
  if (releasedByUnmergedDelta) return { status: "unknown", reason: "a delta CRL released this serial from hold; without merging its base CRL the revocation status is undetermined" };
2405
1568
  if (certStatus !== null) {
2406
1569
  var reasonName = constants.NAMES.CRL_REASON[String(certStatus)] || "unspecified";
@@ -2419,37 +1582,21 @@ function crlChecker(crls, opts) {
2419
1582
  };
2420
1583
  }
2421
1584
 
2422
- // The decoded reasonCode of a revoked CRL entry (crl.parse surfaces it as a
2423
- // number), or null when absent.
2424
1585
  var OID_REASON_CODE = oid.byName("reasonCode");
2425
1586
  function crlEntryReason(entry) {
2426
- // Coverage residual -- the `|| []` fallback is unreachable: schema-crl guarantees
2427
- // crlEntryExtensions is an array (empty when absent).
2428
1587
  var exts = entry.crlEntryExtensions || [];
2429
1588
  for (var i = 0; i < exts.length; i++) {
2430
- if (exts[i].oid === OID_REASON_CODE) return exts[i].value; // stable OID, not the display name
1589
+ if (exts[i].oid === OID_REASON_CODE) return exts[i].value;
2431
1590
  }
2432
1591
  return null;
2433
1592
  }
2434
1593
 
2435
- // Verify a raw signature over tbsBytes with an SPKI public key -- the shared core
2436
- // of every certificate / CRL / OCSP signature check. Resolve the algorithm
2437
- // descriptor, enforce the key-OID <-> sig-OID binding (the algorithm-confusion
2438
- // guard, RFC 9814 sec. 4), import the SPKI, bridge an ECDSA DER signature to
2439
- // P1363, and verify. Any fault -- an unresolvable/forbidden algorithm, a
2440
- // key/sig mismatch, an import or verify failure -- resolves false: a signature
2441
- // check never throws out of this path, it fails closed. `rawSig` is the raw
2442
- // signature octets (the caller has already unwrapped any BIT STRING and rejected
2443
- // a non-octet-aligned one).
2444
1594
  function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
2445
1595
  var d;
2446
1596
  try {
2447
1597
  d = resolveDescriptor(sigAlg);
2448
1598
  assertKeyMatchesSigAlg(spkiBytes, sigAlg.oid, d);
2449
1599
  } catch (_e) { return Promise.resolve(false); }
2450
- // A composite-signed CRL / OCSP response verifies through the same combinator
2451
- // (both halves must pass) that the certificate path uses -- one composite verify,
2452
- // never a second parallel one.
2453
1600
  if (d.composite) return compositeSig.compositeVerify(spkiBytes, rawSig, tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature").then(function (r) { return r.ok === true; });
2454
1601
  return _importVerifyKey(spkiBytes, d).then(function (key) {
2455
1602
  var sig = rawSig;
@@ -2458,34 +1605,17 @@ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
2458
1605
  }).then(function (ok) { return ok === true; }, function () { return false; });
2459
1606
  }
2460
1607
 
2461
- // Inject this validator's signature engine into the shared internal crl-verify seam, so pki.crl.verify and
2462
- // pki.path.crlChecker both route through this ONE engine (no second, weaker CRL verifier) without exposing
2463
- // the seam on the public pki.path surface.
2464
1608
  crlVerify.setEngine(_verifyWithSpki);
2465
1609
 
2466
- // The same seam for PKCS#10 proof of possession, so pki.csr.verify checks an inbound certification
2467
- // request through this ONE engine rather than the producing side's self-check, which waives the
2468
- // EdDSA low-order-point gate because it runs over a key the caller already controls.
2469
1610
  csrVerify.setEngine(_verifyWithSpki);
2470
1611
 
2471
- // And for RFC 4211 proof of possession, so an inbound CertReqMsg is checked through the same one
2472
- // engine as the PKCS#10 form of the same question.
2473
1612
  crmfVerify.setEngine(_verifyWithSpki);
2474
1613
 
2475
- // And for an RFC 5755 attribute certificate, whose AC issuer is an out-of-path signer reached the
2476
- // same way.
2477
1614
  attrcertVerify.setEngine(_verifyWithSpki);
2478
1615
 
2479
- // And for a signed OCSPRequest (RFC 6960 sec. 4.1.1), so pki.ocsp.verifyRequest checks a requestor's
2480
- // signature through the same one engine that verifies a response, never a second, weaker request path.
2481
1616
  ocspRequestVerify.setEngine(_verifyWithSpki);
2482
1617
 
2483
- // ---- the OCSP revocation checker (RFC 6960) -------------------------------
2484
1618
 
2485
- // The OCSP response-verification core (responder authorization + CertID binding + currency +
2486
- // status) lives ONCE in lib/ocsp-verify.js, composed here by pki.path.ocspChecker AND by
2487
- // pki.ocsp.verify -- there is no second, weaker OCSP verify path. This binds the path-validate-
2488
- // owned signature engine + RFC 5280 cert-profile gates into that shared core.
2489
1619
  var ocspCore = ocspVerify.makeOcspVerify({
2490
1620
  verifyWithSpki: _verifyWithSpki,
2491
1621
  decodeExt: decodeExt, findExt: findExt,
@@ -2496,25 +1626,8 @@ var ocspCore = ocspVerify.makeOcspVerify({
2496
1626
  dnEqual: dnEqual,
2497
1627
  });
2498
1628
 
2499
- // Inject this validator's signature engine + full path build/validate into the internal cmp-verify seam,
2500
- // so pki.cmp.verify routes incoming CMP signature protection through the SAME engine (never a second, weaker
2501
- // CMP verifier; never build's self-check, which skips the EdDSA low-order-point gate) and chains an
2502
- // out-of-path signer certificate through the FULL RFC 5280 sec. 6.1 path validation -- without exposing any
2503
- // of it on the public pki.path surface (index.js exports the whole path-validate module).
2504
1629
  cmpVerify.setEngine({ verifyWithSpki: _verifyWithSpki, build: build, validate: validate });
2505
- // pki.cmp.session validates the ISSUED leaf certificate (its signature + chain) through the same engine.
2506
- // `verifyWithSpki` so a root CA key update's rollover signatures are checked by the same signature
2507
- // engine (with its EdDSA low-order-point and algorithm-confusion gates) that verifies a message's
2508
- // protection, rather than a second, weaker check inside the session.
2509
1630
  cmpSession.setEngine({ build: build, validate: validate, toAnchor: toAnchor, coerceCert: coerceCert, verifyWithSpki: _verifyWithSpki });
2510
- // pki.cms.verify chains a SignedData's signer certificate to the anchors the CALLER named, so its
2511
- // `trusted` is decided by this one path engine rather than a second, weaker walk of its own.
2512
- // `toAnchor` so cms.verify can validate the caller's anchors ONCE at entry, before any signer is
2513
- // walked -- otherwise a message whose signers all failed would never reach a build call and a
2514
- // malformed anchor would pass unnoticed.
2515
- // `resolvePurposeOpts` so cms.verify can reject a malformed requiredEku / checkPurpose at ITS entry
2516
- // point, through the SAME definition the walk uses -- a message whose signers all failed never
2517
- // reaches a build call, and a caller's configuration must not be judged by the message's quality.
2518
1631
  cmsVerify.setEngine({ build: build, validate: validate, toAnchor: toAnchor,
2519
1632
  resolvePurposeOpts: resolvePurposeOpts, assertAnchorConstraints: assertAnchorConstraints });
2520
1633
 
@@ -2549,17 +1662,11 @@ cmsVerify.setEngine({ build: build, validate: validate, toAnchor: toAnchor,
2549
1662
  * typeof checker.check; // "function"
2550
1663
  */
2551
1664
  function ocspChecker(responses) {
2552
- // The same door verifyOcspResponse uses. This checker's responses reach the identical three-part
2553
- // signature check, so accepting a parsed object here on a truthy responseStatus would leave the
2554
- // door shut on one of the two ways into that check and open on the other.
2555
1665
  var parsed = (responses || []).map(function (r) { return _ocspFromBytes(r); });
2556
1666
  return {
2557
1667
  check: async function (cert, issuer, ctx) {
2558
1668
  var time = ctx.time;
2559
1669
  var historical = ctx.historicalMode === true;
2560
- // Issuer DN candidates to match the CertID against (RFC 6960 sec. 4.1.1 names
2561
- // the checked cert's issuer field; a response MAY instead carry the issuer
2562
- // certificate's own subject encoding -- sec. 7.1-equal but not byte-identical).
2563
1670
  var issuerNameCandidates = [cert.issuer.bytes];
2564
1671
  function addNameCandidate(nm) {
2565
1672
  if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
@@ -2570,12 +1677,6 @@ function ocspChecker(responses) {
2570
1677
  try { issuerKeyBits = ocspVerify.ocspKeyValue(issuer.workingPublicKey); }
2571
1678
  catch (_e) { return { status: "unknown", reason: "the issuer public key could not be read to recompute the OCSP CertID" }; }
2572
1679
 
2573
- // A serial is revoked if ANY authoritative, verified, current response says
2574
- // so -- a clean response must never shadow a revoking one (the crlChecker
2575
- // fail-closed law). "good" needs at least one authoritative match; every
2576
- // other outcome is undetermined. The shared verify core evaluates each
2577
- // response (responder authorization + signature + CertID + currency +
2578
- // status) and returns a per-response summary this aggregates.
2579
1680
  var revokedResult = null;
2580
1681
  var sawGood = false;
2581
1682
  var sawUnknownStatus = false;
@@ -2662,26 +1763,12 @@ function verifyOcspResponse(response, cert, issuerCert, time, opts) {
2662
1763
  var parsedResponse, subject, issuer;
2663
1764
  try {
2664
1765
  parsedResponse = _ocspFromBytes(response);
2665
- // The two certificates go through the same door as the response, for the same reason: this
2666
- // verdict is about a certificate's IDENTITY -- its serial and its issuer's name and key are
2667
- // what the CertID is matched against, and the issuer's key is what authorizes a delegated
2668
- // responder. A caller-assembled certificate could name one identity while carrying another's
2669
- // signed bytes, so both are re-derived from the bytes their parser read.
2670
1766
  subject = coerceCert(cert, "the certificate");
2671
1767
  issuer = coerceCert(issuerCert, "the issuer certificate");
2672
1768
  } catch (e) { return Promise.reject(e); }
2673
1769
  return _verifyOcspParsed(parsedResponse, subject, issuer, time, opts);
2674
1770
  }
2675
1771
 
2676
- // The response, always parsed from the bytes the caller handed over.
2677
- //
2678
- // A signature check has three parts -- the signature, the algorithm that verifies it, and the byte
2679
- // range it covers -- and on a claimed-parsed response all three are separate properties of one
2680
- // caller-supplied object. Pair a real CA's signature over a certificate IT issued with that
2681
- // certificate's own tbsBytes and algorithm, label them `signature` / `tbsResponseDataBytes` /
2682
- // `signatureAlgorithm`, and the check verifies: a genuine signature, over the bytes it was made
2683
- // over, under the right key -- for a structure the responder never produced. Parsing binds the three
2684
- // to one byte string, which is the only thing that makes the verdict about a response at all.
2685
1772
  var _OCSP_CLAIM = ["responseStatus", "basicResponse", "tbsResponseDataBytes"];
2686
1773
  function _ocspFromBytes(response) {
2687
1774
  return guard.parsed.fromTrustedSource(response, "ocspResponse", _OCSP_CLAIM, function (bytes) {
@@ -2693,28 +1780,15 @@ function _ocspFromBytes(response) {
2693
1780
  "verifyOcspResponse: the response must be its DER bytes, a PEM string, or an unmodified pki.schema.ocsp.parseResponse result: the signature, the algorithm that verifies it and the bytes it covers are separate properties of a parsed object, so a REBUILT response (Object.assign, spread, a JSON round-trip) could have the three describe different responses and is refused");
2694
1781
  }
2695
1782
 
2696
- // The verification over a response this module has ALREADY derived from bytes -- reached only
2697
- // through the door above and through ocspChecker, which parses the responder's own reply. Not
2698
- // exported: this module's export object IS pki.path, so anything on it is public surface and frozen
2699
- // by the API snapshot, whatever a comment beside it claims.
2700
1783
  function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
2701
1784
  opts = opts || {};
2702
- // The currency + responder-cert validity windows compare against `time`; a missing or invalid
2703
- // check date must fail closed (a NaN compares false against every bound), never silently pass.
2704
1785
  if (!guard.time.isDate(time) || isNaN(guard.time.instantOf(time))) {
2705
1786
  return Promise.reject(E("path/bad-input", "verifyOcspResponse: time must be a valid Date (the currency + responder-validity check date)"));
2706
1787
  }
2707
- // Bind the supplied issuer certificate to the target: the target's issuer DN must equal the
2708
- // issuer cert's subject DN AND the target's signature must verify under the issuer's key. A
2709
- // direct-CA responder is authorized by exactly this issuer identity and the CertID is recomputed
2710
- // under the issuer's key, so without the cryptographic binding a rogue certificate sharing the
2711
- // issuer's subject DN (but a different key) could recompute a matching CertID and sign a "good"
2712
- // response for a certificate that CA never issued. ocspChecker gets an issuer already chained by
2713
- // the path validator; the standalone entry must establish the binding itself. Fail closed.
2714
1788
  function unbound(reason) { return { status: "unknown", responderAuthorized: false, signatureValid: false, matched: false, reason: reason }; }
2715
1789
  var boundName;
2716
1790
  try { boundName = dnEqual(cert.issuer.rdns, issuerCert.subject.rdns); }
2717
- catch (e) { return Promise.reject(e); } // an embedded control byte in a DN -> path/name-chaining
1791
+ catch (e) { return Promise.reject(e); }
2718
1792
  if (!boundName) {
2719
1793
  return Promise.resolve(unbound("the supplied issuer certificate's subject does not match the target certificate's issuer"));
2720
1794
  }
@@ -2735,91 +1809,32 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
2735
1809
  });
2736
1810
  }
2737
1811
 
2738
- // ---- certification path BUILDING (pki.path.build, RFC 4158) ----------------
2739
1812
 
2740
- // A soft extension decode for the RFC 4158 sec. 3.5 SORT hints (AKI/SKI/
2741
- // basicConstraints/keyUsage): a present-but-undecodable extension on an
2742
- // UNTRUSTED pool candidate degrades to "no hint", never a hard fail -- the sort
2743
- // weight is advisory, the branch is still tried, and validate is the authority.
2744
1813
  function softDecode(cert, extOid) {
2745
1814
  try { return decodeExt(cert, extOid); }
2746
1815
  catch (_e) { return null; }
2747
1816
  }
2748
1817
 
2749
- // dnEqual that fails a control-byte DN (CVE-2009-2408) closed to "not a match"
2750
- // rather than rejecting the whole build -- one malformed pool cert must not
2751
- // poison a buildable path (parity with selfIssued's swallow). The hard
2752
- // name-chaining gate remains validate's own dnEqual over the chosen path.
2753
1818
  function nameMatchSoft(rdnsA, rdnsB) {
2754
1819
  try { return dnEqual(rdnsA, rdnsB); }
2755
1820
  catch (_e) { return false; }
2756
1821
  }
2757
1822
 
2758
- // A certificate reaching a VERDICT is re-derived from the bytes the parser read, never trusted as
2759
- // the object it arrives as. Completeness -- every field present with the right type -- is not enough
2760
- // here, because a certificate's meaning is one signature over one byte range while a parsed
2761
- // certificate presents that range, the signature, and the fields the range encodes as separate
2762
- // properties. Keep a real CA certificate's `tbsBytes` and signature and substitute only its
2763
- // `subjectPublicKeyInfo`, and every completeness rule passes, this walk verifies the ORIGINAL signed
2764
- // range, and then uses the substituted key to check the next certificate -- a forged chain built out
2765
- // of a genuine certificate. Emptying `extensions` is the same move against basicConstraints,
2766
- // keyUsage, the name constraints and the unknown-critical rule.
2767
- //
2768
- // So the door takes the parser's record instead: a certificate from pki.schema.x509.parse re-parses
2769
- // from the bytes it was read from, and anything done to that object since is discarded. One a caller
2770
- // assembled has no record and is refused rather than silently believed.
2771
1823
  function coerceCert(input, label) {
2772
1824
  return guard.parsed.acceptDerived(input, "certificate", x509.parse, E, "path/bad-input",
2773
1825
  label || "a certificate");
2774
1826
  }
2775
1827
 
2776
- // A trust-store entry is either a ready anchor tuple { name, publicKey,
2777
- // algorithm, parameters? } or a self-signed root certificate reduced to that
2778
- // tuple. The algorithm is the SPKI KEY-algorithm OID (the sec. 6.1.4(f)
2779
- // parameter-inheritance value), mirroring trust.js _mkAnchor -- NOT the
2780
- // signature OID. The anchor is an input to validate, never one of the path certs.
2781
- // The two key-purpose options, resolved and typo-checked. Extracted so a CALLER can validate them
2782
- // at ITS entry point rather than only when a path is actually walked: a format verb that skips the
2783
- // walk -- pki.cms.verify does when no signer verified -- would otherwise accept a malformed
2784
- // purpose in silence, making configuration validity depend on the message. One definition, so the
2785
- // answer cannot drift between the caller's early check and the walk's own.
2786
- //
2787
- // `requiredEku` gates the TARGET certificate's own EKU extension; `checkPurpose` selects which
2788
- // per-purpose key the ANCHOR's NSS trust metadata (purposes / distrustAfter) is consulted under.
2789
- // They are independent, and each is a registered OID name or a canonical dotted OID.
2790
- // An anchor's CONSTRAINT metadata for one purpose, validated fail-closed and returned normalized.
2791
- // A PRESENT-but-malformed distrustAfter (an Invalid Date: instanceof Date yet a NaN time) would
2792
- // make `notBefore > it` NaN-false and SILENTLY drop the distrust restriction -- the NaN-Date
2793
- // fail-open. Absent metadata is no restriction and returns null.
2794
- //
2795
- // Separate from resolvePurposeOpts because it validates the ANCHOR rather than the options, and
2796
- // exposed for the same reason: a caller that may never reach the walk -- pki.cms.verify when no
2797
- // signer verified -- has to be able to reject a malformed anchor at ITS entry point, through this
2798
- // same definition, so configuration validity never depends on the message.
2799
- // Does this anchor carry trust metadata that only a named key purpose can unlock? A non-empty
2800
- // `distrustAfter` or `purposes` map is such metadata -- both are indexed BY purpose, so both are
2801
- // inert without one. An EMPTY map states no constraint and is not a reason to refuse.
2802
1828
  function _hasPurposeScopedMetadata(ta) {
2803
1829
  if (!ta || typeof ta !== "object") return false;
2804
1830
  return guard.list.anyMatches(["distrustAfter", "purposes"], function (k) {
2805
1831
  var m = ta[k];
2806
- // Own names, not Object.keys: a non-enumerable own entry is still a restriction and must count, so it cannot
2807
- // evade this "the anchor carries purpose-scoped metadata -> require checkPurpose" gate. An inherited map
2808
- // (Object.create(base) or a polluted Object.prototype) is already refused by toAnchor before an anchor
2809
- // reaches here, so this read does not resolve an inherited value.
2810
1832
  return !!m && typeof m === "object" && intrinsic.getOwnPropertyNames(m).length > 0;
2811
1833
  });
2812
1834
  }
2813
1835
 
2814
1836
  function assertAnchorConstraints(ta, checkPurpose) {
2815
1837
  if (!checkPurpose || !ta) return null;
2816
- // Read distrustAfter ONCE into a local: reading the anchor field for the presence gate and again for the
2817
- // value would let an accessor-backed field answer the two reads differently and slip the distrust control.
2818
- // The normalized anchor already materializes this to plain data; reading once holds the guarantee for any
2819
- // caller and keeps this from being a live-getter multi-read even if a future path skips normalization. An
2820
- // inherited distrustAfter (Object.create(base), or a polluted Object.prototype) is already refused by toAnchor
2821
- // -- _captureConstraintMap / the certificate branch reject a `"distrustAfter" in entry` that is not own -- so
2822
- // this read never resolves an inherited value on an anchor that reached here.
2823
1838
  var da = ta.distrustAfter;
2824
1839
  var d = (da && intrinsic.hasOwn(da, checkPurpose)) ? da[checkPurpose] : null;
2825
1840
  if (d == null) return null;
@@ -2834,9 +1849,6 @@ function resolvePurposeOpts(opts) {
2834
1849
  }
2835
1850
  requiredEku = opts.requiredEku.map(function (p) {
2836
1851
  if (typeof p !== "string" || p.length === 0) throw E("path/bad-input", "validate: opts.requiredEku entries must be non-empty strings");
2837
- // A dotted-form attempt (leads with a digit) must be a canonical OID -- a
2838
- // loose regex accepted a leading-zero / out-of-bounds key that would never
2839
- // match the canonical EKU the target advertises; anything else is a name.
2840
1852
  if (_startsWithDigit(p)) return guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p));
2841
1853
  var dotted = oid.byName(p);
2842
1854
  if (typeof dotted !== "string") throw E("path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p) + " is not a registered OID name");
@@ -2850,7 +1862,7 @@ function resolvePurposeOpts(opts) {
2850
1862
  }
2851
1863
  if (_startsWithDigit(opts.checkPurpose)) {
2852
1864
  var cpDotted = guard.identifier.assertCanonicalOid(opts.checkPurpose, E, "path/bad-input", "validate: opts.checkPurpose");
2853
- checkPurpose = oid.name(cpDotted) || cpDotted; // normalize a dotted purpose OID to its name for the anchor map
1865
+ checkPurpose = oid.name(cpDotted) || cpDotted;
2854
1866
  } else {
2855
1867
  if (typeof oid.byName(opts.checkPurpose) !== "string") throw E("path/bad-input", "validate: opts.checkPurpose " + JSON.stringify(opts.checkPurpose) + " is not a registered OID name");
2856
1868
  checkPurpose = opts.checkPurpose;
@@ -2859,35 +1871,11 @@ function resolvePurposeOpts(opts) {
2859
1871
  return { requiredEku: requiredEku, checkPurpose: checkPurpose };
2860
1872
  }
2861
1873
 
2862
- // Snapshot a trust-anchor purposes / distrustAfter map (purpose name -> boolean or Date) to a fresh plain
2863
- // object, reading each own entry from its DATA DESCRIPTOR -- never by property access. Reading by value would
2864
- // invoke an accessor entry (a `get serverAuth()`), which is caller code that could mutate the sibling map
2865
- // before it is captured, so an accessor entry is REFUSED instead. A later getter cannot mutate the fresh copy.
2866
- // A distrustAfter entry is a Date: copy it BY VALUE (guard.time.instantOf via the intrinsic) so a later
2867
- // setTime() cannot move the cutoff. Entry enumerability is preserved so the purpose gates see the same shape.
2868
1874
  function _snapshotConstraintMap(m, E, code, who) {
2869
1875
  var out = {};
2870
- // getOwnPropertyNames below reads only OWN keys, so a restriction reached through the prototype --
2871
- // Object.create({ serverAuth: pastDate }) -- would be silently dropped and its cutoff lost. Require the map to
2872
- // be a plain object of THIS realm: its prototype is Object.prototype (the common case) or null. Anything else
2873
- // -- a custom prototype, a null-prototype object carrying entries, a Proxy prototype, or a map from another
2874
- // realm -- is refused, so a prototype-reached restriction is never silently dropped. Only IDENTITY is sound
2875
- // here: a hostile object can mimic every STRUCTURAL signal of a genuine Object.prototype (a non-enumerable
2876
- // data entry is indistinguishable from a built-in), so a structural "looks plain" test always leaves a bypass.
2877
- // A cross-realm map is therefore refused fail-closed -- the operator passes a same-realm plain object (what
2878
- // pki.trust.parseCertdata emits, or a literal in their own code), which is the norm for this security-critical
2879
- // input. A name planted on Object.prototype ITSELF is still tolerated (proto === ObjectProto): the map is plain
2880
- // and consumption reads OWN entries only (the hasOwn gates), so global pollution is ignored, not read. m is
2881
- // non-Proxy (refused at capture) and this is a reference comparison, so no getPrototypeOf / ownKeys trap runs
2882
- // even when the prototype is a Proxy.
2883
1876
  var proto = intrinsic.getPrototypeOf(m);
2884
1877
  if (proto !== null && proto !== intrinsic.ObjectProto) throw E(code, who + ": a trustAnchor constraint map must be a plain object (its own entries only), not one with a custom or cross-realm prototype");
2885
1878
  var keys = intrinsic.getOwnPropertyNames(m);
2886
- // A constraint map is plain STRING-keyed data. A symbol-keyed entry (getOwnPropertyNames omits symbols) or an
2887
- // own __proto__ data property (skipped below so it can't pollute the fresh map) would be DROPPED from the
2888
- // snapshot -- and, keyed by neither a checkPurpose OID name, never applied. Refuse the map rather than silently
2889
- // drop a caller entry, so the snapshot faithfully carries every restriction or the anchor is refused. ownKeys
2890
- // (Reflect.ownKeys) counts strings + symbols; more than the string-only names means a symbol key is present.
2891
1879
  if (intrinsic.ownKeys(m).length !== keys.length) throw E(code, who + ": a trustAnchor constraint map must not have symbol-keyed entries");
2892
1880
  for (var i = 0; i < keys.length; i++) {
2893
1881
  if (keys[i] === "__proto__") throw E(code, who + ": a trustAnchor constraint map must not have an own __proto__ entry");
@@ -2895,29 +1883,17 @@ function _snapshotConstraintMap(m, E, code, who) {
2895
1883
  if (!d || !intrinsic.hasOwn(d, "value")) throw E(code, who + ": a trustAnchor constraint-map entry must be a data property, not an accessor");
2896
1884
  var v = d.value;
2897
1885
  if (intrinsic.types.isDate(v)) v = new intrinsic.Date(guard.time.instantOf(v));
2898
- // Materialize every entry ENUMERABLE. A constraint map is plain data (purpose -> boolean / Date); a caller
2899
- // entry defined non-enumerable would otherwise be copied non-enumerable and then be invisible to the
2900
- // Object.keys-based purpose-scoped-metadata gate, letting a restriction slip the "supply checkPurpose" check.
2901
1886
  intrinsic.defineProperty(out, keys[i], { value: v, enumerable: true, configurable: true, writable: true });
2902
1887
  }
2903
1888
  return out;
2904
1889
  }
2905
1890
 
2906
- // Normalize a captured constraint MAP field for storage. An absent map (null / undefined) carries no
2907
- // restriction and passes through. A present map MUST be a plain object and is snapshotted; a present non-object
2908
- // -- a primitive, a Buffer, or (refused inside the snapshot by the plain-prototype check) an array -- cannot hold
2909
- // a purpose -> boolean/Date restriction, so it is refused rather than passed through as an unusable value that
2910
- // would silently apply no restriction.
2911
1891
  function _normalizeConstraintField(v, E, code, who) {
2912
1892
  if (v === null || v === undefined) return v;
2913
1893
  if (typeof v !== "object" || intrinsic.isBuffer(v)) throw E(code, who + ": a trustAnchor constraint map must be a plain object");
2914
1894
  return _snapshotConstraintMap(v, E, code, who);
2915
1895
  }
2916
1896
 
2917
- // Capture a trust-anchor constraint MAP field (purposes / distrustAfter) WITHOUT invoking a getter: read its
2918
- // OWN DATA descriptor. An accessor-backed map (a `get purposes()`) is caller code that could mutate the sibling
2919
- // map when read, so it is refused; an INHERITED map is refused too (own-only capture), never silently dropped,
2920
- // so a restriction is never lost. A truly absent field is no restriction (undefined).
2921
1897
  function _captureConstraintMap(entry, key, E, code, who) {
2922
1898
  var d = intrinsic.getOwnPropertyDescriptor(entry, key);
2923
1899
  if (d === undefined) {
@@ -2925,29 +1901,15 @@ function _captureConstraintMap(entry, key, E, code, who) {
2925
1901
  return undefined;
2926
1902
  }
2927
1903
  if (!intrinsic.hasOwn(d, "value")) throw E(code, who + ": a trustAnchor " + key + " must be a data property, not an accessor");
2928
- // A constraint map that is a Proxy is refused, the same way the top-level anchor is: its ownKeys / descriptor
2929
- // traps could answer inconsistently -- an empty key list drops a restriction the caller attached, and an
2930
- // isBuffer-answering trap could skip the snapshot entirely -- so the map cannot be trusted to describe itself.
2931
1904
  if (intrinsic.types.isProxy(d.value)) throw E(code, who + ": a trustAnchor " + key + " map must not be a Proxy");
2932
1905
  return d.value;
2933
1906
  }
2934
1907
 
2935
- // Read a field's OWN DATA value from its descriptor, WITHOUT invoking a getter -- an accessor or an inherited
2936
- // field yields undefined. Used for the optional identity metadata (subjectDer / label / mozillaCaPolicy): a
2937
- // getter there, invoked before the key is pinned, could overwrite the key Buffer or mutate the name, so it is
2938
- // never invoked. This metadata does not enter a validation decision, so an accessor / inherited form is simply
2939
- // not carried (unlike a constraint map, which is refused).
2940
1908
  function _captureOwnData(entry, key) {
2941
1909
  var d = intrinsic.getOwnPropertyDescriptor(entry, key);
2942
1910
  return (d && intrinsic.hasOwn(d, "value")) ? d.value : undefined;
2943
1911
  }
2944
1912
 
2945
- // A fresh copy of a distinguished-name attribute record { type, value, name }, so the normalized anchor does
2946
- // not share it with the caller. The consumed fields -- type + value drive the RFC 5280 sec. 7.1 dnEqual
2947
- // comparison, name is the optional registry label -- are read by PROPERTY ACCESS, which follows the prototype
2948
- // chain and reads an accessor once. The anchor API deliberately accepts an inherited / accessor name.rdns
2949
- // (Object.create(base), a prototype getter), so an inherited attribute value must be SNAPSHOT here, not
2950
- // dropped; a field the caller did not supply is omitted.
2951
1913
  function _copyAtv(atv) {
2952
1914
  var out = {};
2953
1915
  var t = atv.type;
@@ -2959,33 +1921,18 @@ function _copyAtv(atv) {
2959
1921
  return out;
2960
1922
  }
2961
1923
 
2962
- // DEEP-copy a caller's rdns so the normalized anchor's DN is independent of the caller's object. A single
2963
- // arraySlice copies only the outer RDN sequence, leaving each nested RDN array and attribute record shared, so
2964
- // a caller mutating name.rdns[i][j].value after normalization (or while an async validate awaits signature
2965
- // verification) would change the issuer identity name chaining re-reads. Copy the outer array, each RDN array,
2966
- // and each attribute record (its type/value are strings, so an own-data copy is enough) -- matching the
2967
- // trust-store name copy (lib/trust.js _copyName).
2968
1924
  function _copyRdnsDeep(rdns) {
2969
1925
  if (!intrinsic.isArray(rdns)) return rdns;
2970
- // Copy only OWN indexed elements and PRESERVE holes -- never read an inherited Array.prototype[i] slot. A
2971
- // slice reads inherited indices (HasProperty walks the chain), so on a SPARSE rdns a polluted Array.prototype
2972
- // slot would be materialized as an own element, filling a hole that guard.name.dnEqual explicitly rejects and
2973
- // letting a malformed name pass chaining. Keeping the array sparse leaves the hole for the downstream guard.
2974
- // Read own indices only (hasOwn) and CREATE each copied index with defineProperty, never a plain assignment:
2975
- // both the read side (a slice walks the chain) and the write side (`out[i] = v` respects an inherited
2976
- // non-writable data property or accessor -- it can throw, fail silently, or invoke an inherited setter) touch
2977
- // the prototype, so a polluted Array.prototype could fill a hole or corrupt a dense rdns into a sparse one.
2978
- // defineProperty is CreateDataProperty: it always makes an own data property regardless of the prototype.
2979
1926
  var out = [];
2980
1927
  out.length = rdns.length;
2981
1928
  for (var i = 0; i < rdns.length; i++) {
2982
- if (!intrinsic.hasOwn(rdns, i)) continue; // leave a hole where the source has one
1929
+ if (!intrinsic.hasOwn(rdns, i)) continue;
2983
1930
  var rdn = rdns[i], val;
2984
1931
  if (intrinsic.isArray(rdn)) {
2985
1932
  var rdnOut = [];
2986
1933
  rdnOut.length = rdn.length;
2987
1934
  for (var j = 0; j < rdn.length; j++) {
2988
- if (!intrinsic.hasOwn(rdn, j)) continue; // preserve inner holes for the same reason
1935
+ if (!intrinsic.hasOwn(rdn, j)) continue;
2989
1936
  var atv = (rdn[j] !== null && typeof rdn[j] === "object") ? _copyAtv(rdn[j]) : rdn[j];
2990
1937
  intrinsic.defineProperty(rdnOut, j, { value: atv, enumerable: true, configurable: true, writable: true });
2991
1938
  }
@@ -2998,31 +1945,11 @@ function _copyRdnsDeep(rdns) {
2998
1945
 
2999
1946
  function toAnchor(entry, verb) {
3000
1947
  var who = verb || "build";
3001
- // Decide tuple-vs-certificate by the discriminator fields, read under try/catch: a throwing name /
3002
- // publicKey / algorithm accessor is a malformed anchor and must surface as the documented path/bad-input
3003
- // entry refusal, not a raw exception leaked past the typed guards. `alg` and the raw publicKey are PINNED
3004
- // to ONE read here and used throughout, so a stateful accessor that returns a value now and throws (or
3005
- // returns a different value) on a later read cannot slip past this guard at the shape check or the snapshot.
3006
1948
  var isTuple = false, alg = null, pubRaw, nameRaw = null, pk = null, purposesSnap, distrustSnap;
3007
1949
  var subjectDerRaw, labelRaw, mozRaw;
3008
- // A Proxy anchor is refused outright. Every field read below -- whether by property access or from an own
3009
- // data descriptor -- is answered by the anchor's own reflection, and a Proxy's traps let the same read
3010
- // return different values on different lookups or report a field absent while forwarding its siblings, so a
3011
- // Proxy could hide a purposes / distrustAfter restriction the operator attached and validate against a
3012
- // weaker anchor than the one supplied. A legitimate anchor is a plain tuple, a parsed certificate, or an
3013
- // Object.create(base) inheriting from one -- none is a Proxy -- so refusing it here costs no real use and
3014
- // removes the whole class of trap-driven anchor forgery. isProxy is an internal-slot check that runs no
3015
- // caller code, so it sits outside the accessor try below and its message is not re-wrapped as an accessor throw.
3016
1950
  if (intrinsic.types.isProxy(entry)) {
3017
1951
  throw E("path/bad-input", who + ": a trustAnchor must not be a Proxy");
3018
1952
  }
3019
- // A Proxy anywhere in the anchor's prototype CHAIN is refused too. entry may legitimately inherit its fields
3020
- // (an Object.create(base) anchor is supported), and those inherited reads -- the `in` tests and the property
3021
- // accesses below -- walk the chain, so a Proxy prototype would run its has / get traps: the has trap could
3022
- // report purposes / distrustAfter absent while the get trap still supplies inherited name / publicKey /
3023
- // algorithm, dropping the restriction and validating a weaker anchor. Walk the chain refusing any Proxy,
3024
- // checking isProxy BEFORE reading its prototype so a Proxy never runs a getPrototypeOf trap here. A legitimate
3025
- // anchor -- a tuple, a certificate, or an Object.create(base) over a plain object -- has a Proxy-free chain.
3026
1953
  if (entry !== null && typeof entry === "object") {
3027
1954
  var proto = intrinsic.getPrototypeOf(entry);
3028
1955
  while (proto !== null) {
@@ -3031,56 +1958,16 @@ function toAnchor(entry, verb) {
3031
1958
  }
3032
1959
  }
3033
1960
  try {
3034
- // Recognize a parsed CERTIFICATE first and route it to coerceCert below -- by PROVENANCE, not by reading
3035
- // its fields. guard.parsed.isRecorded is a getter-free WeakMap lookup: a certificate from
3036
- // pki.schema.x509.parse carries a provenance record, a hand-built tuple does not. A certificate does not
3037
- // carry name / publicKey / algorithm as own properties, but the tuple discriminator reads those by
3038
- // property access, which FOLLOWS the prototype chain -- so a polluted Object.prototype supplying those
3039
- // three would otherwise misclassify a real certificate as a tuple and bind the INHERITED (attacker) key
3040
- // instead of the certificate's own. Recognizing the certificate by its forge-proof provenance mark routes
3041
- // it to coerceCert and never binds an inherited key. Crucially the check is a WeakMap lookup that invokes
3042
- // NO caller getter -- so, unlike a structural probe that would read tbsBytes / subject / ... , it cannot run
3043
- // a tuple-controlled accessor (e.g. a polluted Object.prototype.tbsBytes getter) before publicKey is pinned
3044
- // below, which would otherwise defeat the key-pinning guarantee.
3045
1961
  if (entry && typeof entry === "object" && !Buffer.isBuffer(entry) && !guard.parsed.isRecorded(entry)) {
3046
- // Not a recorded certificate (isRecorded is a getter-free WeakMap lookup), so treat entry as a tuple
3047
- // candidate. entry is not a Proxy (refused at the door above), so its own reflection is truthful: a
3048
- // getOwnPropertyDescriptor read reports its real own data, and an absent field is really absent rather
3049
- // than a trap hiding a forwarded one. The discriminator fields (publicKey / name / algorithm) are read
3050
- // by PROPERTY ACCESS (follows the prototype chain, so an Object.create(base) inherited anchor is
3051
- // recognized), each exactly once, so a stateful own accessor answers each at most once. The prototype
3052
- // chain is never WALKED (Buffer.isBuffer / isRecorded above invoke getPrototypeOf a bounded number of
3053
- // times only), so a hostile getPrototypeOf cannot be driven into a loop.
3054
- //
3055
- // Snapshot the constraint maps FIRST -- before ANY discriminator field (publicKey / name / algorithm) is
3056
- // read -- so no discriminator getter can flip a denied purpose to allowed or move a distrust cutoff
3057
- // between the map's capture and validation. A getter for publicKey or name runs when that field is read;
3058
- // capturing purposes / distrustAfter beforehand puts them out of its reach (the same protection the
3059
- // ordering already gave against the later algorithm getter, now extended to the earlier ones). The
3060
- // identity passthrough (subjectDer, label, mozillaCaPolicy -- the closed producer-derived set) is read
3061
- // once each here too. entry is not a recorded certificate, so these reads do not touch a cert's accessors.
3062
1962
  var pRaw = _captureConstraintMap(entry, "purposes", E, "path/bad-input", who);
3063
1963
  purposesSnap = _normalizeConstraintField(pRaw, E, "path/bad-input", who);
3064
1964
  var dRaw = _captureConstraintMap(entry, "distrustAfter", E, "path/bad-input", who);
3065
1965
  distrustSnap = _normalizeConstraintField(dRaw, E, "path/bad-input", who);
3066
- // The identity passthrough (subjectDer, label, mozillaCaPolicy) is captured from its OWN DATA descriptor
3067
- // too, never by property access: an accessor identity field, read before the key is pinned below, could
3068
- // overwrite the key Buffer or mutate the name, so its getter is never invoked. An accessor / inherited
3069
- // identity field is not carried (it is non-validation metadata), not refused.
3070
1966
  subjectDerRaw = _captureOwnData(entry, "subjectDer");
3071
1967
  labelRaw = _captureOwnData(entry, "label");
3072
1968
  mozRaw = _captureOwnData(entry, "mozillaCaPolicy");
3073
- // Every mutable, validation-relevant field above (the constraint maps and the identity metadata) was
3074
- // captured from a descriptor WITHOUT invoking a getter, so the publicKey read below -- and the name /
3075
- // algorithm reads after it -- cannot have mutated them. publicKey is read (property access, so an
3076
- // inherited publicKey is supported) and pinned to a private Buffer IMMEDIATELY, before the name /
3077
- // algorithm accessors run, so a later getter cannot overwrite the key Buffer after it is read but before
3078
- // it is captured. A non-tuple leaves pk null / isTuple false and falls through to coerceCert below.
3079
1969
  pubRaw = entry.publicKey;
3080
1970
  if (guard.bytes.isByteSource(pubRaw)) {
3081
- // snapshotSource, not snapshot: isByteSource accepts all four BufferSource forms (Buffer, any view,
3082
- // DataView, ArrayBuffer), so the normalizer must too -- snapshot accepts only Buffer/Uint8Array and
3083
- // would accept-then-throw on an ArrayBuffer / DataView anchor publicKey.
3084
1971
  pk = guard.bytes.snapshotSource(pubRaw, PathError, "path/bad-input", who + ": trustAnchor publicKey");
3085
1972
  }
3086
1973
  nameRaw = entry.name;
@@ -3091,38 +1978,6 @@ function toAnchor(entry, verb) {
3091
1978
  throw E("path/bad-input", who + ": a trustAnchor accessor threw", e);
3092
1979
  }
3093
1980
  if (isTuple) {
3094
- // publicKey was read FIRST and pinned to `pk` in the discriminator above, before the algorithm and name
3095
- // accessors ran, so no caller getter can have mutated the key Buffer between its read and its capture.
3096
- // (A non-Buffer publicKey leaves pk null and is refused by the Buffer.isBuffer shape check below.)
3097
- // A ready anchor tuple: validate the shape build + validate consume -- name.rdns
3098
- // (name matching), publicKey bytes and the algorithm OID (the sec. 6.1.4 key
3099
- // hand-off). trustAnchors is a caller option, so a malformed tuple is a config
3100
- // error and fails closed at entry, not a downstream no-path / soft verdict.
3101
- // `algorithm` is the anchor key's own algorithm: an OID string, or the SubjectPublicKeyInfo
3102
- // `algorithm` object a parsed certificate carries ({ oid, parameters }), which validate has always
3103
- // accepted. Built-in verification derives the key algorithm from the (valid) publicKey SPKI and never
3104
- // consults this value, so any algorithm that slips through -- an object with no `oid`, or an oid that
3105
- // is not a real OID -- would validate the path on the SPKI alone and leave the very fail-open this
3106
- // check exists to close. A number, an object without an oid, or another non-shape is a malformed tuple
3107
- // and fails closed; a MISSING algorithm skips this branch and is caught below as not-a-certificate.
3108
- // Materialize the anchor name to a fresh plain object carrying only its CONSUMED sub-fields: rdns (used
3109
- // for DN matching -- read through the chain, so an inherited / Object.create(base) rdns is kept, then
3110
- // DEEP-copied (the outer array, each RDN array, and each attribute record) so a later mutation of the
3111
- // caller's nested RDN cannot change the issuer DN name chaining re-reads) and bytes (the DN DER a
3112
- // cert-derived name carries, kept for a lossless round-trip; not consumed in a name-matching decision, so
3113
- // it is held by reference). Each is read ONCE as DATA; the name object's own shape is never enumerated. path
3114
- // validation re-reads
3115
- // workingIssuerName.rdns during name chaining, so a stateful rdns accessor that answered the shape check
3116
- // with one array must not hand chaining a different issuer DN; the single read stores that one value. A
3117
- // throwing name / name.rdns accessor is a malformed anchor -> path/bad-input, not a raw throw.
3118
- // Capture the two NESTED tuple fields -- algorithm.oid and name.rdns / name.bytes -- GETTER-FREE, from their
3119
- // own data descriptors, never by property access. An ACCESSOR nested field runs caller code that could mutate
3120
- // the sibling before it is captured: a name.rdns getter rewriting a wrong algorithm.oid to the SPKI's real one
3121
- // past the mismatch refusal, or an algorithm.oid getter rewriting the anchor DN. Because BOTH directions
3122
- // cross-mutate, no read ORDER is safe -- so an accessor nested field is refused and no getter ever runs. A
3123
- // Proxy-hosted algorithm / name object is refused before its traps can answer. A legitimate anchor -- a tuple
3124
- // with a plain { oid } algorithm and a plain { rdns, bytes } name, or a parsed certificate's own -- carries
3125
- // plain data and is unaffected.
3126
1981
  var algStr;
3127
1982
  try {
3128
1983
  if (typeof alg === "string") {
@@ -3153,12 +2008,6 @@ function toAnchor(entry, verb) {
3153
2008
  if (nBytes !== undefined) {
3154
2009
  intrinsic.defineProperty(nameOut, "bytes", { value: nBytes, enumerable: true, configurable: true, writable: true });
3155
2010
  }
3156
- // Preserve the parsed Name's dn (the rendered "CN=..., O=..." string a parser emits, used to display /
3157
- // identify the selected root), so a pki.trust anchor or a cert-derived name round-trips its full shape --
3158
- // { rdns, dn, bytes } -- through normalization. Only a STRING dn is carried: identity never flows through
3159
- // this display string (name chaining compares rdns / bytes DER under RFC 5280 sec. 7.1), so an object- or
3160
- // otherwise non-string-valued dn -- which could run coercion code in a display consumer -- is dropped, not
3161
- // carried. Captured getter-free like the others (an accessor dn could otherwise mutate a sibling).
3162
2011
  var dnD = nsrc ? intrinsic.getOwnPropertyDescriptor(nsrc, "dn") : undefined;
3163
2012
  if (dnD && !intrinsic.hasOwn(dnD, "value")) throw E("path/bad-input", who + ": a trustAnchor name.dn must be a data property, not an accessor");
3164
2013
  var nDn = dnD ? dnD.value : undefined;
@@ -3166,58 +2015,17 @@ function toAnchor(entry, verb) {
3166
2015
  intrinsic.defineProperty(nameOut, "dn", { value: nDn, enumerable: true, configurable: true, writable: true });
3167
2016
  }
3168
2017
  } catch (e) { throw E("path/bad-input", who + ": trustAnchor name could not be materialized", e); }
3169
- // hasOwn-gate the read: nameOut is materialized here, so its rdns is either an OWN data property (set above
3170
- // from the caller's own rdns) or absent. Testing the OWN property -- not a plain nameOut.rdns, which would
3171
- // resolve a polluted Object.prototype.rdns on a name that supplied none (and could invoke an inherited
3172
- // throwing getter) -- means a name with no own rdns is refused, never accepted on an inherited array.
3173
2018
  var nameHasRdns = intrinsic.hasOwn(nameOut, "rdns") && intrinsic.isArray(nameOut.rdns);
3174
- // Gate on the NORMALIZED key `pk` (a private Buffer from snapshotSource iff publicKey was any of the four
3175
- // BufferSource forms), not the raw `pubRaw` -- Buffer.isBuffer(pubRaw) would reject a Uint8Array / view /
3176
- // ArrayBuffer publicKey that isByteSource + snapshotSource just accepted.
3177
2019
  if (!nameHasRdns || !Buffer.isBuffer(pk) || algStr === null) {
3178
2020
  throw E("path/bad-input", who + ": a trustAnchor tuple must be { name: { rdns: [...] }, publicKey: Buffer, algorithm: an OID string or a SubjectPublicKeyInfo algorithm carrying an oid }");
3179
2021
  }
3180
- // Hold the stated algorithm to a canonical dotted-decimal OID: a shape-valid but non-canonical value
3181
- // ("not-an-oid", { oid: "garbage" }) would otherwise pass and never match the SPKI-derived algorithm.
3182
2022
  guard.identifier.assertCanonicalOid(algStr, E, "path/bad-input", who + ": trustAnchor algorithm " + algStr);
3183
- // The declared algorithm is redundant with the self-describing publicKey SPKI, and built-in verification
3184
- // reads the SPKI's own key algorithm, never this field. A canonical but WRONG OID (declaring one key
3185
- // algorithm for a key that is actually another) would otherwise be accepted and the path validate on the
3186
- // SPKI alone. Reject a declared OID that does not equal the SubjectPublicKeyInfo's key-algorithm OID.
3187
- // (publicKey was pinned to `pk` at the top of this branch, before any caller getter could mutate it.)
3188
- //
3189
- // Walk the whole SubjectPublicKeyInfo through the shared PKIX schema, not just its first OID: a structure
3190
- // that carries a decodable AlgorithmIdentifier but no key BIT STRING would otherwise pass this gate and
3191
- // slip to a soft valid:false at key-import time, bypassing the promised path/bad-input entry refusal.
3192
2023
  var spkiAlg;
3193
2024
  try { spkiAlg = schema.walk(ANCHOR_SPKI_SCHEMA, asn1.decode(pk), NS).result.algorithm; }
3194
2025
  catch (e) { throw E("path/bad-input", who + ": trustAnchor publicKey is not a valid SubjectPublicKeyInfo", e); }
3195
2026
  if (spkiAlg.oid !== algStr) {
3196
2027
  throw E("path/bad-input", who + ": trustAnchor algorithm " + algStr + " does not match its publicKey key algorithm " + spkiAlg.oid);
3197
2028
  }
3198
- // Return a SELF-CONTAINED anchor: a fresh plain object carrying ONLY the closed, producer-derived anchor
3199
- // field set -- name, publicKey, algorithm, parameters, purposes, distrustAfter, subjectDer, label,
3200
- // mozillaCaPolicy -- the shape pki.trust.parseCertdata documents and path validation + trust dedup
3201
- // consume. Every field was read exactly once above (or is derived from the validated SPKI / pinned key),
3202
- // so a caller getter or Proxy trap runs at most once per field and no second read can disagree with the
3203
- // first; the anchor's own shape is NEVER enumerated (no ownKeys / getOwnPropertyDescriptor / `in`) and the
3204
- // prototype chain is never walked, so a Proxy cannot use an enumeration trap to mutate state and a hostile
3205
- // getPrototypeOf cannot be driven into a loop (Buffer.isBuffer invokes it at most once). A field the caller
3206
- // attached OUTSIDE this set is not carried -- an anchor has a DEFINED shape, not an arbitrary passthrough --
3207
- // and `__proto__` is never assigned, so no caller field reaches Object.prototype's setter.
3208
- // - name, purposes, distrustAfter: materialized above from ONE read each as DATA (an inherited
3209
- // restriction is kept, since those reads followed the prototype chain). Storing DATA is load-bearing --
3210
- // assertAnchorConstraints reads distrustAfter several times (a truthy gate, a hasOwn gate, then the
3211
- // value) and _hasPurposeScopedMetadata / the purpose block read purposes at more than one site, so a
3212
- // live accessor could answer those reads inconsistently and silently drop a distrust or purpose control.
3213
- // - subjectDer, label, mozillaCaPolicy: the trust-store identity fields, read once each in the
3214
- // discriminator, so a store's own entry round-trips onto build's result.trustAnchor.
3215
- // - publicKey is the pinned copy; algorithm and parameters come from the validated SPKI, never a
3216
- // caller-declared field. Seeding the working key from a string OID keeps the sec. 6.1.4 key-algorithm
3217
- // comparison working (an object never equals a cert's keyAlg.oid); taking parameters from the key's own
3218
- // AlgorithmIdentifier stops a declared value that disagrees with it (e.g. P-384 params on a P-256
3219
- // anchor) from being promoted into workingPublicKeyParameters and inherited by a child that omits its
3220
- // own, which would validate a chain RFC 5280 inheritance from the real params rejects.
3221
2029
  var flat = {};
3222
2030
  intrinsic.defineProperty(flat, "name", { value: nameOut, enumerable: true, configurable: true, writable: true });
3223
2031
  if (purposesSnap !== undefined) intrinsic.defineProperty(flat, "purposes", { value: purposesSnap, enumerable: true, configurable: true, writable: true });
@@ -3230,14 +2038,6 @@ function toAnchor(entry, verb) {
3230
2038
  intrinsic.defineProperty(flat, "parameters", { value: spkiAlg.parameters, enumerable: true, configurable: true, writable: true });
3231
2039
  return flat;
3232
2040
  }
3233
- // A certificate anchor carries no per-purpose constraints: the certificate branch below returns { name,
3234
- // publicKey, algorithm, ... } only. Per-purpose purposes / distrustAfter come from a trust-anchor TUPLE (what
3235
- // pki.trust emits), never from properties attached to a parsed certificate -- attaching them there would
3236
- // silently drop the restriction and validate a path the caller meant to forbid. Refuse rather than drop. The
3237
- // `in` check catches an OWN or an INHERITED purposes / distrustAfter (a name reached through the prototype, e.g.
3238
- // a polluted Object.prototype), matching how the tuple branch refuses an inherited constraint field; it invokes
3239
- // no getter (an existence test), and entry is not a Proxy (refused at the door), so no trap runs. A normal
3240
- // certificate has neither on its chain, so this only catches the footgun of hanging a constraint on a cert.
3241
2041
  if (entry && typeof entry === "object" && (("purposes" in entry) || ("distrustAfter" in entry))) {
3242
2042
  throw E("path/bad-input", who + ": a trustAnchor certificate must not carry purposes / distrustAfter -- put per-purpose constraints on a { name, publicKey, algorithm, purposes, distrustAfter } tuple (or a pki.trust anchor), not on a parsed certificate");
3243
2043
  }
@@ -3270,20 +2070,12 @@ function toAnchor(entry, verb) {
3270
2070
  */
3271
2071
  function anchorFromCert(cert) { return toAnchor(cert, "anchorFromCert"); }
3272
2072
 
3273
- // RFC 4158 sec. 2.4.2 loop key: the (subject DN + subjectAltName + subject
3274
- // public key) tuple, NOT the DN alone. DN-alone keying wrongly prunes a
3275
- // legitimate distinct cross-cert (cross-certs share a DN) and misses a same-key
3276
- // loop; the key differentiates a rollover (same DN, new key) from a true repeat.
3277
2073
  function identityKey(cert) {
3278
2074
  var san = findExt(cert, OID.subjectAltName);
3279
2075
  return cert.subject.bytes.toString("hex") + "|" + (san ? san.value.toString("hex") : "") + "|" +
3280
2076
  cert.subjectPublicKeyInfo.bytes.toString("hex");
3281
2077
  }
3282
2078
 
3283
- // A byte-EXACT certificate identity (the signed tbs region + the signature), for deduping certificates fetched
3284
- // over AIA before they enter the shared pool. Unlike identityKey (a subject/SAN/key tuple, deliberately broad for
3285
- // loop pruning), this collapses ONLY true byte-duplicates -- so a mirror URL or a repeating CMS returning the same
3286
- // issuer is added once, but a functionally-different cert sharing a subject+key (e.g. a key rollover) is kept.
3287
2079
  function certDerKey(cert) {
3288
2080
  return cert.tbsBytes.toString("base64") + "|" + (cert.signatureValue && cert.signatureValue.bytes ? cert.signatureValue.bytes.toString("base64") : "");
3289
2081
  }
@@ -3293,47 +2085,34 @@ function childAkiKeyId(cert) {
3293
2085
  return (d && d.value && d.value.keyIdentifier) ? d.value.keyIdentifier : null;
3294
2086
  }
3295
2087
 
3296
- // RFC 4158 sec. 3.5 candidate prioritization -- a SORT weight, never a gate.
3297
- // Higher = tried first (fewer validate calls to reach a valid path). Every term
3298
- // is a hint: a KID match, an anchor-adjacent issuer, CA + keyCertSign presence,
3299
- // and validity at the check time all raise priority, but a lower-scoring
3300
- // candidate is STILL attempted, and validate makes every accept decision.
3301
2088
  function scoreCandidate(cand, childAki, anchors, time) {
3302
2089
  var score = 0;
3303
2090
  if (childAki) {
3304
2091
  var d = softDecode(cand, OID.subjectKeyIdentifier);
3305
- if (d && Buffer.isBuffer(d.value) && d.value.equals(childAki)) score += 1000; // sec. 3.5.12 KID match: heaviest
2092
+ if (d && Buffer.isBuffer(d.value) && d.value.equals(childAki)) score += 1000;
3306
2093
  }
3307
2094
  for (var i = 0; i < anchors.length; i++) {
3308
- if (nameMatchSoft(cand.issuer.rdns, anchors[i].name.rdns)) { score += 100; break; } // sec. 3.5.15/.16 anchor-adjacent
2095
+ if (nameMatchSoft(cand.issuer.rdns, anchors[i].name.rdns)) { score += 100; break; }
3309
2096
  }
3310
2097
  var bc = softDecode(cand, OID.basicConstraints);
3311
- if (bc && bc.value && bc.value.cA === true) score += 10; // sec. 3.5.1 basicConstraints cA
2098
+ if (bc && bc.value && bc.value.cA === true) score += 10;
3312
2099
  var ku = softDecode(cand, OID.keyUsage);
3313
- if (ku && ku.value && ku.value.keyCertSign === true) score += 10; // sec. 3.5.3 keyUsage keyCertSign
2100
+ if (ku && ku.value && ku.value.keyCertSign === true) score += 10;
3314
2101
  var v = cand.validity;
3315
2102
  // allow:nan-date-comparison-unguarded -- cand.validity dates are codec-parsed (asn1 readTime rejects a NaN instant) and time is guard.time.assertValid'd at entry; the validity term is a fail-safe sort hint regardless.
3316
2103
  if (v && guard.time.isDate(v.notBefore) && guard.time.isDate(v.notAfter) &&
3317
2104
  guard.time.instantOf(v.notBefore) <= guard.time.instantOf(time) &&
3318
- guard.time.instantOf(time) <= guard.time.instantOf(v.notAfter)) score += 5; // sec. 3.5.4 validity
2105
+ guard.time.instantOf(time) <= guard.time.instantOf(v.notAfter)) score += 5;
3319
2106
  return score;
3320
2107
  }
3321
2108
 
3322
- // Sort scored issuer candidates by descending priority and push each non-looping one onto the DFS stack as a
3323
- // child frame (the chain grows leaf-ward by PREPENDING the issuer). Shared by the static-pool expansion and the
3324
- // AIA-fallback expansion so both enforce the SAME total-work counter (sec. 3.5 breadth cap), loop pruning
3325
- // (the identity visited-set), and priority order. `scored` is [{cand, score}]; returns the number of candidates
3326
- // ticked so the caller can advance its `considered` tally.
3327
2109
  function _pushCandidates(frame, scored, stack, counter) {
3328
- scored.sort(function (a, b) { return a.score - b.score; }); // ascending -> push lowest first so the highest is popped first
2110
+ scored.sort(function (a, b) { return a.score - b.score; });
3329
2111
  var n = 0;
3330
2112
  for (var ci = 0; ci < scored.length; ci++) {
3331
- counter.tick(); // breadth / total-work cap -> throws path/build-limit
2113
+ counter.tick();
3332
2114
  n += 1;
3333
2115
  var cand = scored[ci].cand, candKey = identityKey(cand);
3334
- // Loop detection and the dedupe sets below are resource bounds, so they are asked through the
3335
- // captured methods: a replaced `Set.prototype.has` answering false never prunes and the build
3336
- // walks a cycle until a cap stops it.
3337
2116
  if (intrinsic.setHas(frame.keys, candKey)) continue;
3338
2117
  var childKeys = new Set(frame.keys);
3339
2118
  intrinsic.setAdd(childKeys, candKey);
@@ -3342,112 +2121,69 @@ function _pushCandidates(frame, scored, stack, counter) {
3342
2121
  return n;
3343
2122
  }
3344
2123
 
3345
- // ---- AIA caIssuers network fetching (RFC 5280 sec. 4.2.2.1, opt-in over pki.transport) --------------------
3346
- // Discover a MISSING intermediate by GETting the caIssuers accessLocation of the certificate being chained
3347
- // past, feeding the fetched cert(s) into the SAME candidate search. SSRF / amplification bounded: https-only,
3348
- // NO private / loopback / link-local destination -- an IP LITERAL is refused by the pre-check, and a DNS NAME
3349
- // that RESOLVES to such an address is refused (and the address pinned) by the transport's blockPrivateAddresses
3350
- // filter set on every AIA request, so an untrusted cert cannot drive an authenticated GET to an internal service
3351
- // / cloud metadata by literal OR by hostname; a
3352
- // total fetch budget (a SILENT cap -- stop fetching, never a throw that aborts a buildable path), a per-cert URL
3353
- // cap over DISTINCT normalized URLs, a build-wide URL dedupe (fragment-free), a response size + certificate-count
3354
- // cap, no redirect following. Every fetch fault is a SKIP (the DFS continues over the pool). A fetched cert is UNTRUSTED pool
3355
- // material -- when validate is on (the default) it flows through validate() like any candidate and is NEVER a
3356
- // trust anchor; in pure-builder mode (opts.validate:false) it is returned unvalidated, exactly like a static candidate.
3357
-
3358
- // SSRF guard (literal pre-check): is a URL host a private / loopback / link-local / reserved IP LITERAL? An AIA
3359
- // URL comes from an UNTRUSTED certificate, so a literal address into RFC 1918 / loopback / the 169.254 cloud-
3360
- // metadata range must not be fetched (with enterprise TLS trust it would be an authenticated GET to an internal
3361
- // service). This is a fast pre-filter that avoids even opening a socket for an obvious literal; a DNS NAME is
3362
- // judged at RESOLUTION time by the transport's blockPrivateAddresses filter (set on the AIA request below), which
3363
- // also pins the checked address. The IP classification is shared with the transport (one range set, no drift).
2124
+
3364
2125
  function _isBlockedAiaHost(host) {
3365
- if (host.charAt(0) === "[" && host.charAt(host.length - 1) === "]") host = host.slice(1, -1); // an IPv6 literal: URL.hostname keeps the [brackets]
3366
- if (net.isIP(host) === 0) return false; // a DNS name -> not judged here; the transport's resolution-time filter blocks a private resolution
3367
- return httpTransport.isBlockedIp(host); // an IP literal -> the shared private/loopback/link-local classifier
2126
+ if (host.charAt(0) === "[" && host.charAt(host.length - 1) === "]") host = host.slice(1, -1);
2127
+ if (net.isIP(host) === 0) return false;
2128
+ return httpTransport.isBlockedIp(host);
3368
2129
  }
3369
2130
 
3370
- // Parse an AIA response body as a single DER certificate (RFC 2585) OR a certs-only CMS bundle (RFC 5272).
3371
- // The media type is only an ordering HINT (RFC 5280 sec. 4.2.2.1: "should not depend solely on the ... media
3372
- // type") -- both structures are attempted, the wire decides. Returns raw certificate DER Buffers; throws
3373
- // (caught upstream as a skip) if the body is neither.
3374
2131
  function _aiaParseBody(body, contentType, maxCerts) {
3375
- var certsFirst = String(contentType || "").toLowerCase().indexOf("pkcs7") >= 0; // HINT: order the attempts only
2132
+ var certsFirst = String(contentType || "").toLowerCase().indexOf("pkcs7") >= 0;
3376
2133
  var order = certsFirst ? ["certs", "cert"] : ["cert", "certs"];
3377
2134
  for (var i = 0; i < order.length; i++) {
3378
2135
  try {
3379
2136
  if (order[i] === "cert") { x509.parse(body); return [Buffer.from(body)]; }
3380
- return cms.parseCertsOnly(body, E, "path", maxCerts).certificates; // maxCerts bounds the parse of an untrusted bundle
3381
- } catch (_e) { /* structure-sniff: try the other form */ }
2137
+ return cms.parseCertsOnly(body, E, "path", maxCerts).certificates;
2138
+ } catch (_e) { }
3382
2139
  }
3383
2140
  throw E("path/aia-bad-body", "an AIA response body is neither a DER certificate nor a certs-only CMS");
3384
2141
  }
3385
2142
 
3386
- // Fetch ONE caIssuers URL over the injected/default transport; returns the parsed candidate certs, or throws
3387
- // (the caller collapses any throw to a skip). Only a 200 with a non-empty, in-cap body is a cert source (M12).
3388
2143
  async function _aiaFetchOne(uri, aia) {
3389
- // blockPrivateAddresses: the real transport refuses -- and pins -- a hostname that RESOLVES to a private /
3390
- // loopback / link-local address (the literal pre-check only catches an IP literal). An injected test transport
3391
- // ignores the flag; the DFS treats a blocked-address transport error as a silent skip like any fetch fault.
3392
2144
  var res = await aia.transport({ method: "GET", url: uri, tls: aia.tls, timeout: aia.timeout, maxResponseBytes: aia.maxResponseBytes, blockPrivateAddresses: true });
3393
2145
  res = res || {};
3394
2146
  if (res.status !== 200) throw E("path/aia-status", "an AIA fetch returned HTTP " + res.status + " (only 200 is a cert source; no redirect following)");
3395
- // A binary view from an injected transport (Buffer / Uint8Array / ArrayBuffer / DataView) is re-viewed
3396
- // through the byte guard, as the request path does; only a genuine string body falls back to a latin1
3397
- // decode -- a typed array is never String()-coerced to "48,130,..." garbage that then fails to parse.
3398
2147
  var body = guard.bytes.isByteSource(res.body)
3399
2148
  ? guard.bytes.source(res.body, PathError, "path/aia-bad-body", "the AIA response body")
3400
2149
  : Buffer.from(res.body == null ? "" : String(res.body), "latin1");
3401
2150
  if (body.length === 0) throw E("path/aia-empty", "an AIA fetch returned an empty body");
3402
- // Belt for an INJECTED transport that ignores maxResponseBytes (the real transport streaming-aborts at the cap).
3403
2151
  if (body.length > aia.maxResponseBytes) throw E("path/aia-too-large", "an AIA response exceeds the " + aia.maxResponseBytes + "-byte cap");
3404
2152
  var headers = {};
3405
2153
  Object.keys(res.headers || {}).forEach(function (k) { headers[k.toLowerCase()] = res.headers[k]; });
3406
2154
  return _aiaParseBody(body, headers["content-type"], aia.maxCertsPerResponse);
3407
2155
  }
3408
2156
 
3409
- // Discover issuer candidates for `current` from its AIA caIssuers URLs. Returns parsed candidate certs (coerced
3410
- // like any pool cert). Bounded + fail-closed; ticks aia.fetchCounter (the total-budget throw) and mutates
3411
- // aia.fetchedUrls (build-wide dedupe). A cert with no AIA, a malformed AIA, or no fetchable https caIssuers
3412
- // URI simply yields no candidates (RFC 5280 sec. 4.2.2.1 AIA is advisory / non-critical).
3413
2157
  async function _fetchAiaIssuers(current, aia) {
3414
- var d = softDecode(current, OID.authorityInfoAccess); // no / malformed AIA -> no fetch
2158
+ var d = softDecode(current, OID.authorityInfoAccess);
3415
2159
  if (!d || !d.value) return [];
3416
- // Collect DISTINCT, normalized, fetchable https URLs, deduping BEFORE the per-cert cap so a flood of duplicate
3417
- // (or fragment-variant) entries can never crowd out a usable later URL.
3418
2160
  var uris = [];
3419
2161
  var seenThisCert = new Set();
3420
2162
  for (var i = 0; i < d.value.length; i++) {
3421
2163
  var ad = d.value[i];
3422
- if (ad.accessMethod !== OID.caIssuers) continue; // ONLY id-ad-caIssuers, never id-ad-ocsp
3423
- if (!ad.accessLocation || ad.accessLocation.tag !== 6) continue; // ONLY a uniformResourceIdentifier [6]
3424
- // Parse the URI ONCE: the NORMALIZED href is both the dedupe key AND the exact URL handed to the transport.
2164
+ if (ad.accessMethod !== OID.caIssuers) continue;
2165
+ if (!ad.accessLocation || ad.accessLocation.tag !== 6) continue;
3425
2166
  var u;
3426
2167
  try { u = new URL(ad.accessLocation.value); }
3427
- catch (_e) { continue; } // an unparseable URI -> skip (catch on its own line so the swallow gate can trace it)
3428
- if (u.protocol !== "https:") continue; // https-only: no socket for http/ldap/ftp/file/mailto
3429
- if (_isBlockedAiaHost(u.hostname)) continue; // SSRF: never fetch a private / loopback / link-local IP literal
3430
- // A DNS-NAME host's private RESOLUTION can only be blocked by a transport that filters the resolved address;
3431
- // with an UNGUARDED transport (an injected fn that does not vouch blocksPrivateAddresses) fail closed -- fetch
3432
- // IP literals only (already validated above), never a hostname whose resolved address we cannot verify.
2168
+ catch (_e) { continue; }
2169
+ if (u.protocol !== "https:") continue;
2170
+ if (_isBlockedAiaHost(u.hostname)) continue;
3433
2171
  if (net.isIP(_stripBrackets(u.hostname)) === 0 && !aia.transportGuardsAddresses) continue;
3434
- u.hash = ""; // the fragment is never sent on the wire -> not part of the request/dedupe identity
3435
- if (intrinsic.setHas(aia.fetchedUrls, u.href) || intrinsic.setHas(seenThisCert, u.href)) continue; // dedupe (build-wide OR same-cert) on the normalized URL
3436
- if (seenThisCert.size >= aia.maxPerCert) break; // per-cert DISTINCT-url cap, checked BEFORE appending so maxAiaPerCert:0 collects nothing (no fetch at all)
2172
+ u.hash = "";
2173
+ if (intrinsic.setHas(aia.fetchedUrls, u.href) || intrinsic.setHas(seenThisCert, u.href)) continue;
2174
+ if (seenThisCert.size >= aia.maxPerCert) break;
3437
2175
  intrinsic.setAdd(seenThisCert, u.href);
3438
2176
  uris.push(u.href);
3439
2177
  }
3440
2178
  var out = [];
3441
2179
  for (var k = 0; k < uris.length; k++) {
3442
- if (intrinsic.setHas(aia.fetchedUrls, uris[k])) continue; // a same-cert normalization-equal duplicate
3443
- if (aia.fetches >= aia.maxFetches) break; // total budget reached -> STOP fetching (a SILENT cap, never a throw that denies a buildable path)
3444
- intrinsic.setAdd(aia.fetchedUrls, uris[k]); // mark BEFORE the fetch -> fetched at most once, even on failure
2180
+ if (intrinsic.setHas(aia.fetchedUrls, uris[k])) continue;
2181
+ if (aia.fetches >= aia.maxFetches) break;
2182
+ intrinsic.setAdd(aia.fetchedUrls, uris[k]);
3445
2183
  aia.fetches += 1;
3446
2184
  var certs;
3447
2185
  try { certs = await _aiaFetchOne(uris[k], aia); }
3448
- catch (_e2) { continue; } // any fetch / parse fault is a skip; the DFS continues over the pool + other URLs
3449
- // Each response is already capped to maxCertsPerResponse by parseCertsOnly (per RESPONSE), so an earlier
3450
- // URL's bundle never consumes a later URL's allowance -- coerce every returned cert.
2186
+ catch (_e2) { continue; }
3451
2187
  for (var c = 0; c < certs.length; c++) {
3452
2188
  var parsed;
3453
2189
  try { parsed = coerceCert(certs[c]); }
@@ -3526,11 +2262,6 @@ async function _fetchAiaIssuers(current, aia) {
3526
2262
  * });
3527
2263
  * result.valid; // true when a path to a trust anchor was found and validated
3528
2264
  */
3529
- // pki.path.build takes its own options AND forwards every pki.path.validate option to each
3530
- // internal validate() call. The documented contract above says so, and validateOpts() copies
3531
- // them wholesale. So the accepted set is the union, derived from _VALIDATE_OPTS. Retyping it
3532
- // would let the two lists drift the first time validate gains an option. Both verbs now spell the
3533
- // anchor option `trustAnchors`, so the union carries one key and build applies its own per-candidate.
3534
2265
  var _BUILD_OPTS = (function () {
3535
2266
  var o = {
3536
2267
  aiaTimeout: 1, candidates: 1, fetchAia: 1, intermediates: 1, maxAiaFetches: 1,
@@ -3552,17 +2283,11 @@ async function build(leaf, opts) {
3552
2283
  var poolInput = opts.candidates !== undefined ? opts.candidates : opts.intermediates;
3553
2284
  if (poolInput === undefined) poolInput = [];
3554
2285
  if (!intrinsic.isArray(poolInput)) throw E("path/bad-input", "build: opts.candidates must be an array of certificates");
3555
- // A pool larger than the absolute ceiling is rejected at entry (bounds the
3556
- // parse work on an untrusted bundle) -- distinct from the per-search tick
3557
- // budget below, which an operator may set lower to cap the DFS.
3558
2286
  var poolCeiling = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES;
3559
2287
  if (poolInput.length > poolCeiling) throw E("path/bad-input", "build: the candidate pool has " + poolInput.length + " certificates, exceeding the " + poolCeiling + " ceiling");
3560
2288
  var pool;
3561
2289
  try { pool = poolInput.map(coerceCert); }
3562
2290
  catch (e) { throw E("path/bad-input", "build: a candidate certificate did not parse", e); }
3563
- // Byte-exact identities of every cert already in the shared pool, so an AIA-fetched duplicate (a mirror URL or a
3564
- // repeating CMS returning the same issuer) is appended AT MOST ONCE -- it otherwise inflates the pool, charging
3565
- // the candidate budget and the ceiling for each copy before the issuer is ever evaluated.
3566
2291
  var poolDerKeys = new Set();
3567
2292
  for (var pdk = 0; pdk < pool.length; pdk++) intrinsic.setAdd(poolDerKeys, certDerKey(pool[pdk]));
3568
2293
 
@@ -3570,172 +2295,82 @@ async function build(leaf, opts) {
3570
2295
  var anchors = opts.trustAnchors.map(function (a) { return toAnchor(a, "build"); });
3571
2296
 
3572
2297
  guard.time.assertValid(opts.time, E, "path/bad-input", "build: opts.time (the validity-window check date, forwarded to validate)");
3573
- // The chain-length bound is tied to the EFFECTIVE maxPathCerts the interleaved
3574
- // validate uses: a built path holds hop + 1 certificates (the leaf plus each
3575
- // intermediate), so capping hops at maxPathCerts - 1 keeps every path validate
3576
- // sees within its own per-path ceiling -- otherwise a maxDepth of maxPathCerts
3577
- // would assemble a maxPathCerts + 1 path that validate rejects with
3578
- // path/bad-input. The same cap value is computed the way validate computes it
3579
- // (opts.maxPathCerts forwarded), and the explicit-stack search below carries no
3580
- // native-recursion stack risk regardless of the depth.
3581
2298
  var effectiveMaxCerts = guard.limits.cap(opts.maxPathCerts, "build: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
3582
- // depthCeiling is 0 when maxPathCerts is 1: the only path within the limit is a
3583
- // single leaf directly under an anchor (a zero-hop search). maxDepth's minimum
3584
- // is therefore 0, not 1 -- a zero-hop search is legitimate and a maxPathCerts of
3585
- // 1 must not throw before the anchor is even checked.
3586
2299
  var depthCeiling = effectiveMaxCerts - 1;
2300
+ // allow:guard-shape-reinlined -- opts.maxDepth here is the path-building chain depth (capped via guard.limits.cap against the PATH_BUILD_MAX_DEPTH ceiling), a different concept from the decode-recursion depth guard.limits.depthCap enforces
3587
2301
  var maxDepth = guard.limits.cap(opts.maxDepth, "build: opts.maxDepth", Math.min(constants.LIMITS.PATH_BUILD_MAX_DEPTH, depthCeiling), { E: E, code: "path/bad-input", min: 0, max: depthCeiling });
3588
2302
  var maxConsidered = guard.limits.cap(opts.maxCandidatesConsidered, "build: opts.maxCandidatesConsidered", poolCeiling, { E: E, code: "path/bad-input", min: 1 });
3589
2303
  var doValidate = opts.validate !== false;
3590
2304
 
3591
- // AIA caIssuers fetching is OFF unless opts.fetchAia === true -- the default build is byte-identical to
3592
- // today's offline search (no transport constructed, no socket, no new path executed). When on, opts.transport
3593
- // is the injectable seam (tests drive it offline); with no injected transport the default https transport
3594
- // fails closed unless opts.tls carries an anchor / useSystemStore. opts.tls is the TLS trust for the AIA
3595
- // HTTPS host -- DISTINCT from opts.trustAnchors (the PKI trust store the built path validates against).
3596
2305
  var aiaCtx = null;
3597
2306
  if (opts.fetchAia === true) {
3598
- // Validate the AIA-specific options at CONFIG time (a caller typo is a path/bad-input throw, tier 1), not
3599
- // lazily inside the fetch where a bad transport / timeout would be caught as an ordinary fetch fault and
3600
- // silently degrade to path/no-path. A non-function transport can never be called; an injected transport gets
3601
- // a validated timeout (it may not cap the value itself).
3602
2307
  if (opts.transport !== undefined && typeof opts.transport !== "function") {
3603
2308
  throw E("path/bad-input", "build: opts.transport must be a transport function (request) -> Promise<{ status, headers, body }>");
3604
2309
  }
3605
- // Validate aiaTimeout against the SAME bounds the built-in transport enforces (integer, 1..MAX_TIMEOUT) at
3606
- // CONFIG time, so a non-integer / over-ceiling value is a path/bad-input throw here rather than a transport
3607
- // rejection swallowed later as a fetch fault. guard.limits.cap is the exact primitive the transport applies.
3608
2310
  if (opts.aiaTimeout !== undefined) {
3609
2311
  guard.limits.cap(opts.aiaTimeout, "build: opts.aiaTimeout", 1, { E: E, code: "path/bad-input", min: 1, max: httpTransport.MAX_TIMEOUT });
3610
2312
  }
3611
2313
  var aiaTransport = opts.transport || httpTransport.https({ E: E, errPrefix: "path" });
3612
2314
  aiaCtx = {
3613
2315
  transport: aiaTransport,
3614
- // SSRF for a DNS-name AIA host needs resolution-time address filtering + pinning, which only a transport
3615
- // that ADVERTISES the capability performs (the built-in, or an injected one that sets blocksPrivateAddresses
3616
- // to vouch it filters). An injected fn(request) that ignores the flag is treated as UNGUARDED: a DNS-name AIA
3617
- // URL is then fail-closed (skipped) and only an IP literal (validated by the literal pre-check) is fetched.
3618
2316
  transportGuardsAddresses: aiaTransport.blocksPrivateAddresses === true,
3619
2317
  tls: opts.tls || {},
3620
- timeout: opts.aiaTimeout, // validated above; the transport applies its own default + cap
3621
- // Default the AIA response cap BELOW the general HTTP ceiling (a caIssuers response is small); tighten
3622
- // downward only, never above HTTP_MAX_RESPONSE_BYTES.
2318
+ timeout: opts.aiaTimeout,
3623
2319
  maxResponseBytes: guard.limits.cap(opts.maxResponseBytes, "build: opts.maxResponseBytes", constants.LIMITS.PATH_AIA_MAX_RESPONSE_BYTES, { E: E, code: "path/bad-input", min: 1, max: constants.LIMITS.PATH_AIA_MAX_RESPONSE_BYTES }),
3624
2320
  maxPerCert: guard.limits.cap(opts.maxAiaPerCert, "build: opts.maxAiaPerCert", constants.LIMITS.PATH_AIA_MAX_PER_CERT, { E: E, code: "path/bad-input", min: 0 }),
3625
2321
  maxCertsPerResponse: constants.LIMITS.PATH_AIA_MAX_CERTS_PER_RESPONSE,
3626
- // The total fetch budget is a SILENT cap (stop initiating fetches when reached), NOT a throw: a bound on
3627
- // an ADVISORY fetch must never abort a build that the static pool could still complete. maxFetches may be
3628
- // 0 (fetchAia:true but no network -- identical to offline).
3629
2322
  maxFetches: guard.limits.cap(opts.maxAiaFetches, "build: opts.maxAiaFetches", constants.LIMITS.PATH_AIA_MAX_FETCHES, { E: E, code: "path/bad-input", min: 0 }),
3630
2323
  fetchedUrls: new Set(),
3631
2324
  fetches: 0,
3632
2325
  };
3633
2326
  }
3634
2327
 
3635
- // The build-specific options are consumed here; every remaining validate
3636
- // option is forwarded unchanged to the interleaved validate call. Object.keys
3637
- // enumerates only own enumerable properties, so no prototype-pollution belt.
3638
2328
  var BUILD_ONLY_OPT = { candidates: 1, intermediates: 1, trustAnchors: 1, maxDepth: 1, maxCandidatesConsidered: 1, validate: 1,
3639
2329
  fetchAia: 1, transport: 1, tls: 1, maxAiaFetches: 1, maxAiaPerCert: 1, aiaTimeout: 1, maxResponseBytes: 1 };
3640
- // No prototype, so the set forwarded to each internal validate holds what was put in it and
3641
- // nothing a runtime carries. A plain `{}` here re-acquired a polluted name after the caller's
3642
- // own value had been dropped, which is how an explicit `softFail: false` became a waiver.
3643
2330
  var forwarded = Object.create(null);
3644
- // Driven by the contract. Walk the validate options this verb forwards and take the ones the
3645
- // bag answers for.
3646
- //
3647
- // Enumerating the bag reads whatever a property walk chooses to report, and that walk skips a
3648
- // method so a caller's class keeps its own. A known option whose value were a bare function
3649
- // would be indistinguishable from one, and a defaults-style bag like
3650
- // `Object.create({ someOption: fn })` would have it dropped here, leaving each internal
3651
- // validate running without something the caller supplied.
3652
- //
3653
- // No option takes a bare function today. `revocationChecker` is `{ check }`, `verifier` is
3654
- // `{ verify }`, and `transport` is build-only and never forwarded. So this is a hardening
3655
- // rather than a live defect, and there is no vector that fails without it. It is here because
3656
- // asking the contract cannot drop a known option whatever its value, and the alternative makes
3657
- // the forwarded set depend on a walk whose rules exist for an unrelated question.
3658
- //
3659
- // `in` rather than an own check, because an option carried on a prototype is one the caller
3660
- // supplied and `opts.softFail` answers for it. Own membership decides only whether a name is
3661
- // build-only, which is a question about this module's own table and never about the caller.
3662
2331
  Object.keys(_VALIDATE_OPTS).forEach(function (k) {
3663
2332
  if (intrinsic.hasOwn(BUILD_ONLY_OPT, k)) return;
3664
2333
  if (k in opts) forwarded[k] = opts[k];
3665
2334
  });
3666
2335
  function validateOpts(anchor) {
3667
- var vo = Object.create(null); // same reason as `forwarded` above
3668
- // Every name in the forwarded set, which was built from the contract above and has no
3669
- // prototype, so its own keys are exactly what was put in it. A property walk here would
3670
- // apply the method rule a second time and drop a function-valued option all over again;
3671
- // forwarding less than was validated is worse than refusing the bag.
2336
+ var vo = Object.create(null);
3672
2337
  Object.keys(forwarded).forEach(function (f) { vo[f] = forwarded[f]; });
3673
- vo.trustAnchors = anchor; // one anchor per candidate; validate accepts a single anchor or an array
2338
+ vo.trustAnchors = anchor;
3674
2339
  return vo;
3675
2340
  }
3676
2341
 
3677
- // The DoS terminator: tick() once per candidate-issuer expansion so a hostile
3678
- // mesh cannot fan the search without bound (RFC 4158 8.1). A breach throws
3679
- // path/build-limit. candidatesConsidered mirrors the tick count for the caller.
3680
2342
  var counter = guard.limits.counter(maxConsidered, E, "path/build-limit", "build: candidate-issuer expansion");
3681
2343
  var considered = 0;
3682
2344
  var anyChainAssembled = false;
3683
2345
  var bestFail = null;
3684
2346
  var success = null;
3685
2347
 
3686
- // A bounded, EXPLICIT-stack forward DFS from the leaf toward an anchor -- no
3687
- // native recursion, so an operator-raised maxDepth cannot overflow the stack
3688
- // over an untrusted graph. Each frame is a candidate chain [top, ...toward the
3689
- // leaf] (leaf last) plus the identity keys already on it; a frame is expanded
3690
- // by prepending a name-chaining issuer. Highest-priority candidates are pushed
3691
- // LAST so they are popped (explored) FIRST -- depth-first in priority order.
3692
- // Caps are enforced before every expansion; a candidate whose (subject, SAN,
3693
- // public key) tuple is already on the chain is a loop and is pruned.
3694
2348
  var stack = [{ chain: [leafCert], hop: 0, keys: new Set([identityKey(leafCert)]) }];
3695
- // AIA fetch frames are DEFERRED into this queue and drained ONLY when `stack` is empty -- i.e. once the ENTIRE
3696
- // local (static-pool) search has failed. This enforces RFC 4158 sec. 7.2 "local before remote" GLOBALLY across
3697
- // the whole search, not per-branch: a build the static pool can complete never issues a network request, and a
3698
- // higher-priority local dead-end can never fetch ahead of a still-unexplored lower-priority static sibling.
3699
- // Drained DEEPEST-first, and among equal depth EARLIEST-deferred (= highest DFS priority) first: the actual
3700
- // dead end (a missing deeper hop) is fetched before an ancestor whose issuer the pool ALREADY supplies (so a
3701
- // scarce budget is never spent re-retrieving a locally-resolved hop), and a higher-scoring sibling's AIA is
3702
- // tried before a lower-scoring one (a plain LIFO pop would reverse sibling priority and let a stale low-priority
3703
- // branch's dead URL waste a tight budget before the preferred branch).
3704
2349
  var deferredAia = [];
3705
- var deferSeq = 0; // monotonic deferral order (DFS priority): the drain tie-breaks same-depth frames by it
2350
+ var deferSeq = 0;
3706
2351
  while (!success) {
3707
2352
  if (!stack.length) {
3708
- // The local search is drained. Begin / continue the fetch phase: pick a deferred frame with PENDING WORK --
3709
- // one that has NOT yet fetched, OR whose poolMark trails the shared pool (a SIBLING branch fetched a cert
3710
- // SINCE this frame last ran, which may complete it). A drained frame is NOT discarded: a later sibling's
3711
- // fetch can add the very issuer an earlier, already-run branch needed, so every frame stays eligible for
3712
- // pool growth until it has fetched AND seen the whole pool. Among eligible frames pick the deepest, then the
3713
- // earliest-deferred (highest DFS priority). No eligible frame -> the whole search is exhausted.
3714
2353
  var _bi = -1;
3715
2354
  for (var _di = 0; _di < deferredAia.length; _di++) {
3716
2355
  var _f = deferredAia[_di];
3717
- if (_f.fetched && _f.poolMark >= pool.length) continue; // nothing left to fetch or to re-expand against
2356
+ if (_f.fetched && _f.poolMark >= pool.length) continue;
3718
2357
  if (_bi === -1) { _bi = _di; continue; }
3719
2358
  var _bf = deferredAia[_bi];
3720
2359
  if (_f.hop > _bf.hop || (_f.hop === _bf.hop && _f.seq < _bf.seq)) _bi = _di;
3721
2360
  }
3722
- if (_bi === -1) break; // no deferred frame can make further progress
3723
- var fb = deferredAia[_bi]; // NOT removed: it stays eligible for certs a later sibling fetch adds
2361
+ if (_bi === -1) break;
2362
+ var fb = deferredAia[_bi];
3724
2363
  var fbCur = fb.chain[0];
3725
2364
  if (!fb.fetched && aiaCtx && aiaCtx.fetches < aiaCtx.maxFetches) {
3726
- var fetched = await _fetchAiaIssuers(fbCur, aiaCtx); // append newly-fetched issuer(s) to the SHARED pool
2365
+ var fetched = await _fetchAiaIssuers(fbCur, aiaCtx);
3727
2366
  for (var fj = 0; fj < fetched.length; fj++) {
3728
2367
  var fdk = certDerKey(fetched[fj]);
3729
- if (intrinsic.setHas(poolDerKeys, fdk)) continue; // a byte-duplicate (mirror URL / repeating CMS) -> add once, never re-charge the budget/ceiling
2368
+ if (intrinsic.setHas(poolDerKeys, fdk)) continue;
3730
2369
  intrinsic.setAdd(poolDerKeys, fdk);
3731
2370
  if (pool.length < poolCeiling) pool.push(fetched[fj]);
3732
2371
  }
3733
2372
  }
3734
- fb.fetched = true; // the fetch attempt is done (or budget-skipped); the frame stays eligible for FUTURE pool growth
3735
- // Re-expand fb against every pool cert added SINCE it last ran -- its OWN just-fetched certs AND any a SIBLING
3736
- // branch fetched into the shared pool. The pool-index mark skips certs fb already scored (no redundant work);
3737
- // advancing it means fb re-scores only certs added even later. This runs even when the budget is exhausted, so
3738
- // a budget-capped frame still benefits from a sibling fetch.
2373
+ fb.fetched = true;
3739
2374
  var fbAki = childAkiKeyId(fbCur);
3740
2375
  var fbScored = [];
3741
2376
  for (var pj = fb.poolMark; pj < pool.length; pj++) {
@@ -3750,9 +2385,6 @@ async function build(leaf, opts) {
3750
2385
  var frame = stack.pop();
3751
2386
  var current = frame.chain[0];
3752
2387
 
3753
- // Terminate: is `current` issued by a configured anchor? An anchor-adjacent
3754
- // cert completes a candidate path; validate is the authority on acceptance.
3755
- // A found path breaks out here and the enclosing while exits on !success.
3756
2388
  for (var ai = 0; ai < anchors.length; ai++) {
3757
2389
  if (!nameMatchSoft(current.issuer.rdns, anchors[ai].name.rdns)) continue;
3758
2390
  if (!doValidate) { success = { path: frame.chain.slice(), trustAnchor: anchors[ai] }; break; }
@@ -3761,7 +2393,7 @@ async function build(leaf, opts) {
3761
2393
  if (res.valid) { success = { valid: true, path: frame.chain.slice(), trustAnchor: anchors[ai], result: res }; break; }
3762
2394
  if (!bestFail) bestFail = { path: frame.chain.slice(), trustAnchor: anchors[ai], result: res };
3763
2395
  }
3764
- if (success || frame.hop >= maxDepth) continue; // depth cap: this branch is exhausted
2396
+ if (success || frame.hop >= maxDepth) continue;
3765
2397
 
3766
2398
  var childAki = childAkiKeyId(current);
3767
2399
  var scored = [];
@@ -3770,35 +2402,21 @@ async function build(leaf, opts) {
3770
2402
  scored.push({ cand: pool[pi], score: scoreCandidate(pool[pi], childAki, anchors, opts.time) });
3771
2403
  }
3772
2404
  }
3773
- // Opt-in AIA caIssuers fetch as a FALLBACK: DEFER it (drained above only after the ENTIRE local search is
3774
- // exhausted, RFC 4158 sec. 7.2 "local before remote") rather than pushing it onto the stack, so it can never
3775
- // run ahead of an unexplored local sibling -- a build the static pool can complete never fetches. It is gated
3776
- // on `success` being unset (guaranteed by the continue above), NOT on the issuer name failing to match an
3777
- // anchor: an issuer DN that matches an anchor whose KEY did not validate this chain (a CA key rollover -- the
3778
- // real same-DN, different-key intermediate is missing and reachable only via AIA) still needs the fetch.
3779
2405
  if (aiaCtx && frame.hop < maxDepth && aiaCtx.fetches < aiaCtx.maxFetches) {
3780
- // poolMark: the shared-pool length now, so the drain re-expands this frame only against certs added LATER
3781
- // (its own fetch + any sibling branch's fetch), never re-scoring the static pool it expands against below.
3782
- // seq: the deferral order, so the drain can tie-break same-depth frames by DFS priority (earliest first).
3783
- // fetched: false until this frame's own AIA fetch runs; it then stays eligible for later sibling pool growth.
3784
2406
  deferredAia.push({ chain: frame.chain, hop: frame.hop, keys: frame.keys, poolMark: pool.length, seq: deferSeq, fetched: false });
3785
2407
  deferSeq += 1;
3786
2408
  }
3787
2409
  considered += _pushCandidates(frame, scored, stack, counter);
3788
2410
  }
3789
2411
 
3790
- var aiaFetches = aiaCtx ? aiaCtx.fetches : 0; // the count of AIA caIssuers network GETs this build performed (0 when opts.fetchAia is off)
2412
+ var aiaFetches = aiaCtx ? aiaCtx.fetches : 0;
3791
2413
  if (success) {
3792
2414
  if (doValidate) return { valid: true, path: success.path, trustAnchor: success.trustAnchor, result: success.result, candidatesConsidered: considered, aiaFetches: aiaFetches };
3793
2415
  return { path: success.path, trustAnchor: success.trustAnchor, candidatesConsidered: considered, aiaFetches: aiaFetches };
3794
2416
  }
3795
2417
  if (anyChainAssembled) {
3796
- // Chains reached an anchor but none validated -> the soft verdict carrying
3797
- // the best failing validate result (parity with validate; never a throw).
3798
2418
  return { valid: false, path: bestFail.path, trustAnchor: bestFail.trustAnchor, result: bestFail.result, candidatesConsidered: considered, aiaFetches: aiaFetches };
3799
2419
  }
3800
- // No chain to any configured anchor could even be assembled -- a permanent
3801
- // structural verdict (name/key chaining dead-ended before the trust store).
3802
2420
  throw E("path/no-path", "build: no certification path from the leaf to any configured trust anchor could be assembled");
3803
2421
  }
3804
2422
 
@@ -3809,13 +2427,6 @@ module.exports = {
3809
2427
  crlChecker: crlChecker,
3810
2428
  ocspChecker: ocspChecker,
3811
2429
  verifyOcspResponse: verifyOcspResponse,
3812
- // The set of extension OIDs whose CRITICAL semantics this validator processes (RFC 5280 sec. 6.1).
3813
- // Exposed so a linter can distinguish "processed" from "merely decoded" and stay consistent with
3814
- // the path-validation verdict on a critical extension -- a decoder in certExtensionDecoders is NOT
3815
- // by itself proof the criticality is honored. Both sets are frozen so a caller cannot mutate them.
3816
2430
  PROCESSED_EXTENSIONS: PROCESSED_EXTENSIONS,
3817
- // Extensions that ARE processed for an intermediate CA but are unprocessed on the target/leaf, so a
3818
- // critical instance on the target fails closed (RFC 5280 sec. 6.1.5(f)) -- policyMappings is
3819
- // prepare-next-only and SHOULD-be-non-critical. A linter mirrors this for a leaf certificate.
3820
2431
  TARGET_UNPROCESSED_IF_CRITICAL: TARGET_UNPROCESSED_IF_CRITICAL,
3821
2432
  };