@blamejs/pki 0.5.18 → 0.5.20

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.
@@ -67,6 +67,20 @@ function pathCode(e, fallback) {
67
67
  return (e && typeof e.code === "string" && e.code.indexOf("path/") === 0) ? e.code : fallback;
68
68
  }
69
69
 
70
+ // Membership decided by comparison rather than by a prototype method, through the shared guard.
71
+ // Every use below is a RULE: whether a certificate's extended key usages cover the ones the caller
72
+ // required, whether a policy is already expected at a node, whether anyPolicy is in the user's
73
+ // initial set. See guard-list for what a replaced Array.prototype.indexOf did to this validator.
74
+ var contains = guard.list.contains;
75
+ var containsAll = guard.list.containsAll;
76
+ // RFC 5280 6.1.4(a): a policyMappings extension MUST NOT map to or from anyPolicy. The scan for a
77
+ // violating pair is the rule, so it routes through the guard for the same reason.
78
+ function mapsAnyPolicy(mappings) {
79
+ return guard.list.anyMatches(mappings, function (m) {
80
+ return !!m && (m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy);
81
+ });
82
+ }
83
+
70
84
  var NS = pkix.makeNS("path", PathError, oid);
71
85
  var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
72
86
 
@@ -828,7 +842,7 @@ function processPolicies(state, cert, i, checks) {
828
842
  if (p.policyIdentifier === OID.anyPolicy) { anyPolicyPresent = true; anyPolicyQualifiers = p.qualifiersBytes; return; }
829
843
  var matched = false;
830
844
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
831
- if (node.expectedPolicySet.indexOf(p.policyIdentifier) !== -1) {
845
+ if (contains(node.expectedPolicySet, p.policyIdentifier)) {
832
846
  addChild(state, node, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], checks);
833
847
  matched = true;
834
848
  }
@@ -849,7 +863,7 @@ function processPolicies(state, cert, i, checks) {
849
863
  // certificate's own anyPolicy entry ("set the qualifier_set to AP-Q").
850
864
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
851
865
  node.expectedPolicySet.forEach(function (ep) {
852
- var already = node.children.some(function (ch) { return ch.validPolicy === ep; });
866
+ var already = guard.list.anyMatches(node.children, function (ch) { return ch.validPolicy === ep; });
853
867
  if (!already) addChild(state, node, ep, anyPolicyQualifiers, [ep], checks);
854
868
  });
855
869
  });
@@ -929,8 +943,7 @@ function ekuPurposeFails(cert, requiredEku, checks) {
929
943
  catch (e) { checks.push({ name: "extendedKeyUsage", ok: false, code: pathCode(e, "path/bad-extension-value") }); return true; }
930
944
  if (!eku) return false; // absent EKU: unrestricted (4.2.1.12 restricts only when present)
931
945
  var purposes = eku.value;
932
- var ok = purposes.indexOf(OID.anyExtendedKeyUsage) !== -1 ||
933
- requiredEku.every(function (p) { return purposes.indexOf(p) !== -1; });
946
+ var ok = contains(purposes, OID.anyExtendedKeyUsage) || containsAll(purposes, requiredEku);
934
947
  checks.push({ name: "extendedKeyUsage", ok: ok, code: ok ? undefined : "path/eku-not-permitted" });
935
948
  return !ok;
936
949
  }
@@ -950,7 +963,10 @@ function prepareNext(state, cert, i, checks) {
950
963
  try { pm = decodeExt(cert, OID.policyMappings); }
951
964
  catch (e) { checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); return { fatal: true, error: e }; }
952
965
  if (pm) {
953
- var badAny = pm.value.some(function (m) { return m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy; });
966
+ // The scan for a forbidden anyPolicy mapping is the rule itself (RFC 5280 6.1.4(a)), so it is a
967
+ // loop rather than a `some`: one answering false makes the forbidden mapping invisible and the
968
+ // certificate is accepted carrying it.
969
+ var badAny = mapsAnyPolicy(pm.value);
954
970
  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))") }; }
955
971
  if (state.validPolicyTree) applyPolicyMappings(state, pm.value, i);
