@blamejs/pki 0.6.37 → 0.6.38

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,23 @@ 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.6.38 — 2026-09-05
8
+
9
+ A trust anchor can name the namespace its root is trusted for, and pki.path.validate enforces it as the initial name-constraint state.
10
+
11
+ ### Added
12
+
13
+ - A trust anchor passed to pki.path.validate or pki.path.build may carry nameConstraints: { permitted, excluded }, each an array of { tag, base } subtrees. They seed the RFC 5280 section 6.1.1(h)(i) initial permitted and excluded state, so a root a program trusts only for a namespace enforces that namespace even though its certificate states no nameConstraints extension. The anchor's subtrees intersect with an intermediate's own extension and with opts.initialPermittedSubtrees; excluded subtrees are a union.
14
+ - The pki.path.validate verdict reports anchorConstraints.nameConstraintsApplied, so a stored verdict distinguishes an anchor that named a namespace from one that did not, alongside the distrustAfterApplied and purposeTrustApplied it already carries.
15
+
16
+ ### Fixed
17
+
18
+ - pki.path.validate and pki.path.build copy a directoryName subtree base and snapshot an iPAddress one before validating, and read opts.initialPermittedSubtrees and opts.initialExcludedSubtrees once rather than again for each trust anchor or candidate path tried. A caller mutating its own subtree list, entry or base while the path is still verifying signatures no longer changes the namespace the validation enforces; the subtrees that bind are the ones read at entry.
19
+ - A directoryName subtree base whose rdns is an accessor rather than a data property is refused. Its value was read once to check its shape and again to copy it, so an accessor could answer with a restrictive name the first time and an empty one the second, and an empty directoryName constraint matches every name.
20
+ - A subtree list that is a Proxy, that is sparse, or whose entries are accessors is refused, on a trust anchor and on opts.initialPermittedSubtrees and opts.initialExcludedSubtrees alike. Such a list answers its own length and index reads, so it could hold a restrictive subtree while reporting none and leave the namespace unenforced. A directoryName subtree is built from property descriptors alone, so nothing the caller supplies runs while the namespace is read: an accessor, own or inherited, is refused rather than invoked. It is a relative-name sequence of attribute lists and nothing deeper, so an array nested past that, including one that refers to itself, is a typed refusal, as is a hole, an entry the array does not own, an attribute that is not an object, or one lacking its own type and value. Each of those refusals is a configuration error reported wherever the anchor sits among several, rather than a name mismatch a different anchor could mask.
21
+ - A parsed certificate passed as a trust anchor is refused if it carries nameConstraints, the way one carrying purposes or distrustAfter already is. A certificate anchor cannot hold that metadata, so accepting it would have dropped the namespace and validated a leaf the anchor was meant to exclude.
22
+ - An anchor whose nameConstraints names a field other than permitted or excluded is refused rather than read as unconstrained, and its subtrees are checked while the anchor is normalized, so a malformed one is refused wherever that anchor sits among several rather than only once it is reached.
23
+
7
24
  ## v0.6.37 — 2026-09-05
8
25
 
9
26
  A natively signed C509 certificate is refused when it writes an extension in the generic OID form that the registry can carry as an identifier.
