@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.
- package/CHANGELOG.md +29 -0
- package/README.md +3 -3
- package/lib/asn1-der.js +6 -5
- package/lib/cmp-session.js +10 -3
- package/lib/cmp-verify.js +2 -3
- package/lib/compromised-keys.js +114 -0
- package/lib/crmf-sign.js +2 -0
- package/lib/csr-sign.js +4 -0
- package/lib/framework-error.js +20 -7
- package/lib/guard-distrust.js +128 -8
- package/lib/guard-verdict.js +16 -3
- package/lib/http-transport.js +128 -20
- package/lib/lint.js +25 -43
- package/lib/path-validate.js +23 -4
- package/lib/revocation-fetch.js +3 -2
- package/lib/trust.js +40 -2
- package/lib/webauthn-mds.js +3 -2
- package/lib/x509-sign.js +8 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/guard-distrust.js
CHANGED
|
@@ -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
|
-
/**
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
+
});
|
package/lib/guard-verdict.js
CHANGED
|
@@ -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],
|
|
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",
|
|
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,
|
|
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. */
|
package/lib/http-transport.js
CHANGED
|
@@ -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
|
|
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
|
|
441
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
475
|
-
*
|
|
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
|
-
|
|
551
|
+
var strict = list !== null && list.length > 0;
|
|
478
552
|
return function (host, cert) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
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)
|
|
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`),
|
|
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)
|
|
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
|
-
|
|
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
|
|
3992
|
-
|
|
3993
|
-
|
|
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
|
];
|
package/lib/path-validate.js
CHANGED
|
@@ -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 &&
|
|
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
|
-
|
|
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
|
-
|
|
2243
|
-
|
|
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 {
|
package/lib/revocation-fetch.js
CHANGED
|
@@ -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
|
-
|
|
782
|
-
|
|
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);
|