@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.
- package/CHANGELOG.md +66 -0
- package/README.md +7 -6
- package/bin/pki.js +110 -34
- package/lib/acme.js +91 -46
- package/lib/asn1-der.js +6 -5
- package/lib/attrcert-sign.js +4 -3
- package/lib/cmp-build.js +16 -6
- package/lib/cmp-session.js +26 -3
- package/lib/cmp-verify.js +24 -2
- package/lib/cms-compress.js +3 -3
- package/lib/cms-digest.js +3 -3
- package/lib/cms-verify.js +18 -8
- package/lib/compromised-keys.js +114 -0
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +3 -3
- package/lib/crmf-sign.js +2 -0
- package/lib/csr-sign.js +5 -1
- package/lib/ct.js +16 -3
- package/lib/est.js +44 -29
- package/lib/framework-error.js +20 -7
- package/lib/guard-all.js +2 -0
- package/lib/guard-distrust.js +344 -0
- package/lib/guard-json.js +1 -1
- package/lib/guard-verdict.js +16 -3
- package/lib/http-transport.js +412 -41
- package/lib/jose.js +1 -1
- package/lib/lint.js +49 -7
- package/lib/ocsp-verify.js +9 -3
- package/lib/ocsp.js +8 -4
- package/lib/oid.js +4 -0
- package/lib/path-validate.js +131 -16
- package/lib/possession.js +6 -2
- package/lib/related-cert.js +2 -2
- package/lib/revocation-fetch.js +39 -9
- package/lib/scep.js +28 -13
- package/lib/schema-pkix.js +3 -3
- package/lib/sigstore.js +11 -6
- package/lib/smime.js +18 -13
- package/lib/tls-cert-compress.js +6 -5
- package/lib/trust.js +159 -20
- package/lib/trustanchor-build.js +16 -1
- package/lib/tsp-sign.js +7 -1
- package/lib/webauthn-mds.js +30 -8
- package/lib/webauthn.js +46 -14
- package/lib/x509-sign.js +16 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/http-transport.js
CHANGED
|
@@ -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
|
|
55
|
-
* code path that disables server-certificate verification.
|
|
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 (
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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`),
|
|
616
|
-
* (`transport/server-auth-failed`)
|
|
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
|
|
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)
|
|
750
|
-
|
|
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
|
|
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
|
|
1064
|
-
if (o.ca
|
|
1065
|
-
if (o.cert
|
|
1066
|
-
if (o.key
|
|
1067
|
-
if (o.checkServerIdentity
|
|
1068
|
-
if (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
|
|
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) };
|