@blamejs/pki 0.9.5 → 0.9.7
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 +66 -0
- package/README.md +7 -6
- package/bin/pki.js +110 -34
- package/lib/acme.js +91 -46
- package/lib/asn1-der.js +6 -5
- package/lib/attrcert-sign.js +4 -3
- package/lib/cmp-build.js +16 -6
- package/lib/cmp-session.js +26 -3
- package/lib/cmp-verify.js +24 -2
- package/lib/cms-compress.js +3 -3
- package/lib/cms-digest.js +3 -3
- package/lib/cms-verify.js +18 -8
- package/lib/compromised-keys.js +114 -0
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +3 -3
- package/lib/crmf-sign.js +2 -0
- package/lib/csr-sign.js +5 -1
- package/lib/ct.js +16 -3
- package/lib/est.js +44 -29
- package/lib/framework-error.js +20 -7
- package/lib/guard-all.js +2 -0
- package/lib/guard-distrust.js +344 -0
- package/lib/guard-json.js +1 -1
- package/lib/guard-verdict.js +16 -3
- package/lib/http-transport.js +412 -41
- package/lib/jose.js +1 -1
- package/lib/lint.js +49 -7
- package/lib/ocsp-verify.js +9 -3
- package/lib/ocsp.js +8 -4
- package/lib/oid.js +4 -0
- package/lib/path-validate.js +131 -16
- package/lib/possession.js +6 -2
- package/lib/related-cert.js +2 -2
- package/lib/revocation-fetch.js +39 -9
- package/lib/scep.js +28 -13
- package/lib/schema-pkix.js +3 -3
- package/lib/sigstore.js +11 -6
- package/lib/smime.js +18 -13
- package/lib/tls-cert-compress.js +6 -5
- package/lib/trust.js +159 -20
- package/lib/trustanchor-build.js +16 -1
- package/lib/tsp-sign.js +7 -1
- package/lib/webauthn-mds.js +30 -8
- package/lib/webauthn.js +46 -14
- package/lib/x509-sign.js +16 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/jose.js
CHANGED
|
@@ -373,7 +373,7 @@ async function verify(jws, opts) {
|
|
|
373
373
|
* `snapshotDeep` refuses an accessor-backed member rather than reading it once, which is the stronger
|
|
374
374
|
* answer for a value that selects an algorithm. The embedded jwk comes from the parsed header and is
|
|
375
375
|
* this module's own object, so it needs no copy. */
|
|
376
|
-
var suppliedKey = opts.key
|
|
376
|
+
var suppliedKey = opts.key == null ? undefined
|
|
377
377
|
: guard.bytes.snapshotDeep(opts.key, JoseError, "jose/bad-key", "opts.key");
|
|
378
378
|
var jwk = header.jwk || suppliedKey;
|
|
379
379
|
if (!jwk) throw E("jose/bad-key", "a verification key is required (opts.key) when the profile does not embed a jwk");
|
package/lib/lint.js
CHANGED
|
@@ -3908,9 +3908,8 @@ function _isMlKem(cert) {
|
|
|
3908
3908
|
* MUST NOT be present. The row runs in the rfc5280 profile because that is the profile whose own
|
|
3909
3909
|
* clause delegates the question, and the profile every authoring call is held to.
|
|
3910
3910
|
*
|
|
3911
|
-
* Only the id-ecPublicKey arm is here
|
|
3912
|
-
*
|
|
3913
|
-
* subject-key algorithms is adding surface rather than closing this. There is no RSA row: RFC 3279
|
|
3911
|
+
* Only the id-ecPublicKey arm is here; no row is keyed on id-ecDH or id-ecMQV, the identifiers
|
|
3912
|
+
* sec. 3's other two arms govern. There is no RSA row: RFC 3279
|
|
3914
3913
|
* sec. 2.3.1 states a MAY list and no prohibition, so an error-severity finding there would promote
|
|
3915
3914
|
* a MAY-list omission to a MUST NOT and stop the signer emitting a certificate a caller may want. */
|
|
3916
3915
|
var EC_FORBIDDEN_KU_BITS = ["keyEncipherment", "dataEncipherment"];
|
|
@@ -3938,6 +3937,46 @@ var RFC5480_RULES = [
|
|
|
3938
3937
|
},
|
|
3939
3938
|
];
|
|
3940
3939
|
|
|
3940
|
+
function _isEcSubjectKey(cert) {
|
|
3941
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
3942
|
+
if (!spki || !spki.algorithm || !intrinsic.isBuffer(spki.bytes)) return false;
|
|
3943
|
+
var n = spki.algorithm.name;
|
|
3944
|
+
return n === "ecPublicKey" || n === "ecDH" || n === "ecMQV";
|
|
3945
|
+
}
|
|
3946
|
+
function _hasSubjectKey(cert) {
|
|
3947
|
+
return !!cert.subjectPublicKeyInfo && intrinsic.isBuffer(cert.subjectPublicKeyInfo.bytes);
|
|
3948
|
+
}
|
|
3949
|
+
/** @internal The built-in list, keyed by digest, for the row that names a listed key. */
|
|
3950
|
+
var _COMPROMISED_BY_DIGEST = intrinsic.create(null);
|
|
3951
|
+
(function () {
|
|
3952
|
+
var listed = guard.distrust.knownCompromised();
|
|
3953
|
+
for (var i = 0; i < listed.length; i++) _COMPROMISED_BY_DIGEST[listed[i].spkiSha256] = listed[i];
|
|
3954
|
+
})();
|
|
3955
|
+
|
|
3956
|
+
var KEY_COMPROMISE_RULES = [
|
|
3957
|
+
{
|
|
3958
|
+
id: "lint/key-compromise/small-ec-private-key", severity: "error", source: "key-compromise",
|
|
3959
|
+
citation: "CABF TLS BR 6.1.1.3 (4)",
|
|
3960
|
+
message: "the elliptic curve private key is a small integer, so anyone can recover it from the public key",
|
|
3961
|
+
appliesTo: _isEcSubjectKey,
|
|
3962
|
+
check: function (cert) {
|
|
3963
|
+
var curve = guard.distrust.smallEcScalar(cert.subjectPublicKeyInfo.bytes);
|
|
3964
|
+
return curve === null ? null : { context: { curve: curve, scalarBelow: guard.distrust.SMALL_SCALAR_BOUND } };
|
|
3965
|
+
},
|
|
3966
|
+
},
|
|
3967
|
+
{
|
|
3968
|
+
id: "lint/key-compromise/known-compromised-key", severity: "error", source: "key-compromise",
|
|
3969
|
+
citation: "CABF TLS BR 6.1.1.3 (4)",
|
|
3970
|
+
message: "the subject key is on the list of keys whose private key is public (pki.trust.knownCompromisedKeys)",
|
|
3971
|
+
appliesTo: _hasSubjectKey,
|
|
3972
|
+
check: function (cert) {
|
|
3973
|
+
var bytes = cert.subjectPublicKeyInfo.bytes;
|
|
3974
|
+
var entry = _COMPROMISED_BY_DIGEST[guard.distrust.digest(bytes)] || _COMPROMISED_BY_DIGEST[guard.distrust.keyDigest(bytes)];
|
|
3975
|
+
return entry ? { context: { name: entry.name, spkiSha256: entry.spkiSha256 } } : null;
|
|
3976
|
+
},
|
|
3977
|
+
},
|
|
3978
|
+
];
|
|
3979
|
+
|
|
3941
3980
|
var RFC9935_RULES = [
|
|
3942
3981
|
{
|
|
3943
3982
|
id: "lint/rfc9935/kem-key-usage", severity: "error", source: "rfc9935", citation: "RFC 9935 5",
|
|
@@ -6129,7 +6168,7 @@ function _nistKindOf(alg) {
|
|
|
6129
6168
|
/** @internal A key-agreement scheme names a key-derivation function for CMS rather than an algorithm
|
|
6130
6169
|
* a certificate declares, and its name opens with the family word. */
|
|
6131
6170
|
if (name.indexOf("dhSinglePass-") === 0) return undefined;
|
|
6132
|
-
if (name === "ecPublicKey" || name.indexOf("ecdsaWith") === 0 ||
|
|
6171
|
+
if (name === "ecPublicKey" || name === "ecDH" || name === "ecMQV" || name.indexOf("ecdsaWith") === 0 ||
|
|
6133
6172
|
name === "X25519" || name === "X448") return "ecc";
|
|
6134
6173
|
if (name === "Ed25519" || name === "Ed448") return "eddsa";
|
|
6135
6174
|
if (name === "dsa" || name.indexOf("dsaWith") === 0 ||
|
|
@@ -6719,6 +6758,7 @@ var PROFILES = Object.assign(Object.create(null), {
|
|
|
6719
6758
|
* from a rule that quietly replaced the other. */
|
|
6720
6759
|
"cnsa-2.0": CNSA2_RULES.concat(RFC9881_RULES).concat(RFC9935_RULES)
|
|
6721
6760
|
.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6761
|
+
"key-compromise": intrinsic.concatList(KEY_COMPROMISE_RULES, RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6722
6762
|
});
|
|
6723
6763
|
var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
|
|
6724
6764
|
.concat(RFC9763_RULES).concat(RFC7633_RULES).concat(X509_ALTSIG_RULES)
|
|
@@ -6732,6 +6772,7 @@ var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat
|
|
|
6732
6772
|
* operand and builds through ArraySpeciesCreate, so a replacement decides which rows the default
|
|
6733
6773
|
* set holds. The pre-existing chain above is counted in the conversion budget. */
|
|
6734
6774
|
ALL_RULES = intrinsic.concatList(ALL_RULES, RFC5480_RULES);
|
|
6775
|
+
ALL_RULES = intrinsic.concatList(ALL_RULES, KEY_COMPROMISE_RULES);
|
|
6735
6776
|
|
|
6736
6777
|
var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
|
|
6737
6778
|
return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
|
|
@@ -6820,8 +6861,9 @@ var CSR_PROFILES = Object.assign(Object.create(null), {
|
|
|
6820
6861
|
"rfc2986": CSR_STRUCTURAL_RULES,
|
|
6821
6862
|
"cabf-tls": CSR_STRUCTURAL_RULES.concat(CABF_TLS_CSR_RULES),
|
|
6822
6863
|
"rfc9883": RFC9883_CSR_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6864
|
+
"key-compromise": intrinsic.concatList(KEY_COMPROMISE_RULES, RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6823
6865
|
});
|
|
6824
|
-
var ALL_CSR_RULES = CSR_STRUCTURAL_RULES.concat(RFC9883_CSR_RULES);
|
|
6866
|
+
var ALL_CSR_RULES = intrinsic.concatList(CSR_STRUCTURAL_RULES.concat(RFC9883_CSR_RULES), KEY_COMPROMISE_RULES);
|
|
6825
6867
|
|
|
6826
6868
|
function _subjectIsNonEmpty(csr) {
|
|
6827
6869
|
var rdns = csr.subject && csr.subject.rdns;
|
|
@@ -7751,8 +7793,8 @@ function rules(profile, artifact) {
|
|
|
7751
7793
|
* // -> ["rfc5280", "rfc5480", "rfc9881", "rfc9909", "rfc9935", "rfc9763", "rfc7633",
|
|
7752
7794
|
* // "x509-altsig", "cabf-tls", "cabf-smime", "cabf-cs", "etsi-qc", "mozilla-root",
|
|
7753
7795
|
* // "chrome-root", "microsoft-root", "apple-root", "nist-pqc-transition", "cnsa-2.0",
|
|
7754
|
-
* // "rfc2986", "rfc9883", "rfc5280-crl", "rfc6960", "rfc5019",
|
|
7755
|
-
* // "rfc3161", "rfc5755"]
|
|
7796
|
+
* // "key-compromise", "rfc2986", "rfc9883", "rfc5280-crl", "rfc6960", "rfc5019",
|
|
7797
|
+
* // "rfc5652", "rfc8933", "rfc3161", "rfc5755"]
|
|
7756
7798
|
*/
|
|
7757
7799
|
/** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
|
|
7758
7800
|
* request form: `pki.lint.csr` takes the same name and runs those rows against what a request
|
package/lib/ocsp-verify.js
CHANGED
|
@@ -74,7 +74,12 @@ function makeOcspVerify(deps) {
|
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
/** @internal `distrusted` is a list `guard.distrust.keys` returned, or null. A responder whose key
|
|
78
|
+
* is on it is not an authorized responder. A listed issuing CA authorizes no responder at all: its
|
|
79
|
+
* own answer is refused, and so is a delegate it certified, since the holder of the listed key can
|
|
80
|
+
* certify one. */
|
|
81
|
+
async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits, time, distrusted) {
|
|
82
|
+
if (guard.distrust.has(distrusted, issuer.workingPublicKey)) return null;
|
|
78
83
|
var rid = basicResponse.responderID;
|
|
79
84
|
var matchesIssuer = false;
|
|
80
85
|
try {
|
|
@@ -93,6 +98,7 @@ function makeOcspVerify(deps) {
|
|
|
93
98
|
else if (rid.byKey) identifies = _bytesEqual(rid.byKey, await ocspDigest("SHA-1", ocspKeyValue(rc.subjectPublicKeyInfo.bytes)));
|
|
94
99
|
} catch (_e) { identifies = false; }
|
|
95
100
|
if (!identifies) continue;
|
|
101
|
+
if (guard.distrust.has(distrusted, rc.subjectPublicKeyInfo.bytes) || guard.distrust.has(distrusted, ocspResponderSpki(rc, issuer))) continue;
|
|
96
102
|
var issuedByCa;
|
|
97
103
|
try { issuedByCa = dnEqual(rc.issuer.rdns, cert.issuer.rdns); }
|
|
98
104
|
catch (_e) { continue; }
|
|
@@ -141,11 +147,11 @@ function makeOcspVerify(deps) {
|
|
|
141
147
|
}, extras);
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
async function evaluateResponse(resp, cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical) {
|
|
150
|
+
async function evaluateResponse(resp, cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical, distrusted) {
|
|
145
151
|
if (resp.responseStatus.code !== 0) return _evaluation({ reason: "non-successful OCSP responseStatus (" + resp.responseStatus.code + ")" });
|
|
146
152
|
var br = resp.basicResponse;
|
|
147
153
|
if (!br) return _evaluation({ reason: "successful OCSP response carries no BasicOCSPResponse" });
|
|
148
|
-
var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time);
|
|
154
|
+
var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time, distrusted || null);
|
|
149
155
|
if (!signerSpki) return _evaluation({ applicable: true, reason: "no authorized OCSP responder signs this response (RFC 6960 sec. 4.2.2.2)" });
|
|
150
156
|
if (!(await verifyWithSpki(br.signatureAlgorithm, br.signature, signerSpki, br.tbsResponseDataBytes))) {
|
|
151
157
|
return _evaluation({ applicable: true, responderAuthorized: true, reason: "the OCSP response signature does not verify over tbsResponseData" });
|
package/lib/ocsp.js
CHANGED
|
@@ -682,6 +682,9 @@ function buildErrorResponse(status) {
|
|
|
682
682
|
* time the validation instant (default: now).
|
|
683
683
|
* requestNonce the nonce the client sent; when given, the response MUST echo it (constant-time).
|
|
684
684
|
* historicalMode defer a strictly-future revocation (report good) instead of revoking on skew.
|
|
685
|
+
* distrustedKeys SHA-256 digests of SubjectPublicKeyInfo values (64 hex characters or 32 bytes each, see
|
|
686
|
+
* `pki.trust.spkiDigest`); a response signed by a listed key, the issuing CA's or a
|
|
687
|
+
* delegate's, has no authorized responder and is `"unknown"`.
|
|
685
688
|
* @example
|
|
686
689
|
* var ca = await pki.key.generate("Ed25519");
|
|
687
690
|
* var caKey = await pki.key.export(ca.privateKey);
|
|
@@ -699,7 +702,7 @@ function buildErrorResponse(status) {
|
|
|
699
702
|
* var res = await pki.ocsp.verify(responseDer, { cert: leafDer, issuer: caDer });
|
|
700
703
|
* res.status; // "good" | "revoked" | "unknown"
|
|
701
704
|
*/
|
|
702
|
-
var _VERIFY_OPTS = Object.assign(Object.create(null), { cert: 1, historicalMode: 1, issuer: 1, requestNonce: 1, time: 1 });
|
|
705
|
+
var _VERIFY_OPTS = Object.assign(Object.create(null), { cert: 1, distrustedKeys: 1, historicalMode: 1, issuer: 1, requestNonce: 1, time: 1 });
|
|
703
706
|
|
|
704
707
|
function verify(response, opts) {
|
|
705
708
|
try {
|
|
@@ -708,8 +711,9 @@ function verify(response, opts) {
|
|
|
708
711
|
"pki.ocsp.verify has an unknown option (the request nonce to bind against is `requestNonce`): ");
|
|
709
712
|
} catch (e) { return _promiseReject(_Promise, e); }
|
|
710
713
|
if (opts.cert == null || opts.issuer == null) return _promiseReject(_Promise, _err("ocsp/bad-input", "verify requires opts.cert and opts.issuer"));
|
|
711
|
-
var parsed, cert, issuerCert, time;
|
|
714
|
+
var parsed, cert, issuerCert, time, distrusted;
|
|
712
715
|
try {
|
|
716
|
+
distrusted = guard.distrust.keys(opts.distrustedKeys, _err, "ocsp/bad-input", "opts.distrustedKeys");
|
|
713
717
|
parsed = _responseFromBytes(response);
|
|
714
718
|
cert = _certOf(opts.cert, "the target certificate");
|
|
715
719
|
issuerCert = _certOf(opts.issuer, "the issuer certificate");
|
|
@@ -728,7 +732,7 @@ function verify(response, opts) {
|
|
|
728
732
|
/** @internal Captured: this continuation is where the response's status becomes the verdict a
|
|
729
733
|
* caller reads, so a settled value in place of it reports a status nothing evaluated. */
|
|
730
734
|
var evaluated = pathValidate.verifyOcspResponse(parsed, cert, issuerCert, time,
|
|
731
|
-
{ historicalMode: opts.historicalMode === true, requestNonce: reqNonce });
|
|
735
|
+
{ historicalMode: opts.historicalMode === true, requestNonce: reqNonce, distrustedKeys: distrusted });
|
|
732
736
|
return _ocspIntrinsic.chain(evaluated, function (verdict) {
|
|
733
737
|
return guard.verdict.of(verdict, { valid: verdict.status === "good" });
|
|
734
738
|
});
|
|
@@ -949,7 +953,7 @@ function httpRequest(der, responderUrl, opts) {
|
|
|
949
953
|
if (!guard.encoding.isAsciiText(responderUrl)) {
|
|
950
954
|
throw E("ocsp/bad-input", "httpRequest: the responder URL must be US-ASCII (RFC 3986 sec. 2); percent-encode the rest before passing it, since the RFC 5019 sec. 5 size bound counts the bytes that go on the wire");
|
|
951
955
|
}
|
|
952
|
-
var asked = opts.method
|
|
956
|
+
var asked = opts.method == null ? null : opts.method;
|
|
953
957
|
if (asked !== null && asked !== "GET" && asked !== "POST") {
|
|
954
958
|
throw E("ocsp/bad-input", "httpRequest: opts.method must be \"GET\" or \"POST\"");
|
|
955
959
|
}
|
package/lib/oid.js
CHANGED
|
@@ -351,6 +351,10 @@ var FAMILIES = Object.assign(Object.create(null), {
|
|
|
351
351
|
sect233k1: 26, sect233r1: 27, secp192k1: 31, secp224k1: 32, secp224r1: 33,
|
|
352
352
|
secp384r1: 34, secp521r1: 35, sect409k1: 36, sect409r1: 37, sect571k1: 38, sect571r1: 39 } },
|
|
353
353
|
|
|
354
|
+
/** @internal RFC 5480 sec. 2.1.2: the two algorithm identifiers that restrict an elliptic curve key to
|
|
355
|
+
* key agreement. The key itself is an ecPublicKey key, which is how a distrust list reads it. */
|
|
356
|
+
secgSchemes: { base: [1, 3, 132, 1], of: { ecDH: 12, ecMQV: 13 } },
|
|
357
|
+
|
|
354
358
|
brainpool: { base: [1, 3, 36, 3, 3, 2, 8, 1, 1], of: {
|
|
355
359
|
brainpoolP160r1: 1, brainpoolP160t1: 2, brainpoolP192r1: 3, brainpoolP192t1: 4,
|
|
356
360
|
brainpoolP224r1: 5, brainpoolP224t1: 6, brainpoolP256r1: 7, brainpoolP256t1: 8,
|
package/lib/path-validate.js
CHANGED
|
@@ -1023,8 +1023,8 @@ function _initialState(certs, params, seeds, anchor, n, ta, policySwitches) {
|
|
|
1023
1023
|
validPolicyGraph: graphRoot(),
|
|
1024
1024
|
policyIndex: intrinsic.create(null),
|
|
1025
1025
|
policyNodeCount: 1,
|
|
1026
|
-
maxPolicyNodes: params.maxPolicyNodes
|
|
1027
|
-
minRsaModulusBits: params.minRsaModulusBits
|
|
1026
|
+
maxPolicyNodes: params.maxPolicyNodes != null ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
|
|
1027
|
+
minRsaModulusBits: params.minRsaModulusBits != null ? params.minRsaModulusBits : constants.LIMITS.RSA_MIN_MODULUS_BITS,
|
|
1028
1028
|
permittedGenerations: intrinsic.selectList(seeds.permittedGenerations, function (g) { return g.length > 0; }),
|
|
1029
1029
|
excludedSubtrees: seeds.excluded,
|
|
1030
1030
|
explicitPolicy: _flagSet(policySwitches.initialExplicitPolicy, ta.policyFlags, "requireExplicitPolicy") ? 0 : n + 1,
|
|
@@ -1129,6 +1129,17 @@ function spliceSpkiParameters(spki, algOid, paramsBytes) {
|
|
|
1129
1129
|
]);
|
|
1130
1130
|
}
|
|
1131
1131
|
|
|
1132
|
+
/** @internal The certificate's key with the parameters it inherits from its issuer spliced in, the
|
|
1133
|
+
* way `updateWorkingKey` builds the key the next signature is verified under, or null when the
|
|
1134
|
+
* certificate states its own parameters or inherits none. Read before `updateWorkingKey` runs for
|
|
1135
|
+
* this certificate, while `state` still holds the issuer's working key. */
|
|
1136
|
+
function _inheritedSpki(state, cert) {
|
|
1137
|
+
var keyAlg = cert.subjectPublicKeyInfo.algorithm;
|
|
1138
|
+
if (!isNullOrAbsentParams(keyAlg.parameters)) return null;
|
|
1139
|
+
if (keyAlg.oid !== state.workingPublicKeyAlgorithm || !state.workingPublicKeyParameters) return null;
|
|
1140
|
+
return spliceSpkiParameters(cert.subjectPublicKeyInfo, keyAlg.oid, state.workingPublicKeyParameters);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1132
1143
|
function updateWorkingKey(state, cert) {
|
|
1133
1144
|
var keyAlg = cert.subjectPublicKeyInfo.algorithm;
|
|
1134
1145
|
if (!isNullOrAbsentParams(keyAlg.parameters)) {
|
|
@@ -1370,7 +1381,7 @@ function _revocationRow(row, rv) {
|
|
|
1370
1381
|
* (`initialExplicitPolicy`, `initialAnyPolicyInhibit`,
|
|
1371
1382
|
* `initialPolicyMappingInhibit`) each take `true`, `false`, `null` or an absent
|
|
1372
1383
|
* option, and any other value throws `path/bad-input` for the same reason. Returns `{ valid, revocationChecked, anchorConstraints,
|
|
1373
|
-
* path, results, workingPublicKey, workingPublicKeyAlgorithm,
|
|
1384
|
+
* distrustedKeysApplied, path, results, workingPublicKey, workingPublicKeyAlgorithm,
|
|
1374
1385
|
* workingPublicKeyParameters, validPolicyGraph, userConstrainedPolicySet }` where `results[i].checks`
|
|
1375
1386
|
* carries a per-check reason code (`path/*`) for every step. Pure and
|
|
1376
1387
|
* re-entrant: no input object is mutated. An empty path or a missing anchor
|
|
@@ -1422,6 +1433,27 @@ function _revocationRow(row, rv) {
|
|
|
1422
1433
|
* `softFail` says. `softFail` opts into an undetermined answer; the built-in
|
|
1423
1434
|
* checkers report one as `{ status: "unknown" }` and do not throw.
|
|
1424
1435
|
*
|
|
1436
|
+
* `distrustedKeys` is a list of SHA-256 digests of DER SubjectPublicKeyInfo values, each 64
|
|
1437
|
+
* hexadecimal characters or 32 bytes (`pki.trust.spkiDigest` computes one from a certificate). A
|
|
1438
|
+
* certificate on the path whose key is listed fails with `path/distrusted-key` in its
|
|
1439
|
+
* `distrustedKey` row, and a listed trust anchor key fails the path with the same code in the
|
|
1440
|
+
* `distrustedIssuerKey` row of the certificate the anchor issued. A key matches when the digest of
|
|
1441
|
+
* the SubjectPublicKeyInfo as the certificate encodes it, or as node re-encodes the key (an
|
|
1442
|
+
* uncompressed EC point, RSA parameters with NULL), is listed, so one entry refuses a CA certificate
|
|
1443
|
+
* and every cross-certificate for the same key. No revocation status is asked for a certificate at
|
|
1444
|
+
* or below a distrusted key, and a standalone `crlChecker`, `ocspChecker` or `fetchingChecker` given
|
|
1445
|
+
* the list in its context answers `"unknown"` for an issuer whose key is listed.
|
|
1446
|
+
*
|
|
1447
|
+
* A key whose private key is public is refused whether or not a list is given, and no option turns
|
|
1448
|
+
* the refusal off: a key on `pki.trust.knownCompromisedKeys()`, and an elliptic curve key on P-256,
|
|
1449
|
+
* P-384, P-521 or secp256k1 whose private key is an integer below 256. Such a key fails the path with
|
|
1450
|
+
* `path/compromised-key`, in the `compromisedKey` row of the certificate that carries it or in the
|
|
1451
|
+
* `compromisedIssuerKey` row of the certificate a trust anchor with such a key issued. A compromised
|
|
1452
|
+
* key is reported in these rows rather than as distrusted when the list names it too, and the
|
|
1453
|
+
* revocation checkers answer `"unknown"` for a compromised issuer as they do for a listed one.
|
|
1454
|
+
* `distrustedKeysApplied` reports whether a non-empty list was supplied. A malformed list is
|
|
1455
|
+
* `path/bad-input`.
|
|
1456
|
+
*
|
|
1425
1457
|
* `anchorConstraints` reports what the anchor's own trust metadata decided:
|
|
1426
1458
|
* the `checkedPurpose` it was judged under, and whether the `distrustAfter`
|
|
1427
1459
|
* date, the `purposes` delegator map and the `nameConstraints` namespace each
|
|
@@ -1461,7 +1493,7 @@ function _revocationRow(row, rv) {
|
|
|
1461
1493
|
* res.valid; // boolean; res.results[0].checks carries the per-check codes
|
|
1462
1494
|
*/
|
|
1463
1495
|
var _VALIDATE_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
1464
|
-
checkPurpose: 1, ctLogList: 1, ctPolicy: 1, historicalMode: 1, identity: 1, initialAnyPolicyInhibit: 1,
|
|
1496
|
+
checkPurpose: 1, ctLogList: 1, ctPolicy: 1, distrustedKeys: 1, historicalMode: 1, identity: 1, initialAnyPolicyInhibit: 1,
|
|
1465
1497
|
initialExcludedSubtrees: 1, initialExplicitPolicy: 1, initialPermittedSubtrees: 1,
|
|
1466
1498
|
initialPolicyMappingInhibit: 1, maxPathCerts: 1, maxPolicyNodes: 1, minRsaModulusBits: 1, requireRevocation: 1,
|
|
1467
1499
|
requiredEku: 1, revocationChecker: 1, softFail: 1, time: 1, trustAnchors: 1,
|
|
@@ -1516,8 +1548,12 @@ async function validate(path, opts) {
|
|
|
1516
1548
|
if (suppliedVerifier !== null && typeof suppliedVerify !== "function") {
|
|
1517
1549
|
throw E("path/bad-input", "validate: opts.verifier must carry a verify(args) function");
|
|
1518
1550
|
}
|
|
1551
|
+
/** @internal The distrust list is read and copied here, once, and every anchor attempt reads the
|
|
1552
|
+
* copy: the attempts run after awaits, and a list re-read from the caller's array there could have
|
|
1553
|
+
* been emptied by a revocation checker that ran in between. */
|
|
1554
|
+
var distrusted = guard.distrust.keys(checked.distrustedKeys, E, "path/bad-input", "validate: opts.distrustedKeys");
|
|
1519
1555
|
var scope = intrinsic.freeze(intrinsic.assign(intrinsic.create(null), {
|
|
1520
|
-
verifier: suppliedVerifier, verify: suppliedVerify,
|
|
1556
|
+
verifier: suppliedVerifier, verify: suppliedVerify, distrusted: distrusted,
|
|
1521
1557
|
}));
|
|
1522
1558
|
return _validate(path, checked, intrinsic.freeze(intrinsic.create(null)), scope);
|
|
1523
1559
|
}
|
|
@@ -1670,6 +1706,14 @@ async function _validate(path, opts, run, scope) {
|
|
|
1670
1706
|
var revocationRan = false, revocationWaived = false, revocationUndetermined = false;
|
|
1671
1707
|
var prefixAuthentic = true;
|
|
1672
1708
|
var anchorDistrustApplied = false, anchorPurposeApplied = false;
|
|
1709
|
+
var distrusted = scope.distrusted;
|
|
1710
|
+
var anchorKeyCompromised = guard.distrust.compromised(anchor.publicKey);
|
|
1711
|
+
var anchorKeyDistrusted = guard.distrust.has(distrusted, anchor.publicKey);
|
|
1712
|
+
/** @internal The first path index whose issuer chain holds a distrusted key: the anchor's key
|
|
1713
|
+
* makes it 0, a certificate's own key makes it that certificate. From there down, no revocation
|
|
1714
|
+
* status is asked for. The path fails either way, and a checker that fetches would open a
|
|
1715
|
+
* connection on behalf of certificates issued under a key the caller refuses. */
|
|
1716
|
+
var distrustedFrom = anchorKeyDistrusted ? 0 : -1;
|
|
1673
1717
|
if (!checkPurpose && _hasPurposeScopedMetadata(anchor)) {
|
|
1674
1718
|
throw E("path/bad-input", "validate: the trust anchor carries purpose-scoped metadata (distrustAfter / purposes), which is keyed by key purpose -- supply opts.checkPurpose to say which purpose this validation is for, or the constraint cannot be applied");
|
|
1675
1719
|
}
|
|
@@ -1705,6 +1749,28 @@ async function _validate(path, opts, run, scope) {
|
|
|
1705
1749
|
* no certificate under it is authenticated however its own check reads. */
|
|
1706
1750
|
if (!sigRes.ok) prefixAuthentic = false;
|
|
1707
1751
|
|
|
1752
|
+
/** @internal The digest covers the SubjectPublicKeyInfo as the certificate encodes it, not the
|
|
1753
|
+
* working key, which carries parameters inherited from the issuer. A cross-signed certificate
|
|
1754
|
+
* carries the same encoded key as the certificate it duplicates, so one entry refuses both. */
|
|
1755
|
+
if (idx === 0 && anchorKeyCompromised !== null) {
|
|
1756
|
+
guard.list.append(checks, { name: "compromisedIssuerKey", ok: false, code: "path/compromised-key" });
|
|
1757
|
+
failed = true;
|
|
1758
|
+
} else if (idx === 0 && anchorKeyDistrusted) {
|
|
1759
|
+
guard.list.append(checks, { name: "distrustedIssuerKey", ok: false, code: "path/distrusted-key" });
|
|
1760
|
+
failed = true;
|
|
1761
|
+
}
|
|
1762
|
+
var inheritedSpki = _inheritedSpki(state, cert);
|
|
1763
|
+
var keyCompromised = guard.distrust.compromised(cert.subjectPublicKeyInfo.bytes) || guard.distrust.compromised(inheritedSpki);
|
|
1764
|
+
if (keyCompromised !== null) {
|
|
1765
|
+
guard.list.append(checks, { name: "compromisedKey", ok: false, code: "path/compromised-key" });
|
|
1766
|
+
failed = true;
|
|
1767
|
+
if (distrustedFrom === -1) distrustedFrom = idx;
|
|
1768
|
+
} else if (guard.distrust.has(distrusted, cert.subjectPublicKeyInfo.bytes) || guard.distrust.has(distrusted, inheritedSpki)) {
|
|
1769
|
+
guard.list.append(checks, { name: "distrustedKey", ok: false, code: "path/distrusted-key" });
|
|
1770
|
+
failed = true;
|
|
1771
|
+
if (distrustedFrom === -1) distrustedFrom = idx;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1708
1774
|
if (compositeSig.COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
|
|
1709
1775
|
var cku = compositeKeyUsageCheck(cert);
|
|
1710
1776
|
guard.list.append(checks,{ name: "compositeKeyUsage", ok: cku.ok, code: cku.ok ? undefined : cku.code });
|
|
@@ -1760,7 +1826,13 @@ async function _validate(path, opts, run, scope) {
|
|
|
1760
1826
|
* socket, so asking anyway lets an unauthenticated certificate choose a destination and spend
|
|
1761
1827
|
* the fetch budget. The row says the status was not asked for, which is an undetermined
|
|
1762
1828
|
* outcome and not a checked one. */
|
|
1763
|
-
if (revocationChecker &&
|
|
1829
|
+
if (revocationChecker && distrustedFrom !== -1 && idx >= distrustedFrom) {
|
|
1830
|
+
guard.list.append(checks, { name: "revocation", ok: false, status: "unknown",
|
|
1831
|
+
code: "path/revocation-undetermined",
|
|
1832
|
+
reason: "a key at or above this certificate on the path is distrusted, so no revocation status was asked for" });
|
|
1833
|
+
revocationUndetermined = true;
|
|
1834
|
+
revocationRan = true;
|
|
1835
|
+
} else if (revocationChecker && !prefixAuthentic) {
|
|
1764
1836
|
guard.list.append(checks, { name: "revocation", ok: false, status: "unknown",
|
|
1765
1837
|
code: "path/revocation-undetermined",
|
|
1766
1838
|
reason: sigRes.ok
|
|
@@ -1771,7 +1843,7 @@ async function _validate(path, opts, run, scope) {
|
|
|
1771
1843
|
} else if (revocationChecker) {
|
|
1772
1844
|
var issuerCert = idx > 0 ? certs[idx - 1] : null;
|
|
1773
1845
|
var rv, rvError = null;
|
|
1774
|
-
try { rv = await revocationChecker.check(cert, { workingIssuerName: state.workingIssuerName, workingPublicKey: state.workingPublicKey, workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm, issuerCert: issuerCert }, { time: opts.time, historicalMode: opts.historicalMode === true, run: run }); }
|
|
1846
|
+
try { rv = await revocationChecker.check(cert, { workingIssuerName: state.workingIssuerName, workingPublicKey: state.workingPublicKey, workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm, issuerCert: issuerCert }, { time: opts.time, historicalMode: opts.historicalMode === true, run: run, distrustedKeys: distrusted }); }
|
|
1775
1847
|
catch (e) { rv = { status: "error" }; rvError = e; }
|
|
1776
1848
|
var rvStatus = (rv && typeof rv.status === "string") ? rv.status : "unknown";
|
|
1777
1849
|
if (rv && rv.status === "good") { guard.list.append(checks, _revocationRow({ name: "revocation", ok: true, status: "good" }, rv)); }
|
|
@@ -1894,6 +1966,7 @@ async function _validate(path, opts, run, scope) {
|
|
|
1894
1966
|
return guard.verdict.of({
|
|
1895
1967
|
valid: !failed,
|
|
1896
1968
|
identityChecked: identityChecked,
|
|
1969
|
+
distrustedKeysApplied: distrusted !== null && distrusted.length > 0,
|
|
1897
1970
|
revocationChecked: !revocationRan ? false
|
|
1898
1971
|
: revocationUndetermined ? "undetermined"
|
|
1899
1972
|
: revocationWaived ? "waived" : "determined",
|
|
@@ -2181,6 +2254,13 @@ function crlChecker(crls, opts) {
|
|
|
2181
2254
|
check: async function (cert, issuer, ctx) {
|
|
2182
2255
|
var time = ctx.time;
|
|
2183
2256
|
var historical = ctx.historicalMode === true;
|
|
2257
|
+
/** @internal Every CRL this checker takes is verified under the issuer's key, so a listed
|
|
2258
|
+
* issuer key leaves no CRL that can answer. */
|
|
2259
|
+
var distrusted = guard.distrust.keys(ctx.distrustedKeys, E, "path/bad-input", "crlChecker: ctx.distrustedKeys");
|
|
2260
|
+
var issuerRefusal = guard.distrust.refusal(distrusted, issuer.workingPublicKey, "distrustedKeys");
|
|
2261
|
+
if (issuerRefusal !== null) {
|
|
2262
|
+
return guard.verdict.of({ status: "unknown", reason: "the issuer's key " + issuerRefusal + ", so no CRL it signs is an answer" });
|
|
2263
|
+
}
|
|
2184
2264
|
var certIsCa = null, certScopeFault = null;
|
|
2185
2265
|
try {
|
|
2186
2266
|
var bc = decodeExt(cert, OID.basicConstraints);
|
|
@@ -2473,6 +2553,7 @@ function ocspChecker(responses) {
|
|
|
2473
2553
|
check: async function (cert, issuer, ctx) {
|
|
2474
2554
|
var time = ctx.time;
|
|
2475
2555
|
var historical = ctx.historicalMode === true;
|
|
2556
|
+
var distrusted = guard.distrust.keys(ctx.distrustedKeys, E, "path/bad-input", "ocspChecker: ctx.distrustedKeys");
|
|
2476
2557
|
var issuerNameCandidates = [cert.issuer.bytes];
|
|
2477
2558
|
function addNameCandidate(nm) {
|
|
2478
2559
|
if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) guard.list.append(issuerNameCandidates, nm.bytes);
|
|
@@ -2488,7 +2569,7 @@ function ocspChecker(responses) {
|
|
|
2488
2569
|
var sawUnknownStatus = false;
|
|
2489
2570
|
|
|
2490
2571
|
for (var k = 0; k < parsed.length; k++) {
|
|
2491
|
-
var v = await ocspCore.evaluateResponse(parsed[k], cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical);
|
|
2572
|
+
var v = await ocspCore.evaluateResponse(parsed[k], cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical, distrusted);
|
|
2492
2573
|
if (v.revoked && !revokedResult) {
|
|
2493
2574
|
revokedResult = guard.verdict.of({ status: "revoked", revocationReason: v.revoked.revocationReason, revocationTime: v.revoked.revocationTime, reason: v.revoked.reason });
|
|
2494
2575
|
}
|
|
@@ -2536,6 +2617,11 @@ function ocspChecker(responses) {
|
|
|
2536
2617
|
* `time` either way. `time` must be a valid `Date`. A malformed response's parse
|
|
2537
2618
|
* fault surfaces as the parser's typed `ocsp/*` / `asn1/*` error.
|
|
2538
2619
|
*
|
|
2620
|
+
* `opts.distrustedKeys` lists SHA-256 SubjectPublicKeyInfo digests (see `pki.trust.spkiDigest`). A
|
|
2621
|
+
* response signed by a listed key, whether the issuing CA's or a delegated responder's, has no
|
|
2622
|
+
* authorized responder and is `"unknown"` with `responderAuthorized: false`. `pki.path.ocspChecker`
|
|
2623
|
+
* and `pki.path.fetchingChecker` apply the list `pki.path.validate` was given.
|
|
2624
|
+
*
|
|
2539
2625
|
* `opts.requestNonce` is the nonce bytes the request carried. Given one, the response must echo it
|
|
2540
2626
|
* (RFC 9654 sec. 2.1, RFC 6960 sec. 4.4.1), compared in constant time, and `nonceMatched` reports
|
|
2541
2627
|
* the outcome: `true`, `false`, or `null` when no nonce was sent. An unechoed nonce takes a `good`
|
|
@@ -2571,7 +2657,7 @@ function ocspChecker(responses) {
|
|
|
2571
2657
|
* var v = await pki.path.verifyOcspResponse(der, cert, issuerCert, new Date());
|
|
2572
2658
|
* v.status; // "good" | "revoked" | "unknown"
|
|
2573
2659
|
*/
|
|
2574
|
-
var _VERIFY_OCSP_OPTS = intrinsic.assign(intrinsic.create(null), { historicalMode: 1, requestNonce: 1 });
|
|
2660
|
+
var _VERIFY_OCSP_OPTS = intrinsic.assign(intrinsic.create(null), { distrustedKeys: 1, historicalMode: 1, requestNonce: 1 });
|
|
2575
2661
|
|
|
2576
2662
|
async function verifyOcspResponse(response, cert, issuerCert, time, opts) {
|
|
2577
2663
|
var parsedResponse, subject, issuer, checkedOpts;
|
|
@@ -2583,11 +2669,13 @@ async function verifyOcspResponse(response, cert, issuerCert, time, opts) {
|
|
|
2583
2669
|
checkedOpts = guard.identifier.optionsObject(opts, E, "path/bad-input", "verifyOcspResponse: opts");
|
|
2584
2670
|
guard.identifier.assertKnownKeys(checkedOpts, _VERIFY_OCSP_OPTS, E, "path/bad-input",
|
|
2585
2671
|
"pki.path.verifyOcspResponse has an unknown option (the request nonce to bind against is `requestNonce`). The unknown option was: ");
|
|
2672
|
+
var distrusted = guard.distrust.keys(checkedOpts.distrustedKeys, E, "path/bad-input", "verifyOcspResponse: opts.distrustedKeys");
|
|
2586
2673
|
parsedResponse = _ocspFromBytes(response);
|
|
2587
2674
|
subject = coerceCert(cert, "the certificate");
|
|
2588
2675
|
issuer = coerceCert(issuerCert, "the issuer certificate");
|
|
2589
2676
|
} catch (e) { return _reject(_Promise, e); }
|
|
2590
|
-
var v = await _verifyOcspParsed(parsedResponse, subject, issuer, time,
|
|
2677
|
+
var v = await _verifyOcspParsed(parsedResponse, subject, issuer, time,
|
|
2678
|
+
{ historicalMode: checkedOpts.historicalMode, requestNonce: checkedOpts.requestNonce, distrustedKeys: distrusted });
|
|
2591
2679
|
var out = guard.verdict.of(v);
|
|
2592
2680
|
return guard.verdict.of(out, { valid: out.status === "good" });
|
|
2593
2681
|
}
|
|
@@ -2646,7 +2734,7 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
|
2646
2734
|
try { issuerKeyBits = ocspVerify.ocspKeyValue(issuerCert.subjectPublicKeyInfo.bytes); }
|
|
2647
2735
|
catch (_e) { return unbound("the issuer public key could not be read to recompute the OCSP CertID"); }
|
|
2648
2736
|
return intrinsic.chain(ocspCore.evaluateResponse(parsedResponse, cert, issuerCtx, issuerKeyBits,
|
|
2649
|
-
issuerNameCandidates, time, opts.historicalMode === true), function (v) {
|
|
2737
|
+
issuerNameCandidates, time, opts.historicalMode === true, opts.distrustedKeys || null), function (v) {
|
|
2650
2738
|
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);
|
|
2651
2739
|
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);
|
|
2652
2740
|
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);
|
|
@@ -2714,6 +2802,7 @@ function assertAnchorConstraints(ta, checkPurpose) {
|
|
|
2714
2802
|
|
|
2715
2803
|
function resolvePurposeOpts(opts) {
|
|
2716
2804
|
var requiredEku = null;
|
|
2805
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no EKU is required, so a null must be refused rather than drop the restriction
|
|
2717
2806
|
if (opts.requiredEku !== undefined) {
|
|
2718
2807
|
if (!intrinsic.isArray(opts.requiredEku) || opts.requiredEku.length === 0) {
|
|
2719
2808
|
throw E("path/bad-input", "validate: opts.requiredEku must be a non-empty array of key-purpose OID names or dotted OID strings");
|
|
@@ -2727,6 +2816,7 @@ function resolvePurposeOpts(opts) {
|
|
|
2727
2816
|
});
|
|
2728
2817
|
}
|
|
2729
2818
|
var checkPurpose = null;
|
|
2819
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no purpose is checked, so a null must be refused rather than drop the check
|
|
2730
2820
|
if (opts.checkPurpose !== undefined) {
|
|
2731
2821
|
if (typeof opts.checkPurpose !== "string" || opts.checkPurpose.length === 0) {
|
|
2732
2822
|
throw E("path/bad-input", "validate: opts.checkPurpose must be a key-purpose OID name or dotted OID string");
|
|
@@ -3499,6 +3589,7 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3499
3589
|
* @opts maxAiaPerCert Cap on caIssuers URLs tried per certificate (default `C.LIMITS.PATH_AIA_MAX_PER_CERT`).
|
|
3500
3590
|
* @opts aiaTimeout Per-fetch timeout in ms, forwarded to the transport.
|
|
3501
3591
|
* @opts maxResponseBytes Per-fetch response size cap, forwarded to the transport (tightenable downward only).
|
|
3592
|
+
* @opts distrustedKeys SHA-256 SubjectPublicKeyInfo digests refused on every path `validate` is handed (see `pki.path.validate`). It is read once at this door, refused with `path/bad-input` when malformed even if no path assembles, and refused alongside `validate: false`. A path through a distrusted intermediate or anchor fails, so the search backtracks to another, such as a cross-certificate to a different root.
|
|
3502
3593
|
* @opts (validate options) Every `pki.path.validate` option (`requiredEku`, `revocationChecker`, `checkPurpose`, the initial policy inputs, ...) is forwarded unchanged. `requireRevocation` and the three policy inputs are typed at this door as well as at `validate`'s, because a build reaches an AIA fetch before it reaches `validate` and a build that assembles no path never calls it. When `userInitialPolicySet` is supplied, build returns the accepted path whose user-constrained policy set best satisfies it (RFC 4158 sec. 4 forward policy chaining) rather than the first accepted path; `validate` still gates every path, so this changes only which valid path is chosen. The ranking explores more of the candidate graph than a first-accept build, so it is bounded by `maxCandidatesConsidered` like the rest of the search. Reaching that cap means stop looking, not nothing found: if a valid path was already accepted, build returns the best-policy one of those; it reaches `path/build-limit` only when the cap is hit before any valid path is accepted. An `anyPolicy` entry makes the set unconstrained, so ranking is skipped and the first accepted path is returned.
|
|
3503
3594
|
* @example
|
|
3504
3595
|
* var pair = await pki.key.generate("Ed25519");
|
|
@@ -3523,6 +3614,21 @@ var _BUILD_OPTS = (function () {
|
|
|
3523
3614
|
return o;
|
|
3524
3615
|
})();
|
|
3525
3616
|
|
|
3617
|
+
/** @internal The TLS policy for the AIA host, checked and copied at the build door. An AIA fetch fault
|
|
3618
|
+
* is a silent skip, so a misspelled key or a malformed distrust list that reached the transport
|
|
3619
|
+
* would only ever show as an intermediate that was not fetched. */
|
|
3620
|
+
function _aiaTls(tls) {
|
|
3621
|
+
if (tls == null) return intrinsic.create(null);
|
|
3622
|
+
guard.identifier.assertKnownKeys(tls, httpTransport.TLS_KEYS, E, "path/bad-input", "build: opts.tls has an unknown option ");
|
|
3623
|
+
/** @internal Each setting the transport reads is taken by value, inherited or own, as the transport
|
|
3624
|
+
* reads the bag. */
|
|
3625
|
+
var copy = guard.identifier.pickSettings(tls, intrinsic.keys(httpTransport.TLS_KEYS));
|
|
3626
|
+
if (copy.distrustedKeys != null) {
|
|
3627
|
+
copy.distrustedKeys = guard.distrust.keys(copy.distrustedKeys, E, "path/bad-input", "build: opts.tls.distrustedKeys");
|
|
3628
|
+
}
|
|
3629
|
+
return copy;
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3526
3632
|
async function build(leaf, opts) {
|
|
3527
3633
|
opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "build: opts");
|
|
3528
3634
|
guard.identifier.assertKnownKeys(opts, _BUILD_OPTS, E, "path/bad-input",
|
|
@@ -3565,11 +3671,19 @@ async function build(leaf, opts) {
|
|
|
3565
3671
|
}
|
|
3566
3672
|
});
|
|
3567
3673
|
}
|
|
3674
|
+
/** @internal Typed and copied at this door for the reason the switches are: a build that
|
|
3675
|
+
* assembles no path never reaches `validate`, and every path it does assemble is validated after
|
|
3676
|
+
* an await. Every `validate` call is handed this copy. */
|
|
3677
|
+
var buildDistrusted = guard.distrust.keys(opts.distrustedKeys, E, "path/bad-input", "build: opts.distrustedKeys");
|
|
3678
|
+
if (!doValidateOpt && buildDistrusted !== null && buildDistrusted.length > 0) {
|
|
3679
|
+
throw E("path/bad-input", "build: opts.distrustedKeys refuses keys during validation, and opts.validate is false, " +
|
|
3680
|
+
"which returns the ordered path without validating; drop one of the two");
|
|
3681
|
+
}
|
|
3568
3682
|
var leafCert;
|
|
3569
3683
|
try { leafCert = coerceCert(leaf); }
|
|
3570
3684
|
catch (e) { throw E("path/bad-input", "build: the leaf certificate did not parse", e); }
|
|
3571
3685
|
|
|
3572
|
-
var poolInput = opts.candidates
|
|
3686
|
+
var poolInput = opts.candidates != null ? opts.candidates : opts.intermediates;
|
|
3573
3687
|
if (poolInput === undefined) poolInput = [];
|
|
3574
3688
|
if (!intrinsic.isArray(poolInput)) throw E("path/bad-input", "build: opts.candidates must be an array of certificates");
|
|
3575
3689
|
var poolCeiling = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES;
|
|
@@ -3604,7 +3718,7 @@ async function build(leaf, opts) {
|
|
|
3604
3718
|
var maxConsidered = guard.limits.cap(opts.maxCandidatesConsidered, "build: opts.maxCandidatesConsidered", poolCeiling, { E: E, code: "path/bad-input", min: 1 });
|
|
3605
3719
|
var doValidate = doValidateOpt;
|
|
3606
3720
|
|
|
3607
|
-
var direction = opts.direction
|
|
3721
|
+
var direction = opts.direction != null ? opts.direction : "forward";
|
|
3608
3722
|
if (direction !== "forward" && direction !== "reverse" && direction !== "auto") {
|
|
3609
3723
|
throw E("path/bad-input", "build: opts.direction must be \"forward\", \"reverse\", or \"auto\", got " + guard.text.showValue(opts.direction));
|
|
3610
3724
|
}
|
|
@@ -3613,7 +3727,7 @@ async function build(leaf, opts) {
|
|
|
3613
3727
|
|
|
3614
3728
|
var aiaCtx = null;
|
|
3615
3729
|
if (opts.fetchAia === true) {
|
|
3616
|
-
if (opts.aiaTimeout
|
|
3730
|
+
if (opts.aiaTimeout != null) {
|
|
3617
3731
|
guard.limits.cap(opts.aiaTimeout, "build: opts.aiaTimeout", 1, { E: E, code: "path/bad-input", min: 1, max: httpTransport.MAX_TIMEOUT });
|
|
3618
3732
|
}
|
|
3619
3733
|
var aiaTransport = checkedTransport || httpTransport.https({ E: E, errPrefix: "path" });
|
|
@@ -3625,7 +3739,7 @@ async function build(leaf, opts) {
|
|
|
3625
3739
|
* hostname taken from a certificate would be opened on that promise.
|
|
3626
3740
|
* @guard-via guard.identifier.declaresOwn */
|
|
3627
3741
|
transportGuardsAddresses: guard.identifier.declaresOwn(aiaTransport, "blocksPrivateAddresses"),
|
|
3628
|
-
tls: opts.tls
|
|
3742
|
+
tls: _aiaTls(opts.tls),
|
|
3629
3743
|
timeout: opts.aiaTimeout,
|
|
3630
3744
|
maxResponseBytes: guard.limits.cap(opts.maxResponseBytes, "build: opts.maxResponseBytes", constants.LIMITS.PATH_AIA_MAX_RESPONSE_BYTES, { E: E, code: "path/bad-input", min: 1, max: constants.LIMITS.PATH_AIA_MAX_RESPONSE_BYTES }),
|
|
3631
3745
|
maxPerCert: guard.limits.cap(opts.maxAiaPerCert, "build: opts.maxAiaPerCert", constants.LIMITS.PATH_AIA_MAX_PER_CERT, { E: E, code: "path/bad-input", min: 0 }),
|
|
@@ -3645,7 +3759,7 @@ async function build(leaf, opts) {
|
|
|
3645
3759
|
* @guard-via guard.identifier.snapshotPresentOptions */
|
|
3646
3760
|
var FORWARD_BY_VALUE = intrinsic.create(null);
|
|
3647
3761
|
intrinsic.forEach(intrinsic.keys(_VALIDATE_OPTS), function (k) {
|
|
3648
|
-
if (intrinsic.hasOwn(BUILD_ONLY_OPT, k) || intrinsic.hasOwn(forwardedSwitches, k)) return;
|
|
3762
|
+
if (intrinsic.hasOwn(BUILD_ONLY_OPT, k) || intrinsic.hasOwn(forwardedSwitches, k) || k === "distrustedKeys") return;
|
|
3649
3763
|
FORWARD_BY_VALUE[k] = 1;
|
|
3650
3764
|
});
|
|
3651
3765
|
var forwarded = guard.identifier.snapshotPresentOptions(opts, FORWARD_BY_VALUE);
|
|
@@ -3659,6 +3773,7 @@ async function build(leaf, opts) {
|
|
|
3659
3773
|
});
|
|
3660
3774
|
if (buildPermRaw != null) forwarded.initialPermittedSubtrees = buildPermSeeds;
|
|
3661
3775
|
if (buildExclRaw != null) forwarded.initialExcludedSubtrees = buildExclSeeds;
|
|
3776
|
+
if (buildDistrusted !== null) forwarded.distrustedKeys = buildDistrusted;
|
|
3662
3777
|
function validateOpts(anchor) {
|
|
3663
3778
|
var vo = Object.create(null);
|
|
3664
3779
|
intrinsic.forEach(intrinsic.keys(forwarded), function (f) { vo[f] = forwarded[f]; });
|
package/lib/possession.js
CHANGED
|
@@ -118,7 +118,7 @@ var STATEMENT = schema.seq([
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
var KNOWN_VERIFY_OPTS = _assign(_create(null), {
|
|
121
|
-
trustAnchors: 1, time: 1, signatureCertificate: 1, intermediates: 1,
|
|
121
|
+
trustAnchors: 1, time: 1, signatureCertificate: 1, intermediates: 1, distrustedKeys: 1,
|
|
122
122
|
});
|
|
123
123
|
|
|
124
124
|
/** @internal Whether a certificate is the one an IssuerAndSerialNumber names. Both halves are compared,
|
|
@@ -345,6 +345,7 @@ var _isSignatureOnlyKey = pkix.signatureOnlyKeyOid(NS);
|
|
|
345
345
|
* that is present and is not a usable `Date` is `possession/bad-input`.
|
|
346
346
|
* @opts signatureCertificate The signature certificate, for a statement that omits it; refused when it is not the one `signer` names.
|
|
347
347
|
* @opts intermediates The rest of the signature certificate's path, when an intermediate CA issued it rather than an anchor directly. Ordered as `pki.path.validate` takes a path, from the certificate nearest an anchor down to the issuer of the signature certificate, which this verb appends. Omitted, the signature certificate is validated on its own, which succeeds only when an anchor issued it.
|
|
348
|
+
* @opts distrustedKeys SHA-256 digests of SubjectPublicKeyInfo values (64 hex characters or 32 bytes each). A listed key on the anchor, an intermediate or the signature certificate makes `pathValidated` and `valid` false. A malformed list is `possession/bad-input`.
|
|
348
349
|
*
|
|
349
350
|
* @example
|
|
350
351
|
* var kem = await pki.key.generate({ name: "ML-KEM-768" });
|
|
@@ -382,6 +383,7 @@ function _verifyRequest(request, opts) {
|
|
|
382
383
|
guard.identifier.assertKnownKeys(opts, KNOWN_VERIFY_OPTS, _err, "possession/bad-input",
|
|
383
384
|
"pki.possession.verifyRequest has an unknown option: ");
|
|
384
385
|
var o = guard.identifier.snapshotOptions(opts, KNOWN_VERIFY_OPTS);
|
|
386
|
+
var distrusted = guard.distrust.keys(o.distrustedKeys, _err, "possession/bad-input", "opts.distrustedKeys");
|
|
385
387
|
if (o.trustAnchors == null || !_isArray(o.trustAnchors) || o.trustAnchors.length === 0) {
|
|
386
388
|
throw _err("possession/bad-input",
|
|
387
389
|
"opts.trustAnchors is required: RFC 9883 sec. 4 makes certification path validation of the signature certificate a MUST, and a verdict that skipped it would not answer the question asked");
|
|
@@ -522,8 +524,10 @@ function _verifyRequest(request, opts) {
|
|
|
522
524
|
"authorize a certification request (RFC 5280 sec. 4.2.1.3)");
|
|
523
525
|
}
|
|
524
526
|
|
|
527
|
+
var pathOpts = { trustAnchors: o.trustAnchors, time: at };
|
|
528
|
+
if (distrusted !== null) pathOpts.distrustedKeys = distrusted;
|
|
525
529
|
return intrinsic.chain(
|
|
526
|
-
_promiseResolve(_Promise, _validatePath(chain,
|
|
530
|
+
_promiseResolve(_Promise, _validatePath(chain, pathOpts)),
|
|
527
531
|
function (pathResult) {
|
|
528
532
|
var pathValidated = pathResult.valid === true;
|
|
529
533
|
if (!pathValidated) _push(reasons, "the signature certificate's certification path is not valid, and RFC 9883 sec. 4 makes that a MUST");
|
package/lib/related-cert.js
CHANGED
|
@@ -208,7 +208,7 @@ var KNOWN_VERIFY_OPTS = _assign(_create(null), { digestAlgorithm: 1, signatureAl
|
|
|
208
208
|
* an attacker's key verifies forever without it, and a verifier that cannot perform a MUST check must not
|
|
209
209
|
* answer true. `at` is the instant judged against, for a caller reading a request recorded earlier. */
|
|
210
210
|
function _assertFresh(requestTime, o) {
|
|
211
|
-
if (o.maxAge
|
|
211
|
+
if (o.maxAge == null) {
|
|
212
212
|
throw _err("relatedcert/no-freshness-policy",
|
|
213
213
|
"pki.relatedCert.verifyRequest needs opts.maxAge, the seconds a requestTime may be old: RFC 9763 " +
|
|
214
214
|
"sec. 3.2 requires the certification authority to check that it is sufficiently fresh and leaves " +
|
|
@@ -217,7 +217,7 @@ function _assertFresh(requestTime, o) {
|
|
|
217
217
|
}
|
|
218
218
|
var maxAge = guard.range.authoredInteger(o.maxAge, _err, "relatedcert/bad-input", "opts.maxAge");
|
|
219
219
|
if (maxAge < 0n) throw _err("relatedcert/bad-input", "opts.maxAge must not be negative");
|
|
220
|
-
var at = o.at
|
|
220
|
+
var at = o.at == null ? new intrinsic.Date() : o.at;
|
|
221
221
|
if (!guard.time.isDate(at)) throw _err("relatedcert/bad-input", "opts.at must be a Date");
|
|
222
222
|
guard.time.assertValid(at, _err, "relatedcert/bad-input", "opts.at");
|
|
223
223
|
var atMs = guard.time.instantOf(at);
|