@blamejs/pki 0.5.19 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ 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.20 — 2026-08-20
8
+
9
+ 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.
10
+
11
+ ### Added
12
+
13
+ - `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.
14
+
15
+ ### Changed
16
+
17
+ - 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.
18
+
19
+ ### Fixed
20
+
21
+ - `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.
22
+ - `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.
23
+ - 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.
24
+ - 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.
25
+ - 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.
26
+ - `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`.
27
+ - `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).
28
+ - 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.
29
+ - 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.
30
+
7
31
  ## v0.5.19 — 2026-08-20
8
32
 
9
33
  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.
package/lib/cms-verify.js CHANGED
@@ -39,7 +39,9 @@ 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");
@@ -71,7 +73,7 @@ var DIGEST_HASH = {
71
73
  // foreign fault and loses this module's own unsupported-algorithm verdict.
72
74
  var SIG_HASH = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
73
75
  // Is `name` a message-digest algorithm this verifier supports?
74
- function _supportedDigest(name) { return !!DIGEST_HASH[name]; }
76
+ function _supportedDigest(name) { return guard.intrinsic.hasOwn(DIGEST_HASH, name) && !!DIGEST_HASH[name]; }
75
77
  // The digest of `content` under the named algorithm, resolved to a Buffer.
76
78
  function _computeDigest(name, content) {
77
79
  return subtle.digest(DIGEST_HASH[name], content).then(function (d) { return Buffer.from(d); });
@@ -366,7 +368,7 @@ function _verifyOne(si, content, eContentType, parsedCerts, csTarget) {
366
368
  // meet the parameter set's security strength -- a below-strength digest is the weaker link and is
367
369
  // rejected fail-closed (the RFC's "verifiers MAY reject", taken by the strict-verifier posture).
368
370
  // 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)" });
371
+ 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
372
  // RFC 9814 sec. 4: an SLH-DSA SignerInfo's message-digest algorithm is the one paired with the
371
373
  // parameter set (the sec. 4 list, always at least twice the tree-hash size); the signer emits it
372
374
  // and this strict verifier requires it, so a digest that does not match the set's paired hash fails
@@ -768,7 +770,9 @@ function _verify(input, opts) {
768
770
  // message a second time to learn it, and a check that needs a second parse is a check most
769
771
  // callers will not write.
770
772
  var res = {
771
- valid: signers.length > 0 && signers.every(function (s) { return s.ok === true; }),
773
+ // Signature soundness is decided by comparison for the same reason the trust verdict below
774
+ // is: `every` replaced after load reports the whole set sound without reading one signer.
775
+ valid: signers.length > 0 && guard.list.allMatch(signers, function (s) { return s.ok === true; }),
772
776
  eContentType: parsed.encapContentInfo.eContentType,
773
777
  signers: signers,
774
778
  };
@@ -873,6 +877,12 @@ function _keyUsagePermitsSigning(parsedCerts, der) {
873
877
  return true; // absent: unconstrained (sec. 4.2.1.3)
874
878
  }
875
879
 
880
+ // Every signer trusted? The answer IS the message-level trust verdict, so it is decided by
881
+ // comparison instead of by an `every`. See guard-list.
882
+ function _allTrusted(signers) {
883
+ return guard.list.allMatch(signers, function (s) { return s.trusted === true; });
884
+ }
885
+
876
886
  // `cfg` is verify's SNAPSHOT, never the caller's options object -- the parameter is named for that
877
887
  // so a future caller cannot hand it the live one without noticing.
878
888
  function _applyTrust(res, parsedCerts, cfg) {
@@ -963,7 +973,10 @@ function _applyTrust(res, parsedCerts, cfg) {
963
973
  });
964
974
  });
965
975
  }, Promise.resolve()).then(function () {
966
- res.trusted = res.valid && res.signers.length > 0 && res.signers.every(function (s) { return s.trusted === true; });
976
+ // The trust verdict is decided by comparison, not by a prototype method: `every` replaced after
977
+ // load answers true without consulting a single signer, and a message no anchor covers reports
978
+ // trusted:true.
979
+ res.trusted = res.valid && res.signers.length > 0 && _allTrusted(res.signers);
967
980
  });
968
981
  }
969
982
  // 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");
@@ -74,6 +75,7 @@ module.exports = {
74
75
  text: text,
75
76
  limits: limits,
76
77
  crypto: crypto,
78
+ list: list,
77
79
  range: range,
78
80
  time: time,
79
81
  name: name,
@@ -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
+ };
@@ -45,6 +45,9 @@ OCSP_CERTID_HASHES[oid.byName("sha256")] = "SHA-256";
45
45
  OCSP_CERTID_HASHES[oid.byName("sha384")] = "SHA-384";
46
46
  OCSP_CERTID_HASHES[oid.byName("sha512")] = "SHA-512";
47
47
  var OID_OCSP_SIGNING = oid.byName("ocspSigning");
48
+ // Does this extended-key-usage list assert the given purpose? The answer IS an authorization
49
+ // decision, so it goes through the membership guard rather than a prototype method. See guard-list.
50
+ var _asserts = guard.list.contains;
48
51
  var OID_OCSP_NOCHECK = oid.byName("ocspNoCheck");
49
52
  var OID_EKU = oid.byName("extKeyUsage");
50
53
  var OID_KEY_USAGE = oid.byName("keyUsage");
@@ -105,7 +108,7 @@ function makeOcspVerify(deps) {
105
108
  // 5280 sec. 7.1-equal byte encoding of the validated issuer DN to try.
106
109
  async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
107
110
  if (certID.serialNumberHex !== cert.serialNumberHex) return false;
108
- var hashName = OCSP_CERTID_HASHES[certID.hashAlgorithm.oid];
111
+ var hashName = guard.intrinsic.hasOwn(OCSP_CERTID_HASHES, certID.hashAlgorithm.oid) ? OCSP_CERTID_HASHES[certID.hashAlgorithm.oid] : null;
109
112
  if (!hashName) return false;
110
113
  var keyHash = await ocspDigest(hashName, issuerKeyBits);
111
114
  if (!certID.issuerKeyHash.equals(keyHash)) return false;
@@ -154,7 +157,11 @@ function makeOcspVerify(deps) {
154
157
  var eku;
155
158
  try { eku = decodeExt(rc, OID_EKU); }
156
159
  catch (_e) { continue; }
157
- if (!eku || eku.value.indexOf(OID_OCSP_SIGNING) === -1) continue;
160
+ // The membership test IS the delegation rule, so it is decided by comparison rather than by a
161
+ // prototype method: dispatched live, an `Array.prototype.indexOf` replaced after load reports
162
+ // the purpose present on ANY certificate, and a cert an attacker put in the response's own
163
+ // certs bag is accepted as the authorized responder.
164
+ if (!eku || !_asserts(eku.value, OID_OCSP_SIGNING)) continue;
158
165
  // A delegate asserting keyUsage MUST permit digitalSignature; an absent keyUsage is
159
166
  // unrestricted, an unreadable one is not authoritative.
160
167
  var ku;
@@ -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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.5.19",
3
+ "version": "0.5.20",
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:e979b712-f27c-4c30-ba8e-4e9ab88715fd",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-20T21:35:06.293Z",
8
+ "timestamp": "2026-08-20T23:29:06.438Z",
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.20",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.5.19",
25
+ "version": "0.5.20",
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.20",
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.20",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]