@blamejs/pki 0.9.6 → 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.
@@ -9,6 +9,7 @@ var constants = require("./constants");
9
9
  var gBytes = require("./guard-bytes");
10
10
  var gList = require("./guard-list");
11
11
  var gCrypto = require("./guard-crypto");
12
+ var compromisedKeys = require("./compromised-keys");
12
13
 
13
14
  var _freeze = intrinsic.freeze;
14
15
  var _isArray = intrinsic.isArray;
@@ -182,17 +183,119 @@ function keyDigest(spkiBytes) {
182
183
  return digest(canonical !== null ? canonical : spkiBytes);
183
184
  }
184
185
 
185
- /** Whether the key `spkiBytes` names is on `list`, a value `keys` returned. A `null` or empty list
186
- * answers `false`. The digest of the bytes as given is compared, and so is `keyDigest`, so an entry
187
- * `keyDigest` computed matches the key however a certificate encodes it (a compressed EC point, RSA
188
- * parameters without NULL), and an entry computed over raw bytes matches a certificate with the same
189
- * bytes.
186
+ /** @internal The built-in list, keyed by digest. */
187
+ var COMPROMISED = _create(null);
188
+ for (var _ci = 0; _ci < compromisedKeys.KEYS.length; _ci++) COMPROMISED[compromisedKeys.KEYS[_ci].spkiSha256] = compromisedKeys.KEYS[_ci];
189
+
190
+ /** @internal The curves the small-scalar table is built for, by JWK curve name, with the width of a
191
+ * coordinate in bytes, and the scalar bound. Each table holds the x-coordinates of d*G for d from 1
192
+ * to the bound minus one, built once per curve on first use (measured on Node 26: 4 ms for P-256,
193
+ * 138 ms for P-384, 321 ms for P-521). The points d*G and (n-d)*G share an x-coordinate, so one
194
+ * comparison covers both. */
195
+ var SMALL_SCALAR_CURVES = intrinsic.assign(_create(null), {
196
+ "P-256": { name: "prime256v1", width: 32 },
197
+ "P-384": { name: "secp384r1", width: 48 },
198
+ "P-521": { name: "secp521r1", width: 66 },
199
+ "secp256k1": { name: "secp256k1", width: 32 },
200
+ });
201
+ var SMALL_SCALAR_BOUND = 256;
202
+ var _smallScalarTables = _create(null);
203
+ function _smallScalarTable(curve) {
204
+ if (_smallScalarTables[curve.name]) return _smallScalarTables[curve.name];
205
+ var table = _create(null);
206
+ var scalar = intrinsic.bufferAlloc(curve.width);
207
+ for (var d = 1; d < SMALL_SCALAR_BOUND; d++) {
208
+ scalar[curve.width - 1] = d;
209
+ table[_bufToString(gCrypto.ecPointFromScalar(curve.name, scalar), "hex", 1, 1 + curve.width)] = true;
210
+ }
211
+ _smallScalarTables[curve.name] = table;
212
+ return table;
213
+ }
214
+
215
+ /** The curve name of an elliptic curve key whose private scalar is below 256, or below 256 short of
216
+ * the group order, or `null` for any other key. Anyone can recover such a private key from the public
217
+ * key by trying each candidate. The key is read through node's own import after an id-ecDH or
218
+ * id-ecMQV identifier is read as ecPublicKey, so a named or explicit curve and an uncompressed,
219
+ * compressed or hybrid point are all judged; a point that is not on the curve is not a key and
220
+ * answers `null`. The covered curves are P-256, P-384, P-521 and secp256k1.
221
+ *
222
+ * @enforced-by behavioral -- the defect is a curve or an encoding the table does not reach, which has
223
+ * no code shape; the guard is the per-curve and per-encoding vectors in guard-distrust.test.js and
224
+ * lint-key-compromise.test.js.
225
+ */
226
+ function smallEcScalar(spkiBytes) {
227
+ var jwk;
228
+ try {
229
+ var spki = _unrestricted(spkiBytes, _codec());
230
+ var key = gCrypto.publicKey({ key: spki, format: "der", type: "spki" });
231
+ if (gCrypto.keyTypeOf(key) !== "ec") return null;
232
+ jwk = gCrypto.exportKey(key, { format: "jwk" });
233
+ } catch (_e) {
234
+ // allow:swallow-unverified a key node does not import as an elliptic curve point has no scalar to judge
235
+ return null;
236
+ }
237
+ var curve = typeof jwk.crv === "string" && _hasOwn(SMALL_SCALAR_CURVES, jwk.crv) ? SMALL_SCALAR_CURVES[jwk.crv] : null;
238
+ if (curve === null || typeof jwk.x !== "string") return null;
239
+ return _smallScalarTable(curve)[_bufToString(intrinsic.bufferFrom(jwk.x, "base64url"), "hex")] === true ? curve.name : null;
240
+ }
241
+
242
+ /** Why the private key of `spkiBytes` is public, as one sentence, or `null`. A key is compromised
243
+ * when its digest, as given or as `keyDigest` computes it, is on the list `pki.trust.knownCompromisedKeys()`
244
+ * returns, or when `smallEcScalar` answers a curve for it. Every verifier in this toolkit refuses such
245
+ * a key, with no option that turns the refusal off.
246
+ *
247
+ * @enforced-by behavioral -- the defect is a door that does not refuse, which has no rename-proof code
248
+ * shape; every door reaches this through `has`, and the guard is the compromised-key vector each verb
249
+ * carries.
250
+ */
251
+ function compromised(spkiBytes) {
252
+ if (spkiBytes === null || spkiBytes === undefined) return null;
253
+ var entry = COMPROMISED[digest(spkiBytes)] || COMPROMISED[keyDigest(spkiBytes)];
254
+ if (entry) return "the key is the published key of " + entry.name + ": " + entry.reason;
255
+ var curve = smallEcScalar(spkiBytes);
256
+ if (curve !== null) return "the " + curve + " private key is an integer below " + SMALL_SCALAR_BOUND + ", so anyone can recover it from the public key";
257
+ return null;
258
+ }
259
+
260
+ /** Throws `E(code, ...)` when `compromised(spkiBytes)` answers, naming the key by `label` and saying
261
+ * why its private key is public. A verb that certifies a key, signs with one or requests a
262
+ * certificate for one calls this on each key it is handed.
263
+ *
264
+ * @enforced-by behavioral -- the defect is a signing verb that does not call this, which has no
265
+ * rename-proof code shape; the guard is the compromised-key vector each signing verb carries.
266
+ */
267
+ function refuseCompromised(spkiBytes, E, code, label) {
268
+ var why = compromised(spkiBytes);
269
+ if (why !== null) throw E(code, label + " is a compromised key: " + why);
270
+ }
271
+
272
+ /** Why the key `spkiBytes` names is refused, as the end of a sentence about the key, or `null` when
273
+ * `has(list, spkiBytes)` is false: "is a compromised key: " and the `compromised` reason, or "is
274
+ * listed in " and `label`, the option the caller's list came from.
275
+ *
276
+ * @enforced-by behavioral -- a message has no rename-proof code shape; the guard is the vectors that
277
+ * assert each door's message names a compromised key as one and a listed key as listed.
278
+ */
279
+ function refusal(list, spkiBytes, label) {
280
+ var why = compromised(spkiBytes);
281
+ if (why !== null) return "is a compromised key: " + why;
282
+ return has(list, spkiBytes) ? "is listed in " + label : null;
283
+ }
284
+
285
+ /** Whether the key `spkiBytes` names is refused: it is compromised (see `compromised`), or it is on
286
+ * `list`, a value `keys` returned. A `null` or empty list still refuses a compromised key. The digest
287
+ * of the bytes as given is compared with the list, and so is `keyDigest`, so an entry `keyDigest`
288
+ * computed matches the key however a certificate encodes it (a compressed EC point, RSA parameters
289
+ * without NULL), and an entry computed over raw bytes matches a certificate with the same bytes.
190
290
  *
191
291
  * @enforced-by behavioral -- membership has no rename-proof code shape; the RED vectors are the
192
- * listed-key refusals at each verb, and the re-encodings in guard-distrust.test.js in both directions.
292
+ * listed-key and compromised-key refusals at each verb, and the re-encodings in guard-distrust.test.js
293
+ * in both directions.
193
294
  */
