@blamejs/pki 0.7.0 → 0.7.2

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,22 @@ 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.7.2 — 2026-09-10
8
+
9
+ A PKCS#12 iteration cap is read once, so it cannot be answered with a value that is not a number.
10
+
11
+ ### Security
12
+
13
+ - pki.pkcs12.open and pki.pkcs12.verifyMac hold a store to opts.maxIterations however that option is supplied. The option is the caller's own bound on key-derivation work, and it was read once for each part of its shape check and again by every later pass that applies it: the message authentication code, and then each bag the store carries. An option reached through an accessor answers each read separately, so one that returned a valid integer to the shape check and a non-numeric value to a later comparison left the cap as NaN. No count is above NaN, so the comparison stopped refusing anything and a store demanding far more work than the caller allowed was accepted and its keys derived. A store carrying no message authentication code reaches this through its bags alone, so opening one with opts.allowUnauthenticated was affected even though the authenticated path was not consulted. Both verbs now take the whole options object once, at entry, and every pass reads that one copy, which closes the same door for every option either verb accepts rather than for this one alone. A caller passing options as plain values was never affected. The same read-once shape is applied to the PBES2 and CMP iteration caps, where no route was found that reaches them this way.
14
+
15
+ ## v0.7.1 — 2026-09-10
16
+
17
+ A wildcard name in a certificate is held to an excluded name constraint it can reach.
18
+
19
+ ### Security
20
+
21
+ - pki.path.validate refuses a certificate whose wildcard dNSName reaches a name an excluded subtree forbids. An excluded subtree states the names a certificate must not present, and a wildcard subject alternative name stands for every name one label below its parent rather than for the literal text. Comparing the text alone let a leaf carrying only *.example.com pass an exclusion of bar.example.com, which is a name that leaf presents. Any chain whose certificate authority relies on an excluded dNSName subtree to keep a subordinate away from a name was affected, in every version that shipped name-constraint validation. The permitted direction is unchanged and is not widened by this: a permitted subtree still has to cover the whole of a wildcard, so a base of foo.com admits *.foo.com while a base of bar.example.com does not admit *.example.com. A wildcard reaches exactly one label, so an excluded name deeper than that is out of its reach and is not refused: *.example.com is still admitted against an exclusion of deep.bar.example.com.
22
+
7
23
  ## v0.7.0 — 2026-09-09
8
24
 
9
25
  A Sigstore bundle signed over an artifact's own bytes is verified, not just one wrapping an attestation.
package/lib/cmp-verify.js CHANGED
@@ -535,11 +535,15 @@ async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
535
535
 
