@blamejs/pki 0.9.5 → 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/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. Sec. 3's other two arms govern id-ecDH and id-ecMQV, which
3912
- * this registry does not name, so a row keyed on them could never fire; registering them as
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", "rfc5652", "rfc8933",
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
@@ -74,7 +74,12 @@ function makeOcspVerify(deps) {
74
74
  return false;
75
75
  }
76
76
 
77
- async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits, time) {
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 === undefined ? null : 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,
@@ -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 !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
1027
- minRsaModulusBits: params.minRsaModulusBits !== undefined ? params.minRsaModulusBits : constants.LIMITS.RSA_MIN_MODULUS_BITS,
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 && !prefixAuthentic) {
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",
@@ -2181,6 +2236,12 @@ function crlChecker(crls, opts) {
2181
2236
  check: async function (cert, issuer, ctx) {
2182
2237
  var time = ctx.time;
2183
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
+ }
2184
2245
  var certIsCa = null, certScopeFault = null;
2185
2246
  try {
2186
2247
  var bc = decodeExt(cert, OID.basicConstraints);
@@ -2473,6 +2534,7 @@ function ocspChecker(responses) {
2473
2534
  check: async function (cert, issuer, ctx) {
2474
2535
  var time = ctx.time;
2475
2536
  var historical = ctx.historicalMode === true;
2537
+ var distrusted = guard.distrust.keys(ctx.distrustedKeys, E, "path/bad-input", "ocspChecker: ctx.distrustedKeys");
2476
2538
  var issuerNameCandidates = [cert.issuer.bytes];
2477
2539
  function addNameCandidate(nm) {
2478
2540
  if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) guard.list.append(issuerNameCandidates, nm.bytes);
@@ -2488,7 +2550,7 @@ function ocspChecker(responses) {
2488
2550
  var sawUnknownStatus = false;
2489
2551
 
2490
2552
  for (var k = 0; k < parsed.length; k++) {
2491
- 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);
2492
2554
  if (v.revoked && !revokedResult) {
2493
2555
  revokedResult = guard.verdict.of({ status: "revoked", revocationReason: v.revoked.revocationReason, revocationTime: v.revoked.revocationTime, reason: v.revoked.reason });
2494
2556
  }
@@ -2536,6 +2598,11 @@ function ocspChecker(responses) {
2536
2598
  * `time` either way. `time` must be a valid `Date`. A malformed response's parse
2537
2599
  * fault surfaces as the parser's typed `ocsp/*` / `asn1/*` error.
2538
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
+ *
2539
2606
  * `opts.requestNonce` is the nonce bytes the request carried. Given one, the response must echo it
2540
2607
  * (RFC 9654 sec. 2.1, RFC 6960 sec. 4.4.1), compared in constant time, and `nonceMatched` reports
2541
2608
  * the outcome: `true`, `false`, or `null` when no nonce was sent. An unechoed nonce takes a `good`
@@ -2571,7 +2638,7 @@ function ocspChecker(responses) {
2571
2638
  * var v = await pki.path.verifyOcspResponse(der, cert, issuerCert, new Date());
2572
2639
  * v.status; // "good" | "revoked" | "unknown"
2573
2640
  */
2574
- 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 });
2575
2642
 
2576
2643
  async function verifyOcspResponse(response, cert, issuerCert, time, opts) {
2577
2644
  var parsedResponse, subject, issuer, checkedOpts;
@@ -2583,11 +2650,13 @@ async function verifyOcspResponse(response, cert, issuerCert, time, opts) {
2583
2650
  checkedOpts = guard.identifier.optionsObject(opts, E, "path/bad-input", "verifyOcspResponse: opts");
2584
2651
  guard.identifier.assertKnownKeys(checkedOpts, _VERIFY_OCSP_OPTS, E, "path/bad-input",
2585
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");
2586
2654
  parsedResponse = _ocspFromBytes(response);
2587
2655
  subject = coerceCert(cert, "the certificate");
2588
2656
  issuer = coerceCert(issuerCert, "the issuer certificate");
2589
2657
  } catch (e) { return _reject(_Promise, e); }
2590
- var v = await _verifyOcspParsed(parsedResponse, subject, issuer, time, checkedOpts);
2658
+ var v = await _verifyOcspParsed(parsedResponse, subject, issuer, time,
2659
+ { historicalMode: checkedOpts.historicalMode, requestNonce: checkedOpts.requestNonce, distrustedKeys: distrusted });
2591
2660
  var out = guard.verdict.of(v);
2592
2661
  return guard.verdict.of(out, { valid: out.status === "good" });
2593
2662
  }
@@ -2646,7 +2715,7 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
2646
2715
  try { issuerKeyBits = ocspVerify.ocspKeyValue(issuerCert.subjectPublicKeyInfo.bytes); }
2647
2716
  catch (_e) { return unbound("the issuer public key could not be read to recompute the OCSP CertID"); }
2648
2717
  return intrinsic.chain(ocspCore.evaluateResponse(parsedResponse, cert, issuerCtx, issuerKeyBits,
2649
- issuerNameCandidates, time, opts.historicalMode === true), function (v) {
2718
+ issuerNameCandidates, time, opts.historicalMode === true, opts.distrustedKeys || null), function (v) {
2650
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);
2651
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);
2652
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);
@@ -2714,6 +2783,7 @@ function assertAnchorConstraints(ta, checkPurpose) {
2714
2783
 
2715
2784
  function resolvePurposeOpts(opts) {
2716
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
2717
2787
  if (opts.requiredEku !== undefined) {
2718
2788
  if (!intrinsic.isArray(opts.requiredEku) || opts.requiredEku.length === 0) {
2719
2789
  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 +2797,7 @@ function resolvePurposeOpts(opts) {
2727
2797
  });
2728
2798
  }
2729
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
2730
2801
  if (opts.checkPurpose !== undefined) {
2731
2802
  if (typeof opts.checkPurpose !== "string" || opts.checkPurpose.length === 0) {
2732
2803
  throw E("path/bad-input", "validate: opts.checkPurpose must be a key-purpose OID name or dotted OID string");
@@ -3499,6 +3570,7 @@ async function _fetchAiaIssuers(current, aia) {
3499
3570
  * @opts maxAiaPerCert Cap on caIssuers URLs tried per certificate (default `C.LIMITS.PATH_AIA_MAX_PER_CERT`).
3500
3571
  * @opts aiaTimeout Per-fetch timeout in ms, forwarded to the transport.
3501
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.
3502
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.
3503
3575
  * @example
3504
3576
  * var pair = await pki.key.generate("Ed25519");
@@ -3523,6 +3595,21 @@ var _BUILD_OPTS = (function () {
3523
3595
  return o;
3524
3596
  })();
3525
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
+
3526
3613
  async function build(leaf, opts) {
3527
3614
  opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "build: opts");
3528
3615
  guard.identifier.assertKnownKeys(opts, _BUILD_OPTS, E, "path/bad-input",
@@ -3565,11 +3652,19 @@ async function build(leaf, opts) {
3565
3652
  }
3566
3653
  });
3567
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
+ }
3568
3663
  var leafCert;
3569
3664
  try { leafCert = coerceCert(leaf); }
3570
3665
  catch (e) { throw E("path/bad-input", "build: the leaf certificate did not parse", e); }
3571
3666
 
3572
- var poolInput = opts.candidates !== undefined ? opts.candidates : opts.intermediates;
3667
+ var poolInput = opts.candidates != null ? opts.candidates : opts.intermediates;
3573
3668
  if (poolInput === undefined) poolInput = [];
3574
3669
  if (!intrinsic.isArray(poolInput)) throw E("path/bad-input", "build: opts.candidates must be an array of certificates");
3575
3670
  var poolCeiling = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES;
@@ -3604,7 +3699,7 @@ async function build(leaf, opts) {
3604
3699
  var maxConsidered = guard.limits.cap(opts.maxCandidatesConsidered, "build: opts.maxCandidatesConsidered", poolCeiling, { E: E, code: "path/bad-input", min: 1 });
3605
3700
  var doValidate = doValidateOpt;
3606
3701
 
3607
- var direction = opts.direction !== undefined ? opts.direction : "forward";
3702
+ var direction = opts.direction != null ? opts.direction : "forward";
3608
3703
  if (direction !== "forward" && direction !== "reverse" && direction !== "auto") {
3609
3704
  throw E("path/bad-input", "build: opts.direction must be \"forward\", \"reverse\", or \"auto\", got " + guard.text.showValue(opts.direction));
3610
3705
  }
@@ -3613,7 +3708,7 @@ async function build(leaf, opts) {
3613
3708
 
3614
3709
  var aiaCtx = null;
3615
3710
  if (opts.fetchAia === true) {
3616
- if (opts.aiaTimeout !== undefined) {
3711
+ if (opts.aiaTimeout != null) {
3617
3712
  guard.limits.cap(opts.aiaTimeout, "build: opts.aiaTimeout", 1, { E: E, code: "path/bad-input", min: 1, max: httpTransport.MAX_TIMEOUT });
3618
3713
  }
3619
3714
  var aiaTransport = checkedTransport || httpTransport.https({ E: E, errPrefix: "path" });
@@ -3625,7 +3720,7 @@ async function build(leaf, opts) {
3625
3720
  * hostname taken from a certificate would be opened on that promise.
3626
3721
  * @guard-via guard.identifier.declaresOwn */
3627
3722
  transportGuardsAddresses: guard.identifier.declaresOwn(aiaTransport, "blocksPrivateAddresses"),
3628
- tls: opts.tls != null ? opts.tls : intrinsic.create(null),
3723
+ tls: _aiaTls(opts.tls),
3629
3724
  timeout: opts.aiaTimeout,
3630
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 }),
3631
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 }),
@@ -3645,7 +3740,7 @@ async function build(leaf, opts) {
3645
3740
  * @guard-via guard.identifier.snapshotPresentOptions */
3646
3741
  var FORWARD_BY_VALUE = intrinsic.create(null);
3647
3742
  intrinsic.forEach(intrinsic.keys(_VALIDATE_OPTS), function (k) {
3648
- 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;
3649
3744
  FORWARD_BY_VALUE[k] = 1;
3650
3745
  });
3651
3746
  var forwarded = guard.identifier.snapshotPresentOptions(opts, FORWARD_BY_VALUE);
@@ -3659,6 +3754,7 @@ async function build(leaf, opts) {
3659
3754
  });
3660
3755
  if (buildPermRaw != null) forwarded.initialPermittedSubtrees = buildPermSeeds;
3661
3756
  if (buildExclRaw != null) forwarded.initialExcludedSubtrees = buildExclSeeds;
3757
+ if (buildDistrusted !== null) forwarded.distrustedKeys = buildDistrusted;
3662
3758
  function validateOpts(anchor) {
3663
3759
  var vo = Object.create(null);
3664
3760
  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, { trustAnchors: o.trustAnchors, time: at })),
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");
@@ -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 === undefined) {
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 === undefined ? new intrinsic.Date() : 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);