194
295
  function has(list, spkiBytes) {
195
- if (list === null || list === undefined || list.length === 0 || spkiBytes === null || spkiBytes === undefined) return false;
296
+ if (spkiBytes === null || spkiBytes === undefined) return false;
297
+ if (compromised(spkiBytes) !== null) return true;
298
+ if (list === null || list === undefined || list.length === 0) return false;
196
299
  if (gList.contains(list, digest(spkiBytes))) return true;
197
300
  return gList.contains(list, keyDigest(spkiBytes));
198
301
  }
@@ -221,4 +324,21 @@ function union(a, b) {
221
324
  return _freeze(out);
222
325
  }
223
326
 
224
- module.exports = _freeze({ keys: keys, digest: digest, keyDigest: keyDigest, has: has, union: union });
327
+ /** A copy of the built-in list: one `{ spkiSha256, name, reason, sources }` record per key, each
328
+ * frozen, in a new array the caller may change.
329
+ *
330
+ * @enforced-by behavioral -- a copy has no rename-proof code shape; the guard is the vector that
331
+ * changes the returned array and reads the list again.
332
+ */
333
+ function knownCompromised() {
334
+ var out = [];
335
+ for (var i = 0; i < compromisedKeys.KEYS.length; i++) gList.append(out, compromisedKeys.KEYS[i]);
336
+ return out;
337
+ }
338
+
339
+ module.exports = _freeze({
340
+ keys: keys, digest: digest, keyDigest: keyDigest, has: has, union: union,
341
+ compromised: compromised, refuseCompromised: refuseCompromised, refusal: refusal,
342
+ smallEcScalar: smallEcScalar, knownCompromised: knownCompromised,
343
+ SMALL_SCALAR_BOUND: SMALL_SCALAR_BOUND,
344
+ });
@@ -9,6 +9,19 @@ var _defineProperty = require("./guard-intrinsic").defineProperty;
9
9
  var _getOwnPropertyDescriptor = require("./guard-intrinsic").getOwnPropertyDescriptor;
