@blamejs/pki 0.5.19 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.5.21 — 2026-08-21
8
+
9
+ The check that holds the crypto engine to load-time captures could be satisfied around, so nine modules had taken the safe primitive at one site and were never held to it anywhere else.
10
+
11
+ ### Added
12
+
13
+ - `guard.intrinsic.bufferEquals`, the captured byte-identity comparison the bindings above are decided with.
14
+
15
+ ### Changed
16
+
17
+ - Taking the captures has one spelling again. The guard orchestrator no longer re-exports them, so a module reaches them by importing the module that holds them, and the check reads that off the loaded module graph rather than off the text. This is the enforcement change: a module that adopts one safe primitive is held to the rule everywhere, and no way of writing the import, or of writing something that resembles one, changes the answer.
18
+ - The seven modules the widened check surfaced carry a declared count of remaining reads rather than an exemption. A new live read pushes a module past its figure and fails, and converting reads without lowering the figure fails too, so the number keeps naming the real count. A module reaching zero leaves the list and is held to zero.
19
+
20
+ ### Fixed
21
+
22
+ - An OCSP response is bound to the certificate it answers for with captured byte comparisons. The CertID says the response concerns this issuer and the responder ID says this key signed it, and both were decided by a `Buffer.prototype.equals` read at call time: replaced after load it reports every hash equal, and a `good` signed by a different CA answers for the certificate under check.
23
+ - The signer's key is matched to its certificate under a captured case fold. WebCrypto algorithm names compare case-insensitively, so the check folds both sides; dispatched live, a replaced fold reported two different algorithm names equal and a key whose algorithm does not match the certificate passed the gate that exists to catch exactly that.
24
+ - `pki.sign` decides the RSASSA-PSS parameters, the digest tables and its key-shape tests on captured operations, so a caller who replaces one after this package loads cannot change which algorithm a signature runs under.
25
+ - A signing key is classified by asking the value what it is rather than by `instanceof` against a global. The old test read the `Uint8Array` binding at call time, and it answered wrongly in both directions on its own terms: a real typed array from another realm was not one, and an object built on the prototype was.
26
+
27
+ ## v0.5.20 — 2026-08-20
28
+
29
+ Five verdicts that turned on list membership now decide it by comparison, so replacing an array method after the module loads cannot change what a certificate, a responder or a signed message is found to be.
30
+
31
+ ### Added
32
+
33
+ - `guard-list`, an internal choke point for membership decided by comparison. Four verbs cover the rule shapes a verdict needs: is this value present, are all of these present, does any member satisfy a test, do all of them. An absent list is honestly empty rather than a throw, because these run on verdict paths where a throw would read as a different verdict. It is not on the public namespace; the surface is the verdicts above.
34
+
35
+ ### Changed
36
+
37
+ - Certification-path building asks its loop-detection and fetch-dedupe sets through captured methods. These bound the work a hostile chain can cause, and a replaced `Set.prototype.has` answering false let a cycle be walked and a URL be fetched repeatedly until a size cap stopped it.
38
+
39
+ ### Fixed
40
+
41
+ - `pki.path.validate` holds its `requiredEku` gate against a replaced `Array.prototype.indexOf`. The purpose test IS the decision about what the certificate may be used for, and dispatched live it reported `serverAuth` present on a leaf whose extended key usage carried only `emailProtection`, returning a valid chain for a certificate the caller had asked it to refuse. Certificate-policy membership and the `anyPolicy` scans in the policy tree are decided the same way.
42
+ - `pki.cms.verify` keeps `trusted: false` for a message no supplied anchor covers. The message-level verdict combined the per-signer answers with `every`, and a replaced one reported the whole set trusted without consulting a single signer.
43
+ - An OCSP response is still refused when its delegate lacks id-kp-OCSPSigning. Responder authorization is the decision about who may answer for an issuer, and reading the purpose list through the prototype let a delegate the CA never authorized speak for the whole issuer.
44
+ - A TPM attestation certificate still needs the tcg-kp-AIKCertificate purpose. That test is what makes a leaf an attestation key, so reading it live let an ordinary certificate be accepted as one.
45
+ - A timestamp token carrying an unrecognized critical extension is still refused (RFC 3161 sec. 2.4.2). The scan for a critical extension IS the gate, and a replaced `Array.prototype.some` answering false made the extension invisible and the unusable token acceptable.
46
+ - `pki.cms.verify` keeps `valid: false` for a message whose signature does not verify. The soundness verdict combined the per-signer answers the same way the trust verdict did, so a corrupted signature reported `valid: true`.
47
+ - `pki.path.validate` asks its processed-extension tables for their OWN entries. Freezing a table prevents a write to it and does not prevent an inherited lookup, so a name planted on `Object.prototype` answered for every OID the table did not carry and an unrecognized critical extension counted as processed. On a CA certificate only one of the two tables is consulted, so the chain was accepted rather than refused (RFC 5280 sec. 6.1.4).
48
+ - A trust anchor's `purposes` map is read for its own entries too. The map is the operator's restriction, and an inherited `true` granted the purpose on every anchor whose map did not name it. `distrustAfter` is read the same way.
49
+ - The ML-DSA digest-strength rule (RFC 9882 sec. 3.3) is one question both the signer and the CMS verifier ask, and it answers from the policy table's own entries. A digest name planted on `Object.prototype` answered for every name a parameter set's row omits, so a below-strength digest passed the strength check: `sha256` was accepted for ML-DSA-87 on both paths. The check also had two copies, which is why it now lives in the module that owns the table.
50
+
7
51
  ## v0.5.19 — 2026-08-20
8
52
 
9
53
  The crypto engine and the JWS signer decide with operations captured at load, so code that runs after the module does cannot change what a permission check concludes.
@@ -23,6 +23,7 @@ var schemaCms = require("./schema-cms");
23
23
  var webcrypto = require("./webcrypto");
24
24
  var frameworkError = require("./framework-error");
25
25
  var guard = require("./guard-all");
26
+ var intrinsic = require("./guard-intrinsic");
26
27
  var pbes2 = require("./pbes2");
27
28
  var b = asn1.build;
28
29
  var subtle = webcrypto.webcrypto.subtle;
