@blamejs/pki 0.8.11 → 0.8.14

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.
@@ -622,39 +622,118 @@ function absorbNameConstraints(state, decoded) {
622
622
  }
623
623
 
624
624
 
625
- function rootNode() {
626
- return { depth: 0, validPolicy: OID.anyPolicy, qualifierSet: [], expectedPolicySet: [OID.anyPolicy], children: [], parent: null };
627
- }
628
- function treeWithoutParent(node) {
629
- if (!node) return null;
630
- return {
631
- depth: node.depth,
632
- validPolicy: node.validPolicy,
633
- qualifierSet: node.qualifierSet,
634
- expectedPolicySet: node.expectedPolicySet,
635
- children: node.children.map(treeWithoutParent),
636
- };
637
- }
638
- function leavesAt(tree, depth) {
625
+ /**
626
+ * @internal The RFC 9618 valid_policy_graph. RFC 5280 sec. 6.1.3 step (d)(1)(i) creates one child per
627
+ * matching parent, so a chain where several issuer policies map to one subject policy multiplies the
628
+ * structure at every level: measured on a conforming chain carrying four policies mapped to each
629
+ * other, the tree wants 5461 nodes at six certificates against this graph's 25, and the node cap
630
+ * refuses the path. RFC 9618 sec. 5.2 keeps at most ONE node per (depth, policy identifier) and
631
+ * gives it several parents instead, which holds the same information in linear size.
632
+ *
633
+ * `state.policyIndex[depth]` maps a policy identifier to the one node at that depth, so the "is there
634
+ * already a node here for this policy" question every step asks is a lookup rather than a walk.
635
+ */
636
+ function graphRoot() {
637
+ return { depth: 0, validPolicy: OID.anyPolicy, qualifierSet: [], expectedPolicySet: [OID.anyPolicy], children: [], parents: [] };
638
+ }
639
+ function policyLayer(state, depth) {
640
+ var layer = state.policyIndex[depth];
641
+ if (layer === undefined) { layer = intrinsic.create(null); state.policyIndex[depth] = layer; }
642
+ return layer;
643
+ }
644
+ function nodeAt(state, depth, policyOid) {
645
+ var layer = state.policyIndex[depth];
646
+ if (layer === undefined || !intrinsic.hasOwn(layer, policyOid)) return null;
647
+ return layer[policyOid];
648
+ }
649
+ function nodesAt(state, depth) {
650
+ var layer = state.policyIndex[depth];
651
+ if (layer === undefined) return [];
639
652
  var out = [];
640
- if (!tree) return out;
641
- (function walk(node) {
642
- if (node.depth === depth) { guard.list.append(out, node); return; }
643
- node.children.forEach(walk);
644
- })(tree);
653
+ intrinsic.forEach(intrinsic.keys(layer), function (k) { guard.list.append(out, layer[k]); });
645
654
  return out;
646
655
  }
647
- function pruneChildless(tree, depth) {
656
+ /** @internal One node per (depth, policy). Meeting the same pair twice joins the parents onto the
657
+ * node that is already there rather than making a second one, which is the invariant the wrap-up
658
+ * procedure's single-parent test depends on. */
659
+ function addPolicyNode(state, depth, validPolicy, qualifiers, expectedPolicySet, parents) {
660
+ if (parents.length === 0) return null;
661
+ var node = nodeAt(state, depth, validPolicy);
662
+ if (node === null) {
663
+ if (state.policyNodeCount >= state.maxPolicyNodes) { state._capHit = true; return null; }
664
+ node = { depth: depth, validPolicy: validPolicy, qualifierSet: qualifiers ? [qualifiers] : [],
665
+ expectedPolicySet: expectedPolicySet, children: [], parents: [] };
666
+ policyLayer(state, depth)[validPolicy] = node;
667
+ state.policyNodeCount++;
668
+ }
669
+ for (var i = 0; i < parents.length; i++) {
670
+ if (node.parents.indexOf(parents[i]) !== -1) continue;
671
+ guard.list.append(node.parents, parents[i]);
672
+ guard.list.append(parents[i].children, node);
673
+ }
674
+ return node;
675
+ }
676
+ function removePolicyNode(state, node) {
677
+ node.parents.forEach(function (p) {
678
+ var idx = p.children.indexOf(node);
679
+ if (idx !== -1) p.children.splice(idx, 1);
680
+ });
681
+ node.children.forEach(function (c) {
682
+ var idx = c.parents.indexOf(node);
683
+ if (idx !== -1) c.parents.splice(idx, 1);
684
+ });
685
+ node.parents = [];
686
+ var layer = state.policyIndex[node.depth];
687
+ if (layer !== undefined && layer[node.validPolicy] === node) delete layer[node.validPolicy];
688
+ state.policyNodeCount--;
689
+ }
690
+ /** @internal RFC 9618 sec. 5.3 step (d)(3): delete every node of depth `depth` or less that has no
691
+ * children, and repeat, since deleting one can leave its own parents childless. Depth zero is the
692
+ * root, which is never deleted: an empty graph is a root with no children, which the caller reads
693
+ * as NULL. */
694
+ function prunePolicyChildless(state, depth) {
648
695
  for (var d = depth; d > 0; d--) {
649
- leavesAt(tree, d).forEach(function (node) {
650
- if (node.children.length === 0 && node.parent) {
651
- var idx = node.parent.children.indexOf(node);
652
- if (idx !== -1) node.parent.children.splice(idx, 1);
653
- }
696
+ nodesAt(state, d).forEach(function (node) {
697
+ if (node.children.length === 0) removePolicyNode(state, node);
698
+ });
699
+ }
700
+ }
701
+ function graphIsEmpty(state) { return state.validPolicyGraph.children.length === 0; }
702
+ /**
703
+ * @internal The graph a caller reads, as a flat list of nodes linked by index. A graph is not a
704
+ * tree, and handing back a root to walk would invite the caller to recurse into every child and
705
+ * rebuild the exponential structure this replaced: `JSON.stringify` on such a value re-expands a
706
+ * shared node once per path that reaches it. A node list is linear however it is read, holds the
707
+ * parent relation the wrap-up procedure needs, and carries no cycle to serialize. The root is
708
+ * index 0.
709
+ */
710
+ function graphSnapshot(root) {
711
+ if (!root) return null;
712
+ var originals = [];
713
+ var indexOfNode = new intrinsic.Map();
714
+ (function walk(node) {
715
+ if (intrinsic.mapHas(indexOfNode, node)) return;
716
+ intrinsic.mapSet(indexOfNode, node, originals.length);
717
+ guard.list.append(originals, node);
718
+ node.children.forEach(walk);
719
+ })(root);
720
+ function idsOf(list) {
721
+ var out = [];
722
+ for (var j = 0; j < list.length; j++) {
723
+ if (intrinsic.mapHas(indexOfNode, list[j])) guard.list.append(out, intrinsic.mapGet(indexOfNode, list[j]));
724
+ }
725
+ return out;
726
+ }
727
+ var nodes = [];
728
+ for (var i = 0; i < originals.length; i++) {
729
+ var o = originals[i];
730
+ guard.list.append(nodes, {
731
+ id: i, depth: o.depth, validPolicy: o.validPolicy, qualifierSet: o.qualifierSet,
732
+ expectedPolicySet: o.expectedPolicySet, parents: idsOf(o.parents), children: idsOf(o.children),
654
733
  });
655
734
  }
735
+ return { nodes: nodes };
656
736
  }
657
- function treeIsEmpty(tree) { return tree.children.length === 0; }
658
737
 
659
738
 
660
739
  function isSubtreeBaseValid(tag, base) {
@@ -725,24 +804,90 @@ function _anchorSubtreeSeeds(anchor) {
725
804
  };
726
805
  }
727
806
 
728
- function initialize(certs, params, seeds, anchor) {
807
+ /**
808
+ * @internal An RFC 5914 trust anchor's `certPath` constraints, read off the anchor `pki.trust`
809
+ * produced. Absent on an anchor from any other source, so every reader below sees empty lists and
810
+ * nothing changes for a caller who does not use the format.
811
+ */
812
+ function _anchorRfc5914Constraints(anchor) {
813
+ var empty = { permitted: [], excluded: [], policySet: null, policyFlags: null, pathLenConstraint: null };
814
+ var c = anchor === null || anchor === undefined ? undefined : anchor.constraints;
815
+ if (!c || typeof c !== "object") return empty;
816
+ var nc = c.nameConstraints;
817
+ return {
818
+ permitted: nc && intrinsic.isArray(nc.permitted) ? checkedSubtreeSeeds(nc.permitted, "trustAnchors[].constraints.nameConstraints.permitted") : [],
819
+ excluded: nc && intrinsic.isArray(nc.excluded) ? checkedSubtreeSeeds(nc.excluded, "trustAnchors[].constraints.nameConstraints.excluded") : [],
820
+ policySet: intrinsic.isArray(c.policySet) ? c.policySet : null,
821
+ policyFlags: c.policyFlags && typeof c.policyFlags === "object" ? c.policyFlags : null,
822
+ pathLenConstraint: intrinsic.isInteger(c.pathLenConstraint) ? c.pathLenConstraint : null,
823
+ };
824
+ }
825
+
826
+ /**
827
+ * @internal RFC 5937 sec. 3.2, the policy set. anyPolicy on either side means "any policy satisfies
828
+ * me", so intersecting with it leaves the other side's set untouched: filtering it literally would
829
+ * compare a concrete identifier against the wildcard identifier, find no match, and refuse a path
830
+ * both sides accept. Only where BOTH name concrete policies is the intersection a comparison, and
831
+ * an empty result there is a real answer rather than "unconstrained": the two name disjoint policy
832
+ * spaces, so no policy satisfies both.
833
+ */
834
+ function _mergedPolicySet(callerSet, anchorSet) {
835
+ if (anchorSet === null) return callerSet;
836
+ var callerAny = callerSet === undefined || callerSet === null || contains(callerSet, OID.anyPolicy);
837
+ var anchorAny = contains(anchorSet, OID.anyPolicy);
838
+ if (anchorAny && callerAny) return [OID.anyPolicy];
839
+ if (anchorAny) return intrinsic.map(callerSet, function (p) { return p; });
840
+ if (callerAny) return intrinsic.map(anchorSet, function (p) { return p; });
841
+ return intrinsic.filter(anchorSet, function (p) { return contains(callerSet, p); });
842
+ }
843
+
844
+ /**
845
+ * @internal RFC 5937 sec. 3.2, the flags. Each is one-directional: a flag the anchor asserts turns
846
+ * the matching initial value on where it was off and never off where it was on, so an anchor can
847
+ * only narrow what a caller asked for. That is what makes RFC 5914's "always enforced" safe.
848
+ */
849
+ function _flagSet(callerValue, anchorFlags, name) {
850
+ if (callerValue) return true;
851
+ return anchorFlags !== null && anchorFlags[name] === true;
852
+ }
853
+
854
+ function initialize(certs, params, seeds, anchor, ta) {
729
855
  var n = certs.length;
856
+ var state = _initialState(certs, params, seeds, anchor, n, ta);
857
+ /** @internal The root is the depth-zero anyPolicy node, and it is indexed like any other, so the
858
+ * "is there a node at this depth for this policy" lookup answers for depth zero too. */
859
+ policyLayer(state, 0)[OID.anyPolicy] = state.validPolicyGraph;
860
+ return state;
861
+ }
862
+
863
+ function _initialState(certs, params, seeds, anchor, n, ta) {
864
+ /** @internal The anchor's constraints are read HERE rather than merged into a copy of `params`,
865
+ * because a copy answers only for the own properties the copy loop enumerated. A caller option
866
+ * defined non-enumerably would be dropped by the copy and the anchor would then WIDEN the
867
+ * validation, which is the one thing sec. 3.2 does not allow. Reading the caller's object
868
+ * directly keeps every option live whatever its descriptor. */
869
+ var anchorPathLen = ta.pathLenConstraint;
730
870
  return {
731
- validPolicyTree: rootNode(),
871
+ validPolicyGraph: graphRoot(),
872
+ policyIndex: [],
732
873
  policyNodeCount: 1,
733
874
  maxPolicyNodes: params.maxPolicyNodes !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
734
875
  minRsaModulusBits: params.minRsaModulusBits !== undefined ? params.minRsaModulusBits : constants.LIMITS.RSA_MIN_MODULUS_BITS,
735
876
  permittedGenerations: intrinsic.filter(seeds.permittedGenerations, function (g) { return g.length > 0; }),
736
877
  excludedSubtrees: seeds.excluded,
737
- explicitPolicy: params.initialExplicitPolicy ? 0 : n + 1,
738
- inhibitAnyPolicy: params.initialAnyPolicyInhibit ? 0 : n + 1,
739
- policyMapping: params.initialPolicyMappingInhibit ? 0 : n + 1,
878
+ explicitPolicy: _flagSet(params.initialExplicitPolicy, ta.policyFlags, "requireExplicitPolicy") ? 0 : n + 1,
879
+ inhibitAnyPolicy: _flagSet(params.initialAnyPolicyInhibit, ta.policyFlags, "inhibitAnyPolicy") ? 0 : n + 1,
880
+ policyMapping: _flagSet(params.initialPolicyMappingInhibit, ta.policyFlags, "inhibitPolicyMapping") ? 0 : n + 1,
740
881
  workingPublicKeyAlgorithm: anchor.algorithm,
741
882
  workingPublicKey: anchor.publicKey,
742
883
  workingPublicKeyParameters: anchor.parameters || null,
743
884
  workingIssuerName: anchor.name,
744
- maxPathLength: n,
745
- userInitialPolicySet: params.userInitialPolicySet || [OID.anyPolicy],
885
+ /** @internal RFC 5937 sec. 3.2: "If a basic constraints extension is associated with the trust
886
+ * anchor and contains a pathLenConstraint value, set the max_path_length state variable equal
887
+ * to the pathLenConstraint value". Taken as the lower of the two, since sec. 6.1.2's own
888
+ * initial value is the path length and an anchor may only narrow. */
889
+ maxPathLength: intrinsic.isInteger(anchorPathLen) && anchorPathLen < n ? anchorPathLen : n,
890
+ userInitialPolicySet: _mergedPolicySet(params.userInitialPolicySet, ta.policySet) || [OID.anyPolicy],
746
891
  results: [],
747
892
  };
748
893
  }
@@ -757,55 +902,61 @@ function processPolicies(state, cert, i, checks) {
757
902
  try { cp = decodeExt(cert, OID.certificatePolicies); }
758
903
  catch (e) { guard.list.append(checks,{ name: "policies", ok: false, code: "path/bad-policy" }); return { fatal: true, error: e }; }
759
904
 
760
- if (cp && state.validPolicyTree) {
905
+ if (cp && state.validPolicyGraph) {
761
906
  var policies = cp.value;
762
- var depth = i - 1;
907
+ var prev = i - 1;
763
908
  var anyPolicyActive = state.inhibitAnyPolicy > 0 || (i < state._n && selfIssued(cert));
764
909
  var anyPolicyPresent = false;
765
910
  var anyPolicyQualifiers = null;
911
+
912
+ /** @internal RFC 9618 sec. 5.3 step (d)(1): ONE node per policy, carrying every depth i-1 node
913
+ * that expected it. Creating one per parent instead is what made the structure exponential. */
766
914
  policies.forEach(function (p) {
767
915
  if (p.policyIdentifier === OID.anyPolicy) { anyPolicyPresent = true; anyPolicyQualifiers = p.qualifiersBytes; return; }
768
- var matched = false;
769
- leavesAt(state.validPolicyTree, depth).forEach(function (node) {
770
- if (contains(node.expectedPolicySet, p.policyIdentifier)) {
771
- addChild(state, node, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], checks);
772
- matched = true;
773
- }
916
+ var parents = intrinsic.filter(nodesAt(state, prev), function (node) {
917
+ return contains(node.expectedPolicySet, p.policyIdentifier);
774
918
  });
775
- if (!matched) {
776
- leavesAt(state.validPolicyTree, depth).forEach(function (node) {
777
- if (node.validPolicy === OID.anyPolicy) addChild(state, node, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], checks);
778
- });
919
+ if (parents.length === 0) {
920
+ /** @internal (d)(1)(ii): no node expected it, so it hangs off the anyPolicy node if there
921
+ * is one. That single anyPolicy parent is what step (g)(2) later reads. */
922
+ var anyPrev = nodeAt(state, prev, OID.anyPolicy);
923
+ if (anyPrev === null) return;
924
+ parents = [anyPrev];
779
925
  }
926
+ addPolicyNode(state, i, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], parents);
780
927
  });
928
+
929
+ /** @internal (d)(2): for each policy some depth i-1 node expects, if depth i has no node for it
930
+ * yet, one node carrying every parent that expected it. */
781
931
  if (anyPolicyPresent && anyPolicyActive) {
782
- leavesAt(state.validPolicyTree, depth).forEach(function (node) {
932
+ var expected = [];
933
+ nodesAt(state, prev).forEach(function (node) {
783
934
  node.expectedPolicySet.forEach(function (ep) {
784
- var already = guard.list.anyMatches(node.children, function (ch) { return ch.validPolicy === ep; });
785
- if (!already) addChild(state, node, ep, anyPolicyQualifiers, [ep], checks);
935
+ if (expected.indexOf(ep) === -1) guard.list.append(expected, ep);
786
936
  });
787
937
  });
938
+ expected.forEach(function (ep) {
939
+ if (nodeAt(state, i, ep) !== null) return;
940
+ var parents = intrinsic.filter(nodesAt(state, prev), function (node) {
941
+ return contains(node.expectedPolicySet, ep);
942
+ });
943
+ addPolicyNode(state, i, ep, anyPolicyQualifiers, [ep], parents);
944
+ });
788
945
  }
789
- if (state._capHit) return { fatal: true, error: E("path/policy-tree-cap", "policy tree exceeded the node cap") };
790
- pruneChildless(state.validPolicyTree, depth);
791
- if (treeIsEmpty(state.validPolicyTree)) state.validPolicyTree = null;
946
+
947
+ if (state._capHit) return { fatal: true, error: E("path/policy-tree-cap", "policy graph exceeded the node cap") };
948
+ prunePolicyChildless(state, prev);
949
+ if (graphIsEmpty(state)) state.validPolicyGraph = null;
792
950
  } else if (!cp) {
793
- state.validPolicyTree = null;
951
+ state.validPolicyGraph = null;
794
952
  }
795
953
 
796
- if (!(state.explicitPolicy > 0 || state.validPolicyTree !== null)) {
954
+ if (!(state.explicitPolicy > 0 || state.validPolicyGraph !== null)) {
797
955
  guard.list.append(checks,{ name: "policy", ok: false, code: "path/policy-required" });
798
- return { fatal: true, error: E("path/policy-required", "explicit policy required but the valid-policy tree is empty") };
956
+ return { fatal: true, error: E("path/policy-required", "explicit policy required but the valid-policy graph is empty") };
799
957
  }
800
- return { fatal: false };
801
- }
802
-
803
- function addChild(state, parent, validPolicy, qualifiers, expectedPolicySet, checks) {
804
- if (state.policyNodeCount >= state.maxPolicyNodes) { state._capHit = true; return; }
805
- var node = { depth: parent.depth + 1, validPolicy: validPolicy, qualifierSet: qualifiers ? [qualifiers] : [], expectedPolicySet: expectedPolicySet, children: [], parent: parent };
806
- guard.list.append(parent.children, node);
807
- state.policyNodeCount++;
808
958
  void checks;
959
+ return { fatal: false };
809
960
  }
810
961
 
811
962
  function isNullOrAbsentParams(p) {
@@ -859,7 +1010,7 @@ function prepareNext(state, cert, i, checks) {
859
1010
  if (pm) {
860
1011
  var badAny = mapsAnyPolicy(pm.value);
861
1012
  if (badAny) { guard.list.append(checks,{ name: "policyMappings", ok: false, code: "path/bad-policy" }); return { fatal: true, error: E("path/bad-policy", "policyMappings must not map to or from anyPolicy (RFC 5280 6.1.4(a))") }; }
862
- if (state.validPolicyTree) applyPolicyMappings(state, pm.value, i);
1013
+ if (state.validPolicyGraph) applyPolicyMappings(state, pm.value, i);
863
1014
  }
864
1015
 
865
1016
  state.workingIssuerName = cert.subject;
@@ -919,36 +1070,49 @@ function prepareNext(state, cert, i, checks) {
919
1070
  return { fatal: false };
920
1071
  }
921
1072
 
1073
+ /** @internal RFC 9618 sec. 5.4 rewrites RFC 5280 sec. 6.1.4 step (b) over the graph. Each clause is
1074
+ * singular where the tree's was plural, because at most one node at a depth carries a given policy.
1075
+ *
1076
+ * A mapping naming anyPolicy on either side is skipped here as well as refused by the caller
1077
+ * (sec. 6.1.4(a)), because mapping one in would rewrite the anyPolicy node's expected policy set and
1078
+ * the graph's invariant is that it is always {anyPolicy}. The caller refusing it keeps that from
1079
+ * happening today; skipping it here is what keeps it true of this function. */
922
1080
  function applyPolicyMappings(state, mappings, i) {
923
- var depth = i;
924
1081
  if (state.policyMapping > 0) {
925
1082
  var mappedFrom = intrinsic.create(null);
926
- mappings.forEach(function (m) { guard.list.append(mappedFrom[m.issuerDomainPolicy] = mappedFrom[m.issuerDomainPolicy] || [], m.subjectDomainPolicy); });
927
- var depthI = leavesAt(state.validPolicyTree, depth);
928
- var anyNodes = depthI.filter(function (nd) { return nd.validPolicy === OID.anyPolicy; });
929
- Object.keys(mappedFrom).forEach(function (idp) {
930
- var idpNodes = depthI.filter(function (nd) { return nd.validPolicy === idp; });
931
- if (idpNodes.length) {
932
- idpNodes.forEach(function (nd) { nd.expectedPolicySet = mappedFrom[idp].slice(); });
933
- } else {
934
- anyNodes.forEach(function (anyNode) {
935
- if (anyNode.parent) addChild(state, anyNode.parent, idp, anyNode.qualifierSet[0] || null, mappedFrom[idp].slice(), []);
936
- });
937
- }
1083
+ mappings.forEach(function (m) {
1084
+ if (m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy) return;
1085
+ guard.list.append(mappedFrom[m.issuerDomainPolicy] = mappedFrom[m.issuerDomainPolicy] || [], m.subjectDomainPolicy);
938
1086
  });
939
- } else {
940
- var mappedSet = intrinsic.create(null);
941
- mappings.forEach(function (m) { mappedSet[m.issuerDomainPolicy] = true; });
942
- if (!state.validPolicyTree) return;
943
- leavesAt(state.validPolicyTree, depth).forEach(function (node) {
944
- if (mappedSet[node.validPolicy] && node.parent) {
945
- var idx = node.parent.children.indexOf(node);
946
- if (idx !== -1) node.parent.children.splice(idx, 1);
1087
+ intrinsic.forEach(intrinsic.keys(mappedFrom), function (idp) {
1088
+ var node = nodeAt(state, i, idp);
1089
+ if (node !== null) {
1090
+ /** @internal (b)(1) */
1091
+ node.expectedPolicySet = mappedFrom[idp].slice();
1092
+ return;
947
1093
  }
1094
+ /** @internal (b)(2): no node for the issuer domain policy, so one is generated under the
1095
+ * anyPolicy node at depth i-1, taking the depth-i anyPolicy node's qualifiers. */
1096
+ var anyHere = nodeAt(state, i, OID.anyPolicy);
1097
+ if (anyHere === null) return;
1098
+ var anyPrev = nodeAt(state, i - 1, OID.anyPolicy);
1099
+ if (anyPrev === null) return;
1100
+ addPolicyNode(state, i, idp, anyHere.qualifierSet[0] || null, mappedFrom[idp].slice(), [anyPrev]);
948
1101
  });
949
- pruneChildless(state.validPolicyTree, depth - 1);
950
- if (treeIsEmpty(state.validPolicyTree)) state.validPolicyTree = null;
951
- }
1102
+ return;
1103
+ }
1104
+ /** @internal (b)(3): mapping is inhibited, so the node for each issuer domain policy is deleted
1105
+ * and every ancestor it leaves childless goes with it. */
1106
+ if (!state.validPolicyGraph) return;
1107
+ var deleted = false;
1108
+ mappings.forEach(function (m) {
1109
+ if (m.issuerDomainPolicy === OID.anyPolicy) return;
1110
+ var node = nodeAt(state, i, m.issuerDomainPolicy);
1111
+ if (node !== null) { removePolicyNode(state, node); deleted = true; }
1112
+ });
1113
+ if (!deleted) return;
1114
+ prunePolicyChildless(state, i - 1);
1115
+ if (graphIsEmpty(state)) state.validPolicyGraph = null;
952
1116
  }
953
1117
 
954
1118
  var TARGET_UNPROCESSED_IF_CRITICAL = intrinsic.create(null);
@@ -1028,11 +1192,20 @@ function _revocationRow(row, rv) {
1028
1192
  * are validated at the entry point. A mis-shaped value throws `path/bad-input`,
1029
1193
  * so it cannot silently go unapplied. Returns `{ valid, revocationChecked, anchorConstraints,
1030
1194
  * path, results, workingPublicKey, workingPublicKeyAlgorithm,
1031
- * workingPublicKeyParameters, validPolicyTree, userConstrainedPolicySet }` where `results[i].checks`
1195
+ * workingPublicKeyParameters, validPolicyGraph, userConstrainedPolicySet }` where `results[i].checks`
1032
1196
  * carries a per-check reason code (`path/*`) for every step. Pure and
1033
1197
  * re-entrant: no input object is mutated. An empty path or a missing anchor
1034
1198
  * throws a typed `PathError`.
1035
1199
  *
1200
+ * Certificate policies are processed as the RFC 9618 `valid_policy_graph`, which replaces the
1201
+ * RFC 5280 sec. 6.1 `valid_policy_tree`. `validPolicyGraph` is a directed acyclic graph holding at
1202
+ * most one node per depth and policy identifier; a node reached from several policies carries them
1203
+ * all in `parents`, where the tree held a separate node under each. The graph is linear in the
1204
+ * number of policies and mappings on the path, so a chain whose tree would not fit inside
1205
+ * `maxPolicyNodes` validates. It is `{ nodes }`, a flat list linked by index, each entry
1206
+ * `{ id, depth, validPolicy, qualifierSet, expectedPolicySet, parents, children }` where `parents`
1207
+ * and `children` hold the ids of other entries and the root is id 0.
1208
+ *
1036
1209
  * A certificate signed with the RFC 9802 stateful hash-based algorithm
1037
1210
  * `id-alg-hss-lms-hashsig` is verified like any other, through the toolkit's own
1038
1211
  * HSS engine rather than the crypto engine. The signature covers the whole
@@ -1240,16 +1413,22 @@ async function _validate(path, opts, run) {
1240
1413
  guard.limits.cap(opts.maxPolicyNodes, "validate: opts.maxPolicyNodes", undefined, { E: E, code: "path/bad-input", min: 1 });
1241
1414
  _assertUserInitialPolicySet(opts.userInitialPolicySet, "validate");
1242
1415
  var anchorNc = _anchorSubtreeSeeds(anchor);
1416
+ /** @internal RFC 5937 sec. 3.2 says how an RFC 5914 anchor's own constraints reach this
1417
+ * algorithm, and the rule is uniform: the stricter of the anchor's value and the caller's wins.
1418
+ * Its name constraints are a SEPARATE generation from the caller's and from the `nameConstraints`
1419
+ * overlay, because generations intersect and a replacement would let one loosen the other. */
1420
+ var taConstraints = _anchorRfc5914Constraints(anchor);
1243
1421
  var seeds = {
1244
- permittedGenerations: [callerPermitted, anchorNc.permitted],
1245
- excluded: intrinsic.concat(callerExcluded, anchorNc.excluded),
1422
+ permittedGenerations: [callerPermitted, anchorNc.permitted, taConstraints.permitted],
1423
+ excluded: intrinsic.concat(intrinsic.concat(callerExcluded, anchorNc.excluded), taConstraints.excluded),
1246
1424
  };
1247
- var anchorNameConstraintsApplied = anchorNc.permitted.length > 0 || anchorNc.excluded.length > 0;
1425
+ var anchorNameConstraintsApplied = anchorNc.permitted.length > 0 || anchorNc.excluded.length > 0 ||
1426
+ taConstraints.permitted.length > 0 || taConstraints.excluded.length > 0;
1248
1427
  var purposeOpts = resolvePurposeOpts(opts);
1249
1428
  var requiredEku = purposeOpts.requiredEku;
1250
1429
  var checkPurpose = purposeOpts.checkPurpose;
1251
1430
 
1252
- var state = initialize(certs, opts, seeds, anchor);
1431
+ var state = initialize(certs, opts, seeds, anchor, taConstraints);
1253
1432
  state._n = n;
1254
1433
  state.requiredEku = requiredEku;
1255
1434
  var verifier = opts.verifier || null;
@@ -1488,28 +1667,42 @@ async function _validate(path, opts, run) {
1488
1667
  workingPublicKey: state.workingPublicKey,
1489
1668
  workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm,
1490
1669
  workingPublicKeyParameters: state.workingPublicKeyParameters,
1491
- validPolicyTree: treeWithoutParent(state.validPolicyTree),
1670
+ validPolicyGraph: graphSnapshot(state.validPolicyGraph),
1492
1671
  userConstrainedPolicySet: ucps,
1493
1672
  });
1494
1673
  }
1495
1674
 
1675
+ /**
1676
+ * @internal RFC 9618 sec. 5.5 rewrites RFC 5280 sec. 6.1.5 step (g) over the graph. The set is read
1677
+ * from the nodes whose only parent is the anyPolicy node, at ANY depth rather than at the deepest
1678
+ * one, plus the anyPolicy node at depth n. Policy qualifiers are not collected: sec. 5.5 step
1679
+ * (g)(4)(ii) lets an application that does not use them skip it, and this returns identifiers.
1680
+ */
1496
1681
  function userConstrainedPolicies(state, n) {
1497
- if (!state.validPolicyTree) return [];
1682
+ if (!state.validPolicyGraph) return [];
1683
+ var authority = [];
1684
+ function note(node) {
1685
+ if (authority.indexOf(node.validPolicy) === -1) guard.list.append(authority, node.validPolicy);
1686
+ }
1687
+ /** @internal (g)(2) */
1688
+ for (var d = 1; d <= n; d++) {
1689
+ nodesAt(state, d).forEach(function (node) {
1690
+ if (node.validPolicy === OID.anyPolicy) return;
1691
+ if (node.parents.length === 1 && node.parents[0].validPolicy === OID.anyPolicy) note(node);
1692
+ });
1693
+ }
1694
+ /** @internal (g)(3) */
1695
+ var anyAtN = nodeAt(state, n, OID.anyPolicy);
1696
+ if (anyAtN !== null) note(anyAtN);
1697
+
1698
+ /** @internal (g)(5) and (g)(6) */
1498
1699
  var uips = state.userInitialPolicySet;
1499
- var anyUser = contains(uips, OID.anyPolicy);
1500
- var leaves = leavesAt(state.validPolicyTree, n);
1501
- var explicit = intrinsic.create(null), hasAnyLeaf = false;
1502
- leaves.forEach(function (node) {
1503
- if (node.validPolicy === OID.anyPolicy) hasAnyLeaf = true;
1504
- else explicit[node.validPolicy] = true;
1505
- });
1506
- var set = intrinsic.create(null);
1507
- Object.keys(explicit).forEach(function (p) { if (anyUser || contains(uips, p)) set[p] = true; });
1508
- if (hasAnyLeaf) {
1509
- if (anyUser) set[OID.anyPolicy] = true;
1510
- else uips.forEach(function (p) { set[p] = true; });
1700
+ if (contains(uips, OID.anyPolicy)) return authority;
1701
+ var out = intrinsic.filter(authority, function (p) { return contains(uips, p); });
1702
+ if (contains(authority, OID.anyPolicy)) {
1703
+ uips.forEach(function (p) { if (out.indexOf(p) === -1) guard.list.append(out, p); });
1511
1704
  }
1512
- return Object.keys(set);
1705
+ return out;
1513
1706
  }
1514
1707
 
1515
1708
 
@@ -2367,6 +2560,70 @@ function _precaptureAnchorNameConstraints(list, who) {
2367
2560
  }
2368
2561
  return out;
2369
2562
  }
2563
+ var _TA_CONSTRAINT_KEYS = intrinsic.assign(intrinsic.create(null), {
2564
+ nameConstraints: 1, policySet: 1, policyFlags: 1, pathLenConstraint: 1 });
2565
+ var _TA_FLAG_KEYS = intrinsic.assign(intrinsic.create(null), {
2566
+ inhibitPolicyMapping: 1, requireExplicitPolicy: 1, inhibitAnyPolicy: 1 });
2567
+ /**
2568
+ * @internal An RFC 5914 anchor's own constraints, snapshotted the way every other anchor field is:
2569
+ * read once from its own data property, copied, and never read again. A caller object that answered
2570
+ * one value to the check and another to the enforcement would otherwise decide the namespace and
2571
+ * the policy set a path is held to.
2572
+ */
2573
+ function _copyAnchorRfc5914Constraints(c, who) {
2574
+ var label = who + ": a trustAnchor constraints";
2575
+ if (c === null || typeof c !== "object" || intrinsic.isArray(c)) {
2576
+ throw E("path/bad-input", label + " must be a { nameConstraints, policySet, policyFlags, pathLenConstraint } object");
2577
+ }
2578
+ guard.identifier.assertPlainRecord(c, E, "path/bad-input", label);
2579
+ guard.identifier.assertKnownKeys(c, _TA_CONSTRAINT_KEYS, E, "path/bad-input",
2580
+ label + " names nameConstraints, policySet, policyFlags and pathLenConstraint. The unknown key was: ");
2581
+ var out = {};
2582
+ var ncD = intrinsic.getOwnPropertyDescriptor(c, "nameConstraints");
2583
+ if (ncD !== undefined) {
2584
+ if (!intrinsic.hasOwn(ncD, "value")) throw E("path/bad-input", label + ".nameConstraints must be a data property, not an accessor");
2585
+ intrinsic.defineProperty(out, "nameConstraints", { value: _copyAnchorNameConstraints(ncD.value, who + ": a trustAnchor constraints"), enumerable: true, configurable: true, writable: true });
2586
+ }
2587
+ var psD = intrinsic.getOwnPropertyDescriptor(c, "policySet");
2588
+ if (psD !== undefined) {
2589
+ if (!intrinsic.hasOwn(psD, "value")) throw E("path/bad-input", label + ".policySet must be a data property, not an accessor");
2590
+ var ps = psD.value;
2591
+ if (!intrinsic.isArray(ps) || intrinsic.types.isProxy(ps)) throw E("path/bad-input", label + ".policySet must be a plain array of policy identifiers");
2592
+ var psOut = [];
2593
+ for (var i = 0; i < ps.length; i++) {
2594
+ var pd = intrinsic.getOwnPropertyDescriptor(ps, i);
2595
+ if (pd === undefined || !intrinsic.hasOwn(pd, "value")) throw E("path/bad-input", label + ".policySet entry " + i + " must be a data property, not a hole or an accessor");
2596
+ if (typeof pd.value !== "string" || pd.value.length === 0) throw E("path/bad-input", label + ".policySet entries must be dotted-decimal policy identifiers");
2597
+ guard.list.append(psOut, guard.identifier.assertCanonicalOid(pd.value, E, "path/bad-input", label + ".policySet entry " + pd.value));
2598
+ }
2599
+ intrinsic.defineProperty(out, "policySet", { value: psOut, enumerable: true, configurable: true, writable: true });
2600
+ }
2601
+ var pfD = intrinsic.getOwnPropertyDescriptor(c, "policyFlags");
2602
+ if (pfD !== undefined) {
2603
+ if (!intrinsic.hasOwn(pfD, "value")) throw E("path/bad-input", label + ".policyFlags must be a data property, not an accessor");
2604
+ var pf = pfD.value;
2605
+ if (pf === null || typeof pf !== "object" || intrinsic.isArray(pf)) throw E("path/bad-input", label + ".policyFlags must be an object of boolean flags");
2606
+ guard.identifier.assertPlainRecord(pf, E, "path/bad-input", label + ".policyFlags");
2607
+ guard.identifier.assertKnownKeys(pf, _TA_FLAG_KEYS, E, "path/bad-input", label + ".policyFlags names the three RFC 5914 flags. The unknown key was: ");
2608
+ var fOut = {};
2609
+ intrinsic.forEach(intrinsic.keys(_TA_FLAG_KEYS), function (k) {
2610
+ var fd = intrinsic.getOwnPropertyDescriptor(pf, k);
2611
+ if (fd === undefined) return;
2612
+ if (!intrinsic.hasOwn(fd, "value")) throw E("path/bad-input", label + ".policyFlags." + k + " must be a data property, not an accessor");
2613
+ if (typeof fd.value !== "boolean") throw E("path/bad-input", label + ".policyFlags." + k + " must be a boolean");
2614
+ intrinsic.defineProperty(fOut, k, { value: fd.value, enumerable: true, configurable: true, writable: true });
2615
+ });
2616
+ intrinsic.defineProperty(out, "policyFlags", { value: fOut, enumerable: true, configurable: true, writable: true });
2617
+ }
2618
+ var plD = intrinsic.getOwnPropertyDescriptor(c, "pathLenConstraint");
2619
+ if (plD !== undefined) {
2620
+ if (!intrinsic.hasOwn(plD, "value")) throw E("path/bad-input", label + ".pathLenConstraint must be a data property, not an accessor");
2621
+ if (!intrinsic.isInteger(plD.value) || plD.value < 0) throw E("path/bad-input", label + ".pathLenConstraint must be a non-negative integer");
2622
+ intrinsic.defineProperty(out, "pathLenConstraint", { value: plD.value, enumerable: true, configurable: true, writable: true });
2623
+ }
2624
+ return out;
2625
+ }
2626
+
2370
2627
  function _copyAnchorNameConstraints(nc, who) {
2371
2628
  if (nc === null || typeof nc !== "object" || intrinsic.isArray(nc)) {
2372
2629
  throw E("path/bad-input", who + ": a trustAnchor nameConstraints must be a { permitted, excluded } object of { tag, base } subtree entries");
@@ -2435,7 +2692,7 @@ function _copyRdnsDeep(rdns) {
2435
2692
  function toAnchor(entry, verb) {
2436
2693
  var who = verb || "build";
2437
2694
  var isTuple = false, alg = null, pubRaw, nameRaw = null, pk = null, purposesSnap, distrustSnap;
2438
- var subjectDerRaw, labelRaw, mozRaw, ncSnap;
2695
+ var subjectDerRaw, labelRaw, mozRaw, ncSnap, taSnap;
2439
2696
  if (intrinsic.types.isProxy(entry)) {
2440
2697
  throw E("path/bad-input", who + ": a trustAnchor must not be a Proxy");
2441
2698
  }
@@ -2454,6 +2711,8 @@ function toAnchor(entry, verb) {
2454
2711
  distrustSnap = _normalizeConstraintField(dRaw, E, "path/bad-input", who);
2455
2712
  var ncRaw = _captureConstraintMap(entry, "nameConstraints", E, "path/bad-input", who);
2456
2713
  ncSnap = ncRaw == null ? undefined : _copyAnchorNameConstraints(ncRaw, who);
2714
+ var taRaw = _captureConstraintMap(entry, "constraints", E, "path/bad-input", who);
2715
+ taSnap = taRaw == null ? undefined : _copyAnchorRfc5914Constraints(taRaw, who);
2457
2716
  subjectDerRaw = _captureOwnData(entry, "subjectDer");
2458
2717
  labelRaw = _captureOwnData(entry, "label");
2459
2718
  mozRaw = _captureOwnData(entry, "mozillaCaPolicy");
@@ -2522,6 +2781,7 @@ function toAnchor(entry, verb) {
2522
2781
  if (purposesSnap !== undefined) intrinsic.defineProperty(flat, "purposes", { value: purposesSnap, enumerable: true, configurable: true, writable: true });
2523
2782
  if (distrustSnap !== undefined) intrinsic.defineProperty(flat, "distrustAfter", { value: distrustSnap, enumerable: true, configurable: true, writable: true });
2524
2783
  if (ncSnap !== undefined) intrinsic.defineProperty(flat, "nameConstraints", { value: ncSnap, enumerable: true, configurable: true, writable: true });
2784
+ if (taSnap !== undefined) intrinsic.defineProperty(flat, "constraints", { value: taSnap, enumerable: true, configurable: true, writable: true });
2525
2785
  if (subjectDerRaw !== undefined) intrinsic.defineProperty(flat, "subjectDer", { value: subjectDerRaw, enumerable: true, configurable: true, writable: true });
2526
2786
  if (labelRaw !== undefined) intrinsic.defineProperty(flat, "label", { value: labelRaw, enumerable: true, configurable: true, writable: true });
2527
2787
  if (mozRaw !== undefined) intrinsic.defineProperty(flat, "mozillaCaPolicy", { value: mozRaw, enumerable: true, configurable: true, writable: true });
@@ -2537,7 +2797,17 @@ function toAnchor(entry, verb) {
2537
2797
  try { cert = coerceCert(entry); }
2538
2798
  catch (e) { throw E("path/bad-input", who + ": a trustAnchor entry must be a { name, publicKey, algorithm } tuple or a certificate", e); }
2539
2799
  var spki = cert.subjectPublicKeyInfo;
2540
- return { name: cert.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters, subjectDer: cert.subject.bytes };
2800
+ var out = { name: cert.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters, subjectDer: cert.subject.bytes };
2801
+ /** @internal The certificate's own basic constraints are associated with the anchor, so its
2802
+ * pathLenConstraint bounds the path below it (RFC 5937 sec. 3.2). Defined rather than assigned,
2803
+ * for the reason every other anchor field is. */
2804
+ var certPathLen = pkix.anchorPathLenFromExtensions(cert.extensions, NS, who + ": the trustAnchor certificate");
2805
+ if (certPathLen !== null) {
2806
+ var c = intrinsic.create(null);
2807
+ c.pathLenConstraint = certPathLen;
2808
+ guard.verdict.set(out, "constraints", c);
2809
+ }
2810
+ return out;
2541
2811
  }
2542
2812
 
2543
2813
  /**
@@ -2773,8 +3043,7 @@ async function _aiaFetchOne(uri, aia) {
2773
3043
  : Buffer.from(res.body == null ? "" : String(res.body), "latin1");
2774
3044
  if (body.length === 0) throw E("path/aia-empty", "an AIA fetch returned an empty body");
2775
3045
  if (body.length > aia.maxResponseBytes) throw E("path/aia-too-large", "an AIA response exceeds the " + aia.maxResponseBytes + "-byte cap");
2776
- var headers = intrinsic.create(null);
2777
- Object.keys(res.headers || {}).forEach(function (k) { headers[k.toLowerCase()] = res.headers[k]; });
3046
+ var headers = guard.header.lowerCased(res.headers);
2778
3047
  return _aiaParseBody(body, headers["content-type"], aia.maxCertsPerResponse);
2779
3048
  }
2780
3049