@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +7 -6
  3. package/bin/pki.js +110 -34
  4. package/lib/acme.js +91 -46
  5. package/lib/attrcert-sign.js +4 -3
  6. package/lib/cmp-build.js +16 -6
  7. package/lib/cmp-session.js +19 -3
  8. package/lib/cmp-verify.js +25 -2
  9. package/lib/cms-compress.js +3 -3
  10. package/lib/cms-digest.js +10 -5
  11. package/lib/cms-encrypt.js +6 -1
  12. package/lib/cms-sign.js +16 -0
  13. package/lib/cms-verify.js +32 -14
  14. package/lib/constants.js +1 -0
  15. package/lib/crl-sign.js +3 -3
  16. package/lib/csr-sign.js +6 -2
  17. package/lib/ct.js +16 -3
  18. package/lib/est.js +67 -31
  19. package/lib/guard-all.js +2 -0
  20. package/lib/guard-distrust.js +224 -0
  21. package/lib/guard-identifier.js +27 -1
  22. package/lib/guard-json.js +1 -1
  23. package/lib/http-transport.js +299 -36
  24. package/lib/jose.js +1 -1
  25. package/lib/lint.js +67 -7
  26. package/lib/ocsp-verify.js +9 -3
  27. package/lib/ocsp.js +16 -4
  28. package/lib/oid.js +4 -0
  29. package/lib/path-validate.js +119 -18
  30. package/lib/pkcs12-build.js +6 -1
  31. package/lib/pki-build.js +38 -6
  32. package/lib/possession.js +6 -2
  33. package/lib/related-cert.js +2 -2
  34. package/lib/revocation-fetch.js +46 -11
  35. package/lib/scep.js +39 -14
  36. package/lib/schema-pkix.js +3 -3
  37. package/lib/sigstore.js +11 -6
  38. package/lib/smime.js +32 -16
  39. package/lib/tls-cert-compress.js +6 -5
  40. package/lib/trust.js +121 -20
  41. package/lib/trustanchor-build.js +16 -1
  42. package/lib/tsp-sign.js +15 -2
  43. package/lib/webauthn-mds.js +29 -8
  44. package/lib/webauthn.js +46 -14
  45. package/lib/x509-sign.js +26 -10
  46. package/package.json +2 -2
  47. package/sbom.cdx.json +6 -6
@@ -51,8 +51,9 @@
51
51
  * transport. Trust is EXPLICIT and fail-closed: a request is refused unless it
52
52
  * carries an https URL and either a `tls.anchors` set (an Explicit trust-anchor
53
53
  * database, mapped to the node `ca` option) or an explicit `tls.useSystemStore`
54
- * opt-in to node's bundled roots. `rejectUnauthorized` is always on: there is no
55
- * code path that disables server-certificate verification. The response body is
54
+ * opt-in to the platform certificate store. `rejectUnauthorized` is always on: there is no
55
+ * code path that disables server-certificate verification. `tls.distrustedKeys` refuses a
56
+ * server whose verified chain carries a listed key. The response body is
56
57
  * bounded WHILE it streams: the accumulator aborts the socket the instant the running
57
58
  * total crosses `maxResponseBytes`, before a byte reaches a decoder. A protocol
58
59
  * client MAY parameterize the transport with its own `(code, message, cause)` error
@@ -87,6 +88,17 @@ var nodeUtil = require("node:util");
87
88
  var constants = require("./constants");
88
89
  var guard = require("./guard-all");
89
90
  var intrinsic = require("./guard-intrinsic");
