@blamejs/pki 0.8.10 → 0.8.12

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);
654
698
  });
655
699
  }
656
700
  }
657
- function treeIsEmpty(tree) { return tree.children.length === 0; }
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),
733
+ });
734
+ }
735
+ return { nodes: nodes };
736
+ }
658
737
 
659
738
 
660
739
  function isSubtreeBaseValid(tag, base) {
@@ -727,8 +806,17 @@ function _anchorSubtreeSeeds(anchor) {
727
806
 
728
807
  function initialize(certs, params, seeds, anchor) {
729
808
  var n = certs.length;
809
+ var state = _initialState(certs, params, seeds, anchor, n);
810
+ /** @internal The root is the depth-zero anyPolicy node, and it is indexed like any other, so the
811
+ * "is there a node at this depth for this policy" lookup answers for depth zero too. */
812
+ policyLayer(state, 0)[OID.anyPolicy] = state.validPolicyGraph;
813
+ return state;
814
+ }
815
+
816
+ function _initialState(certs, params, seeds, anchor, n) {
730
817
  return {
731
- validPolicyTree: rootNode(),
818
+ validPolicyGraph: graphRoot(),
819
+ policyIndex: [],
732
820
  policyNodeCount: 1,
733
821
  maxPolicyNodes: params.maxPolicyNodes !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
734
822
  minRsaModulusBits: params.minRsaModulusBits !== undefined ? params.minRsaModulusBits : constants.LIMITS.RSA_MIN_MODULUS_BITS,
@@ -757,55 +845,61 @@ function processPolicies(state, cert, i, checks) {
757
845
  try { cp = decodeExt(cert, OID.certificatePolicies); }
758
846
  catch (e) { guard.list.append(checks,{ name: "policies", ok: false, code: "path/bad-policy" }); return { fatal: true, error: e }; }
759
847
 
760
- if (cp && state.validPolicyTree) {
848
+ if (cp && state.validPolicyGraph) {
761
849
  var policies = cp.value;
762
- var depth = i - 1;
850
+ var prev = i - 1;
763
851
  var anyPolicyActive = state.inhibitAnyPolicy > 0 || (i < state._n && selfIssued(cert));
764
852
  var anyPolicyPresent = false;
765
853
  var anyPolicyQualifiers = null;
854
+
855
+ /** @internal RFC 9618 sec. 5.3 step (d)(1): ONE node per policy, carrying every depth i-1 node
856
+ * that expected it. Creating one per parent instead is what made the structure exponential. */
766
857
  policies.forEach(function (p) {
767
858
  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
- }
859
+ var parents = intrinsic.filter(nodesAt(state, prev), function (node) {
860
+ return contains(node.expectedPolicySet, p.policyIdentifier);
774
861
  });
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
- });
862
+ if (parents.length === 0) {
863
+ /** @internal (d)(1)(ii): no node expected it, so it hangs off the anyPolicy node if there
864
+ * is one. That single anyPolicy parent is what step (g)(2) later reads. */
865
+ var anyPrev = nodeAt(state, prev, OID.anyPolicy);
866
+ if (anyPrev === null) return;
867
+ parents = [anyPrev];
779
868
  }
869
+ addPolicyNode(state, i, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], parents);
780
870
  });
871
+
872
+ /** @internal (d)(2): for each policy some depth i-1 node expects, if depth i has no node for it
873
+ * yet, one node carrying every parent that expected it. */
781
874
  if (anyPolicyPresent && anyPolicyActive) {
782
- leavesAt(state.validPolicyTree, depth).forEach(function (node) {
875
+ var expected = [];
876
+ nodesAt(state, prev).forEach(function (node) {
783
877
  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);
878
+ if (expected.indexOf(ep) === -1) guard.list.append(expected, ep);
879
+ });
880
+ });
881
+ expected.forEach(function (ep) {
882
+ if (nodeAt(state, i, ep) !== null) return;
883
+ var parents = intrinsic.filter(nodesAt(state, prev), function (node) {
884
+ return contains(node.expectedPolicySet, ep);
786
885
  });
886
+ addPolicyNode(state, i, ep, anyPolicyQualifiers, [ep], parents);
787
887
  });
788
888
  }
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;
889
+
890
+ if (state._capHit) return { fatal: true, error: E("path/policy-tree-cap", "policy graph exceeded the node cap") };
891
+ prunePolicyChildless(state, prev);
892
+ if (graphIsEmpty(state)) state.validPolicyGraph = null;
792
893
  } else if (!cp) {
793
- state.validPolicyTree = null;
894
+ state.validPolicyGraph = null;
794
895
  }
795
896
 
796
- if (!(state.explicitPolicy > 0 || state.validPolicyTree !== null)) {
897
+ if (!(state.explicitPolicy > 0 || state.validPolicyGraph !== null)) {
797
898
  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") };
899
+ return { fatal: true, error: E("path/policy-required", "explicit policy required but the valid-policy graph is empty") };
799
900
  }
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
901
  void checks;
902
+ return { fatal: false };
809
903
  }
810
904
 
811
905
  function isNullOrAbsentParams(p) {
@@ -859,7 +953,7 @@ function prepareNext(state, cert, i, checks) {
859
953
  if (pm) {
860
954
  var badAny = mapsAnyPolicy(pm.value);
861
955
  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);
956
+ if (state.validPolicyGraph) applyPolicyMappings(state, pm.value, i);
863
957
  }
864
958
 
865
959
  state.workingIssuerName = cert.subject;
@@ -919,36 +1013,49 @@ function prepareNext(state, cert, i, checks) {
919
1013
  return { fatal: false };
920
1014
  }
921
1015
 
1016
+ /** @internal RFC 9618 sec. 5.4 rewrites RFC 5280 sec. 6.1.4 step (b) over the graph. Each clause is
1017
+ * singular where the tree's was plural, because at most one node at a depth carries a given policy.
1018
+ *
1019
+ * A mapping naming anyPolicy on either side is skipped here as well as refused by the caller
1020
+ * (sec. 6.1.4(a)), because mapping one in would rewrite the anyPolicy node's expected policy set and
1021
+ * the graph's invariant is that it is always {anyPolicy}. The caller refusing it keeps that from
1022
+ * happening today; skipping it here is what keeps it true of this function. */
922
1023
  function applyPolicyMappings(state, mappings, i) {
923
- var depth = i;
924
1024
  if (state.policyMapping > 0) {
925
1025
  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
- }
1026
+ mappings.forEach(function (m) {
1027
+ if (m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy) return;
1028
+ guard.list.append(mappedFrom[m.issuerDomainPolicy] = mappedFrom[m.issuerDomainPolicy] || [], m.subjectDomainPolicy);
938
1029
  });
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);
1030
+ intrinsic.forEach(intrinsic.keys(mappedFrom), function (idp) {
1031
+ var node = nodeAt(state, i, idp);
1032
+ if (node !== null) {
1033
+ /** @internal (b)(1) */
1034
+ node.expectedPolicySet = mappedFrom[idp].slice();
1035
+ return;
947
1036
  }
1037
+ /** @internal (b)(2): no node for the issuer domain policy, so one is generated under the
1038
+ * anyPolicy node at depth i-1, taking the depth-i anyPolicy node's qualifiers. */
1039
+ var anyHere = nodeAt(state, i, OID.anyPolicy);
1040
+ if (anyHere === null) return;
1041
+ var anyPrev = nodeAt(state, i - 1, OID.anyPolicy);
1042
+ if (anyPrev === null) return;
1043
+ addPolicyNode(state, i, idp, anyHere.qualifierSet[0] || null, mappedFrom[idp].slice(), [anyPrev]);
948
1044
  });
949
- pruneChildless(state.validPolicyTree, depth - 1);
950
- if (treeIsEmpty(state.validPolicyTree)) state.validPolicyTree = null;
951
- }
1045
+ return;
1046
+ }
1047
+ /** @internal (b)(3): mapping is inhibited, so the node for each issuer domain policy is deleted
1048
+ * and every ancestor it leaves childless goes with it. */
1049
+ if (!state.validPolicyGraph) return;
1050
+ var deleted = false;
1051
+ mappings.forEach(function (m) {
1052
+ if (m.issuerDomainPolicy === OID.anyPolicy) return;
1053
+ var node = nodeAt(state, i, m.issuerDomainPolicy);
1054
+ if (node !== null) { removePolicyNode(state, node); deleted = true; }
1055
+ });
1056
+ if (!deleted) return;
1057
+ prunePolicyChildless(state, i - 1);
1058
+ if (graphIsEmpty(state)) state.validPolicyGraph = null;
952
1059
  }