10
10
  var _hasOwn = require("./guard-intrinsic").hasOwn;
11
11
  var _Object = require("./guard-intrinsic").Object;
12
+ var _create = require("./guard-intrinsic").create;
13
+
14
+ /** @internal A writable, configurable data descriptor with no prototype. A descriptor written as an
15
+ * object literal inherits from `Object.prototype`, and `defineProperty` reads `get` and `set` through
16
+ * that chain, so an inherited `get` beside the `value` made every define here throw. */
17
+ function _data(value, enumerable) {
18
+ var d = _create(null);
19
+ d.value = value;
20
+ d.enumerable = enumerable;
21
+ d.configurable = true;
22
+ d.writable = true;
23
+ return d;
24
+ }
12
25
 
13
26
  function _copyOwn(out, src) {
14
27
  var keys = _keys(src);
@@ -16,7 +29,7 @@ function _copyOwn(out, src) {
16
29
  var d = _getOwnPropertyDescriptor(src, keys[i]);
17
30
  if (!d) continue;
18
31
  var value = _hasOwn(d, "value") ? d.value : src[keys[i]];
19
- _defineProperty(out, keys[i], { value: value, enumerable: true, configurable: true, writable: true });
32
+ _defineProperty(out, keys[i], _data(value, true));
20
33
  }
21
34
  }
22
35
 
@@ -49,7 +62,7 @@ function shield(target) {
49
62
  * machinery looks, and reading it to decide would be the one read that does not count. */
50
63
  if (d && _hasOwn(d, "value") && typeof d.value !== "function") return target;
51
64
  }
52
- _defineProperty(target, "then", { value: undefined, enumerable: false, configurable: true, writable: true });
65
+ _defineProperty(target, "then", _data(undefined, false));
53
66
  return target;
54
67
  }
