@blamejs/pki 0.6.36 → 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,34 @@ 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
+
24
+ ## v0.6.37 — 2026-09-05
25
+
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.
27
+
28
+ ### Fixed
29
+
30
+ - pki.schema.c509.parse refuses a natively signed certificate (c509CertificateType 2) that writes an extension as an OID and a DER value when a C509 registry identifier carries the same extension value. Draft section 3.7 reserves the generic form for re-encoded certificates, where it stays accepted. An extension whose OID has no registry identifier, and one whose value the compact encodings cannot represent under that certificate type's rules, both keep the generic form.
31
+ - pki.schema.c509.encode chooses an extension's form under the rules of the certificate type it is writing. It previously judged whether a value could be written compactly using the rules for a re-encoded certificate, so a natively signed certificate could be given a compact form its own decoder does not accept.
32
+ - pki.schema.c509.encode resolves an extension named only by its OID to the matching registry identifier. It looked the identifier up by name alone, so an extension supplied without one was written in the generic OID form even where a compact encoding carried it.
33
+ - pki.schema.c509 decides what belongs to the C509 registries by dotted OID rather than by the name pki.oid.register maps that OID to. Registering a different name for a built-in OID previously changed what the toolkit emitted for an unrelated certificate, and for commonName, a curve, ecPublicKey or an ECDSA signature algorithm it stopped a certificate converting to C509 at all.
34
+
7
35
  ## v0.6.36 — 2026-09-04
8
36
 
9
37
  A natively signed C509 certificate is refused when it spells an algorithm generically instead of using the registry entry that names the same value.
@@ -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]; });
@@ -63,6 +63,11 @@ var PK_ALG_BY_INT = {
63
63
  3: { alg: _name("ecPublicKey"), curve: _name("secp521r1") },
64
64
  };
65
65
  var EC_FIELD_BYTES = Object.assign(Object.create(null), { "prime256v1": 32, "secp384r1": 48, "secp521r1": 66 });
66
+ /** @internal Curve identity follows the dotted OID; pki.oid.register can rename one at any time. */
67
+ var EC_CURVE_BY_OID = Object.create(null);
68
+ Object.keys(EC_FIELD_BYTES).forEach(function (n) { EC_CURVE_BY_OID[oid.byName(n)] = n; });
69
+ var SIG_ALG_BY_OID = Object.create(null);
70
+ Object.keys(SIG_ALG_BY_INT).forEach(function (k) { SIG_ALG_BY_OID[oid.byName(SIG_ALG_BY_INT[k])] = SIG_ALG_BY_INT[k]; });
66
71
 
