@blamejs/pki 0.5.7 → 0.5.9
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 +418 -386
- package/MIGRATING.md +43 -0
- package/README.md +12 -12
- package/lib/acme.js +31 -31
- package/lib/asn1-der.js +10 -10
- package/lib/attrcert-sign.js +19 -19
- package/lib/byte-reader.js +6 -6
- package/lib/byte-writer.js +5 -5
- package/lib/cbor-det.js +27 -24
- package/lib/cmc-build.js +97 -92
- package/lib/cmc-verify.js +106 -30
- package/lib/cmp-build.js +25 -25
- package/lib/cmp-session.js +70 -70
- package/lib/cmp-verify.js +71 -57
- package/lib/cms-compress.js +7 -7
- package/lib/cms-decrypt.js +90 -72
- package/lib/cms-encrypt.js +32 -32
- package/lib/cms-sign.js +74 -55
- package/lib/cms-verify.js +97 -75
- package/lib/composite-sig.js +13 -13
- package/lib/constants.js +4 -4
- package/lib/crl-sign.js +22 -22
- package/lib/crl-verify.js +7 -6
- package/lib/crmf-sign.js +14 -14
- package/lib/csr-sign.js +8 -8
- package/lib/ct.js +37 -37
- package/lib/edwards-point.js +7 -7
- package/lib/est.js +98 -55
- package/lib/framework-error.js +5 -5
- package/lib/guard-all.js +5 -3
- package/lib/guard-async.js +4 -4
- package/lib/guard-bytes.js +79 -79
- package/lib/guard-compress.js +17 -17
- package/lib/guard-crypto.js +1 -1
- package/lib/guard-der.js +152 -0
- package/lib/guard-encoding.js +15 -15
- package/lib/guard-header.js +3 -3
- package/lib/guard-identifier.js +16 -16
- package/lib/guard-json.js +15 -15
- package/lib/guard-limits.js +7 -7
- package/lib/guard-name.js +81 -16
- package/lib/guard-parsed.js +80 -80
- package/lib/guard-range.js +19 -19
- package/lib/guard-secret.js +11 -10
- package/lib/guard-text.js +6 -6
- package/lib/guard-time.js +10 -10
- package/lib/hpke.js +18 -17
- package/lib/http-digest.js +35 -35
- package/lib/http-retry-after.js +13 -13
- package/lib/http-transport.js +20 -19
- package/lib/inspect.js +53 -53
- package/lib/ip-utils.js +2 -2
- package/lib/jose.js +13 -13
- package/lib/key.js +16 -16
- package/lib/lint.js +51 -51
- package/lib/merkle.js +51 -36
- package/lib/mime.js +18 -18
- package/lib/ocsp-verify.js +10 -10
- package/lib/ocsp.js +13 -13
- package/lib/oid.js +29 -29
- package/lib/path-validate.js +114 -113
- package/lib/pbes2.js +16 -16
- package/lib/pkcs12-build.js +53 -53
- package/lib/pki-build.js +48 -20
- package/lib/rc2.js +1 -1
- package/lib/rfc3339.js +5 -5
- package/lib/schema-all.js +31 -31
- package/lib/schema-attrcert.js +12 -12
- package/lib/schema-c509.js +149 -197
- package/lib/schema-cmc.js +58 -58
- package/lib/schema-cmp.js +43 -43
- package/lib/schema-cms.js +45 -45
- package/lib/schema-crl.js +7 -7
- package/lib/schema-crmf.js +28 -28
- package/lib/schema-csr.js +12 -12
- package/lib/schema-csrattrs.js +16 -16
- package/lib/schema-engine.js +18 -18
- package/lib/schema-ocsp.js +15 -15
- package/lib/schema-pkcs12.js +20 -20
- package/lib/schema-pkcs8.js +2 -2
- package/lib/schema-pkix.js +131 -126
- package/lib/schema-smime.js +19 -19
- package/lib/schema-tsp.js +12 -12
- package/lib/schema-x509.js +3 -3
- package/lib/shbs.js +18 -18
- package/lib/sign-scheme.js +13 -13
- package/lib/sigstore.js +10 -11
- package/lib/sleep.js +1 -1
- package/lib/smime.js +308 -96
- package/lib/tls-cert-compress.js +18 -18
- package/lib/trust.js +27 -27
- package/lib/tsp-sign.js +17 -17
- package/lib/validator-all.js +1 -1
- package/lib/validator-attcert.js +1 -1
- package/lib/validator-cose.js +43 -44
- package/lib/validator-keydesc.js +3 -3
- package/lib/validator-sig.js +13 -13
- package/lib/validator-tls.js +11 -11
- package/lib/validator-tpm.js +20 -19
- package/lib/webauthn-mds.js +66 -66
- package/lib/webauthn.js +33 -33
- package/lib/webcrypto.js +15 -15
- package/lib/x509-sign.js +13 -13
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -88,7 +88,7 @@ var OID = {
|
|
|
88
88
|
caIssuers: oid.byName("caIssuers"),
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
-
// The set of extension OIDs the validator
|
|
91
|
+
// The set of extension OIDs the validator processes. An unrecognized critical
|
|
92
92
|
// extension outside this set fails the path (6.1.4(o), 6.1.5(e)).
|
|
93
93
|
// extendedKeyUsage is recognized: the critical form is legal (4.2.1.12) and
|
|
94
94
|
// appears in the wild (RFC 6960 4.2.2.2 delegated OCSP responders); its
|
|
@@ -102,7 +102,7 @@ var PROCESSED_EXTENSIONS = {};
|
|
|
102
102
|
// validateCriticalExtensionStructure via the registered decoder. freshestCRL
|
|
103
103
|
// stays OUT: sec. 4.2.1.15 requires it non-critical and the validator does not
|
|
104
104
|
// consult it (no delta merge), so a critical instance fails unrecognized.
|
|
105
|
-
// qcStatements is deliberately
|
|
105
|
+
// qcStatements is deliberately left unprocessed: a critical QC statement (QcLimitValue reliance limit,
|
|
106
106
|
// QcType certificate purpose) asserts qualified-certificate semantics a relying party MUST enforce, and
|
|
107
107
|
// this validator does not enforce them (nor expose a handler to). Marking it processed would let a caller
|
|
108
108
|
// treat a certificate as valid outside its asserted critical QC constraints, so a critical qcStatements
|
|
@@ -113,7 +113,7 @@ var PROCESSED_EXTENSIONS = {};
|
|
|
113
113
|
OID.extKeyUsage, OID.cRLDistributionPoints].
|
|
114
114
|
forEach(function (o) { PROCESSED_EXTENSIONS[o] = true; });
|
|
115
115
|
// Frozen after seeding: this exact object is both consulted by the critical-extension check here and
|
|
116
|
-
// exported (for pki.lint to stay consistent). A caller must not be able to add an OID
|
|
116
|
+
// exported (for pki.lint to stay consistent). A caller must not be able to add an OID, because that
|
|
117
117
|
// would make an attacker's critical, decoder-less extension pass as "processed" and skip both the
|
|
118
118
|
// unrecognized-critical check and structural validation. Freezing makes any such write a no-op.
|
|
119
119
|
Object.freeze(PROCESSED_EXTENSIONS);
|
|
@@ -130,11 +130,11 @@ var SIG_ALGS = {};
|
|
|
130
130
|
// NULL must be present -- RSASSA-PKCS1-v1_5, RFC 4055 sec. 5) or "absent" (parameters
|
|
131
131
|
// must be omitted -- ECDSA/EdDSA/ML-DSA, RFC 5758/8410). A cert deviating from
|
|
132
132
|
// its algorithm's required shape is malformed and rejected before verify.
|
|
133
|
-
// `sameKeyOid` marks the one-shot families whose
|
|
134
|
-
//
|
|
135
|
-
// WebCrypto imports an SPKI of
|
|
136
|
-
// with the real key (
|
|
137
|
-
//
|
|
133
|
+
// `sameKeyOid` marks the one-shot families whose public-key algorithm OID equals
|
|
134
|
+
// the signature algorithm OID: EdDSA, ML-DSA, SLH-DSA. For these, Node's
|
|
135
|
+
// WebCrypto imports an SPKI of another type under the requested name and verifies
|
|
136
|
+
// with the real key (unlike RSA/ECDSA, where a mismatched SPKI is refused at
|
|
137
|
+
// import), so the issuer-key <-> signature-algorithm consistency (RFC 9814 sec. 4)
|
|
138
138
|
// must be checked structurally: the SPKI OID must equal the signature OID.
|
|
139
139
|
function _sig(name, verify, imp, params, ecdsa, sameKeyOid) {
|
|
140
140
|
var entry = { verify: verify, imp: imp, params: params };
|
|
@@ -223,9 +223,9 @@ function resolveRsaPss(paramsBytes) {
|
|
|
223
223
|
// RSASSA-PSS-params ::= SEQUENCE { hashAlgorithm [0] DEFAULT sha1,
|
|
224
224
|
// maskGenAlgorithm [1] DEFAULT mgf1SHA1, saltLength [2] INTEGER DEFAULT 20,
|
|
225
225
|
// trailerField [3] DEFAULT 1 }. WebCrypto verifies with MGF1 keyed to the
|
|
226
|
-
//
|
|
227
|
-
// that deviates cannot be honored, so it is
|
|
228
|
-
//
|
|
226
|
+
// same hash as the signature and trailerField 0xBC (1). A declared value
|
|
227
|
+
// that deviates cannot be honored, so it is rejected; verifying it under
|
|
228
|
+
// WebCrypto's defaults would be a signatureAlgorithm bypass.
|
|
229
229
|
// RFC 4055 DEFAULTs are SHA-1 (hashAlgorithm and mgf1SHA1). Because SHA-1 is
|
|
230
230
|
// rejected, an absent hashAlgorithm or maskGenAlgorithm would resolve to SHA-1
|
|
231
231
|
// and must be REJECTED -- a supported PSS AlgorithmIdentifier must state both
|
|
@@ -244,8 +244,8 @@ function resolveRsaPss(paramsBytes) {
|
|
|
244
244
|
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 + "]");
|
|
245
245
|
pssLastTag = f.tagNumber;
|
|
246
246
|
// Every RSASSA-PSS-params field is an EXPLICIT [n] wrapper (constructed)
|
|
247
|
-
// around
|
|
248
|
-
// primitive/childless or multi-child context field is malformed
|
|
247
|
+
// around exactly one value (an AlgorithmIdentifier or an INTEGER); a
|
|
248
|
+
// primitive/childless or multi-child context field is malformed: reading
|
|
249
249
|
// f.children[0] and ignoring the rest would accept non-DER parameters.
|
|
250
250
|
if (!f.children || f.children.length !== 1) throw E("path/unsupported-algorithm", "malformed RSASSA-PSS parameter field [" + f.tagNumber + "] (an EXPLICIT wrapper carries exactly one value)");
|
|
251
251
|
if (f.tagNumber === 0) {
|
|
@@ -305,7 +305,7 @@ function resolveDescriptor(sigAlg) {
|
|
|
305
305
|
// RFC 9814 sec. 4 issuer-key <-> signature-algorithm consistency (algorithm-confusion
|
|
306
306
|
// defense). For the one-shot families whose public key shares the signature OID
|
|
307
307
|
// (EdDSA, ML-DSA, SLH-DSA), Node's WebCrypto imports an SPKI of a DIFFERENT type
|
|
308
|
-
// under the requested name and verifies with the real key
|
|
308
|
+
// under the requested name and verifies with the real key, so an Ed25519-signed
|
|
309
309
|
// certificate labelled SLH-DSA would otherwise validate. Enforce structurally:
|
|
310
310
|
// the issuer SPKI's algorithm OID MUST equal the signature algorithm OID. (For
|
|
311
311
|
// RSA/ECDSA -- different key vs signature OIDs -- WebCrypto's import already rejects
|
|
@@ -331,9 +331,9 @@ function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
|
|
|
331
331
|
// compositeKeyUsageCheck (below) enforces the sec. 5.2 signature-only keyUsage restriction.
|
|
332
332
|
|
|
333
333
|
// draft-ietf-lamps-pq-composite-sigs sec. 5.2: a certificate whose SubjectPublicKeyInfo
|
|
334
|
-
// carries a composite ML-DSA OID,
|
|
335
|
-
// one signature bit (digitalSignature / nonRepudiation / keyCertSign / cRLSign) and
|
|
336
|
-
// NOT assert any encryption or key-establishment bit
|
|
334
|
+
// carries a composite ML-DSA OID, if it has a keyUsage extension, MUST assert at least
|
|
335
|
+
// one signature bit (digitalSignature / nonRepudiation / keyCertSign / cRLSign) and
|
|
336
|
+
// MUST NOT assert any encryption or key-establishment bit. A composite ML-DSA key is a
|
|
337
337
|
// signature-only key (ML-DSA cannot encrypt or agree, so a "dual usage" key is forbidden
|
|
338
338
|
// even when the traditional component could encrypt). The caller invokes this only for a
|
|
339
339
|
// composite-keyed certificate; an absent keyUsage places no restriction (RFC 5280 4.2.1.3).
|
|
@@ -359,9 +359,9 @@ function compositeKeyUsageCheck(cert) {
|
|
|
359
359
|
var ML_KEM_OIDS = {};
|
|
360
360
|
["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n) { ML_KEM_OIDS[oid.byName(n)] = true; });
|
|
361
361
|
|
|
362
|
-
// RFC 9935 sec. 5: a certificate whose SubjectPublicKeyInfo carries an id-ml-kem-* OID,
|
|
363
|
-
// has a keyUsage extension, MUST assert keyEncipherment as the
|
|
364
|
-
// key is a key-establishment-only key (it cannot sign or agree, so no other bit is legitimate,
|
|
362
|
+
// RFC 9935 sec. 5: a certificate whose SubjectPublicKeyInfo carries an id-ml-kem-* OID, if it
|
|
363
|
+
// has a keyUsage extension, MUST assert keyEncipherment as the only key usage set, since an
|
|
364
|
+
// ML-KEM key is a key-establishment-only key (it cannot sign or agree, so no other bit is legitimate,
|
|
365
365
|
// and an unnamed/reserved bit set alongside keyEncipherment is equally forbidden). The caller
|
|
366
366
|
// invokes this only for an ML-KEM-keyed certificate; an absent keyUsage places no restriction
|
|
367
367
|
// (RFC 5280 sec. 4.2.1.3). This also makes an ML-KEM "CA" (keyCertSign) an explicit reject.
|
|
@@ -380,12 +380,13 @@ function kemKeyUsageCheck(cert) {
|
|
|
380
380
|
return { ok: true };
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
// Import a descriptor's verification key, validating an EdDSA point
|
|
384
|
-
// low-order (e.g. identity or all-zeroes) Ed25519/Ed448 SPKI without
|
|
385
|
-
// verifies a forged signature.
|
|
386
|
-
// (CRL / OCSP-response) path import through, so neither can skip
|
|
387
|
-
// issuer / responder key fails the caller closed (a rejected
|
|
388
|
-
// verdict), never verifying a forged chain or a forged
|
|
383
|
+
// Import a descriptor's verification key, validating an EdDSA point before the import runs:
|
|
384
|
+
// node/OpenSSL import a low-order (e.g. identity or all-zeroes) Ed25519/Ed448 SPKI without
|
|
385
|
+
// complaint and such a key verifies a forged signature. Both the certificate path and the
|
|
386
|
+
// revocation (CRL / OCSP-response) path import through this single seam, so neither can skip
|
|
387
|
+
// the point gate; a low-order issuer / responder key fails the caller closed (a rejected
|
|
388
|
+
// promise the caller maps to a bad verdict), never verifying a forged chain or a forged
|
|
389
|
+
// revocation.
|
|
389
390
|
function _importVerifyKey(spkiBytes, d) {
|
|
390
391
|
try {
|
|
391
392
|
if (d.eddsa) edwardsPoint.validateSpki(spkiBytes, d.eddsa, PathError, "path/bad-signature");
|
|
@@ -474,7 +475,7 @@ function requireCriticalExt(ext, name, checks) {
|
|
|
474
475
|
// Split an addr-spec into [localPart, host] at its single "@". Returns null when
|
|
475
476
|
// there is no "@" (a bare host/domain constraint), or "ambiguous" when there are
|
|
476
477
|
// multiple "@": a conformant certificate rfc822Name is a simple addr-spec with
|
|
477
|
-
//
|
|
478
|
+
// exactly one "@" (RFC 5280 sec. 4.2.1.6 deprecates the quoted local part, and an
|
|
478
479
|
// addr-spec domain never contains "@"), so a multi-"@" mailbox like
|
|
479
480
|
// "a@b"@example.com cannot be split reliably and must fail closed.
|
|
480
481
|
function splitMailbox(addr) {
|
|
@@ -511,8 +512,8 @@ function emailMatch(constraint, mailbox) {
|
|
|
511
512
|
}
|
|
512
513
|
|
|
513
514
|
// Strip a single trailing dot (the absolute-FQDN root label) so "evil.com."
|
|
514
|
-
// and "evil.com" compare equal
|
|
515
|
-
// dNSName constraint.
|
|
515
|
+
// and "evil.com" compare equal. Without that, a trailing-dot SAN would escape
|
|
516
|
+
// a dNSName constraint.
|
|
516
517
|
function stripTrailingDot(s) { return s.charAt(s.length - 1) === "." ? s.slice(0, -1) : s; }
|
|
517
518
|
|
|
518
519
|
// Host-suffix match with the RFC 5280 sec. 4.2.1.10 leading-period rule shared by
|
|
@@ -527,7 +528,7 @@ function hostConstraintMatch(constraint, host) {
|
|
|
527
528
|
// Is `host` a fully qualified domain name (as a URI host constraint requires,
|
|
528
529
|
// RFC 5280 sec. 4.2.1.10)? A dotless single label (localhost), an IPv4/IPv6 literal,
|
|
529
530
|
// or a value carrying non-hostname characters (a scheme "://", a path "/", a
|
|
530
|
-
// port ":") is
|
|
531
|
+
// port ":") is not a FQDN and cannot be matched against a domain-suffix
|
|
531
532
|
// constraint. Only [A-Za-z0-9.-] with at least one dot qualifies.
|
|
532
533
|
function isFqdnHost(host) {
|
|
533
534
|
var h = stripTrailingDot(host);
|
|
@@ -538,12 +539,12 @@ function isFqdnHost(host) {
|
|
|
538
539
|
}
|
|
539
540
|
|
|
540
541
|
// A URI constraint applies to the host part: a leading "." matches subdomains
|
|
541
|
-
// only; a bare host matches that host
|
|
542
|
-
//
|
|
543
|
-
// IP literal,
|
|
544
|
-
// "http://blocked.example"
|
|
545
|
-
//
|
|
546
|
-
// name silently escape a critical constraint
|
|
542
|
+
// only; a bare host matches that host exactly (not subdomains, per sec. 4.2.1.10).
|
|
543
|
+
// Both sides must be a fully qualified domain name. A URI SAN with no host / an
|
|
544
|
+
// IP literal, or a malformed constraint that is not an FQDN (e.g. a full URI
|
|
545
|
+
// "http://blocked.example" where the required form is "blocked.example"), cannot
|
|
546
|
+
// be evaluated, so it returns "unsupported" and the caller fails closed; reporting
|
|
547
|
+
// it as a plain non-match would let the name silently escape a critical constraint.
|
|
547
548
|
function uriMatch(constraint, uri) {
|
|
548
549
|
var host = uriHost(uri);
|
|
549
550
|
if (host === null) return "unsupported";
|
|
@@ -560,10 +561,10 @@ function uriHost(uri) {
|
|
|
560
561
|
if (!m) return null;
|
|
561
562
|
var authority = m[1];
|
|
562
563
|
// authority = [ userinfo "@" ] host [ ":" port ]. RFC 3986 userinfo does not
|
|
563
|
-
// contain a raw "@", so a conformant authority has
|
|
564
|
-
//
|
|
565
|
-
// (null -> uriMatch returns "unsupported" -> the caller rejects the path)
|
|
566
|
-
//
|
|
564
|
+
// contain a raw "@", so a conformant authority has at most one "@". With more
|
|
565
|
+
// than one the host cannot be determined reliably, so this fails closed
|
|
566
|
+
// (null -> uriMatch returns "unsupported" -> the caller rejects the path);
|
|
567
|
+
// guessing a host could slip a URI name constraint.
|
|
567
568
|
var firstAt = authority.indexOf("@");
|
|
568
569
|
if (firstAt !== authority.lastIndexOf("@")) return null;
|
|
569
570
|
if (firstAt !== -1) authority = authority.slice(firstAt + 1);
|
|
@@ -583,9 +584,9 @@ function ipMatch(constraint, addr) {
|
|
|
583
584
|
|
|
584
585
|
// Does GeneralName-like {tagNumber, value} match a constraint base of the same
|
|
585
586
|
// form? Returns true/false for a supported form, null when the forms differ
|
|
586
|
-
// (not comparable), or the string "unsupported" when the forms
|
|
587
|
-
//
|
|
588
|
-
//
|
|
587
|
+
// (not comparable), or the string "unsupported" when the forms match but this
|
|
588
|
+
// validator does not implement that form's comparison. The caller must then fail
|
|
589
|
+
// closed: an unenforceable constraint cannot be read as "no match".
|
|
589
590
|
function nameMatchesConstraint(gnTag, gnValue, base) {
|
|
590
591
|
if (base.tagNumber !== gnTag) return null; // different form -> not comparable
|
|
591
592
|
switch (gnTag) {
|
|
@@ -616,8 +617,8 @@ function certNameForms(cert) {
|
|
|
616
617
|
var san = decodeExt(cert, OID.subjectAltName);
|
|
617
618
|
var hasRfc822San = false;
|
|
618
619
|
if (san) {
|
|
619
|
-
// Preserve
|
|
620
|
-
// not decode (x400Address [3] / ediPartyName [5])
|
|
620
|
+
// Preserve every SAN entry, including a form whose value the validator does
|
|
621
|
+
// not decode (x400Address [3] / ediPartyName [5]); dropping one would let a
|
|
621
622
|
// critical constraint of that same unsupported form pass unenforced. The
|
|
622
623
|
// constraint check fails such a form closed (name-constraint-unsupported).
|
|
623
624
|
san.value.names.forEach(function (nm) {
|
|
@@ -629,10 +630,10 @@ function certNameForms(cert) {
|
|
|
629
630
|
});
|
|
630
631
|
}
|
|
631
632
|
// RFC 5280 sec. 4.2.1.10: the legacy emailAddress in the subject DN is checked as
|
|
632
|
-
// an rfc822Name
|
|
633
|
-
// rfc822Name entry. A SAN of a
|
|
634
|
-
// cover the email, so the subject-DN email must still be constrained
|
|
635
|
-
// excluded/non-permitted mailbox would slip an rfc822Name constraint.
|
|
633
|
+
// an rfc822Name unless the SAN already carries the email identity as an
|
|
634
|
+
// rfc822Name entry. A SAN of a different form (e.g. dNSName only) does not
|
|
635
|
+
// cover the email, so the subject-DN email must still be constrained. Otherwise
|
|
636
|
+
// an excluded/non-permitted mailbox would slip an rfc822Name constraint.
|
|
636
637
|
if (!hasRfc822San) {
|
|
637
638
|
cert.subject.rdns.forEach(function (rdn) {
|
|
638
639
|
rdn.forEach(function (atv) {
|
|
@@ -645,17 +646,17 @@ function certNameForms(cert) {
|
|
|
645
646
|
}
|
|
646
647
|
|
|
647
648
|
// Check a cert's own names against the accumulated constraints. `excluded` is
|
|
648
|
-
// a
|
|
649
|
+
// a union (any match rejects). `permitted` is the intersection of every
|
|
649
650
|
// absorbing cert's permittedSubtrees (6.1.4(g)): the permitted set is tracked
|
|
650
|
-
// as one
|
|
651
|
-
// subtree of form F in
|
|
652
|
-
// would compute the
|
|
651
|
+
// as one generation per absorbing cert, and a name of form F must match a
|
|
652
|
+
// subtree of form F in every generation that constrains form F. A flat pool
|
|
653
|
+
// would compute the union, letting a subordinate CA broaden what its parent
|
|
653
654
|
// permitted (a name-constraint bypass).
|
|
654
655
|
function checkNameConstraints(state, cert) {
|
|
655
656
|
var forms = certNameForms(cert);
|
|
656
657
|
// Excluded: any match -> reject. A constraint of the same form the validator
|
|
657
658
|
// cannot compare ("unsupported") means a critical exclusion it cannot
|
|
658
|
-
// enforce
|
|
659
|
+
// enforce, so it fails closed on the same footing as a match.
|
|
659
660
|
for (var e = 0; e < state.excludedSubtrees.length; e++) {
|
|
660
661
|
var ex = state.excludedSubtrees[e];
|
|
661
662
|
for (var i = 0; i < forms.length; i++) {
|
|
@@ -703,7 +704,7 @@ function absorbNameConstraints(state, decoded) {
|
|
|
703
704
|
function rootNode() {
|
|
704
705
|
return { depth: 0, validPolicy: OID.anyPolicy, qualifierSet: [], expectedPolicySet: [OID.anyPolicy], children: [], parent: null };
|
|
705
706
|
}
|
|
706
|
-
// A deep copy of the valid-policy tree
|
|
707
|
+
// A deep copy of the valid-policy tree that omits the internal `parent` back-pointer,
|
|
707
708
|
// so the structured verdict returned to callers is acyclic (JSON.stringify-safe).
|
|
708
709
|
// The `parent` link is an implementation detail of the 6.1.3 processing, not part
|
|
709
710
|
// of the RFC 5280 valid_policy_tree a consumer inspects.
|
|
@@ -778,7 +779,7 @@ function initialize(certs, params, seeds) {
|
|
|
778
779
|
policyNodeCount: 1,
|
|
779
780
|
maxPolicyNodes: params.maxPolicyNodes !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
|
|
780
781
|
// Each absorbing cert's permittedSubtrees is one generation; a name must be
|
|
781
|
-
// admitted by
|
|
782
|
+
// admitted by every generation (intersection). An initial seed is generation 0.
|
|
782
783
|
permittedGenerations: seeds.permitted.length ? [seeds.permitted] : [],
|
|
783
784
|
excludedSubtrees: seeds.excluded,
|
|
784
785
|
explicitPolicy: params.initialExplicitPolicy ? 0 : n + 1,
|
|
@@ -796,8 +797,8 @@ function initialize(certs, params, seeds) {
|
|
|
796
797
|
|
|
797
798
|
// self-issued = subject DN equals issuer DN. dnEqual throws on a NUL/control
|
|
798
799
|
// DN (CVE-2009-2408); a malformed-DN cert is never "self-issued" (and is failed
|
|
799
|
-
// by the name-chaining check), so
|
|
800
|
-
// whole validate() promise
|
|
800
|
+
// by the name-chaining check), so the throw is swallowed here, keeping it from
|
|
801
|
+
// rejecting the whole validate() promise at these unwrapped call sites.
|
|
801
802
|
function selfIssued(cert) {
|
|
802
803
|
try { return dnEqual(cert.subject.rdns, cert.issuer.rdns); }
|
|
803
804
|
catch (_e) { return false; }
|
|
@@ -812,8 +813,8 @@ function processPolicies(state, cert, i, checks) {
|
|
|
812
813
|
var policies = cp.value;
|
|
813
814
|
var depth = i - 1;
|
|
814
815
|
// anyPolicy processing is active only while inhibit_anyPolicy > 0, or for a
|
|
815
|
-
// self-issued non-final cert
|
|
816
|
-
// cert-asserted anyPolicy. 4.2.1.14 inhibition
|
|
816
|
+
// self-issued non-final cert. What the gate covers is the (d)(2) expansion of
|
|
817
|
+
// a cert-asserted anyPolicy and nothing more. 4.2.1.14 inhibition rests entirely on
|
|
817
818
|
// that gate: a depth-(i-1) anyPolicy node created while processing was
|
|
818
819
|
// active remains matchable in (d)(1)(ii).
|
|
819
820
|
var anyPolicyActive = state.inhibitAnyPolicy > 0 || (i < state._n && selfIssued(cert));
|
|
@@ -829,7 +830,7 @@ function processPolicies(state, cert, i, checks) {
|
|
|
829
830
|
}
|
|
830
831
|
});
|
|
831
832
|
if (!matched) {
|
|
832
|
-
// 6.1.3(d)(1)(ii): no expected-policy match
|
|
833
|
+
// 6.1.3(d)(1)(ii): with no expected-policy match, create the node from a
|
|
833
834
|
// depth-(i-1) anyPolicy node. The RFC runs this step UNCONDITIONALLY
|
|
834
835
|
// (no inhibit clause); gating it would false-reject a path whose
|
|
835
836
|
// specific policy chains through a legitimately created anyPolicy node.
|
|
@@ -901,8 +902,8 @@ function updateWorkingKey(state, cert) {
|
|
|
901
902
|
state.workingPublicKeyParameters = null;
|
|
902
903
|
}
|
|
903
904
|
// When this cert's SPKI omits its algorithm parameters but the working set
|
|
904
|
-
// carries inherited ones, store a reconstructed SPKI
|
|
905
|
-
//
|
|
905
|
+
// carries inherited ones, store a reconstructed SPKI: the next signature verify
|
|
906
|
+
// needs a complete key and would fail on the bare bytes.
|
|
906
907
|
if (isNullOrAbsentParams(keyAlg.parameters) && state.workingPublicKeyParameters) {
|
|
907
908
|
state.workingPublicKey = spliceSpkiParameters(cert.subjectPublicKeyInfo, keyAlg.oid, state.workingPublicKeyParameters);
|
|
908
909
|
} else {
|
|
@@ -915,7 +916,7 @@ function updateWorkingKey(state, cert) {
|
|
|
915
916
|
// carrying an extendedKeyUsage must assert every one (or anyExtendedKeyUsage).
|
|
916
917
|
// Applied to the TARGET cert (its own purposes) AND to every intermediate CA
|
|
917
918
|
// (EKU chaining: an EKU on a CA constrains the purposes below it), so marking
|
|
918
|
-
// extKeyUsage a
|
|
919
|
+
// extKeyUsage a processed critical extension is sound. The semantic gate runs
|
|
919
920
|
// wherever the extension appears, never only on the leaf. A cert with no EKU is
|
|
920
921
|
// unconstrained. Returns true if the cert FAILS the required-purpose check.
|
|
921
922
|
function ekuPurposeFails(cert, requiredEku, checks) {
|
|
@@ -954,7 +955,7 @@ function prepareNext(state, cert, i, checks) {
|
|
|
954
955
|
state.workingIssuerName = cert.subject;
|
|
955
956
|
updateWorkingKey(state, cert);
|
|
956
957
|
|
|
957
|
-
// (g) name constraints absorb
|
|
958
|
+
// (g) name constraints absorb, once this cert's own names have been checked.
|
|
958
959
|
var nc;
|
|
959
960
|
try { nc = decodeExt(cert, OID.nameConstraints); }
|
|
960
961
|
catch (e) { checks.push({ name: "nameConstraints", ok: false, code: pathCode(e, "path/bad-name-constraints") }); return { fatal: true, error: e }; }
|
|
@@ -986,7 +987,7 @@ function prepareNext(state, cert, i, checks) {
|
|
|
986
987
|
if (iapCritErr) return { fatal: true, error: iapCritErr };
|
|
987
988
|
if (iap && iap.value < state.inhibitAnyPolicy) state.inhibitAnyPolicy = iap.value;
|
|
988
989
|
|
|
989
|
-
// (k) basicConstraints cA gate
|
|
990
|
+
// (k) basicConstraints cA gate, the single authoritative CA check.
|
|
990
991
|
var bc;
|
|
991
992
|
try { bc = decodeExt(cert, OID.basicConstraints); }
|
|
992
993
|
catch (e) { checks.push({ name: "basicConstraints", ok: false, code: "path/bad-basic-constraints" }); return { fatal: true, error: e }; }
|
|
@@ -995,7 +996,7 @@ function prepareNext(state, cert, i, checks) {
|
|
|
995
996
|
return { fatal: true, error: E("path/not-a-ca", "intermediate certificate is not a CA (basicConstraints cA is not TRUE, RFC 5280 6.1.4(k))") };
|
|
996
997
|
}
|
|
997
998
|
// 4.2.1.9: a CA certificate used to validate certificate signatures MUST mark
|
|
998
|
-
// basicConstraints critical. A non-critical cA:TRUE is non-conforming
|
|
999
|
+
// basicConstraints critical. A non-critical cA:TRUE is non-conforming. A
|
|
999
1000
|
// relying party that skips non-critical extensions would not see the CA bit.
|
|
1000
1001
|
var bcCritErr = requireCriticalExt(bc, "basicConstraints", checks);
|
|
1001
1002
|
if (bcCritErr) return { fatal: true, error: bcCritErr };
|
|
@@ -1022,9 +1023,9 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1022
1023
|
if (state.policyMapping > 0) {
|
|
1023
1024
|
// 6.1.4(b)(1): for each depth-i node whose valid_policy is an ID-P that
|
|
1024
1025
|
// the extension maps, REPLACE its expected_policy_set with the SET of
|
|
1025
|
-
// subjectDomainPolicy values mapped from that ID-P
|
|
1026
|
-
// the pre-mapping policy
|
|
1027
|
-
// asserting the mapped-away policy
|
|
1026
|
+
// subjectDomainPolicy values mapped from that ID-P. Appending would retain
|
|
1027
|
+
// the pre-mapping policy, letting a later cert satisfy the chain by
|
|
1028
|
+
// asserting the mapped-away policy.
|
|
1028
1029
|
var mappedFrom = {}; // issuerDomainPolicy -> [subjectDomainPolicy, ...]
|
|
1029
1030
|
mappings.forEach(function (m) { (mappedFrom[m.issuerDomainPolicy] = mappedFrom[m.issuerDomainPolicy] || []).push(m.subjectDomainPolicy); });
|
|
1030
1031
|
var depthI = leavesAt(state.validPolicyTree, depth);
|
|
@@ -1034,7 +1035,7 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1034
1035
|
if (idpNodes.length) {
|
|
1035
1036
|
idpNodes.forEach(function (nd) { nd.expectedPolicySet = mappedFrom[idp].slice(); });
|
|
1036
1037
|
} else {
|
|
1037
|
-
// 6.1.4(b)(1): no depth-i ID-P node
|
|
1038
|
+
// 6.1.4(b)(1): with no depth-i ID-P node but a depth-i anyPolicy node, generate
|
|
1038
1039
|
// the missing ID-P node under the anyPolicy node's parent with the mapped
|
|
1039
1040
|
// expected set (else an anyPolicy-only CA loses the mapping).
|
|
1040
1041
|
anyNodes.forEach(function (anyNode) {
|
|
@@ -1045,7 +1046,7 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1045
1046
|
} else {
|
|
1046
1047
|
// 6.1.4(b)(2), policy_mapping == 0: delete every depth-i node whose
|
|
1047
1048
|
// valid_policy is a mapped ID-P, then prune. A prior mapping in the same
|
|
1048
|
-
// extension may have already emptied the tree
|
|
1049
|
+
// extension may have already emptied the tree, so stop if it is gone.
|
|
1049
1050
|
var mappedSet = {};
|
|
1050
1051
|
mappings.forEach(function (m) { mappedSet[m.issuerDomainPolicy] = true; });
|
|
1051
1052
|
// Coverage residual -- unreachable today: applyPolicyMappings is invoked once per
|
|
@@ -1067,14 +1068,14 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1067
1068
|
}
|
|
1068
1069
|
}
|
|
1069
1070
|
|
|
1070
|
-
// policyMappings is semantically processed
|
|
1071
|
+
// policyMappings is semantically processed only in the prepare-for-next step
|
|
1071
1072
|
// (sec. 6.1.4(a),(b)), which does not run for the target certificate. It is also
|
|
1072
|
-
// SHOULD-be-non-critical (sec. 4.2.1.5), so a
|
|
1073
|
-
// is both anomalous and unprocessed
|
|
1074
|
-
//
|
|
1073
|
+
// SHOULD-be-non-critical (sec. 4.2.1.5), so a critical policyMappings on the target
|
|
1074
|
+
// is both anomalous and unprocessed, and must fail closed (sec. 6.1.5(f)); accepting
|
|
1075
|
+
// it would let, e.g., a mapping to/from anyPolicy slip past the sec. 6.1.4(a) rejection
|
|
1075
1076
|
// the intermediate path applies. (nameConstraints / inhibitAnyPolicy are also
|
|
1076
1077
|
// prepare-next-only but are MUST-be-critical CA extensions, so a critical one on
|
|
1077
|
-
// a terminal CA cert is conforming and is
|
|
1078
|
+
// a terminal CA cert is conforming and is not treated as unprocessed here.)
|
|
1078
1079
|
var TARGET_UNPROCESSED_IF_CRITICAL = {};
|
|
1079
1080
|
TARGET_UNPROCESSED_IF_CRITICAL[OID.policyMappings] = true;
|
|
1080
1081
|
Object.freeze(TARGET_UNPROCESSED_IF_CRITICAL);
|
|
@@ -1121,13 +1122,13 @@ function validateCriticalExtensionStructure(cert) {
|
|
|
1121
1122
|
* `userInitialPolicySet`, and `initialPermittedSubtrees` /
|
|
1122
1123
|
* `initialExcludedSubtrees` -- arrays of `{ tag, base }` where `tag` is the
|
|
1123
1124
|
* GeneralName tag number and `base` that form's constraint value), an
|
|
1124
|
-
* optional `requiredEku` (key purposes
|
|
1125
|
-
*
|
|
1126
|
-
* is unrestricted, RFC 5280 4.2.1.12), and an optional
|
|
1127
|
-
* The value-carrying options (`time`, `maxPathCerts`,
|
|
1128
|
-
* subtree seeds, `userInitialPolicySet`, `requiredEku`)
|
|
1129
|
-
* entry point
|
|
1130
|
-
* silently
|
|
1125
|
+
* optional `requiredEku` (key purposes the target's extendedKeyUsage must
|
|
1126
|
+
* assert, given as registered OID names or dotted OID strings; an absent
|
|
1127
|
+
* extension is unrestricted, RFC 5280 4.2.1.12), and an optional
|
|
1128
|
+
* `revocationChecker`. The value-carrying options (`time`, `maxPathCerts`,
|
|
1129
|
+
* `maxPolicyNodes`, the subtree seeds, `userInitialPolicySet`, `requiredEku`)
|
|
1130
|
+
* are validated at the entry point. A mis-shaped value throws `path/bad-input`,
|
|
1131
|
+
* so it cannot silently go unapplied. Returns `{ valid, revocationChecked, anchorConstraints,
|
|
1131
1132
|
* path, results, workingPublicKey, workingPublicKeyAlgorithm,
|
|
1132
1133
|
* workingPublicKeyParameters, validPolicyTree }` where `results[i].checks`
|
|
1133
1134
|
* carries a per-check reason code (`path/*`) for every step. Pure and
|
|
@@ -1142,19 +1143,18 @@ function validateCriticalExtensionStructure(cert) {
|
|
|
1142
1143
|
* one could not be answered at all and the path fails for it. The
|
|
1143
1144
|
* per-certificate `revocation` check carries the `status` it was decided on and
|
|
1144
1145
|
* marks a waiver, so "checked, good" is distinguishable from "could not check,
|
|
1145
|
-
* and you waived it"
|
|
1146
|
-
*
|
|
1147
|
-
* fails
|
|
1148
|
-
* `softFail` says
|
|
1149
|
-
*
|
|
1146
|
+
* and you waived it", a distinction a stored verdict is re-read to settle. A
|
|
1147
|
+
* throw from a checker is a fault in the checker: no status was reported, so the
|
|
1148
|
+
* path fails as `path/revocation-checker-error` carrying the fault whatever
|
|
1149
|
+
* `softFail` says. `softFail` opts into an undetermined answer; the built-in
|
|
1150
|
+
* checkers report one as `{ status: "unknown" }` and do not throw.
|
|
1150
1151
|
*
|
|
1151
1152
|
* `anchorConstraints` reports what the anchor's own trust metadata decided:
|
|
1152
1153
|
* the `checkedPurpose` it was judged under, and whether the `distrustAfter`
|
|
1153
1154
|
* date and the `purposes` delegator map each applied. That metadata is keyed BY
|
|
1154
1155
|
* key purpose, so an anchor carrying it while `opts.checkPurpose` is absent is a
|
|
1155
|
-
* configuration fault (`path/bad-input`)
|
|
1156
|
-
*
|
|
1157
|
-
* current leaf.
|
|
1156
|
+
* configuration fault (`path/bad-input`). A silently inert constraint would let
|
|
1157
|
+
* a root distrusted years ago quietly validate a current leaf.
|
|
1158
1158
|
*
|
|
1159
1159
|
* @example
|
|
1160
1160
|
* var pair = await pki.key.generate("Ed25519");
|
|
@@ -1838,7 +1838,7 @@ function selectDelta(candidates) {
|
|
|
1838
1838
|
* reasons, so a CA that partitions by reason code is served. A DELTA CRL is
|
|
1839
1839
|
* merged onto a complete CRL it may be combined with (sec. 5.2.4 / 6.3.3(c)):
|
|
1840
1840
|
* the delta is searched first, the complete CRL only if the delta left the
|
|
1841
|
-
* status unrevoked, and `removeFromCRL` then releases the certificate
|
|
1841
|
+
* status unrevoked, and `removeFromCRL` then releases the certificate. A
|
|
1842
1842
|
* base+delta pair reports a real verdict where the base alone could not. A
|
|
1843
1843
|
* delta that merges with nothing is still consulted for revocation and still
|
|
1844
1844
|
* blocks "good": merging may turn undetermined into good or revoked, never a
|
|
@@ -2288,9 +2288,9 @@ cmsVerify.setEngine({ build: build, validate: validate, toAnchor: toAnchor,
|
|
|
2288
2288
|
* Build an OCSP-backed `RevocationChecker` for `pki.path.validate`'s
|
|
2289
2289
|
* `revocationChecker` option from a set of pre-fetched OCSP responses (DER/PEM
|
|
2290
2290
|
* or already-parsed). For each certificate it locates a SingleResponse whose
|
|
2291
|
-
* CertID binds this cert's serial to its issuer
|
|
2292
|
-
* and `issuerKeyHash` under the CertID's own hashAlgorithm
|
|
2293
|
-
* a response using either matches
|
|
2291
|
+
* CertID binds this cert's serial to its issuer (recomputing `issuerNameHash`
|
|
2292
|
+
* and `issuerKeyHash` under the CertID's own hashAlgorithm, SHA-1 or SHA-2, so
|
|
2293
|
+
* a response using either matches), confirms the responder is authorized (the
|
|
2294
2294
|
* issuing CA directly, or a valid CA-issued delegate bearing both id-kp-OCSPSigning
|
|
2295
2295
|
* and id-pkix-ocsp-nocheck), verifies the response signature over
|
|
2296
2296
|
* `tbsResponseDataBytes`, checks currency
|
|
@@ -2368,10 +2368,10 @@ function ocspChecker(responses) {
|
|
|
2368
2368
|
* @related pki.ocsp.verify, pki.path.ocspChecker
|
|
2369
2369
|
*
|
|
2370
2370
|
* Verify a single OCSP response for one certificate
|
|
2371
|
-
* against its already-parsed issuer certificate at `time
|
|
2372
|
-
* `pki.ocsp.verify` composes after parsing its inputs (most callers want
|
|
2373
|
-
* ergonomic entry, which also handles DER/PEM decoding and request-nonce
|
|
2374
|
-
* matching). It runs the
|
|
2371
|
+
* against its already-parsed issuer certificate at `time`. This is the lower-level
|
|
2372
|
+
* primitive `pki.ocsp.verify` composes after parsing its inputs (most callers want
|
|
2373
|
+
* that ergonomic entry, which also handles DER/PEM decoding and request-nonce
|
|
2374
|
+
* matching). It runs exactly the gates the path validator's `ocspChecker`
|
|
2375
2375
|
* does: it locates the SingleResponse whose CertID binds this cert's serial to
|
|
2376
2376
|
* its issuer (recomputing `issuerNameHash`/`issuerKeyHash` under the CertID's
|
|
2377
2377
|
* own hashAlgorithm), confirms the responder is authorized (the issuing CA
|
|
@@ -2818,20 +2818,21 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
2818
2818
|
*
|
|
2819
2819
|
* Candidate issuers are matched by RFC 5280 7.1 name chaining, prioritized by the RFC 4158 3.5
|
|
2820
2820
|
* heuristics (a subjectKeyIdentifier/authorityKeyIdentifier match, an anchor-adjacent issuer,
|
|
2821
|
-
* CA + keyCertSign, validity at the check time
|
|
2822
|
-
* depth-first with backtracking: the first ordered path that
|
|
2823
|
-
* name or key-identifier match is only an ordering hint;
|
|
2824
|
-
* so `build` never weakens or duplicates a 6.1 check. The
|
|
2825
|
-
* bounded
|
|
2826
|
-
* visited-set keyed on the (subject, subjectAltName, public key)
|
|
2827
|
-
* cycle or Bridge-CA fan-out terminates deterministically
|
|
2821
|
+
* CA + keyCertSign, validity at the check time; each orders the search without excluding a
|
|
2822
|
+
* candidate), and searched depth-first with backtracking: the first ordered path that
|
|
2823
|
+
* `pki.path.validate` accepts wins. A name or key-identifier match is only an ordering hint;
|
|
2824
|
+
* every accept flows through `validate`, so `build` never weakens or duplicates a 6.1 check. The
|
|
2825
|
+
* search over the untrusted pool is bounded: a depth cap on chain length, a total-work cap on
|
|
2826
|
+
* candidate expansions, and a visited-set keyed on the (subject, subjectAltName, public key)
|
|
2827
|
+
* tuple. A cross-certificate cycle or Bridge-CA fan-out therefore terminates deterministically;
|
|
2828
|
+
* the search cannot grow without bound.
|
|
2828
2829
|
*
|
|
2829
2830
|
* `leaf` is a DER `Buffer`, a PEM string, or an already-parsed `pki.schema.x509` object. Returns
|
|
2830
2831
|
* `{ valid, path, trustAnchor, result, candidatesConsidered }`, where `path` is the ordered array
|
|
2831
2832
|
* `validate` consumes (anchor-proximal first, leaf last, the anchor excluded). Fail-closed: bad
|
|
2832
2833
|
* options throw `path/bad-input`; no chain to any anchor throws `path/no-path`; chains that
|
|
2833
2834
|
* assemble but none validate return `{ valid:false }` with the best failing `validate` result;
|
|
2834
|
-
* the search bound throws `path/build-limit`. By default `build` is OFFLINE (zero network)
|
|
2835
|
+
* the search bound throws `path/build-limit`. By default `build` is OFFLINE (zero network), so supply
|
|
2835
2836
|
* intermediates in `opts.candidates`. Set `opts.fetchAia: true` to opt in to fetching a MISSING intermediate
|
|
2836
2837
|
* from a certificate's Authority Information Access `caIssuers` URL (RFC 5280 sec. 4.2.2.1) over
|
|
2837
2838
|
* `pki.transport`: the fetch triggers only on a pool miss, every fetched certificate is UNTRUSTED pool material
|
|
@@ -2840,7 +2841,7 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
2840
2841
|
* buildable path), a per-cert URL cap, a build-wide URL dedupe, a response size + certificate-count cap, and no
|
|
2841
2842
|
* redirect following; every fetch fault is a silent skip. `aiaFetches` reports how many network GETs the build
|
|
2842
2843
|
* performed (`0` when `fetchAia` is off). NOTE: with `opts.validate:false` (pure-builder mode) a fetched cert is
|
|
2843
|
-
* returned unvalidated, identical to a static candidate
|
|
2844
|
+
* returned unvalidated, identical to a static candidate; the "flows through validate" guarantee needs validation on.
|
|
2844
2845
|
*
|
|
2845
2846
|
* @opts candidates The untrusted candidate CA pool (array of DER/PEM/parsed certs; alias `intermediates`).
|
|
2846
2847
|
* @opts trustAnchors The trust store (non-empty array of `{ name, publicKey, algorithm }` tuples or self-signed root certificates).
|
|
@@ -2848,10 +2849,10 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
2848
2849
|
* @opts maxDepth Chain-length depth cap (default `C.LIMITS.PATH_BUILD_MAX_DEPTH`).
|
|
2849
2850
|
* @opts maxCandidatesConsidered Total-work cap on candidate expansions (default `C.LIMITS.PATH_BUILD_MAX_CANDIDATES`).
|
|
2850
2851
|
* @opts validate `false` returns the ordered path without validating (pure-builder mode; default `true`).
|
|
2851
|
-
* @opts fetchAia `true` opts in to AIA caIssuers network fetching of a missing intermediate (default `false
|
|
2852
|
-
* @opts transport The injectable transport seam (`fn(request) -> Promise<{ status, headers, body }>`); tests drive the fetch offline. With none, the default `pki.transport.https` is used, which fails closed unless `opts.tls` carries trust. SSRF: for a caIssuers URL with a DNS hostname, a custom transport is used
|
|
2853
|
-
* @opts tls The TLS trust for the AIA HTTPS host (`{ anchors, useSystemStore, ... }`)
|
|
2854
|
-
* @opts maxAiaFetches Total AIA network GET budget across the whole build (default `C.LIMITS.PATH_AIA_MAX_FETCHES`); on reaching it the builder stops fetching (a silent cap)
|
|
2852
|
+
* @opts fetchAia `true` opts in to AIA caIssuers network fetching of a missing intermediate (default `false`, fully offline). Off unless set; when set, a fetch runs only on a pool miss for a non-anchor-adjacent cert, and (with validation on) every fetched cert still flows through `validate`.
|
|
2853
|
+
* @opts transport The injectable transport seam (`fn(request) -> Promise<{ status, headers, body }>`); tests drive the fetch offline. With none, the default `pki.transport.https` is used, which fails closed unless `opts.tls` carries trust. SSRF: for a caIssuers URL with a DNS hostname, a custom transport is used only if it declares `fn.blocksPrivateAddresses = true`, which vouches that it refuses (and pins) a resolved private / loopback / link-local / special-use address, as the default transport does. Without that marker a DNS-name AIA URL is fail-closed (skipped) and only an IP-literal URL (validated up front) is fetched; set the marker on your transport when it filters resolved addresses.
|
|
2854
|
+
* @opts tls The TLS trust for the AIA HTTPS host (`{ anchors, useSystemStore, ... }`). This is distinct from `opts.trustAnchors`, the PKI trust store the path validates against. The default transport refuses an unpinned server.
|
|
2855
|
+
* @opts maxAiaFetches Total AIA network GET budget across the whole build (default `C.LIMITS.PATH_AIA_MAX_FETCHES`); on reaching it the builder stops fetching (a silent cap) and does not throw, so a fetch bound cannot deny a path the pool could build.
|
|
2855
2856
|
* @opts maxAiaPerCert Cap on caIssuers URLs tried per certificate (default `C.LIMITS.PATH_AIA_MAX_PER_CERT`).
|
|
2856
2857
|
* @opts aiaTimeout Per-fetch timeout in ms, forwarded to the transport.
|
|
2857
2858
|
* @opts maxResponseBytes Per-fetch response size cap, forwarded to the transport (tightenable downward only).
|