@blamejs/pki 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +7 -6
  3. package/bin/pki.js +110 -34
  4. package/lib/acme.js +91 -46
  5. package/lib/asn1-der.js +6 -5
  6. package/lib/attrcert-sign.js +4 -3
  7. package/lib/cmp-build.js +16 -6
  8. package/lib/cmp-session.js +26 -3
  9. package/lib/cmp-verify.js +24 -2
  10. package/lib/cms-compress.js +3 -3
  11. package/lib/cms-digest.js +3 -3
  12. package/lib/cms-verify.js +18 -8
  13. package/lib/compromised-keys.js +114 -0
  14. package/lib/constants.js +1 -0
  15. package/lib/crl-sign.js +3 -3
  16. package/lib/crmf-sign.js +2 -0
  17. package/lib/csr-sign.js +5 -1
  18. package/lib/ct.js +16 -3
  19. package/lib/est.js +44 -29
  20. package/lib/framework-error.js +20 -7
  21. package/lib/guard-all.js +2 -0
  22. package/lib/guard-distrust.js +344 -0
  23. package/lib/guard-json.js +1 -1
  24. package/lib/guard-verdict.js +16 -3
  25. package/lib/http-transport.js +412 -41
  26. package/lib/jose.js +1 -1
  27. package/lib/lint.js +49 -7
  28. package/lib/ocsp-verify.js +9 -3
  29. package/lib/ocsp.js +8 -4
  30. package/lib/oid.js +4 -0
  31. package/lib/path-validate.js +131 -16
  32. package/lib/possession.js +6 -2
  33. package/lib/related-cert.js +2 -2
  34. package/lib/revocation-fetch.js +39 -9
  35. package/lib/scep.js +28 -13
  36. package/lib/schema-pkix.js +3 -3
  37. package/lib/sigstore.js +11 -6
  38. package/lib/smime.js +18 -13
  39. package/lib/tls-cert-compress.js +6 -5
  40. package/lib/trust.js +159 -20
  41. package/lib/trustanchor-build.js +16 -1
  42. package/lib/tsp-sign.js +7 -1
  43. package/lib/webauthn-mds.js +30 -8
  44. package/lib/webauthn.js +46 -14
  45. package/lib/x509-sign.js +16 -5
  46. package/package.json +1 -1
  47. package/sbom.cdx.json +6 -6
@@ -398,7 +398,8 @@ async function _tryOcsp(state, cert, issuer, ctx, candidate) {
398
398
  hit = got.body;
399
399
  }
400
400
  var v = await state.verifyOcspResponse(hit, cert, issuerCert, ctx.time,
401
- { historicalMode: ctx.historicalMode === true, requestNonce: state.nonce ? _requestNonce(parsedReq) : undefined });
401
+ { historicalMode: ctx.historicalMode === true, requestNonce: state.nonce ? _requestNonce(parsedReq) : undefined,
402
+ distrustedKeys: state.distrusted });
402
403
  /** @internal `valid` on that verdict means the certificate is good, not that the response was
403
404
  * acceptable, so reading it as acceptance would turn a `revoked` answer into an undetermined
404
405
  * one that `opts.softFail` could then waive. Acceptance is the three conditions the gate
@@ -695,6 +696,21 @@ function _deltaUrlsFor(parsedCrl, state) {
695
696
  * var checker = pki.path.fetchingChecker({ transport: pki.transport.https({}) });
696
697
  * console.log(typeof checker.check);
697
698
  */
699
+ /** @internal The TLS policy every fetch is sent with, copied once at the door, with its distrust list
700
+ * typed there. A fetch fault becomes an undetermined verdict, so a malformed list that reached the
701
+ * transport would be reported as a responder that did not answer. */
702
+ function _tlsSnapshot(tls) {
703
+ if (tls == null) return tls;
704
+ guard.identifier.assertKnownKeys(tls, httpTransport.TLS_KEYS, E, "path/bad-input", "fetchingChecker: opts.tls has an unknown option ");
705
+ /** @internal Each setting the transport reads is taken by value, whether the bag holds it or
706
+ * inherits it, which is how the transport itself reads the bag. */
707
+ var copy = guard.identifier.pickSettings(tls, intrinsic.keys(httpTransport.TLS_KEYS));
708
+ if (copy.distrustedKeys != null) {
709
+ copy.distrustedKeys = guard.distrust.keys(copy.distrustedKeys, E, "path/bad-input", "fetchingChecker: opts.tls.distrustedKeys");
710
+ }
711
+ return copy;
712
+ }
713
+
698
714
  function fetchingChecker(opts) {
699
715
  opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "fetchingChecker: opts");