953
1060
 
954
1061
  var TARGET_UNPROCESSED_IF_CRITICAL = intrinsic.create(null);
@@ -1028,11 +1135,20 @@ function _revocationRow(row, rv) {
1028
1135
  * are validated at the entry point. A mis-shaped value throws `path/bad-input`,
1029
1136
  * so it cannot silently go unapplied. Returns `{ valid, revocationChecked, anchorConstraints,
1030
1137
  * path, results, workingPublicKey, workingPublicKeyAlgorithm,
1031
- * workingPublicKeyParameters, validPolicyTree, userConstrainedPolicySet }` where `results[i].checks`
1138
+ * workingPublicKeyParameters, validPolicyGraph, userConstrainedPolicySet }` where `results[i].checks`
1032
1139
  * carries a per-check reason code (`path/*`) for every step. Pure and
1033
1140
  * re-entrant: no input object is mutated. An empty path or a missing anchor
1034
1141
  * throws a typed `PathError`.
1035
1142
  *
1143
+ * Certificate policies are processed as the RFC 9618 `valid_policy_graph`, which replaces the
1144
+ * RFC 5280 sec. 6.1 `valid_policy_tree`. `validPolicyGraph` is a directed acyclic graph holding at
1145
+ * most one node per depth and policy identifier; a node reached from several policies carries them
1146
+ * all in `parents`, where the tree held a separate node under each. The graph is linear in the
1147
+ * number of policies and mappings on the path, so a chain whose tree would not fit inside
1148
+ * `maxPolicyNodes` validates. It is `{ nodes }`, a flat list linked by index, each entry
1149
+ * `{ id, depth, validPolicy, qualifierSet, expectedPolicySet, parents, children }` where `parents`
1150
+ * and `children` hold the ids of other entries and the root is id 0.
1151
+ *
1036
1152
  * A certificate signed with the RFC 9802 stateful hash-based algorithm
1037
1153
  * `id-alg-hss-lms-hashsig` is verified like any other, through the toolkit's own
1038
1154
  * HSS engine rather than the crypto engine. The signature covers the whole
@@ -1488,28 +1604,42 @@ async function _validate(path, opts, run) {
1488
1604
  workingPublicKey: state.workingPublicKey,
1489
1605
  workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm,
1490
1606
  workingPublicKeyParameters: state.workingPublicKeyParameters,
1491
- validPolicyTree: treeWithoutParent(state.validPolicyTree),
1607
+ validPolicyGraph: graphSnapshot(state.validPolicyGraph),
1492
1608
  userConstrainedPolicySet: ucps,
1493
1609
  });
1494
1610
  }
1495
1611
 
1612
+ /**
1613
+ * @internal RFC 9618 sec. 5.5 rewrites RFC 5280 sec. 6.1.5 step (g) over the graph. The set is read
1614
+ * from the nodes whose only parent is the anyPolicy node, at ANY depth rather than at the deepest
1615
+ * one, plus the anyPolicy node at depth n. Policy qualifiers are not collected: sec. 5.5 step
1616
+ * (g)(4)(ii) lets an application that does not use them skip it, and this returns identifiers.
1617
+ */
1496
1618
  function userConstrainedPolicies(state, n) {
1497
- if (!state.validPolicyTree) return [];
1619
+ if (!state.validPolicyGraph) return [];
1620
+ var authority = [];
1621
+ function note(node) {
1622
+ if (authority.indexOf(node.validPolicy) === -1) guard.list.append(authority, node.validPolicy);
1623
+ }
1624
+ /** @internal (g)(2) */
1625
+ for (var d = 1; d <= n; d++) {
1626
+ nodesAt(state, d).forEach(function (node) {
1627
+ if (node.validPolicy === OID.anyPolicy) return;
1628
+ if (node.parents.length === 1 && node.parents[0].validPolicy === OID.anyPolicy) note(node);
1629
+ });
1630
+ }
1631
+ /** @internal (g)(3) */
1632
+ var anyAtN = nodeAt(state, n, OID.anyPolicy);
1633
+ if (anyAtN !== null) note(anyAtN);
1634
+
1635
+ /** @internal (g)(5) and (g)(6) */
1498
1636
  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; });