536
536
  function _capWork(iterationCount, salt, keyLength, prfHash, opts) {
537
537
  var cap = PBMAC1_MAX_ITER;
538
- if (opts.maxIterations != null) {
539
- if (typeof opts.maxIterations !== "number" || !_isFinite(opts.maxIterations) || opts.maxIterations < 1 || _floor(opts.maxIterations) !== opts.maxIterations) {
538
+ /** @internal Read once: the shape check and the comparison must read the same answer, or an
539
+ * option reached through an accessor passes as a number and then answers the comparison with
540
+ * something that is not one, leaving the cap NaN and bounding nothing. */
541
+ var wantedIter = opts.maxIterations;
542
+ if (wantedIter != null) {
543
+ if (typeof wantedIter !== "number" || !_isFinite(wantedIter) || wantedIter < 1 || _floor(wantedIter) !== wantedIter) {
540
544
  throw _err("cmp/bad-input", "opts.maxIterations must be a positive integer");
541
545
  }
542
- cap = _min(opts.maxIterations, cap);
546
+ cap = _min(wantedIter, cap);
543
547
  }
544
548
  if (iterationCount < PBMAC1_MIN_ITER) throw _err("cmp/bad-input", "the PBMAC1 iterationCount " + iterationCount + " is below the floor " + PBMAC1_MIN_ITER + " (RFC 8018 sec. 4.2)");
545
549
  if (iterationCount > cap) throw _err("cmp/bad-input", "the PBMAC1 iterationCount " + iterationCount + " exceeds the cap " + cap);
@@ -52,6 +52,8 @@ var cmpSession = require("./cmp-session");
52
52
  var guard = require("./guard-all");
53
53
  var intrinsic = require("./guard-intrinsic");
54
54
  var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
55
+ var _strSlice = intrinsic.uncurry(String.prototype.slice);
56
+ var _strIndexOf = intrinsic.uncurry(String.prototype.indexOf);
55
57
  var _resolve = intrinsic.uncurry(intrinsic.promiseResolve);
56
58
  var constants = require("./constants");
57
59
  var validator = require("./validator-all");
@@ -420,11 +422,28 @@ function emailMatch(constraint, mailbox) {
420
422
 
421
423
  var stripTrailingDot = guard.name.stripTrailingDot;
422
424
 
423
- function hostConstraintMatch(constraint, host) {
425
+ /** @internal `reachIntoBase` asks the excluded question rather than the permitted one. A wildcard
426
+ * dNSName stands for every name one label below its parent, so it is a set rather than a name. A
427
+ * permitted subtree asks whether that whole set stays inside the base, which the literal comparison
428
+ * already answers: `*.foo.com` ends with `.foo.com`, so a base of `foo.com` covers all of it. An
429
+ * excluded subtree asks the other question, whether any member of the set falls inside the base, and
430
+ * the literal comparison answers it in the fail-open direction: `*.example.com` is not the string
431
+ * `bar.example.com`, so an exclusion of `bar.example.com` admitted a certificate that presents
432
+ * exactly the name the exclusion forbids. Widening this for the permitted direction would be the
433
+ * fail-open mirror, so it is asked for only where the caller wants reach. A wildcard reaches exactly
434
+ * one label, so a base below the wildcard's parent is reached only when it is one label below it. */
435
+ function hostConstraintMatch(constraint, host, reachIntoBase) {
424
436
  var c = stripTrailingDot(constraint.toLowerCase()), h = stripTrailingDot(host.toLowerCase());
425
437
  if (c === "") return true;
426
438
  if (c.charAt(0) === ".") return h.length > c.length && h.slice(-c.length) === c;
427
- return h === c || (h.length > c.length && h.slice(-(c.length + 1)) === "." + c);
439
+ if (h === c || (h.length > c.length && h.slice(-(c.length + 1)) === "." + c)) return true;
440
+ if (reachIntoBase === true && h.length > 2 && _charCodeAt(h, 0) === 42 && _charCodeAt(h, 1) === 46) {
441
+ var parent = _strSlice(h, 2);
442
+ if (parent.length > 0 && c.length > parent.length && _strSlice(c, -(parent.length + 1)) === "." + parent) {
443
+ return _strIndexOf(_strSlice(c, 0, c.length - parent.length - 1), ".") === -1;
444
+ }
445
+ }
446
+ return false;
428
447
  }
429
448
 
430
449
  function _charTable(chars) { var t = []; for (var i = 0; i < chars.length; i++) t[_charCodeAt(chars, i)] = true; return t; }
@@ -491,14 +510,14 @@ function ipMatch(constraint, addr) {
491
510
  * certificate carries in its own nameConstraints extension met no door, and the same rule decides it
492
511
  * here. Answering a base outside its tag's form with no match rather than no verdict would make an
493
512
  * excluded subtree that covers nothing exclude nothing. */
494
- function nameMatchesConstraint(gnTag, gnValue, base) {
513
+ function nameMatchesConstraint(gnTag, gnValue, base, reachIntoBase) {
495
514
  if (base.tagNumber !== gnTag) return null;
496
515
  if (gnTag === 1 || gnTag === 2 || gnTag === 6) {
497
516
  if (guard.name.constraintBaseUnreadable(gnTag, base.value)) return "unsupported";
498
517
  }
499
518
  switch (gnTag) {
500
519
  case 1: return emailMatch(base.value, gnValue);
501
- case 2: return hostConstraintMatch(base.value, gnValue);
520
+ case 2: return hostConstraintMatch(base.value, gnValue, reachIntoBase);
502
521
  case 6: return uriMatch(base.value, gnValue);
503
522
  case 7: return ipMatch(base.value, gnValue);
504
523
  case 4: return dnStartsWith(gnValue, base.value);
@@ -540,7 +559,7 @@ function checkNameConstraints(state, cert) {
540
559
  for (var e = 0; e < state.excludedSubtrees.length; e++) {
541
560
  var ex = state.excludedSubtrees[e];
542
561
  for (var i = 0; i < forms.length; i++) {
543
- var m = nameMatchesConstraint(forms[i].tag, forms[i].value, { tagNumber: ex.tag, value: ex.base });
562
+ var m = nameMatchesConstraint(forms[i].tag, forms[i].value, { tagNumber: ex.tag, value: ex.base }, true);
544
563
  if (m === true) return { ok: false, code: "path/name-constraint-excluded" };
545
564
  if (m === "unsupported") return { ok: false, code: "path/name-constraint-unsupported" };
546
565
  }
package/lib/pbes2.js CHANGED
@@ -74,9 +74,13 @@ function parsePbkdf2Params(paramsDer, opts, E, prefix, strictPrf) {
74
74
  if (salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw E(prefix + "/bad-input", "PBKDF2 salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
75
75
  var iterations = guard.range.positiveInt31(asn1.read.integer(kids[1]), E, prefix + "/bad-input", "PBKDF2 iterationCount");
76
76
  var cap = C.LIMITS.PBKDF2_MAX_ITERATIONS;
77
- if (opts && opts.maxIterations != null) {
78
- if (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations) throw E(prefix + "/bad-input", "maxIterations must be a positive integer");
79
- cap = Math.min(opts.maxIterations, cap);
77
+ /** @internal Read once: the shape check and the comparison must read the same answer, or an
78
+ * option reached through an accessor passes as a number and then answers the comparison with
79
+ * something that is not one, leaving the cap NaN and bounding nothing. */
80
+ var wantedIter = opts ? opts.maxIterations : null;
81
+ if (wantedIter != null) {
82
+ if (typeof wantedIter !== "number" || !isFinite(wantedIter) || wantedIter < 1 || Math.floor(wantedIter) !== wantedIter) throw E(prefix + "/bad-input", "maxIterations must be a positive integer");
83
+ cap = Math.min(wantedIter, cap);
80
84
  }
81
85
  if (iterations > cap) throw E(prefix + "/iteration-limit", "PBKDF2 iterationCount " + iterations + " exceeds the cap " + cap);
82
86
  var prfNode = "sha1";
@@ -552,6 +552,8 @@ async function _build(spec, opts) {
552
552
  async function verifyMac(pfx, password, opts) {
553
553
  if (opts != null && typeof opts === "object" && !Buffer.isBuffer(opts)) {
554
554
  guard.identifier.assertKnownKeys(opts, _VERIFY_MAC_OPTS, _err, "pkcs12/bad-input", "pki.pkcs12.verifyMac has an unknown option ");
555
+ /** @internal Taken once here, for the reason the same call in `open` states. */
556
+ opts = guard.identifier.snapshotOptions(opts, _VERIFY_MAC_OPTS);
555
557
  }
556
558
  var m = _storeFromBytes(pfx);
557
559
  var valid = await _verifyMacOfStore(m, password, opts);
@@ -593,11 +595,16 @@ async function _verifyMacOfStore(m, password, opts) {
593
595
  return computed.length === expected.length && guard.crypto.constantTimeEqual(computed, expected);
594
596
  }
595
597
 
598
+ /** @internal The caller's bound is read once and the shape check and the comparison both read that
599
+ * one answer. Read separately, an option reached through an accessor satisfies every check as a
600
+ * number and then answers the comparison with something that is not one, so `Math.min` yields NaN.
601
+ * Nothing compares above NaN, so the cap stopped bounding the work it exists to bound. */
596
602
  function _capWork(iterations, salt, opts, keyLength, hardCap) {
597
603
  var cap = hardCap;
598
- if (opts.maxIterations != null) {
599
- if (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations) throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
600
- cap = Math.min(opts.maxIterations, cap);
604
+ var wanted = opts.maxIterations;
605
+ if (wanted != null) {
606
+ if (typeof wanted !== "number" || !isFinite(wanted) || wanted < 1 || Math.floor(wanted) !== wanted) throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
607
+ cap = Math.min(wanted, cap);
601
608
  }
602
609
  if (iterations > cap) throw _err("pkcs12/iteration-limit", "the MAC iteration count " + iterations + " exceeds the cap " + cap);
603
610
  if (salt && salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw _err("pkcs12/bad-input", "the MAC salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
@@ -653,6 +660,12 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
653
660
  async function open(pfx, password, opts) {
654
661
  opts = opts || {};
655
662
  guard.identifier.assertKnownKeys(opts, _OPEN_OPTS, _err, "pkcs12/bad-input", "pki.pkcs12.open has an unknown option ");
663
+ /** @internal Every option is taken once, here, and everything below reads the snapshot. A store
664
+ * is opened in many passes, the MAC and then each bag, so an option left on the caller's object is
665
+ * read again by every one of them. An option reached through an accessor answers each read
666
+ * separately: one that returned a valid integer to this check and something that is not a number
667
+ * to a later bag left the work cap as NaN, and no count is above NaN. */
668
+ opts = guard.identifier.snapshotOptions(opts, _OPEN_OPTS);
656
669
  if (opts.maxIterations != null &&(typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
657
670
  throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
658
671
  }
@@ -732,7 +745,8 @@ function _decryptLegacyPbe(ea, ct, password, opts, budget) {
732
745
  if (e && e.isPkcs12Error) throw e;
733
746
  throw _err("pkcs12/bad-algorithm-parameters", "malformed pkcs-12PbeParams (RFC 7292 App. C)", e);
734
747
  }
735
- var cap = opts.maxIterations != null ? Math.min(opts.maxIterations, CLASSIC_MAC_MAX_ITERATIONS) : CLASSIC_MAC_MAX_ITERATIONS;
748
+ var wantedLegacy = opts.maxIterations;
749
+ var cap = wantedLegacy != null ? Math.min(wantedLegacy, CLASSIC_MAC_MAX_ITERATIONS) : CLASSIC_MAC_MAX_ITERATIONS;
736
750
  if (iterations > cap) throw _err("pkcs12/iteration-limit", "the legacy PBE iteration count " + iterations + " exceeds the cap " + cap);
737
751
  if (salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw _err("pkcs12/bad-input", "the legacy PBE salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
738
752
  var u = P12_KDF_UV[scheme.hash].u;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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:fb4bc11f-cd39-480a-b276-ec20713541e7",
5
+ "serialNumber": "urn:uuid:3b58cd3c-2ea1-4d00-9d93-8181e4a421b2",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-10T03:36:52.727Z",
8
+ "timestamp": "2026-09-10T07:33:50.131Z",
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.7.0",
22
+ "bom-ref": "@blamejs/pki@0.7.2",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.7.0",
25
+ "version": "0.7.2",
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.7.0",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.7.2",
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.7.0",
57
+ "ref": "@blamejs/pki@0.7.2",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]