@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
@@ -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,292 @@ 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) { }
297
+ }
298
+ _systemCaCache = _withoutCompromised(out);
299
+ return _systemCaCache;
300
+ }
301
+
302
+ /** @internal `pems` without the entries holding a certificate whose key is compromised
303
+ * (`guard.distrust.compromised`), so the TLS layer cannot verify a session through one. An entry
304
+ * this reader cannot decode is kept: `_anchorKeySet` reports no certificate for it. */
305
+ function _withoutCompromised(pems) {
306
+ var out = [];
307
+ for (var i = 0; i < pems.length; i++) {
308
+ var entries = _anchorKeySet([pems[i]]).entries;
309
+ var listed = false;
310
+ for (var j = 0; j < entries.length && !listed; j++) listed = guard.distrust.compromised(entries[j].spki) !== null;
311
+ if (!listed) _arrPush(out, pems[i]);
276
312
  }
277
- _systemCaCache = out;
278
313
  return out;
279
314
  }
280
315
 
316
+ /** @internal Throws `E(code, ...)` when a caller's anchor carries a compromised key. `pems` is the
317
+ * list `_pemifyAnchors` returns, and an entry may hold several certificates. */
318
+ function _refuseCompromisedAnchors(pems, E, code, label) {
319
+ var keys = _anchorKeySet(pems).entries;
320
+ for (var i = 0; i < keys.length; i++) {
321
+ var why = guard.distrust.compromised(keys[i].spki);
322
+ if (why !== null) throw E(code, label + " holds a certificate with a compromised key: " + why);
323
+ }
324
+ }
325
+
326
+ /** @internal The first compromised key on node's walk of the peer chain, as `{ digest, compromised }`;
327
+ * `{ unchecked: reason }` when the walk is longer than `LIMITS.PATH_MAX_CERTS` certificates, so part
328
+ * of it would go unread; or null. The walk is read as node builds it and no link is proven: the check
329
+ * refuses on a hit and does not refuse a walk whose links it cannot prove. This runs
330
+ * inside node's `checkServerIdentity` call, which node makes without a `try`, so nothing here may
331
+ * throw. */
332
+ function _compromisedOnWalk(cert) {
333
+ var cur = cert;
334
+ for (var n = 0; cur && _isBuffer(cur.raw); n++) {
335
+ if (n >= constants.LIMITS.PATH_MAX_CERTS) {
336
+ return { unchecked: "the chain is longer than the " + constants.LIMITS.PATH_MAX_CERTS + " certificates the check reads" };
337
+ }
338
+ var spki;
339
+ try { spki = _peerSpki(cur.raw); }
340
+ catch (_e) {
341
+ // allow:swallow-unverified a certificate whose key neither reader returns is not a hit here; a non-empty tls.distrustedKeys refuses it as unproven
342
+ spki = null;
343
+ }
344
+ var why = spki === null ? null : guard.distrust.compromised(spki);
345
+ if (why !== null) return { digest: guard.distrust.digest(spki), compromised: why };
346
+ if (cur.issuerCertificate === cur) return null;
347
+ cur = cur.issuerCertificate;
348
+ }
349
+ return null;
350
+ }
351
+
352
+ /** @internal The DER SubjectPublicKeyInfo of a certificate the TLS layer verified. The toolkit's
353
+ * parser is reached through an inline require because of a circular load (schema-x509 ->
354
+ * schema-pkix -> ct -> this module). A certificate the strict parser refuses was still accepted by
355
+ * the TLS stack, so its key is taken from node's own reading rather than left unchecked. */
356
+ function _peerSpki(raw) {
357
+ var x509 = require("./schema-x509"); // allow:inline-require -- circular load: schema-x509 -> schema-pkix -> ct -> this module
358
+ try { return x509.parse(raw).subjectPublicKeyInfo.bytes; }
359
+ catch (_e) {
360
+ // allow:swallow-unverified the parser's refusal is answered from node's reading of the same certificate
361
+ return guard.crypto.exportKey(_x509PublicKey(new _X509Certificate(raw)), { type: "spki", format: "der" });
362
+ }
363
+ }
364
+
365
+ /** @internal Whether `child` carries a signature that verifies under `parent`'s key. A certificate
366
+ * node cannot read, or whose signature algorithm it cannot verify, answers false. */
367
+ function _signedBy(child, parent) {
368
+ try { return _x509Verify(new _X509Certificate(child), _x509PublicKey(new _X509Certificate(parent))) === true; }
369
+ catch (_e) {
370
+ // allow:swallow-unverified a link node cannot verify is reported as unproven, and the caller refuses it
371
+ return false;
372
+ }
373
+ }
374
+
375
+ /** @internal Whether `raw` is inside its validity window now and, for a certificate that issues
376
+ * another on the walk, is a CA certificate. A certificate node cannot read answers false. */
377
+ function _usableLink(raw, mustBeCa, now) {
378
+ try {
379
+ var c = new _X509Certificate(raw);
380
+ if (mustBeCa && _x509Ca(c) !== true) return false;
381
+ return guard.time.instantOf(_x509ValidFrom(c)) <= now && now <= guard.time.instantOf(_x509ValidTo(c));
382
+ } catch (_e) {
383
+ // allow:swallow-unverified a certificate node cannot read is reported as unproven, and the caller refuses it
384
+ return false;
385
+ }
386
+ }
387
+
388
+ /** @internal The trust anchors a connection is opened under, as `{ keys, entries }`: `keys` is a set
389
+ * keyed by `guard.distrust.keyDigest`, and `entries` holds each anchor certificate node reads, with
390
+ * its key, for the branches through the trust store `_storeBranchListed` follows. `caList` holds PEM
391
+ * text, which may carry several certificates, and DER buffers. The codec is reached through an
392
+ * inline require for the circular load named at `_peerSpki`. */
393
+ function _anchorKeySet(caList) {
394
+ var pkix = require("./schema-pkix"); // allow:inline-require -- circular load: schema-pkix -> ct -> this module
395
+ var set = _objectCreate(null);
396
+ var entries = [];
397
+ for (var i = 0; i < caList.length; i++) {
398
+ var a = caList[i];
399
+ var ders = [];
400
+ if (_isBuffer(a) && !_isPemBuffer(a)) ders = [a];
401
+ else {
402
+ var rows;
403
+ try { rows = pkix.pemDecodeBundle(typeof a === "string" ? a : _bufToString(a, "latin1"), {}, Error); }
404
+ catch (_e) {
405
+ // allow:swallow-unverified an anchor this reader cannot decode adds no key, so a chain ending there is refused
406
+ rows = [];
407
+ }
408
+ for (var r = 0; r < rows.length; r++) if (rows[r].label === "CERTIFICATE") _arrPush(ders, rows[r].der);
409
+ }
410
+ for (var d = 0; d < ders.length; d++) {
411
+ try {
412
+ var spki = _peerSpki(ders[d]);
413
+ set[guard.distrust.keyDigest(spki)] = true;
414
+ _arrPush(entries, { x509: new _X509Certificate(ders[d]), spki: spki });
415
+ } catch (_e2) {
416
+ // allow:swallow-unverified an anchor whose key cannot be read adds no key, so a chain ending there is refused
417
+ void 0;
418
+ }
419
+ }
420
+ }
421
+ return { keys: set, entries: entries };
422
+ }
423
+ var _systemAnchorKeys = null;
424
+ function _systemAnchorKeySet() {
425
+ if (_systemAnchorKeys === null) _systemAnchorKeys = _anchorKeySet(_systemCa());
426
+ return _systemAnchorKeys;
427
+ }
428
+ /** @internal The anchors for one hop: the caller's, and the platform store's when the hop uses it.
429
+ * Computed only when the hop has a distrust list to apply. */
430
+ function _hopAnchorKeys(distrust, anchorPems, useSystem) {
431
+ if (distrust === null || distrust.length === 0) return null;
432
+ var own = _anchorKeySet(anchorPems);
433
+ if (!useSystem) return own;
434
+ var sys = _systemAnchorKeySet();
435
+ var names = _objectKeys(sys.keys);
436
+ for (var i = 0; i < names.length; i++) own.keys[names[i]] = true;
437
+ return { keys: own.keys, entries: _concatList(own.entries, sys.entries) };
438
+ }
439
+
440
+ /** @internal The digest of a listed key on a branch through the trust store above `childX509`, or
441
+ * null. OpenSSL looks in the trust store for an issuer before it looks at what the server sent, so
442
+ * when the store holds a certificate that issues one on node's walk (an intermediate, or a
443
+ * cross-certificate), the session may have been verified up that branch instead. Every such branch
444
+ * is followed upward through the store, and a listed key anywhere on one refuses the session.
445
+ * `visited` bounds the search to one visit per anchor certificate. */
446
+ function _storeBranchListed(childX509, anchors, list, visited) {
447
+ for (var i = 0; i < anchors.entries.length; i++) {
448
+ var e = anchors.entries[i];
449
+ if (_arrIndexOf(visited, e) !== -1) continue;
450
+ var issues;
451
+ try { issues = _x509CheckIssued(childX509, e.x509) === true && _x509Verify(childX509, _x509PublicKey(e.x509)) === true; }
452
+ catch (_e) {
453
+ // allow:swallow-unverified a store certificate node cannot compare is not an issuer on any branch
454
+ issues = false;
455
+ }
456
+ if (!issues) continue;
457
+ _arrPush(visited, e);
458
+ if (guard.distrust.has(list, e.spki)) return e.spki;
459
+ var above = _storeBranchListed(e.x509, anchors, list, visited);
460
+ if (above !== null) return above;
461
+ }
462
+ return null;
463
+ }
464
+
465
+ /** @internal The first distrusted key on the chain the session verified, as `{ digest }`; null
466
+ * when none is listed; or `{ unproven: reason }` when the walk cannot stand for the verified chain.
467
+ *
468
+ * The walk follows the detailed peer certificate's `issuerCertificate` links, which node builds by
469
+ * name and key identifier from the certificates the server sent and then from the trust store,
470
+ * without checking a signature, a validity period or whether the top is trusted. OpenSSL may have
471
+ * verified the session through different certificates with the same names, so the walk is held to
472
+ * what makes it a path in its own right: each link is signed by the certificate above it, each
473
+ * certificate below the top is inside its validity window, each one that issues another is a CA,
474
+ * and the top is self-issued with the key of a configured trust anchor (`anchors.keys`). OpenSSL
475
+ * looks in the trust store for an issuer first, so where the store holds a certificate that issues
476
+ * one on the walk, the session may have been verified up the store's branch rather than the walk;
477
+ * every such branch is followed (`_storeBranchListed`) and a listed key on one refuses the session.
478
+ * Node gives no way to read the chain OpenSSL verified, so the check covers every chain the session
479
+ * could have been verified through, and may refuse a session whose verified chain was clean. The
480
+ * walk reads up to `LIMITS.PATH_MAX_CERTS` certificates. */
481
+ function _distrustedInChain(cert, list, anchors) {
482
+ var now = guard.time.instantOf(new _Date());
483
+ var seen = [];
484
+ var visitedStore = [];
485
+ var cur = cert;
486
+ for (;;) {
487
+ if (seen.length >= constants.LIMITS.PATH_MAX_CERTS) {
488
+ return { unproven: "the chain is longer than the " + constants.LIMITS.PATH_MAX_CERTS + " certificates the check reads" };
489
+ }
490
+ var spki;
491
+ try { spki = _peerSpki(cur.raw); }
492
+ catch (_pe) {
493
+ // allow:swallow-unverified a certificate whose key neither reader returns cannot be checked, so the walk is unproven
494
+ return { unproven: "the key of a certificate on the chain could not be read" };
495
+ }
496
+ if (guard.distrust.has(list, spki)) return _keyHit(spki);
497
+ var storeHit;
498
+ try { storeHit = _storeBranchListed(new _X509Certificate(cur.raw), anchors, list, visitedStore); }
499
+ catch (_se) {
500
+ // allow:swallow-unverified a certificate node cannot read leaves the store branches unchecked, so the walk is unproven
501
+ return { unproven: "a certificate on the chain could not be compared with the trust store" };
502
+ }
503
+ if (storeHit !== null) return _keyHit(storeHit);
504
+ var up = cur.issuerCertificate;
505
+ if (up === cur) {
506
+ return anchors.keys[guard.distrust.keyDigest(spki)] === true ? null
507
+ : { unproven: "the chain ends at a self-issued certificate whose key is not a configured trust anchor's" };
508
+ }
509
+ if (!_usableLink(cur.raw, seen.length > 0, now)) {
510
+ return { unproven: "a certificate on the chain is outside its validity period or issues without being a CA" };
511
+ }
512
+ _arrPush(seen, cur);
513
+ if (_arrIndexOf(seen, up) !== -1) return { unproven: "the chain loops back on a certificate it already passed" };
514
+ if (!up || !_isBuffer(up.raw) || !_signedBy(cur.raw, up.raw)) {
515
+ return { unproven: "a link of the chain is not signed by the certificate node places above it" };
516
+ }
517
+ cur = up;
518
+ }
519
+ }
520
+
521
+ /** @internal A refused key as `{ digest, compromised }`, where `compromised` is the reason
522
+ * `guard.distrust.compromised` gives, or null for a key refused only by the caller's list. */
523
+ function _keyHit(spki) {
524
+ return { digest: guard.distrust.digest(spki), compromised: guard.distrust.compromised(spki) };
525
+ }
526
+
527
+ function _distrustError(hit, hop) {
528
+ var e;
529
+ if (hit.compromised || hit.unchecked) {
530
+ e = new Error(hit.compromised
531
+ ? "the " + hop + " certificate chain carries a compromised key (SubjectPublicKeyInfo SHA-256 " + hit.digest + "): " + hit.compromised
532
+ : "the " + hop + " certificate chain could not be checked for a compromised key: " + hit.unchecked);
533
+ return guard.verdict.set(e, "pkiCompromisedKey", hop);
534
+ }
535
+ e = new Error(hit.digest !== undefined
536
+ ? "the " + hop + " certificate chain carries a key listed in tls.distrustedKeys (SubjectPublicKeyInfo SHA-256 " + hit.digest + ")"
537
+ : "the " + hop + " certificate chain could not be checked against tls.distrustedKeys: " + hit.unproven);
538
+ return guard.verdict.set(e, "pkiDistrustedKey", hop);
539
+ }
540
+
541
+ /** @internal An identity check that also refuses a chain carrying a compromised or distrusted key.
542
+ * With no list, the chain node walks is checked for a compromised key (`_compromisedOnWalk`); with a
543
+ * list, the walk is held to the proof `_distrustedInChain` describes, and a key on the list or a
544
+ * compromised key anywhere on it refuses the session. The chain is read before the identity check
545
+ * runs, because a caller's hook receives the same certificate object and can change it. The identity
546
+ * verdict is returned first, so a name mismatch is still reported as one. Node calls this without a
547
+ * `try`, so a hook that throws is returned as the refusal, and the refusal is built before the hook
548
+ * runs. Anything else in the body that throws, such as a read a changed prototype answers, returns a
549
+ * refusal built from string concatenation alone. */
550
+ function _withDistrust(check, list, hop, anchorKeys) {
551
+ var strict = list !== null && list.length > 0;
552
+ return function (host, cert) {
553
+ try {
554
+ var hit = strict ? _distrustedInChain(cert, list, anchorKeys) : _compromisedOnWalk(cert);
555
+ var refusal = hit === null ? undefined : _distrustError(hit, hop);
556
+ var err;
557
+ try { err = check(host, cert); }
558
+ catch (e) {
559
+ var thrown = new Error("the " + hop + " identity check (tls.checkServerIdentity) threw: " + guard.text.describeThrown(e));
560
+ return guard.verdict.set(thrown, "cause", e);
561
+ }
562
+ if (err) return err;
563
+ return refusal;
564
+ } catch (_e) {
565
+ // allow:swallow-unverified the throw is returned as a refusal of the session, which is the verdict a failed check has to give
566
+ return new Error("the " + hop + " certificate chain could not be checked");
567
+ }
568
+ };
569
+ }
570
+
281
571
  function _isBlockedIp(ip) {
282
572
  /** @internal The argument has to BE a string, and what cannot be classified is blocked. `isIP`
283
573
  * converts its argument, so a value whose conversion answers a loopback literal to the family
@@ -334,8 +624,7 @@ function _isBlockedIp(ip) {
334
624
 
335
625
  function _blockedAddrErr(hostname, address) {
336
626
  var e = new Error("refusing to connect to " + hostname + " -> " + address + " (private / loopback / link-local address blocked)");
337
- e.pkiBlockedAddress = true;
338
- return e;
627
+ return guard.verdict.set(e, "pkiBlockedAddress", true);
339
628
  }
340
629
  function _makeGuardedLookup(lookupFn) {
341
630
  return function guardedLookup(hostname, options, callback) {
@@ -389,6 +678,10 @@ function _hasWordBounded(l, word) {
389
678
  function _isLowerWordChar(c) { return (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95; }
390
679
  function _classifyError(e, C) {
391
680
  if (e && e.pkiBlockedAddress) return C("blocked-address");
681
+ if (e && e.pkiCompromisedKey === "proxy") return C("proxy-key-compromised");
682
+ if (e && e.pkiCompromisedKey === "server") return C("server-key-compromised");
683
+ if (e && e.pkiDistrustedKey === "proxy") return C("proxy-key-distrusted");
684
+ if (e && e.pkiDistrustedKey === "server") return C("server-key-distrusted");
392
685
  var s = _stringOf((e && e.code) || "") + " " + _stringOf((e && e.message) || "");
393
686
  var l = _toLowerCase(s);
394
687
  if (_strIndexOf(l, "protocol_version") !== -1 || _strIndexOf(l, "unsupported_protocol") !== -1 || _strIndexOf(l, "version_too_low") !== -1 ||
@@ -403,7 +696,11 @@ function _keySet(names) { var m = _objectCreate(null); for (var i = 0; i < names
403
696
  var PROXY_KEYS = _keySet(["url", "auth", "tls"]);
404
697
  var PROXY_AUTH_KEYS = _keySet(["scheme", "username", "password", "allowMD5", "allowLegacyQop"]);
405
698
  var PROXY_DIGEST_KNOBS = ["allowMD5", "allowLegacyQop"];
406
- var PROXY_TLS_KEYS = _keySet(["anchors", "useSystemStore", "servername", "minVersion"]);
699
+ var PROXY_TLS_KEYS = _keySet(["anchors", "useSystemStore", "servername", "minVersion", "distrustedKeys"]);
700
+ /** @internal The names a `tls` policy bag may carry. A name outside them is refused rather than
701
+ * ignored: a misspelled `distrustedKeys` or `anchors` would otherwise leave the connection under a
702
+ * policy the caller did not state. */
703
+ var TLS_KEYS = _keySet(["anchors", "useSystemStore", "cert", "key", "minVersion", "servername", "checkServerIdentity", "distrustedKeys"]);
407
704
 
408
705
  function _copyAnchor(a) { return (a !== null && typeof a === "object" && nodeUtil.types.isProxy(a)) ? a : (_isBuffer(a) ? _bufferFrom(a) : a); }
409
706
  function _noopProxyErr(code, message) { var e = new Error(message); e.code = code; e.isPkiError = true; return e; }
@@ -461,6 +758,12 @@ function snapshotProxy(p) {
461
758
  out.tls.anchors = copied;
462
759
  } else if (typeof a === "string") out.tls.anchors = a;
463
760
  else if (a !== null && typeof a === "object" && !nodeUtil.types.isProxy(a) && _isBuffer(a)) out.tls.anchors = _bufferFrom(a);
761
+ /** @internal The distrust list is copied for the reason the anchors are: the snapshot is taken
762
+ * so a caller's later edit cannot change the policy, and a shared array would carry the edit. */
763
+ var dk = p.tls.distrustedKeys;
764
+ if (dk !== undefined && dk !== null) {
765
+ out.tls.distrustedKeys = guard.distrust.keys(dk, _noopProxyErr, "transport/bad-proxy", "opts.proxy.tls.distrustedKeys");
766
+ }
464
767
  }