956
972
  }
@@ -1084,12 +1100,16 @@ var TARGET_UNPROCESSED_IF_CRITICAL = {};
1084
1100
  TARGET_UNPROCESSED_IF_CRITICAL[OID.policyMappings] = true;
1085
1101
  Object.freeze(TARGET_UNPROCESSED_IF_CRITICAL);
1086
1102
 
1103
+ // Membership is asked of the table's OWN properties. Freezing the table stops a write to it and
1104
+ // stops nothing about the prototype chain: a name planted on Object.prototype answers for every OID
1105
+ // the table does not carry, and read live this reported an unrecognized critical extension as
1106
+ // processed. On a CA certificate only the first table is consulted, so the chain was accepted.
1087
1107
  function unrecognizedCriticalExtension(cert, isTarget) {
1088
1108
  for (var i = 0; i < cert.extensions.length; i++) {
1089
1109
  var ext = cert.extensions[i];
1090
1110
  if (!ext.critical) continue;
1091
- if (!PROCESSED_EXTENSIONS[ext.oid]) return ext.oid;
1092
- if (isTarget && TARGET_UNPROCESSED_IF_CRITICAL[ext.oid]) return ext.oid;
1111
+ if (!guard.intrinsic.hasOwn(PROCESSED_EXTENSIONS, ext.oid)) return ext.oid;
1112
+ if (isTarget && guard.intrinsic.hasOwn(TARGET_UNPROCESSED_IF_CRITICAL, ext.oid)) return ext.oid;
1093
1113
  }
1094
1114
  return null;
1095
1115
  }