1637
+ if (contains(uips, OID.anyPolicy)) return authority;
1638
+ var out = intrinsic.filter(authority, function (p) { return contains(uips, p); });
1639
+ if (contains(authority, OID.anyPolicy)) {
1640
+ uips.forEach(function (p) { if (out.indexOf(p) === -1) guard.list.append(out, p); });
1511
1641
  }
1512
- return Object.keys(set);
1642
+ return out;
1513
1643
  }
1514
1644
 
1515
1645
 
package/lib/schema-all.js CHANGED
@@ -345,10 +345,13 @@ function _detectOrNull(der, label) {
345
345
 
346
346
  module.exports = {
347
347
  engine: engine,
348
- x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
348
+ x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode,
349
+ decodeExtensions: x509.decodeExtensions, decodeExtension: x509.decodeExtension },
349
350
  c509: { parse: c509.parse, encode: c509.encode },
350
- crl: { parse: crl.parse, pemDecode: crl.pemDecode, pemEncode: crl.pemEncode },
351
- csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
351
+ crl: { parse: crl.parse, pemDecode: crl.pemDecode, pemEncode: crl.pemEncode,
352
+ decodeExtensions: crl.decodeExtensions, decodeExtension: crl.decodeExtension },
353
+ csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode,
354
+ decodeExtensions: csr.decodeExtensions, decodeExtension: csr.decodeExtension },
352
355
  pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
