@blamejs/pki 0.9.4 → 0.9.6
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 +60 -0
- package/README.md +7 -6
- package/bin/pki.js +110 -34
- package/lib/acme.js +91 -46
- package/lib/attrcert-sign.js +4 -3
- package/lib/cmp-build.js +16 -6
- package/lib/cmp-session.js +19 -3
- package/lib/cmp-verify.js +25 -2
- package/lib/cms-compress.js +3 -3
- package/lib/cms-digest.js +10 -5
- package/lib/cms-encrypt.js +6 -1
- package/lib/cms-sign.js +16 -0
- package/lib/cms-verify.js +32 -14
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +3 -3
- package/lib/csr-sign.js +6 -2
- package/lib/ct.js +16 -3
- package/lib/est.js +67 -31
- package/lib/guard-all.js +2 -0
- package/lib/guard-distrust.js +224 -0
- package/lib/guard-identifier.js +27 -1
- package/lib/guard-json.js +1 -1
- package/lib/http-transport.js +299 -36
- package/lib/jose.js +1 -1
- package/lib/lint.js +67 -7
- package/lib/ocsp-verify.js +9 -3
- package/lib/ocsp.js +16 -4
- package/lib/oid.js +4 -0
- package/lib/path-validate.js +119 -18
- package/lib/pkcs12-build.js +6 -1
- package/lib/pki-build.js +38 -6
- package/lib/possession.js +6 -2
- package/lib/related-cert.js +2 -2
- package/lib/revocation-fetch.js +46 -11
- package/lib/scep.js +39 -14
- package/lib/schema-pkix.js +3 -3
- package/lib/sigstore.js +11 -6
- package/lib/smime.js +32 -16
- package/lib/tls-cert-compress.js +6 -5
- package/lib/trust.js +121 -20
- package/lib/trustanchor-build.js +16 -1
- package/lib/tsp-sign.js +15 -2
- package/lib/webauthn-mds.js +29 -8
- package/lib/webauthn.js +46 -14
- package/lib/x509-sign.js +26 -10
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
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,64 @@ var RFC5480_RULES = [
|
|
|
3938
3937
|
},
|
|
3939
3938
|
];
|
|
3940
3939
|
|
|
3940
|
+
/** @internal The curves the small-scalar table is built for, and the scalar bound. Each table holds
|
|
3941
|
+
* the x-coordinates of d*G for d from 1 to the bound minus one, built once per curve on first use
|
|
3942
|
+
* (measured on Node 26: 4 ms for P-256, 138 ms for P-384, 321 ms for P-521). The points d*G and
|
|
3943
|
+
* (n-d)*G share an x-coordinate, so one comparison covers both. */
|
|
3944
|
+
var SMALL_SCALAR_WIDTH = intrinsic.assign(intrinsic.create(null), { prime256v1: 32, secp384r1: 48, secp521r1: 66, secp256k1: 32 });
|
|
3945
|
+
var SMALL_SCALAR_BOUND = 256;
|
|
3946
|
+
var _smallScalarTables = intrinsic.create(null);
|
|
3947
|
+
function _smallScalarTable(curve) {
|
|
3948
|
+
if (_smallScalarTables[curve]) return _smallScalarTables[curve];
|
|
3949
|
+
var width = SMALL_SCALAR_WIDTH[curve];
|
|
3950
|
+
var table = intrinsic.create(null);
|
|
3951
|
+
var scalar = intrinsic.bufferAlloc(width);
|
|
3952
|
+
for (var d = 1; d < SMALL_SCALAR_BOUND; d++) {
|
|
3953
|
+
scalar[width - 1] = d;
|
|
3954
|
+
var point = guard.crypto.ecPointFromScalar(curve, scalar);
|
|
3955
|
+
table[intrinsic.bufToString(point, "hex", 1, 1 + width)] = true;
|
|
3956
|
+
}
|
|
3957
|
+
_smallScalarTables[curve] = table;
|
|
3958
|
+
return table;
|
|
3959
|
+
}
|
|
3960
|
+
/** @internal The JWK curve names the table covers, mapped to the curve names this file reports. */
|
|
3961
|
+
var SMALL_SCALAR_JWK_CURVES = intrinsic.assign(intrinsic.create(null), {
|
|
3962
|
+
"P-256": "prime256v1", "P-384": "secp384r1", "P-521": "secp521r1", "secp256k1": "secp256k1",
|
|
3963
|
+
});
|
|
3964
|
+
/** @internal The curve and the x-coordinate of an elliptic curve subject key, read through node's own
|
|
3965
|
+
* import, or null. The import accepts a named or explicit curve and an uncompressed, compressed or
|
|
3966
|
+
* hybrid point, and it refuses a point that is not on the curve, so a key that is not one reports
|
|
3967
|
+
* nothing here. */
|
|
3968
|
+
function _ecKeyCoordinate(spki) {
|
|
3969
|
+
try {
|
|
3970
|
+
var jwk = guard.crypto.exportKey(guard.crypto.publicKey({ key: spki.bytes, format: "der", type: "spki" }), { format: "jwk" });
|
|
3971
|
+
var curve = SMALL_SCALAR_JWK_CURVES[jwk.crv];
|
|
3972
|
+
if (curve === undefined || typeof jwk.x !== "string") return null;
|
|
3973
|
+
return { curve: curve, x: intrinsic.bufToString(intrinsic.bufferFrom(jwk.x, "base64url"), "hex") };
|
|
3974
|
+
} catch (_e) {
|
|
3975
|
+
// allow:swallow-unverified a subject key node does not import is not an elliptic curve point this row can judge
|
|
3976
|
+
return null;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
function _isEcSubjectKey(cert) {
|
|
3980
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
3981
|
+
return !!spki && !!spki.algorithm && spki.algorithm.name === "ecPublicKey" && intrinsic.isBuffer(spki.bytes);
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
var KEY_COMPROMISE_RULES = [
|
|
3985
|
+
{
|
|
3986
|
+
id: "lint/key-compromise/small-ec-private-key", severity: "error", source: "key-compromise",
|
|
3987
|
+
citation: "CABF TLS BR 6.1.1.3 (4)",
|
|
3988
|
+
message: "the elliptic curve private key is a small integer, so anyone can recover it from the public key",
|
|
3989
|
+
appliesTo: _isEcSubjectKey,
|
|
3990
|
+
check: function (cert) {
|
|
3991
|
+
var key = _ecKeyCoordinate(cert.subjectPublicKeyInfo);
|
|
3992
|
+
if (key === null) return null;
|
|
3993
|
+
return _smallScalarTable(key.curve)[key.x] === true ? { context: { curve: key.curve, scalarBelow: SMALL_SCALAR_BOUND } } : null;
|
|
3994
|
+
},
|
|
3995
|
+
},
|
|
3996
|
+
];
|
|
3997
|
+
|
|
3941
3998
|
var RFC9935_RULES = [
|
|
3942
3999
|
{
|
|
3943
4000
|
id: "lint/rfc9935/kem-key-usage", severity: "error", source: "rfc9935", citation: "RFC 9935 5",
|
|
@@ -6129,7 +6186,7 @@ function _nistKindOf(alg) {
|
|
|
6129
6186
|
/** @internal A key-agreement scheme names a key-derivation function for CMS rather than an algorithm
|
|
6130
6187
|
* a certificate declares, and its name opens with the family word. */
|
|
6131
6188
|
if (name.indexOf("dhSinglePass-") === 0) return undefined;
|
|
6132
|
-
if (name === "ecPublicKey" || name.indexOf("ecdsaWith") === 0 ||
|
|
6189
|
+
if (name === "ecPublicKey" || name === "ecDH" || name === "ecMQV" || name.indexOf("ecdsaWith") === 0 ||
|
|
6133
6190
|
name === "X25519" || name === "X448") return "ecc";
|
|
6134
6191
|
if (name === "Ed25519" || name === "Ed448") return "eddsa";
|
|
6135
6192
|
if (name === "dsa" || name.indexOf("dsaWith") === 0 ||
|
|
@@ -6719,6 +6776,7 @@ var PROFILES = Object.assign(Object.create(null), {
|
|
|
6719
6776
|
* from a rule that quietly replaced the other. */
|
|
6720
6777
|
"cnsa-2.0": CNSA2_RULES.concat(RFC9881_RULES).concat(RFC9935_RULES)
|
|
6721
6778
|
.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6779
|
+
"key-compromise": intrinsic.concatList(KEY_COMPROMISE_RULES, RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6722
6780
|
});
|
|
6723
6781
|
var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
|
|
6724
6782
|
.concat(RFC9763_RULES).concat(RFC7633_RULES).concat(X509_ALTSIG_RULES)
|
|
@@ -6732,6 +6790,7 @@ var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat
|
|
|
6732
6790
|
* operand and builds through ArraySpeciesCreate, so a replacement decides which rows the default
|
|
6733
6791
|
* set holds. The pre-existing chain above is counted in the conversion budget. */
|
|
6734
6792
|
ALL_RULES = intrinsic.concatList(ALL_RULES, RFC5480_RULES);
|
|
6793
|
+
ALL_RULES = intrinsic.concatList(ALL_RULES, KEY_COMPROMISE_RULES);
|
|
6735
6794
|
|
|
6736
6795
|
var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
|
|
6737
6796
|
return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
|
|
@@ -6820,8 +6879,9 @@ var CSR_PROFILES = Object.assign(Object.create(null), {
|
|
|
6820
6879
|
"rfc2986": CSR_STRUCTURAL_RULES,
|
|
6821
6880
|
"cabf-tls": CSR_STRUCTURAL_RULES.concat(CABF_TLS_CSR_RULES),
|
|
6822
6881
|
"rfc9883": RFC9883_CSR_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6882
|
+
"key-compromise": intrinsic.concatList(KEY_COMPROMISE_RULES, RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6823
6883
|
});
|
|
6824
|
-
var ALL_CSR_RULES = CSR_STRUCTURAL_RULES.concat(RFC9883_CSR_RULES);
|
|
6884
|
+
var ALL_CSR_RULES = intrinsic.concatList(CSR_STRUCTURAL_RULES.concat(RFC9883_CSR_RULES), KEY_COMPROMISE_RULES);
|
|
6825
6885
|
|
|
6826
6886
|
function _subjectIsNonEmpty(csr) {
|
|
6827
6887
|
var rdns = csr.subject && csr.subject.rdns;
|
|
@@ -7751,8 +7811,8 @@ function rules(profile, artifact) {
|
|
|
7751
7811
|
* // -> ["rfc5280", "rfc5480", "rfc9881", "rfc9909", "rfc9935", "rfc9763", "rfc7633",
|
|
7752
7812
|
* // "x509-altsig", "cabf-tls", "cabf-smime", "cabf-cs", "etsi-qc", "mozilla-root",
|
|
7753
7813
|
* // "chrome-root", "microsoft-root", "apple-root", "nist-pqc-transition", "cnsa-2.0",
|
|
7754
|
-
* // "rfc2986", "rfc9883", "rfc5280-crl", "rfc6960", "rfc5019",
|
|
7755
|
-
* // "rfc3161", "rfc5755"]
|
|
7814
|
+
* // "key-compromise", "rfc2986", "rfc9883", "rfc5280-crl", "rfc6960", "rfc5019",
|
|
7815
|
+
* // "rfc5652", "rfc8933", "rfc3161", "rfc5755"]
|
|
7756
7816
|
*/
|
|
7757
7817
|
/** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
|
|
7758
7818
|
* 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
|
@@ -316,6 +316,8 @@ function _normCertDer(cert, what) {
|
|
|
316
316
|
* extendedRevoke emit the id-pkix-ocsp-extended-revoke extension (RFC 6960 sec. 4.4.8); implied
|
|
317
317
|
* by a non-issued response, where `false` is refused.
|
|
318
318
|
* embedCert `false` to omit certs [0] (a direct-CA response the client already trusts).
|
|
319
|
+
* For both switches, a value other than `true`, `false`, `null` or an absent
|
|
320
|
+
* option is refused `ocsp/bad-input`.
|
|
319
321
|
* pem emit a PEM `OCSP RESPONSE` string instead of DER.
|
|
320
322
|
* @example
|
|
321
323
|
* var ca = await pki.key.generate("Ed25519");
|
|
@@ -350,6 +352,12 @@ function _sign(responseData, responder, opts) {
|
|
|
350
352
|
opts = guard.identifier.optionsObject(opts, _err, "ocsp/bad-input", "pki.ocsp.sign options");
|
|
351
353
|
guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "ocsp/bad-input",
|
|
352
354
|
"pki.ocsp.sign has an unknown option (the nonce to echo back is `nonce`): ");
|
|
355
|
+
/** @internal Both switches are read below against one boolean each, which answered for a value
|
|
356
|
+
* of any other type as the opposite of what the caller asked: `embedCert: "false"` carried the
|
|
357
|
+
* responder certificate a direct-CA response leaves out, and `extendedRevoke: "false"` placed
|
|
358
|
+
* the extension while escaping the sec. 2.2 refusal that reads the same option. */
|
|
359
|
+
guard.identifier.booleanOptionDefaultOn(opts.embedCert, _err, "ocsp/bad-input", "pki.ocsp.sign opts.embedCert");
|
|
360
|
+
guard.identifier.booleanOption(opts.extendedRevoke, _err, "ocsp/bad-input", "pki.ocsp.sign opts.extendedRevoke");
|
|
353
361
|
responseData = responseData || _ocspIntrinsic.create(null);
|
|
354
362
|
if (typeof responseData !== "object") throw _err("ocsp/bad-input", "the responseData must be an object { responderID, responses, producedAt? }");
|
|
355
363
|
guard.identifier.assertKnownKeys(responseData, _RESPONSE_DATA_KEYS, _err, "ocsp/bad-input", "unknown responseData field (accepted: responderID, responses, producedAt): ");
|
|
@@ -674,6 +682,9 @@ function buildErrorResponse(status) {
|
|
|
674
682
|
* time the validation instant (default: now).
|
|
675
683
|
* requestNonce the nonce the client sent; when given, the response MUST echo it (constant-time).
|
|
676
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"`.
|
|
677
688
|
* @example
|
|
678
689
|
* var ca = await pki.key.generate("Ed25519");
|
|
679
690
|
* var caKey = await pki.key.export(ca.privateKey);
|
|
@@ -691,7 +702,7 @@ function buildErrorResponse(status) {
|
|
|
691
702
|
* var res = await pki.ocsp.verify(responseDer, { cert: leafDer, issuer: caDer });
|
|
692
703
|
* res.status; // "good" | "revoked" | "unknown"
|
|
693
704
|
*/
|
|
694
|
-
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 });
|
|
695
706
|
|
|
696
707
|
function verify(response, opts) {
|
|
697
708
|
try {
|
|
@@ -700,8 +711,9 @@ function verify(response, opts) {
|
|
|
700
711
|
"pki.ocsp.verify has an unknown option (the request nonce to bind against is `requestNonce`): ");
|
|
701
712
|
} catch (e) { return _promiseReject(_Promise, e); }
|
|
702
713
|
if (opts.cert == null || opts.issuer == null) return _promiseReject(_Promise, _err("ocsp/bad-input", "verify requires opts.cert and opts.issuer"));
|
|
703
|
-
var parsed, cert, issuerCert, time;
|
|
714
|
+
var parsed, cert, issuerCert, time, distrusted;
|
|
704
715
|
try {
|
|
716
|
+
distrusted = guard.distrust.keys(opts.distrustedKeys, _err, "ocsp/bad-input", "opts.distrustedKeys");
|
|
705
717
|
parsed = _responseFromBytes(response);
|
|
706
718
|
cert = _certOf(opts.cert, "the target certificate");
|
|
707
719
|
issuerCert = _certOf(opts.issuer, "the issuer certificate");
|
|
@@ -720,7 +732,7 @@ function verify(response, opts) {
|
|
|
720
732
|
/** @internal Captured: this continuation is where the response's status becomes the verdict a
|
|
721
733
|
* caller reads, so a settled value in place of it reports a status nothing evaluated. */
|
|
722
734
|
var evaluated = pathValidate.verifyOcspResponse(parsed, cert, issuerCert, time,
|
|
723
|
-
{ historicalMode: opts.historicalMode === true, requestNonce: reqNonce });
|
|
735
|
+
{ historicalMode: opts.historicalMode === true, requestNonce: reqNonce, distrustedKeys: distrusted });
|
|
724
736
|
return _ocspIntrinsic.chain(evaluated, function (verdict) {
|
|
725
737
|
return guard.verdict.of(verdict, { valid: verdict.status === "good" });
|
|
726
738
|
});
|
|
@@ -941,7 +953,7 @@ function httpRequest(der, responderUrl, opts) {
|
|
|
941
953
|
if (!guard.encoding.isAsciiText(responderUrl)) {
|
|
942
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");
|
|
943
955
|
}
|
|
944
|
-
var asked = opts.method
|
|
956
|
+
var asked = opts.method == null ? null : opts.method;
|
|
945
957
|
if (asked !== null && asked !== "GET" && asked !== "POST") {
|
|
946
958
|
throw E("ocsp/bad-input", "httpRequest: opts.method must be \"GET\" or \"POST\"");
|
|
947
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,19 @@ 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
|
+
* `distrustedKeysApplied` reports whether a non-empty list was supplied. A malformed list is
|
|
1447
|
+
* `path/bad-input`.
|
|
1448
|
+
*
|
|
1425
1449
|
* `anchorConstraints` reports what the anchor's own trust metadata decided:
|
|
1426
1450
|
* the `checkedPurpose` it was judged under, and whether the `distrustAfter`
|
|
1427
1451
|
* date, the `purposes` delegator map and the `nameConstraints` namespace each
|
|
@@ -1461,7 +1485,7 @@ function _revocationRow(row, rv) {
|
|
|
1461
1485
|
* res.valid; // boolean; res.results[0].checks carries the per-check codes
|
|
1462
1486
|
*/
|
|
1463
1487
|
var _VALIDATE_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
1464
|
-
checkPurpose: 1, ctLogList: 1, ctPolicy: 1, historicalMode: 1, identity: 1, initialAnyPolicyInhibit: 1,
|
|
1488
|
+
checkPurpose: 1, ctLogList: 1, ctPolicy: 1, distrustedKeys: 1, historicalMode: 1, identity: 1, initialAnyPolicyInhibit: 1,
|
|
1465
1489
|
initialExcludedSubtrees: 1, initialExplicitPolicy: 1, initialPermittedSubtrees: 1,
|
|
1466
1490
|
initialPolicyMappingInhibit: 1, maxPathCerts: 1, maxPolicyNodes: 1, minRsaModulusBits: 1, requireRevocation: 1,
|
|
1467
1491
|
requiredEku: 1, revocationChecker: 1, softFail: 1, time: 1, trustAnchors: 1,
|
|
@@ -1516,8 +1540,12 @@ async function validate(path, opts) {
|
|
|
1516
1540
|
if (suppliedVerifier !== null && typeof suppliedVerify !== "function") {
|
|
1517
1541
|
throw E("path/bad-input", "validate: opts.verifier must carry a verify(args) function");
|
|
1518
1542
|
}
|
|
1543
|
+
/** @internal The distrust list is read and copied here, once, and every anchor attempt reads the
|
|
1544
|
+
* copy: the attempts run after awaits, and a list re-read from the caller's array there could have
|
|
1545
|
+
* been emptied by a revocation checker that ran in between. */
|
|
1546
|
+
var distrusted = guard.distrust.keys(checked.distrustedKeys, E, "path/bad-input", "validate: opts.distrustedKeys");
|
|
1519
1547
|
var scope = intrinsic.freeze(intrinsic.assign(intrinsic.create(null), {
|
|
1520
|
-
verifier: suppliedVerifier, verify: suppliedVerify,
|
|
1548
|
+
verifier: suppliedVerifier, verify: suppliedVerify, distrusted: distrusted,
|
|
1521
1549
|
}));
|
|
1522
1550
|
return _validate(path, checked, intrinsic.freeze(intrinsic.create(null)), scope);
|
|
1523
1551
|
}
|
|
@@ -1670,6 +1698,13 @@ async function _validate(path, opts, run, scope) {
|
|
|
1670
1698
|
var revocationRan = false, revocationWaived = false, revocationUndetermined = false;
|
|
1671
1699
|
var prefixAuthentic = true;
|
|
1672
1700
|
var anchorDistrustApplied = false, anchorPurposeApplied = false;
|
|
1701
|
+
var distrusted = scope.distrusted;
|
|
1702
|
+
var anchorKeyDistrusted = guard.distrust.has(distrusted, anchor.publicKey);
|
|
1703
|
+
/** @internal The first path index whose issuer chain holds a distrusted key: the anchor's key
|
|
1704
|
+
* makes it 0, a certificate's own key makes it that certificate. From there down, no revocation
|
|
1705
|
+
* status is asked for. The path fails either way, and a checker that fetches would open a
|
|
1706
|
+
* connection on behalf of certificates issued under a key the caller refuses. */
|
|
1707
|
+
var distrustedFrom = anchorKeyDistrusted ? 0 : -1;
|
|
1673
1708
|
if (!checkPurpose && _hasPurposeScopedMetadata(anchor)) {
|
|
1674
1709
|
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
1710
|
}
|
|
@@ -1705,6 +1740,19 @@ async function _validate(path, opts, run, scope) {
|
|
|
1705
1740
|
* no certificate under it is authenticated however its own check reads. */
|
|
1706
1741
|
if (!sigRes.ok) prefixAuthentic = false;
|
|
1707
1742
|
|
|
1743
|
+
/** @internal The digest covers the SubjectPublicKeyInfo as the certificate encodes it, not the
|
|
1744
|
+
* working key, which carries parameters inherited from the issuer. A cross-signed certificate
|
|
1745
|
+
* carries the same encoded key as the certificate it duplicates, so one entry refuses both. */
|
|
1746
|
+
if (idx === 0 && anchorKeyDistrusted) {
|
|
1747
|
+
guard.list.append(checks, { name: "distrustedIssuerKey", ok: false, code: "path/distrusted-key" });
|
|
1748
|
+
failed = true;
|
|
1749
|
+
}
|
|
1750
|
+
if (guard.distrust.has(distrusted, cert.subjectPublicKeyInfo.bytes) || guard.distrust.has(distrusted, _inheritedSpki(state, cert))) {
|
|
1751
|
+
guard.list.append(checks, { name: "distrustedKey", ok: false, code: "path/distrusted-key" });
|
|
1752
|
+
failed = true;
|
|
1753
|
+
if (distrustedFrom === -1) distrustedFrom = idx;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1708
1756
|
if (compositeSig.COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
|
|
1709
1757
|
var cku = compositeKeyUsageCheck(cert);
|
|
1710
1758
|
guard.list.append(checks,{ name: "compositeKeyUsage", ok: cku.ok, code: cku.ok ? undefined : cku.code });
|
|
@@ -1760,7 +1808,13 @@ async function _validate(path, opts, run, scope) {
|
|
|
1760
1808
|
* socket, so asking anyway lets an unauthenticated certificate choose a destination and spend
|
|
1761
1809
|
* the fetch budget. The row says the status was not asked for, which is an undetermined
|
|
1762
1810
|
* outcome and not a checked one. */
|
|
1763
|
-
if (revocationChecker &&
|
|
1811
|
+
if (revocationChecker && distrustedFrom !== -1 && idx >= distrustedFrom) {
|
|
1812
|
+
guard.list.append(checks, { name: "revocation", ok: false, status: "unknown",
|
|
1813
|
+
code: "path/revocation-undetermined",
|
|
1814
|
+
reason: "a key at or above this certificate on the path is distrusted, so no revocation status was asked for" });
|
|
1815
|
+
revocationUndetermined = true;
|
|
1816
|
+
revocationRan = true;
|
|
1817
|
+
} else if (revocationChecker && !prefixAuthentic) {
|
|
1764
1818
|
guard.list.append(checks, { name: "revocation", ok: false, status: "unknown",
|
|
1765
1819
|
code: "path/revocation-undetermined",
|
|
1766
1820
|
reason: sigRes.ok
|
|
@@ -1771,7 +1825,7 @@ async function _validate(path, opts, run, scope) {
|
|
|
1771
1825
|
} else if (revocationChecker) {
|
|
1772
1826
|
var issuerCert = idx > 0 ? certs[idx - 1] : null;
|
|
1773
1827
|
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 }); }
|
|
1828
|
+
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
1829
|
catch (e) { rv = { status: "error" }; rvError = e; }
|
|
1776
1830
|
var rvStatus = (rv && typeof rv.status === "string") ? rv.status : "unknown";
|
|
1777
1831
|
if (rv && rv.status === "good") { guard.list.append(checks, _revocationRow({ name: "revocation", ok: true, status: "good" }, rv)); }
|
|
@@ -1894,6 +1948,7 @@ async function _validate(path, opts, run, scope) {
|
|
|
1894
1948
|
return guard.verdict.of({
|
|
1895
1949
|
valid: !failed,
|
|
1896
1950
|
identityChecked: identityChecked,
|
|
1951
|
+
distrustedKeysApplied: distrusted !== null && distrusted.length > 0,
|
|
1897
1952
|
revocationChecked: !revocationRan ? false
|
|
1898
1953
|
: revocationUndetermined ? "undetermined"
|
|
1899
1954
|
: revocationWaived ? "waived" : "determined",
|
|
@@ -2147,7 +2202,9 @@ function selectDelta(candidates) {
|
|
|
2147
2202
|
* revocationDate.
|
|
2148
2203
|
*
|
|
2149
2204
|
* @opts
|
|
2150
|
-
* useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)).
|
|
2205
|
+
* useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)). Default `true`;
|
|
2206
|
+
* a value other than `true`, `false`, `null` or an absent option is refused
|
|
2207
|
+
* `path/bad-input`.
|
|
2151
2208
|
* Default true. When false a delta is never merged; it is
|
|
2152
2209
|
* still consulted for revocation.
|
|
2153
2210
|
*
|
|
@@ -2171,11 +2228,20 @@ function crlChecker(crls, opts) {
|
|
|
2171
2228
|
return guard.parsed.acceptDerived(c, "crl", crl.parse, E, "path/bad-input",
|
|
2172
2229
|
"crlChecker: crls[" + ci + "]");
|
|
2173
2230
|
});
|
|
2174
|
-
|
|
2231
|
+
/** @internal The sec. 6.3.1(b) merge, read as the boolean it documents: the comparison this
|
|
2232
|
+
* replaces answered for a value of any other type by merging deltas the caller turned off. */
|
|
2233
|
+
var useDeltas = guard.identifier.booleanOptionDefaultOn(opts && opts.useDeltas, E,
|
|
2234
|
+
"path/bad-input", "crlChecker: opts.useDeltas");
|
|
2175
2235
|
return {
|
|
2176
2236
|
check: async function (cert, issuer, ctx) {
|
|
2177
2237
|
var time = ctx.time;
|
|
2178
2238
|
var historical = ctx.historicalMode === true;
|
|
2239
|
+
/** @internal Every CRL this checker takes is verified under the issuer's key, so a listed
|
|
2240
|
+
* issuer key leaves no CRL that can answer. */
|
|
2241
|
+
var distrusted = guard.distrust.keys(ctx.distrustedKeys, E, "path/bad-input", "crlChecker: ctx.distrustedKeys");
|
|
2242
|
+
if (guard.distrust.has(distrusted, issuer.workingPublicKey)) {
|
|
2243
|
+
return guard.verdict.of({ status: "unknown", reason: "the issuer's key is listed in distrustedKeys, so no CRL it signs is an answer" });
|
|
2244
|
+
}
|
|
2179
2245
|
var certIsCa = null, certScopeFault = null;
|
|
2180
2246
|
try {
|
|
2181
2247
|
var bc = decodeExt(cert, OID.basicConstraints);
|
|
@@ -2468,6 +2534,7 @@ function ocspChecker(responses) {
|
|
|
2468
2534
|
check: async function (cert, issuer, ctx) {
|
|
2469
2535
|
var time = ctx.time;
|
|
2470
2536
|
var historical = ctx.historicalMode === true;
|
|
2537
|
+
var distrusted = guard.distrust.keys(ctx.distrustedKeys, E, "path/bad-input", "ocspChecker: ctx.distrustedKeys");
|
|
2471
2538
|
var issuerNameCandidates = [cert.issuer.bytes];
|
|
2472
2539
|
function addNameCandidate(nm) {
|
|
2473
2540
|
if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) guard.list.append(issuerNameCandidates, nm.bytes);
|
|
@@ -2483,7 +2550,7 @@ function ocspChecker(responses) {
|
|
|
2483
2550
|
var sawUnknownStatus = false;
|
|
2484
2551
|
|
|
2485
2552
|
for (var k = 0; k < parsed.length; k++) {
|
|
2486
|
-
var v = await ocspCore.evaluateResponse(parsed[k], cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical);
|
|
2553
|
+
var v = await ocspCore.evaluateResponse(parsed[k], cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical, distrusted);
|
|
2487
2554
|
if (v.revoked && !revokedResult) {
|
|
2488
2555
|
revokedResult = guard.verdict.of({ status: "revoked", revocationReason: v.revoked.revocationReason, revocationTime: v.revoked.revocationTime, reason: v.revoked.reason });
|
|
2489
2556
|
}
|
|
@@ -2531,6 +2598,11 @@ function ocspChecker(responses) {
|
|
|
2531
2598
|
* `time` either way. `time` must be a valid `Date`. A malformed response's parse
|
|
2532
2599
|
* fault surfaces as the parser's typed `ocsp/*` / `asn1/*` error.
|
|
2533
2600
|
*
|
|
2601
|
+
* `opts.distrustedKeys` lists SHA-256 SubjectPublicKeyInfo digests (see `pki.trust.spkiDigest`). A
|
|
2602
|
+
* response signed by a listed key, whether the issuing CA's or a delegated responder's, has no
|
|
2603
|
+
* authorized responder and is `"unknown"` with `responderAuthorized: false`. `pki.path.ocspChecker`
|
|
2604
|
+
* and `pki.path.fetchingChecker` apply the list `pki.path.validate` was given.
|
|
2605
|
+
*
|
|
2534
2606
|
* `opts.requestNonce` is the nonce bytes the request carried. Given one, the response must echo it
|
|
2535
2607
|
* (RFC 9654 sec. 2.1, RFC 6960 sec. 4.4.1), compared in constant time, and `nonceMatched` reports
|
|
2536
2608
|
* the outcome: `true`, `false`, or `null` when no nonce was sent. An unechoed nonce takes a `good`
|
|
@@ -2566,7 +2638,7 @@ function ocspChecker(responses) {
|
|
|
2566
2638
|
* var v = await pki.path.verifyOcspResponse(der, cert, issuerCert, new Date());
|
|
2567
2639
|
* v.status; // "good" | "revoked" | "unknown"
|
|
2568
2640
|
*/
|
|
2569
|
-
var _VERIFY_OCSP_OPTS = intrinsic.assign(intrinsic.create(null), { historicalMode: 1, requestNonce: 1 });
|
|
2641
|
+
var _VERIFY_OCSP_OPTS = intrinsic.assign(intrinsic.create(null), { distrustedKeys: 1, historicalMode: 1, requestNonce: 1 });
|
|
2570
2642
|
|
|
2571
2643
|
async function verifyOcspResponse(response, cert, issuerCert, time, opts) {
|
|
2572
2644
|
var parsedResponse, subject, issuer, checkedOpts;
|
|
@@ -2578,11 +2650,13 @@ async function verifyOcspResponse(response, cert, issuerCert, time, opts) {
|
|
|
2578
2650
|
checkedOpts = guard.identifier.optionsObject(opts, E, "path/bad-input", "verifyOcspResponse: opts");
|
|
2579
2651
|
guard.identifier.assertKnownKeys(checkedOpts, _VERIFY_OCSP_OPTS, E, "path/bad-input",
|
|
2580
2652
|
"pki.path.verifyOcspResponse has an unknown option (the request nonce to bind against is `requestNonce`). The unknown option was: ");
|
|
2653
|
+
var distrusted = guard.distrust.keys(checkedOpts.distrustedKeys, E, "path/bad-input", "verifyOcspResponse: opts.distrustedKeys");
|
|
2581
2654
|
parsedResponse = _ocspFromBytes(response);
|
|
2582
2655
|
subject = coerceCert(cert, "the certificate");
|
|
2583
2656
|
issuer = coerceCert(issuerCert, "the issuer certificate");
|
|
2584
2657
|
} catch (e) { return _reject(_Promise, e); }
|
|
2585
|
-
var v = await _verifyOcspParsed(parsedResponse, subject, issuer, time,
|
|
2658
|
+
var v = await _verifyOcspParsed(parsedResponse, subject, issuer, time,
|
|
2659
|
+
{ historicalMode: checkedOpts.historicalMode, requestNonce: checkedOpts.requestNonce, distrustedKeys: distrusted });
|
|
2586
2660
|
var out = guard.verdict.of(v);
|
|
2587
2661
|
return guard.verdict.of(out, { valid: out.status === "good" });
|
|
2588
2662
|
}
|
|
@@ -2641,7 +2715,7 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
|
2641
2715
|
try { issuerKeyBits = ocspVerify.ocspKeyValue(issuerCert.subjectPublicKeyInfo.bytes); }
|
|
2642
2716
|
catch (_e) { return unbound("the issuer public key could not be read to recompute the OCSP CertID"); }
|
|
2643
2717
|
return intrinsic.chain(ocspCore.evaluateResponse(parsedResponse, cert, issuerCtx, issuerKeyBits,
|
|
2644
|
-
issuerNameCandidates, time, opts.historicalMode === true), function (v) {
|
|
2718
|
+
issuerNameCandidates, time, opts.historicalMode === true, opts.distrustedKeys || null), function (v) {
|
|
2645
2719
|
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);
|
|
2646
2720
|
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);
|
|
2647
2721
|
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);
|
|
@@ -2709,6 +2783,7 @@ function assertAnchorConstraints(ta, checkPurpose) {
|
|
|
2709
2783
|
|
|
2710
2784
|
function resolvePurposeOpts(opts) {
|
|
2711
2785
|
var requiredEku = null;
|
|
2786
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no EKU is required, so a null must be refused rather than drop the restriction
|
|
2712
2787
|
if (opts.requiredEku !== undefined) {
|
|
2713
2788
|
if (!intrinsic.isArray(opts.requiredEku) || opts.requiredEku.length === 0) {
|
|
2714
2789
|
throw E("path/bad-input", "validate: opts.requiredEku must be a non-empty array of key-purpose OID names or dotted OID strings");
|
|
@@ -2722,6 +2797,7 @@ function resolvePurposeOpts(opts) {
|
|
|
2722
2797
|
});
|
|
2723
2798
|
}
|
|
2724
2799
|
var checkPurpose = null;
|
|
2800
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no purpose is checked, so a null must be refused rather than drop the check
|
|
2725
2801
|
if (opts.checkPurpose !== undefined) {
|
|
2726
2802
|
if (typeof opts.checkPurpose !== "string" || opts.checkPurpose.length === 0) {
|
|
2727
2803
|
throw E("path/bad-input", "validate: opts.checkPurpose must be a key-purpose OID name or dotted OID string");
|
|
@@ -3494,6 +3570,7 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3494
3570
|
* @opts maxAiaPerCert Cap on caIssuers URLs tried per certificate (default `C.LIMITS.PATH_AIA_MAX_PER_CERT`).
|
|
3495
3571
|
* @opts aiaTimeout Per-fetch timeout in ms, forwarded to the transport.
|
|
3496
3572
|
* @opts maxResponseBytes Per-fetch response size cap, forwarded to the transport (tightenable downward only).
|
|
3573
|
+
* @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.
|
|
3497
3574
|
* @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.
|
|
3498
3575
|
* @example
|
|
3499
3576
|
* var pair = await pki.key.generate("Ed25519");
|
|
@@ -3518,6 +3595,21 @@ var _BUILD_OPTS = (function () {
|
|
|
3518
3595
|
return o;
|
|
3519
3596
|
})();
|
|
3520
3597
|
|
|
3598
|
+
/** @internal The TLS policy for the AIA host, checked and copied at the build door. An AIA fetch fault
|
|
3599
|
+
* is a silent skip, so a misspelled key or a malformed distrust list that reached the transport
|
|
3600
|
+
* would only ever show as an intermediate that was not fetched. */
|
|
3601
|
+
function _aiaTls(tls) {
|
|
3602
|
+
if (tls == null) return intrinsic.create(null);
|
|
3603
|
+
guard.identifier.assertKnownKeys(tls, httpTransport.TLS_KEYS, E, "path/bad-input", "build: opts.tls has an unknown option ");
|
|
3604
|
+
/** @internal Each setting the transport reads is taken by value, inherited or own, as the transport
|
|
3605
|
+
* reads the bag. */
|
|
3606
|
+
var copy = guard.identifier.pickSettings(tls, intrinsic.keys(httpTransport.TLS_KEYS));
|
|
3607
|
+
if (copy.distrustedKeys != null) {
|
|
3608
|
+
copy.distrustedKeys = guard.distrust.keys(copy.distrustedKeys, E, "path/bad-input", "build: opts.tls.distrustedKeys");
|
|
3609
|
+
}
|
|
3610
|
+
return copy;
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3521
3613
|
async function build(leaf, opts) {
|
|
3522
3614
|
opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "build: opts");
|
|
3523
3615
|
guard.identifier.assertKnownKeys(opts, _BUILD_OPTS, E, "path/bad-input",
|
|
@@ -3560,11 +3652,19 @@ async function build(leaf, opts) {
|
|
|
3560
3652
|
}
|
|
3561
3653
|
});
|
|
3562
3654
|
}
|
|
3655
|
+
/** @internal Typed and copied at this door for the reason the switches are: a build that
|
|
3656
|
+
* assembles no path never reaches `validate`, and every path it does assemble is validated after
|
|
3657
|
+
* an await. Every `validate` call is handed this copy. */
|
|
3658
|
+
var buildDistrusted = guard.distrust.keys(opts.distrustedKeys, E, "path/bad-input", "build: opts.distrustedKeys");
|
|
3659
|
+
if (!doValidateOpt && buildDistrusted !== null && buildDistrusted.length > 0) {
|
|
3660
|
+
throw E("path/bad-input", "build: opts.distrustedKeys refuses keys during validation, and opts.validate is false, " +
|
|
3661
|
+
"which returns the ordered path without validating; drop one of the two");
|
|
3662
|
+
}
|
|
3563
3663
|
var leafCert;
|
|
3564
3664
|
try { leafCert = coerceCert(leaf); }
|
|
3565
3665
|
catch (e) { throw E("path/bad-input", "build: the leaf certificate did not parse", e); }
|
|
3566
3666
|
|
|
3567
|
-
var poolInput = opts.candidates
|
|
3667
|
+
var poolInput = opts.candidates != null ? opts.candidates : opts.intermediates;
|
|
3568
3668
|
if (poolInput === undefined) poolInput = [];
|
|
3569
3669
|
if (!intrinsic.isArray(poolInput)) throw E("path/bad-input", "build: opts.candidates must be an array of certificates");
|
|
3570
3670
|
var poolCeiling = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES;
|
|
@@ -3599,7 +3699,7 @@ async function build(leaf, opts) {
|
|
|
3599
3699
|
var maxConsidered = guard.limits.cap(opts.maxCandidatesConsidered, "build: opts.maxCandidatesConsidered", poolCeiling, { E: E, code: "path/bad-input", min: 1 });
|
|
3600
3700
|
var doValidate = doValidateOpt;
|
|
3601
3701
|
|
|
3602
|
-
var direction = opts.direction
|
|
3702
|
+
var direction = opts.direction != null ? opts.direction : "forward";
|
|
3603
3703
|
if (direction !== "forward" && direction !== "reverse" && direction !== "auto") {
|
|
3604
3704
|
throw E("path/bad-input", "build: opts.direction must be \"forward\", \"reverse\", or \"auto\", got " + guard.text.showValue(opts.direction));
|
|
3605
3705
|
}
|
|
@@ -3608,7 +3708,7 @@ async function build(leaf, opts) {
|
|
|
3608
3708
|
|
|
3609
3709
|
var aiaCtx = null;
|
|
3610
3710
|
if (opts.fetchAia === true) {
|
|
3611
|
-
if (opts.aiaTimeout
|
|
3711
|
+
if (opts.aiaTimeout != null) {
|
|
3612
3712
|
guard.limits.cap(opts.aiaTimeout, "build: opts.aiaTimeout", 1, { E: E, code: "path/bad-input", min: 1, max: httpTransport.MAX_TIMEOUT });
|
|
3613
3713
|
}
|
|
3614
3714
|
var aiaTransport = checkedTransport || httpTransport.https({ E: E, errPrefix: "path" });
|
|
@@ -3620,7 +3720,7 @@ async function build(leaf, opts) {
|
|
|
3620
3720
|
* hostname taken from a certificate would be opened on that promise.
|
|
3621
3721
|
* @guard-via guard.identifier.declaresOwn */
|
|
3622
3722
|
transportGuardsAddresses: guard.identifier.declaresOwn(aiaTransport, "blocksPrivateAddresses"),
|
|
3623
|
-
tls: opts.tls
|
|
3723
|
+
tls: _aiaTls(opts.tls),
|
|
3624
3724
|
timeout: opts.aiaTimeout,
|
|
3625
3725
|
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 }),
|
|
3626
3726
|
maxPerCert: guard.limits.cap(opts.maxAiaPerCert, "build: opts.maxAiaPerCert", constants.LIMITS.PATH_AIA_MAX_PER_CERT, { E: E, code: "path/bad-input", min: 0 }),
|
|
@@ -3640,7 +3740,7 @@ async function build(leaf, opts) {
|
|
|
3640
3740
|
* @guard-via guard.identifier.snapshotPresentOptions */
|
|
3641
3741
|
var FORWARD_BY_VALUE = intrinsic.create(null);
|
|
3642
3742
|
intrinsic.forEach(intrinsic.keys(_VALIDATE_OPTS), function (k) {
|
|
3643
|
-
if (intrinsic.hasOwn(BUILD_ONLY_OPT, k) || intrinsic.hasOwn(forwardedSwitches, k)) return;
|
|
3743
|
+
if (intrinsic.hasOwn(BUILD_ONLY_OPT, k) || intrinsic.hasOwn(forwardedSwitches, k) || k === "distrustedKeys") return;
|
|
3644
3744
|
FORWARD_BY_VALUE[k] = 1;
|
|
3645
3745
|
});
|
|
3646
3746
|
var forwarded = guard.identifier.snapshotPresentOptions(opts, FORWARD_BY_VALUE);
|
|
@@ -3654,6 +3754,7 @@ async function build(leaf, opts) {
|
|
|
3654
3754
|
});
|
|
3655
3755
|
if (buildPermRaw != null) forwarded.initialPermittedSubtrees = buildPermSeeds;
|
|
3656
3756
|
if (buildExclRaw != null) forwarded.initialExcludedSubtrees = buildExclSeeds;
|
|
3757
|
+
if (buildDistrusted !== null) forwarded.distrustedKeys = buildDistrusted;
|
|
3657
3758
|
function validateOpts(anchor) {
|
|
3658
3759
|
var vo = Object.create(null);
|
|
3659
3760
|
intrinsic.forEach(intrinsic.keys(forwarded), function (f) { vo[f] = forwarded[f]; });
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -532,7 +532,7 @@ function _normalizeSpec(spec, opts, form) {
|
|
|
532
532
|
* @opts
|
|
533
533
|
* - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
|
|
534
534
|
* - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations?, password? }`. `password` overrides the shared integrity password for the MAC. `keyLength` is a `pbmac1` field: the classic Appendix B derivation produces a key at the hash's own output length, and naming one under `hmac` is refused.
|
|
535
|
-
* - `integrity` -- `{ mode: 'public-key', signer: { cert, key, digestAlgorithm?, pss? } | signers: [ ... ], sid?, signingTime?, certificates? }` for public-key integrity (a CMS SignedData authSafe over any `pki.cms.sign` signer algorithm, no MacData). Combining it with a truthy `mac` is rejected.
|
|
535
|
+
* - `integrity` -- `{ mode: 'public-key', signer: { cert, key, digestAlgorithm?, pss? } | signers: [ ... ], sid?, signingTime?, certificates? }` for public-key integrity (a CMS SignedData authSafe over any `pki.cms.sign` signer algorithm, no MacData). Combining it with a truthy `mac` is rejected. `certificates` embeds the signer certificates in the authSafe, default true; a value other than `true`, `false`, `null` or an absent option is refused `pkcs12/bad-input`.
|
|
536
536
|
* - `recipientCerts` -- (public-key privacy convenience) `[certDer|PEM, ...]`; the convenience-form cert + key are placed in one recipient-enveloped safe (a plaintext keyBag). For the full form, set per-`safeContents` `recipients: [{ cert }, ...]` -- CERTIFICATE recipients only (RSA-OAEP / ECDH / X25519 / X448 / ML-KEM, dispatched off the cert key); a password or KEK recipient is not public-key privacy and is rejected -- with an optional `contentEncryptionAlgorithm` (`aes-128|192|256-cbc`, default 256; GCM/AEAD rejected). `encrypt` (password) and `recipients` (public-key) on the same safe is rejected; privacy is independent of integrity.
|
|
537
537
|
* - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
|
|
538
538
|
* @example
|
|
@@ -572,6 +572,11 @@ async function _build(spec, opts) {
|
|
|
572
572
|
if (opts.integrity != null) {
|
|
573
573
|
if (typeof opts.integrity !== "object" || Array.isArray(opts.integrity)) throw _err("pkcs12/bad-input", "opts.integrity must be an object { mode, signer|signers, ... }");
|
|
574
574
|
guard.identifier.assertKnownKeys(opts.integrity, _INTEGRITY_OPTS, _err, "pkcs12/bad-input", "opts.integrity has an unknown option ");
|
|
575
|
+
/** @internal Forwarded to `pki.cms.sign` below by comparing it to `false`, which answered for a
|
|
576
|
+
* value of any other type by embedding the signer certificates the caller asked to leave out
|
|
577
|
+
* of the authSafe. */
|
|
578
|
+
guard.identifier.booleanOptionDefaultOn(opts.integrity.certificates, _err, "pkcs12/bad-input",
|
|
579
|
+
"opts.integrity.certificates");
|
|
575
580
|
if (opts.integrity.mode !== "public-key") {
|
|
576
581
|
throw _err("pkcs12/bad-integrity-mode", "opts.integrity.mode must be \"public-key\" (the only mode it selects); omit opts.integrity for password integrity, got " + guard.text.showValue(opts.integrity.mode));
|
|
577
582
|
}
|