55
68
 
@@ -68,7 +81,7 @@ function carries(obj, key) {
68
81
 
69
82
  // @enforced-by behavioral -- for a target that cannot be rebuilt, an Error carrying a field a
70
83
  function set(target, key, value) {
71
- _defineProperty(target, key, { value: value, enumerable: true, configurable: true, writable: true });
84
+ _defineProperty(target, key, _data(value, true));
72
85
  /** @internal The target leaves here carrying the sentinel as well. A field set on an object is the
73
86
  * same kind of value a verdict is, and a target that reached a promise resolution without one would
74
87
  * hand the lookup to Object.prototype however it was assembled. */
@@ -295,10 +295,60 @@ function _systemCa() {
295
295
  // allow:swallow-unverified an unreadable platform store answers empty, and an empty list is refused by every caller
296
296
  try { var c = intrinsic.apply(_getCACertificates, nodeTls, ["system"]); if (_isArray(c)) out = _concatList(out, c); } catch (_e) { }
297
297
  }
298
- _systemCaCache = out;
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]);
312
+ }
299
313
  return out;
300
314
  }
301
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
+
302
352
  /** @internal The DER SubjectPublicKeyInfo of a certificate the TLS layer verified. The toolkit's
303
353
  * parser is reached through an inline require because of a circular load (schema-x509 ->
304
354
  * schema-pkix -> ct -> this module). A certificate the strict parser refuses was still accepted by
@@ -405,7 +455,7 @@ function _storeBranchListed(childX509, anchors, list, visited) {
405
455
  }
406
456
  if (!issues) continue;
407
457
  _arrPush(visited, e);
408
- if (guard.distrust.has(list, e.spki)) return guard.distrust.digest(e.spki);
458
+ if (guard.distrust.has(list, e.spki)) return e.spki;
409
459
  var above = _storeBranchListed(e.x509, anchors, list, visited);
410
460
  if (above !== null) return above;
411
461
  }
@@ -437,15 +487,20 @@ function _distrustedInChain(cert, list, anchors) {
437
487
  if (seen.length >= constants.LIMITS.PATH_MAX_CERTS) {
438
488
  return { unproven: "the chain is longer than the " + constants.LIMITS.PATH_MAX_CERTS + " certificates the check reads" };
439
489
  }
440
- var spki = _peerSpki(cur.raw);
441
- if (guard.distrust.has(list, spki)) return { digest: guard.distrust.digest(spki) };
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);
442
497
  var storeHit;
443
498
  try { storeHit = _storeBranchListed(new _X509Certificate(cur.raw), anchors, list, visitedStore); }
444
499
  catch (_se) {
445
500
  // allow:swallow-unverified a certificate node cannot read leaves the store branches unchecked, so the walk is unproven
446
501
  return { unproven: "a certificate on the chain could not be compared with the trust store" };
447
502
  }
448
- if (storeHit !== null) return { digest: storeHit };
503
+ if (storeHit !== null) return _keyHit(storeHit);
449
504
  var up = cur.issuerCertificate;
450
505
  if (up === cur) {
451
506
  return anchors.keys[guard.distrust.keyDigest(spki)] === true ? null
@@ -463,23 +518,53 @@ function _distrustedInChain(cert, list, anchors) {
463
518
  }
464
519
  }
465
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
+
466
527
  function _distrustError(hit, hop) {
467
- var e = new Error(hit.digest !== undefined
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
468
536
  ? "the " + hop + " certificate chain carries a key listed in tls.distrustedKeys (SubjectPublicKeyInfo SHA-256 " + hit.digest + ")"
469
537
  : "the " + hop + " certificate chain could not be checked against tls.distrustedKeys: " + hit.unproven);
470
- e.pkiDistrustedKey = hop;
471
- return e;
538
+ return guard.verdict.set(e, "pkiDistrustedKey", hop);
472
539
  }
473
540
 
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. */
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. */
476
550
  function _withDistrust(check, list, hop, anchorKeys) {
477
- if (list === null || list.length === 0) return check;
551
+ var strict = list !== null && list.length > 0;
478
552
  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);
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
+ }
483
568
  };