353
356
  pkcs1: {
354
357
  parse: pkcs1.parse, parsePublic: pkcs1.parsePublic,
@@ -361,10 +364,12 @@ module.exports = {
361
364
  cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
362
365
  ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode, pemEncode: ocsp.pemEncode },
363
366
  tsp: { parse: tsp.parse, parseResponse: tsp.parseResponse, parseRequest: tsp.parseRequest, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode, pemEncode: tsp.pemEncode },
364
- crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode, pemEncode: crmf.pemEncode },
367
+ crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode, pemEncode: crmf.pemEncode,
368
+ decodeExtensions: crmf.decodeExtensions, decodeExtension: crmf.decodeExtension },
365
369
  cmp: { parse: cmp.parse, pemDecode: cmp.pemDecode, pemEncode: cmp.pemEncode },
366
370
  csrattrs: { parse: csrattrs.parse },
367
- attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode, pemEncode: attrcert.pemEncode },
371
+ attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode, pemEncode: attrcert.pemEncode,
372
+ decodeExtensions: attrcert.decodeExtensions, decodeExtension: attrcert.decodeExtension },
368
373
  smime: { parseSigningCertificate: smime.parseSigningCertificate, parseSigningCertificateV2: smime.parseSigningCertificateV2, parseSmimeCapabilities: smime.parseSmimeCapabilities, decodeAttribute: smime.decodeAttribute },
369
374
  cmc: { parse: cmc.parse, parsePkiData: cmc.parsePkiData, parsePkiResponse: cmc.parsePkiResponse },