465
768
  return out;
466
769
  } catch (_e) {
@@ -472,7 +775,7 @@ function snapshotProxy(p) {
472
775
  /** @internal `originHost`, `statedPort` and `scheme` are the values the request path already read off
473
776
  * the parsed origin; they are passed in rather than read again here, so the authority this builds for
474
777
  * the CONNECT target is the same authority the socket options were built from. */
475
- function _validateProxy(request, defaults, E, C, originHost, originStatedPort, originScheme) {
778
+ function _validateProxy(request, defaults, E, C, originHost, originStatedPort, originScheme, originDistrust) {
476
779
  var rawProxy = request.proxy;
477
780
  var proxy = rawProxy !== undefined ? rawProxy : defaults.proxy;
478
781
  if (proxy === undefined || proxy === null) return null;
@@ -542,9 +845,23 @@ function _validateProxy(request, defaults, E, C, originHost, originStatedPort, o
542
845
  * consult `Symbol.isConcatSpreadable` on each argument, so capturing the method leaves the
543
846
  * SPREADING replaceable and an inherited hook replaced a validated anchor with another one in
544
847
  * the CONNECT options. `concatList` copies own indexes and appends own indexes. */
545
- if (pHasAnchors) pCa = _concatList(pCa, _pemifyAnchors(pAnchors));
848
+ if (pHasAnchors) {
849
+ var pPems = _pemifyAnchors(pAnchors);
850
+ _refuseCompromisedAnchors(pPems, E, C("bad-proxy"), "opts.proxy.tls.anchors");
851
+ pCa = _concatList(pCa, pPems);
852
+ }
546
853
  if (pUseSystem) pCa = _concatList(pCa, _systemCa());
547
- proxyTls = { ca: pCa.length ? pCa : undefined, minVersion: pMinVersion, servername: pServername };
854
+ if (pCa.length === 0) {
855
+ throw E(C("no-trust-anchors"), "proxy.tls.useSystemStore is set and the platform certificate store returned no certificates, " +
856
+ "so the proxy cannot be authenticated; set proxy.tls.anchors");
857
+ }
858
+ /** @internal A key the caller distrusts is refused on this hop too, whether it was listed for
859
+ * the proxy or for the origin. */
860
+ var pDistrust = guard.distrust.union(
861
+ guard.distrust.keys(ptls ? ptls.distrustedKeys : undefined, E, C("bad-proxy"), "opts.proxy.tls.distrustedKeys"),
862
+ originDistrust);
863
+ proxyTls = { ca: pCa, minVersion: pMinVersion, servername: pServername, distrust: pDistrust,
864
+ anchorKeys: _hopAnchorKeys(pDistrust, pHasAnchors ? _pemifyAnchors(pAnchors) : [], pUseSystem) };
548
865
  }
549
866
 
550
867
  var originAuthHost = _isIP(originHost) === 6 ? "[" + originHost + "]" : originHost;
@@ -587,6 +904,8 @@ function _digestProxyCredential(headers, pinfo, C, E) {
587
904
  }, E);
588
905
  }
589
906
  function _proxyErrorClass(e, C) {
907
+ if (e && e.pkiCompromisedKey === "proxy") return C("proxy-key-compromised");
908
+ if (e && e.pkiDistrustedKey === "proxy") return C("proxy-key-distrusted");
590
909
  var code = _stringOf((e && e.code) || "");
591
910
  if (_strIndexOf(code, "ERR_TLS") === 0 || _strIndexOf(code, "ERR_SSL") === 0 || _strIndexOf(code, "SSL") !== -1 ||
592
911
  _strIndexOf(code, "CERT") !== -1 || _strIndexOf(code, "SELF_SIGNED") !== -1 || _strIndexOf(code, "VERIFY") !== -1 ||
@@ -605,28 +924,47 @@ function _proxyErrorClass(e, C) {
605
924
  *
606
925
  * Build a fail-closed `node:https` transport: `transport(request) -> Promise<{ status,
607
926
  * headers, body }>`. `defaults` binds a `tls` policy (`anchors` -> the node `ca`;
608
- * `useSystemStore` to opt into the bundled roots; `cert`/`key` for mutual TLS;
927
+ * `useSystemStore` to opt into the platform certificate store; `distrustedKeys` to refuse
928
+ * listed keys; `cert`/`key` for mutual TLS;
609
929
  * `minVersion` 'TLSv1.2' (default) or 'TLSv1.3'; `servername`; a `checkServerIdentity`
610
930
  * that may only tighten) plus `timeout` and `maxResponseBytes` budgets. Each `request`
611
- * ({ method, url, headers, body, tls, timeout, maxResponseBytes }) may override them.
931
+ * ({ method, url, headers, body, tls, timeout, maxResponseBytes }) may override them,
932
+ * except `distrustedKeys`: the request's list is added to the default's.
612
933
  * 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
934
+ * anchor nor `useSystemStore`, or with `useSystemStore` and an empty platform store
935
+ * (`transport/no-trust-anchors`), a body over the streaming
614
936
  * 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.
937
+ * -floor negotiation (`transport/tls-floor`), a failed server authentication
938
+ * (`transport/server-auth-failed`), a chain carrying a key whose private key is public
939
+ * (`transport/server-key-compromised`), or a verified chain carrying a distrusted key
940
+ * (`transport/server-key-distrusted`) all fail closed; `rejectUnauthorized` is always on.
941
+ * The compromised-key check runs on every connection and hop with no option to turn it off: a
942
+ * certificate on the chain node reports whose key is on `pki.trust.knownCompromisedKeys()`, or is an
943
+ * elliptic curve key whose private key is an integer below 256, refuses the server
944
+ * (`transport/proxy-key-compromised` for a proxy). That chain is the one node builds from the
945
+ * certificates the server sent and the trust store, and a link of it is not proven; set
946
+ * `tls.distrustedKeys` for the proven walk described below, which applies the same compromised keys.
947
+ * A chain longer than `C.LIMITS.PATH_MAX_CERTS` is refused with the same code, and the chain is read
948
+ * before a `tls.checkServerIdentity` hook runs. A hook that throws refuses the server with
949
+ * `transport/server-auth-failed`.
950
+ * A `tls.anchors` or `proxy.tls.anchors` certificate whose key is compromised is refused with
951
+ * `transport/bad-input` or `transport/bad-proxy` before any connection, and under `useSystemStore`
952
+ * such a certificate in the platform store is left out of the certificates the connection trusts.
617
953
  * A protocol client passes its own error factory (`defaults.E`) + `defaults.errPrefix`
618
954
  * to surface domain codes (`est/...`). The transport owns no HTTP/redirect/auth
619
955
  * semantics; those live in the message layer that consumes the response triple.
620
956
  *
621
957
  * @opts
622
958
  * - `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).
959
+ * - `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.
960
+ * - `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.
961
+ * - 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
962
  * - `tls.cert` / `tls.key` -- client certificate + key for mutual-TLS re-enrollment.
625
963
  * - `tls.minVersion` -- 'TLSv1.2' (default) or 'TLSv1.3'; never below the floor.
626
964
  * - `tls.servername` / `tls.checkServerIdentity` -- SNI + RFC 6125 identity; may tighten, never disable.
627
965
  * - `timeout` -- ms (default C.TIME.seconds(30)); `maxResponseBytes` -- default LIMITS.HTTP_MAX_RESPONSE_BYTES, tightenable downward only.
628
966
  * - `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).
967
+ * - `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
968
  * @example
631
969
  * var t = pki.transport.https({ tls: { anchors: [caPem] } });
632
970
  * var res = await t({ method: "GET", url: "https://ca.example/.well-known/est/cacerts" });
@@ -645,6 +983,12 @@ function httpsTransport(defaults) {
645
983
  * the settings read from this record is the opt-in that turns away an unpinned server. */
646
984
  var rawDefaultTls = defaults.tls;
647
985
  var tlsDefaults = rawDefaultTls != null ? rawDefaultTls : intrinsic.create(null);
986
+ if (rawDefaultTls != null) {
987
+ guard.identifier.assertKnownKeys(tlsDefaults, TLS_KEYS, E, C("bad-input"), "the transport default tls has an unknown option ");
988
+ }
989
+ /** @internal The default distrust list is read and copied once, here, so editing the caller's
990
+ * array after the transport is built changes nothing. */
991
+ var defaultDistrust = guard.distrust.keys(tlsDefaults.distrustedKeys, E, C("bad-input"), "the transport default tls.distrustedKeys");
648
992
 
649
993
  function _prepare(request) {
650
994
  var url;
@@ -674,6 +1018,7 @@ function httpsTransport(defaults) {
674
1018
  * public verb instead of this module's typed refusal. */
675
1019
  var rawReqTls = request.tls;
676
1020
  var reqTls = rawReqTls != null ? rawReqTls : intrinsic.create(null);
1021
+ if (rawReqTls != null) guard.identifier.assertKnownKeys(reqTls, TLS_KEYS, E, C("bad-input"), "tls has an unknown option ");
677
1022
  var rawAnchors = reqTls.anchors;
678
1023
  var anchors = rawAnchors !== undefined ? rawAnchors : tlsDefaults.anchors;
679
1024
  var rawUseSystem = reqTls.useSystemStore;
@@ -686,6 +1031,20 @@ function httpsTransport(defaults) {
686
1031
  if (scheme === "https:" && !hasAnchors && !useSystem) {
687
1032
  throw E(C("no-trust-anchors"), "no explicit trust anchor and useSystemStore not set -- refusing an unpinned server (RFC 7030 sec. 3.6)");
688
1033
  }
1034
+ /** @internal The platform store is read here so an empty one is refused before any socket
1035
+ * opens. A connection given an empty `ca` list falls back to Node's default store, which is
1036
+ * Node's compiled-in set plus NODE_EXTRA_CA_CERTS, and that is not the store the caller asked
1037
+ * for. */
1038
+ var systemCa = useSystem ? _systemCa() : [];
1039
+ if (scheme === "https:" && !hasAnchors && systemCa.length === 0) {
1040
+ throw E(C("no-trust-anchors"), "useSystemStore is set and the platform certificate store returned no certificates, " +
1041
+ "so the server cannot be authenticated; set tls.anchors, or install the host's CA bundle");
1042
+ }
1043
+ /** @internal The transport's list and the request's are combined, so a request cannot drop a
1044
+ * key the transport was built to refuse by naming a shorter list. */
1045
+ var rawDistrust = reqTls.distrustedKeys;
1046
+ var distrust = guard.distrust.union(defaultDistrust,
1047
+ guard.distrust.keys(rawDistrust, E, C("bad-input"), "tls.distrustedKeys"));
689
1048
 
690
1049
  var rawTimeout = request.timeout;
691
1050
  var timeout = _budget(rawTimeout !== undefined ? rawTimeout : defaults.timeout, "timeout", DEFAULT_TIMEOUT, MAX_TIMEOUT, E, C("bad-input"));
@@ -711,10 +1070,14 @@ function httpsTransport(defaults) {
711
1070
  var host = _stripIpv6Brackets(_urlHostname(url));
712
1071
  var plaintext = scheme === "http:";
713
1072
  var statedPort = _urlPort(url);
1073
+ /** @internal `defaultPort` is what node compares the port with when it writes the Host header.
1074
+ * With no agent there is no default to compare with, so node appends the port every time and a
1075
+ * URL with no port is sent as `Host: name:80`. Some servers answer that with an error. */
714
1076
  var options = {
715
1077
  method: request.method || "GET",
716
1078
  hostname: host,
717
1079
  port: statedPort || (plaintext ? 80 : 443),
1080
+ defaultPort: plaintext ? 80 : 443,
718
1081
  path: _urlPathname(url) + _urlSearch(url),
719
1082
  headers: reqHeaders,
720
1083
  minVersion: minVersion,
@@ -746,17 +1109,22 @@ function httpsTransport(defaults) {
746
1109
  if (typeof sni !== "string") throw E(C("bad-input"), "tls.servername must be a string, got " + guard.text.showValue(sni));
747
1110
  if (sni && !_isIP(sni)) options.servername = sni;
748
1111
  var caList = [];
749
- if (hasAnchors) caList = _concatList(caList, _pemifyAnchors(anchors));
750
- if (useSystem) caList = _concatList(caList, _systemCa());
1112
+ if (hasAnchors) {
1113
+ var anchorPems = _pemifyAnchors(anchors);
1114
+ _refuseCompromisedAnchors(anchorPems, E, C("bad-input"), "tls.anchors");
1115
+ caList = _concatList(caList, anchorPems);
1116
+ }
1117
+ if (useSystem) caList = _concatList(caList, systemCa);
751
1118
  if (caList.length) options.ca = caList;
752
1119
  if (cert) options.cert = cert;
753
1120
  if (key) options.key = key;
1121
+ var anchorKeys = _hopAnchorKeys(distrust, hasAnchors ? _pemifyAnchors(anchors) : [], useSystem);
754
1122
  if (callerCsi) {
755
- options.checkServerIdentity = function (host, cert2) {
1123
+ options.checkServerIdentity = _withDistrust(function (host, cert2) {
756
1124
  var baseErr = _checkServerIdentity(host, cert2);
757
1125
  if (baseErr) return baseErr;
758
1126
  return callerCsi(host, cert2);
759
- };
1127
+ }, distrust, "server", anchorKeys);
760
1128
  } else {
761
1129
  /** @internal With no caller hook, the option is set to the CAPTURE rather than left out. Left
762
1130
  * out, node's own TLS stack looks the function up on its module export when the handshake
@@ -764,15 +1132,15 @@ function httpsTransport(defaults) {
764
1132
  * another host is accepted. Measured: with this absent, a request asking for a name the
765
1133
  * server's certificate does not carry was admitted. The function is node's own default, taken
766
1134
  * at load, so the verdict is the one it would have reached anyway. */
767
- options.checkServerIdentity = _checkServerIdentity;
1135
+ options.checkServerIdentity = _withDistrust(_checkServerIdentity, distrust, "server", anchorKeys);
768
1136
  }
769
1137
  var proxyInfo;
770
- try { proxyInfo = _validateProxy(request, defaults, E, C, host, statedPort, scheme); }
1138
+ try { proxyInfo = _validateProxy(request, defaults, E, C, host, statedPort, scheme, distrust); }
771
1139
  catch (e) {
772
1140
  var typedProxyError = false;
773
1141
  try {
774
1142
  var ec = (e instanceof Error) && !nodeUtil.types.isProxy(e) ? e.code : undefined;
775
- typedProxyError = ec === C("bad-proxy") || ec === C("proxy-auth-requires-tls");
1143
+ typedProxyError = ec === C("bad-proxy") || ec === C("proxy-auth-requires-tls") || ec === C("no-trust-anchors");
776
1144
  } catch (_ie) { /* allow:swallow-unverified reading .code on a hostile error object can throw; treat any such failure as untyped */ }
777
1145
  if (typedProxyError) throw e;
778
1146
  throw E(C("bad-proxy"), "opts.proxy could not be validated", e);
@@ -785,7 +1153,7 @@ function httpsTransport(defaults) {
785
1153
  options.lookup = _guardedLookup;
786
1154
  }
787
1155
 
788
- return { options: options, timeout: timeout, maxBytes: maxBytes, body: body, proxy: proxyInfo, blockPrivate: blockPrivate, callerCsi: callerCsi, plaintext: plaintext };
1156
+ return { options: options, timeout: timeout, maxBytes: maxBytes, body: body, proxy: proxyInfo, blockPrivate: blockPrivate, callerCsi: callerCsi, plaintext: plaintext, distrust: distrust, anchorKeys: anchorKeys };
789
1157
  }
790
1158
 
791
1159
  var _transportFn = function transport(request) {
@@ -833,7 +1201,7 @@ function httpsTransport(defaults) {
833
1201
  * options are the ones node computed for this hop, forwarded unchanged, so every TLS
834
1202
  * setting the request carries still applies; only the function that opens the socket is
835
1203
  * this module's. The tunnel path already supplies its own and keeps it. */
836
- if (options.createConnection === undefined) {
1204
+ if (options.createConnection == null) {
837
1205
  options.createConnection = prep.plaintext
838
1206
  ? function (opts) { return intrinsic.apply(_netConnect, nodeNet, [opts]); }
839
1207
  : function (opts) { return intrinsic.apply(_tlsConnect, nodeTls, [opts]); };
@@ -949,7 +1317,8 @@ function httpsTransport(defaults) {
949
1317
  connectOpts.minVersion = pinfo.proxyTls.minVersion;
950
1318
  var pIdentity = pinfo.proxyTls.servername || pinfo.host;
951
1319
  connectOpts.servername = (pIdentity && !_isIP(pIdentity)) ? pIdentity : "";
952
- connectOpts.checkServerIdentity = function (host, cert) { return _checkServerIdentity(pIdentity, cert); };
1320
+ connectOpts.checkServerIdentity = _withDistrust(function (host, cert) { return _checkServerIdentity(pIdentity, cert); },
1321
+ pinfo.proxyTls.distrust, "proxy", pinfo.proxyTls.anchorKeys);
953
1322
  connectOpts.createConnection = function (opts) { return intrinsic.apply(_tlsConnect, nodeTls, [opts]); };
954
1323
  cr = intrinsic.apply(_httpsRequest, nodeHttps, [connectOpts]);
955
1324
  } else {
@@ -997,11 +1366,11 @@ function httpsTransport(defaults) {
997
1366
  ca: prep.options.ca,
998
1367
  minVersion: prep.options.minVersion,
999
1368
  rejectUnauthorized: true,
1000
- checkServerIdentity: function (host, cert) {
1369
+ checkServerIdentity: _withDistrust(function (host, cert) {
1001
1370
  var baseErr = _checkServerIdentity(idHost, cert);
1002
1371
  if (baseErr) return baseErr;
1003
1372
  return prep.callerCsi ? prep.callerCsi(idHost, cert) : undefined;
1004
- },
1373
+ }, prep.distrust, "server", prep.anchorKeys),
1005
1374
  cert: prep.options.cert,
1006
1375
  key: prep.options.key,
1007
1376
  }]);
@@ -1060,12 +1429,12 @@ function httpsTransport(defaults) {
1060
1429
  host: o.hostname, port: o.port, minVersion: o.minVersion,
1061
1430
  rejectUnauthorized: o.rejectUnauthorized,
1062
1431
  };
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;
1432
+ if (o.servername != null) connectOpts.servername = o.servername;
1433
+ if (o.ca != null) connectOpts.ca = o.ca;
1434
+ if (o.cert != null) connectOpts.cert = o.cert;
1435
+ if (o.key != null) connectOpts.key = o.key;
1436
+ if (o.checkServerIdentity != null) connectOpts.checkServerIdentity = o.checkServerIdentity;
1437
+ if (o.lookup != null) connectOpts.lookup = o.lookup;
1069
1438
 
1070
1439
  return new _Promise(function (resolve, reject) {
1071
1440
  var settled = false;
@@ -1134,7 +1503,8 @@ function httpsTransport(defaults) {
1134
1503
  * one. A request with neither an explicit anchor nor `useSystemStore` is refused
1135
1504
  * (`transport/no-trust-anchors`) before any socket is opened, and an endpoint whose chain does
1136
1505
  * 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`)
1506
+ * (`transport/server-auth-failed`). A chain carrying a key listed in `tls.distrustedKeys` is
1507
+ * refused (`transport/server-key-distrusted`). A configured proxy is refused (`transport/bad-proxy`)
1138
1508
  * rather than bypassed, since connecting directly would reach an origin the caller asked to
1139
1509
  * reach through the proxy.
1140
1510
  *
@@ -1151,4 +1521,5 @@ function httpsTransport(defaults) {
1151
1521
  */
1152
1522
  function peerChain(request, defaults) { return httpsTransport(defaults || intrinsic.create(null)).peerChain(request); }
1153
1523
 
1154
- module.exports = { https: httpsTransport, peerChain: peerChain, isBlockedIp: _isBlockedIp, _makeGuardedLookup: _makeGuardedLookup, MAX_TIMEOUT: MAX_TIMEOUT, snapshotProxy: snapshotProxy };
1524
+ module.exports = { https: httpsTransport, peerChain: peerChain, isBlockedIp: _isBlockedIp, _makeGuardedLookup: _makeGuardedLookup, MAX_TIMEOUT: MAX_TIMEOUT, snapshotProxy: snapshotProxy,
1525
+ TLS_KEYS: intrinsic.freeze(TLS_KEYS) };