700
716
  guard.identifier.assertKnownKeys(opts, CHECKER_OPTS, E, "path/bad-input",
@@ -703,6 +719,7 @@ function fetchingChecker(opts) {
703
719
  * accessor twice, so the transport that passed the door is the transport that gets called. */
704
720
  var transport = guard.identifier.assertCallable(opts.transport, E, "path/bad-input",
705
721
  "fetchingChecker: opts.transport", TRANSPORT_SHAPE);
722
+ // allow:caller-option-presence-tested-against-undefined -- absence selects the permissive default, so a null must not reach it
706
723
  var allowPlaintextHttp = opts.allowPlaintextHttp === undefined ? true : opts.allowPlaintextHttp === true;
707
724
  var cfg = {
708
725
  transport: transport,
@@ -717,7 +734,7 @@ function fetchingChecker(opts) {
717
734
  useDeltas: guard.identifier.booleanOptionDefaultOn(opts.useDeltas, E, "path/bad-input",
718
735
  "fetchingChecker: opts.useDeltas"),
719
736
  nonce: _booleanOption(opts.nonce, "fetchingChecker: opts.nonce"),
720
- tls: opts.tls,
737
+ tls: _tlsSnapshot(opts.tls),
721
738
  maxFetches: guard.limits.cap(opts.maxFetches, "fetchingChecker: opts.maxFetches", LIMITS.REVOCATION_MAX_FETCHES,
722
739
  { E: E, code: "path/bad-input", min: 1, max: LIMITS.REVOCATION_MAX_FETCHES }),
723
740
  maxPerCert: guard.limits.cap(opts.maxPerCert, "fetchingChecker: opts.maxPerCert", LIMITS.REVOCATION_MAX_PER_CERT,
@@ -757,10 +774,18 @@ function fetchingChecker(opts) {
757
774
 
758
775
  return {
759
776
  check: async function (cert, issuer, ctx) {
777
+ /** @internal Typed before any location is decoded or fetched: a malformed list is the
778
+ * caller's fault and is thrown, where a fetch fault below becomes an undetermined verdict. */
779
+ var distrusted = guard.distrust.keys(ctx == null ? undefined : ctx.distrustedKeys, E, "path/bad-input",
780
+ "fetchingChecker: ctx.distrustedKeys");
781
+ var issuerRefusal = guard.distrust.refusal(distrusted, issuer.workingPublicKey, "distrustedKeys");
782
+ if (issuerRefusal !== null) {
783
+ return _unknown("the issuer's key " + issuerRefusal + ", so nothing it signs is an answer", [], []);
784
+ }
760
785
  var notes = [], skips = [];
761
786
  var budget = budgetFor(ctx == null ? null : ctx.run);
762
787
  var state = intrinsic.assign(intrinsic.create(null), cfg, {
763
- notes: notes, cache: cache, crlCache: crlCache,
788
+ notes: notes, cache: cache, crlCache: crlCache, distrusted: distrusted,
764
789
  deadline: budget.deadline,
765
790
  fetches: budget.fetches,
766
791
  urls: new intrinsic.Set(),
@@ -787,7 +812,9 @@ function fetchingChecker(opts) {
787
812
  * number of retries. A responder that answers `revoked` or `good` ends it; anything else
788
813
  * falls through, because sec. 5 of RFC 6960 makes the CRL the fallback and never an
789
814
  * assumption of good standing. */
790
- var lastReason = null;
815
+ /** @internal Each source keeps its own last failure, so an undetermined verdict names the
816
+ * responder's fault and the CRL's. They usually have different causes. */
817
+ var ocspReason = null, crlReason = null;
791
818
  for (var a = 0; a <= cfg.retries && ocspCands.length > 0; a++) {
792
819
  for (var o = 0; o < ocspCands.length; o++) {
793
820
  if (!_takeUrl(state, ocspCands[o].url)) {
@@ -797,18 +824,21 @@ function fetchingChecker(opts) {
797
824
  try {
798
825
  var got = await _tryOcsp(state, cert, issuer, ctx, ocspCands[o]);
799
826
  if (got.status === "good" || got.status === "revoked") return _verdict(got, notes, skips);
800
- lastReason = got.reason + " reported " + got.status;
801
- } catch (e) { lastReason = _say(e); }
827
+ ocspReason = got.reason + " reported " + got.status;
828
+ } catch (e) { ocspReason = _say(e); }
802
829
  }
803
830
  }
804
831
  if (crlCands.length > 0) {
805
832
  try {
806
833
  var viaCrl = await _tryCrl(state, cert, issuer, ctx, crlCands, certDeltas);
807
834
  if (viaCrl.status === "good" || viaCrl.status === "revoked") return _verdict(viaCrl, notes, skips);
808
- lastReason = viaCrl.reason;
809
- } catch (e2) { lastReason = _say(e2); }
835
+ crlReason = viaCrl.reason;
836
+ } catch (e2) { crlReason = _say(e2); }
810
837
  }
811
- return _unknown(lastReason || "no revocation source answered", notes, skips);
838
+ var why = [];
839
+ if (ocspReason !== null) _append(why, "OCSP: " + ocspReason);
840
+ if (crlReason !== null) _append(why, "CRL: " + crlReason);
841
+ return _unknown(why.length ? intrinsic.join(why, "; ") : "no revocation source answered", notes, skips);
812
842
  },
813
843
  };
814
844
  }
package/lib/scep.js CHANGED
@@ -689,10 +689,25 @@ function _responseLen(body) {
689
689
  if (Buffer.isBuffer(body)) return guard.bytes.lengthOf(body);
690
690
  return guard.bytes.lengthOf(guard.bytes.source(body, ScepError, "scep/bad-response", "the response body"));
691
691
  }
692
- /** @internal The settings this client forwards to its transport. */
693
- var TLS_SETTINGS = ["anchors", "useSystemStore", "cert", "key", "minVersion", "servername", "checkServerIdentity"];
694
- function _tlsForRequest(opts) {
695
- return guard.identifier.pickSettings(opts.tls, TLS_SETTINGS);
692
+ /** @internal The settings this client forwards to its transport. The distrust list is typed here, and
693
+ * the frozen copy is the one every hop carries. A setting not on the list is refused. */
694
+ var TLS_SETTINGS = ["anchors", "useSystemStore", "cert", "key", "minVersion", "servername", "checkServerIdentity", "distrustedKeys"];
695
+ var TLS_KNOWN = intrinsic.freeze(guard.identifier.pickSettings(null, TLS_SETTINGS));
696
+ function _distrustOf(value) {
697
+ return guard.distrust.keys(value, _err, "scep/bad-input", "opts.tls.distrustedKeys");
698
+ }
699
+ function _tlsForRequest(tls) {
700
+ guard.identifier.assertKnownKeys(tls, TLS_KNOWN, _err, "scep/bad-input", "unknown opts.tls setting ");
701
+ var t = guard.identifier.pickSettings(tls, TLS_SETTINGS);
702
+ t.distrustedKeys = _distrustOf(t.distrustedKeys);
703
+ return t;
704
+ }
705
+ /** @internal A verb that reaches the transport only after an await copies the caller's tls at its door,
706
+ * with the distrust list typed there, so a later change to the caller's array does not reach the request.
707
+ * Each setting is read the way `_tlsForRequest` reads it, a prototype included. */
708
+ function _snapshotTls(tls) {
709
+ if (tls == null) return tls;
710
+ return _tlsForRequest(tls);
696
711
  }
697
712
  function _redirectTarget(current, location, method, allowCrossOrigin) {
698
713
  if (location == null || _trim(String(location)) === "") throw _err("scep/bad-response", "a redirect response carried no Location header");
@@ -736,7 +751,7 @@ function _pkiRequest(method, opUrl, reqMsg, clientOpts) {
736
751
  function _drive(method, url, body, headers, opts, transport, budgets) {
737
752
  var redirects = 0, curMethod = method, curBody = body, initialOrigin = _urlOrigin(url);
738
753
  var proxySnap = _snapshotProxy(opts.proxy);
739
- var tlsBase = _tlsForRequest(opts);
754
+ var tlsBase = _tlsForRequest(opts.tls);
740
755
  /** @internal Read as the boolean it documents, on the one path every network verb here takes and
741
756
  * before the first request. Taken by truthiness, `allowCrossOriginRedirect: "false"` ALLOWED the
742
757
  * cross-origin redirect on an unsafe method that the check below exists to refuse. */
@@ -868,7 +883,7 @@ function _assertStrongProfile(caps, profile) {
868
883
  * - `transport` -- an injected transport (defaults to `pki.transport.https`); the request is a GET with no body.
869
884
  * - `expectSCEPStandard` -- refuse (`scep/weak-profile`) if the response does not advertise `SCEPStandard`. Takes `true`, `false`, `null` or an absent option; any other value is refused `scep/bad-input`.
870
885
  * - `requireStrongProfile` -- refuse unless AES + SHA-256 + POSTPKIOperation are all advertised. Read the same way.
871
- * - `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
886
+ * - `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport). `tls.distrustedKeys` lists SHA-256 SubjectPublicKeyInfo digests (see pki.trust.spkiDigest); a server whose chain carries a listed key is refused with `scep/server-key-distrusted` on every hop, and a malformed list is `scep/bad-input`. A `tls` setting other than `anchors`, `useSystemStore`, `cert`, `key`, `minVersion`, `servername`, `checkServerIdentity` and `distrustedKeys` is refused `scep/bad-input`.
872
887
  * @example
873
888
  * // requires: scepTransport -- an injected transport (omit to use pki.transport.https)
874
889
  * var caps = await pki.scep.getCACaps("http://ca.example/scep", { transport: scepTransport });
@@ -952,7 +967,7 @@ function _normalizeFingerprint(fp, algName) {
952
967
  * @opts
953
968
  * - `expectedFingerprint` -- a hex string or Buffer; a returned certificate must hash to it, or the response is refused.
954
969
  * - `fingerprintAlgorithm` -- the hash for the fingerprint (default "sha256").
955
- * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
970
+ * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport). `tls.distrustedKeys` lists SHA-256 SubjectPublicKeyInfo digests (see pki.trust.spkiDigest); a server whose chain carries a listed key is refused with `scep/server-key-distrusted` on every hop, and a malformed list is `scep/bad-input`. A `tls` setting other than `anchors`, `useSystemStore`, `cert`, `key`, `minVersion`, `servername`, `checkServerIdentity` and `distrustedKeys` is refused `scep/bad-input`.
956
971
  * @example
957
972
  * // requires: scepCaCertTransport -- an injected transport returning a single CA certificate
958
973
  * var ca = await pki.scep.getCACert("http://ca.example/scep", { transport: scepCaCertTransport });
@@ -1008,7 +1023,7 @@ async function getCACert(baseUrl, opts) {
1008
1023
  *
1009
1024
  * @opts
1010
1025
  * - `caCertificate` -- the current SCEP CA certificate DER; the response is authenticated against its key (required).
1011
- * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
1026
+ * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport). `tls.distrustedKeys` lists SHA-256 SubjectPublicKeyInfo digests (see pki.trust.spkiDigest); a server whose chain carries a listed key is refused with `scep/server-key-distrusted` on every hop, and a malformed list is `scep/bad-input`. A `tls` setting other than `anchors`, `useSystemStore`, `cert`, `key`, `minVersion`, `servername`, `checkServerIdentity` and `distrustedKeys` is refused `scep/bad-input`.
1012
1027
  * @example
1013
1028
  * // requires: caCertDer -- the current CA certificate (from getCACert), scepTransport
1014
1029
  * var next = await pki.scep.getNextCACert("http://ca.example/scep", { caCertificate: caCertDer, transport: scepTransport });
@@ -1114,7 +1129,7 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
1114
1129
  var recipientKey = _captureRecipientKey(opts.recipientKey);
1115
1130
  var signer = { cert: guard.bytes.snapshotSource(opts.signer.cert, ScepError, "scep/bad-input", "signer.cert"), key: opts.signer.key };
1116
1131
  var clientOpts = {
1117
- tls: opts.tls != null ? guard.identifier.ownOptions(opts.tls) : opts.tls,
1132
+ tls: _snapshotTls(opts.tls),
1118
1133
  maxRedirects: opts.maxRedirects,
1119
1134
  allowCrossOriginRedirect: opts.allowCrossOriginRedirect,
1120
1135
  timeout: opts.timeout,
@@ -1187,7 +1202,7 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
1187
1202
  * - `sleep` -- an injectable sleeper `(ms) -> Promise` for the Retry-After wait (default a real timer).
1188
1203
  * - `onRetryAfter` -- an optional observer `(delaySeconds) -> void` called before each poll sleep.
1189
1204
  * - `httpMethod` -- `"POST"` (default) or `"GET"`. RFC 8894 sec. 4.1 sends the PKIOperation message over POST; `"GET"` carries it base64-encoded in the URL `message` query for a legacy CA that lacks POST.
1190
- * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
1205
+ * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport). `tls.distrustedKeys` lists SHA-256 SubjectPublicKeyInfo digests (see pki.trust.spkiDigest); a server whose chain carries a listed key is refused with `scep/server-key-distrusted` on every hop, and a malformed list is `scep/bad-input`. A `tls` setting other than `anchors`, `useSystemStore`, `cert`, `key`, `minVersion`, `servername`, `checkServerIdentity` and `distrustedKeys` is refused `scep/bad-input`.
1191
1206
  * @example
1192
1207
  * // requires: scepCsr, caCertDer, clientCertDer, clientKeyPkcs8, scepTransport
1193
1208
  * var out = await pki.scep.enroll("http://ca.example/scep", { csr: scepCsr, caCert: caCertDer,
@@ -1265,7 +1280,7 @@ async function _queryOperation(messageType, baseUrl, opts, verbName) {
1265
1280
  var recipientKey = _captureRecipientKey(opts.recipientKey);
1266
1281
  var signer = { cert: guard.bytes.snapshotSource(opts.signer.cert, ScepError, "scep/bad-input", "signer.cert"), key: opts.signer.key };
1267
1282
  var want = _resolveQueryTarget(opts);
1268
- var clientOpts = { tls: opts.tls != null ? guard.identifier.ownOptions(opts.tls) : opts.tls, maxRedirects: opts.maxRedirects, allowCrossOriginRedirect: opts.allowCrossOriginRedirect, timeout: opts.timeout, maxResponseBytes: opts.maxResponseBytes, proxy: _snapshotProxy(opts.proxy) };
1283
+ var clientOpts = { tls: _snapshotTls(opts.tls), maxRedirects: opts.maxRedirects, allowCrossOriginRedirect: opts.allowCrossOriginRedirect, timeout: opts.timeout, maxResponseBytes: opts.maxResponseBytes, proxy: _snapshotProxy(opts.proxy) };
1269
1284
  if (checkedTransport !== undefined) clientOpts.transport = checkedTransport;
1270
1285
  var senderNonce = guard.crypto.randomBytes(16);
1271
1286
  var transactionId = opts.transactionId != null ? opts.transactionId : _defaultTransactionId();
@@ -1305,7 +1320,7 @@ async function _queryOperation(messageType, baseUrl, opts, verbName) {
1305
1320
  * - `responderCert` -- the certificate to authenticate the CA response signature against (defaults to `caCert`).
1306
1321
  * - `transactionId` -- a caller-unique PrintableString (defaults to 16 random bytes, hex-encoded).
1307
1322
  * - `httpMethod` -- `"POST"` (default) or `"GET"`. RFC 8894 sec. 4.1 sends the PKIOperation message over POST; `"GET"` carries it base64-encoded in the URL `message` query for a legacy CA that lacks POST.
1308
- * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
1323
+ * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport). `tls.distrustedKeys` lists SHA-256 SubjectPublicKeyInfo digests (see pki.trust.spkiDigest); a server whose chain carries a listed key is refused with `scep/server-key-distrusted` on every hop, and a malformed list is `scep/bad-input`. A `tls` setting other than `anchors`, `useSystemStore`, `cert`, `key`, `minVersion`, `servername`, `checkServerIdentity` and `distrustedKeys` is refused `scep/bad-input`.
1309
1324
  * @example
1310
1325
  * // requires: clientCertDer, caCertDer, clientKeyPkcs8, scepTransport
1311
1326
  * var out = await pki.scep.getCert("http://ca.example/scep", { certificate: clientCertDer, caCert: caCertDer,
@@ -1354,7 +1369,7 @@ async function getCert(baseUrl, opts) {
1354
1369
  * - `responderCert` -- the certificate to authenticate the CA response against (defaults to `caCert`).
1355
1370
  * - `transactionId` -- a caller-unique PrintableString (defaults to 16 random bytes, hex-encoded).
1356
1371
  * - `httpMethod` -- `"POST"` (default) or `"GET"`. RFC 8894 sec. 4.1 sends the PKIOperation message over POST; `"GET"` carries it base64-encoded in the URL `message` query for a legacy CA that lacks POST.
1357
- * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
1372
+ * - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport). `tls.distrustedKeys` lists SHA-256 SubjectPublicKeyInfo digests (see pki.trust.spkiDigest); a server whose chain carries a listed key is refused with `scep/server-key-distrusted` on every hop, and a malformed list is `scep/bad-input`. A `tls` setting other than `anchors`, `useSystemStore`, `cert`, `key`, `minVersion`, `servername`, `checkServerIdentity` and `distrustedKeys` is refused `scep/bad-input`.
1358
1373
  * @example
1359
1374
  * // requires: clientCertDer, caCertDer, clientKeyPkcs8, scepTransport
1360
1375
  * var out = await pki.scep.getCrl("http://ca.example/scep", { certificate: clientCertDer, caCert: caCertDer,
@@ -1557,7 +1557,7 @@ function _extensionRow(ext, opts, index, state, decoded, code, profile) {
1557
1557
  row.value = ext.value;
1558
1558
  row.scope = opts.scope;
1559
1559
  row.index = index;
1560
- row.containerIndex = opts.containerIndex === undefined ? null : opts.containerIndex;
1560
+ row.containerIndex = opts.containerIndex == null ? null : opts.containerIndex;
1561
1561
  row.state = state;
1562
1562
  row.decoded = decoded;
1563
1563
  row.code = code;
@@ -1754,7 +1754,7 @@ function extensionDoor(spec) {
1754
1754
  }
1755
1755
 
1756
1756
  function decodeExtension(ext, callerOpts) {
1757
- var read = readOpts(callerOpts, "decodeExtension", spec.defaultScope === undefined);
1757
+ var read = readOpts(callerOpts, "decodeExtension", spec.defaultScope == null);
1758
1758
  var key = read.scope === undefined ? spec.defaultScope : read.scope;
1759
1759
  if (typeof key !== "string" || !_hasOwn(spec.scopes, key)) {
1760
1760
  throw E(badInput, "pki.schema." + spec.key + ".decodeExtension needs opts.scope naming which " +
@@ -2617,7 +2617,7 @@ function spki(ns, opts) {
2617
2617
 
2618
2618
  function attribute(ns, opts) {
2619
2619
  opts = opts || _create(null);
2620
- var minValues = opts.minValues === undefined ? 1 : opts.minValues;
2620
+ var minValues = opts.minValues == null ? 1 : opts.minValues;
2621
2621
  return schema.seq([
2622
2622
  schema.field("type", schema.oidLeaf()),
2623
2623
  schema.field("values", schema.setOf(schema.any(), { assert: "set", min: minValues, max: constants.LIMITS.ATTRIBUTE_MAX_VALUES,
package/lib/sigstore.js CHANGED
@@ -1244,7 +1244,7 @@ async function _verifyRekorEntry(te, rekorKeys, leafDer, contentType, arm, paylo
1244
1244
  * be checked. A token that does not verify is passed over rather than refused: it is a statement from
1245
1245
  * an authority this caller did not pin, which says nothing either way. With no token verifying, the
1246
1246
  * caller is left with no attested instant and the verdict says so. */
1247
- async function _timestampInstants(vm, sigBytes, tsaRoots) {
1247
+ async function _timestampInstants(vm, sigBytes, tsaRoots, distrusted) {
1248
1248
  if (!tsaRoots) return null;
1249
1249
  var anchors = _normTsaAnchors(tsaRoots);
1250
1250
  if (!anchors.length) {
@@ -1271,7 +1271,7 @@ async function _timestampInstants(vm, sigBytes, tsaRoots) {
1271
1271
  ? _b64(t.signedTimestamp, "rfc3161Timestamps[" + i + "].signedTimestamp") : null;
1272
1272
  if (der === null) continue;
1273
1273
  var res;
1274
- try { res = await _tspVerify(der, sigBytes, { trustAnchors: anchors }); }
1274
+ try { res = await _tspVerify(der, sigBytes, { trustAnchors: anchors, distrustedKeys: distrusted }); }
1275
1275
  catch (_e) { continue; } // allow:swallow-unverified a token this caller cannot check says nothing; the next is tried and a bundle with none verifying is refused for want of an instant
1276
1276
  if (!res || res.valid !== true || res.trusted !== true) continue;
1277
1277
  var ms = guard.time.instantOf(res.genTime);
@@ -1402,7 +1402,7 @@ function _ctLogFor(logs, logIdBytes, timeMs) {
1402
1402
  * here read the caller's array after a yield, so a caller could add an anchor while verification was
1403
1403
  * pending and decide which CA is trusted: an empty list that grows during the timestamp check would have
1404
1404
  * passed a chain the call was made without any anchor for. */
1405
- async function _verifyChain(leaf, chainDers, roots, timeMs) {
1405
+ async function _verifyChain(leaf, chainDers, roots, timeMs, distrusted) {
1406
1406
  var bundleLinks = _map(_arraySlice(chainDers, 1), function (d, i) { return _parseCert(d, "bundle intermediate [" + i + "]"); });
1407
1407
  var inWindow = _filter(roots, function (r) { return _inWindow(timeMs, r.validFor); });
1408
1408
  if (!inWindow.length) {
@@ -1449,6 +1449,7 @@ async function _verifyChain(leaf, chainDers, roots, timeMs) {
1449
1449
  time: new _Date(timeMs),
1450
1450
  historicalMode: true,
1451
1451
  requiredEku: ["codeSigning"],
1452
+ distrustedKeys: distrusted,
1452
1453
  });
1453
1454
  } catch (e) {
1454
1455
  if (e && e.code === "path/no-path") {
@@ -1557,7 +1558,7 @@ function _fulcioExtValue(ext, leafArc) {
1557
1558
 
1558
1559
  var IDENTITY_FIELDS = ["san", "issuer", "sourceRepositoryURI"];
1559
1560
  var IDENTITY_KEYS = _assign(_create(null), { san: 1, issuer: 1, sourceRepositoryURI: 1 });
1560
- var _VERIFY_BUNDLE_OPTS = _assign(_create(null), { fulcioRoots: 1, rekorKeys: 1, ctLogs: 1, identity: 1, predicateType: 1, time: 1, artifact: 1, tsaRoots: 1 });
1561
+ var _VERIFY_BUNDLE_OPTS = _assign(_create(null), { fulcioRoots: 1, rekorKeys: 1, ctLogs: 1, identity: 1, predicateType: 1, time: 1, artifact: 1, tsaRoots: 1, distrustedKeys: 1 });
1561
1562
 
1562
1563
  /** @internal The bytes a message signature covers, taken once. There is no shape here in which a
1563
1564
  * caller hands over a precomputed digest: the arm's own field comment forbids using one as a
@@ -1702,6 +1703,7 @@ function _statement(payload, payloadType, expectedPredicate) {
1702
1703
  * predicateType: string, // optional: require this in-toto predicateType (e.g. the SLSA URI); dsse_envelope bundles only
1703
1704
  * artifact: BufferSource, // the bytes a message_signature covers; required for that arm, refused for a dsse_envelope
1704
1705
  * tsaRoots: Array, // optional timestamp-authority anchors: a DER Buffer or { der } each; required to date a Rekor v2 bundle, which carries no signed entry timestamp
1706
+ * distrustedKeys: Array, // optional SHA-256 digests of SubjectPublicKeyInfo DER (64 hex characters or 32 bytes); a listed key fails the Fulcio chain, or the timestamp authority's chain so its token dates nothing
1705
1707
  * time: Date, // optional check-date override (default: the Rekor integratedTime, or an RFC 3161 token's genTime on a v2 entry)
1706
1708
  *
1707
1709
  * @example
@@ -1736,6 +1738,9 @@ async function verifyBundle(bundle, opts) {
1736
1738
  * verification is pending would decide a check the call was made without it. */
1737
1739
  var fulcioRoots = _normRoots(o.fulcioRoots || []);
1738
1740
  var ctLogs = o.ctLogs === undefined || o.ctLogs === null ? o.ctLogs : _normCtLogs(o.ctLogs);
1741
+ /** @internal Typed here, before the timestamp leg, because that leg passes over a token whose
1742
+ * verification throws, and a malformed list must be reported as the caller's input fault. */
1743
+ var distrusted = guard.distrust.keys(o.distrustedKeys, _err, "sigstore/bad-input", "opts.distrustedKeys");
1739
1744
 
1740
1745
  /** @internal Each option belongs to one arm, and one supplied for the other is refused rather than
1741
1746
  * passed over. An ignored option reads as a check that ran: opts.artifact on a DSSE bundle names
@@ -1796,7 +1801,7 @@ async function verifyBundle(bundle, opts) {
1796
1801
  * SIGNATURE bytes, which is what dates the ephemeral certificate that made them; a token over the
1797
1802
  * payload would date the thing signed rather than the signing. */
1798
1803
  var tsaMsList = null;
1799
- if (!haveCallerTime) tsaMsList = await _timestampInstants(vm, derSig, o.tsaRoots);
1804
+ if (!haveCallerTime) tsaMsList = await _timestampInstants(vm, derSig, o.tsaRoots, distrusted);
1800
1805
  var chainDers = _chainDers(vm);
1801
1806
  /** @internal A caller-pinned instant is the same for every candidate, so the chain it decides is
1802
1807
  * computed on the first candidate and reused rather than rebuilt per entry. Without one the instant
@@ -1873,7 +1878,7 @@ async function verifyBundle(bundle, opts) {
1873
1878
  "timestamp authority, or supply opts.time");
1874
1879
  }
1875
1880
  var candidateChain = pinnedChain !== null ? pinnedChain
1876
- : await _verifyChain(leaf, chainDers, fulcioRoots, candidateTime);
1881
+ : await _verifyChain(leaf, chainDers, fulcioRoots, candidateTime, distrusted);
1877
1882
  if (haveCallerTime) pinnedChain = candidateChain;
1878
1883
  var candidateScts = await _verifyEmbeddedScts(leaf, leafDer, candidateChain.issuer, ctLogs, candidateTime, candidateBound);
1879
1884
  rekor = candidate;
package/lib/smime.js CHANGED
@@ -63,7 +63,7 @@ var SIGN_OPTS = Object.assign(Object.create(null), {
63
63
  });
64
64
  var VERIFY_OPTS = Object.assign(Object.create(null), {
65
65
  certs: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1, strictMicalg: 1,
66
- legacyHeaderProtection: 1, expectedSender: 1, allowWeakDigests: 1,
66
+ legacyHeaderProtection: 1, expectedSender: 1, allowWeakDigests: 1, distrustedKeys: 1,
67
67
  });
68
68
  var ENCRYPT_OPTS = Object.assign(Object.create(null), {
69
69
  entity: 1, contentType: 1, protectHeaders: 1, headers: 1, hcp: 1,
@@ -591,9 +591,9 @@ function _hpSurface(content, outerEnt, expectedMode, authenticated, legacyEnable
591
591
 
592
592
  function _cmsSignOpts(opts, detached) {
593
593
  var o = { detached: detached };
594
- if (opts.signingTime !== undefined) o.signingTime = opts.signingTime;
594
+ if (opts.signingTime != null) o.signingTime = opts.signingTime;
595
595
  if (opts.sid) o.sid = opts.sid;
596
- if (opts.signedAttributes !== undefined) o.signedAttributes = opts.signedAttributes;
596
+ if (opts.signedAttributes != null) o.signedAttributes = opts.signedAttributes;
597
597
  if (opts.additionalSignedAttributes) o.additionalSignedAttributes = opts.additionalSignedAttributes;
598
598
  return o;
599
599
  }
@@ -781,6 +781,9 @@ function _capped(msg) {
781
781
  * @opts time the instant the signer's chain is judged at (default now). Only read with `trustAnchors`.
782
782
  * @opts requiredEku key purposes the SIGNER certificate must carry. Defaults to `["emailProtection"]`.
783
783
  * @opts checkPurpose the purpose the ANCHOR's own trust metadata must permit. Defaults to `"emailProtection"`.
784
+ * @opts distrustedKeys SHA-256 digests of SubjectPublicKeyInfo DER (64 hexadecimal characters or 32 bytes
785
+ * each, see `pki.trust.spkiDigest`), forwarded to `cms.verify` with `trustAnchors`. A
786
+ * listed key on the signer's chain, the anchor included, reports the signer as not trusted.
784
787
  * @opts strictMicalg reject a `multipart/signed` whose `micalg` disagrees with the SignerInfo digest.
785
788
  * @opts legacyHeaderProtection opt in to detecting a LEGACY RFC 8551 header-protected message (RFC 9788 sec. 4.10): a Cryptographic Payload that is a bare `message/rfc822` wrap with no `hp=` parameter. When set, a precisely-identified legacy message surfaces the inner message's headers under `headerProtection.legacy = { headers, mode, fromMismatch, confidential }`, where `headers` is an ordered `[{ name, value }]` array (retaining legally-repeated fields such as `Received`) and the mode is inferred from the envelope (`clear` here). They are not placed under `protectedHeaders`, and `present` stays `false`. Consuming `headerProtection.legacy.headers` is an explicit choice: a legacy message is structurally indistinguishable from an ordinary forwarded `message/rfc822`, so this is a heuristic (RFC 9788 sec. 4.10.2: "not based on any strong end-to-end guarantees"); cross-check `legacy.fromMismatch`. Anything not precisely identified (a nested crypto layer, an `hp=` on the inner message, a non-`message/rfc822` payload, a duplicate of a singleton field, or a duplicate Content-Type) reports `legacy: null`. Off by default. The signed-and-encrypted form (RFC 9788 Appendix C.3.17) is a documented gap (`legacy: null` at `decrypt`; surfaces as `clear` only via the caller's re-`verify` step), because the non-recursive layered API exposes no single seam holding both the inner signature verdict and the outer header section.
786
789
  * @example
@@ -795,6 +798,7 @@ function _capped(msg) {
795
798
  async function verify(message, opts) {
796
799
  opts = opts || _create(null);
797
800
  opts = _knownOpts(opts, VERIFY_OPTS, "verify");
801
+ var distrustedKeys = guard.distrust.keys(opts.distrustedKeys, _err, "smime/bad-input", "pki.smime.verify opts.distrustedKeys");
798
802
  var ent = mime.parse(message, SmimeError, "smime/bad-mime");
799
803
  var ct = ent.contentType;
800
804
  var vOpts = {};
@@ -803,8 +807,9 @@ async function verify(message, opts) {
803
807
  vOpts.trustAnchors = opts.trustAnchors;
804
808
  vOpts.requiredEku = opts.requiredEku != null ? opts.requiredEku : ["emailProtection"];
805
809
  vOpts.checkPurpose = opts.checkPurpose != null ? opts.checkPurpose : "emailProtection";
810
+ if (distrustedKeys !== null) vOpts.distrustedKeys = distrustedKeys;
806
811
  }
807
- if (opts.time !== undefined) vOpts.time = opts.time;
812
+ if (opts.time != null) vOpts.time = opts.time;
808
813
  if (opts.allowWeakDigests === true) vOpts.allowWeakDigests = true;
809
814
  if (_isPkcs7(ct.type, "mime")) {
810
815
  if (ct.params["smime-type"] && ct.params["smime-type"] !== "signed-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(ct.params["smime-type"]) + " (only signed-data)");
@@ -854,10 +859,10 @@ function _envelopedTypeOf(der, badParseCode) {
854
859
 
855
860
  function _cmsEncryptOpts(opts) {
856
861
  var o = {};
857
- if (opts.contentEncryptionAlgorithm !== undefined) o.contentEncryptionAlgorithm = opts.contentEncryptionAlgorithm;
858
- if (opts.oaepHash !== undefined) o.oaepHash = opts.oaepHash;
859
- if (opts.keyIdentifier !== undefined) o.keyIdentifier = opts.keyIdentifier;
860
- if (opts.ukm !== undefined) o.ukm = opts.ukm;
862
+ if (opts.contentEncryptionAlgorithm != null) o.contentEncryptionAlgorithm = opts.contentEncryptionAlgorithm;
863
+ if (opts.oaepHash != null) o.oaepHash = opts.oaepHash;
864
+ if (opts.keyIdentifier != null) o.keyIdentifier = opts.keyIdentifier;
865
+ if (opts.ukm != null) o.ukm = opts.ukm;
861
866
  return o;
862
867
  }
863
868
 
@@ -972,12 +977,12 @@ async function decrypt(message, keyMaterial, opts) {
972
977
  var smimeType = _envelopedTypeOf(der, "smime/unsupported-type");
973
978
  if (opts.strictSmimeType && st && stLower !== smimeType.toLowerCase()) throw _err("smime/smime-type-mismatch", "the header smime-type " + JSON.stringify(st) + " disagrees with the CMS body (" + smimeType + ")");
974
979
  var cmsOpts = {};
975
- if (opts.recipientIndex !== undefined) cmsOpts.recipientIndex = opts.recipientIndex;
976
- if (opts.maxIterations !== undefined) cmsOpts.maxIterations = opts.maxIterations;
980
+ if (opts.recipientIndex != null) cmsOpts.recipientIndex = opts.recipientIndex;
981
+ if (opts.maxIterations != null) cmsOpts.maxIterations = opts.maxIterations;
977
982
  /** @internal Forwarded rather than re-decided here: an S/MIME body is a CMS structure and the rule about
978
983
  * RSAES-PKCS1-v1_5 over an unauthenticated content belongs to the verb that opens it. An
979
984
  * `openssl smime -encrypt` or `openssl cms -encrypt` message is exactly that combination by default. */
980
- if (opts.allowUnauthenticatedRsa15 !== undefined) cmsOpts.allowUnauthenticatedRsa15 = opts.allowUnauthenticatedRsa15;
985
+ if (opts.allowUnauthenticatedRsa15 != null) cmsOpts.allowUnauthenticatedRsa15 = opts.allowUnauthenticatedRsa15;
981
986
  var res = await cms.decrypt(der, keyMaterial, cmsOpts);
982
987
  return Object.assign({
983
988
  content: res.content, smimeType: smimeType, authenticated: res.authenticated,
@@ -1014,7 +1019,7 @@ async function compress(content, opts) {
1014
1019
  opts = _knownOpts(opts, COMPRESS_OPTS, "compress");
1015
1020
  var entity = _entityBytes(content, opts);
1016
1021
  var cOpts = {};
1017
- if (opts.level !== undefined) cOpts.level = opts.level;
1022
+ if (opts.level != null) cOpts.level = opts.level;
1018
1023
  var der = _toBuf(await cms.compress(entity, cOpts));
1019
1024
  var head = _pkcs7MimeHead("compressed-data", "smime.p7z");
1020
1025
  return _capped(Buffer.concat([head, _base64Body(der), mime.CRLF]));
@@ -1053,7 +1058,7 @@ async function decompress(message, opts) {
1053
1058
  if (st && st.toLowerCase() !== "compressed-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(st) + " (only compressed-data)");
1054
1059
  var der = _decodeCms(ent);
1055
1060
  var dOpts = {};
1056
- if (opts.maxOutputBytes !== undefined) dOpts.maxOutputBytes = opts.maxOutputBytes;
1061
+ if (opts.maxOutputBytes != null) dOpts.maxOutputBytes = opts.maxOutputBytes;
1057
1062
  var res = await cms.decompress(der, dOpts);
1058
1063
  return { content: res.content, contentType: res.contentType, contentTypeName: res.contentTypeName, compressionAlgorithm: res.compressionAlgorithm };
1059
1064
  }
@@ -88,7 +88,7 @@ var HEADER_BYTES = 8;
88
88
 
89
89
  function _resolveCap(opts) {
90
90
  var cap = C.LIMITS.TLS_CERT_MSG_MAX_BYTES;
91
- if (opts.maxOutputBytes !== undefined) {
91
+ if (opts.maxOutputBytes != null) {
92
92
  var mo = opts.maxOutputBytes;
93
93
  if (typeof mo !== "number" || !isFinite(mo) || mo <= 0 || Math.floor(mo) !== mo) {
94
94
  throw _err("tls/bad-input", "opts.maxOutputBytes must be a positive integer");
@@ -99,6 +99,7 @@ function _resolveCap(opts) {
99
99
  }
100
100
 
101
101
  function _resolveAllowed(opts) {
102
+ // allow:caller-option-presence-tested-against-undefined -- absence accepts every codec this build supports, so a null must be refused rather than widen the allowlist
102
103
  if (opts.allowedAlgorithms === undefined) return SUPPORTED.slice();
103
104
  var list = opts.allowedAlgorithms;
104
105
  if (!Array.isArray(list)) throw _err("tls/bad-input", "opts.allowedAlgorithms must be an array of algorithm names or numbers");
@@ -263,7 +264,7 @@ function decompressCertificate(bytes, opts) {
263
264
  */
264
265
  function parseCertificateMessage(bytes, opts) {
265
266
  opts = opts || intrinsic.create(null);
266
- var type = opts.certificateType === undefined ? "X509" : opts.certificateType;
267
+ var type = opts.certificateType == null ? "X509" : opts.certificateType;
267
268
  if (type !== "X509" && type !== "RawPublicKey") {
268
269
  throw _err("tls/bad-input", "opts.certificateType must be \"X509\" or \"RawPublicKey\"");
269
270
  }
@@ -330,7 +331,7 @@ function compressCertificate(certificateMessage, opts) {
330
331
  " bytes, over the " + C.LIMITS.TLS_CERT_MSG_MAX_BYTES + "-byte handshake framing limit");
331
332
  }
332
333
  var view = guard.bytes.snapshot(viewed, TlsError, "tls/bad-input", "the certificate message");
333
- var sel = opts.algorithm === undefined ? "zlib" : opts.algorithm;
334
+ var sel = opts.algorithm == null ? "zlib" : opts.algorithm;
334
335
  var name = (typeof sel === "number") ? ALG_BY_NUMBER[sel] : sel;
335
336
  if (typeof name !== "string" || SUPPORTED.indexOf(name) === -1) {
336
337
  throw _err("tls/unsupported-algorithm", "opts.algorithm names an algorithm this toolkit cannot compress: " + JSON.stringify(sel));
@@ -338,12 +339,12 @@ function compressCertificate(certificateMessage, opts) {
338
339
  if (view.length < MIN_CERT_MSG_BYTES) {
339
340
  throw _err("tls/bad-input", "a Certificate message is at least " + MIN_CERT_MSG_BYTES + " bytes (RFC 8446 sec. 4.4.2)");
340
341
  }
341
- if (opts.level !== undefined && (typeof opts.level !== "number" || !isFinite(opts.level) || Math.floor(opts.level) !== opts.level)) {
342
+ if (opts.level != null && (typeof opts.level !== "number" || !isFinite(opts.level) || Math.floor(opts.level) !== opts.level)) {
342
343
  throw _err("tls/bad-input", "opts.level must be an integer");
343
344
  }
344
345
  var stream;
345
346
  try {
346
- stream = COMPRESS[name](view, opts.level !== undefined ? LEVEL_OPT[name](opts.level) : undefined);
347
+ stream = COMPRESS[name](view, opts.level != null ? LEVEL_OPT[name](opts.level) : undefined);
347
348
  } catch (e) {
348
349
  throw _err("tls/bad-input", "the certificate message could not be compressed (check opts.level)", e);
349
350
  }