370
375
  pem: {
@@ -590,19 +590,9 @@ _AC_EXT_DECODERS[_Oav("acProxying")] = _acProxying;
590
590
  /** @internal The three RFC 5280 extensions RFC 5755 sec. 4.3.3 to 4.3.5 profile for an AC are read
591
591
  * through the certificate decoders, so a relying party running the sec. 6 "pointer in AC" scheme
592
592
  * reads the distribution point and the OCSP location off the parse rather than off raw bytes. */
593
- var _CERT_EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
594
- /** @internal The certificate decoders close over the namespace they were built with, so a parse
595
- * that named caps gets its own table and every decode below these three reads the caller's
596
- * figure. A caller outside a parse passes no context and reuses the module's table. */
597
- function _certExtDecoder(name) {
598
- var key = _Oav(name);
599
- return function (buf, ctx) {
600
- return (ctx ? pkix.certExtensionDecoders(schema.withResources(NS, ctx)).byOid : _CERT_EXT_DECODERS)[key](buf);
601
- };
602
- }
603
- _AC_EXT_DECODERS[_Oav("authorityKeyIdentifier")] = _certExtDecoder("authorityKeyIdentifier");
604
- _AC_EXT_DECODERS[_Oav("authorityInfoAccess")] = _certExtDecoder("authorityInfoAccess");
605
- _AC_EXT_DECODERS[_Oav("cRLDistributionPoints")] = _certExtDecoder("cRLDistributionPoints");
593
+ _AC_EXT_DECODERS[_Oav("authorityKeyIdentifier")] = pkix.scopedCertDecoder(NS, "authorityKeyIdentifier");
594
+ _AC_EXT_DECODERS[_Oav("authorityInfoAccess")] = pkix.scopedCertDecoder(NS, "authorityInfoAccess");
595
+ _AC_EXT_DECODERS[_Oav("cRLDistributionPoints")] = pkix.scopedCertDecoder(NS, "cRLDistributionPoints");
606
596
 
607
597
  // @internal
608
598
  function validateAttributeValue(type, valueDer) {
@@ -614,6 +604,108 @@ function validateExtensionValue(extnOid, extnValueDer) {
614
604
  return dec ? dec(extnValueDer) : null;
615
605
  }
616
606
 
607
+ /** @internal The criticality RFC 5755 fixes for each extension it profiles for an attribute
608
+ * certificate. The AC signer refuses to write an extension against this table and the extension
609
+ * door reports one that arrived against it, from this one row set. */
610
+ var AC_FIXED_CRITICALITY_ROWS = [
611
+ ["acAuditIdentity", true, "RFC 5755 sec. 4.3.1"], ["targetInformation", true, "RFC 5755 sec. 4.3.2"],
612
+ ["authorityKeyIdentifier", false, "RFC 5755 sec. 4.3.3"], ["authorityInfoAccess", false, "RFC 5755 sec. 4.3.4"],
613
+ ["cRLDistributionPoints", false, "RFC 5755 sec. 4.3.5"], ["noRevAvail", false, "RFC 5755 sec. 4.3.6"],
614
+ ["acProxying", true, "RFC 5755 sec. 7.3"], ["aaControls", true, "RFC 5755 sec. 7.4"],
615
+ ];
616
+ function extensionCriticality(oidRegistry) {
617
+ var table = intrinsic.create(null);
618
+ intrinsic.forEach(AC_FIXED_CRITICALITY_ROWS, function (r) {
619
+ table[oidRegistry.byName(r[0])] = { critical: r[1], citation: r[2] };
620
+ });
621
+ return table;
622
+ }
623
+
624
+ /** @internal The attribute-certificate scope. The table is this module's own, passed to the shared
625
+ * door rather than reached for from it, because a registry in `schema-pkix.js` that required this
626
+ * module would invert the dependency. */
627
+ var AC_SCOPES = intrinsic.assign(intrinsic.create(null), {
628
+ "attribute-certificate": {
629
+ /** @internal Every decoder in this table takes the walk context and threads it into its own
630
+ * decode, so the one table serves every call and the caller's caps still reach the value. */
631
+ decoders: function () { return _AC_EXT_DECODERS; },
632
+ criticality: extensionCriticality(oid),
633
+ },
634
+ });
635
+
636
+ var DOOR = pkix.extensionDoor({
637
+ ns: NS, ErrorClass: AttrCertError, key: "attrcert", kind: "attributeCertificate",
638
+ what: "the attribute certificate", parse: parse, scopes: AC_SCOPES,
639
+ defaultScope: "attribute-certificate",
640
+ segments: function (ac) { return [{ list: ac.extensions || [], scope: "attribute-certificate" }]; },
641
+ });
642
+
643
+ /**
644
+ * @primitive pki.schema.attrcert.decodeExtensions
645
+ * @signature pki.schema.attrcert.decodeExtensions(acOrBytes, opts?) -> rows
646
+ * @since 0.8.11
647
+ * @status stable
648
+ * @spec RFC 5755 sec. 4.2.9, RFC 5755 sec. 4.3
649
+ * @related pki.schema.attrcert.parse, pki.schema.x509.decodeExtensions
650
+ *
651
+ * Read an attribute certificate's extensions as decoded records. The argument is the object `parse`
652
+ * returned or the bytes `parse` takes.
653
+ *
654
+ * Each row is `{ oid, name, critical, value, scope, index, containerIndex, state, decoded, code,
655
+ * profile }`, with `scope` `"attribute-certificate"` and `containerIndex` `null`. `state` is the
656
+ * discriminator, and `profile` reports the criticality RFC 5755 fixes for the extension.
657
+ *
658
+ * RFC 5755 sec. 5 item 7 has a verifier reject an AC carrying a critical extension it does not
659
+ * recognize. That is the caller's rule to apply, and the two facts it needs, `critical` and
660
+ * `state`, are on the row.
661
+ *
662
+ * @opts
663
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
664
+ * undecodable value. It does not turn an unrecognized OID into a throw.
665
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
666
+ * @example
667
+ * var pair = await pki.key.generate("Ed25519");
668
+ * var key = await pki.key.export(pair.privateKey);
669
+ * var aaCert = await pki.x509.sign({ subject: "Example AA", subjectPublicKey: await pki.key.export(pair.publicKey),
670
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
671
+ * var acDer = await pki.attrcert.sign({ holder: { entityName: [{ directoryName: "CN=Holder" }] },
672
+ * attributes: { role: { roleName: "https://example.test/role/admin" } }, serialNumber: 1n,
673
+ * notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
674
+ * extensions: { noRevAvail: true } }, { cert: aaCert, key: key });
675
+ * var rows = pki.schema.attrcert.decodeExtensions(acDer);
676
+ * rows.filter(function (r) { return r.critical && r.state === "unrecognized"; }).length; // -> 0
677
+ */
678
+ var decodeExtensions = DOOR.decodeExtensions;
679
+
680
+ /**
681
+ * @primitive pki.schema.attrcert.decodeExtension
682
+ * @signature pki.schema.attrcert.decodeExtension(ext, opts?) -> row
683
+ * @since 0.8.11
684
+ * @status stable
685
+ * @spec RFC 5755 sec. 4.3
686
+ * @related pki.schema.attrcert.decodeExtensions
687
+ *
688
+ * Read one attribute-certificate extension record as a decoded row, at scope
689
+ * `"attribute-certificate"`. The argument is `{ oid, name, critical, value }`.
690
+ *
691
+ * @opts
692
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
693
+ * undecodable value.
694
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
695
+ * @example
696
+ * var pair = await pki.key.generate("Ed25519");
697
+ * var key = await pki.key.export(pair.privateKey);
698
+ * var aaCert = await pki.x509.sign({ subject: "Example AA", subjectPublicKey: await pki.key.export(pair.publicKey),
699
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
700
+ * var acDer = await pki.attrcert.sign({ holder: { entityName: [{ directoryName: "CN=Holder" }] },
701
+ * attributes: { role: { roleName: "https://example.test/role/admin" } }, serialNumber: 1n,
702
+ * notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
703
+ * extensions: { noRevAvail: true } }, { cert: aaCert, key: key });
704
+ * var ac = pki.schema.attrcert.parse(acDer);
705
+ * pki.schema.attrcert.decodeExtension(ac.extensions[0]).profile.citation; // -> "RFC 5755 sec. 4.3.6"
706
+ */
707
+ var decodeExtension = DOOR.decodeExtension;
708
+
617
709
  module.exports = {
618
710
  parse: parse,
619
711
  parseV1: parseV1,
@@ -621,6 +713,9 @@ module.exports = {
621
713
  pemEncode: pemEncode,
622
714
  matches: matches,
623
715
  matchesV1: matchesV1,
716
+ decodeExtensions: decodeExtensions,
717
+ decodeExtension: decodeExtension,
718
+ extensionCriticality: extensionCriticality,
624
719
  readGeneralName: readGeneralName,
625
720
  validateAttributeValue: validateAttributeValue,
626
721
  validateExtensionValue: validateExtensionValue,