@blamejs/pki 0.8.54 → 0.8.56
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 +84 -0
- package/lib/acme.js +2 -2
- package/lib/asn1-der.js +17 -5
- package/lib/attrcert-verify.js +1 -1
- package/lib/cmc-verify.js +1 -1
- package/lib/cmp-session.js +2 -2
- package/lib/cmp-verify.js +2 -2
- package/lib/cms-decrypt.js +4 -1
- package/lib/cms-encrypt.js +4 -1
- package/lib/cms-sign.js +26 -12
- package/lib/cms-verify.js +1 -1
- package/lib/composite-kem.js +30 -12
- package/lib/composite-sig.js +42 -23
- package/lib/constants.js +7 -2
- package/lib/crl-verify.js +1 -1
- package/lib/crmf-sign.js +72 -43
- package/lib/crmf-verify.js +1 -1
- package/lib/csr-verify.js +1 -1
- package/lib/ct.js +11 -13
- package/lib/edwards-point.js +1 -1
- package/lib/est.js +3 -3
- package/lib/guard-async.js +22 -1
- package/lib/guard-bytes.js +6 -3
- package/lib/guard-identifier.js +24 -1
- package/lib/guard-intrinsic.js +474 -0
- package/lib/http-digest.js +146 -62
- package/lib/http-transport.js +82 -17
- package/lib/identity-match.js +2 -2
- package/lib/merkle.js +2 -2
- package/lib/ocsp-request-verify.js +1 -1
- package/lib/ocsp.js +46 -22
- package/lib/path-validate.js +234 -88
- package/lib/pbes2.js +14 -7
- package/lib/schema-attrcert.js +2 -2
- package/lib/schema-cmc.js +2 -2
- package/lib/schema-cmp.js +2 -2
- package/lib/schema-cms.js +2 -2
- package/lib/schema-crmf.js +2 -2
- package/lib/schema-csr.js +2 -2
- package/lib/schema-pkix.js +3 -3
- package/lib/schema-x509.js +2 -2
- package/lib/sigstore.js +14 -3
- package/lib/tlog.js +7 -4
- package/lib/trust.js +187 -104
- package/lib/tsp-sign.js +1 -1
- package/lib/webauthn-mds.js +257 -121
- package/lib/webauthn.js +327 -164
- package/lib/webcrypto.js +10 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -60,6 +60,13 @@ var _pop = intrinsic.pop;
|
|
|
60
60
|
var _splice = intrinsic.splice;
|
|
61
61
|
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
62
62
|
var _strIndexOf = intrinsic.uncurry(String.prototype.indexOf);
|
|
63
|
+
/** @internal The case fold a name constraint is compared under. Read off the live prototype, it is
|
|
64
|
+
* the replaceable operation that decides whether a name falls inside an EXCLUDED subtree, and a
|
|
65
|
+
* replacement that mangles the constraint alone while leaving the host as it is makes the exclusion
|
|
66
|
+
* stop matching: measured, a leaf naming `www.bad.example` under an `excludedSubtrees` of
|
|
67
|
+
* `bad.example` was ADMITTED with no failure code at all. Appending to both sides preserves the
|
|
68
|
+
* suffix relation and hides this, so only the asymmetric replacement shows it. */
|
|
69
|
+
var _strToLowerCase = intrinsic.toLowerCase;
|
|
63
70
|
var _resolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
64
71
|
var _reject = intrinsic.uncurry(intrinsic.promiseReject);
|
|
65
72
|
/** @internal The promise constructor, captured at load. `promiseResolve` builds its promise from the
|
|
@@ -81,6 +88,12 @@ var validator = require("./validator-all");
|
|
|
81
88
|
var cms = require("./schema-cms");
|
|
82
89
|
var httpTransport = require("./http-transport");
|
|
83
90
|
var net = require("net");
|
|
91
|
+
/** @internal The address-family classification, captured at load. It decides the SSRF question for an
|
|
92
|
+
* AIA URL: a host the classifier calls an IP LITERAL is fetched without requiring the transport to
|
|
93
|
+
* declare that it filters resolved addresses, so a replacement answering that a DNS name is a
|
|
94
|
+
* literal fetches it under a guarantee nothing made. The export is writable, and this runs before
|
|
95
|
+
* the blocklist it feeds. */
|
|
96
|
+
var _netIsIP = net.isIP;
|
|
84
97
|
var compositeSig = require("./composite-sig");
|
|
85
98
|
/** @internal Captured at load: `pki.shbs` is the same module object the public surface exposes, and its
|
|
86
99
|
* `verify` is a writable property, so reading it when a signature is checked would let whoever wrote it
|
|
@@ -92,7 +105,7 @@ var PathError = errors.PathError;
|
|
|
92
105
|
function E(code, message, cause) { return new PathError(code, message, cause); }
|
|
93
106
|
function pathCode(e, fallback) {
|
|
94
107
|
if (!e || typeof e.code !== "string") return fallback;
|
|
95
|
-
if (e.code
|
|
108
|
+
if (_strIndexOf(e.code, "path/") === 0) return e.code;
|
|
96
109
|
if (e.code === "webcrypto/not-supported" || e.code === "webcrypto/data") return "path/unsupported-algorithm";
|
|
97
110
|
return fallback;
|
|
98
111
|
}
|
|
@@ -218,7 +231,7 @@ function resolveRsaPss(paramsBytes) {
|
|
|
218
231
|
throw E("path/unsupported-algorithm", "RSASSA-PSS parameters must be an RSASSA-PSS-params SEQUENCE (RFC 4055)");
|
|
219
232
|
}
|
|
220
233
|
var pssLastTag = -1;
|
|
221
|
-
n.children
|
|
234
|
+
intrinsic.forEach(n.children, function (f) {
|
|
222
235
|
if (f.tagClass !== "context") throw E("path/unsupported-algorithm", "RSASSA-PSS-params fields must be context-tagged (RFC 4055)");
|
|
223
236
|
if (f.tagNumber > 3 || f.tagNumber <= pssLastTag) throw E("path/unsupported-algorithm", "RSASSA-PSS-params has an unexpected, duplicate, or out-of-order field [" + f.tagNumber + "]");
|
|
224
237
|
pssLastTag = f.tagNumber;
|
|
@@ -492,12 +505,18 @@ function builtinVerify(state, cert) {
|
|
|
492
505
|
if (d.composite) return compositeSig.compositeVerify(state.workingPublicKey, cert.signatureValue.bytes, cert.tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature");
|
|
493
506
|
if (d.shbs) return _resolve(_Promise, _shbsVerify(state.workingPublicKey, cert.signatureValue.bytes, cert.tbsBytes));
|
|
494
507
|
var key;
|
|
495
|
-
|
|
508
|
+
/** @internal Both continuations come from the capture. A continuation is read off the promise at
|
|
509
|
+
* the call, and the one below IS where the signature verdict is decided: a replacement settling
|
|
510
|
+
* the import step hands this a key nothing imported, and one settling the verify step reports
|
|
511
|
+
* `ok: true` for a signature that was never checked, after which a chain whose issuer name matches
|
|
512
|
+
* a supplied root is accepted on a signature that does not verify. */
|
|
513
|
+
var verified = intrinsic.chain(_importVerifyKey(state.workingPublicKey, d), function (k) {
|
|
496
514
|
key = k;
|
|
497
515
|
var sig = cert.signatureValue.bytes;
|
|
498
516
|
if (d.ecdsa) sig = validator.sig.ecdsaDerToP1363(sig, key.algorithm.namedCurve, PathError, "path/bad-signature");
|
|
499
517
|
return _subtleVerify(subtle, d.verify, key, sig, cert.tbsBytes);
|
|
500
|
-
})
|
|
518
|
+
});
|
|
519
|
+
return intrinsic.chain(verified, function (ok) {
|
|
501
520
|
return guard.verdict.of({ ok: ok === true });
|
|
502
521
|
}, function (e) {
|
|
503
522
|
return guard.verdict.of({ ok: false, code: pathCode(e, "path/bad-signature"), error: e });
|
|
@@ -542,7 +561,7 @@ var splitMailbox = guard.name.splitMailbox;
|
|
|
542
561
|
function emailMatch(constraint, mailbox) {
|
|
543
562
|
var mb = splitMailbox(mailbox);
|
|
544
563
|
if (mb === "ambiguous") return "unsupported";
|
|
545
|
-
if (constraint
|
|
564
|
+
if (_strIndexOf(constraint, "@") !== -1) {
|
|
546
565
|
var cb = splitMailbox(constraint);
|
|
547
566
|
if (cb === "ambiguous" || cb === null || mb === null) return "unsupported";
|
|
548
567
|
/** @internal A constraint naming a whole mailbox asks the RFC 5280 sec. 7.5 question, so it is
|
|
@@ -555,10 +574,10 @@ function emailMatch(constraint, mailbox) {
|
|
|
555
574
|
return "unsupported";
|
|
556
575
|
}
|
|
557
576
|
if (mb === null) return "unsupported";
|
|
558
|
-
var host = stripTrailingDot(mb[1]
|
|
577
|
+
var host = stripTrailingDot(_strToLowerCase(mb[1]));
|
|
559
578
|
if (host === "") return "unsupported";
|
|
560
|
-
var c = stripTrailingDot(constraint
|
|
561
|
-
if (c
|
|
579
|
+
var c = stripTrailingDot(_strToLowerCase(constraint));
|
|
580
|
+
if (_charCodeAt(c, 0) === 46) return host.length > c.length && _strSlice(host, -c.length) === c;
|
|
562
581
|
return host === c;
|
|
563
582
|
}
|
|
564
583
|
|
|
@@ -575,10 +594,10 @@ var stripTrailingDot = guard.name.stripTrailingDot;
|
|
|
575
594
|
* fail-open mirror, so it is asked for only where the caller wants reach. A wildcard reaches exactly
|
|
576
595
|
* one label, so a base below the wildcard's parent is reached only when it is one label below it. */
|
|
577
596
|
function hostConstraintMatch(constraint, host, reachIntoBase) {
|
|
578
|
-
var c = stripTrailingDot(constraint
|
|
597
|
+
var c = stripTrailingDot(_strToLowerCase(constraint)), h = stripTrailingDot(_strToLowerCase(host));
|
|
579
598
|
if (c === "") return true;
|
|
580
|
-
if (c
|
|
581
|
-
if (h === c || (h.length > c.length && h
|
|
599
|
+
if (_charCodeAt(c, 0) === 46) return h.length > c.length && _strSlice(h, -c.length) === c;
|
|
600
|
+
if (h === c || (h.length > c.length && _strSlice(h, -(c.length + 1)) === "." + c)) return true;
|
|
582
601
|
if (reachIntoBase === true && h.length > 2 && _charCodeAt(h, 0) === 42 && _charCodeAt(h, 1) === 46) {
|
|
583
602
|
var parent = _strSlice(h, 2);
|
|
584
603
|
if (parent.length > 0 && c.length > parent.length && _strSlice(c, -(parent.length + 1)) === "." + parent) {
|
|
@@ -596,16 +615,16 @@ var isFqdnHost = guard.name.isFqdnHost;
|
|
|
596
615
|
* without them. */
|
|
597
616
|
function _stripBrackets(s) {
|
|
598
617
|
var n = s.length;
|
|
599
|
-
return (n >= 2 && _charCodeAt(s, 0) === 91 && _charCodeAt(s, n - 1) === 93) ? s
|
|
618
|
+
return (n >= 2 && _charCodeAt(s, 0) === 91 && _charCodeAt(s, n - 1) === 93) ? _strSlice(s, 1, n - 1) : s;
|
|
600
619
|
}
|
|
601
620
|
|
|
602
621
|
function uriMatch(constraint, uri) {
|
|
603
622
|
var host = uriHost(uri);
|
|
604
623
|
if (host === null) return "unsupported";
|
|
605
624
|
if (!isFqdnHost(host)) return "unsupported";
|
|
606
|
-
var c = stripTrailingDot(constraint
|
|
607
|
-
if (!isFqdnHost(c
|
|
608
|
-
if (c
|
|
625
|
+
var c = stripTrailingDot(_strToLowerCase(constraint)), h = stripTrailingDot(_strToLowerCase(host));
|
|
626
|
+
if (!isFqdnHost(_charCodeAt(c, 0) === 46 ? _strSlice(c, 1) : c)) return "unsupported";
|
|
627
|
+
if (_charCodeAt(c, 0) === 46) return h.length > c.length && _strSlice(h, -c.length) === c;
|
|
609
628
|
return h === c;
|
|
610
629
|
}
|
|
611
630
|
|
|
@@ -660,15 +679,20 @@ function certNameForms(cert) {
|
|
|
660
679
|
var forms = [];
|
|
661
680
|
var san = decodeExt(cert, OID.subjectAltName);
|
|
662
681
|
var hasRfc822San = false;
|
|
682
|
+
/** @internal The walks that collect the certificate's OWN names come from the captures. A replaced
|
|
683
|
+
* `Array.prototype.forEach` that skips an entry here leaves the name out of `forms`, and a name
|
|
684
|
+
* that was never collected is a name no constraint can match: measured, skipping the leaf's
|
|
685
|
+
* `subjectAltName` entry admitted it under an exclusion that named it exactly. The comparison
|
|
686
|
+
* operations and the subtree collection were converted first and neither covers this. */
|
|
663
687
|
if (san) {
|
|
664
|
-
san.value.names
|
|
688
|
+
intrinsic.forEach(san.value.names, function (nm) {
|
|
665
689
|
if (nm.tagNumber === 1) hasRfc822San = true;
|
|
666
690
|
guard.list.append(forms, { tag: nm.tagNumber, value: nm.value === undefined ? null : nm.value });
|
|
667
691
|
});
|
|
668
692
|
}
|
|
669
693
|
if (!hasRfc822San) {
|
|
670
|
-
cert.subject.rdns
|
|
671
|
-
|
|
694
|
+
intrinsic.forEach(cert.subject.rdns, function (rdn) {
|
|
695
|
+
intrinsic.forEach(rdn, function (atv) {
|
|
672
696
|
if (atv.type === OID.emailAddress && typeof atv.value === "string") guard.list.append(forms, { tag: 1, value: atv.value });
|
|
673
697
|
});
|
|
674
698
|
});
|
|
@@ -690,10 +714,15 @@ function checkNameConstraints(state, cert) {
|
|
|
690
714
|
for (var f = 0; f < forms.length; f++) {
|
|
691
715
|
var nf = forms[f];
|
|
692
716
|
for (var g = 0; g < state.permittedGenerations.length; g++) {
|
|
693
|
-
|
|
717
|
+
/** @internal The filter and the walk come from the captures. An EMPTY result here means "this
|
|
718
|
+
* generation constrains no name of this form", which SKIPS the permitted check for it, so a
|
|
719
|
+
* replaced `filter` answering with an empty array admits a name no subtree permits. The
|
|
720
|
+
* excluded direction refuses on a match and the permitted direction refuses on the absence of
|
|
721
|
+
* one, so this is the arm where a dropped entry opens rather than closes. */
|
|
722
|
+
var formSubtrees = intrinsic.selectList(state.permittedGenerations[g], function (s) { return s.tag === nf.tag; });
|
|
694
723
|
if (!formSubtrees.length) continue;
|
|
695
724
|
var permitted = false, unsupported = false;
|
|
696
|
-
|
|
725
|
+
intrinsic.forEach(formSubtrees, function (s) {
|
|
697
726
|
var r = nameMatchesConstraint(nf.tag, nf.value, { tagNumber: s.tag, value: s.base });
|
|
698
727
|
if (r === true) permitted = true;
|
|
699
728
|
else if (r === "unsupported") unsupported = true;
|
|
@@ -704,13 +733,18 @@ function checkNameConstraints(state, cert) {
|
|
|
704
733
|
return guard.verdict.of({ ok: true });
|
|
705
734
|
}
|
|
706
735
|
|
|
736
|
+
/** @internal The walks that COLLECT the constraints come from the captures, not the live prototypes.
|
|
737
|
+
* Converting the comparisons alone does not cover the collection that feeds them: a replaced
|
|
738
|
+
* `Array.prototype.forEach` that skips an excluded-subtree entry means the exclusion is never
|
|
739
|
+
* recorded, so there is nothing left to compare against. Measured, a leaf naming `www.bad.example`
|
|
740
|
+
* was ADMITTED with no failure code when the entry was skipped on its way in. */
|
|
707
741
|
function absorbNameConstraints(state, decoded) {
|
|
708
742
|
if (decoded.permittedSubtrees.length) {
|
|
709
|
-
guard.list.append(state.permittedGenerations, decoded.permittedSubtrees
|
|
743
|
+
guard.list.append(state.permittedGenerations, intrinsic.mapList(decoded.permittedSubtrees, function (st) {
|
|
710
744
|
return { tag: st.base.tagNumber, base: st.base.value };
|
|
711
745
|
}));
|
|
712
746
|
}
|
|
713
|
-
decoded.excludedSubtrees
|
|
747
|
+
intrinsic.forEach(decoded.excludedSubtrees, function (st) {
|
|
714
748
|
guard.list.append(state.excludedSubtrees, { tag: st.base.tagNumber, base: st.base.value });
|
|
715
749
|
});
|
|
716
750
|
}
|
|
@@ -761,19 +795,22 @@ function addPolicyNode(state, depth, validPolicy, qualifiers, expectedPolicySet,
|
|
|
761
795
|
state.policyNodeCount++;
|
|
762
796
|
}
|
|
763
797
|
for (var i = 0; i < parents.length; i++) {
|
|
764
|
-
if (
|
|
798
|
+
if (intrinsic.indexOf(node.parents, parents[i]) !== -1) continue;
|
|
765
799
|
guard.list.append(node.parents, parents[i]);
|
|
766
800
|
guard.list.append(parents[i].children, node);
|
|
767
801
|
}
|
|
768
802
|
return node;
|
|
769
803
|
}
|
|
770
804
|
function removePolicyNode(state, node) {
|
|
771
|
-
|
|
772
|
-
|
|
805
|
+
/** @internal The unlinking walks come from the captures. Removing a node is how a policy stops
|
|
806
|
+
* being valid for the chain, so a walk that skips an edge leaves the node reachable and the policy
|
|
807
|
+
* still counted after the step that disqualified it. */
|
|
808
|
+
intrinsic.forEach(node.parents, function (p) {
|
|
809
|
+
var idx = intrinsic.indexOf(p.children, node);
|
|
773
810
|
if (idx !== -1) _splice(p.children, idx, 1);
|
|
774
811
|
});
|
|
775
|
-
node.children
|
|
776
|
-
var idx = c.parents
|
|
812
|
+
intrinsic.forEach(node.children, function (c) {
|
|
813
|
+
var idx = intrinsic.indexOf(c.parents, node);
|
|
777
814
|
if (idx !== -1) _splice(c.parents, idx, 1);
|
|
778
815
|
});
|
|
779
816
|
node.parents = [];
|
|
@@ -787,7 +824,10 @@ function removePolicyNode(state, node) {
|
|
|
787
824
|
* as NULL. */
|
|
788
825
|
function prunePolicyChildless(state, depth) {
|
|
789
826
|
for (var d = depth; d > 0; d--) {
|
|
790
|
-
|
|
827
|
+
/** @internal The pruning walk is captured. A node with no children is one no policy below it
|
|
828
|
+
* depends on, and leaving it in place is what makes an emptied graph read as non-empty, which is
|
|
829
|
+
* the test `initialExplicitPolicy` fails a chain on. */
|
|
830
|
+
intrinsic.forEach(nodesAt(state, d), function (node) {
|
|
791
831
|
if (node.children.length === 0) removePolicyNode(state, node);
|
|
792
832
|
});
|
|
793
833
|
}
|
|
@@ -809,7 +849,7 @@ function graphSnapshot(root) {
|
|
|
809
849
|
if (intrinsic.mapHas(indexOfNode, node)) return;
|
|
810
850
|
intrinsic.mapSet(indexOfNode, node, originals.length);
|
|
811
851
|
guard.list.append(originals, node);
|
|
812
|
-
node.children
|
|
852
|
+
intrinsic.forEach(node.children, walk);
|
|
813
853
|
})(root);
|
|
814
854
|
function idsOf(list) {
|
|
815
855
|
var out = [];
|
|
@@ -930,9 +970,9 @@ function _mergedPolicySet(callerSet, anchorSet) {
|
|
|
930
970
|
var callerAny = callerSet === undefined || callerSet === null || contains(callerSet, OID.anyPolicy);
|
|
931
971
|
var anchorAny = contains(anchorSet, OID.anyPolicy);
|
|
932
972
|
if (anchorAny && callerAny) return [OID.anyPolicy];
|
|
933
|
-
if (anchorAny) return intrinsic.
|
|
934
|
-
if (callerAny) return intrinsic.
|
|
935
|
-
return intrinsic.
|
|
973
|
+
if (anchorAny) return intrinsic.mapList(callerSet, function (p) { return p; });
|
|
974
|
+
if (callerAny) return intrinsic.mapList(anchorSet, function (p) { return p; });
|
|
975
|
+
return intrinsic.selectList(anchorSet, function (p) { return contains(callerSet, p); });
|
|
936
976
|
}
|
|
937
977
|
|
|
938
978
|
/**
|
|
@@ -971,7 +1011,7 @@ function _initialState(certs, params, seeds, anchor, n, ta, policySwitches) {
|
|
|
971
1011
|
policyNodeCount: 1,
|
|
972
1012
|
maxPolicyNodes: params.maxPolicyNodes !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
|
|
973
1013
|
minRsaModulusBits: params.minRsaModulusBits !== undefined ? params.minRsaModulusBits : constants.LIMITS.RSA_MIN_MODULUS_BITS,
|
|
974
|
-
permittedGenerations: intrinsic.
|
|
1014
|
+
permittedGenerations: intrinsic.selectList(seeds.permittedGenerations, function (g) { return g.length > 0; }),
|
|
975
1015
|
excludedSubtrees: seeds.excluded,
|
|
976
1016
|
explicitPolicy: _flagSet(policySwitches.initialExplicitPolicy, ta.policyFlags, "requireExplicitPolicy") ? 0 : n + 1,
|
|
977
1017
|
inhibitAnyPolicy: _flagSet(policySwitches.initialAnyPolicyInhibit, ta.policyFlags, "inhibitAnyPolicy") ? 0 : n + 1,
|
|
@@ -1009,9 +1049,14 @@ function processPolicies(state, cert, i, checks) {
|
|
|
1009
1049
|
|
|
1010
1050
|
/** @internal RFC 9618 sec. 5.3 step (d)(1): ONE node per policy, carrying every depth i-1 node
|
|
1011
1051
|
* that expected it. Creating one per parent instead is what made the structure exponential. */
|
|
1012
|
-
|
|
1052
|
+
/** @internal The walk comes from the capture. It decides which of the certificate's SIGNED
|
|
1053
|
+
* policy entries reach the graph, so a replacement calling the callback with a fabricated
|
|
1054
|
+
* `policyIdentifier` builds a node for an OID the certificate never asserted, and under
|
|
1055
|
+
* `initialExplicitPolicy` with a stated `userInitialPolicySet` that is what makes an otherwise
|
|
1056
|
+
* policy-invalid chain acceptable. */
|
|
1057
|
+
intrinsic.forEach(policies, function (p) {
|
|
1013
1058
|
if (p.policyIdentifier === OID.anyPolicy) { anyPolicyPresent = true; anyPolicyQualifiers = p.qualifiersBytes; return; }
|
|
1014
|
-
var parents = intrinsic.
|
|
1059
|
+
var parents = intrinsic.selectList(nodesAt(state, prev), function (node) {
|
|
1015
1060
|
return contains(node.expectedPolicySet, p.policyIdentifier);
|
|
1016
1061
|
});
|
|
1017
1062
|
if (parents.length === 0) {
|
|
@@ -1028,14 +1073,16 @@ function processPolicies(state, cert, i, checks) {
|
|
|
1028
1073
|
* yet, one node carrying every parent that expected it. */
|
|
1029
1074
|
if (anyPolicyPresent && anyPolicyActive) {
|
|
1030
1075
|
var expected = [];
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1076
|
+
/** @internal The anyPolicy arm's three walks and its membership test come from the captures
|
|
1077
|
+
* for the same reason: each decides which policies the graph carries at this depth. */
|
|
1078
|
+
intrinsic.forEach(nodesAt(state, prev), function (node) {
|
|
1079
|
+
intrinsic.forEach(node.expectedPolicySet, function (ep) {
|
|
1080
|
+
if (intrinsic.indexOf(expected, ep) === -1) guard.list.append(expected, ep);
|
|
1034
1081
|
});
|
|
1035
1082
|
});
|
|
1036
|
-
|
|
1083
|
+
intrinsic.forEach(expected, function (ep) {
|
|
1037
1084
|
if (nodeAt(state, i, ep) !== null) return;
|
|
1038
|
-
var parents = intrinsic.
|
|
1085
|
+
var parents = intrinsic.selectList(nodesAt(state, prev), function (node) {
|
|
1039
1086
|
return contains(node.expectedPolicySet, ep);
|
|
1040
1087
|
});
|
|
1041
1088
|
addPolicyNode(state, i, ep, anyPolicyQualifiers, [ep], parents);
|
|
@@ -1178,15 +1225,23 @@ function prepareNext(state, cert, i, checks) {
|
|
|
1178
1225
|
function applyPolicyMappings(state, mappings, i) {
|
|
1179
1226
|
if (state.policyMapping > 0) {
|
|
1180
1227
|
var mappedFrom = intrinsic.create(null);
|
|
1181
|
-
|
|
1228
|
+
/** @internal Both mapping walks are captured. The pair a mapping carries is what substitutes one
|
|
1229
|
+
* policy for another down the chain, so a walk handing back a fabricated pair maps a policy the
|
|
1230
|
+
* certificate does not state, and one that skips a pair leaves the issuer's policy in place
|
|
1231
|
+
* where the step requires it to be replaced. */
|
|
1232
|
+
intrinsic.forEach(mappings, function (m) {
|
|
1182
1233
|
if (m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy) return;
|
|
1183
1234
|
guard.list.append(mappedFrom[m.issuerDomainPolicy] = mappedFrom[m.issuerDomainPolicy] || [], m.subjectDomainPolicy);
|
|
1184
1235
|
});
|
|
1185
1236
|
intrinsic.forEach(intrinsic.keys(mappedFrom), function (idp) {
|
|
1186
1237
|
var node = nodeAt(state, i, idp);
|
|
1187
1238
|
if (node !== null) {
|
|
1188
|
-
/** @internal (b)(1)
|
|
1189
|
-
|
|
1239
|
+
/** @internal (b)(1). The copy is built through a value this toolkit constructs itself. A
|
|
1240
|
+
* captured `slice` still builds its result through the constructor it reads off the
|
|
1241
|
+
* receiver, and this copy BECOMES the node's expectedPolicySet, so a construction that
|
|
1242
|
+
* substitutes an element maps an issuer domain policy to a subject policy the certificate
|
|
1243
|
+
* never authorized and the path reports valid. */
|
|
1244
|
+
node.expectedPolicySet = intrinsic.copyList(mappedFrom[idp]);
|
|
1190
1245
|
return;
|
|
1191
1246
|
}
|
|
1192
1247
|
/** @internal (b)(2): no node for the issuer domain policy, so one is generated under the
|
|
@@ -1195,7 +1250,7 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1195
1250
|
if (anyHere === null) return;
|
|
1196
1251
|
var anyPrev = nodeAt(state, i - 1, OID.anyPolicy);
|
|
1197
1252
|
if (anyPrev === null) return;
|
|
1198
|
-
addPolicyNode(state, i, idp, anyHere.qualifierSet[0] || null, mappedFrom[idp]
|
|
1253
|
+
addPolicyNode(state, i, idp, anyHere.qualifierSet[0] || null, intrinsic.copyList(mappedFrom[idp]), [anyPrev]);
|
|
1199
1254
|
});
|
|
1200
1255
|
return;
|
|
1201
1256
|
}
|
|
@@ -1203,7 +1258,7 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1203
1258
|
* and every ancestor it leaves childless goes with it. */
|
|
1204
1259
|
if (!state.validPolicyGraph) return;
|
|
1205
1260
|
var deleted = false;
|
|
1206
|
-
|
|
1261
|
+
intrinsic.forEach(mappings, function (m) {
|
|
1207
1262
|
if (m.issuerDomainPolicy === OID.anyPolicy) return;
|
|
1208
1263
|
var node = nodeAt(state, i, m.issuerDomainPolicy);
|
|
1209
1264
|
if (node !== null) { removePolicyNode(state, node); deleted = true; }
|
|
@@ -1479,7 +1534,13 @@ async function _validate(path, opts, run, scope) {
|
|
|
1479
1534
|
throw E("path/bad-input", "validate: opts.minRsaModulusBits may only raise the " + constants.LIMITS.RSA_MIN_MODULUS_BITS + "-bit minimum, not lower it");
|
|
1480
1535
|
}
|
|
1481
1536
|
}
|
|
1482
|
-
|
|
1537
|
+
/** @internal The certificate list is built through a value this toolkit constructs itself, because
|
|
1538
|
+
* its LENGTH is what bounds the validation loop below. A built-in projection builds its result
|
|
1539
|
+
* through the constructor it reads off the receiver, and one answering with a shorter object drops
|
|
1540
|
+
* certificates from the middle of the path: the loop then never reaches the intermediate that
|
|
1541
|
+
* carried a name constraint or a basicConstraints path-length bound, and the shortened path is
|
|
1542
|
+
* what gets validated. */
|
|
1543
|
+
var certs = intrinsic.mapList(path, function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
|
|
1483
1544
|
var n = certs.length;
|
|
1484
1545
|
if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
|
|
1485
1546
|
/** @internal The four switches that turn a requirement on, each read as the boolean it documents
|
|
@@ -1564,7 +1625,7 @@ async function _validate(path, opts, run, scope) {
|
|
|
1564
1625
|
var taConstraints = _anchorRfc5914Constraints(anchor);
|
|
1565
1626
|
var seeds = {
|
|
1566
1627
|
permittedGenerations: [callerPermitted, anchorNc.permitted, taConstraints.permitted],
|
|
1567
|
-
excluded: intrinsic.
|
|
1628
|
+
excluded: intrinsic.concatList(intrinsic.concatList(callerExcluded, anchorNc.excluded), taConstraints.excluded),
|
|
1568
1629
|
};
|
|
1569
1630
|
var anchorNameConstraintsApplied = anchorNc.permitted.length > 0 || anchorNc.excluded.length > 0 ||
|
|
1570
1631
|
taConstraints.permitted.length > 0 || taConstraints.excluded.length > 0;
|
|
@@ -1855,12 +1916,16 @@ function shieldResults(results) {
|
|
|
1855
1916
|
function userConstrainedPolicies(state, n) {
|
|
1856
1917
|
if (!state.validPolicyGraph) return [];
|
|
1857
1918
|
var authority = [];
|
|
1919
|
+
/** @internal The walks and membership tests that build this set are captured. This IS the policy
|
|
1920
|
+
* answer the caller reads and the one `initialExplicitPolicy` requires to be non-empty, so a walk
|
|
1921
|
+
* that adds a policy no node carries reports a chain as valid for it, and one that drops a policy
|
|
1922
|
+
* fails a chain that satisfies the stated set. */
|
|
1858
1923
|
function note(node) {
|
|
1859
|
-
if (
|
|
1924
|
+
if (intrinsic.indexOf(authority, node.validPolicy) === -1) guard.list.append(authority, node.validPolicy);
|
|
1860
1925
|
}
|
|
1861
1926
|
/** @internal (g)(2) */
|
|
1862
1927
|
for (var d = 1; d <= n; d++) {
|
|
1863
|
-
nodesAt(state, d)
|
|
1928
|
+
intrinsic.forEach(nodesAt(state, d), function (node) {
|
|
1864
1929
|
if (node.validPolicy === OID.anyPolicy) return;
|
|
1865
1930
|
if (node.parents.length === 1 && node.parents[0].validPolicy === OID.anyPolicy) note(node);
|
|
1866
1931
|
});
|
|
@@ -1872,9 +1937,9 @@ function userConstrainedPolicies(state, n) {
|
|
|
1872
1937
|
/** @internal (g)(5) and (g)(6) */
|
|
1873
1938
|
var uips = state.userInitialPolicySet;
|
|
1874
1939
|
if (contains(uips, OID.anyPolicy)) return authority;
|
|
1875
|
-
var out = intrinsic.
|
|
1940
|
+
var out = intrinsic.selectList(authority, function (p) { return contains(uips, p); });
|
|
1876
1941
|
if (contains(authority, OID.anyPolicy)) {
|
|
1877
|
-
|
|
1942
|
+
intrinsic.forEach(uips, function (p) { if (intrinsic.indexOf(out, p) === -1) guard.list.append(out, p); });
|
|
1878
1943
|
}
|
|
1879
1944
|
return out;
|
|
1880
1945
|
}
|
|
@@ -2077,7 +2142,10 @@ function crlChecker(crls, opts) {
|
|
|
2077
2142
|
if (crls !== undefined && crls !== null && !intrinsic.isArray(crls)) {
|
|
2078
2143
|
throw E("path/bad-input", "crlChecker: crls must be an array of CRLs");
|
|
2079
2144
|
}
|
|
2080
|
-
|
|
2145
|
+
/** @internal Mapped with the protocol-free verb: `map` builds its result through a constructor read
|
|
2146
|
+
* off the receiver and a species read off that, and a caller's array is where that constructor comes
|
|
2147
|
+
* from. A result that reads as empty is a checker with no CRLs, which answers "nothing revoked". */
|
|
2148
|
+
var parsed = intrinsic.mapList(crls || [], function (c, ci) {
|
|
2081
2149
|
return guard.parsed.acceptDerived(c, "crl", crl.parse, E, "path/bad-input",
|
|
2082
2150
|
"crlChecker: crls[" + ci + "]");
|
|
2083
2151
|
});
|
|
@@ -2277,15 +2345,22 @@ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
|
|
|
2277
2345
|
d = resolveDescriptor(sigAlg);
|
|
2278
2346
|
assertKeyMatchesSigAlg(spkiBytes, sigAlg.oid, d);
|
|
2279
2347
|
} catch (_e) { return _resolve(_Promise, false); }
|
|
2280
|
-
if (d.composite)
|
|
2348
|
+
if (d.composite) {
|
|
2349
|
+
return intrinsic.chain(compositeSig.compositeVerify(spkiBytes, rawSig, tbsBytes, d.composite,
|
|
2350
|
+
PathError, "path/unsupported-algorithm", "path/bad-signature"), function (r) { return r.ok === true; });
|
|
2351
|
+
}
|
|
2281
2352
|
/** @internal RFC 9802 sec. 6 names the CertificateList as well as the Certificate, so a revocation list
|
|
2282
2353
|
* signed with a stateful hash-based key reaches the same engine the certificate path uses. */
|
|
2283
2354
|
if (d.shbs) return _resolve(_Promise, _shbsVerify(spkiBytes, rawSig, tbsBytes).ok === true);
|
|
2284
|
-
|
|
2355
|
+
/** @internal The same two continuations, for the engine every other verifier in this toolkit is
|
|
2356
|
+
* wired to: this is what tells a CRL, a CSR, a CRMF proof and an OCSP response's signature from
|
|
2357
|
+
* one that does not verify, and a `true` resolved past the callback is that verdict. */
|
|
2358
|
+
var checked = intrinsic.chain(_importVerifyKey(spkiBytes, d), function (key) {
|
|
2285
2359
|
var sig = rawSig;
|
|
2286
2360
|
if (d.ecdsa) sig = validator.sig.ecdsaDerToP1363(sig, key.algorithm.namedCurve, PathError, "path/bad-signature");
|
|
2287
2361
|
return _subtleVerify(subtle, d.verify, key, sig, tbsBytes);
|
|
2288
|
-
})
|
|
2362
|
+
});
|
|
2363
|
+
return intrinsic.chain(checked, function (ok) { return ok === true; }, function () { return false; });
|
|
2289
2364
|
}
|
|
2290
2365
|
|
|
2291
2366
|
crlVerify.setEngine(_verifyWithSpki);
|
|
@@ -2364,7 +2439,9 @@ function ocspChecker(responses) {
|
|
|
2364
2439
|
if (responses !== undefined && responses !== null && !intrinsic.isArray(responses)) {
|
|
2365
2440
|
throw E("path/bad-input", "ocspChecker: responses must be an array of OCSP responses");
|
|
2366
2441
|
}
|
|
2367
|
-
|
|
2442
|
+
/** @internal Mapped with the protocol-free verb, for the reason at `crlChecker`: a result reading as
|
|
2443
|
+
* empty is a checker holding no responses, and every certificate it is asked about is "unknown". */
|
|
2444
|
+
var parsed = intrinsic.mapList(responses || [], function (r) { return _ocspFromBytes(r); });
|
|
2368
2445
|
return {
|
|
2369
2446
|
check: async function (cert, issuer, ctx) {
|
|
2370
2447
|
var time = ctx.time;
|
|
@@ -2501,7 +2578,10 @@ function _ocspFromBytes(response) {
|
|
|
2501
2578
|
|
|
2502
2579
|
function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
2503
2580
|
opts = opts || {};
|
|
2504
|
-
|
|
2581
|
+
/** @internal The captured finite test rather than a live `isNaN`: a valid Date's instant is always
|
|
2582
|
+
* finite and an invalid one's is not, so the two answer the same question, and the instant is read
|
|
2583
|
+
* ONCE into the value the test decides on. */
|
|
2584
|
+
if (!guard.time.isDate(time) || !intrinsic.numberIsFinite(guard.time.instantOf(time))) {
|
|
2505
2585
|
return _reject(_Promise, E("path/bad-input", "verifyOcspResponse: time must be a valid Date (the currency + responder-validity check date)"));
|
|
2506
2586
|
}
|
|
2507
2587
|
/** @internal Snapshotted in the synchronous prologue: the bytes compared are the bytes the caller
|
|
@@ -2525,7 +2605,11 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
|
2525
2605
|
if (!boundName) {
|
|
2526
2606
|
return _resolve(_Promise, unbound("the supplied issuer certificate's subject does not match the target certificate's issuer"));
|
|
2527
2607
|
}
|
|
2528
|
-
|
|
2608
|
+
/** @internal Both continuations are taken from the capture, for the reason the verifier's own are:
|
|
2609
|
+
* the first reads the signature verdict that binds this certificate to the supplied issuer, and
|
|
2610
|
+
* the second reads the revocation status the response carries. A settled value in place of either
|
|
2611
|
+
* reports a status for a certificate whose issuer was never confirmed. */
|
|
2612
|
+
return intrinsic.chain(builtinVerify({ workingPublicKey: issuerCert.subjectPublicKeyInfo.bytes }, cert), function (sig) {
|
|
2529
2613
|
if (!sig.ok) return unbound("the target certificate's signature does not verify under the supplied issuer certificate's key");
|
|
2530
2614
|
var issuerCtx = { workingPublicKey: issuerCert.subjectPublicKeyInfo.bytes, workingIssuerName: issuerCert.subject, issuerCert: issuerCert };
|
|
2531
2615
|
var issuerNameCandidates = [cert.issuer.bytes];
|
|
@@ -2534,7 +2618,8 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
|
2534
2618
|
var issuerKeyBits;
|
|
2535
2619
|
try { issuerKeyBits = ocspVerify.ocspKeyValue(issuerCert.subjectPublicKeyInfo.bytes); }
|
|
2536
2620
|
catch (_e) { return unbound("the issuer public key could not be read to recompute the OCSP CertID"); }
|
|
2537
|
-
return ocspCore.evaluateResponse(parsedResponse, cert, issuerCtx, issuerKeyBits,
|
|
2621
|
+
return intrinsic.chain(ocspCore.evaluateResponse(parsedResponse, cert, issuerCtx, issuerKeyBits,
|
|
2622
|
+
issuerNameCandidates, time, opts.historicalMode === true), function (v) {
|
|
2538
2623
|
if (v.revoked) return _withNonceRule(guard.verdict.of({ status: "revoked", responderAuthorized: true, signatureValid: true, matched: true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, revocationReason: v.revoked.revocationReason, revocationTime: v.revoked.revocationTime, reason: v.revoked.reason }), parsedResponse, wantNonce, nonce);
|
|
2539
2624
|
if (v.sawGood) return _withNonceRule(guard.verdict.of({ status: "good", responderAuthorized: true, signatureValid: true, matched: true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, reason: "good" }), parsedResponse, wantNonce, nonce);
|
|
2540
2625
|
return _withNonceRule(guard.verdict.of({ status: "unknown", responderAuthorized: v.responderAuthorized === true, signatureValid: v.signatureValid === true, matched: v.matched === true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, reason: v.reason }), parsedResponse, wantNonce, nonce);
|
|
@@ -2606,7 +2691,7 @@ function resolvePurposeOpts(opts) {
|
|
|
2606
2691
|
if (!intrinsic.isArray(opts.requiredEku) || opts.requiredEku.length === 0) {
|
|
2607
2692
|
throw E("path/bad-input", "validate: opts.requiredEku must be a non-empty array of key-purpose OID names or dotted OID strings");
|
|
2608
2693
|
}
|
|
2609
|
-
requiredEku = opts.requiredEku
|
|
2694
|
+
requiredEku = intrinsic.mapList(opts.requiredEku, function (p) {
|
|
2610
2695
|
if (typeof p !== "string" || p.length === 0) throw E("path/bad-input", "validate: opts.requiredEku entries must be non-empty strings");
|
|
2611
2696
|
if (_startsWithDigit(p)) return guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p));
|
|
2612
2697
|
var dotted = oid.byName(p);
|
|
@@ -3197,7 +3282,7 @@ function _assertUserInitialPolicySet(uips, prefix) {
|
|
|
3197
3282
|
* A candidate whose path length constraint cannot cover the certificates below it sorts under every candidate
|
|
3198
3283
|
* that can, whatever the heuristics score it, and the score orders within each of the two groups. */
|
|
3199
3284
|
function _pushCandidates(frame, scored, stack, counter, down) {
|
|
3200
|
-
|
|
3285
|
+
intrinsic.sort(scored, function (a, b) {
|
|
3201
3286
|
if (!a.ineligible !== !b.ineligible) return a.ineligible ? -1 : 1;
|
|
3202
3287
|
return a.score - b.score;
|
|
3203
3288
|
});
|
|
@@ -3210,21 +3295,23 @@ function _pushCandidates(frame, scored, stack, counter, down) {
|
|
|
3210
3295
|
var childKeys = new Set(frame.keys);
|
|
3211
3296
|
intrinsic.setAdd(childKeys, candKey);
|
|
3212
3297
|
guard.list.append(stack, down
|
|
3213
|
-
? { chain: intrinsic.
|
|
3214
|
-
: { chain: intrinsic.
|
|
3298
|
+
? { chain: intrinsic.concatList(frame.chain, [cand]), hop: frame.hop + 1, keys: childKeys, anchor: frame.anchor }
|
|
3299
|
+
: { chain: intrinsic.concatList([cand], frame.chain), hop: frame.hop + 1, keys: childKeys });
|
|
3215
3300
|
}
|
|
3216
3301
|
return n;
|
|
3217
3302
|
}
|
|
3218
3303
|
|
|
3219
3304
|
|
|
3220
3305
|
function _isBlockedAiaHost(host) {
|
|
3221
|
-
if (host
|
|
3222
|
-
if (
|
|
3306
|
+
if (_charCodeAt(host, 0) === 91 && _charCodeAt(host, host.length - 1) === 93) host = _strSlice(host, 1, -1);
|
|
3307
|
+
if (_netIsIP(host) === 0) return false;
|
|
3223
3308
|
return httpTransport.isBlockedIp(host);
|
|
3224
3309
|
}
|
|
3225
3310
|
|
|
3226
3311
|
function _aiaParseBody(body, contentType, maxCerts) {
|
|
3227
|
-
|
|
3312
|
+
/** @internal The conversion and the fold are captured: together they decide HOW a fetched AIA
|
|
3313
|
+
* response body is parsed, and therefore which certificates are taken out of it. */
|
|
3314
|
+
var certsFirst = _strIndexOf(_strToLowerCase(intrinsic.String(contentType || "")), "pkcs7") >= 0;
|
|
3228
3315
|
var order = certsFirst ? ["certs", "cert"] : ["cert", "certs"];
|
|
3229
3316
|
for (var i = 0; i < order.length; i++) {
|
|
3230
3317
|
try {
|
|
@@ -3267,7 +3354,7 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3267
3354
|
* and the literal-versus-name question have to be answered about the same host. */
|
|
3268
3355
|
var uHost = _urlHostname(u);
|
|
3269
3356
|
if (_isBlockedAiaHost(uHost)) continue;
|
|
3270
|
-
if (
|
|
3357
|
+
if (_netIsIP(_stripBrackets(uHost)) === 0 && !aia.transportGuardsAddresses) continue;
|
|
3271
3358
|
_urlSetHash(u, "");
|
|
3272
3359
|
var uHref = _urlHref(u);
|
|
3273
3360
|
if (intrinsic.setHas(aia.fetchedUrls, uHref) || intrinsic.setHas(seenThisCert, uHref)) continue;
|
|
@@ -3301,7 +3388,7 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3301
3388
|
|
|
3302
3389
|
/**
|
|
3303
3390
|
* @primitive pki.path.build
|
|
3304
|
-
* @signature pki.path.build(leaf, opts) -> Promise<{ valid, path, trustAnchor, result, candidatesConsidered, aiaFetches }>
|
|
3391
|
+
* @signature pki.path.build(leaf, opts) -> Promise<{ valid, path, trustAnchor, result, candidatesConsidered, aiaFetches, depthLimited? }>
|
|
3305
3392
|
* @since 0.3.7
|
|
3306
3393
|
* @status stable
|
|
3307
3394
|
* @spec RFC 4158, RFC 5280
|
|
@@ -3347,7 +3434,12 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3347
3434
|
* `validate` consumes (anchor-proximal first, leaf last, the anchor excluded). Fail-closed: bad
|
|
3348
3435
|
* options throw `path/bad-input`; no chain to any anchor throws `path/no-path`; chains that
|
|
3349
3436
|
* assemble but none validate return `{ valid:false }` with the best failing `validate` result;
|
|
3350
|
-
* the search bound throws `path/build-limit`.
|
|
3437
|
+
* the search bound throws `path/build-limit`. A `{ valid:false }` return carries
|
|
3438
|
+
* `depthLimited: true` when the depth cap stopped an expansion that still had candidates waiting,
|
|
3439
|
+
* which means the chain reported is the best one ASSEMBLED rather than the best one that exists: a
|
|
3440
|
+
* caller that treats `valid:false` as a verdict about the certificates should read that field and
|
|
3441
|
+
* raise `opts.maxDepth` instead. It is absent on a successful return and on a failing one the
|
|
3442
|
+
* search explored to its end. By default `build` is OFFLINE (zero network), so supply
|
|
3351
3443
|
* intermediates in `opts.candidates`. Set `opts.fetchAia: true` to opt in to fetching a MISSING intermediate
|
|
3352
3444
|
* from a certificate's Authority Information Access `caIssuers` URL (RFC 5280 sec. 4.2.2.1) over
|
|
3353
3445
|
* `pki.transport`: the fetch triggers only on a pool miss, every fetched certificate is UNTRUSTED pool material
|
|
@@ -3392,7 +3484,7 @@ var _BUILD_OPTS = (function () {
|
|
|
3392
3484
|
maxAiaPerCert: 1, maxCandidatesConsidered: 1, maxDepth: 1, maxPathCerts: 1,
|
|
3393
3485
|
maxResponseBytes: 1, time: 1, tls: 1, transport: 1, trustAnchors: 1, validate: 1
|
|
3394
3486
|
});
|
|
3395
|
-
|
|
3487
|
+
intrinsic.forEach(intrinsic.keys(_VALIDATE_OPTS), function (k) { o[k] = 1; });
|
|
3396
3488
|
return o;
|
|
3397
3489
|
})();
|
|
3398
3490
|
|
|
@@ -3448,7 +3540,7 @@ async function build(leaf, opts) {
|
|
|
3448
3540
|
var poolCeiling = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES;
|
|
3449
3541
|
if (poolInput.length > poolCeiling) throw E("path/bad-input", "build: the candidate pool has " + poolInput.length + " certificates, exceeding the " + poolCeiling + " ceiling");
|
|
3450
3542
|
var pool;
|
|
3451
|
-
try { pool = intrinsic.
|
|
3543
|
+
try { pool = intrinsic.mapList(poolInput, coerceCert); }
|
|
3452
3544
|
catch (e) { throw E("path/bad-input", "build: a candidate certificate did not parse", e); }
|
|
3453
3545
|
var poolDerKeys = new Set();
|
|
3454
3546
|
for (var pdk = 0; pdk < pool.length; pdk++) intrinsic.setAdd(poolDerKeys, certDerKey(pool[pdk]));
|
|
@@ -3511,20 +3603,30 @@ async function build(leaf, opts) {
|
|
|
3511
3603
|
|
|
3512
3604
|
var BUILD_ONLY_OPT = { candidates: 1, intermediates: 1, trustAnchors: 1, maxDepth: 1, maxCandidatesConsidered: 1, validate: 1,
|
|
3513
3605
|
fetchAia: 1, transport: 1, tls: 1, maxAiaFetches: 1, maxAiaPerCert: 1, aiaTimeout: 1, maxResponseBytes: 1 };
|
|
3514
|
-
|
|
3606
|
+
/** @internal The names this door forwards by VALUE: every validate option that is not consumed by
|
|
3607
|
+
* the builder and not one of the switches typed above. Absence has to survive the copy, so the
|
|
3608
|
+
* presence-preserving snapshot is the one to take: an option the caller omitted stays omitted
|
|
3609
|
+
* rather than arriving at `validate` as a name holding `undefined`.
|
|
3610
|
+
* @guard-via guard.identifier.snapshotPresentOptions */
|
|
3611
|
+
var FORWARD_BY_VALUE = intrinsic.create(null);
|
|
3515
3612
|
intrinsic.forEach(intrinsic.keys(_VALIDATE_OPTS), function (k) {
|
|
3516
|
-
if (intrinsic.hasOwn(BUILD_ONLY_OPT, k)) return;
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3613
|
+
if (intrinsic.hasOwn(BUILD_ONLY_OPT, k) || intrinsic.hasOwn(forwardedSwitches, k)) return;
|
|
3614
|
+
FORWARD_BY_VALUE[k] = 1;
|
|
3615
|
+
});
|
|
3616
|
+
var forwarded = guard.identifier.snapshotPresentOptions(opts, FORWARD_BY_VALUE);
|
|
3617
|
+
/** @internal A switch this door typed is forwarded as the value it typed, overlaid after the copy.
|
|
3618
|
+
* Reading it off the options object again would ask a second time, and normalizing an anchor
|
|
3619
|
+
* between the two runs caller accessors, so the value `validate` is handed need not be the one that
|
|
3620
|
+
* passed here. All four are validate options and none is build-only, so the overlay adds no name
|
|
3621
|
+
* the loop above would have skipped. */
|
|
3622
|
+
intrinsic.forEach(intrinsic.keys(forwardedSwitches), function (k) {
|
|
3623
|
+
forwarded[k] = forwardedSwitches[k];
|
|
3522
3624
|
});
|
|
3523
3625
|
if (buildPermRaw != null) forwarded.initialPermittedSubtrees = buildPermSeeds;
|
|
3524
3626
|
if (buildExclRaw != null) forwarded.initialExcludedSubtrees = buildExclSeeds;
|
|
3525
3627
|
function validateOpts(anchor) {
|
|
3526
3628
|
var vo = Object.create(null);
|
|
3527
|
-
|
|
3629
|
+
intrinsic.forEach(intrinsic.keys(forwarded), function (f) { vo[f] = forwarded[f]; });
|
|
3528
3630
|
vo.trustAnchors = anchor;
|
|
3529
3631
|
return vo;
|
|
3530
3632
|
}
|
|
@@ -3533,6 +3635,10 @@ async function build(leaf, opts) {
|
|
|
3533
3635
|
var considered = 0;
|
|
3534
3636
|
var anyChainAssembled = false;
|
|
3535
3637
|
var bestFail = null;
|
|
3638
|
+
/** @internal Whether the depth bound stopped an expansion that had candidates waiting. A reached
|
|
3639
|
+
* bound is "stopped looking", not "the chain is bad", and the two left this verb returning the
|
|
3640
|
+
* same `valid:false`. */
|
|
3641
|
+
var depthStopped = false;
|
|
3536
3642
|
var success = null;
|
|
3537
3643
|
_assertUserInitialPolicySet(opts.userInitialPolicySet, "build");
|
|
3538
3644
|
var uipsDistinct = null, selectByPolicy = false;
|
|
@@ -3559,7 +3665,7 @@ async function build(leaf, opts) {
|
|
|
3559
3665
|
var frontierCert = rframe.chain.length ? rframe.chain[rframe.chain.length - 1] : null;
|
|
3560
3666
|
var frontierName = frontierCert ? frontierCert.subject.rdns : rframe.anchor.name.rdns;
|
|
3561
3667
|
if (nameMatchSoft(leafCert.issuer.rdns, frontierName)) {
|
|
3562
|
-
var rpath = intrinsic.
|
|
3668
|
+
var rpath = intrinsic.concatList(rframe.chain, [leafCert]);
|
|
3563
3669
|
if (!doValidate) { success = { path: rpath, trustAnchor: rframe.anchor }; break; }
|
|
3564
3670
|
anyChainAssembled = true;
|
|
3565
3671
|
var rres = await validate(rpath, validateOpts(rframe.anchor));
|
|
@@ -3570,7 +3676,6 @@ async function build(leaf, opts) {
|
|
|
3570
3676
|
guard.list.append(accepted, racc);
|
|
3571
3677
|
} else if (!bestFail) bestFail = { path: rpath, trustAnchor: rframe.anchor, result: rres };
|
|
3572
3678
|
}
|
|
3573
|
-
if (rframe.hop >= maxDepth) continue;
|
|
3574
3679
|
var rscored = [];
|
|
3575
3680
|
for (var rpi = 0; rpi < pool.length; rpi++) {
|
|
3576
3681
|
if (nameMatchSoft(pool[rpi].subject.rdns, rframe.anchor.name.rdns) &&
|
|
@@ -3579,6 +3684,13 @@ async function build(leaf, opts) {
|
|
|
3579
3684
|
guard.list.append(rscored, { cand: pool[rpi], score: scoreCandidateReverse(pool[rpi], frontierCert, leafCert, opts.time) });
|
|
3580
3685
|
}
|
|
3581
3686
|
}
|
|
3687
|
+
/** @internal The anchor-first search answers the same question the forward one does, for the
|
|
3688
|
+
* same reason: candidates scored before the bound is consulted, so reaching it can say whether
|
|
3689
|
+
* it stopped anything. */
|
|
3690
|
+
if (rframe.hop >= maxDepth) {
|
|
3691
|
+
if (_anyExpandable(rscored, rframe)) depthStopped = true;
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3582
3694
|
considered += _pushCandidates(rframe, rscored, rstack, counter, true);
|
|
3583
3695
|
}
|
|
3584
3696
|
} else {
|
|
@@ -3625,17 +3737,19 @@ async function build(leaf, opts) {
|
|
|
3625
3737
|
|
|
3626
3738
|
for (var ai = 0; ai < anchors.length; ai++) {
|
|
3627
3739
|
if (!nameMatchSoft(current.issuer.rdns, anchors[ai].name.rdns)) continue;
|
|
3628
|
-
|
|
3740
|
+
/** @internal The REPORTED path is copied the same way the validated one is read, so the chain
|
|
3741
|
+
* an operator sees in the result cannot differ from the chain that was checked. */
|
|
3742
|
+
if (!doValidate) { success = { path: intrinsic.copyList(frame.chain), trustAnchor: anchors[ai] }; break; }
|
|
3629
3743
|
anyChainAssembled = true;
|
|
3630
3744
|
var res = await validate(frame.chain, validateOpts(anchors[ai]));
|
|
3631
3745
|
if (res.valid) {
|
|
3632
|
-
var acc = { valid: true, path: frame.chain
|
|
3746
|
+
var acc = { valid: true, path: intrinsic.copyList(frame.chain), trustAnchor: anchors[ai], result: res };
|
|
3633
3747
|
if (!selectByPolicy) { success = acc; break; }
|
|
3634
3748
|
if (_policyScore(res, uipsDistinct) >= uipsDistinct.length) { success = acc; break; }
|
|
3635
3749
|
guard.list.append(accepted, acc);
|
|
3636
|
-
} else if (!bestFail) bestFail = { path: frame.chain
|
|
3750
|
+
} else if (!bestFail) bestFail = { path: intrinsic.copyList(frame.chain), trustAnchor: anchors[ai], result: res };
|
|
3637
3751
|
}
|
|
3638
|
-
if (success
|
|
3752
|
+
if (success) continue;
|
|
3639
3753
|
|
|
3640
3754
|
var childAki = childAkiKeyId(current);
|
|
3641
3755
|
var scored = [];
|
|
@@ -3645,6 +3759,16 @@ async function build(leaf, opts) {
|
|
|
3645
3759
|
ineligible: _forwardPathLenIneligible(pool[pi], frame.chain) });
|
|
3646
3760
|
}
|
|
3647
3761
|
}
|
|
3762
|
+
/** @internal The candidates are scored BEFORE the depth bound is consulted, so reaching the
|
|
3763
|
+
* bound can say whether it stopped anything. A chain whose last hop lands on the bound with
|
|
3764
|
+
* nothing left to expand was explored to its end, and a chain with candidates waiting was
|
|
3765
|
+
* TRUNCATED: the two produce the same `valid:false` otherwise, and a caller reading that cannot
|
|
3766
|
+
* tell a refused chain from one the search never assembled. Nothing is pushed either way, so the
|
|
3767
|
+
* reorder costs one pool pass on a frame at the bound and changes no outcome. */
|
|
3768
|
+
if (frame.hop >= maxDepth) {
|
|
3769
|
+
if (_anyExpandable(scored, frame)) depthStopped = true;
|
|
3770
|
+
continue;
|
|
3771
|
+
}
|
|
3648
3772
|
if (aiaCtx && frame.hop < maxDepth && aiaCtx.fetches < aiaCtx.maxFetches) {
|
|
3649
3773
|
guard.list.append(deferredAia, { chain: frame.chain, hop: frame.hop, keys: frame.keys, poolMark: pool.length, seq: deferSeq, fetched: false });
|
|
3650
3774
|
deferSeq += 1;
|
|
@@ -3666,17 +3790,39 @@ async function build(leaf, opts) {
|
|
|
3666
3790
|
return guard.verdict.of({ path: success.path, trustAnchor: success.trustAnchor, candidatesConsidered: considered, aiaFetches: aiaFetches });
|
|
3667
3791
|
}
|
|
3668
3792
|
if (anyChainAssembled) {
|
|
3669
|
-
|
|
3793
|
+
/** @internal `depthLimited` is carried only on the FAILING return, and only when the bound
|
|
3794
|
+
* stopped an expansion. On a success the bound hid nothing that mattered, and a caller reading
|
|
3795
|
+
* the field on every verdict would have to ask what it meant for a valid path. */
|
|
3796
|
+
var failed = { valid: false, path: bestFail.path, trustAnchor: bestFail.trustAnchor, result: bestFail.result, candidatesConsidered: considered, aiaFetches: aiaFetches };
|
|
3797
|
+
if (depthStopped) failed.depthLimited = true;
|
|
3798
|
+
return guard.verdict.of(failed);
|
|
3670
3799
|
}
|
|
3671
3800
|
throw E("path/no-path", "build: no certification path from the leaf to any configured trust anchor could be assembled");
|
|
3672
3801
|
}
|
|
3673
3802
|
|
|
3803
|
+
/** @internal Whether raising the depth cap could reach anything through these candidates. A
|
|
3804
|
+
* candidate already on the chain is skipped by `_pushCandidates` on the identity key, so a deeper
|
|
3805
|
+
* search would skip it too: counting it reports a truncated search where the search is exhausted,
|
|
3806
|
+
* which turns a chain this toolkit refuses into one it says it never finished reading. The identity
|
|
3807
|
+
* key and the frame's visited set are the same two the push uses, so the two cannot disagree about
|
|
3808
|
+
* which candidate is reachable. */
|
|
3809
|
+
function _anyExpandable(scored, frame) {
|
|
3810
|
+
for (var i = 0; i < scored.length; i++) {
|
|
3811
|
+
if (!intrinsic.setHas(frame.keys, identityKey(scored[i].cand))) return true;
|
|
3812
|
+
}
|
|
3813
|
+
return false;
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3674
3816
|
/** @internal The fetching checker composes the two gates below, so it loads after this module
|
|
3675
3817
|
* rather than beside it. The require sits in the call so the cycle resolves at first use, which is
|
|
3676
3818
|
* the documented reason an inline require is permitted. */
|
|
3677
3819
|
function fetchingChecker(opts) { return require("./revocation-fetch").fetchingChecker(opts); }
|
|
3678
3820
|
|
|
3679
|
-
|
|
3821
|
+
/** @internal Frozen, because a consumer reads a verb off this object at the call and the verb IS the
|
|
3822
|
+
* check at that point. The metadata anchoring fold reaches `validate` this way, so a replacement
|
|
3823
|
+
* resolving `{ valid: true }` makes an x5c leaf unrelated to every supplied root pass, and the
|
|
3824
|
+
* catalogue that leaf signed is then branded verified. */
|
|
3825
|
+
module.exports = intrinsic.freeze({
|
|
3680
3826
|
validate: validate,
|
|
3681
3827
|
build: build,
|
|
3682
3828
|
anchorFromCert: anchorFromCert,
|
|
@@ -3686,4 +3832,4 @@ module.exports = {
|
|
|
3686
3832
|
verifyOcspResponse: verifyOcspResponse,
|
|
3687
3833
|
PROCESSED_EXTENSIONS: PROCESSED_EXTENSIONS,
|
|
3688
3834
|
TARGET_UNPROCESSED_IF_CRITICAL: TARGET_UNPROCESSED_IF_CRITICAL,
|
|
3689
|
-
};
|
|
3835
|
+
});
|