@@ -415,8 +416,8 @@ async function _kemriCek(ri, km) {
415
416
  // why, by name. An unresolvable OID is left to _openContent too, as the unsupported algorithm it is.
416
417
  function _assertContentCipherMode(eci, ct) {
417
418
  var oidStr = eci.contentEncryptionAlgorithm.oid;
418
- var isAead = guard.intrinsic.hasOwn(AEAD_ALGS, oidStr);
419
- if (!isAead && !guard.intrinsic.hasOwn(CONTENT_MODE, oidStr)) return;
419
+ var isAead = intrinsic.hasOwn(AEAD_ALGS, oidStr);
420
+ if (!isAead && !intrinsic.hasOwn(CONTENT_MODE, oidStr)) return;
420
421
  if (ct === "authEnvelopedData") {
421
422
  if (isAead) return;
422
423
  throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + oidStr + " is not an authenticated encryption algorithm, which authEnvelopedData requires (RFC 5083 sec. 2)");
@@ -428,14 +429,14 @@ function _assertContentCipherMode(eci, ct) {
428
429
  // ---- stage 3: open the content (uniform failure) ---------------------------
429
430
  async function _openContent(parsed, eci, cek, ct) {
430
431
  var alg = eci.contentEncryptionAlgorithm;
431
- var keyBits = guard.intrinsic.hasOwn(CONTENT_KEYBITS, alg.oid) ? CONTENT_KEYBITS[alg.oid] : 0;
432
+ var keyBits = intrinsic.hasOwn(CONTENT_KEYBITS, alg.oid) ? CONTENT_KEYBITS[alg.oid] : 0;
432
433
  if (!keyBits) {
433
434
  // AES-CCM parses completely here -- registered OIDs, the sec. 3.1 nonce bound, the seven legal
434
435
  // ICV lengths -- and is then declined, because AES-GCM is the only AEAD this toolkit implements
435
436
  // for content encryption. Saying only "unsupported <oid>" is what an operator also sees for an
436
437
  // OID nothing in the toolkit recognizes, and the two call for different things: one is a
437
438
  // capability gap to route around, the other is a malformed or exotic message.
438
- if (guard.intrinsic.hasOwn(AEAD_ALGS, alg.oid) && AEAD_ALGS[alg.oid] === "ccm") {
439
+ if (intrinsic.hasOwn(AEAD_ALGS, alg.oid) && AEAD_ALGS[alg.oid] === "ccm") {
439
440
  throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + alg.oid + " is AES-CCM. RFC 5084 sec. 3.1 permits it for this content type; this toolkit implements AES-GCM only for content encryption");
440
441
  }
441
442
  throw _err("cms/unsupported-algorithm", "unsupported contentEncryptionAlgorithm " + alg.oid);
@@ -23,6 +23,7 @@ var schemaCms = require("./schema-cms");
23
23
  var webcrypto = require("./webcrypto");
24
24
  var frameworkError = require("./framework-error");
25
25
  var guard = require("./guard-all");
26
+ var intrinsic = require("./guard-intrinsic");
26
27
  var pbes2 = require("./pbes2");
27
28
  var b = asn1.build;
28
29
  var subtle = webcrypto.webcrypto.subtle;
@@ -565,7 +566,7 @@ function _assertAuthAttrs(pairs, forbidden) {
565
566
  }
566
567
  var t;
567
568
  try { t = asn1.read.oid(node.children[0]); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute type is not an OBJECT IDENTIFIER", e); }
568
- if (guard.intrinsic.hasOwn(seenTypes, t)) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
569
+ if (intrinsic.hasOwn(seenTypes, t)) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
569
570
  seenTypes[t] = 1;
570
571
  if (forbidden && t === forbidden.oid) throw _err("cms/bad-input", forbidden.why);
571
572
  });
package/lib/cms-sign.js CHANGED
@@ -22,6 +22,7 @@ var frameworkError = require("./framework-error");
22
22
  var webcrypto = require("./webcrypto");
23
23
  var signScheme = require("./sign-scheme");
24
24
  var guard = require("./guard-all");
25
+ var intrinsic = require("./guard-intrinsic");
25
26
  var pkiBuild = require("./pki-build"); // the shared post-sign self-check, among other producing helpers
26
27
  var cms = require("./schema-cms");
27
28
 
@@ -117,7 +118,7 @@ function _buildSid(cert, useSki) {
117
118
  function _buildSignedAttrs(pairs) {
118
119
  var seenTypes = {};
119
120
  var attrs = pairs.map(function (p) {
120
- if (guard.intrinsic.hasOwn(seenTypes, p.type)) throw _err("cms/bad-input", "signedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
121
+ if (intrinsic.hasOwn(seenTypes, p.type)) throw _err("cms/bad-input", "signedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
121
122
  seenTypes[p.type] = 1;
122
123
  return b.sequence([b.oid(p.type), b.set(p.values)]);
123
124
  });
@@ -155,8 +156,8 @@ function _buildUnsignedAttrs(list) {
155
156
  var pairs = _resolveAttrPairs(list, "an unsigned attribute value");
156
157
  var seen = {};
157
158
  pairs.forEach(function (p) {
158
- if (guard.intrinsic.hasOwn(UNSIGNED_FORBIDDEN, p.type)) throw _err("cms/bad-input", "the " + UNSIGNED_FORBIDDEN[p.type] + " attribute must not appear as an unsigned attribute (RFC 5652 sec. 11)");
159
- if (guard.intrinsic.hasOwn(seen, p.type)) throw _err("cms/bad-input", "unsignedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
159
+ if (intrinsic.hasOwn(UNSIGNED_FORBIDDEN, p.type)) throw _err("cms/bad-input", "the " + UNSIGNED_FORBIDDEN[p.type] + " attribute must not appear as an unsigned attribute (RFC 5652 sec. 11)");
160
+ if (intrinsic.hasOwn(seen, p.type)) throw _err("cms/bad-input", "unsignedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
160
161
  seen[p.type] = 1;
161
162
  });
162
163
  var setOf = b.set(pairs.map(function (p) { return b.sequence([b.oid(p.type), b.set(p.values)]); }));
package/lib/cms-verify.js CHANGED
@@ -39,11 +39,14 @@ var cmsEncrypt = require("./cms-encrypt");
39
39
  var cmsDecrypt = require("./cms-decrypt");
40
40
  var cmsCompress = require("./cms-compress");
41
41
  var signScheme = require("./sign-scheme");
42
- var MLDSA_SUITABLE_DIGEST = signScheme.MLDSA_SUITABLE_DIGEST; // shared sign/verify digest-strength policy (RFC 9882 sec. 3.3)
42
+ // The digest-strength question is asked of sign-scheme, which owns the policy table, so the signer
43
+ // and this verifier cannot drift (RFC 9882 sec. 3.3).
44
+ var _mldsaDigestSuitable = signScheme.mldsaDigestSuitable;
43
45
  var SLHDSA_BY_OID = signScheme.SLHDSA_BY_OID; // shared SLH-DSA set -> { wc, digest } (RFC 9814 sec. 4 pinned digest)
44
46
  var validator = require("./validator-all");
45
47
  var compositeSig = require("./composite-sig");
46
48
  var guard = require("./guard-all");
49
+ var intrinsic = require("./guard-intrinsic");
47
50
  var frameworkError = require("./framework-error");
48
51
  var pkix = require("./schema-pkix");
49
52
 
@@ -71,7 +74,7 @@ var DIGEST_HASH = {
71
74
  // foreign fault and loses this module's own unsupported-algorithm verdict.
72
75
  var SIG_HASH = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
73
76
  // Is `name` a message-digest algorithm this verifier supports?
74
- function _supportedDigest(name) { return !!DIGEST_HASH[name]; }
77
+ function _supportedDigest(name) { return intrinsic.hasOwn(DIGEST_HASH, name) && !!DIGEST_HASH[name]; }
75
78
  // The digest of `content` under the named algorithm, resolved to a Buffer.
76
79
  function _computeDigest(name, content) {
77
80
  return subtle.digest(DIGEST_HASH[name], content).then(function (d) { return Buffer.from(d); });
@@ -366,7 +369,7 @@ function _verifyOne(si, content, eContentType, parsedCerts, csTarget) {
366
369
  // meet the parameter set's security strength -- a below-strength digest is the weaker link and is
367
370
  // rejected fail-closed (the RFC's "verifiers MAY reject", taken by the strict-verifier posture).
368
371
  // With signed attributes absent the digestAlgorithm has no meaning (sec. 3.3) and is not checked.
369
- if (scheme.kind === "mldsa" && si.signedAttrsBytes && !MLDSA_SUITABLE_DIGEST[scheme.name][si.digestAlgorithm.name]) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " message digest is below the security strength of " + scheme.name + " (RFC 9882 sec. 3.3)" });
372
+ if (scheme.kind === "mldsa" && si.signedAttrsBytes && !_mldsaDigestSuitable(scheme.name, si.digestAlgorithm.name)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " message digest is below the security strength of " + scheme.name + " (RFC 9882 sec. 3.3)" });
370
373
  // RFC 9814 sec. 4: an SLH-DSA SignerInfo's message-digest algorithm is the one paired with the
371
374
  // parameter set (the sec. 4 list, always at least twice the tree-hash size); the signer emits it
372
375
  // and this strict verifier requires it, so a digest that does not match the set's paired hash fails
@@ -768,7 +771,9 @@ function _verify(input, opts) {
768
771
  // message a second time to learn it, and a check that needs a second parse is a check most
769
772
  // callers will not write.
770
773
  var res = {
771
- valid: signers.length > 0 && signers.every(function (s) { return s.ok === true; }),
774
+ // Signature soundness is decided by comparison for the same reason the trust verdict below
775
+ // is: `every` replaced after load reports the whole set sound without reading one signer.
776
+ valid: signers.length > 0 && guard.list.allMatch(signers, function (s) { return s.ok === true; }),
772
777
  eContentType: parsed.encapContentInfo.eContentType,
773
778
  signers: signers,
774
779
  };
@@ -873,6 +878,12 @@ function _keyUsagePermitsSigning(parsedCerts, der) {
873
878
  return true; // absent: unconstrained (sec. 4.2.1.3)
874
879
  }
875
880
 
881
+ // Every signer trusted? The answer IS the message-level trust verdict, so it is decided by
882
+ // comparison instead of by an `every`. See guard-list.
883
+ function _allTrusted(signers) {
884
+ return guard.list.allMatch(signers, function (s) { return s.trusted === true; });
885
+ }
886
+
876
887
  // `cfg` is verify's SNAPSHOT, never the caller's options object -- the parameter is named for that
877
888
  // so a future caller cannot hand it the live one without noticing.
878
889
  function _applyTrust(res, parsedCerts, cfg) {
@@ -963,7 +974,10 @@ function _applyTrust(res, parsedCerts, cfg) {
963
974
  });
964
975
  });
965
976
  }, Promise.resolve()).then(function () {
966
- res.trusted = res.valid && res.signers.length > 0 && res.signers.every(function (s) { return s.trusted === true; });
977
+ // The trust verdict is decided by comparison, not by a prototype method: `every` replaced after
978
+ // load answers true without consulting a single signer, and a message no anchor covers reports
979
+ // trusted:true.
980
+ res.trusted = res.valid && res.signers.length > 0 && _allTrusted(res.signers);
967
981
  });
968
982
  }
969
983
  // Parse a candidate cert DER and index its SKI; a cert that will not parse is skipped (it
package/lib/guard-all.js CHANGED
@@ -55,6 +55,7 @@ var bytes = require("./guard-bytes");
55
55
  var text = require("./guard-text");
56
56
  var limits = require("./guard-limits");
57
57
  var crypto = require("./guard-crypto");
58
+ var list = require("./guard-list");
58
59
  var range = require("./guard-range");
59
60
  var time = require("./guard-time");
60
61
  var name = require("./guard-name");
@@ -67,13 +68,18 @@ var compress = require("./guard-compress");
67
68
  var secret = require("./guard-secret");
68
69
  var parsed = require("./guard-parsed");
69
70
  var async_ = require("./guard-async");
70
- var intrinsic = require("./guard-intrinsic");
71
+ // guard-intrinsic is deliberately NOT re-exported here. Re-exporting it gave taking the captures a
72
+ // second spelling, and the check that holds a module to the discipline could then be satisfied
73
+ // around: a module reached them as `guard.intrinsic.<x>` and never entered scope, so its remaining
74
+ // live reads were never named. With one spelling, a module's own require IS the fact, and the check
75
+ // reads it off the loaded module graph rather than guessing at the text.
71
76
 
72
77
  module.exports = {
73
78
  bytes: bytes,
74
79
  text: text,
75
80
  limits: limits,
76
81
  crypto: crypto,
82
+ list: list,
77
83
  range: range,
78
84
  time: time,
79
85
  name: name,
@@ -86,5 +92,4 @@ module.exports = {
86
92
  secret: secret,
87
93
  parsed: parsed,
88
94
  async: async_,
89
- intrinsic: intrinsic,
90
95
  };
@@ -55,6 +55,10 @@ var _bufferConcat = Buffer.concat;
55
55
  var _bufferIsBuffer = Buffer.isBuffer;
56
56
  var _bufferByteLength = Buffer.byteLength;
57
57
  var _bufferCompare = Buffer.compare;
58
+ // `Buffer.prototype.equals` is the byte-identity test a binding decides on: an OCSP CertID says the
59
+ // response is about this issuer, a responder ID says this key signed it. Dispatched live, one
60
+ // answering true makes a response about a DIFFERENT certificate match the one being asked about.
61
+ var _bufferEquals = uncurry(Buffer.prototype.equals);
58
62
 
59
63
  // The rest of what a guard reads from the runtime while it is deciding something. These are
60
64
  // grouped here rather than captured file by file so the set is enumerable in one place and a guard
@@ -268,6 +272,7 @@ module.exports = {
268
272
  isBuffer: _bufferIsBuffer,
269
273
  byteLength: _bufferByteLength,
270
274
  compare: _bufferCompare,
275
+ bufferEquals: _bufferEquals,
271
276
  isView: _isView,
272
277
  isInteger: _isInteger,
273
278
  isSafeInteger: _isSafeInteger,
@@ -0,0 +1,85 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal, so no operator-facing namespace. The documented surface is every verdict whose rule
6
+ // composes this guard: pki.path.validate (requiredEku, the policy tree), pki.cms.verify (the
7
+ // message trust verdict), the OCSP delegated-responder check, the TPM AIK purpose check, and the
8
+ // timestamp unknown-critical-extension gate.
9
+ //
10
+ // guard-list, membership decided by comparison and never by a prototype method.
11
+ //
12
+ // Defends the intrinsic-substitution class (CWE-349 / CWE-693: a control the caller can neutralize).
13
+ // Written `list.indexOf(v) !== -1` or `list.some(fn)`, a membership test asks the runtime, at the
14
+ // moment of the call, which function `indexOf` or `some` is. A caller who receives control after
15
+ // this package loads, through an option accessor or a `toJSON` or while a verb is suspended on an
16
+ // await, can replace it. The answer becomes whatever they chose, while every check around it still
17
+ // runs and still passes.
18
+ //
19
+ // The distinction that matters is what the answer IS. A scan over a local accumulator produces a
20
+ // value. A scan that decides whether a certificate carries a purpose, whether a policy is expected,
21
+ // or whether every signer is trusted IS the rule. Measured on the tree before this guard existed:
22
+ // with `Array.prototype.indexOf` replaced to report every value present, a certificate restricted
23
+ // to emailProtection satisfied a requiredEku of serverAuth, and pki.path.validate went from
24
+ // valid:false to valid:true.
25
+ //
26
+ // Capturing the method at load (guard-intrinsic) is the other half of the same defense, and is what
27
+ // a module already inside that discipline uses. This guard is for the rule sites themselves. A loop
28
+ // over `===` has nothing to replace, so it holds whatever the prototype says, in a module that has
29
+ // been swept and in one that has not.
30
+
31
+ // contains(list, value) -> boolean. Strict-equality membership, the `indexOf(v) !== -1` rule.
32
+ // A null / undefined list is honestly empty, never a throw: these run on verdict paths.
33
+ // @enforced-by behavioral -- `.indexOf(x) !== -1` has no rename-proof form that separates a
34
+ // membership RULE from its legitimate siblings: the identical token is a substring search
35
+ // (`authority.indexOf(":")`, `addr.indexOf("@")`) and a local index lookup, and a shape matching
36
+ // all of them fired on 37 sites of which most were strings. What makes a site this guard's is
37
+ // what the answer IS -- a decision about a certificate, a policy or a signer -- and that is a
38
+ // judgment no regex makes. The RED vectors are the guard: a codeSigning-only certificate still
39
+ // fails a serverAuth requiredEku, and an unauthorized responder is still refused, each with
40
+ // Array.prototype.indexOf replaced after load.
41
+ function contains(list, value) {
42
+ if (!list) return false;
43
+ for (var i = 0, n = list.length; i < n; i++) if (list[i] === value) return true;
44
+ return false;
45
+ }
46
+
47
+ // containsAll(list, values) -> boolean. Every one of `values` present in `list` -- the shape a
48
+ // "these purposes are all required" rule needs. No `values` is vacuously satisfied.
49
+ // @enforced-by behavioral -- the composition of contains() has no rename-proof shape of its own;
50
+ // the RED vectors (a codeSigning-only certificate still fails a serverAuth requiredEku with
51
+ // Array.prototype.indexOf replaced) are the guard.
52
+ function containsAll(list, values) {
53
+ if (!values) return true;
54
+ for (var i = 0, n = values.length; i < n; i++) if (!contains(list, values[i])) return false;
55
+ return true;
56
+ }
57
+
58
+ // anyMatches(list, predicate) -> boolean. The `some(fn)` rule: does any member satisfy it? The
59
+ // predicate is the caller's own function, so what this guard removes is the DISPATCH, not the test.
60
+ // @enforced-by behavioral -- `.some(` has legitimate non-rule siblings (building a list, reporting
61
+ // a count), so a blanket detector would false-fire; the RED vectors (a critical extension is
62
+ // still seen, an untrusted signer still reported) are the guard.
63
+ function anyMatches(list, predicate) {
64
+ if (!list) return false;
65
+ for (var i = 0, n = list.length; i < n; i++) if (predicate(list[i], i)) return true;
66
+ return false;
67
+ }
68
+
69
+ // allMatch(list, predicate) -> boolean. The `every(fn)` rule, same reasoning as anyMatches. An
70
+ // empty list is vacuously true, matching Array.prototype.every -- a caller that needs "non-empty
71
+ // AND all" tests the length itself, as pki.cms.verify does for the trust verdict.
72
+ // @enforced-by behavioral -- see anyMatches; the RED vector (an untrusted signer keeps
73
+ // trusted:false with Array.prototype.every replaced) is the guard.
74
+ function allMatch(list, predicate) {
75
+ if (!list) return true;
76
+ for (var i = 0, n = list.length; i < n; i++) if (!predicate(list[i], i)) return false;
77
+ return true;
78
+ }
79
+
80
+ module.exports = {
81
+ contains: contains,
82
+ containsAll: containsAll,
83
+ anyMatches: anyMatches,
84
+ allMatch: allMatch,
85
+ };
@@ -29,6 +29,7 @@
29
29
 
30
30
  var asn1 = require("./asn1-der");
31
31
  var guard = require("./guard-all");
32
+ var intrinsic = require("./guard-intrinsic");
32
33
  var oid = require("./oid");
33
34
  var x509 = require("./schema-x509");
34
35
  var compositeSig = require("./composite-sig");
@@ -45,11 +46,19 @@ OCSP_CERTID_HASHES[oid.byName("sha256")] = "SHA-256";
45
46
  OCSP_CERTID_HASHES[oid.byName("sha384")] = "SHA-384";
46
47
  OCSP_CERTID_HASHES[oid.byName("sha512")] = "SHA-512";
47
48
  var OID_OCSP_SIGNING = oid.byName("ocspSigning");
49
+ // Does this extended-key-usage list assert the given purpose? The answer IS an authorization
50
+ // decision, so it goes through the membership guard rather than a prototype method. See guard-list.
51
+ var _asserts = guard.list.contains;
52
+ // Captured at load. Each of these answers a binding question -- does this response concern the
53
+ // certificate being asked about, did this key sign it -- so a replacement made after this module
54
+ // loaded would decide the answer while every check around it still runs.
55
+ var _bufferFrom = intrinsic.bufferFrom;
56
+ var _bytesEqual = intrinsic.bufferEquals;
48
57
  var OID_OCSP_NOCHECK = oid.byName("ocspNoCheck");
49
58
  var OID_EKU = oid.byName("extKeyUsage");
50
59
  var OID_KEY_USAGE = oid.byName("keyUsage");
51
60
 
52
- function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return Buffer.from(h); }); }
61
+ function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return _bufferFrom(h); }); }
53
62
 
54
63
  // The subjectPublicKey BIT STRING value (past the unused-bits octet) of an SPKI DER: the exact
55
64
  // bytes an OCSP CertID issuerKeyHash / byKey KeyHash hash over (RFC 6960 sec. 4.1.1). Throws on a
@@ -105,12 +114,12 @@ function makeOcspVerify(deps) {
105
114
  // 5280 sec. 7.1-equal byte encoding of the validated issuer DN to try.
106
115
  async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
107
116
  if (certID.serialNumberHex !== cert.serialNumberHex) return false;
108
- var hashName = OCSP_CERTID_HASHES[certID.hashAlgorithm.oid];
117
+ var hashName = intrinsic.hasOwn(OCSP_CERTID_HASHES, certID.hashAlgorithm.oid) ? OCSP_CERTID_HASHES[certID.hashAlgorithm.oid] : null;
109
118
  if (!hashName) return false;
110
119
  var keyHash = await ocspDigest(hashName, issuerKeyBits);
111
- if (!certID.issuerKeyHash.equals(keyHash)) return false;
120
+ if (!_bytesEqual(certID.issuerKeyHash, keyHash)) return false;
112
121
  for (var i = 0; i < issuerNameCandidates.length; i++) {
113
- if (certID.issuerNameHash.equals(await ocspDigest(hashName, issuerNameCandidates[i]))) return true;
122
+ if (_bytesEqual(certID.issuerNameHash, await ocspDigest(hashName, issuerNameCandidates[i]))) return true;
114
123
  }
115
124
  return false;
116
125
  }
@@ -123,7 +132,7 @@ function makeOcspVerify(deps) {
123
132
  var matchesIssuer = false;
124
133
  try {
125
134
  if (rid.byName) matchesIssuer = dnEqual(rid.byName.rdns, cert.issuer.rdns);
126
- else if (rid.byKey) matchesIssuer = rid.byKey.equals(await ocspDigest("SHA-1", issuerKeyBits));
135
+ else if (rid.byKey) matchesIssuer = _bytesEqual(rid.byKey, await ocspDigest("SHA-1", issuerKeyBits));
127
136
  } catch (_e) { matchesIssuer = false; }
128
137
  if (matchesIssuer) return issuer.workingPublicKey;
129
138
 
@@ -134,7 +143,7 @@ function makeOcspVerify(deps) {
134
143
  var identifies = false;
135
144
  try {
136
145
  if (rid.byName) identifies = dnEqual(rid.byName.rdns, rc.subject.rdns);
137
- else if (rid.byKey) identifies = rid.byKey.equals(await ocspDigest("SHA-1", ocspKeyValue(rc.subjectPublicKeyInfo.bytes)));
146
+ else if (rid.byKey) identifies = _bytesEqual(rid.byKey, await ocspDigest("SHA-1", ocspKeyValue(rc.subjectPublicKeyInfo.bytes)));
138
147
  } catch (_e) { identifies = false; }
139
148
  if (!identifies) continue;
140
149
  // The delegate MUST be issued directly by the CA that issued the target.
@@ -154,7 +163,11 @@ function makeOcspVerify(deps) {
154
163
  var eku;
155
164
  try { eku = decodeExt(rc, OID_EKU); }
156
165
  catch (_e) { continue; }
157
- if (!eku || eku.value.indexOf(OID_OCSP_SIGNING) === -1) continue;
166
+ // The membership test IS the delegation rule, so it is decided by comparison rather than by a
167
+ // prototype method: dispatched live, an `Array.prototype.indexOf` replaced after load reports
168
+ // the purpose present on ANY certificate, and a cert an attacker put in the response's own
169
+ // certs bag is accepted as the authorized responder.
170
+ if (!eku || !_asserts(eku.value, OID_OCSP_SIGNING)) continue;
158
171
  // A delegate asserting keyUsage MUST permit digitalSignature; an absent keyUsage is
159
172
  // unrestricted, an unreadable one is not authoritative.
160
173
  var ku;
@@ -48,6 +48,7 @@ var cmpVerify = require("./cmp-verify");
48
48
  var cmsVerify = require("./cms-verify");
49
49
  var cmpSession = require("./cmp-session");
50
50
  var guard = require("./guard-all");
51
+ var intrinsic = require("./guard-intrinsic");
51
52
  var constants = require("./constants");
52
53
  var validator = require("./validator-all");
53
54
  var cms = require("./schema-cms");
@@ -67,6 +68,20 @@ function pathCode(e, fallback) {
67
68
  return (e && typeof e.code === "string" && e.code.indexOf("path/") === 0) ? e.code : fallback;
68
69
  }
69
70
 
71
+ // Membership decided by comparison rather than by a prototype method, through the shared guard.
72
+ // Every use below is a RULE: whether a certificate's extended key usages cover the ones the caller
73
+ // required, whether a policy is already expected at a node, whether anyPolicy is in the user's
74
+ // initial set. See guard-list for what a replaced Array.prototype.indexOf did to this validator.
75
+ var contains = guard.list.contains;
76
+ var containsAll = guard.list.containsAll;
77
+ // RFC 5280 6.1.4(a): a policyMappings extension MUST NOT map to or from anyPolicy. The scan for a
78
+ // violating pair is the rule, so it routes through the guard for the same reason.
79
+ function mapsAnyPolicy(mappings) {
80
+ return guard.list.anyMatches(mappings, function (m) {
81
+ return !!m && (m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy);
82
+ });
83
+ }
84
+
70
85
  var NS = pkix.makeNS("path", PathError, oid);
71
86
  var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
72
87
 
@@ -828,7 +843,7 @@ function processPolicies(state, cert, i, checks) {
828
843
  if (p.policyIdentifier === OID.anyPolicy) { anyPolicyPresent = true; anyPolicyQualifiers = p.qualifiersBytes; return; }
829
844
  var matched = false;
830
845
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
831
- if (node.expectedPolicySet.indexOf(p.policyIdentifier) !== -1) {
846
+ if (contains(node.expectedPolicySet, p.policyIdentifier)) {
832
847
  addChild(state, node, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], checks);
833
848
  matched = true;
834
849
  }
@@ -849,7 +864,7 @@ function processPolicies(state, cert, i, checks) {
849
864
  // certificate's own anyPolicy entry ("set the qualifier_set to AP-Q").
850
865
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
851
866
  node.expectedPolicySet.forEach(function (ep) {
852
- var already = node.children.some(function (ch) { return ch.validPolicy === ep; });
867
+ var already = guard.list.anyMatches(node.children, function (ch) { return ch.validPolicy === ep; });
853
868
  if (!already) addChild(state, node, ep, anyPolicyQualifiers, [ep], checks);
854
869
  });
855
870
  });
@@ -929,8 +944,7 @@ function ekuPurposeFails(cert, requiredEku, checks) {
929
944
  catch (e) { checks.push({ name: "extendedKeyUsage", ok: false, code: pathCode(e, "path/bad-extension-value") }); return true; }
930
945
  if (!eku) return false; // absent EKU: unrestricted (4.2.1.12 restricts only when present)
931
946
  var purposes = eku.value;
932
- var ok = purposes.indexOf(OID.anyExtendedKeyUsage) !== -1 ||
933
- requiredEku.every(function (p) { return purposes.indexOf(p) !== -1; });
947
+ var ok = contains(purposes, OID.anyExtendedKeyUsage) || containsAll(purposes, requiredEku);
934
948
  checks.push({ name: "extendedKeyUsage", ok: ok, code: ok ? undefined : "path/eku-not-permitted" });
935
949
  return !ok;
936
950
  }
@@ -950,7 +964,10 @@ function prepareNext(state, cert, i, checks) {
950
964
  try { pm = decodeExt(cert, OID.policyMappings); }
951
965
  catch (e) { checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); return { fatal: true, error: e }; }
952
966
  if (pm) {
953
- var badAny = pm.value.some(function (m) { return m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy; });
967
+ // The scan for a forbidden anyPolicy mapping is the rule itself (RFC 5280 6.1.4(a)), so it is a
968
+ // loop rather than a `some`: one answering false makes the forbidden mapping invisible and the
969
+ // certificate is accepted carrying it.
970
+ var badAny = mapsAnyPolicy(pm.value);
954
971
  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
972
  if (state.validPolicyTree) applyPolicyMappings(state, pm.value, i);
956
973
  }
@@ -1084,12 +1101,16 @@ var TARGET_UNPROCESSED_IF_CRITICAL = {};
1084
1101
  TARGET_UNPROCESSED_IF_CRITICAL[OID.policyMappings] = true;
1085
1102
  Object.freeze(TARGET_UNPROCESSED_IF_CRITICAL);
1086
1103
 
1104
+ // Membership is asked of the table's OWN properties. Freezing the table stops a write to it and
1105
+ // stops nothing about the prototype chain: a name planted on Object.prototype answers for every OID
1106
+ // the table does not carry, and read live this reported an unrecognized critical extension as
1107
+ // processed. On a CA certificate only the first table is consulted, so the chain was accepted.
1087
1108
  function unrecognizedCriticalExtension(cert, isTarget) {
1088
1109
  for (var i = 0; i < cert.extensions.length; i++) {
1089
1110
  var ext = cert.extensions[i];
1090
1111
  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;
1112
+ if (!intrinsic.hasOwn(PROCESSED_EXTENSIONS, ext.oid)) return ext.oid;
1113
+ if (isTarget && intrinsic.hasOwn(TARGET_UNPROCESSED_IF_CRITICAL, ext.oid)) return ext.oid;
1093
1114
  }
1094
1115
  return null;
1095
1116
  }
@@ -1222,7 +1243,7 @@ async function validate(path, opts) {
1222
1243
  // as a SUBSTRING match -- validated to an array of strings instead.
1223
1244
  if (opts.userInitialPolicySet !== undefined) {
1224
1245
  var uipsOk = Array.isArray(opts.userInitialPolicySet) && opts.userInitialPolicySet.length > 0 &&
1225
- opts.userInitialPolicySet.every(function (p) { return typeof p === "string" && p.length > 0; });
1246
+ guard.list.allMatch(opts.userInitialPolicySet, function (p) { return typeof p === "string" && p.length > 0; });
1226
1247
  if (!uipsOk) throw E("path/bad-input", "validate: opts.userInitialPolicySet must be a non-empty array of policy OID strings");
1227
1248
  // Each entry is compared (indexOf) against canonical decoder output, so a
1228
1249
  // non-canonical key would silently never match -- fail the typo closed here.
@@ -1427,7 +1448,7 @@ async function validate(path, opts) {
1427
1448
  var lpm;
1428
1449
  try { lpm = decodeExt(cert, OID.policyMappings); }
1429
1450
  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; })) {
1451
+ if (lpm && mapsAnyPolicy(lpm.value)) {
1431
1452
  checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true;
1432
1453
  }
1433
1454
  // 4.2.1.10 / 4.2.1.14: nameConstraints and inhibitAnyPolicy MUST be
@@ -1470,7 +1491,9 @@ async function validate(path, opts) {
1470
1491
  }
1471
1492
  if (checkPurpose && ta.purposes) {
1472
1493
  anchorPurposeApplied = true;
1473
- if (ta.purposes[checkPurpose] !== true) {
1494
+ // The map is the operator's RESTRICTION, so it is asked for its own entry: an inherited
1495
+ // `true` would grant the purpose on every anchor whose map does not name it.
1496
+ if (!intrinsic.hasOwn(ta.purposes, checkPurpose) || ta.purposes[checkPurpose] !== true) {
1474
1497
  checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
1475
1498
  }
1476
1499
  }
@@ -1502,7 +1525,7 @@ async function validate(path, opts) {
1502
1525
  var policyOk = state.explicitPolicy > 0 || ucps.length > 0;
1503
1526
  if (!policyOk) {
1504
1527
  var last = state.results[state.results.length - 1];
1505
- if (!last.checks.some(function (c) { return c.code === "path/policy-required"; })) {
1528
+ if (!guard.list.anyMatches(last.checks, function (c) { return c.code === "path/policy-required"; })) {
1506
1529
  last.checks.push({ name: "policy", ok: false, code: "path/policy-required" });
1507
1530
  }
1508
1531
  failed = true;
@@ -1539,7 +1562,7 @@ async function validate(path, opts) {
1539
1562
  function userConstrainedPolicies(state, n) {
1540
1563
  if (!state.validPolicyTree) return [];
1541
1564
  var uips = state.userInitialPolicySet;
1542
- var anyUser = uips.indexOf(OID.anyPolicy) !== -1;
1565
+ var anyUser = contains(uips, OID.anyPolicy);
1543
1566
  var leaves = leavesAt(state.validPolicyTree, n);
1544
1567
  var explicit = {}, hasAnyLeaf = false;
1545
1568
  leaves.forEach(function (node) {
@@ -1547,7 +1570,7 @@ function userConstrainedPolicies(state, n) {
1547
1570
  else explicit[node.validPolicy] = true;
1548
1571
  });
1549
1572
  var set = {};
1550
- Object.keys(explicit).forEach(function (p) { if (anyUser || uips.indexOf(p) !== -1) set[p] = true; });
1573
+ Object.keys(explicit).forEach(function (p) { if (anyUser || contains(uips, p)) set[p] = true; });
1551
1574
  // 6.1.5(g) step 3: a depth-n anyPolicy node under a restrictive user set
1552
1575
  // expands to each user policy (the intersection of anyPolicy with the user
1553
1576
  // set is the user set itself).
@@ -2193,7 +2216,7 @@ function crlChecker(crls, opts) {
2193
2216
  // nothing can never erase a revocation the checker would otherwise report.
2194
2217
  for (var dj = 0; dj < classified.deltas.length; dj++) {
2195
2218
  var dRec = classified.deltas[dj];
2196
- if (consumedDeltas.indexOf(dRec) !== -1) continue;
2219
+ if (contains(consumedDeltas, dRec)) continue;
2197
2220
  // A delta that was MERGEABLE with some complete CRL but lost the sec. 5.2.4 @3580 selection
2198
2221
  // does not speak for its scope at all -- the selected delta does, and it was evaluated
2199
2222
  // against the base. Letting a superseded delta contribute its revocation while ignoring its
@@ -2365,7 +2388,7 @@ function ocspChecker(responses) {
2365
2388
  // certificate's own subject encoding -- sec. 7.1-equal but not byte-identical).
2366
2389
  var issuerNameCandidates = [cert.issuer.bytes];
2367
2390
  function addNameCandidate(nm) {
2368
- if (nm && nm.bytes && !issuerNameCandidates.some(function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
2391
+ if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
2369
2392
  }
2370
2393
  if (issuer.issuerCert) addNameCandidate(issuer.issuerCert.subject);
2371
2394
  addNameCandidate(issuer.workingIssuerName);
@@ -2525,7 +2548,7 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
2525
2548
  if (!sig.ok) return unbound("the target certificate's signature does not verify under the supplied issuer certificate's key");
2526
2549
  var issuerCtx = { workingPublicKey: issuerCert.subjectPublicKeyInfo.bytes, workingIssuerName: issuerCert.subject, issuerCert: issuerCert };
2527
2550
  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); }
2551
+ function add(nm) { if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes); }
2529
2552
  add(issuerCert.subject);
2530
2553
  var issuerKeyBits;
2531
2554
  try { issuerKeyBits = ocspVerify.ocspKeyValue(issuerCert.subjectPublicKeyInfo.bytes); }
@@ -2604,14 +2627,15 @@ function coerceCert(input, label) {
2604
2627
  // inert without one. An EMPTY map states no constraint and is not a reason to refuse.
2605
2628
  function _hasPurposeScopedMetadata(ta) {
2606
2629
  if (!ta || typeof ta !== "object") return false;
2607
- return ["distrustAfter", "purposes"].some(function (k) {
2630
+ return guard.list.anyMatches(["distrustAfter", "purposes"], function (k) {
2608
2631
  var m = ta[k];
2609
2632
  return !!m && typeof m === "object" && Object.keys(m).length > 0;
2610
2633
  });
2611
2634
  }
2612
2635
 
2613
2636
  function assertAnchorConstraints(ta, checkPurpose) {
2614
- var d = (checkPurpose && ta && ta.distrustAfter) ? ta.distrustAfter[checkPurpose] : null;
2637
+ var d = (checkPurpose && ta && ta.distrustAfter && intrinsic.hasOwn(ta.distrustAfter, checkPurpose))
2638
+ ? ta.distrustAfter[checkPurpose] : null;
2615
2639
  if (d == null) return null;
2616
2640
  return guard.time.assertValid(d, E, "path/bad-input", "trustAnchor.distrustAfter." + checkPurpose);
2617
2641
  }
@@ -2728,9 +2752,12 @@ function _pushCandidates(frame, scored, stack, counter) {
2728
2752
  counter.tick(); // breadth / total-work cap -> throws path/build-limit
2729
2753
  n += 1;
2730
2754
  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
2755
+ // Loop detection and the dedupe sets below are resource bounds, so they are asked through the
2756
+ // captured methods: a replaced `Set.prototype.has` answering false never prunes and the build
2757
+ // walks a cycle until a cap stops it.
2758
+ if (intrinsic.setHas(frame.keys, candKey)) continue;
2732
2759
  var childKeys = new Set(frame.keys);
2733
- childKeys.add(candKey);
2760
+ intrinsic.setAdd(childKeys, candKey);
2734
2761
  stack.push({ chain: [cand].concat(frame.chain), hop: frame.hop + 1, keys: childKeys });
2735
2762
  }
2736
2763
  return n;
@@ -2821,16 +2848,16 @@ async function _fetchAiaIssuers(current, aia) {
2821
2848
  // IP literals only (already validated above), never a hostname whose resolved address we cannot verify.
2822
2849
  if (net.isIP(u.hostname.replace(/^\[(.*)\]$/, "$1")) === 0 && !aia.transportGuardsAddresses) continue;
2823
2850
  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
2851
+ if (intrinsic.setHas(aia.fetchedUrls, u.href) || intrinsic.setHas(seenThisCert, u.href)) continue; // dedupe (build-wide OR same-cert) on the normalized URL
2825
2852
  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);
2853
+ intrinsic.setAdd(seenThisCert, u.href);
2827
2854
  uris.push(u.href);
2828
2855
  }
2829
2856
  var out = [];
2830
2857
  for (var k = 0; k < uris.length; k++) {
2831
- if (aia.fetchedUrls.has(uris[k])) continue; // a same-cert normalization-equal duplicate
2858
+ if (intrinsic.setHas(aia.fetchedUrls, uris[k])) continue; // a same-cert normalization-equal duplicate
2832
2859
  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
2860
+ intrinsic.setAdd(aia.fetchedUrls, uris[k]); // mark BEFORE the fetch -> fetched at most once, even on failure
2834
2861
  aia.fetches += 1;
2835
2862
  var certs;
2836
2863
  try { certs = await _aiaFetchOne(uris[k], aia); }
@@ -2958,7 +2985,7 @@ async function build(leaf, opts) {
2958
2985
  // repeating CMS returning the same issuer) is appended AT MOST ONCE -- it otherwise inflates the pool, charging
2959
2986
  // the candidate budget and the ceiling for each copy before the issuer is ever evaluated.
2960
2987
  var poolDerKeys = new Set();
2961
- for (var pdk = 0; pdk < pool.length; pdk++) poolDerKeys.add(certDerKey(pool[pdk]));
2988
+ for (var pdk = 0; pdk < pool.length; pdk++) intrinsic.setAdd(poolDerKeys, certDerKey(pool[pdk]));
2962
2989
 
2963
2990
  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
2991
  var anchors = opts.trustAnchors.map(toAnchor);
@@ -3120,8 +3147,8 @@ async function build(leaf, opts) {
3120
3147
  var fetched = await _fetchAiaIssuers(fbCur, aiaCtx); // append newly-fetched issuer(s) to the SHARED pool
3121
3148
  for (var fj = 0; fj < fetched.length; fj++) {
3122
3149
  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);
3150
+ if (intrinsic.setHas(poolDerKeys, fdk)) continue; // a byte-duplicate (mirror URL / repeating CMS) -> add once, never re-charge the budget/ceiling
3151
+ intrinsic.setAdd(poolDerKeys, fdk);
3125
3152
  if (pool.length < poolCeiling) pool.push(fetched[fj]);
3126
3153
  }
3127
3154
  }
@@ -48,6 +48,7 @@ var cmsDecrypt = require("./cms-decrypt"); // the @internal decryptEnvelopedDa
48
48
  var schemaPkcs12 = require("./schema-pkcs12");
49
49
  var pkix = require("./schema-pkix");
50
50
  var guard = require("./guard-all");
51
+ var intrinsic = require("./guard-intrinsic");
51
52
  var frameworkError = require("./framework-error");
52
53
  var C = require("./constants");
53
54
 
@@ -368,7 +369,7 @@ function _buildBag(bag, opts, depth) {
368
369
  if (!bag || typeof bag !== "object") throw _err("pkcs12/bad-input", "each bag must be an object with a type");
369
370
  // Checked against THIS type's table. An unrecognized type falls through to the switch below,
370
371
  // which is the one place that names the permitted set.
371
- if (guard.intrinsic.hasOwn(_BAG_KEYS_BY_TYPE, bag.type)) {
372
+ if (intrinsic.hasOwn(_BAG_KEYS_BY_TYPE, bag.type)) {
372
373
  guard.identifier.assertKnownKeys(bag, _BAG_KEYS_BY_TYPE[bag.type], _err, "pkcs12/bad-input", function (k) {
373
374
  return "unknown field " + JSON.stringify(k) + " on a " + JSON.stringify(bag.type) +
374
375
  " bag; that field is not read for this bag type";
@@ -588,7 +589,7 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
588
589
  function _specForm(spec) {
589
590
  var isObject = !!spec && typeof spec === "object" && !Buffer.isBuffer(spec);
590
591
  var safeContents = isObject ? spec.safeContents : null;
591
- var full = guard.intrinsic.isArray(safeContents);
592
+ var full = intrinsic.isArray(safeContents);
592
593
  return { isObject: isObject, full: full, safeContents: full ? safeContents : null };
593
594
  }
594
595
 
@@ -599,7 +600,7 @@ function _normalizeSpec(spec, opts, form) {
599
600
  // be handed different values.
600
601
  var recipientCerts = opts.recipientCerts;
601
602
  var enveloped = recipientCerts != null;
602
- if (enveloped && (!guard.intrinsic.isArray(recipientCerts) || !recipientCerts.length)) throw _err("pkcs12/bad-input", "opts.recipientCerts must be a non-empty array of recipient certificates");
603
+ if (enveloped && (!intrinsic.isArray(recipientCerts) || !recipientCerts.length)) throw _err("pkcs12/bad-input", "opts.recipientCerts must be a non-empty array of recipient certificates");
603
604
  var certBags = [];
604
605
  if (spec.cert != null) certBags.push({ type: "cert", cert: spec.cert, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
605
606
  if (spec.ca != null && !Array.isArray(spec.ca)) throw _err("pkcs12/bad-input", "spec.ca must be an array of certificates");
@@ -749,7 +750,7 @@ async function _build(spec, opts) {
749
750
  // array reached _buildMacData as an empty descriptor and produced the DEFAULT MAC. Excluded by
750
751
  // the same test opts.integrity already applies, read through the load-time capture so a
751
752
  // replaced Array.isArray cannot reopen it.
752
- if (opts.mac != null && opts.mac !== false && (typeof opts.mac !== "object" || guard.intrinsic.isArray(opts.mac))) throw _err("pkcs12/bad-input", "opts.mac must be false or a { algorithm, hash, salt, iterations, keyLength } object");
753
+ if (opts.mac != null && opts.mac !== false && (typeof opts.mac !== "object" || intrinsic.isArray(opts.mac))) throw _err("pkcs12/bad-input", "opts.mac must be false or a { algorithm, hash, salt, iterations, keyLength } object");
753
754
  if (opts.mac != null && opts.mac !== false) {
754
755
  // The algorithm selects the derivation, so the table that runs is the one belonging to it.
755
756
  // An unrecognized spelling keeps the union; _buildMacData refuses it by name a moment later.
@@ -23,10 +23,28 @@ var webcrypto = require("./webcrypto");
23
23
  var subtle = webcrypto.webcrypto.subtle;
24
24
  var validator = require("./validator-all");
25
25
  var guard = require("./guard-all");
26
+ var intrinsic = require("./guard-intrinsic");
26
27
  var compositeSig = require("./composite-sig");
27
28
  var b = asn1.build;
28
29
  function O(name) { return oid.byName(name); }
29
30
 
31
+ // Captured at load. This module decides which algorithm a signature runs under and whether the
32
+ // caller's key matches the certificate it claims, so an operation read at call time is one the
33
+ // caller answers: a replaced case fold reports two different algorithm names equal, and a replaced
34
+ // `Buffer.isBuffer` admits a key that is not bytes.
35
+ var _BigInt = intrinsic.BigInt;
36
+ var _String = intrinsic.String;
37
+ var _filter = intrinsic.filter;
38
+ var _stringify = intrinsic.stringify;
39
+ var _toUpperCase = intrinsic.toUpperCase;
40
+ var _isBuffer = intrinsic.isBuffer;
41
+ var _bufferFrom = intrinsic.bufferFrom;
42
+ // Asks the value's internal slot rather than `instanceof`, which reads the `Uint8Array` global at
43
+ // call time and answers about whichever constructor is bound then. It also settles the two cases
44
+ // `instanceof` gets wrong on their own terms: a real typed array from another realm is one, and an
45
+ // object merely built on the prototype is not.
46
+ var _isU8 = intrinsic.types.isUint8Array;
47
+
30
48
  var HASH = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
31
49
  var NODE_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512", shake128: "shake128", shake256: "shake256" };
32
50
  var PSS_SALT = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
@@ -52,6 +70,20 @@ var MLDSA_SUITABLE_DIGEST = {
52
70
  "ML-DSA-65": { sha384: 1, sha512: 1, shake256: 1 },
53
71
  "ML-DSA-87": { sha512: 1, shake256: 1 },
54
72
  };
73
+ // mldsaDigestSuitable(mlName, md) -- is `md` listed as meeting `mlName`'s security strength?
74
+ // Both levels are asked for their OWN entries. The gate rejects on a falsy lookup, so a digest name
75
+ // planted on Object.prototype answers for every name the row omits and a below-strength digest
76
+ // passes the RFC 9882 sec. 3.3 check. This is the single home for that question: the signer and the
77
+ // verifier both ask it, and a copy at either would let the two drift.
78
+ // @enforced-by behavioral -- an own-property table read has no rename-proof shape distinct from a
79
+ // legitimate one; the RED vectors (a below-strength digest still refused with its name planted on
80
+ // Object.prototype, on both the sign and verify paths) are the guard.
81
+ function mldsaDigestSuitable(mlName, md) {
82
+ if (!intrinsic.hasOwn(MLDSA_SUITABLE_DIGEST, mlName)) return false;
83
+ var row = MLDSA_SUITABLE_DIGEST[mlName];
84
+ return !!row && intrinsic.hasOwn(row, md) && !!row[md];
85
+ }
86
+
55
87
  var SLHDSA_BY_OID = {};
56
88
  [["sha2-128s", "sha256"], ["sha2-128f", "sha256"], ["sha2-192s", "sha512"], ["sha2-192f", "sha512"],
57
89
  ["sha2-256s", "sha512"], ["sha2-256f", "sha512"], ["shake-128s", "shake128"], ["shake-128f", "shake128"],
@@ -65,7 +97,7 @@ function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(nam
65
97
  function _pssAlgId(digestName) {
66
98
  var hashAlg = b.sequence([b.oid(O(digestName)), b.nullValue()]);
67
99
  var mgf = b.sequence([b.oid(O("mgf1")), hashAlg]);
68
- var params = b.sequence([b.explicit(0, hashAlg), b.explicit(1, mgf), b.explicit(2, b.integer(BigInt(PSS_SALT[HASH[digestName]])))]);
100
+ var params = b.sequence([b.explicit(0, hashAlg), b.explicit(1, mgf), b.explicit(2, b.integer(_BigInt(PSS_SALT[HASH[digestName]])))]);
69
101
  return b.sequence([b.oid(O("rsassaPss")), params]);
70
102
  }
71
103
  // The hash an id-RSASSA-PSS SPKI restricts its key to, or null when it restricts none.
@@ -90,7 +122,7 @@ function _pssHashFromSpki(cert, E) {
90
122
  if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children) {
91
123
  throw E("unsupported-algorithm", "the id-RSASSA-PSS key parameters are not an RSASSA-PSS-params SEQUENCE (RFC 4055 sec. 3.1)");
92
124
  }
93
- var hashField = node.children.filter(function (c) { return c.tagClass === "context" && c.tagNumber === 0; })[0];
125
+ var hashField = _filter(node.children, function (c) { return c.tagClass === "context" && c.tagNumber === 0; })[0];
94
126
  // DEFAULT sha1Identifier -- an omitted hashAlgorithm names SHA-1, which this toolkit does not
95
127
  // sign or verify with, so it is reported as the pin it is and refused by the caller's own table.
96
128
  if (!hashField) return "sha1";
@@ -136,15 +168,15 @@ function resolveSignScheme(cert, so, noSignedAttrs, E) {
136
168
  var comp = compositeSig.COMPOSITE_ALGS[keyOid];
137
169
  if (comp) {
138
170
  if (comp.trad.unsupported) throw E("unsupported-algorithm", "composite " + comp.name + ": " + comp.trad.unsupported);
139
- if (so.digestAlgorithm && so.digestAlgorithm !== comp.phCms) throw E("bad-input", "composite " + comp.name + " fixes the digestAlgorithm to " + comp.phCms + " (draft-ietf-lamps-cms-composite-sigs sec. 3.4); " + JSON.stringify(so.digestAlgorithm) + " conflicts");
171
+ if (so.digestAlgorithm && so.digestAlgorithm !== comp.phCms) throw E("bad-input", "composite " + comp.name + " fixes the digestAlgorithm to " + comp.phCms + " (draft-ietf-lamps-cms-composite-sigs sec. 3.4); " + _stringify(so.digestAlgorithm) + " conflicts");
140
172
  return { composite: comp, digest: comp.phCms, digestAlgId: _algId(comp.phCms, "absent"), sigAlgId: _algId(comp.name, "absent") };
141
173
  }
142
174
  if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
143
175
  var isPssKey = keyOid === O("rsassaPss");
144
176
  var pinned = isPssKey ? _pssHashFromSpki(cert, E) : null;
145
- if (pinned && so.digestAlgorithm && so.digestAlgorithm !== pinned) throw E("bad-input", "the signer key restricts the RSASSA-PSS digest to " + pinned + ", but digestAlgorithm " + JSON.stringify(so.digestAlgorithm) + " was requested");
177
+ if (pinned && so.digestAlgorithm && so.digestAlgorithm !== pinned) throw E("bad-input", "the signer key restricts the RSASSA-PSS digest to " + pinned + ", but digestAlgorithm " + _stringify(so.digestAlgorithm) + " was requested");
146
178
  var d = so.digestAlgorithm || pinned || "sha256";
147
- if (!HASH[d]) throw E("unsupported-algorithm", "unsupported RSA digest algorithm " + JSON.stringify(d));
179
+ if (!HASH[d]) throw E("unsupported-algorithm", "unsupported RSA digest algorithm " + _stringify(d));
148
180
  if (so.pss || isPssKey) return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: _pssAlgId(d), imp: { name: "RSA-PSS", hash: HASH[d] }, sign: { name: "RSA-PSS", saltLength: PSS_SALT[HASH[d]] }, ecdsaDer: false };
149
181
  var rsaSigAlgId = so.combinedRsaSig ? _algId(RSA_PKCS1_SIG[d], "null") : _algId("rsaEncryption", "null");
150
182
  return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: rsaSigAlgId, imp: { name: "RSASSA-PKCS1-v1_5", hash: HASH[d] }, sign: { name: "RSASSA-PKCS1-v1_5" }, ecdsaDer: false };
@@ -156,13 +188,13 @@ function resolveSignScheme(cert, so, noSignedAttrs, E) {
156
188
  var ec = EC_BY_CURVE_OID[curveOid];
157
189
  if (!ec) throw E("unsupported-algorithm", "the signer key is on an unsupported EC curve");
158
190
  var de = so.digestAlgorithm || "sha256";
159
- if (!HASH[de]) throw E("unsupported-algorithm", "unsupported ECDSA digest algorithm " + JSON.stringify(de));
191
+ if (!HASH[de]) throw E("unsupported-algorithm", "unsupported ECDSA digest algorithm " + _stringify(de));
160
192
  return { digest: de, digestAlgId: _algId(de, "absent"), sigAlgId: _algId(ECDSA_ALG[de], "absent"), imp: { name: "ECDSA", namedCurve: ec.curve }, sign: { name: "ECDSA", hash: HASH[de] }, ecdsaDer: true, coordLen: ec.coordLen };
161
193
  }
162
194
  if (keyOid === O("Ed25519") || keyOid === O("Ed448")) {
163
195
  var name = keyOid === O("Ed25519") ? "Ed25519" : "Ed448";
164
196
  var dd = so.digestAlgorithm || (name === "Ed25519" ? "sha512" : "shake256");
165
- if (!NODE_DIGEST[dd]) throw E("unsupported-algorithm", "unsupported " + name + " digest algorithm " + JSON.stringify(dd));
197
+ if (!NODE_DIGEST[dd]) throw E("unsupported-algorithm", "unsupported " + name + " digest algorithm " + _stringify(dd));
166
198
  return { digest: dd, digestAlgId: _algId(dd, "absent"), sigAlgId: _algId(name, "absent"), imp: { name: name }, sign: { name: name }, ecdsaDer: false };
167
199
  }
168
200
  if (MLDSA_BY_OID[keyOid]) {
@@ -172,14 +204,14 @@ function resolveSignScheme(cert, so, noSignedAttrs, E) {
172
204
  md = "sha512";
173
205
  } else {
174
206
  md = so.digestAlgorithm || "sha512";
175
- 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)");
207
+ if (!NODE_DIGEST[md]) throw E("unsupported-algorithm", "unsupported ML-DSA message digest " + _stringify(md));
208
+ 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
209
  }
178
210
  return { digest: md, digestAlgId: _algId(md, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: mlName }, sign: { name: mlName }, ecdsaDer: false };
179
211
  }
180
212
  if (SLHDSA_BY_OID[keyOid]) {
181
213
  var slh = SLHDSA_BY_OID[keyOid];
182
- if (so.digestAlgorithm && so.digestAlgorithm !== slh.digest) throw E("bad-input", "SLH-DSA " + slh.wc + " requires the " + slh.digest + " message digest (RFC 9814 sec. 4); digestAlgorithm " + JSON.stringify(so.digestAlgorithm) + " conflicts");
214
+ if (so.digestAlgorithm && so.digestAlgorithm !== slh.digest) throw E("bad-input", "SLH-DSA " + slh.wc + " requires the " + slh.digest + " message digest (RFC 9814 sec. 4); digestAlgorithm " + _stringify(so.digestAlgorithm) + " conflicts");
183
215
  return { digest: slh.digest, digestAlgId: _algId(slh.digest, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: slh.wc }, sign: { name: slh.wc }, ecdsaDer: false };
184
216
  }
185
217
  throw E("unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
@@ -190,7 +222,7 @@ function _assertKeyMatchesScheme(key, imp, E) {
190
222
  var ka = key.algorithm || {};
191
223
  // WebCrypto algorithm names are ASCII-case-insensitive (RSASSA-PKCS1-v1_5 and RSASSA-PKCS1-V1_5 are one
192
224
  // algorithm), so compare case-folded; a genuine mismatch (e.g. RSASSA-PKCS1-v1_5 vs ECDSA) still fails.
193
- if (String(ka.name).toUpperCase() !== String(imp.name).toUpperCase()) throw E("bad-input", "the signer CryptoKey algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")");
225
+ if (_toUpperCase(_String(ka.name)) !== _toUpperCase(_String(imp.name))) throw E("bad-input", "the signer CryptoKey algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")");
194
226
  if (imp.hash && (!ka.hash || ka.hash.name !== imp.hash)) throw E("bad-input", "the signer CryptoKey hash (" + (ka.hash && ka.hash.name) + ") does not match the signing digest (" + imp.hash + ")");
195
227
  if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw E("bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
196
228
  }
@@ -198,19 +230,19 @@ function _normPkcs8(k, label, E) {
198
230
  // A caller's own Buffer is borrowed, not copied, the same rule _importKey states below. Every
199
231
  // other form is copied, and neither copy is duplicated further, so a private key never gains a
200
232
  // plaintext duplicate this module cannot account for.
201
- if (Buffer.isBuffer(k)) return guard.bytes.view(k, E, "bad-input", label);
202
- if (k instanceof Uint8Array) return guard.bytes.snapshot(k, E, "bad-input", label);
233
+ if (_isBuffer(k)) return guard.bytes.view(k, E, "bad-input", label);
234
+ if (_isU8(k)) return guard.bytes.snapshot(k, E, "bad-input", label);
203
235
  if (typeof k === "string") { try { return pkcs8.pemDecode(k); } catch (e) { throw E("bad-input", label + " PEM could not be decoded", e); } }
204
236
  throw E("bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
205
237
  }
206
238
  function _normCompositeKeys(key, comp, E) {
207
- if (!key || typeof key !== "object" || Buffer.isBuffer(key) || key instanceof Uint8Array || key.mldsa == null || key.trad == null) {
239
+ if (!key || typeof key !== "object" || _isBuffer(key) || _isU8(key) || key.mldsa == null || key.trad == null) {
208
240
  throw E("bad-input", "a composite " + comp.name + " signer key must be { mldsa: <PKCS#8>, trad: <PKCS#8> }");
209
241
  }
210
242
  return { mldsa: _normPkcs8(key.mldsa, "the composite ML-DSA component key", E), trad: _normPkcs8(key.trad, "the composite traditional component key", E) };
211
243
  }
212
244
  function _importKey(key, imp, E) {
213
- if (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type === "private") {
245
+ if (key && typeof key === "object" && !_isBuffer(key) && !(_isU8(key)) && key.type === "private") {
214
246
  _assertKeyMatchesScheme(key, imp, E);
215
247
  // `key` is documented as taking a CryptoKey, so one created outside this engine signs here too --
216
248
  // it is re-imported through this engine, because it carries none of the key material this engine
@@ -222,8 +254,8 @@ function _importKey(key, imp, E) {
222
254
  // only thing that reads it. A caller's own Buffer is passed through untouched: they hold a live
223
255
  // reference and will use it again, so clearing it would destroy their key.
224
256
  var der, owned = false;
225
- if (Buffer.isBuffer(key)) der = guard.bytes.view(key, E, "bad-input", "the signer private key");
226
- else if (key instanceof Uint8Array) { der = guard.bytes.snapshot(key, E, "bad-input", "the signer private key"); owned = true; }
257
+ if (_isBuffer(key)) der = guard.bytes.view(key, E, "bad-input", "the signer private key");
258
+ else if (_isU8(key)) { der = guard.bytes.snapshot(key, E, "bad-input", "the signer private key"); owned = true; }
227
259
  else if (typeof key === "string") {
228
260
  try { der = pkcs8.pemDecode(key); }
229
261
  catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); }
@@ -246,11 +278,11 @@ function _importKey(key, imp, E) {
246
278
  // path signs both component keys and returns mldsaSig || tradSig (composite-sig.js owns it).
247
279
  function signOverTbs(scheme, key, signedBytes, E) {
248
280
  if (scheme.composite) {
249
- return compositeSig.compositeSign(scheme.composite, _normCompositeKeys(key, scheme.composite, E), signedBytes).then(function (sig) { return Buffer.from(sig); });
281
+ return compositeSig.compositeSign(scheme.composite, _normCompositeKeys(key, scheme.composite, E), signedBytes).then(function (sig) { return _bufferFrom(sig); });
250
282
  }
251
283
  return _importKey(key, scheme.imp, E).then(function (priv) {
252
284
  return subtle.sign(scheme.sign, priv, signedBytes).then(function (sigRaw) {
253
- var sig = Buffer.from(sigRaw);
285
+ var sig = _bufferFrom(sigRaw);
254
286
  if (scheme.ecdsaDer) sig = validator.sig.rawToEcdsaDer(sig, scheme.coordLen);
255
287
  return sig;
256
288
  });
@@ -266,5 +298,6 @@ module.exports = {
266
298
  pssSpkiPinnedHash: pssSpkiPinnedHash,
267
299
  signOverTbs: signOverTbs,
268
300
  MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST,
301
+ mldsaDigestSuitable: mldsaDigestSuitable,
269
302
  SLHDSA_BY_OID: SLHDSA_BY_OID,
270
303
  };
package/lib/tsp-sign.js CHANGED
@@ -25,6 +25,7 @@ var pkiX509 = require("./schema-x509");
25
25
  var smime = require("./schema-smime");
26
26
  var schemaTsp = require("./schema-tsp");
27
27
  var guard = require("./guard-all");
28
+ var intrinsic = require("./guard-intrinsic");
28
29
  var frameworkError = require("./framework-error");
29
30
 
30
31
  var pkix = require("./schema-pkix");
@@ -336,7 +337,7 @@ function request(messageImprint, opts) {
336
337
  if (opts.certReq != null && typeof opts.certReq !== "boolean") throw _err("tsp/bad-input", "certReq must be a boolean");
337
338
  if (opts.certReq === true) fields.push(b.boolean(true));
338
339
  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");
340
+ 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
341
  // Each pre-encoded Extension is validated as one, and its extnID must not repeat -- the same
341
342
  // gate every other request builder applies. Spliced in unchecked, a caller relaying a blob it
342
343
  // did not author put fully chosen bytes inside [0] and this encoder emitted DER its own
@@ -507,7 +508,7 @@ var _BIND_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512" };
507
508
  // tsp/* verdict code; a config-shape error throws (the caller's contract).
508
509
  function _imprintMatches(mi, data) {
509
510
  var name = mi.hashAlgorithm && mi.hashAlgorithm.name;
510
- var nodeAlg = name && _VERIFY_DIGEST[name];
511
+ var nodeAlg = (name && intrinsic.hasOwn(_VERIFY_DIGEST, name)) ? _VERIFY_DIGEST[name] : null;
511
512
  if (!nodeAlg) return "tsp/unsupported-algorithm"; // cannot recompute -> never assume a match
512
513
  var wantLen = _VERIFY_DIGEST_LEN[name];
513
514
  // The token's messageImprint MUST be a full-length digest for its algorithm. A short/empty
@@ -732,6 +733,12 @@ function _buildTsaChains(leaf, pool) {
732
733
  // Every option pki.tsp.verify reads. Adding one here is the only way to make it accepted.
733
734
  var _VERIFY_OPTS = { certs: 1, trustAnchor: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1 };
734
735
 
736
+ // Does any extension carry the critical flag? The answer IS the unknown-critical-extension gate, so
737
+ // it is decided by comparison instead of by a `some`. See guard-list.
738
+ function _anyCritical(extensions) {
739
+ return guard.list.anyMatches(extensions, function (e) { return !!e && !!e.critical; });
740
+ }
741
+
735
742
  async function verify(token, data, opts) {
736
743
  opts = opts || {};
737
744
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.verify options must be an object");
@@ -742,7 +749,7 @@ async function verify(token, data, opts) {
742
749
  // nothing to notice it by. Naming the difference at the boundary is the only place it is cheap.
743
750
  guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "tsp/bad-input",
744
751
  "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; }))) {
752
+ if (opts.certs != null && (!Array.isArray(opts.certs) || !guard.list.allMatch(opts.certs, function (c) { return Buffer.isBuffer(c) || c instanceof Uint8Array; }))) {
746
753
  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
754
  }
748
755
  var tokenDer = _tokenDer(token); // DER Buffer / PEM -> DER; a non-token input throws tsp/bad-input
@@ -789,7 +796,9 @@ async function verify(token, data, opts) {
789
796
  // A TSTInfo carrying a CRITICAL extension the verifier cannot process is unusable: RFC 3161 sec.
790
797
  // 2.4.2 defers extension handling to RFC 5280 sec. 4.2, and defines no TSTInfo extension, so any
791
798
  // 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; })) {
799
+ // The scan for a critical extension IS the gate, so it is a loop rather than a `some`: one
800
+ // answering false makes the unrecognized critical extension invisible and the token is accepted.
801
+ if (tst.extensions && _anyCritical(tst.extensions)) {
793
802
  return fail("tsp/unknown-critical-extension");
794
803
  }
795
804
  // 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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.5.19",
3
+ "version": "0.5.21",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:d0064cbc-a37c-4d4f-ae67-0b9ba6887de2",
5
+ "serialNumber": "urn:uuid:353e5368-910e-4a85-a222-14a2add84cee",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-20T21:35:06.293Z",
8
+ "timestamp": "2026-08-21T02:50:54.975Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.5.19",
22
+ "bom-ref": "@blamejs/pki@0.5.21",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.5.19",
25
+ "version": "0.5.21",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.5.19",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.5.21",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.5.19",
57
+ "ref": "@blamejs/pki@0.5.21",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]