484
569
  }
485
570
 
@@ -539,8 +624,7 @@ function _isBlockedIp(ip) {
539
624
 
540
625
  function _blockedAddrErr(hostname, address) {
541
626
  var e = new Error("refusing to connect to " + hostname + " -> " + address + " (private / loopback / link-local address blocked)");
542
- e.pkiBlockedAddress = true;
543
- return e;
627
+ return guard.verdict.set(e, "pkiBlockedAddress", true);
544
628
  }
545
629
  function _makeGuardedLookup(lookupFn) {
546
630
  return function guardedLookup(hostname, options, callback) {
@@ -594,6 +678,8 @@ function _hasWordBounded(l, word) {
594
678
  function _isLowerWordChar(c) { return (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95; }
595
679
  function _classifyError(e, C) {
596
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");
597
683
  if (e && e.pkiDistrustedKey === "proxy") return C("proxy-key-distrusted");
598
684
  if (e && e.pkiDistrustedKey === "server") return C("server-key-distrusted");
599
685
  var s = _stringOf((e && e.code) || "") + " " + _stringOf((e && e.message) || "");
@@ -759,7 +845,11 @@ function _validateProxy(request, defaults, E, C, originHost, originStatedPort, o
759
845
  * consult `Symbol.isConcatSpreadable` on each argument, so capturing the method leaves the
760
846
  * SPREADING replaceable and an inherited hook replaced a validated anchor with another one in
761
847
  * the CONNECT options. `concatList` copies own indexes and appends own indexes. */
762
- 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
+ }
763
853
  if (pUseSystem) pCa = _concatList(pCa, _systemCa());
764
854
  if (pCa.length === 0) {
765
855
  throw E(C("no-trust-anchors"), "proxy.tls.useSystemStore is set and the platform certificate store returned no certificates, " +
@@ -814,6 +904,7 @@ function _digestProxyCredential(headers, pinfo, C, E) {
814
904
  }, E);
815
905
  }
816
906
  function _proxyErrorClass(e, C) {
907
+ if (e && e.pkiCompromisedKey === "proxy") return C("proxy-key-compromised");
817
908
  if (e && e.pkiDistrustedKey === "proxy") return C("proxy-key-distrusted");
818
909
  var code = _stringOf((e && e.code) || "");
819
910
  if (_strIndexOf(code, "ERR_TLS") === 0 || _strIndexOf(code, "ERR_SSL") === 0 || _strIndexOf(code, "SSL") !== -1 ||
@@ -844,8 +935,21 @@ function _proxyErrorClass(e, C) {
844
935
  * (`transport/no-trust-anchors`), a body over the streaming
845
936
  * cap (`transport/response-too-large`), a stalled socket (`transport/timeout`), a below
846
937
  * -floor negotiation (`transport/tls-floor`), a failed server authentication
847
- * (`transport/server-auth-failed`), or a verified chain carrying a distrusted key
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
848
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.
849
953
  * A protocol client passes its own error factory (`defaults.E`) + `defaults.errPrefix`
850
954
  * to surface domain codes (`est/...`). The transport owns no HTTP/redirect/auth
851
955
  * semantics; those live in the message layer that consumes the response triple.
@@ -1005,7 +1109,11 @@ function httpsTransport(defaults) {
1005
1109
  if (typeof sni !== "string") throw E(C("bad-input"), "tls.servername must be a string, got " + guard.text.showValue(sni));
1006
1110
  if (sni && !_isIP(sni)) options.servername = sni;
1007
1111
  var caList = [];
1008
- if (hasAnchors) caList = _concatList(caList, _pemifyAnchors(anchors));
1112
+ if (hasAnchors) {
1113
+ var anchorPems = _pemifyAnchors(anchors);
1114
+ _refuseCompromisedAnchors(anchorPems, E, C("bad-input"), "tls.anchors");
1115
+ caList = _concatList(caList, anchorPems);
1116
+ }
1009
1117
  if (useSystem) caList = _concatList(caList, systemCa);
1010
1118
  if (caList.length) options.ca = caList;
1011
1119
  if (cert) options.cert = cert;
package/lib/lint.js CHANGED
@@ -3937,49 +3937,21 @@ var RFC5480_RULES = [
3937
3937
  },
3938
3938
  ];
3939
3939
 
3940
- /** @internal The curves the small-scalar table is built for, and the scalar bound. Each table holds
3941
- * the x-coordinates of d*G for d from 1 to the bound minus one, built once per curve on first use
3942
- * (measured on Node 26: 4 ms for P-256, 138 ms for P-384, 321 ms for P-521). The points d*G and
3943
- * (n-d)*G share an x-coordinate, so one comparison covers both. */
3944
- var SMALL_SCALAR_WIDTH = intrinsic.assign(intrinsic.create(null), { prime256v1: 32, secp384r1: 48, secp521r1: 66, secp256k1: 32 });
3945
- var SMALL_SCALAR_BOUND = 256;
3946
- var _smallScalarTables = intrinsic.create(null);
3947
- function _smallScalarTable(curve) {
3948
- if (_smallScalarTables[curve]) return _smallScalarTables[curve];
3949
- var width = SMALL_SCALAR_WIDTH[curve];
3950
- var table = intrinsic.create(null);
3951
- var scalar = intrinsic.bufferAlloc(width);
3952
- for (var d = 1; d < SMALL_SCALAR_BOUND; d++) {
3953
- scalar[width - 1] = d;
3954
- var point = guard.crypto.ecPointFromScalar(curve, scalar);
3955
- table[intrinsic.bufToString(point, "hex", 1, 1 + width)] = true;
3956
- }
3957
- _smallScalarTables[curve] = table;
3958
- return table;
3959
- }
3960
- /** @internal The JWK curve names the table covers, mapped to the curve names this file reports. */
3961
- var SMALL_SCALAR_JWK_CURVES = intrinsic.assign(intrinsic.create(null), {
3962
- "P-256": "prime256v1", "P-384": "secp384r1", "P-521": "secp521r1", "secp256k1": "secp256k1",
3963
- });
3964
- /** @internal The curve and the x-coordinate of an elliptic curve subject key, read through node's own
3965
- * import, or null. The import accepts a named or explicit curve and an uncompressed, compressed or
3966
- * hybrid point, and it refuses a point that is not on the curve, so a key that is not one reports
3967
- * nothing here. */
3968
- function _ecKeyCoordinate(spki) {
3969
- try {
3970
- var jwk = guard.crypto.exportKey(guard.crypto.publicKey({ key: spki.bytes, format: "der", type: "spki" }), { format: "jwk" });
3971
- var curve = SMALL_SCALAR_JWK_CURVES[jwk.crv];
3972
- if (curve === undefined || typeof jwk.x !== "string") return null;
3973
- return { curve: curve, x: intrinsic.bufToString(intrinsic.bufferFrom(jwk.x, "base64url"), "hex") };
3974
- } catch (_e) {
3975
- // allow:swallow-unverified a subject key node does not import is not an elliptic curve point this row can judge
3976
- return null;
3977
- }
3978
- }
3979
3940
  function _isEcSubjectKey(cert) {
3980
3941
  var spki = cert.subjectPublicKeyInfo;
3981
- return !!spki && !!spki.algorithm && spki.algorithm.name === "ecPublicKey" && intrinsic.isBuffer(spki.bytes);
3942
+ if (!spki || !spki.algorithm || !intrinsic.isBuffer(spki.bytes)) return false;
3943
+ var n = spki.algorithm.name;
3944
+ return n === "ecPublicKey" || n === "ecDH" || n === "ecMQV";
3982
3945
  }
3946
+ function _hasSubjectKey(cert) {
3947
+ return !!cert.subjectPublicKeyInfo && intrinsic.isBuffer(cert.subjectPublicKeyInfo.bytes);
3948
+ }
3949
+ /** @internal The built-in list, keyed by digest, for the row that names a listed key. */
3950
+ var _COMPROMISED_BY_DIGEST = intrinsic.create(null);
3951
+ (function () {
3952
+ var listed = guard.distrust.knownCompromised();
3953
+ for (var i = 0; i < listed.length; i++) _COMPROMISED_BY_DIGEST[listed[i].spkiSha256] = listed[i];
3954
+ })();
3983
3955
 
3984
3956
  var KEY_COMPROMISE_RULES = [
3985
3957
  {
@@ -3988,9 +3960,19 @@ var KEY_COMPROMISE_RULES = [
3988
3960
  message: "the elliptic curve private key is a small integer, so anyone can recover it from the public key",
3989
3961
  appliesTo: _isEcSubjectKey,
3990
3962
  check: function (cert) {
3991
- var key = _ecKeyCoordinate(cert.subjectPublicKeyInfo);
3992
- if (key === null) return null;
3993
- return _smallScalarTable(key.curve)[key.x] === true ? { context: { curve: key.curve, scalarBelow: SMALL_SCALAR_BOUND } } : null;
3963
+ var curve = guard.distrust.smallEcScalar(cert.subjectPublicKeyInfo.bytes);
3964
+ return curve === null ? null : { context: { curve: curve, scalarBelow: guard.distrust.SMALL_SCALAR_BOUND } };
3965
+ },
3966
+ },
3967
+ {
3968
+ id: "lint/key-compromise/known-compromised-key", severity: "error", source: "key-compromise",
3969
+ citation: "CABF TLS BR 6.1.1.3 (4)",
3970
+ message: "the subject key is on the list of keys whose private key is public (pki.trust.knownCompromisedKeys)",
3971
+ appliesTo: _hasSubjectKey,
3972
+ check: function (cert) {
3973
+ var bytes = cert.subjectPublicKeyInfo.bytes;
3974
+ var entry = _COMPROMISED_BY_DIGEST[guard.distrust.digest(bytes)] || _COMPROMISED_BY_DIGEST[guard.distrust.keyDigest(bytes)];
3975
+ return entry ? { context: { name: entry.name, spkiSha256: entry.spkiSha256 } } : null;
3994
3976
  },
3995
3977
  },
3996
3978
  ];
@@ -1443,6 +1443,14 @@ function _revocationRow(row, rv) {
1443
1443
  * and every cross-certificate for the same key. No revocation status is asked for a certificate at
1444
1444
  * or below a distrusted key, and a standalone `crlChecker`, `ocspChecker` or `fetchingChecker` given
1445
1445
  * the list in its context answers `"unknown"` for an issuer whose key is listed.
1446
+ *
1447
+ * A key whose private key is public is refused whether or not a list is given, and no option turns
1448
+ * the refusal off: a key on `pki.trust.knownCompromisedKeys()`, and an elliptic curve key on P-256,
1449
+ * P-384, P-521 or secp256k1 whose private key is an integer below 256. Such a key fails the path with
1450
+ * `path/compromised-key`, in the `compromisedKey` row of the certificate that carries it or in the
1451
+ * `compromisedIssuerKey` row of the certificate a trust anchor with such a key issued. A compromised
1452
+ * key is reported in these rows rather than as distrusted when the list names it too, and the
1453
+ * revocation checkers answer `"unknown"` for a compromised issuer as they do for a listed one.
1446
1454
  * `distrustedKeysApplied` reports whether a non-empty list was supplied. A malformed list is
1447
1455
  * `path/bad-input`.
1448
1456
  *
@@ -1699,6 +1707,7 @@ async function _validate(path, opts, run, scope) {
1699
1707
  var prefixAuthentic = true;
1700
1708
  var anchorDistrustApplied = false, anchorPurposeApplied = false;
1701
1709
  var distrusted = scope.distrusted;
1710
+ var anchorKeyCompromised = guard.distrust.compromised(anchor.publicKey);
1702
1711
  var anchorKeyDistrusted = guard.distrust.has(distrusted, anchor.publicKey);
1703
1712
  /** @internal The first path index whose issuer chain holds a distrusted key: the anchor's key
1704
1713
  * makes it 0, a certificate's own key makes it that certificate. From there down, no revocation
@@ -1743,11 +1752,20 @@ async function _validate(path, opts, run, scope) {
1743
1752
  /** @internal The digest covers the SubjectPublicKeyInfo as the certificate encodes it, not the
1744
1753
  * working key, which carries parameters inherited from the issuer. A cross-signed certificate
1745
1754
  * carries the same encoded key as the certificate it duplicates, so one entry refuses both. */
1746
- if (idx === 0 && anchorKeyDistrusted) {
1755
+ if (idx === 0 && anchorKeyCompromised !== null) {
1756
+ guard.list.append(checks, { name: "compromisedIssuerKey", ok: false, code: "path/compromised-key" });
1757
+ failed = true;
1758
+ } else if (idx === 0 && anchorKeyDistrusted) {
1747
1759
  guard.list.append(checks, { name: "distrustedIssuerKey", ok: false, code: "path/distrusted-key" });
1748
1760
  failed = true;
1749
1761
  }
1750
- if (guard.distrust.has(distrusted, cert.subjectPublicKeyInfo.bytes) || guard.distrust.has(distrusted, _inheritedSpki(state, cert))) {
1762
+ var inheritedSpki = _inheritedSpki(state, cert);
1763
+ var keyCompromised = guard.distrust.compromised(cert.subjectPublicKeyInfo.bytes) || guard.distrust.compromised(inheritedSpki);
1764
+ if (keyCompromised !== null) {
1765
+ guard.list.append(checks, { name: "compromisedKey", ok: false, code: "path/compromised-key" });
1766
+ failed = true;
1767
+ if (distrustedFrom === -1) distrustedFrom = idx;
1768
+ } else if (guard.distrust.has(distrusted, cert.subjectPublicKeyInfo.bytes) || guard.distrust.has(distrusted, inheritedSpki)) {
1751
1769
  guard.list.append(checks, { name: "distrustedKey", ok: false, code: "path/distrusted-key" });
1752
1770
  failed = true;
1753
1771
  if (distrustedFrom === -1) distrustedFrom = idx;
@@ -2239,8 +2257,9 @@ function crlChecker(crls, opts) {
2239
2257
  /** @internal Every CRL this checker takes is verified under the issuer's key, so a listed
2240
2258
  * issuer key leaves no CRL that can answer. */
2241
2259
  var distrusted = guard.distrust.keys(ctx.distrustedKeys, E, "path/bad-input", "crlChecker: ctx.distrustedKeys");
2242
- if (guard.distrust.has(distrusted, issuer.workingPublicKey)) {
2243
- return guard.verdict.of({ status: "unknown", reason: "the issuer's key is listed in distrustedKeys, so no CRL it signs is an answer" });
2260
+ var issuerRefusal = guard.distrust.refusal(distrusted, issuer.workingPublicKey, "distrustedKeys");
2261
+ if (issuerRefusal !== null) {
2262
+ return guard.verdict.of({ status: "unknown", reason: "the issuer's key " + issuerRefusal + ", so no CRL it signs is an answer" });
2244
2263
  }
2245
2264
  var certIsCa = null, certScopeFault = null;
2246
2265
  try {
@@ -778,8 +778,9 @@ function fetchingChecker(opts) {
778
778
  * caller's fault and is thrown, where a fetch fault below becomes an undetermined verdict. */
779
779
  var distrusted = guard.distrust.keys(ctx == null ? undefined : ctx.distrustedKeys, E, "path/bad-input",
780
780
  "fetchingChecker: ctx.distrustedKeys");
781
- if (guard.distrust.has(distrusted, issuer.workingPublicKey)) {
782
- return _unknown("the issuer's key is listed in distrustedKeys, so nothing it signs is an answer", [], []);
781
+ var issuerRefusal = guard.distrust.refusal(distrusted, issuer.workingPublicKey, "distrustedKeys");
782
+ if (issuerRefusal !== null) {
783
+ return _unknown("the issuer's key " + issuerRefusal + ", so nothing it signs is an answer", [], []);
783
784
  }
784
785
  var notes = [], skips = [];
785
786
  var budget = budgetFor(ctx == null ? null : ctx.run);