@@ -1222,7 +1242,7 @@ async function validate(path, opts) {
1222
1242
  // as a SUBSTRING match -- validated to an array of strings instead.
1223
1243
  if (opts.userInitialPolicySet !== undefined) {
1224
1244
  var uipsOk = Array.isArray(opts.userInitialPolicySet) && opts.userInitialPolicySet.length > 0 &&
1225
- opts.userInitialPolicySet.every(function (p) { return typeof p === "string" && p.length > 0; });
1245
+ guard.list.allMatch(opts.userInitialPolicySet, function (p) { return typeof p === "string" && p.length > 0; });
1226
1246
  if (!uipsOk) throw E("path/bad-input", "validate: opts.userInitialPolicySet must be a non-empty array of policy OID strings");
1227
1247
  // Each entry is compared (indexOf) against canonical decoder output, so a
1228
1248
  // non-canonical key would silently never match -- fail the typo closed here.
@@ -1427,7 +1447,7 @@ async function validate(path, opts) {
1427
1447
  var lpm;
1428
1448
  try { lpm = decodeExt(cert, OID.policyMappings); }
1429
1449
  catch (_e) { lpm = null; checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true; }
1430
- if (lpm && lpm.value.some(function (m) { return m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy; })) {
1450
+ if (lpm && mapsAnyPolicy(lpm.value)) {
1431
1451
  checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true;
1432
1452
  }
1433
1453
  // 4.2.1.10 / 4.2.1.14: nameConstraints and inhibitAnyPolicy MUST be
@@ -1470,7 +1490,9 @@ async function validate(path, opts) {
1470
1490
  }
1471
1491
  if (checkPurpose && ta.purposes) {
1472
1492
  anchorPurposeApplied = true;
1473
- if (ta.purposes[checkPurpose] !== true) {
1493
+ // The map is the operator's RESTRICTION, so it is asked for its own entry: an inherited
1494
+ // `true` would grant the purpose on every anchor whose map does not name it.
1495
+ if (!guard.intrinsic.hasOwn(ta.purposes, checkPurpose) || ta.purposes[checkPurpose] !== true) {
1474
1496
  checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
1475
1497
  }
1476
1498
  }
@@ -1502,7 +1524,7 @@ async function validate(path, opts) {
1502
1524
  var policyOk = state.explicitPolicy > 0 || ucps.length > 0;
1503
1525
  if (!policyOk) {
1504
1526
  var last = state.results[state.results.length - 1];
1505
- if (!last.checks.some(function (c) { return c.code === "path/policy-required"; })) {
1527
+ if (!guard.list.anyMatches(last.checks, function (c) { return c.code === "path/policy-required"; })) {
1506
1528
  last.checks.push({ name: "policy", ok: false, code: "path/policy-required" });
1507
1529
  }
1508
1530
  failed = true;
@@ -1539,7 +1561,7 @@ async function validate(path, opts) {
1539
1561
  function userConstrainedPolicies(state, n) {
1540
1562
  if (!state.validPolicyTree) return [];
1541
1563
  var uips = state.userInitialPolicySet;
1542
- var anyUser = uips.indexOf(OID.anyPolicy) !== -1;
1564
+ var anyUser = contains(uips, OID.anyPolicy);
1543
1565
  var leaves = leavesAt(state.validPolicyTree, n);
1544
1566
  var explicit = {}, hasAnyLeaf = false;
1545
1567
  leaves.forEach(function (node) {
@@ -1547,7 +1569,7 @@ function userConstrainedPolicies(state, n) {
1547
1569
  else explicit[node.validPolicy] = true;
1548
1570
  });
1549
1571
  var set = {};
1550
- Object.keys(explicit).forEach(function (p) { if (anyUser || uips.indexOf(p) !== -1) set[p] = true; });
1572
+ Object.keys(explicit).forEach(function (p) { if (anyUser || contains(uips, p)) set[p] = true; });
1551
1573
  // 6.1.5(g) step 3: a depth-n anyPolicy node under a restrictive user set
1552
1574
  // expands to each user policy (the intersection of anyPolicy with the user
1553
1575
  // set is the user set itself).
@@ -2193,7 +2215,7 @@ function crlChecker(crls, opts) {
2193
2215
  // nothing can never erase a revocation the checker would otherwise report.
2194
2216
  for (var dj = 0; dj < classified.deltas.length; dj++) {
2195
2217
  var dRec = classified.deltas[dj];
2196
- if (consumedDeltas.indexOf(dRec) !== -1) continue;
2218
+ if (contains(consumedDeltas, dRec)) continue;
2197
2219
  // A delta that was MERGEABLE with some complete CRL but lost the sec. 5.2.4 @3580 selection
2198
2220
  // does not speak for its scope at all -- the selected delta does, and it was evaluated
2199
2221
  // against the base. Letting a superseded delta contribute its revocation while ignoring its
@@ -2365,7 +2387,7 @@ function ocspChecker(responses) {
2365
2387
  // certificate's own subject encoding -- sec. 7.1-equal but not byte-identical).
2366
2388
  var issuerNameCandidates = [cert.issuer.bytes];
2367
2389
  function addNameCandidate(nm) {
2368
- if (nm && nm.bytes && !issuerNameCandidates.some(function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
2390
+ if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
2369
2391
  }
2370
2392
  if (issuer.issuerCert) addNameCandidate(issuer.issuerCert.subject);
2371
2393
  addNameCandidate(issuer.workingIssuerName);
@@ -2525,7 +2547,7 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
2525
2547
  if (!sig.ok) return unbound("the target certificate's signature does not verify under the supplied issuer certificate's key");
2526
2548
  var issuerCtx = { workingPublicKey: issuerCert.subjectPublicKeyInfo.bytes, workingIssuerName: issuerCert.subject, issuerCert: issuerCert };
2527
2549
  var issuerNameCandidates = [cert.issuer.bytes];
2528
- function add(nm) { if (nm && nm.bytes && !issuerNameCandidates.some(function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes); }
2550
+ function add(nm) { if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes); }
2529
2551
  add(issuerCert.subject);
2530
2552
  var issuerKeyBits;
2531
2553
  try { issuerKeyBits = ocspVerify.ocspKeyValue(issuerCert.subjectPublicKeyInfo.bytes); }
@@ -2604,14 +2626,15 @@ function coerceCert(input, label) {
2604
2626
  // inert without one. An EMPTY map states no constraint and is not a reason to refuse.
2605
2627
  function _hasPurposeScopedMetadata(ta) {
2606
2628
  if (!ta || typeof ta !== "object") return false;
2607
- return ["distrustAfter", "purposes"].some(function (k) {
2629
+ return guard.list.anyMatches(["distrustAfter", "purposes"], function (k) {
2608
2630
  var m = ta[k];
2609
2631
  return !!m && typeof m === "object" && Object.keys(m).length > 0;
2610
2632
  });
2611
2633
  }
2612
2634
 
2613
2635
  function assertAnchorConstraints(ta, checkPurpose) {
2614
- var d = (checkPurpose && ta && ta.distrustAfter) ? ta.distrustAfter[checkPurpose] : null;
2636
+ var d = (checkPurpose && ta && ta.distrustAfter && guard.intrinsic.hasOwn(ta.distrustAfter, checkPurpose))
2637
+ ? ta.distrustAfter[checkPurpose] : null;
2615
2638
  if (d == null) return null;
2616
2639
  return guard.time.assertValid(d, E, "path/bad-input", "trustAnchor.distrustAfter." + checkPurpose);
2617
2640
  }
@@ -2728,9 +2751,12 @@ function _pushCandidates(frame, scored, stack, counter) {
2728
2751
  counter.tick(); // breadth / total-work cap -> throws path/build-limit
2729
2752
  n += 1;
2730
2753
  var cand = scored[ci].cand, candKey = identityKey(cand);
2731
- if (frame.keys.has(candKey)) continue; // the (subject, SAN, key) tuple is already on this branch -> a loop, prune
2754
+ // Loop detection and the dedupe sets below are resource bounds, so they are asked through the
2755
+ // captured methods: a replaced `Set.prototype.has` answering false never prunes and the build
2756
+ // walks a cycle until a cap stops it.
2757
+ if (guard.intrinsic.setHas(frame.keys, candKey)) continue;
2732
2758
  var childKeys = new Set(frame.keys);
2733
- childKeys.add(candKey);
2759
+ guard.intrinsic.setAdd(childKeys, candKey);
2734
2760
  stack.push({ chain: [cand].concat(frame.chain), hop: frame.hop + 1, keys: childKeys });
2735
2761
  }
2736
2762
  return n;
@@ -2821,16 +2847,16 @@ async function _fetchAiaIssuers(current, aia) {
2821
2847
  // IP literals only (already validated above), never a hostname whose resolved address we cannot verify.
2822
2848
  if (net.isIP(u.hostname.replace(/^\[(.*)\]$/, "$1")) === 0 && !aia.transportGuardsAddresses) continue;
2823
2849
  u.hash = ""; // the fragment is never sent on the wire -> not part of the request/dedupe identity
2824
- if (aia.fetchedUrls.has(u.href) || seenThisCert.has(u.href)) continue; // dedupe (build-wide OR same-cert) on the normalized URL
2850
+ if (guard.intrinsic.setHas(aia.fetchedUrls, u.href) || guard.intrinsic.setHas(seenThisCert, u.href)) continue; // dedupe (build-wide OR same-cert) on the normalized URL
2825
2851
  if (seenThisCert.size >= aia.maxPerCert) break; // per-cert DISTINCT-url cap, checked BEFORE appending so maxAiaPerCert:0 collects nothing (no fetch at all)
2826
- seenThisCert.add(u.href);
2852
+ guard.intrinsic.setAdd(seenThisCert, u.href);
2827
2853
  uris.push(u.href);
2828
2854
  }
2829
2855
  var out = [];
2830
2856
  for (var k = 0; k < uris.length; k++) {
2831
- if (aia.fetchedUrls.has(uris[k])) continue; // a same-cert normalization-equal duplicate
2857
+ if (guard.intrinsic.setHas(aia.fetchedUrls, uris[k])) continue; // a same-cert normalization-equal duplicate
2832
2858
  if (aia.fetches >= aia.maxFetches) break; // total budget reached -> STOP fetching (a SILENT cap, never a throw that denies a buildable path)
2833
- aia.fetchedUrls.add(uris[k]); // mark BEFORE the fetch -> fetched at most once, even on failure
2859
+ guard.intrinsic.setAdd(aia.fetchedUrls, uris[k]); // mark BEFORE the fetch -> fetched at most once, even on failure
2834
2860
  aia.fetches += 1;
2835
2861
  var certs;
2836
2862
  try { certs = await _aiaFetchOne(uris[k], aia); }
@@ -2958,7 +2984,7 @@ async function build(leaf, opts) {
2958
2984
  // repeating CMS returning the same issuer) is appended AT MOST ONCE -- it otherwise inflates the pool, charging
2959
2985
  // the candidate budget and the ceiling for each copy before the issuer is ever evaluated.
2960
2986
  var poolDerKeys = new Set();
2961
- for (var pdk = 0; pdk < pool.length; pdk++) poolDerKeys.add(certDerKey(pool[pdk]));
2987
+ for (var pdk = 0; pdk < pool.length; pdk++) guard.intrinsic.setAdd(poolDerKeys, certDerKey(pool[pdk]));
2962
2988
 
2963
2989
  if (!Array.isArray(opts.trustAnchors) || opts.trustAnchors.length === 0) throw E("path/bad-input", "build: opts.trustAnchors must be a non-empty array of anchor tuples or root certificates");
2964
2990
  var anchors = opts.trustAnchors.map(toAnchor);
@@ -3120,8 +3146,8 @@ async function build(leaf, opts) {
3120
3146
  var fetched = await _fetchAiaIssuers(fbCur, aiaCtx); // append newly-fetched issuer(s) to the SHARED pool
3121
3147
  for (var fj = 0; fj < fetched.length; fj++) {
3122
3148
  var fdk = certDerKey(fetched[fj]);
3123
- if (poolDerKeys.has(fdk)) continue; // a byte-duplicate (mirror URL / repeating CMS) -> add once, never re-charge the budget/ceiling
3124
- poolDerKeys.add(fdk);
3149
+ if (guard.intrinsic.setHas(poolDerKeys, fdk)) continue; // a byte-duplicate (mirror URL / repeating CMS) -> add once, never re-charge the budget/ceiling
3150
+ guard.intrinsic.setAdd(poolDerKeys, fdk);
3125
3151
  if (pool.length < poolCeiling) pool.push(fetched[fj]);
3126
3152
  }
3127
3153
  }
@@ -52,6 +52,20 @@ var MLDSA_SUITABLE_DIGEST = {
52
52
  "ML-DSA-65": { sha384: 1, sha512: 1, shake256: 1 },
53
53
  "ML-DSA-87": { sha512: 1, shake256: 1 },
54
54
  };
55
+ // mldsaDigestSuitable(mlName, md) -- is `md` listed as meeting `mlName`'s security strength?
56
+ // Both levels are asked for their OWN entries. The gate rejects on a falsy lookup, so a digest name
57
+ // planted on Object.prototype answers for every name the row omits and a below-strength digest
58
+ // passes the RFC 9882 sec. 3.3 check. This is the single home for that question: the signer and the
59
+ // verifier both ask it, and a copy at either would let the two drift.
60
+ // @enforced-by behavioral -- an own-property table read has no rename-proof shape distinct from a
61
+ // legitimate one; the RED vectors (a below-strength digest still refused with its name planted on
62
+ // Object.prototype, on both the sign and verify paths) are the guard.
63
+ function mldsaDigestSuitable(mlName, md) {
64
+ if (!guard.intrinsic.hasOwn(MLDSA_SUITABLE_DIGEST, mlName)) return false;
65
+ var row = MLDSA_SUITABLE_DIGEST[mlName];
66
+ return !!row && guard.intrinsic.hasOwn(row, md) && !!row[md];
67
+ }
68
+
55
69
  var SLHDSA_BY_OID = {};
56
70
  [["sha2-128s", "sha256"], ["sha2-128f", "sha256"], ["sha2-192s", "sha512"], ["sha2-192f", "sha512"],
57
71
  ["sha2-256s", "sha512"], ["sha2-256f", "sha512"], ["shake-128s", "shake128"], ["shake-128f", "shake128"],
@@ -173,7 +187,7 @@ function resolveSignScheme(cert, so, noSignedAttrs, E) {
173
187
  } else {
174
188
  md = so.digestAlgorithm || "sha512";
175
189
  if (!NODE_DIGEST[md]) throw E("unsupported-algorithm", "unsupported ML-DSA message digest " + JSON.stringify(md));
176
- if (!MLDSA_SUITABLE_DIGEST[mlName][md]) throw E("unsupported-algorithm", "the " + md + " message digest is below the security strength of " + mlName + " (RFC 9882 sec. 3.3)");
190
+ if (!mldsaDigestSuitable(mlName, md)) throw E("unsupported-algorithm", "the " + md + " message digest is below the security strength of " + mlName + " (RFC 9882 sec. 3.3)");
177
191
  }
178
192
  return { digest: md, digestAlgId: _algId(md, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: mlName }, sign: { name: mlName }, ecdsaDer: false };
179
193
  }
@@ -266,5 +280,6 @@ module.exports = {
266
280
  pssSpkiPinnedHash: pssSpkiPinnedHash,
267
281
  signOverTbs: signOverTbs,
268
282
  MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST,
283
+ mldsaDigestSuitable: mldsaDigestSuitable,
269
284
  SLHDSA_BY_OID: SLHDSA_BY_OID,
270
285
  };
package/lib/tsp-sign.js CHANGED
@@ -336,7 +336,7 @@ function request(messageImprint, opts) {
336
336
  if (opts.certReq != null && typeof opts.certReq !== "boolean") throw _err("tsp/bad-input", "certReq must be a boolean");
337
337
  if (opts.certReq === true) fields.push(b.boolean(true));
338
338
  if (opts.extensions != null) {
339
- if (!Array.isArray(opts.extensions) || !opts.extensions.every(function (e) { return Buffer.isBuffer(e) || e instanceof Uint8Array; })) throw _err("tsp/bad-input", "extensions must be an array of encoded Extension DER buffers");
339
+ if (!Array.isArray(opts.extensions) || !guard.list.allMatch(opts.extensions, function (e) { return Buffer.isBuffer(e) || e instanceof Uint8Array; })) throw _err("tsp/bad-input", "extensions must be an array of encoded Extension DER buffers");
340
340
  // Each pre-encoded Extension is validated as one, and its extnID must not repeat -- the same
341
341
  // gate every other request builder applies. Spliced in unchecked, a caller relaying a blob it
342
342
  // did not author put fully chosen bytes inside [0] and this encoder emitted DER its own
@@ -507,7 +507,7 @@ var _BIND_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512" };
507
507
  // tsp/* verdict code; a config-shape error throws (the caller's contract).
508
508
  function _imprintMatches(mi, data) {
509
509
  var name = mi.hashAlgorithm && mi.hashAlgorithm.name;
510
- var nodeAlg = name && _VERIFY_DIGEST[name];
510
+ var nodeAlg = (name && guard.intrinsic.hasOwn(_VERIFY_DIGEST, name)) ? _VERIFY_DIGEST[name] : null;
511
511
  if (!nodeAlg) return "tsp/unsupported-algorithm"; // cannot recompute -> never assume a match
512
512
  var wantLen = _VERIFY_DIGEST_LEN[name];
513
513
  // The token's messageImprint MUST be a full-length digest for its algorithm. A short/empty
@@ -732,6 +732,12 @@ function _buildTsaChains(leaf, pool) {
732
732
  // Every option pki.tsp.verify reads. Adding one here is the only way to make it accepted.
733
733
  var _VERIFY_OPTS = { certs: 1, trustAnchor: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1 };
734
734
 
735
+ // Does any extension carry the critical flag? The answer IS the unknown-critical-extension gate, so
736
+ // it is decided by comparison instead of by a `some`. See guard-list.
737
+ function _anyCritical(extensions) {
738
+ return guard.list.anyMatches(extensions, function (e) { return !!e && !!e.critical; });
739
+ }
740
+
735
741
  async function verify(token, data, opts) {
736
742
  opts = opts || {};
737
743
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.verify options must be an object");
@@ -742,7 +748,7 @@ async function verify(token, data, opts) {
742
748
  // nothing to notice it by. Naming the difference at the boundary is the only place it is cheap.
743
749
  guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "tsp/bad-input",
744
750
  "pki.tsp.verify has an unknown option (note the anchor option here is `trustAnchor`, singular, an anchor tuple -- not the `trustAnchors` certificate list pki.cms.verify takes) ");
745
- if (opts.certs != null && (!Array.isArray(opts.certs) || !opts.certs.every(function (c) { return Buffer.isBuffer(c) || c instanceof Uint8Array; }))) {
751
+ if (opts.certs != null && (!Array.isArray(opts.certs) || !guard.list.allMatch(opts.certs, function (c) { return Buffer.isBuffer(c) || c instanceof Uint8Array; }))) {
746
752
  throw _err("tsp/bad-input", "pki.tsp.verify opts.certs must be an array of DER certificate Buffers"); // a bad element is a caller error, never silently dropped
747
753
  }
748
754
  var tokenDer = _tokenDer(token); // DER Buffer / PEM -> DER; a non-token input throws tsp/bad-input
@@ -789,7 +795,9 @@ async function verify(token, data, opts) {
789
795
  // A TSTInfo carrying a CRITICAL extension the verifier cannot process is unusable: RFC 3161 sec.
790
796
  // 2.4.2 defers extension handling to RFC 5280 sec. 4.2, and defines no TSTInfo extension, so any
791
797
  // critical one is unrecognized (the unknown-critical-extension gate the cert path also applies).
792
- if (tst.extensions && tst.extensions.some(function (e) { return e.critical; })) {
798
+ // The scan for a critical extension IS the gate, so it is a loop rather than a `some`: one
799
+ // answering false makes the unrecognized critical extension invisible and the token is accepted.
800
+ if (tst.extensions && _anyCritical(tst.extensions)) {
793
801
  return fail("tsp/unknown-critical-extension");
794
802
  }
795
803
  // M13 -- the ESSCertID(V2) binding: the verifying cert MUST be the one the signer identified.
@@ -29,6 +29,12 @@
29
29
 
30
30
  var oid = require("./oid");
31
31
  var asn1 = require("./asn1-der");
32
+ var guard = require("./guard-all");
33
+
34
+ // Does this extended-key-usage list assert the given purpose? The answer decides whether a
35
+ // certificate qualifies as the kind the rule names, so it goes through the membership guard
36
+ // instead of a prototype method. See guard-list.
37
+ var _asserts = guard.list.contains;
32
38
 
33
39
  // requireV3(cert, E, code) -- the leaf attestation certificate MUST be X.509 v3.
34
40
  // @enforced-by behavioral -- a version-field check has no rename-proof code shape; the RED
@@ -73,7 +79,10 @@ function aikCert(cert, exts, E, code) {
73
79
  assertNotCa(cert, exts, E, code);
74
80
  if ((cert.subject.rdns || []).length !== 0) throw new E(code, "the tpm AIK certificate subject MUST be empty (WebAuthn 8.3.1)");
75
81
  var eku = exts.decode(cert, "extKeyUsage");
76
- if (!eku || !Array.isArray(eku.value) || eku.value.indexOf(oid.byName("tcgKpAikCertificate")) === -1) {
82
+ // The purpose test IS the rule that makes this an AIK certificate, so it is decided by comparison
83
+ // rather than by a prototype method: a replaced `indexOf` reports the purpose present on any
84
+ // certificate and an ordinary leaf is accepted as a TPM attestation key.
85
+ if (!eku || !Array.isArray(eku.value) || !_asserts(eku.value, oid.byName("tcgKpAikCertificate"))) {
77
86
  throw new E(code, "the tpm AIK certificate lacks the tcg-kp-AIKCertificate extended key purpose (WebAuthn 8.3.1)");
78
87
  }
79
88
  var san = exts.decode(cert, "subjectAltName");