@@ -553,19 +553,61 @@ function isSubtreeBaseValid(tag, base) {
553
553
 
554
554
  function checkedSubtreeSeeds(list, optName) {
555
555
  if (list === undefined || list === null) return [];
556
- if (!intrinsic.isArray(list)) throw E("path/bad-input", "validate: opts." + optName + " must be an array of { tag, base } subtree entries");
557
- return list.map(function (st) {
558
- if (!st || typeof st !== "object" || !Number.isInteger(st.tag) || st.tag < 0 || st.tag > 8) {
559
- throw E("path/bad-input", "validate: opts." + optName + " entries must be { tag: <GeneralName tag number 0..8>, base: <that form's constraint value> }");
556
+ if (!intrinsic.isArray(list) || intrinsic.types.isProxy(list)) throw E("path/bad-input", "validate: opts." + optName + " must be a plain array of { tag, base } subtree entries");
557
+ for (var li = 0; li < list.length; li++) {
558
+ var ld = intrinsic.getOwnPropertyDescriptor(list, li);
559
+ if (ld === undefined) throw E("path/bad-input", "validate: opts." + optName + " must not be a sparse array; index " + li + " is a hole");
560
+ if (!intrinsic.hasOwn(ld, "value")) throw E("path/bad-input", "validate: opts." + optName + " entry " + li + " must be a data property, not an accessor");
561
+ }
562
+ /** @internal Built with a plain loop into a fresh array: Array.prototype.map consults the caller
563
+ * array's constructor and Symbol.species, which is more caller code running mid-copy. */
564
+ var seedsOut = [];
565
+ for (var si = 0; si < list.length; si++) {
566
+ intrinsic.push(seedsOut, (function (st) {
567
+ var shapeMsg = "validate: opts." + optName + " entries must be { tag: <GeneralName tag number 0..8>, base: <that form's constraint value> }";
568
+ if (!st || typeof st !== "object" || intrinsic.types.isProxy(st)) throw E("path/bad-input", shapeMsg);
569
+ /** @internal Read tag and base once each, from their own data properties: an accessor could
570
+ * answer with a restrictive value to a check and a permissive one to the copy. */
571
+ var tagD = intrinsic.getOwnPropertyDescriptor(st, "tag");
572
+ var baseD = intrinsic.getOwnPropertyDescriptor(st, "base");
573
+ if (tagD !== undefined && !intrinsic.hasOwn(tagD, "value")) throw E("path/bad-input", "validate: opts." + optName + " entry tag must be a data property, not an accessor");
574
+ if (baseD !== undefined && !intrinsic.hasOwn(baseD, "value")) throw E("path/bad-input", "validate: opts." + optName + " entry base must be a data property, not an accessor");
575
+ var tag = tagD === undefined ? undefined : tagD.value;
576
+ var base = baseD === undefined ? undefined : baseD.value;
577
+ if (!Number.isInteger(tag) || tag < 0 || tag > 8) throw E("path/bad-input", shapeMsg);
578
+ if (tag === 7 && guard.bytes.isByteSource(base)) {
579
+ base = guard.bytes.snapshotSource(base, PathError, "path/bad-input", "an iPAddress subtree base");
560
580
  }
561
- var base = (st.tag === 7 && guard.bytes.isByteSource(st.base))
562
- ? guard.bytes.snapshotSource(st.base, PathError, "path/bad-input", "an iPAddress subtree base")
563
- : st.base;
564
- if (!isSubtreeBaseValid(st.tag, base)) {
565
- throw E("path/bad-input", "validate: opts." + optName + " entries must be { tag: <GeneralName tag number 0..8>, base: <that form's constraint value> }");
581
+ if (tag === 4 && base !== null && typeof base === "object") {
582
+ var seedRdns = _capturedRdns(base, "validate: opts." + optName);
583
+ if (seedRdns !== undefined) {
584
+ var dnCopy = {};
585
+ intrinsic.defineProperty(dnCopy, "rdns", { value: seedRdns, enumerable: true, configurable: true, writable: true });
586
+ base = dnCopy;
587
+ }
566
588
  }
567
- return { tag: st.tag, base: base };
568
- });
589
+ if (!isSubtreeBaseValid(tag, base)) throw E("path/bad-input", shapeMsg);
590
+ return { tag: tag, base: base };
591
+ })(intrinsic.getOwnPropertyDescriptor(list, si).value));
592
+ }
593
+ return seedsOut;
594
+ }
595
+
596
+ /**
597
+ * @internal RFC 5280 sec. 6.1.1(h)(i): the anchor's own permitted/excluded subtrees, the way a root
598
+ * program restricts a root to a namespace its certificate does not state. Validated through the same
599
+ * entry-point checker the caller's seeds use, so a mis-shaped subtree is refused rather than dropped.
600
+ */
601
+ function _anchorSubtreeSeeds(anchor) {
602
+ var nc = anchor.nameConstraints;
603
+ if (nc === undefined || nc === null) return { permitted: [], excluded: [] };
604
+ if (typeof nc !== "object" || intrinsic.isArray(nc)) {
605
+ throw E("path/bad-input", "validate: a trustAnchor nameConstraints must be a { permitted, excluded } object of { tag, base } subtree entries");
606
+ }
607
+ return {
608
+ permitted: checkedSubtreeSeeds(nc.permitted, "trustAnchors[].nameConstraints.permitted"),
609
+ excluded: checkedSubtreeSeeds(nc.excluded, "trustAnchors[].nameConstraints.excluded"),
610
+ };
569
611
  }
570
612
 
571
613
  function initialize(certs, params, seeds, anchor) {
@@ -574,7 +616,7 @@ function initialize(certs, params, seeds, anchor) {
574
616
  validPolicyTree: rootNode(),
575
617
  policyNodeCount: 1,
576
618
  maxPolicyNodes: params.maxPolicyNodes !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
577
- permittedGenerations: seeds.permitted.length ? [seeds.permitted] : [],
619
+ permittedGenerations: intrinsic.filter(seeds.permittedGenerations, function (g) { return g.length > 0; }),
578
620
  excludedSubtrees: seeds.excluded,
579
621
  explicitPolicy: params.initialExplicitPolicy ? 0 : n + 1,
580
622
  inhibitAnyPolicy: params.initialAnyPolicyInhibit ? 0 : n + 1,
@@ -867,10 +909,21 @@ function validateCriticalExtensionStructure(cert) {
867
909
  *
868
910
  * `anchorConstraints` reports what the anchor's own trust metadata decided:
869
911
  * the `checkedPurpose` it was judged under, and whether the `distrustAfter`
870
- * date and the `purposes` delegator map each applied. That metadata is keyed BY
871
- * key purpose, so an anchor carrying it while `opts.checkPurpose` is absent is a
872
- * configuration fault (`path/bad-input`). A silently inert constraint would let
873
- * a root distrusted years ago quietly validate a current leaf.
912
+ * date, the `purposes` delegator map and the `nameConstraints` namespace each
913
+ * applied. The purpose-keyed metadata is keyed BY key purpose, so an anchor
914
+ * carrying it while `opts.checkPurpose` is absent is a configuration fault
915
+ * (`path/bad-input`). A silently inert constraint would let a root distrusted
916
+ * years ago quietly validate a current leaf.
917
+ *
918
+ * An anchor may also carry `nameConstraints`, a `{ permitted, excluded }` pair
919
+ * of `{ tag, base }` subtrees naming the namespace that root is trusted for.
920
+ * A root program restricts a root this way when the restriction appears in no
921
+ * `nameConstraints` extension the root certificate carries. The subtrees become
922
+ * the RFC 5280 sec. 6.1.1(h)(i) initial permitted and excluded state, so an
923
+ * intermediate's own `nameConstraints` extension intersects with them and a leaf
924
+ * must satisfy both. They are a separate generation from
925
+ * `opts.initialPermittedSubtrees`, so those two intersect as well. A mis-shaped
926
+ * subtree is refused with `path/bad-input` rather than left inert.
874
927
  *
875
928
  * @example
876
929
  * var pair = await pki.key.generate("Ed25519");
@@ -938,17 +991,35 @@ async function validate(path, opts) {
938
991
  var certs = path.map(function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
939
992
  var n = certs.length;
940
993
  if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
994
+ /** @internal Captured before any anchor is normalized: an anchor field may be an accessor, and
995
+ * that getter runs caller code which could otherwise clear these lists first. */
996
+ var callerPermitted = checkedSubtreeSeeds(opts.initialPermittedSubtrees, "initialPermittedSubtrees");
997
+ var callerExcluded = checkedSubtreeSeeds(opts.initialExcludedSubtrees, "initialExcludedSubtrees");
941
998
  var rawAnchors = opts.trustAnchors;
942
999
  var anchorList = intrinsic.isArray(rawAnchors) ? rawAnchors : [rawAnchors];
943
1000
  if (rawAnchors == null || anchorList.length === 0) throw E("path/bad-input", "validate: trustAnchors is required (a single anchor or a non-empty array of anchors)");
1001
+ /** @internal Read the caller's list once here, whatever its length, so neither branch below reads
1002
+ * one of its indices twice. */
1003
+ if (intrinsic.isArray(rawAnchors)) anchorList = _ownAnchorEntries(rawAnchors, "validate");
944
1004
  if (anchorList.length > 1) {
1005
+ var anchorEntries = anchorList;
1006
+ var preNc = _precaptureAnchorNameConstraints(anchorEntries, "validate");
945
1007
  var tuples = [];
946
- for (var ti = 0; ti < anchorList.length; ti++) { tuples[ti] = toAnchor(anchorList[ti], "validate"); }
1008
+ for (var ti = 0; ti < anchorEntries.length; ti++) {
1009
+ tuples[ti] = toAnchor(anchorEntries[ti], "validate");
1010
+ if (preNc[ti] !== undefined) intrinsic.defineProperty(tuples[ti], "nameConstraints", { value: preNc[ti], enumerable: true, configurable: true, writable: true });
1011
+ }
1012
+ /** @internal Each anchor is a separate attempt and the later ones run after an await, so the
1013
+ * caller's own subtree lists are read once here rather than again per attempt. */
1014
+ var permRawOnce = opts.initialPermittedSubtrees;
1015
+ var exclRawOnce = opts.initialExcludedSubtrees;
947
1016
  var singleOpts = function (a) {
948
1017
  var keys = intrinsic.ownKeys(opts);
949
1018
  var o = {};
950
1019
  for (var ki = 0; ki < keys.length; ki++) { o[keys[ki]] = opts[keys[ki]]; }
951
1020
  o.trustAnchors = a;
1021
+ if (permRawOnce != null) o.initialPermittedSubtrees = callerPermitted;
1022
+ if (exclRawOnce != null) o.initialExcludedSubtrees = callerExcluded;
952
1023
  return o;
953
1024
  };
954
1025
  var lastMatched = null, anyMatched = false;
@@ -965,10 +1036,12 @@ async function validate(path, opts) {
965
1036
  guard.time.assertValid(opts.time, E, "path/bad-input", "validate: opts.time (the always-on validity-window check date)");
966
1037
  guard.limits.cap(opts.maxPolicyNodes, "validate: opts.maxPolicyNodes", undefined, { E: E, code: "path/bad-input", min: 1 });
967
1038
  _assertUserInitialPolicySet(opts.userInitialPolicySet, "validate");
1039
+ var anchorNc = _anchorSubtreeSeeds(anchor);
968
1040
  var seeds = {
969
- permitted: checkedSubtreeSeeds(opts.initialPermittedSubtrees, "initialPermittedSubtrees"),
970
- excluded: checkedSubtreeSeeds(opts.initialExcludedSubtrees, "initialExcludedSubtrees"),
1041
+ permittedGenerations: [callerPermitted, anchorNc.permitted],
1042
+ excluded: intrinsic.concat(callerExcluded, anchorNc.excluded),
971
1043
  };
1044
+ var anchorNameConstraintsApplied = anchorNc.permitted.length > 0 || anchorNc.excluded.length > 0;
972
1045
  var purposeOpts = resolvePurposeOpts(opts);
973
1046
  var requiredEku = purposeOpts.requiredEku;
974
1047
  var checkPurpose = purposeOpts.checkPurpose;
@@ -1158,6 +1231,7 @@ async function validate(path, opts) {
1158
1231
  checkedPurpose: checkPurpose || null,
1159
1232
  distrustAfterApplied: anchorDistrustApplied,
1160
1233
  purposeTrustApplied: anchorPurposeApplied,
1234
+ nameConstraintsApplied: anchorNameConstraintsApplied,
1161
1235
  },
1162
1236
  path: certs,
1163
1237
  results: state.results,
@@ -1903,6 +1977,198 @@ function _captureOwnData(entry, key) {
1903
1977
  return (d && intrinsic.hasOwn(d, "value")) ? d.value : undefined;
1904
1978
  }
1905
1979
 
1980
+ /**
1981
+ * @internal Copy the anchor's subtree lists getter-free, so a caller mutating the entry afterwards
1982
+ * cannot widen the namespace the validation already resolved.
1983
+ */
1984
+ /**
1985
+ * @internal Read a directoryName base's `rdns` once, from its own data property. Reading it twice,
1986
+ * to test its shape and then to copy it, lets an accessor answer differently each time and hand the
1987
+ * validation an empty DN, which matches every name.
1988
+ */
1989
+ var _ATV_KEYS = ["type", "value", "name"];
1990
+ /**
1991
+ * @internal Build the directoryName's own copy from property descriptors alone, so no caller code
1992
+ * runs while the namespace is read: a getter, own or inherited, could clear another part of the
1993
+ * namespace before it is captured. A directoryName is a relative-name sequence of attribute lists
1994
+ * and nothing deeper, so exactly those levels are walked and a self-referencing array is refused
1995
+ * rather than followed.
1996
+ */
1997
+ function _copyDnRdns(rdns, who) {
1998
+ _assertPlainDnRdns(rdns, who);
1999
+ var out = [];
2000
+ for (var i = 0; i < rdns.length; i++) {
2001
+ var rd = intrinsic.getOwnPropertyDescriptor(rdns, i);
2002
+ if (rd === undefined) throw E("path/bad-input", who + " index " + i + " is a hole or is inherited; every entry must be its own");
2003
+ var rdn = rd.value;
2004
+ if (!intrinsic.isArray(rdn)) throw E("path/bad-input", who + "[" + i + "] must be an array of attributes");
2005
+ var rdnOut = [];
2006
+ for (var j = 0; j < rdn.length; j++) {
2007
+ var ad = intrinsic.getOwnPropertyDescriptor(rdn, j);
2008
+ if (ad === undefined) throw E("path/bad-input", who + "[" + i + "] index " + j + " is a hole or is inherited; every entry must be its own");
2009
+ var atv = ad.value;
2010
+ if (atv === null || typeof atv !== "object") throw E("path/bad-input", who + "[" + i + "][" + j + "] must be an attribute object carrying a type and a value");
2011
+ var atvOut = {};
2012
+ for (var k = 0; k < _ATV_KEYS.length; k++) {
2013
+ var fd = intrinsic.getOwnPropertyDescriptor(atv, _ATV_KEYS[k]);
2014
+ if (fd === undefined) continue;
2015
+ intrinsic.defineProperty(atvOut, _ATV_KEYS[k], { value: fd.value, enumerable: true, configurable: true, writable: true });
2016
+ }
2017
+ if (!intrinsic.hasOwn(atvOut, "type") || !intrinsic.hasOwn(atvOut, "value")) {
2018
+ throw E("path/bad-input", who + "[" + i + "][" + j + "] must carry its type and value as own data properties");
2019
+ }
2020
+ intrinsic.push(rdnOut, atvOut);
2021
+ }
2022
+ intrinsic.push(out, rdnOut);
2023
+ }
2024
+ return out;
2025
+ }
2026
+ /**
2027
+ * @internal The shape check the copy above relies on: a Proxy answers its own reads and an accessor
2028
+ * runs caller code, so either one at any level of the directoryName is refused before it is copied.
2029
+ */
2030
+ function _assertPlainDnRdns(rdns, who) {
2031
+ if (intrinsic.types.isProxy(rdns)) throw E("path/bad-input", who + " must be a plain array, not a Proxy");
2032
+ for (var i = 0; i < rdns.length; i++) {
2033
+ var rd = intrinsic.getOwnPropertyDescriptor(rdns, i);
2034
+ if (rd === undefined) continue;
2035
+ if (!intrinsic.hasOwn(rd, "value")) throw E("path/bad-input", who + " entry " + i + " must be a data property, not an accessor");
2036
+ var rdn = rd.value;
2037
+ if (!intrinsic.isArray(rdn)) continue;
2038
+ if (intrinsic.types.isProxy(rdn)) throw E("path/bad-input", who + "[" + i + "] must be a plain array, not a Proxy");
2039
+ for (var j = 0; j < rdn.length; j++) {
2040
+ var ad = intrinsic.getOwnPropertyDescriptor(rdn, j);
2041
+ if (ad === undefined) continue;
2042
+ if (!intrinsic.hasOwn(ad, "value")) throw E("path/bad-input", who + "[" + i + "] entry " + j + " must be a data property, not an accessor");
2043
+ var atv = ad.value;
2044
+ if (atv === null || typeof atv !== "object") continue;
2045
+ if (intrinsic.types.isProxy(atv)) throw E("path/bad-input", who + "[" + i + "][" + j + "] must not be a Proxy");
2046
+ if (intrinsic.isArray(atv)) throw E("path/bad-input", who + "[" + i + "][" + j + "] must be an attribute object, not an array");
2047
+ for (var k = 0; k < _ATV_KEYS.length; k++) {
2048
+ var fd = intrinsic.getOwnPropertyDescriptor(atv, _ATV_KEYS[k]);
2049
+ if (fd !== undefined && !intrinsic.hasOwn(fd, "value")) {
2050
+ throw E("path/bad-input", who + "[" + i + "][" + j + "]." + _ATV_KEYS[k] + " must be a data property, not an accessor");
2051
+ }
2052
+ }
2053
+ }
2054
+ }
2055
+ }
2056
+ function _capturedRdns(base, who) {
2057
+ if (intrinsic.types.isProxy(base)) throw E("path/bad-input", who + ": a trustAnchor nameConstraints directoryName base must not be a Proxy");
2058
+ /** @internal An inherited Proxy answers `in` through its has trap, which is caller code running
2059
+ * inside what is meant to be a getter-free read. */
2060
+ var baseProto = intrinsic.getPrototypeOf(base);
2061
+ while (baseProto !== null) {
2062
+ if (intrinsic.types.isProxy(baseProto)) throw E("path/bad-input", who + ": a trustAnchor nameConstraints directoryName base must not inherit from a Proxy");
2063
+ baseProto = intrinsic.getPrototypeOf(baseProto);
2064
+ }
2065
+ var d = intrinsic.getOwnPropertyDescriptor(base, "rdns");
2066
+ if (d === undefined) {
2067
+ if ("rdns" in base) throw E("path/bad-input", who + ": a trustAnchor nameConstraints directoryName rdns must be an own data property, not inherited");
2068
+ return undefined;
2069
+ }
2070
+ if (!intrinsic.hasOwn(d, "value")) throw E("path/bad-input", who + ": a trustAnchor nameConstraints directoryName rdns must be a data property, not an accessor");
2071
+ if (!intrinsic.isArray(d.value)) return undefined;
2072
+ return _copyDnRdns(d.value, who + ": a trustAnchor nameConstraints directoryName rdns");
2073
+ }
2074
+ var _NC_SUBTREE_KEYS = ["tag", "base"];
2075
+ var _NC_LIST_KEYS = ["permitted", "excluded"];
2076
+ function _copyAnchorSubtreeList(list, key, who) {
2077
+ if (list === undefined || list === null) return undefined;
2078
+ if (!intrinsic.isArray(list) || intrinsic.types.isProxy(list)) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " must be a plain array of { tag, base } subtree entries");
2079
+ var copied = [];
2080
+ var len = list.length;
2081
+ for (var i = 0; i < len; i++) {
2082
+ var entryD = intrinsic.getOwnPropertyDescriptor(list, i);
2083
+ if (entryD === undefined) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " must not be a sparse array; index " + i + " is a hole");
2084
+ if (!intrinsic.hasOwn(entryD, "value")) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " entry " + i + " must be a data property, not an accessor");
2085
+ var st = entryD.value;
2086
+ if (st === null || typeof st !== "object" || intrinsic.types.isProxy(st)) {
2087
+ throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " entry must be a plain { tag, base } object");
2088
+ }
2089
+ var out = {};
2090
+ for (var k = 0; k < _NC_SUBTREE_KEYS.length; k++) {
2091
+ var field = _NC_SUBTREE_KEYS[k];
2092
+ var fd = intrinsic.getOwnPropertyDescriptor(st, field);
2093
+ if (fd === undefined) continue;
2094
+ if (!intrinsic.hasOwn(fd, "value")) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " " + field + " must be a data property, not an accessor");
2095
+ var fv = fd.value;
2096
+ if (field === "base" && fv !== null && typeof fv === "object") {
2097
+ var rdnsOwn = _capturedRdns(fv, who);
2098
+ if (rdnsOwn !== undefined) {
2099
+ var dnBase = {};
2100
+ intrinsic.defineProperty(dnBase, "rdns", { value: rdnsOwn, enumerable: true, configurable: true, writable: true });
2101
+ fv = dnBase;
2102
+ } else if (guard.bytes.isByteSource(fv)) {
2103
+ fv = guard.bytes.snapshotSource(fv, PathError, "path/bad-input", who + ": a trustAnchor nameConstraints iPAddress subtree base");
2104
+ }
2105
+ }
2106
+ intrinsic.defineProperty(out, field, { value: fv, enumerable: true, configurable: true, writable: true });
2107
+ }
2108
+ intrinsic.push(copied, out);
2109
+ }
2110
+ return copied;
2111
+ }
2112
+ /**
2113
+ * @internal Anchors are normalized one at a time, and each may expose fields through getters that
2114
+ * run caller code. Capture every anchor's namespace first, so an earlier anchor's getter cannot
2115
+ * clear a later anchor's restriction before that anchor is reached.
2116
+ */
2117
+ /**
2118
+ * @internal Read a caller's anchor list once, from its own indices, into a fresh array. The list is
2119
+ * walked twice, to capture namespaces and then to normalize, and an accessor index could answer
2120
+ * differently each time.
2121
+ */
2122
+ function _ownAnchorEntries(list, who) {
2123
+ if (intrinsic.types.isProxy(list)) throw E("path/bad-input", who + ": trustAnchors must be a plain array, not a Proxy");
2124
+ var out = [];
2125
+ for (var i = 0; i < list.length; i++) {
2126
+ var d = intrinsic.getOwnPropertyDescriptor(list, i);
2127
+ if (d === undefined) throw E("path/bad-input", who + ": trustAnchors index " + i + " is a hole or is inherited; every entry must be its own");
2128
+ if (!intrinsic.hasOwn(d, "value")) throw E("path/bad-input", who + ": trustAnchors entry " + i + " must be a data property, not an accessor");
2129
+ intrinsic.push(out, d.value);
2130
+ }
2131
+ return out;
2132
+ }
2133
+ function _precaptureAnchorNameConstraints(list, who) {
2134
+ var out = [];
2135
+ for (var i = 0; i < list.length; i++) {
2136
+ var entry = list[i];
2137
+ var captured;
2138
+ if (entry && typeof entry === "object" && !intrinsic.isBuffer(entry) && !guard.parsed.isRecorded(entry) && !intrinsic.types.isProxy(entry)) {
2139
+ var raw = _captureConstraintMap(entry, "nameConstraints", E, "path/bad-input", who);
2140
+ captured = raw == null ? undefined : _copyAnchorNameConstraints(raw, who);
2141
+ }
2142
+ intrinsic.push(out, captured);
2143
+ }
2144
+ return out;
2145
+ }
2146
+ function _copyAnchorNameConstraints(nc, who) {
2147
+ if (nc === null || typeof nc !== "object" || intrinsic.isArray(nc)) {
2148
+ throw E("path/bad-input", who + ": a trustAnchor nameConstraints must be a { permitted, excluded } object of { tag, base } subtree entries");
2149
+ }
2150
+ guard.identifier.assertPlainRecord(nc, E, "path/bad-input", who + ": a trustAnchor nameConstraints");
2151
+ guard.identifier.assertKnownKeys(nc, { permitted: 1, excluded: 1 }, E, "path/bad-input",
2152
+ who + ": a trustAnchor nameConstraints names its subtree lists `permitted` and `excluded`. The unknown key was: ");
2153
+ var out = {};
2154
+ for (var i = 0; i < _NC_LIST_KEYS.length; i++) {
2155
+ var key = _NC_LIST_KEYS[i];
2156
+ var d = intrinsic.getOwnPropertyDescriptor(nc, key);
2157
+ if (d === undefined) {
2158
+ if (key in nc) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " must be an own data property, not inherited");
2159
+ continue;
2160
+ }
2161
+ if (!intrinsic.hasOwn(d, "value")) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " must be a data property, not an accessor");
2162
+ var copiedList = _copyAnchorSubtreeList(d.value, key, who);
2163
+ if (copiedList === undefined) continue;
2164
+ /** @internal Validated while the anchor is normalized, so a malformed subtree is refused
2165
+ * wherever the anchor sits in the list rather than only once that anchor is reached. */
2166
+ var checkedList = checkedSubtreeSeeds(copiedList, "trustAnchors[].nameConstraints." + key);
2167
+ intrinsic.defineProperty(out, key, { value: checkedList, enumerable: true, configurable: true, writable: true });
2168
+ }
2169
+ return out;
2170
+ }
2171
+
1906
2172
  function _copyAtv(atv) {
1907
2173
  var out = {};
1908
2174
  var t = atv.type;
@@ -1939,7 +2205,7 @@ function _copyRdnsDeep(rdns) {
1939
2205
  function toAnchor(entry, verb) {
1940
2206
  var who = verb || "build";
1941
2207
  var isTuple = false, alg = null, pubRaw, nameRaw = null, pk = null, purposesSnap, distrustSnap;
1942
- var subjectDerRaw, labelRaw, mozRaw;
2208
+ var subjectDerRaw, labelRaw, mozRaw, ncSnap;
1943
2209
  if (intrinsic.types.isProxy(entry)) {
1944
2210
  throw E("path/bad-input", who + ": a trustAnchor must not be a Proxy");
1945
2211
  }
@@ -1956,6 +2222,8 @@ function toAnchor(entry, verb) {
1956
2222
  purposesSnap = _normalizeConstraintField(pRaw, E, "path/bad-input", who);
1957
2223
  var dRaw = _captureConstraintMap(entry, "distrustAfter", E, "path/bad-input", who);
1958
2224
  distrustSnap = _normalizeConstraintField(dRaw, E, "path/bad-input", who);
2225
+ var ncRaw = _captureConstraintMap(entry, "nameConstraints", E, "path/bad-input", who);
2226
+ ncSnap = ncRaw == null ? undefined : _copyAnchorNameConstraints(ncRaw, who);
1959
2227
  subjectDerRaw = _captureOwnData(entry, "subjectDer");
1960
2228
  labelRaw = _captureOwnData(entry, "label");
1961
2229
  mozRaw = _captureOwnData(entry, "mozillaCaPolicy");
@@ -2023,6 +2291,7 @@ function toAnchor(entry, verb) {
2023
2291
  intrinsic.defineProperty(flat, "name", { value: nameOut, enumerable: true, configurable: true, writable: true });
2024
2292
  if (purposesSnap !== undefined) intrinsic.defineProperty(flat, "purposes", { value: purposesSnap, enumerable: true, configurable: true, writable: true });
2025
2293
  if (distrustSnap !== undefined) intrinsic.defineProperty(flat, "distrustAfter", { value: distrustSnap, enumerable: true, configurable: true, writable: true });
2294
+ if (ncSnap !== undefined) intrinsic.defineProperty(flat, "nameConstraints", { value: ncSnap, enumerable: true, configurable: true, writable: true });
2026
2295
  if (subjectDerRaw !== undefined) intrinsic.defineProperty(flat, "subjectDer", { value: subjectDerRaw, enumerable: true, configurable: true, writable: true });
2027
2296
  if (labelRaw !== undefined) intrinsic.defineProperty(flat, "label", { value: labelRaw, enumerable: true, configurable: true, writable: true });
2028
2297
  if (mozRaw !== undefined) intrinsic.defineProperty(flat, "mozillaCaPolicy", { value: mozRaw, enumerable: true, configurable: true, writable: true });
@@ -2031,8 +2300,8 @@ function toAnchor(entry, verb) {
2031
2300
  intrinsic.defineProperty(flat, "parameters", { value: spkiAlg.parameters, enumerable: true, configurable: true, writable: true });
2032
2301
  return flat;
2033
2302
  }
2034
- if (entry && typeof entry === "object" && (("purposes" in entry) || ("distrustAfter" in entry))) {
2035
- throw E("path/bad-input", who + ": a trustAnchor certificate must not carry purposes / distrustAfter -- put per-purpose constraints on a { name, publicKey, algorithm, purposes, distrustAfter } tuple (or a pki.trust anchor), not on a parsed certificate");
2303
+ if (entry && typeof entry === "object" && (("purposes" in entry) || ("distrustAfter" in entry) || ("nameConstraints" in entry))) {
2304
+ throw E("path/bad-input", who + ": a trustAnchor certificate must not carry purposes / distrustAfter / nameConstraints -- put anchor constraints on a { name, publicKey, algorithm, purposes, distrustAfter, nameConstraints } tuple (or a pki.trust anchor), not on a parsed certificate");
2036
2305
  }
2037
2306
  var cert;
2038
2307
  try { cert = coerceCert(entry); }
@@ -2349,7 +2618,20 @@ async function build(leaf, opts) {
2349
2618
  for (var pdk = 0; pdk < pool.length; pdk++) intrinsic.setAdd(poolDerKeys, certDerKey(pool[pdk]));
2350
2619
 
2351
2620
  if (!intrinsic.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");
2352
- var anchors = opts.trustAnchors.map(function (a) { return toAnchor(a, "build"); });
2621
+ /** @internal Captured before any anchor is normalized: an anchor field may be an accessor whose
2622
+ * getter runs caller code, and every candidate is validated in its own pass after an await. */
2623
+ var buildPermRaw = opts.initialPermittedSubtrees;
2624
+ var buildExclRaw = opts.initialExcludedSubtrees;
2625
+ var buildPermSeeds = checkedSubtreeSeeds(buildPermRaw, "initialPermittedSubtrees");
2626
+ var buildExclSeeds = checkedSubtreeSeeds(buildExclRaw, "initialExcludedSubtrees");
2627
+ var buildEntries = _ownAnchorEntries(opts.trustAnchors, "build");
2628
+ var buildPreNc = _precaptureAnchorNameConstraints(buildEntries, "build");
2629
+ var anchors = [];
2630
+ for (var bai = 0; bai < buildEntries.length; bai++) {
2631
+ var bAnchor = toAnchor(buildEntries[bai], "build");
2632
+ if (buildPreNc[bai] !== undefined) intrinsic.defineProperty(bAnchor, "nameConstraints", { value: buildPreNc[bai], enumerable: true, configurable: true, writable: true });
2633
+ intrinsic.push(anchors, bAnchor);
2634
+ }
2353
2635
 
2354
2636
  guard.time.assertValid(opts.time, E, "path/bad-input", "build: opts.time (the validity-window check date, forwarded to validate)");
2355
2637
  var effectiveMaxCerts = guard.limits.cap(opts.maxPathCerts, "build: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
@@ -2396,6 +2678,8 @@ async function build(leaf, opts) {
2396
2678
  if (intrinsic.hasOwn(BUILD_ONLY_OPT, k)) return;
2397
2679
  if (k in opts) forwarded[k] = opts[k];
2398
2680
  });
2681
+ if (buildPermRaw != null) forwarded.initialPermittedSubtrees = buildPermSeeds;
2682
+ if (buildExclRaw != null) forwarded.initialExcludedSubtrees = buildExclSeeds;
2399
2683
  function validateOpts(anchor) {
2400
2684
  var vo = Object.create(null);
2401
2685
  Object.keys(forwarded).forEach(function (f) { vo[f] = forwarded[f]; });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.6.37",
3
+ "version": "0.6.38",
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:82faf919-c224-4430-992b-dd3055e65ed3",
5
+ "serialNumber": "urn:uuid:adf5e979-6804-475f-a0d0-8aef04be0317",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-05T03:43:35.188Z",
8
+ "timestamp": "2026-09-05T07:57:13.454Z",
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.6.37",
22
+ "bom-ref": "@blamejs/pki@0.6.38",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.6.37",
25
+ "version": "0.6.38",
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.6.37",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.6.38",
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.6.37",
57
+ "ref": "@blamejs/pki@0.6.38",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]