67
72
  var ATTR_BY_INT = {
68
73
  0: _name("emailAddress"),
@@ -1351,7 +1356,9 @@ function _sdaFromDer(node) {
1351
1356
  var setNode = attr.children[1];
1352
1357
  if (setNode.tagClass !== "universal" || setNode.tagNumber !== asn1.TAGS.SET || !setNode.children || setNode.children.length < 1) return null;
1353
1358
  var dotted = asn1.read.oid(attr.children[0]);
1354
- var nm = oid.name(dotted), tint = nm != null ? ATTR_TO_INT[nm] : undefined;
1359
+ var canonAttr = ATTR_NAME_BY_OID[dotted];
1360
+ var nm = canonAttr === undefined ? oid.name(dotted) : canonAttr;
1361
+ var tint = canonAttr === undefined ? undefined : ATTR_TO_INT[canonAttr];
1355
1362
  var intForm = _sdaTryIntForm(tint, nm, setNode.children);
1356
1363
  if (intForm != null) {
1357
1364
  out.push(cbor.build.int(BigInt(intForm.sign < 0 ? -tint : tint)));
@@ -1364,12 +1371,24 @@ function _sdaFromDer(node) {
1364
1371
  return cbor.build.array(out);
1365
1372
  }
1366
1373
 
1367
- function _tryCompactExtValue(name, der) {
1374
+ /**
1375
+ * @internal Draft sec. 3.7 for extensions. The specific form was available when the registry names
1376
+ * this extension AND the int form can carry this exact value: a compact-codec extension only when
1377
+ * the codec represents it, and one without a codec always, since its int form carries the same
1378
+ * bytes. An extension the codec cannot represent keeps the generic form on every certificate type.
1379
+ */
1380
+ function _assertSpecificExtension(dotted, valContent, isNative) {
1381
+ var canon = EXT_NAME_BY_OID[dotted];
1382
+ if (!isNative || canon === undefined) return;
1383
+ if (EXT_COMPACT[canon] && (!Buffer.isBuffer(valContent) || _tryCompactExtValue(canon, valContent, isNative) == null)) return;
1384
+ throw _err("c509/non-specific-encoding", "extension " + canon + " has a C509 registry id that carries this value, and a natively signed certificate uses only the specific CBOR encoding (draft sec. 3.7)");
1385
+ }
1386
+ function _tryCompactExtValue(name, der, isNative) {
1368
1387
  var compact = _extValueFromDer(name, der);
1369
1388
  if (compact == null) return null;
1370
1389
  var recon;
1371
1390
  try {
1372
- recon = _extValueToDer(name, cbor.decode(compact));
1391
+ recon = _extValueToDer(name, cbor.decode(compact), isNative);
1373
1392
  } catch (_e) {
1374
1393
  return null;
1375
1394
  }
@@ -1418,6 +1437,7 @@ function _extensions(node, isNative) {
1418
1437
  if (valNode.majorType !== 2) throw _err("c509/bad-extensions", "a non-critical ~oid extension value must be a byte string");
1419
1438
  valContent = valNode.content;
1420
1439
  }
1440
+ _assertSpecificExtension(extOid, valContent, isNative);
1421
1441
  }
1422
1442
  out.push({ name: name, oid: extOid, critical: critical, value: valContent || null });
1423
1443
  }
@@ -1637,8 +1657,14 @@ function matches(node) {
1637
1657
 
1638
1658
  var ATTR_TO_INT = Object.create(null);
1639
1659
  Object.keys(ATTR_BY_INT).forEach(function (k) { ATTR_TO_INT[ATTR_BY_INT[k]] = Number(k); });
1660
+ /** @internal An attribute's C509 registry membership follows its dotted OID, not its display name. */
1661
+ var ATTR_NAME_BY_OID = Object.create(null);
1662
+ Object.keys(ATTR_BY_INT).forEach(function (k) { ATTR_NAME_BY_OID[oid.byName(ATTR_BY_INT[k])] = ATTR_BY_INT[k]; });
1640
1663
  var EXT_TO_INT = Object.create(null);
1641
1664
  Object.keys(EXT_BY_INT).forEach(function (k) { if (EXT_COMPACT[EXT_BY_INT[k]]) EXT_TO_INT[EXT_BY_INT[k]] = Number(k); });
1665
+ /** @internal An extension's C509 registry membership follows its dotted OID, not its display name. */
1666
+ var EXT_NAME_BY_OID = Object.create(null);
1667
+ Object.keys(EXT_BY_INT).forEach(function (k) { EXT_NAME_BY_OID[oid.byName(EXT_BY_INT[k])] = EXT_BY_INT[k]; });
1642
1668
 
1643
1669
  function _minBytes(n) {
1644
1670
  if (typeof n !== "bigint") throw _err("c509/bad-serial", "a ~biguint value must be a BigInt, got " + _describe(n));
@@ -1696,7 +1722,24 @@ function _encSpk(r) {
1696
1722
  if (!Buffer.isBuffer(r.subjectPublicKey)) throw _err("c509/bad-spki", "the subjectPublicKey bytes are missing");
1697
1723
  return cbor.build.byteString(r.subjectPublicKey);
1698
1724
  }
1699
- function _encExtensions(exts) {
1725
+ /** @internal An extension names itself by name, by OID, or by both; the registry is keyed by name. */
1726
+ function _extIdentity(ext) {
1727
+ var dotted = ext.oid || (typeof ext.name === "string" ? oid.byName(ext.name) : null);
1728
+ var canon = dotted ? EXT_NAME_BY_OID[dotted] : undefined;
1729
+ if (canon !== undefined) return { dotted: dotted, name: canon };
1730
+ return { dotted: dotted, name: typeof ext.name === "string" ? ext.name : (dotted ? oid.name(dotted) : null) };
1731
+ }
1732
+ /**
1733
+ * @internal The bits a lone keyUsage is written as. Derived from a DER value only when the compact
1734
+ * form reproduces that value byte for byte, since the bare integer covers the nine defined bits and
1735
+ * a wider BIT STRING would lose the rest.
1736
+ */
1737
+ function _extKeyUsageBits(ext, isNative) {
1738
+ if (typeof ext.keyUsageBits === "number") return ext.keyUsageBits;
1739
+ if (!Buffer.isBuffer(ext.value) || _tryCompactExtValue("keyUsage", ext.value, isNative) == null) return null;
1740
+ return _keyUsageBitsFromDer(ext.value);
1741
+ }
1742
+ function _encExtensions(exts, isNative) {
1700
1743
  _assertDense(exts, "c509/bad-input", "the extensions array");
1701
1744
  exts.forEach(function (ext) {
1702
1745
  if (!ext || typeof ext !== "object") throw _err("c509/bad-input", "each extension must be an object, got " + _describe(ext));
@@ -1709,18 +1752,20 @@ function _encExtensions(exts) {
1709
1752
  throw _err("c509/bad-extensions", "keyUsageBits must be a safe integer, got " + _describe(ext.keyUsageBits));
1710
1753
  }
1711
1754
  });
1712
- if (exts.length === 1 && exts[0].name === "keyUsage" && typeof exts[0].keyUsageBits === "number") {
1713
- return cbor.build.int(BigInt(exts[0].critical ? -exts[0].keyUsageBits : exts[0].keyUsageBits));
1755
+ if (exts.length === 1 && _extIdentity(exts[0]).name === "keyUsage") {
1756
+ var onlyBits = _extKeyUsageBits(exts[0], isNative);
1757
+ if (typeof onlyBits === "number") return cbor.build.int(BigInt(exts[0].critical ? -onlyBits : onlyBits));
1714
1758
  }
1715
1759
  var items = [];
1716
1760
  exts.forEach(function (ext) {
1717
- var ei = EXT_TO_INT[ext.name];
1718
- var compact = (ei !== undefined && Buffer.isBuffer(ext.value)) ? _tryCompactExtValue(ext.name, ext.value) : null;
1761
+ var ident = _extIdentity(ext);
1762
+ var extDotted = ident.dotted, extName = ident.name;
1763
+ var ei = extName == null ? undefined : EXT_TO_INT[extName];
1764
+ var compact = (ei !== undefined && Buffer.isBuffer(ext.value)) ? _tryCompactExtValue(extName, ext.value, isNative) : null;
1719
1765
  if (compact != null) {
1720
1766
  items.push(cbor.build.int(BigInt(ext.critical ? -ei : ei)));
1721
1767
  items.push(compact);
1722
1768
  } else {
1723
- var extDotted = ext.oid || (typeof ext.name === "string" ? oid.byName(ext.name) : null);
1724
1769
  if (!extDotted) throw _err("c509/bad-input", "an extension must carry an OID or a name in the OID registry, got " + _describe(ext.name));
1725
1770
  items.push(cbor.build.byteString(asn1.encodeOidContent(extDotted)));
1726
1771
  if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + (ext.oid || ext.name) + " has no byte-string value to encode");
@@ -1764,7 +1809,7 @@ function _encodeC509Array(r) {
1764
1809
  _encName(r.subject, true),
1765
1810
  _encAlgorithm(r.subjectPublicKeyAlgorithm, PK_ALG_BY_INT),
1766
1811
  _encSpk(r),
1767
- _encExtensions(r.extensions),
1812
+ _encExtensions(r.extensions, Number(r.certificateType) === 2),
1768
1813
  cbor.build.byteString(r.signatureValue),
1769
1814
  ]);
1770
1815
  parse(arr);
@@ -1828,7 +1873,9 @@ function _c509NameFromDer(nameBytes) {
1828
1873
  (node.children || []).forEach(function (rdnSet) {
1829
1874
  if (!rdnSet.children || rdnSet.children.length !== 1) throw _err("c509/non-invertible", "a C509 Name requires single-attribute RDNs");
1830
1875
  var attr = rdnSet.children[0];
1831
- var attrName = oid.name(asn1.read.oid(attr.children[0]));
1876
+ var attrDotted = asn1.read.oid(attr.children[0]);
1877
+ var attrCanon = ATTR_NAME_BY_OID[attrDotted];
1878
+ var attrName = attrCanon === undefined ? oid.name(attrDotted) : attrCanon;
1832
1879
  if (attrName == null || ATTR_TO_INT[attrName] === undefined) throw _err("c509/non-invertible", "attribute type " + attrName + " has no C509 registry integer");
1833
1880
  var valNode = attr.children[1];
1834
1881
  var value;
@@ -1875,19 +1922,19 @@ function _minCurveForMag(mag) {
1875
1922
  for (var i = 0; i < CURVE_ORDER.length; i++) { if (mag <= WEBCRYPTO_FIELD_BYTES[CURVE_ORDER[i]]) return CURVE_ORDER[i]; }
1876
1923
  return null;
1877
1924
  }
1878
- function _resolveIssuerSigCurve(c, opts) {
1925
+ function _resolveIssuerSigCurve(c, opts, sigAlgName) {
1879
1926
  var mag = _sigMagWidth(c.signatureValue.bytes), curve;
1880
1927
  if (opts && opts.issuerCurve != null) {
1881
1928
  curve = String(opts.issuerCurve);
1882
1929
  if (NODE_TO_WEBCRYPTO[curve]) curve = NODE_TO_WEBCRYPTO[curve];
1883
1930
  if (!WEBCRYPTO_FIELD_BYTES[curve]) throw _err("c509/bad-input", "opts.issuerCurve must be P-256 / P-384 / P-521 (or prime256v1 / secp384r1 / secp521r1); got " + opts.issuerCurve);
1884
1931
  } else {
1885
- curve = SIG_ALG_TO_CURVE[c.signatureAlgorithm.name];
1886
- if (!curve) throw _err("c509/non-invertible", "cannot resolve the issuer signing curve for signature algorithm " + c.signatureAlgorithm.name);
1932
+ curve = SIG_ALG_TO_CURVE[sigAlgName];
1933
+ if (!curve) throw _err("c509/non-invertible", "cannot resolve the issuer signing curve for signature algorithm " + sigAlgName);
1887
1934
  var minCurve = _minCurveForMag(mag);
1888
1935
  if (!minCurve) throw _err("c509/non-invertible", "the ECDSA signature r/s width " + mag + " exceeds every supported curve field");
1889
- if (WEBCRYPTO_FIELD_BYTES[curve] < mag) throw _err("c509/non-invertible", "the issuer signed with a non-standard digest/curve pairing (r/s width " + mag + " exceeds the " + curve + " field implied by " + c.signatureAlgorithm.name + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
1890
- if (WEBCRYPTO_FIELD_BYTES[curve] > WEBCRYPTO_FIELD_BYTES[minCurve]) throw _err("c509/non-invertible", "signature algorithm " + c.signatureAlgorithm.name + " does not uniquely determine the issuer curve (r/s width " + mag + " also fits " + minCurve + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
1936
+ if (WEBCRYPTO_FIELD_BYTES[curve] < mag) throw _err("c509/non-invertible", "the issuer signed with a non-standard digest/curve pairing (r/s width " + mag + " exceeds the " + curve + " field implied by " + sigAlgName + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
1937
+ if (WEBCRYPTO_FIELD_BYTES[curve] > WEBCRYPTO_FIELD_BYTES[minCurve]) throw _err("c509/non-invertible", "signature algorithm " + sigAlgName + " does not uniquely determine the issuer curve (r/s width " + mag + " also fits " + minCurve + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
1891
1938
  }
1892
1939
  if (mag > WEBCRYPTO_FIELD_BYTES[curve]) throw _err("c509/non-invertible", "the ECDSA signature r/s width " + mag + " does not fit the resolved " + curve + " field");
1893
1940
  return curve;
@@ -1902,13 +1949,16 @@ _derToType3 = function (input, opts) {
1902
1949
  var c;
1903
1950
  try { c = x509.parse(input); } catch (e) { throw _err("c509/bad-input", "the input is not a valid X.509 certificate", e); }
1904
1951
  if (c.version !== 3) throw _err("c509/non-invertible", "C509 covers X.509 v3 certificates; got v" + c.version);
1905
- if (String(c.signatureAlgorithm.name || "").toLowerCase().indexOf("ecdsa") !== 0) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (c.signatureAlgorithm.name || "an unregistered algorithm"));
1906
- if (c.subjectPublicKeyInfo.algorithm.name !== "ecPublicKey") throw _err("c509/non-invertible", "type-3 C509 encoding covers only EC (ecPublicKey) certificates in v1; got " + (c.subjectPublicKeyInfo.algorithm.name || "an unregistered algorithm"));
1952
+ var sigCanon = SIG_ALG_BY_OID[c.signatureAlgorithm.oid];
1953
+ var sigAlgName = sigCanon === undefined ? c.signatureAlgorithm.name : sigCanon;
1954
+ if (String(sigAlgName || "").toLowerCase().indexOf("ecdsa") !== 0) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (sigAlgName || "an unregistered algorithm"));
1955
+ if (c.subjectPublicKeyInfo.algorithm.oid !== oid.byName("ecPublicKey")) throw _err("c509/non-invertible", "type-3 C509 encoding covers only EC (ecPublicKey) certificates in v1; got " + (c.subjectPublicKeyInfo.algorithm.name || "an unregistered algorithm"));
1907
1956
  var curveOid = asn1.read.oid(asn1.decode(c.subjectPublicKeyInfo.algorithm.parameters));
1908
- var curve = oid.name(curveOid);
1957
+ var curveCanon = EC_CURVE_BY_OID[curveOid];
1958
+ var curve = curveCanon === undefined ? oid.name(curveOid) : curveCanon;
1909
1959
  var coordLen = EC_FIELD_BYTES[curve];
1910
1960
  if (!coordLen) throw _err("c509/non-invertible", "unsupported EC subject curve " + (curve || curveOid));
1911
- var sigCurve = _resolveIssuerSigCurve(c, opts);
1961
+ var sigCurve = _resolveIssuerSigCurve(c, opts, sigAlgName);
1912
1962
  var spkiNode = asn1.decode(c.subjectPublicKeyInfo.bytes);
1913
1963
  return {
1914
1964
  certificateType: 3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.6.36",
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:8d50e6dd-1ddf-46f8-b8db-ed97195096c3",
5
+ "serialNumber": "urn:uuid:adf5e979-6804-475f-a0d0-8aef04be0317",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-05T01:58:33.232Z",
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.36",
22
+ "bom-ref": "@blamejs/pki@0.6.38",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.6.36",
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.36",
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.36",
57
+ "ref": "@blamejs/pki@0.6.38",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]