91
+ /** @internal The certificate reader, and the two operations the distrust walk proves a link with,
92
+ * taken at load. A replaced `verify` would answer whether each link of a server chain is signed by
93
+ * the certificate above it. */
94
+ var _X509Certificate = require("node:crypto").X509Certificate;
95
+ var _x509Verify = intrinsic.uncurry(_X509Certificate.prototype.verify);
96
+ var _x509PublicKey = intrinsic.uncurry(intrinsic.getOwnPropertyDescriptor(_X509Certificate.prototype, "publicKey").get);
97
+ var _x509Ca = intrinsic.uncurry(intrinsic.getOwnPropertyDescriptor(_X509Certificate.prototype, "ca").get);
98
+ var _x509CheckIssued = intrinsic.uncurry(_X509Certificate.prototype.checkIssued);
99
+ var _x509ValidFrom = intrinsic.uncurry(intrinsic.getOwnPropertyDescriptor(_X509Certificate.prototype, "validFromDate").get);
100
+ var _x509ValidTo = intrinsic.uncurry(intrinsic.getOwnPropertyDescriptor(_X509Certificate.prototype, "validToDate").get);
101
+ var _Date = intrinsic.Date;
90
102
  /** @internal `resolve` and `reject` build their promise from the RECEIVER, so reading the global
91
103
  * binding at the call hands the construction to whatever that binding holds. A refusal returned as a
92
104
  * rejected promise is a verdict: a replacement whose `reject` resolves lets a request the preparation
@@ -245,10 +257,16 @@ function _peerChain(socket) {
245
257
  return out;
246
258
  }
247
259
 
260
+ /** @internal Whether an anchor buffer is PEM text rather than DER. A DER certificate is a SEQUENCE,
261
+ * so its first byte is 0x30; a buffer that starts with anything else is PEM text, which may carry a
262
+ * comment ahead of its armor. */
263
+ function _isPemBuffer(a) {
264
+ return _looksLikePemArmor(_bufToString(a, "latin1", 0, 64)) || (a.length > 0 && a[0] !== 0x30);
265
+ }
248
266
  function _pemifyAnchor(a) {
249
267
  if (typeof a === "string") return a;
250
268
  if (_isBuffer(a)) {
251
- if (_looksLikePemArmor(_bufToString(a, "latin1", 0, 64))) return a;
269
+ if (_isPemBuffer(a)) return a;
252
270
  var b64 = _wrap64Lines(_bufToString(a, "base64"));
253
271
  return "-----BEGIN CERTIFICATE-----\n" + b64 + "-----END CERTIFICATE-----\n";
254
272
  }
@@ -264,20 +282,207 @@ function _pemifyAnchors(anchors) {
264
282
  * trust a `useSystemStore` connection is accepted under. */
265
283
  var _getCACertificates = typeof nodeTls.getCACertificates === "function" ? nodeTls.getCACertificates : null;
266
284
 
285
+ /** @internal The platform store alone: the certificates the operating system trusts. Node's
286
+ * compiled-in Mozilla set is a separate store that changes only when Node is upgraded, so a root a
287
+ * distribution has removed would stay trusted if it were added here. A store that cannot be read
288
+ * answers an empty list, and the request paths refuse an empty list rather than leave `ca` unset,
289
+ * which would hand the connection Node's default store instead. */
267
290
  var _systemCaCache = null;
268
291
  function _systemCa() {
269
292
  if (_systemCaCache !== null) return _systemCaCache;
270
293
  var out = [];
271
294
  if (_getCACertificates !== null) {
272
- _arrForEach(["system", "bundled"], function (t) {
273
- // allow:swallow-unverified a store TYPE unsupported on this node is skipped; the other type
274
- try { var c = intrinsic.apply(_getCACertificates, nodeTls, [t]); if (_isArray(c)) out = _concatList(out, c); } catch (_e) { }
275
- });
295
+ // allow:swallow-unverified an unreadable platform store answers empty, and an empty list is refused by every caller
296
+ try { var c = intrinsic.apply(_getCACertificates, nodeTls, ["system"]); if (_isArray(c)) out = _concatList(out, c); } catch (_e) { }
276
297
  }
277
298
  _systemCaCache = out;
278
299
  return out;
279
300
  }
280
301
 
302
+ /** @internal The DER SubjectPublicKeyInfo of a certificate the TLS layer verified. The toolkit's
303
+ * parser is reached through an inline require because of a circular load (schema-x509 ->
304
+ * schema-pkix -> ct -> this module). A certificate the strict parser refuses was still accepted by
305
+ * the TLS stack, so its key is taken from node's own reading rather than left unchecked. */
306
+ function _peerSpki(raw) {
307
+ var x509 = require("./schema-x509"); // allow:inline-require -- circular load: schema-x509 -> schema-pkix -> ct -> this module
308
+ try { return x509.parse(raw).subjectPublicKeyInfo.bytes; }
309
+ catch (_e) {
310
+ // allow:swallow-unverified the parser's refusal is answered from node's reading of the same certificate
311
+ return guard.crypto.exportKey(_x509PublicKey(new _X509Certificate(raw)), { type: "spki", format: "der" });
312
+ }
313
+ }
314
+
315
+ /** @internal Whether `child` carries a signature that verifies under `parent`'s key. A certificate
316
+ * node cannot read, or whose signature algorithm it cannot verify, answers false. */
317
+ function _signedBy(child, parent) {
318
+ try { return _x509Verify(new _X509Certificate(child), _x509PublicKey(new _X509Certificate(parent))) === true; }
319
+ catch (_e) {
320
+ // allow:swallow-unverified a link node cannot verify is reported as unproven, and the caller refuses it
321
+ return false;
322
+ }
323
+ }
324
+
325
+ /** @internal Whether `raw` is inside its validity window now and, for a certificate that issues
326
+ * another on the walk, is a CA certificate. A certificate node cannot read answers false. */
327
+ function _usableLink(raw, mustBeCa, now) {
328
+ try {
329
+ var c = new _X509Certificate(raw);
330
+ if (mustBeCa && _x509Ca(c) !== true) return false;
331
+ return guard.time.instantOf(_x509ValidFrom(c)) <= now && now <= guard.time.instantOf(_x509ValidTo(c));
332
+ } catch (_e) {
333
+ // allow:swallow-unverified a certificate node cannot read is reported as unproven, and the caller refuses it
334
+ return false;
335
+ }
336
+ }
337
+
338
+ /** @internal The trust anchors a connection is opened under, as `{ keys, entries }`: `keys` is a set
339
+ * keyed by `guard.distrust.keyDigest`, and `entries` holds each anchor certificate node reads, with
340
+ * its key, for the branches through the trust store `_storeBranchListed` follows. `caList` holds PEM
341
+ * text, which may carry several certificates, and DER buffers. The codec is reached through an
342
+ * inline require for the circular load named at `_peerSpki`. */
343
+ function _anchorKeySet(caList) {
344
+ var pkix = require("./schema-pkix"); // allow:inline-require -- circular load: schema-pkix -> ct -> this module
345
+ var set = _objectCreate(null);
346
+ var entries = [];
347
+ for (var i = 0; i < caList.length; i++) {
348
+ var a = caList[i];
349
+ var ders = [];
350
+ if (_isBuffer(a) && !_isPemBuffer(a)) ders = [a];
351
+ else {
352
+ var rows;
353
+ try { rows = pkix.pemDecodeBundle(typeof a === "string" ? a : _bufToString(a, "latin1"), {}, Error); }
354
+ catch (_e) {
355
+ // allow:swallow-unverified an anchor this reader cannot decode adds no key, so a chain ending there is refused
356
+ rows = [];
357
+ }
358
+ for (var r = 0; r < rows.length; r++) if (rows[r].label === "CERTIFICATE") _arrPush(ders, rows[r].der);
359
+ }
360
+ for (var d = 0; d < ders.length; d++) {
361
+ try {
362
+ var spki = _peerSpki(ders[d]);
363
+ set[guard.distrust.keyDigest(spki)] = true;
364
+ _arrPush(entries, { x509: new _X509Certificate(ders[d]), spki: spki });
365
+ } catch (_e2) {
366
+ // allow:swallow-unverified an anchor whose key cannot be read adds no key, so a chain ending there is refused
367
+ void 0;
368
+ }
369
+ }
370
+ }
371
+ return { keys: set, entries: entries };
372
+ }
373
+ var _systemAnchorKeys = null;
374
+ function _systemAnchorKeySet() {
375
+ if (_systemAnchorKeys === null) _systemAnchorKeys = _anchorKeySet(_systemCa());
376
+ return _systemAnchorKeys;
377
+ }
378
+ /** @internal The anchors for one hop: the caller's, and the platform store's when the hop uses it.
379
+ * Computed only when the hop has a distrust list to apply. */
380
+ function _hopAnchorKeys(distrust, anchorPems, useSystem) {
381
+ if (distrust === null || distrust.length === 0) return null;
382
+ var own = _anchorKeySet(anchorPems);
383
+ if (!useSystem) return own;
384
+ var sys = _systemAnchorKeySet();
385
+ var names = _objectKeys(sys.keys);
386
+ for (var i = 0; i < names.length; i++) own.keys[names[i]] = true;
387
+ return { keys: own.keys, entries: _concatList(own.entries, sys.entries) };
388
+ }
389
+
390
+ /** @internal The digest of a listed key on a branch through the trust store above `childX509`, or
391
+ * null. OpenSSL looks in the trust store for an issuer before it looks at what the server sent, so
392
+ * when the store holds a certificate that issues one on node's walk (an intermediate, or a
393
+ * cross-certificate), the session may have been verified up that branch instead. Every such branch
394
+ * is followed upward through the store, and a listed key anywhere on one refuses the session.
395
+ * `visited` bounds the search to one visit per anchor certificate. */
396
+ function _storeBranchListed(childX509, anchors, list, visited) {
397
+ for (var i = 0; i < anchors.entries.length; i++) {
398
+ var e = anchors.entries[i];
399
+ if (_arrIndexOf(visited, e) !== -1) continue;
400
+ var issues;
401
+ try { issues = _x509CheckIssued(childX509, e.x509) === true && _x509Verify(childX509, _x509PublicKey(e.x509)) === true; }
402
+ catch (_e) {
403
+ // allow:swallow-unverified a store certificate node cannot compare is not an issuer on any branch
404
+ issues = false;
405
+ }
406
+ if (!issues) continue;
407
+ _arrPush(visited, e);
408
+ if (guard.distrust.has(list, e.spki)) return guard.distrust.digest(e.spki);
409
+ var above = _storeBranchListed(e.x509, anchors, list, visited);
410
+ if (above !== null) return above;
411
+ }
412
+ return null;
413
+ }
414
+
415
+ /** @internal The first distrusted key on the chain the session verified, as `{ digest }`; null
416
+ * when none is listed; or `{ unproven: reason }` when the walk cannot stand for the verified chain.
417
+ *
418
+ * The walk follows the detailed peer certificate's `issuerCertificate` links, which node builds by
419
+ * name and key identifier from the certificates the server sent and then from the trust store,
420
+ * without checking a signature, a validity period or whether the top is trusted. OpenSSL may have
421
+ * verified the session through different certificates with the same names, so the walk is held to
422
+ * what makes it a path in its own right: each link is signed by the certificate above it, each
423
+ * certificate below the top is inside its validity window, each one that issues another is a CA,
424
+ * and the top is self-issued with the key of a configured trust anchor (`anchors.keys`). OpenSSL
425
+ * looks in the trust store for an issuer first, so where the store holds a certificate that issues
426
+ * one on the walk, the session may have been verified up the store's branch rather than the walk;
427
+ * every such branch is followed (`_storeBranchListed`) and a listed key on one refuses the session.
428
+ * Node gives no way to read the chain OpenSSL verified, so the check covers every chain the session
429
+ * could have been verified through, and may refuse a session whose verified chain was clean. The
430
+ * walk reads up to `LIMITS.PATH_MAX_CERTS` certificates. */
431
+ function _distrustedInChain(cert, list, anchors) {
432
+ var now = guard.time.instantOf(new _Date());
433
+ var seen = [];
434
+ var visitedStore = [];
435
+ var cur = cert;
436
+ for (;;) {
437
+ if (seen.length >= constants.LIMITS.PATH_MAX_CERTS) {
438
+ return { unproven: "the chain is longer than the " + constants.LIMITS.PATH_MAX_CERTS + " certificates the check reads" };
439
+ }
440
+ var spki = _peerSpki(cur.raw);
441
+ if (guard.distrust.has(list, spki)) return { digest: guard.distrust.digest(spki) };
442
+ var storeHit;
443
+ try { storeHit = _storeBranchListed(new _X509Certificate(cur.raw), anchors, list, visitedStore); }
444
+ catch (_se) {
445
+ // allow:swallow-unverified a certificate node cannot read leaves the store branches unchecked, so the walk is unproven
446
+ return { unproven: "a certificate on the chain could not be compared with the trust store" };
447
+ }
448
+ if (storeHit !== null) return { digest: storeHit };
449
+ var up = cur.issuerCertificate;
450
+ if (up === cur) {
451
+ return anchors.keys[guard.distrust.keyDigest(spki)] === true ? null
452
+ : { unproven: "the chain ends at a self-issued certificate whose key is not a configured trust anchor's" };
453
+ }
454
+ if (!_usableLink(cur.raw, seen.length > 0, now)) {
455
+ return { unproven: "a certificate on the chain is outside its validity period or issues without being a CA" };
456
+ }
457
+ _arrPush(seen, cur);
458
+ if (_arrIndexOf(seen, up) !== -1) return { unproven: "the chain loops back on a certificate it already passed" };
459
+ if (!up || !_isBuffer(up.raw) || !_signedBy(cur.raw, up.raw)) {
460
+ return { unproven: "a link of the chain is not signed by the certificate node places above it" };
461
+ }
462
+ cur = up;
463
+ }
464
+ }
465
+
466
+ function _distrustError(hit, hop) {
467
+ var e = new Error(hit.digest !== undefined
468
+ ? "the " + hop + " certificate chain carries a key listed in tls.distrustedKeys (SubjectPublicKeyInfo SHA-256 " + hit.digest + ")"
469
+ : "the " + hop + " certificate chain could not be checked against tls.distrustedKeys: " + hit.unproven);
470
+ e.pkiDistrustedKey = hop;
471
+ return e;
472
+ }
473
+
474
+ /** @internal An identity check that also refuses a chain carrying a distrusted key. The identity
475
+ * verdict comes first, so a name mismatch is still reported as one. */
476
+ function _withDistrust(check, list, hop, anchorKeys) {
477
+ if (list === null || list.length === 0) return check;
478
+ return function (host, cert) {
479
+ var err = check(host, cert);
480
+ if (err) return err;
481
+ var hit = _distrustedInChain(cert, list, anchorKeys);
482
+ return hit === null ? undefined : _distrustError(hit, hop);
483
+ };
484
+ }
485
+
281
486
  function _isBlockedIp(ip) {
282
487
  /** @internal The argument has to BE a string, and what cannot be classified is blocked. `isIP`
283
488
  * converts its argument, so a value whose conversion answers a loopback literal to the family
@@ -389,6 +594,8 @@ function _hasWordBounded(l, word) {
389
594
  function _isLowerWordChar(c) { return (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95; }
390
595
  function _classifyError(e, C) {
391
596
  if (e && e.pkiBlockedAddress) return C("blocked-address");
597
+ if (e && e.pkiDistrustedKey === "proxy") return C("proxy-key-distrusted");
598
+ if (e && e.pkiDistrustedKey === "server") return C("server-key-distrusted");
392
599
  var s = _stringOf((e && e.code) || "") + " " + _stringOf((e && e.message) || "");
393
600
  var l = _toLowerCase(s);
394
601
  if (_strIndexOf(l, "protocol_version") !== -1 || _strIndexOf(l, "unsupported_protocol") !== -1 || _strIndexOf(l, "version_too_low") !== -1 ||
@@ -403,7 +610,11 @@ function _keySet(names) { var m = _objectCreate(null); for (var i = 0; i < names
403
610
  var PROXY_KEYS = _keySet(["url", "auth", "tls"]);
404
611
  var PROXY_AUTH_KEYS = _keySet(["scheme", "username", "password", "allowMD5", "allowLegacyQop"]);
405
612
  var PROXY_DIGEST_KNOBS = ["allowMD5", "allowLegacyQop"];
406
- var PROXY_TLS_KEYS = _keySet(["anchors", "useSystemStore", "servername", "minVersion"]);
613
+ var PROXY_TLS_KEYS = _keySet(["anchors", "useSystemStore", "servername", "minVersion", "distrustedKeys"]);
614
+ /** @internal The names a `tls` policy bag may carry. A name outside them is refused rather than
615
+ * ignored: a misspelled `distrustedKeys` or `anchors` would otherwise leave the connection under a
616
+ * policy the caller did not state. */
617
+ var TLS_KEYS = _keySet(["anchors", "useSystemStore", "cert", "key", "minVersion", "servername", "checkServerIdentity", "distrustedKeys"]);
407
618
 
408
619
  function _copyAnchor(a) { return (a !== null && typeof a === "object" && nodeUtil.types.isProxy(a)) ? a : (_isBuffer(a) ? _bufferFrom(a) : a); }
409
620
  function _noopProxyErr(code, message) { var e = new Error(message); e.code = code; e.isPkiError = true; return e; }
@@ -461,6 +672,12 @@ function snapshotProxy(p) {
461
672
  out.tls.anchors = copied;
462
673
  } else if (typeof a === "string") out.tls.anchors = a;
463
674
  else if (a !== null && typeof a === "object" && !nodeUtil.types.isProxy(a) && _isBuffer(a)) out.tls.anchors = _bufferFrom(a);
675
+ /** @internal The distrust list is copied for the reason the anchors are: the snapshot is taken
676
+ * so a caller's later edit cannot change the policy, and a shared array would carry the edit. */
677
+ var dk = p.tls.distrustedKeys;
678
+ if (dk !== undefined && dk !== null) {
679
+ out.tls.distrustedKeys = guard.distrust.keys(dk, _noopProxyErr, "transport/bad-proxy", "opts.proxy.tls.distrustedKeys");
680
+ }
464
681
  }
465
682
  return out;
466
683
  } catch (_e) {
@@ -472,7 +689,7 @@ function snapshotProxy(p) {
472
689
  /** @internal `originHost`, `statedPort` and `scheme` are the values the request path already read off
473
690
  * the parsed origin; they are passed in rather than read again here, so the authority this builds for
474
691
  * the CONNECT target is the same authority the socket options were built from. */
475
- function _validateProxy(request, defaults, E, C, originHost, originStatedPort, originScheme) {
692
+ function _validateProxy(request, defaults, E, C, originHost, originStatedPort, originScheme, originDistrust) {
476
693
  var rawProxy = request.proxy;
477
694
  var proxy = rawProxy !== undefined ? rawProxy : defaults.proxy;
478
695
  if (proxy === undefined || proxy === null) return null;
@@ -544,7 +761,17 @@ function _validateProxy(request, defaults, E, C, originHost, originStatedPort, o
544
761
  * the CONNECT options. `concatList` copies own indexes and appends own indexes. */
545
762
  if (pHasAnchors) pCa = _concatList(pCa, _pemifyAnchors(pAnchors));
546
763
  if (pUseSystem) pCa = _concatList(pCa, _systemCa());
547
- proxyTls = { ca: pCa.length ? pCa : undefined, minVersion: pMinVersion, servername: pServername };
764
+ if (pCa.length === 0) {
765
+ throw E(C("no-trust-anchors"), "proxy.tls.useSystemStore is set and the platform certificate store returned no certificates, " +
766
+ "so the proxy cannot be authenticated; set proxy.tls.anchors");
767
+ }
768
+ /** @internal A key the caller distrusts is refused on this hop too, whether it was listed for
769
+ * the proxy or for the origin. */
770
+ var pDistrust = guard.distrust.union(
771
+ guard.distrust.keys(ptls ? ptls.distrustedKeys : undefined, E, C("bad-proxy"), "opts.proxy.tls.distrustedKeys"),
772
+ originDistrust);
773
+ proxyTls = { ca: pCa, minVersion: pMinVersion, servername: pServername, distrust: pDistrust,
774
+ anchorKeys: _hopAnchorKeys(pDistrust, pHasAnchors ? _pemifyAnchors(pAnchors) : [], pUseSystem) };
548
775
  }
549
776
 
550
777
  var originAuthHost = _isIP(originHost) === 6 ? "[" + originHost + "]" : originHost;
@@ -587,6 +814,7 @@ function _digestProxyCredential(headers, pinfo, C, E) {
587
814
  }, E);
588
815
  }
589
816
  function _proxyErrorClass(e, C) {
817
+ if (e && e.pkiDistrustedKey === "proxy") return C("proxy-key-distrusted");
590
818
  var code = _stringOf((e && e.code) || "");
591
819
  if (_strIndexOf(code, "ERR_TLS") === 0 || _strIndexOf(code, "ERR_SSL") === 0 || _strIndexOf(code, "SSL") !== -1 ||
592
820
  _strIndexOf(code, "CERT") !== -1 || _strIndexOf(code, "SELF_SIGNED") !== -1 || _strIndexOf(code, "VERIFY") !== -1 ||
@@ -605,28 +833,34 @@ function _proxyErrorClass(e, C) {
605
833
  *
606
834
  * Build a fail-closed `node:https` transport: `transport(request) -> Promise<{ status,
607
835
  * headers, body }>`. `defaults` binds a `tls` policy (`anchors` -> the node `ca`;
608
- * `useSystemStore` to opt into the bundled roots; `cert`/`key` for mutual TLS;
836
+ * `useSystemStore` to opt into the platform certificate store; `distrustedKeys` to refuse
837
+ * listed keys; `cert`/`key` for mutual TLS;
609
838
  * `minVersion` 'TLSv1.2' (default) or 'TLSv1.3'; `servername`; a `checkServerIdentity`
610
839
  * that may only tighten) plus `timeout` and `maxResponseBytes` budgets. Each `request`
611
- * ({ method, url, headers, body, tls, timeout, maxResponseBytes }) may override them.
840
+ * ({ method, url, headers, body, tls, timeout, maxResponseBytes }) may override them,
841
+ * except `distrustedKeys`: the request's list is added to the default's.
612
842
  * A non-https URL (`transport/insecure-url`), a request with neither an explicit
613
- * anchor nor `useSystemStore` (`transport/no-trust-anchors`), a body over the streaming
843
+ * anchor nor `useSystemStore`, or with `useSystemStore` and an empty platform store
844
+ * (`transport/no-trust-anchors`), a body over the streaming
614
845
  * cap (`transport/response-too-large`), a stalled socket (`transport/timeout`), a below
615
- * -floor negotiation (`transport/tls-floor`), or a failed server authentication
616
- * (`transport/server-auth-failed`) all fail closed; `rejectUnauthorized` is always on.
846
+ * -floor negotiation (`transport/tls-floor`), a failed server authentication
847
+ * (`transport/server-auth-failed`), or a verified chain carrying a distrusted key
848
+ * (`transport/server-key-distrusted`) all fail closed; `rejectUnauthorized` is always on.
617
849
  * A protocol client passes its own error factory (`defaults.E`) + `defaults.errPrefix`
618
850
  * to surface domain codes (`est/...`). The transport owns no HTTP/redirect/auth
619
851
  * semantics; those live in the message layer that consumes the response triple.
620
852
  *
621
853
  * @opts
622
854
  * - `tls.anchors` -- Explicit trust anchor(s): a DER/PEM Buffer, an array, or PEM string(s) (node `ca`).
623
- * - `tls.useSystemStore` -- boolean; the one opt-in to node's bundled CA store (default false).
855
+ * - `tls.useSystemStore` -- boolean; the one opt-in to the platform certificate store, `tls.getCACertificates("system")` (default false). Node's compiled-in roots are not included; pass them as `tls.anchors` to use them. An empty platform store is refused with `transport/no-trust-anchors` unless `tls.anchors` is also set.
856
+ * - `tls.distrustedKeys` -- SHA-256 digests of SubjectPublicKeyInfo values (64 hex characters or 32 bytes each, see `pki.trust.spkiDigest`). A server whose verified chain carries a listed key, at any position including the root, is refused with `transport/server-key-distrusted`. The chain node reports is checked as a path of its own: each link is signed by the certificate above it, each certificate below the top is inside its validity period and each one that issues another is a CA, and the top is self-issued with the key of a configured anchor. Where a configured anchor certificate (an intermediate or a cross-certificate, as a platform store can hold) issues a certificate on that chain, the branch up through the anchors is checked too, since OpenSSL may have verified the session that way. A chain that fails any of these, or is longer than `C.LIMITS.PATH_MAX_CERTS`, is refused with the same code while a list is set, so a session can be refused when the chain OpenSSL verified was clean but another chain it could have used is not. A request's list is added to the transport default's, which is read when the transport is built.
857
+ * - A `tls` bag naming anything other than the options above is refused with `transport/bad-input`, the transport default's when the transport is built and a request's before a socket opens.
624
858
  * - `tls.cert` / `tls.key` -- client certificate + key for mutual-TLS re-enrollment.
625
859
  * - `tls.minVersion` -- 'TLSv1.2' (default) or 'TLSv1.3'; never below the floor.
626
860
  * - `tls.servername` / `tls.checkServerIdentity` -- SNI + RFC 6125 identity; may tighten, never disable.
627
861
  * - `timeout` -- ms (default C.TIME.seconds(30)); `maxResponseBytes` -- default LIMITS.HTTP_MAX_RESPONSE_BYTES, tightenable downward only.
628
862
  * - `blockPrivateAddresses` -- boolean; when true, an IP-literal host OR a hostname resolving to a private / loopback / link-local address is refused (`transport/blocked-address`), and a resolved address is pinned for the connection. For fetching an untrusted-certificate URL (AIA caIssuers); default false. It cannot be combined with `proxy`: a forward proxy resolves the origin itself, so the transport cannot enforce the block on it, and the combination is refused (`transport/bad-proxy`).
629
- * - `proxy` -- reach the https origin through a forward HTTP proxy: `{ url: "http://proxy:3128" | "https://proxy:3128", auth?: { scheme: "basic" | "digest", username, password, allowMD5?, allowLegacyQop? }, tls?: { anchors, useSystemStore, servername, minVersion } }`. A CONNECT tunnel is opened to the proxy and the origin's TLS is negotiated inside it under the identical origin trust policy, so the proxy cannot read the origin's encrypted session or substitute the origin certificate. `auth` requires an `https://` proxy: the credentials ride the authenticated TLS-to-proxy channel (verified against `proxy.tls`, which is required for an https proxy), so a plaintext `http://` proxy carrying `auth` is refused (`transport/proxy-auth-requires-tls`) rather than exposing them to the proxy hop. An `http://` proxy is tunnel-only. Basic (RFC 7617) sends its credential on the first CONNECT. Digest (RFC 7616) answers the proxy's `Proxy-Authenticate` challenge on a `407`, hashing the method `CONNECT` over the authority-form target, and answers exactly once: a proxy that repeats its challenge has rejected the credential. The Digest policy is the one the origin verbs use, so MD5 and a challenge carrying no `qop` are refused unless `allowMD5` / `allowLegacyQop` says otherwise (`transport/proxy-digest-weak-algorithm`, `transport/proxy-digest-unsupported-algorithm`, `transport/proxy-digest-no-qop`, `transport/proxy-digest-bad-challenge`). A `407` is `transport/proxy-auth-failed` (credentials rejected) or `transport/proxy-auth-required` (none supplied, or none of the offered schemes is the configured one); a proxy certificate that does not verify is `transport/proxy-tls-failed`; a non-2xx CONNECT is `transport/proxy-connect-failed`; a malformed option is `transport/bad-proxy`. Off by default (a direct connection).
863
+ * - `proxy` -- reach the https origin through a forward HTTP proxy: `{ url: "http://proxy:3128" | "https://proxy:3128", auth?: { scheme: "basic" | "digest", username, password, allowMD5?, allowLegacyQop? }, tls?: { anchors, useSystemStore, servername, minVersion, distrustedKeys } }`. A CONNECT tunnel is opened to the proxy and the origin's TLS is negotiated inside it under the identical origin trust policy, so the proxy cannot read the origin's encrypted session or substitute the origin certificate. `auth` requires an `https://` proxy: the credentials ride the authenticated TLS-to-proxy channel (verified against `proxy.tls`, which is required for an https proxy), so a plaintext `http://` proxy carrying `auth` is refused (`transport/proxy-auth-requires-tls`) rather than exposing them to the proxy hop. An `http://` proxy is tunnel-only. Basic (RFC 7617) sends its credential on the first CONNECT. Digest (RFC 7616) answers the proxy's `Proxy-Authenticate` challenge on a `407`, hashing the method `CONNECT` over the authority-form target, and answers exactly once: a proxy that repeats its challenge has rejected the credential. The Digest policy is the one the origin verbs use, so MD5 and a challenge carrying no `qop` are refused unless `allowMD5` / `allowLegacyQop` says otherwise (`transport/proxy-digest-weak-algorithm`, `transport/proxy-digest-unsupported-algorithm`, `transport/proxy-digest-no-qop`, `transport/proxy-digest-bad-challenge`). A `407` is `transport/proxy-auth-failed` (credentials rejected) or `transport/proxy-auth-required` (none supplied, or none of the offered schemes is the configured one); a proxy certificate that does not verify is `transport/proxy-tls-failed`; a proxy chain carrying a key listed in `proxy.tls.distrustedKeys` or the origin's `tls.distrustedKeys` is `transport/proxy-key-distrusted`; a non-2xx CONNECT is `transport/proxy-connect-failed`; a malformed option is `transport/bad-proxy`. Off by default (a direct connection).
630
864
  * @example
631
865
  * var t = pki.transport.https({ tls: { anchors: [caPem] } });
632
866
  * var res = await t({ method: "GET", url: "https://ca.example/.well-known/est/cacerts" });
@@ -645,6 +879,12 @@ function httpsTransport(defaults) {
645
879
  * the settings read from this record is the opt-in that turns away an unpinned server. */
646
880
  var rawDefaultTls = defaults.tls;
647
881
  var tlsDefaults = rawDefaultTls != null ? rawDefaultTls : intrinsic.create(null);
882
+ if (rawDefaultTls != null) {
883
+ guard.identifier.assertKnownKeys(tlsDefaults, TLS_KEYS, E, C("bad-input"), "the transport default tls has an unknown option ");
884
+ }
885
+ /** @internal The default distrust list is read and copied once, here, so editing the caller's
886
+ * array after the transport is built changes nothing. */
887
+ var defaultDistrust = guard.distrust.keys(tlsDefaults.distrustedKeys, E, C("bad-input"), "the transport default tls.distrustedKeys");
648
888
 
649
889
  function _prepare(request) {
650
890
  var url;
@@ -674,6 +914,7 @@ function httpsTransport(defaults) {
674
914
  * public verb instead of this module's typed refusal. */
675
915
  var rawReqTls = request.tls;
676
916
  var reqTls = rawReqTls != null ? rawReqTls : intrinsic.create(null);
917
+ if (rawReqTls != null) guard.identifier.assertKnownKeys(reqTls, TLS_KEYS, E, C("bad-input"), "tls has an unknown option ");
677
918
  var rawAnchors = reqTls.anchors;
678
919
  var anchors = rawAnchors !== undefined ? rawAnchors : tlsDefaults.anchors;
679
920
  var rawUseSystem = reqTls.useSystemStore;
@@ -686,6 +927,20 @@ function httpsTransport(defaults) {
686
927
  if (scheme === "https:" && !hasAnchors && !useSystem) {
687
928
  throw E(C("no-trust-anchors"), "no explicit trust anchor and useSystemStore not set -- refusing an unpinned server (RFC 7030 sec. 3.6)");
688
929
  }
930
+ /** @internal The platform store is read here so an empty one is refused before any socket
931
+ * opens. A connection given an empty `ca` list falls back to Node's default store, which is
932
+ * Node's compiled-in set plus NODE_EXTRA_CA_CERTS, and that is not the store the caller asked
933
+ * for. */
934
+ var systemCa = useSystem ? _systemCa() : [];
935
+ if (scheme === "https:" && !hasAnchors && systemCa.length === 0) {
936
+ throw E(C("no-trust-anchors"), "useSystemStore is set and the platform certificate store returned no certificates, " +
937
+ "so the server cannot be authenticated; set tls.anchors, or install the host's CA bundle");
938
+ }
939
+ /** @internal The transport's list and the request's are combined, so a request cannot drop a
940
+ * key the transport was built to refuse by naming a shorter list. */
941
+ var rawDistrust = reqTls.distrustedKeys;
942
+ var distrust = guard.distrust.union(defaultDistrust,
943
+ guard.distrust.keys(rawDistrust, E, C("bad-input"), "tls.distrustedKeys"));
689
944
 
690
945
  var rawTimeout = request.timeout;
691
946
  var timeout = _budget(rawTimeout !== undefined ? rawTimeout : defaults.timeout, "timeout", DEFAULT_TIMEOUT, MAX_TIMEOUT, E, C("bad-input"));
@@ -711,10 +966,14 @@ function httpsTransport(defaults) {
711
966
  var host = _stripIpv6Brackets(_urlHostname(url));
712
967
  var plaintext = scheme === "http:";
713
968
  var statedPort = _urlPort(url);
969
+ /** @internal `defaultPort` is what node compares the port with when it writes the Host header.
970
+ * With no agent there is no default to compare with, so node appends the port every time and a
971
+ * URL with no port is sent as `Host: name:80`. Some servers answer that with an error. */
714
972
  var options = {
715
973
  method: request.method || "GET",
716
974
  hostname: host,
717
975
  port: statedPort || (plaintext ? 80 : 443),
976
+ defaultPort: plaintext ? 80 : 443,
718
977
  path: _urlPathname(url) + _urlSearch(url),
719
978
  headers: reqHeaders,
720
979
  minVersion: minVersion,
@@ -747,16 +1006,17 @@ function httpsTransport(defaults) {
747
1006
  if (sni && !_isIP(sni)) options.servername = sni;
748
1007
  var caList = [];
749
1008
  if (hasAnchors) caList = _concatList(caList, _pemifyAnchors(anchors));
750
- if (useSystem) caList = _concatList(caList, _systemCa());
1009
+ if (useSystem) caList = _concatList(caList, systemCa);
751
1010
  if (caList.length) options.ca = caList;
752
1011
  if (cert) options.cert = cert;
753
1012
  if (key) options.key = key;
1013
+ var anchorKeys = _hopAnchorKeys(distrust, hasAnchors ? _pemifyAnchors(anchors) : [], useSystem);
754
1014
  if (callerCsi) {
755
- options.checkServerIdentity = function (host, cert2) {
1015
+ options.checkServerIdentity = _withDistrust(function (host, cert2) {
756
1016
  var baseErr = _checkServerIdentity(host, cert2);
757
1017
  if (baseErr) return baseErr;
758
1018
  return callerCsi(host, cert2);
759
- };
1019
+ }, distrust, "server", anchorKeys);
760
1020
  } else {
761
1021
  /** @internal With no caller hook, the option is set to the CAPTURE rather than left out. Left
762
1022
  * out, node's own TLS stack looks the function up on its module export when the handshake
@@ -764,15 +1024,15 @@ function httpsTransport(defaults) {
764
1024
  * another host is accepted. Measured: with this absent, a request asking for a name the
765
1025
  * server's certificate does not carry was admitted. The function is node's own default, taken
766
1026
  * at load, so the verdict is the one it would have reached anyway. */
767
- options.checkServerIdentity = _checkServerIdentity;
1027
+ options.checkServerIdentity = _withDistrust(_checkServerIdentity, distrust, "server", anchorKeys);
768
1028
  }
769
1029
  var proxyInfo;
770
- try { proxyInfo = _validateProxy(request, defaults, E, C, host, statedPort, scheme); }
1030
+ try { proxyInfo = _validateProxy(request, defaults, E, C, host, statedPort, scheme, distrust); }
771
1031
  catch (e) {
772
1032
  var typedProxyError = false;
773
1033
  try {
774
1034
  var ec = (e instanceof Error) && !nodeUtil.types.isProxy(e) ? e.code : undefined;
775
- typedProxyError = ec === C("bad-proxy") || ec === C("proxy-auth-requires-tls");
1035
+ typedProxyError = ec === C("bad-proxy") || ec === C("proxy-auth-requires-tls") || ec === C("no-trust-anchors");
776
1036
  } catch (_ie) { /* allow:swallow-unverified reading .code on a hostile error object can throw; treat any such failure as untyped */ }
777
1037
  if (typedProxyError) throw e;
778
1038
  throw E(C("bad-proxy"), "opts.proxy could not be validated", e);
@@ -785,7 +1045,7 @@ function httpsTransport(defaults) {
785
1045
  options.lookup = _guardedLookup;
786
1046
  }
787
1047
 
788
- return { options: options, timeout: timeout, maxBytes: maxBytes, body: body, proxy: proxyInfo, blockPrivate: blockPrivate, callerCsi: callerCsi, plaintext: plaintext };
1048
+ return { options: options, timeout: timeout, maxBytes: maxBytes, body: body, proxy: proxyInfo, blockPrivate: blockPrivate, callerCsi: callerCsi, plaintext: plaintext, distrust: distrust, anchorKeys: anchorKeys };
789
1049
  }
790
1050
 
791
1051
  var _transportFn = function transport(request) {
@@ -833,7 +1093,7 @@ function httpsTransport(defaults) {
833
1093
  * options are the ones node computed for this hop, forwarded unchanged, so every TLS
834
1094
  * setting the request carries still applies; only the function that opens the socket is
835
1095
  * this module's. The tunnel path already supplies its own and keeps it. */
836
- if (options.createConnection === undefined) {
1096
+ if (options.createConnection == null) {
837
1097
  options.createConnection = prep.plaintext
838
1098
  ? function (opts) { return intrinsic.apply(_netConnect, nodeNet, [opts]); }
839
1099
  : function (opts) { return intrinsic.apply(_tlsConnect, nodeTls, [opts]); };
@@ -949,7 +1209,8 @@ function httpsTransport(defaults) {
949
1209
  connectOpts.minVersion = pinfo.proxyTls.minVersion;
950
1210
  var pIdentity = pinfo.proxyTls.servername || pinfo.host;
951
1211
  connectOpts.servername = (pIdentity && !_isIP(pIdentity)) ? pIdentity : "";
952
- connectOpts.checkServerIdentity = function (host, cert) { return _checkServerIdentity(pIdentity, cert); };
1212
+ connectOpts.checkServerIdentity = _withDistrust(function (host, cert) { return _checkServerIdentity(pIdentity, cert); },
1213
+ pinfo.proxyTls.distrust, "proxy", pinfo.proxyTls.anchorKeys);
953
1214
  connectOpts.createConnection = function (opts) { return intrinsic.apply(_tlsConnect, nodeTls, [opts]); };
954
1215
  cr = intrinsic.apply(_httpsRequest, nodeHttps, [connectOpts]);
955
1216
  } else {
@@ -997,11 +1258,11 @@ function httpsTransport(defaults) {
997
1258
  ca: prep.options.ca,
998
1259
  minVersion: prep.options.minVersion,
999
1260
  rejectUnauthorized: true,
1000
- checkServerIdentity: function (host, cert) {
1261
+ checkServerIdentity: _withDistrust(function (host, cert) {
1001
1262
  var baseErr = _checkServerIdentity(idHost, cert);
1002
1263
  if (baseErr) return baseErr;
1003
1264
  return prep.callerCsi ? prep.callerCsi(idHost, cert) : undefined;
1004
- },
1265
+ }, prep.distrust, "server", prep.anchorKeys),
1005
1266
  cert: prep.options.cert,
1006
1267
  key: prep.options.key,
1007
1268
  }]);
@@ -1060,12 +1321,12 @@ function httpsTransport(defaults) {
1060
1321
  host: o.hostname, port: o.port, minVersion: o.minVersion,
1061
1322
  rejectUnauthorized: o.rejectUnauthorized,
1062
1323
  };
1063
- if (o.servername !== undefined) connectOpts.servername = o.servername;
1064
- if (o.ca !== undefined) connectOpts.ca = o.ca;
1065
- if (o.cert !== undefined) connectOpts.cert = o.cert;
1066
- if (o.key !== undefined) connectOpts.key = o.key;
1067
- if (o.checkServerIdentity !== undefined) connectOpts.checkServerIdentity = o.checkServerIdentity;
1068
- if (o.lookup !== undefined) connectOpts.lookup = o.lookup;
1324
+ if (o.servername != null) connectOpts.servername = o.servername;
1325
+ if (o.ca != null) connectOpts.ca = o.ca;
1326
+ if (o.cert != null) connectOpts.cert = o.cert;
1327
+ if (o.key != null) connectOpts.key = o.key;
1328
+ if (o.checkServerIdentity != null) connectOpts.checkServerIdentity = o.checkServerIdentity;
1329
+ if (o.lookup != null) connectOpts.lookup = o.lookup;
1069
1330
 
1070
1331
  return new _Promise(function (resolve, reject) {
1071
1332
  var settled = false;
@@ -1134,7 +1395,8 @@ function httpsTransport(defaults) {
1134
1395
  * one. A request with neither an explicit anchor nor `useSystemStore` is refused
1135
1396
  * (`transport/no-trust-anchors`) before any socket is opened, and an endpoint whose chain does
1136
1397
  * not build to the configured anchors, or whose name does not match the URL, is refused
1137
- * (`transport/server-auth-failed`). A configured proxy is refused (`transport/bad-proxy`)
1398
+ * (`transport/server-auth-failed`). A chain carrying a key listed in `tls.distrustedKeys` is
1399
+ * refused (`transport/server-key-distrusted`). A configured proxy is refused (`transport/bad-proxy`)
1138
1400
  * rather than bypassed, since connecting directly would reach an origin the caller asked to
1139
1401
  * reach through the proxy.
1140
1402
  *
@@ -1151,4 +1413,5 @@ function httpsTransport(defaults) {
1151
1413
  */
1152
1414
  function peerChain(request, defaults) { return httpsTransport(defaults || intrinsic.create(null)).peerChain(request); }
1153
1415
 
1154
- module.exports = { https: httpsTransport, peerChain: peerChain, isBlockedIp: _isBlockedIp, _makeGuardedLookup: _makeGuardedLookup, MAX_TIMEOUT: MAX_TIMEOUT, snapshotProxy: snapshotProxy };
1416
+ module.exports = { https: httpsTransport, peerChain: peerChain, isBlockedIp: _isBlockedIp, _makeGuardedLookup: _makeGuardedLookup, MAX_TIMEOUT: MAX_TIMEOUT, snapshotProxy: snapshotProxy,
1417
+ TLS_KEYS: intrinsic.freeze(TLS_KEYS) };
package/lib/jose.js CHANGED
@@ -373,7 +373,7 @@ async function verify(jws, opts) {
373
373
  * `snapshotDeep` refuses an accessor-backed member rather than reading it once, which is the stronger
374
374
  * answer for a value that selects an algorithm. The embedded jwk comes from the parsed header and is
375
375
  * this module's own object, so it needs no copy. */
376
- var suppliedKey = opts.key === undefined ? undefined
376
+ var suppliedKey = opts.key == null ? undefined
377
377
  : guard.bytes.snapshotDeep(opts.key, JoseError, "jose/bad-key", "opts.key");
378
378
  var jwk = header.jwk || suppliedKey;
379
379
  if (!jwk) throw E("jose/bad-key", "a verification key is required (opts.key) when the profile does not embed a jwk");