@blamejs/pki 0.8.48 → 0.8.50
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 +20 -0
- package/lib/acme.js +21 -15
- package/lib/cmp-build.js +10 -5
- package/lib/ct.js +263 -57
- package/lib/est.js +12 -6
- package/lib/guard-bytes.js +17 -2
- package/lib/guard-identifier.js +22 -0
- package/lib/guard-intrinsic.js +51 -0
- package/lib/http-digest.js +3 -1
- package/lib/http-transport.js +48 -20
- package/lib/ocsp.js +3 -1
- package/lib/path-validate.js +5 -2
- package/lib/revocation-fetch.js +4 -1
- package/lib/scep.js +12 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.8.50 — 2026-10-05
|
|
8
|
+
|
|
9
|
+
Decide where a certificate-transparency request goes and which trust anchors it is made under before reading anything else the caller passed, and keep a copied byte argument out of the caller's reach.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `pki.ct.getProofByHash` and `pki.ct.getSthConsistency` read the fields of the signed tree heads they were given before the request was shaped, and `pki.ct.addChain` and `pki.ct.addPreChain` read the elements of the certificate chain. A getter on any of those fields runs the caller's code at that moment, so it could rewrite the URL and send the request to another host, or empty `tls.anchors` after the check that refuses an unpinned log had already approved them, which opened the connection with no anchors at all while leaving the URL it reported unchanged. `pki.ct.fetchLogList` had the same ordering for a different reason: it parsed the URL, which converts it and so can run a caller's `toString`, before it read the TLS configuration. No order of reads fixes this on its own, because whichever option is read first, reading a value nested inside it runs the caller's code and that code can rewrite the ones not yet read: reading the TLS first lets a getter on `tls.anchors` rewrite the URL and the pinned key, and shaping the request first lets a header getter replace the key a tree head is verified under, in place if it is a buffer. So two shapes are refused rather than ordered around. `opts.url` has to be a string, because asking an object for its text form runs the caller's code, and so does `opts.sigUrl`, the detached-signature URL `pki.ct.fetchLogList` takes, which is a destination on the same footing. `opts.tls` may not carry an accessor, because reading its members has to be inert. All three draw `ct/bad-input`. With those refused, every option a verb declares is taken at its entry, the pinned key is copied there, and the request is sent and the signature checked from what was taken. A caller passing plain values and a string URL sees no change, and options supplied through a defaults object on the prototype chain still work, including the transport.
|
|
14
|
+
- The trust anchors, the client certificate and the client key a `pki.ct` verb is given are copied where they are taken, not referenced. Copying the anchor list copied the references in it, and an anchor is a mutable buffer, so a getter elsewhere in the options could write other bytes over an anchor after the check that refuses an unpinned log had approved it: every reference still pointed where it had, the URL was unchanged, and the connection was made under trust material no check had seen. A PEM string cannot be altered after the fact and is taken as it is; a value that is neither bytes nor a string reaches the transport unchanged, so a shape it does not accept still draws the refusal it draws today. Two side effects of taking the copy: a byte source that is not a Buffer, such as the `ArrayBuffer` that `crypto.subtle.exportKey` returns, is normalized to one and now reaches the connection as a usable anchor, and an anchor larger than the DER ceiling is refused as `ct/too-large` where it was previously passed on.
|
|
15
|
+
- A TLS setting nobody set can no longer be answered by `Object.prototype`. A verb that fetches builds a record of the TLS settings for the request, and where the caller named none that record was an empty object literal, which inherits. A value installed on `Object.prototype` was therefore read from it, and one of the settings read that way is `useSystemStore`, the opt-in that turns off the refusal of a server with no pinned trust anchor: with it installed, a call that had named neither a transport nor an anchor opened a connection trusting the system store where it should have been refused. The record now has no prototype, so nothing can be inherited into it. Fixed in `pki.ct`, `pki.acme`, `pki.est`, `pki.scep`, `pki.cmp`, the certificate-path builder's authority-information-access fetch, and `pki.transport` itself, which is the refusal every one of those delegates to.
|
|
16
|
+
- A byte input copied at a verb's entry is copied into storage of its own. The copy was taken with `Buffer.from`, which for a small result allocates out of the shared 64 KiB pool, so a caller whose own value came from that pool holds a view of the whole store and the copy lands inside it at another offset. A caller's code running after the copy and before the copy is used could then find it and overwrite it: supplying the pinned log key as a pooled buffer and rewriting the copy from inside the transport had `pki.ct.getSth` accept a tree head signed by another key. This is the copy every verb in the package takes of a byte argument, so the fix reaches each of them, not only the certificate-transparency ones. Copies of a caller's keys, hashes and signed bytes are now out of the caller's reach once taken, which is what taking them is for.
|
|
17
|
+
- A request URL is parsed with the `URL` constructor and the string conversion the package captured at load, in every module that builds a destination from one: `pki.ct`, `pki.acme`, `pki.est`, `pki.scep`, `pki.cmp`, `pki.ocsp`, the revocation fetcher, the path validator, the HTTP transport and its Digest authentication. Reading either off the global object at the moment of the parse read whatever had been installed there by then. A caller's code running mid-request, through a getter on an option the verb had yet to read, could replace the constructor with one answering a genuine URL that named another host, or replace the conversion so it answered with another host's URL outright, and the endpoint was built from that answer while the URL the caller passed was still the one the call reported. Capturing the constructor is not enough on its own, because the value it returns reads its parts through `URL.prototype`, which is also replaceable: redefining `href` there answered every read of the destination. So `pki.ct` and the HTTP transport read each part of a parsed destination, the scheme, host, port, path and query, through accessors captured at load, and the transport reads the scheme once and decides from that one string whether the request is allowed, whether a trust anchor is required and which port it defaults to.
|
|
18
|
+
|
|
19
|
+
## v0.8.49 — 2026-10-05
|
|
20
|
+
|
|
21
|
+
Contact the certificate-transparency log the caller named, and refuse an options object that answers a second read differently.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `pki.ct.getSth`, `pki.ct.getProofByHash`, `pki.ct.getSthConsistency`, `pki.ct.getEntries`, `pki.ct.getRoots` and `pki.ct.addChain` read `opts.url` once to check it was present and again to build the endpoint. An options object whose `url` is an accessor therefore answered the two separately: the check saw the log the caller named and the request went wherever the second read pointed, which for a measured case was a different host entirely. `opts.tls` was read the same way, once for the trust-anchor check that refuses an unpinned log and once more for the connection, so the anchors that satisfied the check need not have been the anchors the socket used. Each verb now takes its options through one door at its own entry, and that door refuses an options object carrying an accessor, which is the rule `pki.ct.signSct` in the same module already applied. It also refuses an object that changes which options it carries while they are being read, since no set of checks can answer for one. A caller passing a plain object sees no change. The refusal is `ct/bad-input` and it happens before any request is sent, so a rejected options object cannot reach a log. Refusing an accessor on the options object is not the whole rule, because the door never inspects the members of a value, and a getter on a header name runs while the request headers are being copied. Three things it could reach from there are now taken before it runs. The URL is taken and converted to text at the same point, since holding the value alone is not enough when the value is an object whose text form names the host: a getter that rewrote an object-valued URL's `href` moved the request just as a second read would have. The TLS settings are flattened at that point too, and the anchor list is copied rather than shared, because a getter that emptied the pinned list after the trust-anchor check had approved it left the connection pinned to nothing. A caller that supplies options through a defaults object on the prototype chain is unaffected, including a function-valued `transport`. `pki.ct.fetchLogList` checked which option names it had been given and never entered that door at all, so its `url` was read once to parse and once to fetch, its `sigUrl` twice, and its TLS configuration once for the anchor check and once for the connection; it enters the same door now, over its own option set. Note that this is stricter than before: an options object with an accessor property was previously accepted, and the toolkit answered from whichever read it happened to make. Pass plain values.
|
|
26
|
+
|
|
7
27
|
## v0.8.48 — 2026-10-05
|
|
8
28
|
|
|
9
29
|
Assemble a verification verdict out of component results through the operation the package captured at load, and refuse the reserved end-of-contents encoding where an element belongs.
|
package/lib/acme.js
CHANGED
|
@@ -58,6 +58,11 @@ var _resolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
|
58
58
|
* replacement installed after load could take over, and what it decides is the value that gets
|
|
59
59
|
* awaited. */
|
|
60
60
|
var _Promise = intrinsic.Promise;
|
|
61
|
+
/** @internal The URL parser, taken at load for the same reason: what it returns names the host a
|
|
62
|
+
* request goes to, and this module resolves a `Location` and a link relation against a directory
|
|
63
|
+
* origin with it. */
|
|
64
|
+
var _URL = intrinsic.URL;
|
|
65
|
+
var _urlCanParse = intrinsic.urlCanParse;
|
|
61
66
|
/** @internal A refusal returned as a REJECTED promise is a verdict too: read from the global binding, a
|
|
62
67
|
* replacement whose `reject` resolves instead turns the refusal into a pass. */
|
|
63
68
|
var _reject = intrinsic.uncurry(intrinsic.promiseReject);
|
|
@@ -198,7 +203,7 @@ var RENEWAL_RETRY_DEFAULT_SECONDS = constants.TIME.hours(6) / constants.TIME.sec
|
|
|
198
203
|
function _isUrl(v) {
|
|
199
204
|
if (!_isString(v) || !_isHttpUrlShape(v)) return false;
|
|
200
205
|
var u;
|
|
201
|
-
try { u = new
|
|
206
|
+
try { u = new _URL(v); }
|
|
202
207
|
catch (_e) { return false; }
|
|
203
208
|
return (u.protocol === "http:" || u.protocol === "https:") && u.hostname.length > 0;
|
|
204
209
|
}
|
|
@@ -1441,7 +1446,7 @@ var CLIENT_MAX_NONCE_POOL = 32;
|
|
|
1441
1446
|
function _clientUrl(urlStr) {
|
|
1442
1447
|
var s = String(urlStr);
|
|
1443
1448
|
var url;
|
|
1444
|
-
try { url = new
|
|
1449
|
+
try { url = new _URL(s); }
|
|
1445
1450
|
catch (e) { throw E("acme/bad-url", "the ACME URL did not parse: " + s, e); }
|
|
1446
1451
|
if (url.protocol !== "https:") throw E("acme/insecure-url", "ACME requires https (RFC 8555 sec. 6.1), got " + url.protocol + " for " + s);
|
|
1447
1452
|
var _uAuth = _uriAuthority(s, true);
|
|
@@ -1465,11 +1470,11 @@ function _clientUrl(urlStr) {
|
|
|
1465
1470
|
}
|
|
1466
1471
|
function _resolveLocation(loc, base) {
|
|
1467
1472
|
var isAbsolute = true;
|
|
1468
|
-
try { new
|
|
1473
|
+
try { new _URL(loc); }
|
|
1469
1474
|
catch (_e) { isAbsolute = false; }
|
|
1470
1475
|
if (isAbsolute) return _clientUrl(loc);
|
|
1471
1476
|
var u;
|
|
1472
|
-
try { u = new
|
|
1477
|
+
try { u = new _URL(loc, base); }
|
|
1473
1478
|
catch (e) { throw E("acme/bad-url", "the Location header did not resolve to a valid URL: " + JSON.stringify(loc), e); }
|
|
1474
1479
|
if (_queryRepaired(loc, u)) throw E("acme/bad-url", "a relative Link/Location query is not canonical (resolution re-encoded it): " + JSON.stringify(loc));
|
|
1475
1480
|
return _clientUrl(u.href);
|
|
@@ -1479,9 +1484,10 @@ function _queryRepaired(rawRef, resolvedUrl) {
|
|
|
1479
1484
|
return qi !== -1 && rawRef.slice(qi).split("#")[0] !== resolvedUrl.search;
|
|
1480
1485
|
}
|
|
1481
1486
|
var _defaultSleep = require("./sleep").sleep;
|
|
1487
|
+
/** @internal The settings this client forwards to its transport. */
|
|
1488
|
+
var TLS_SETTINGS = ["anchors", "useSystemStore", "cert", "key", "minVersion", "servername", "checkServerIdentity"];
|
|
1482
1489
|
function _clientTls(o) {
|
|
1483
|
-
|
|
1484
|
-
return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
|
|
1490
|
+
return guard.identifier.pickSettings(o.tls, TLS_SETTINGS);
|
|
1485
1491
|
}
|
|
1486
1492
|
function _splitPemChain(text) {
|
|
1487
1493
|
var ders;
|
|
@@ -1646,7 +1652,7 @@ function _badIpLiteral(uri) {
|
|
|
1646
1652
|
var interior = _ipLiteralInterior(uri);
|
|
1647
1653
|
if (interior === null) return false;
|
|
1648
1654
|
if (_isIpvFuture(interior)) return false;
|
|
1649
|
-
return !
|
|
1655
|
+
return !_urlCanParse("http://[" + interior + "]/");
|
|
1650
1656
|
}
|
|
1651
1657
|
function _uriStructurallyInvalid(uri) {
|
|
1652
1658
|
return !_bracketsOnlyInAuthority(uri) || _hasEmptyAuthority(uri) || _authorityMultiAt(uri) || _multiFragment(uri) || _relFirstSegHasColon(uri) || _badPort(uri) || _badIpLiteral(uri);
|
|
@@ -1757,7 +1763,7 @@ function _parseLinkRelation(headers, base, relName, opts) {
|
|
|
1757
1763
|
if (hNames[hi].toLowerCase() === "link") { raw = headers[hNames[hi]]; break; }
|
|
1758
1764
|
}
|
|
1759
1765
|
if (raw == null) return [];
|
|
1760
|
-
var baseUrl = new
|
|
1766
|
+
var baseUrl = new _URL(base), baseOrigin = baseUrl.origin, baseHref = baseUrl.href;
|
|
1761
1767
|
var baseKey = _dedupKey(baseHref);
|
|
1762
1768
|
var fields = Array.isArray(raw) ? raw : [raw], out = [], seen = Object.create(null), distinctCount = 0, totalBytes = 0;
|
|
1763
1769
|
for (var fi = 0; fi < fields.length; fi++) {
|
|
@@ -1777,7 +1783,7 @@ function _parseLinkRelation(headers, base, relName, opts) {
|
|
|
1777
1783
|
if (lv.anchor != null) {
|
|
1778
1784
|
if (_linkUriInvalid(lv.anchor)) continue;
|
|
1779
1785
|
var au;
|
|
1780
|
-
try { au = new
|
|
1786
|
+
try { au = new _URL(lv.anchor, base); }
|
|
1781
1787
|
catch (_ae) { continue; }
|
|
1782
1788
|
if (_queryRepaired(lv.anchor, au)) continue;
|
|
1783
1789
|
if (au.href !== baseHref) continue;
|
|
@@ -1789,7 +1795,7 @@ function _parseLinkRelation(headers, base, relName, opts) {
|
|
|
1789
1795
|
if (opts.singleton) throw E("acme/bad-link", "a rel=\"" + relName + "\" Link target is not a usable ACME request URL: " + _stringify(lv.uri), e);
|
|
1790
1796
|
continue;
|
|
1791
1797
|
}
|
|
1792
|
-
var parsed = new
|
|
1798
|
+
var parsed = new _URL(resolved);
|
|
1793
1799
|
if (parsed.origin !== baseOrigin) throw E("acme/bad-link", "a rel=\"" + relName + "\" Link target is not on the request URL's origin (SSRF guard): " + JSON.stringify(lv.uri));
|
|
1794
1800
|
var key = _dedupKey(parsed.href);
|
|
1795
1801
|
if (seen[key]) continue;
|
|
@@ -1929,7 +1935,7 @@ function client(directoryUrl, opts) {
|
|
|
1929
1935
|
|
|
1930
1936
|
var transport = checkedTransport;
|
|
1931
1937
|
if (!transport) {
|
|
1932
|
-
var t0 = opts.tls
|
|
1938
|
+
var t0 = opts.tls != null ? opts.tls : intrinsic.create(null);
|
|
1933
1939
|
var hasAnchors = t0.anchors !== undefined && t0.anchors !== null && !(Array.isArray(t0.anchors) && t0.anchors.length === 0);
|
|
1934
1940
|
if (!hasAnchors && t0.useSystemStore !== true) throw E("acme/no-trust-anchors", "no explicit trust anchor and tls.useSystemStore not set to true (RFC 8555 sec. 6.1)");
|
|
1935
1941
|
transport = httpTransport.https({ E: E, errPrefix: "acme" });
|
|
@@ -1950,10 +1956,10 @@ function client(directoryUrl, opts) {
|
|
|
1950
1956
|
var dirCache = null;
|
|
1951
1957
|
var nonces = [];
|
|
1952
1958
|
var kid = null;
|
|
1953
|
-
var trustedOrigin = new
|
|
1959
|
+
var trustedOrigin = new _URL(dirUrl).origin;
|
|
1954
1960
|
function _tlsFor(url) {
|
|
1955
1961
|
var t = budgets.tls;
|
|
1956
|
-
if (new
|
|
1962
|
+
if (new _URL(url).origin !== trustedOrigin) {
|
|
1957
1963
|
t = Object.assign({}, t);
|
|
1958
1964
|
t.cert = null; t.key = null; t.servername = null;
|
|
1959
1965
|
}
|
|
@@ -2018,7 +2024,7 @@ function client(directoryUrl, opts) {
|
|
|
2018
2024
|
if (redirects >= budgets.maxRedirects) throw E("acme/too-many-redirects", "the redirect budget of " + budgets.maxRedirects + " was exceeded");
|
|
2019
2025
|
if (!_isString(h["location"])) throw E("acme/bad-redirect", "a " + status + " redirect carried no Location header (RFC 7231 sec. 6.4)");
|
|
2020
2026
|
var next;
|
|
2021
|
-
try { next = new
|
|
2027
|
+
try { next = new _URL(h["location"], url).href; }
|
|
2022
2028
|
catch (e) { throw E("acme/bad-redirect", "the redirect Location did not resolve to a valid URL", e); }
|
|
2023
2029
|
return _sendFollowing(method, _clientUrl(next), headers, body, redirects + 1);
|
|
2024
2030
|
}
|
|
@@ -2364,7 +2370,7 @@ function client(directoryUrl, opts) {
|
|
|
2364
2370
|
return _resource("renewalInfo").then(function (base) {
|
|
2365
2371
|
// allow:nan-date-comparison-unguarded -- notAfterMs is a codec-parsed cert date; clk() is validated finite.
|
|
2366
2372
|
if (clk() > notAfterMs) throw E("acme/certificate-expired", "the certificate expired before the RenewalInfo request could be issued; a client MUST NOT check RenewalInfo after expiry (RFC 9773 sec. 4.3)");
|
|
2367
|
-
var u = new
|
|
2373
|
+
var u = new _URL(base);
|
|
2368
2374
|
u.pathname = _stripTrailingSlash(u.pathname) + "/" + certId;
|
|
2369
2375
|
var url = _clientUrl(u.href);
|
|
2370
2376
|
return _send("GET", url, { accept: "application/json" }, null).then(function (res) {
|
package/lib/cmp-build.js
CHANGED
|
@@ -34,6 +34,10 @@ var intrinsic = require("./guard-intrinsic");
|
|
|
34
34
|
* the construction to whatever that binding holds, and one settling without awaiting its argument runs
|
|
35
35
|
* the continuation before the check has finished. */
|
|
36
36
|
var _Promise = intrinsic.Promise;
|
|
37
|
+
/** @internal The URL parser, taken at load: what it returns names the host a transfer is sent to. The
|
|
38
|
+
* conversion it is handed is taken with it, since a replacement answers with a host of its own. */
|
|
39
|
+
var _URL = intrinsic.URL;
|
|
40
|
+
var _stringOf = intrinsic.String;
|
|
37
41
|
var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
38
42
|
var _hasOwn = intrinsic.hasOwn;
|
|
39
43
|
var _isArray = intrinsic.isArray;
|
|
@@ -997,9 +1001,10 @@ function _cmpMessageDer(input) {
|
|
|
997
1001
|
return der;
|
|
998
1002
|
}
|
|
999
1003
|
|
|
1004
|
+
/** @internal The settings this transfer forwards to its transport. */
|
|
1005
|
+
var TLS_SETTINGS = ["anchors", "useSystemStore", "cert", "key", "minVersion", "servername", "checkServerIdentity"];
|
|
1000
1006
|
function _tlsForTransfer(opts) {
|
|
1001
|
-
|
|
1002
|
-
return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
|
|
1007
|
+
return guard.identifier.pickSettings(opts.tls, TLS_SETTINGS);
|
|
1003
1008
|
}
|
|
1004
1009
|
|
|
1005
1010
|
function _ctypeToken(headers) { return String((headers || {})["content-type"] || "").split(";")[0].trim().toLowerCase(); }
|
|
@@ -1053,10 +1058,10 @@ function _transfer(url, message, opts) {
|
|
|
1053
1058
|
var transport = _assertTransport(opts);
|
|
1054
1059
|
var der = _cmpMessageDer(message);
|
|
1055
1060
|
var parsedUrl;
|
|
1056
|
-
try { parsedUrl = new
|
|
1061
|
+
try { parsedUrl = new _URL(_stringOf(url)); }
|
|
1057
1062
|
catch (e) { throw _err("cmp/bad-url", "the CMP URL did not parse: " + guard.text.showValue(url), e); }
|
|
1058
1063
|
if (!transport) {
|
|
1059
|
-
var t = opts.tls
|
|
1064
|
+
var t = opts.tls != null ? opts.tls : intrinsic.create(null);
|
|
1060
1065
|
var hasAnchors = t.anchors !== undefined && t.anchors !== null && !(Array.isArray(t.anchors) && t.anchors.length === 0);
|
|
1061
1066
|
if (!hasAnchors && t.useSystemStore !== true) throw _err("cmp/no-trust-anchors", "no explicit trust anchor and tls.useSystemStore not set to true -- refusing an unpinned server (RFC 9811 sec. 5)");
|
|
1062
1067
|
transport = httpTransport.https({ E: _err, errPrefix: "cmp" });
|
|
@@ -1120,7 +1125,7 @@ function wellKnownUrl(base, opts) {
|
|
|
1120
1125
|
return "unknown wellKnownUrl option " + JSON.stringify(k) + " (expected label / operation)";
|
|
1121
1126
|
});
|
|
1122
1127
|
var u;
|
|
1123
|
-
try { u = new
|
|
1128
|
+
try { u = new _URL(_stringOf(base)); }
|
|
1124
1129
|
catch (e) { throw _err("cmp/bad-url", "the CMP base URL did not parse: " + guard.text.showValue(base), e); }
|
|
1125
1130
|
if (u.protocol !== "https:" && u.protocol !== "http:") throw _err("cmp/bad-url", "the CMP base URL must be http or https (a well-known URI is authority-rooted over HTTP), got " + u.protocol + ": " + JSON.stringify(String(base)));
|
|
1126
1131
|
if (u.search || u.hash) throw _err("cmp/bad-url", "the CMP base URL must not carry a query or fragment component (RFC 9811 sec. 3.4)");
|
package/lib/ct.js
CHANGED
|
@@ -67,7 +67,17 @@ var _bufCopy = intrinsic.uncurry(Buffer.prototype.copy);
|
|
|
67
67
|
var _writeU64BE = intrinsic.uncurry(Buffer.prototype.writeBigUInt64BE);
|
|
68
68
|
var _searchSet = intrinsic.uncurry(URLSearchParams.prototype.set);
|
|
69
69
|
var _numberOf = intrinsic.Number;
|
|
70
|
-
var _URL = URL;
|
|
70
|
+
var _URL = intrinsic.URL;
|
|
71
|
+
/** @internal Every part of a parsed destination is read through the accessor captured at load, not off
|
|
72
|
+
* the value's own prototype: a header getter that redefined `URL.prototype.href` had the request go to
|
|
73
|
+
* the host its getter named while the url the caller passed was still the one reported. */
|
|
74
|
+
var _urlHref = intrinsic.urlHref;
|
|
75
|
+
var _urlOrigin = intrinsic.urlOrigin;
|
|
76
|
+
var _urlProtocol = intrinsic.urlProtocol;
|
|
77
|
+
var _urlPathname = intrinsic.urlPathname;
|
|
78
|
+
var _urlSearchParams = intrinsic.urlSearchParams;
|
|
79
|
+
var _urlSetPathname = intrinsic.urlSetPathname;
|
|
80
|
+
var _urlSetSearch = intrinsic.urlSetSearch;
|
|
71
81
|
var _JSON_SUFFIX = ".json";
|
|
72
82
|
function _endsWithJson(s) { return s.length >= _JSON_SUFFIX.length && _strSlice(s, s.length - _JSON_SUFFIX.length) === _JSON_SUFFIX; }
|
|
73
83
|
var ByteReader = require("./byte-reader.js");
|
|
@@ -1171,12 +1181,48 @@ var DEFAULT_FETCH_TIMEOUT = C.TIME.seconds(30);
|
|
|
1171
1181
|
var MAX_FETCH_TIMEOUT = C.TIME.seconds(600);
|
|
1172
1182
|
var KNOWN_FETCH_OPTS = intrinsic.assign(intrinsic.create(null), { url: 1, signerKey: 1, sigUrl: 1, transport: 1, tls: 1, headers: 1, timeout: 1, maxResponseBytes: 1, requireJsonContentType: 1 });
|
|
1173
1183
|
|
|
1184
|
+
/** @internal The TLS settings flattened into a record of plain values, with the anchor LIST copied.
|
|
1185
|
+
* `opts.tls` is the caller's object and its members are read later than the trust-anchor check that
|
|
1186
|
+
* approves them, so leaving either shared let a getter elsewhere in the bag empty the pinned anchors
|
|
1187
|
+
* and rewrite the servername after the check had passed: the anchors checked were then not the anchors
|
|
1188
|
+
* the connection used. Copying the list is what closes that, since emptying an array the caller still
|
|
1189
|
+
* holds is the lever. Flatten once, check that record, and connect under the same one. */
|
|
1190
|
+
/** @internal One piece of trust material, copied rather than referenced. An anchor, a client
|
|
1191
|
+
* certificate and a client key are each a Buffer the caller still holds, and copying the list that
|
|
1192
|
+
* holds them copies the references in it: a header getter that wrote an attacker's PEM OVER an
|
|
1193
|
+
* anchor's bytes left every reference pointing where it did and still opened the connection under
|
|
1194
|
+
* trust material the unpinned-log check had already approved, MEASURED on this module. A PEM string
|
|
1195
|
+
* cannot be changed after the fact and is taken as it is; anything that is neither is handed on
|
|
1196
|
+
* unchanged, so the transport keeps giving the refusal it gives today for a shape it does not
|
|
1197
|
+
* accept. */
|
|
1198
|
+
function _trustValue(v, field) {
|
|
1199
|
+
if (typeof v === "string") return v;
|
|
1200
|
+
if (guard.bytes.isByteSource(v)) return _toBuffer(v, field, _ctErr, "ct/bad-input");
|
|
1201
|
+
return v;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1174
1204
|
function _tlsForFetch(opts) {
|
|
1175
|
-
|
|
1176
|
-
|
|
1205
|
+
/** @internal A record with NO prototype where the caller supplied no TLS at all, because the empty
|
|
1206
|
+
* object a literal gives inherits from `Object.prototype` and every field below is then answerable
|
|
1207
|
+
* from there. MEASURED: a getter inside `opts.sth` set `Object.prototype.useSystemStore = true`, the
|
|
1208
|
+
* record built here inherited it, and the refusal that turns away an unpinned log was skipped, so a
|
|
1209
|
+
* call that had named neither a transport nor an anchor opened a connection trusting the system
|
|
1210
|
+
* store. Where the caller DID supply a TLS record this is settled at the door and the record read
|
|
1211
|
+
* here is the flat one taken then, whose seven fields are its own. */
|
|
1212
|
+
var t = opts.tls != null ? opts.tls : intrinsic.create(null);
|
|
1213
|
+
var anchors = t.anchors;
|
|
1214
|
+
/** @internal Copied by index rather than with `slice`, which consults the array's species and so
|
|
1215
|
+
* would run a caller's constructor. `guard.list.copyMap` walks it with a narrowed count and copies
|
|
1216
|
+
* each element's bytes as it goes. */
|
|
1217
|
+
anchors = intrinsic.isArray(anchors)
|
|
1218
|
+
? guard.list.copyMap(anchors, function (a, i) { return _trustValue(a, "opts.tls.anchors[" + i + "]"); })
|
|
1219
|
+
: _trustValue(anchors, "opts.tls.anchors");
|
|
1220
|
+
return { anchors: anchors, useSystemStore: t.useSystemStore, cert: _trustValue(t.cert, "opts.tls.cert"),
|
|
1221
|
+
key: _trustValue(t.key, "opts.tls.key"), minVersion: t.minVersion, servername: t.servername,
|
|
1222
|
+
checkServerIdentity: t.checkServerIdentity };
|
|
1177
1223
|
}
|
|
1178
1224
|
|
|
1179
|
-
function _sameOrigin(a, b) { return new
|
|
1225
|
+
function _sameOrigin(a, b) { return _urlOrigin(new _URL(a)) === _urlOrigin(new _URL(b)); }
|
|
1180
1226
|
|
|
1181
1227
|
/** @internal Refused at the door and again where the value is used. */
|
|
1182
1228
|
function _assertTransport(opts) {
|
|
@@ -1188,17 +1234,20 @@ function _fetchCtypeToken(headers) { return String((headers || {})["content-type
|
|
|
1188
1234
|
|
|
1189
1235
|
function _parseHttpsUrl(u, label) {
|
|
1190
1236
|
var parsed;
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1237
|
+
var text = _stringOf(u);
|
|
1238
|
+
try { parsed = new _URL(text); }
|
|
1239
|
+
catch (e) { throw _ctErr("ct/bad-url", "the CT log-list " + label + " did not parse: " + text, e); }
|
|
1240
|
+
var scheme = _urlProtocol(parsed);
|
|
1241
|
+
if (scheme !== "https:") throw _ctErr("ct/insecure-url", "the CT log-list " + label + " must be https, got " + scheme + " for " + text);
|
|
1194
1242
|
return parsed;
|
|
1195
1243
|
}
|
|
1196
1244
|
|
|
1197
1245
|
function _sigUrlFor(parsed) {
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
sig
|
|
1201
|
-
|
|
1246
|
+
var path = _urlPathname(parsed);
|
|
1247
|
+
if (!_endsWithJson(path)) throw _ctErr("ct/bad-input", "cannot derive the detached-signature URL from a non-.json path (" + JSON.stringify(path) + "); pass opts.sigUrl explicitly");
|
|
1248
|
+
var sig = new _URL(_urlHref(parsed));
|
|
1249
|
+
_urlSetPathname(sig, _strSlice(path, 0, path.length - _JSON_SUFFIX.length) + ".sig");
|
|
1250
|
+
return _urlHref(sig);
|
|
1202
1251
|
}
|
|
1203
1252
|
|
|
1204
1253
|
function _fetchBody(transport, url, req, label) {
|
|
@@ -1231,6 +1280,10 @@ var STH_PREIMAGE_BYTES = 50;
|
|
|
1231
1280
|
var CT_VERSION_V1 = 0;
|
|
1232
1281
|
var SIG_TYPE_TREE_HASH = 1;
|
|
1233
1282
|
var CT_V1_PREFIX = "ct/v1/";
|
|
1283
|
+
/** @internal The union across the sec. 4 messages rather than one set per message, because a caller
|
|
1284
|
+
* drives several of them from a single options bag: the same record carries the pinned key for the
|
|
1285
|
+
* tree head and the range for the entries. An option a message does not read is ignored by it, so the
|
|
1286
|
+
* refusal this list makes is of a name no message knows. */
|
|
1234
1287
|
var KNOWN_CLIENT_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
1235
1288
|
url: 1, logKey: 1, transport: 1, tls: 1, headers: 1, timeout: 1, maxResponseBytes: 1,
|
|
1236
1289
|
sth: 1, oldSth: 1, newSth: 1, leafHash: 1, start: 1, end: 1, chain: 1,
|
|
@@ -1241,50 +1294,179 @@ var KNOWN_CLIENT_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
|
1241
1294
|
* with or without the `ct/v1/` prefix the specification fixes, and neither spelling doubles it. */
|
|
1242
1295
|
function _endpoint(url, message) {
|
|
1243
1296
|
var parsed = _parseHttpsUrl(url, "log URL");
|
|
1244
|
-
var path = parsed
|
|
1297
|
+
var path = _urlPathname(parsed);
|
|
1245
1298
|
if (path.length === 0 || path[path.length - 1] !== "/") path += "/";
|
|
1246
1299
|
var tail = CT_V1_PREFIX;
|
|
1247
1300
|
if (path.length >= CT_V1_PREFIX.length + 1 &&
|
|
1248
1301
|
_strSlice(path, path.length - CT_V1_PREFIX.length) === CT_V1_PREFIX) tail = "";
|
|
1249
|
-
var out = new _URL(parsed
|
|
1250
|
-
out
|
|
1251
|
-
out
|
|
1302
|
+
var out = new _URL(_urlHref(parsed));
|
|
1303
|
+
_urlSetPathname(out, path + tail + message);
|
|
1304
|
+
_urlSetSearch(out, "");
|
|
1252
1305
|
return out;
|
|
1253
1306
|
}
|
|
1254
1307
|
|
|
1308
|
+
/** @internal The door every sec. 4 message enters by: the declared options refused if one is not
|
|
1309
|
+
* recognized, then taken ONCE into a plain record. A caller's bag is the caller's, so an option
|
|
1310
|
+
* reached through an accessor answers each read separately, and this code read several of them twice:
|
|
1311
|
+
* `url` for a presence check and again to build the endpoint, which let the check approve one host
|
|
1312
|
+
* while the request went to another; `tls` for the anchor check and again for the connection, which
|
|
1313
|
+
* would be a pin approved on one object and a socket opened under another. Reading from the record
|
|
1314
|
+
* afterwards means the value that was checked is the value that is used. Every verb calls this at its
|
|
1315
|
+
* own entry and passes the record down, rather than relying on a check further in. */
|
|
1316
|
+
/** @internal `keyName` is the option this message VERIFIES against, or null for one that verifies
|
|
1317
|
+
* nothing. The option list is the union across the messages so that one bag drives several of them,
|
|
1318
|
+
* and an option a message does not read is ignored by it; settling the pinned key at every door
|
|
1319
|
+
* instead would make a message that never checks a signature refuse a bag over a field it does not
|
|
1320
|
+
* consume. `getSth` and the two chain submissions name it; the proof, consistency, entries and roots
|
|
1321
|
+
* messages pass null. */
|
|
1322
|
+
function _clientDoor(opts, keyName) {
|
|
1323
|
+
/** @internal `optionsObject` is the door `signSct` in this module already enters by, and it REFUSES
|
|
1324
|
+
* an options bag carrying an accessor rather than tolerating one: a bag whose properties are not
|
|
1325
|
+
* plain values is a shape no set of checks can answer for, and it also refuses one that changes
|
|
1326
|
+
* which options it carries while they are read. Refusing beats copying, because a copy leaves the
|
|
1327
|
+
* caller's shape acceptable and every future read in this module has to remember to use the copy.
|
|
1328
|
+
* It also leaves ABSENCE intact, which materializing the declared names would not: an absent
|
|
1329
|
+
* `transport` means "open the connection yourself, under the pinned anchors", while one present
|
|
1330
|
+
* holding `undefined` is a transport that is not callable. */
|
|
1331
|
+
var bag = guard.identifier.optionsObject(opts, _ctErr, "ct/bad-input", "pki.ct client options");
|
|
1332
|
+
guard.identifier.assertKnownKeys(bag, KNOWN_CLIENT_OPTS, _ctErr, "ct/bad-input", "unknown opts field ");
|
|
1333
|
+
/** @internal The bag is returned as it came, not copied. A copy cannot reproduce what reading it
|
|
1334
|
+
* does: an option supplied through a caller's defaults object is reached along the prototype chain,
|
|
1335
|
+
* and the enumeration that would drive a copy deliberately omits an inherited FUNCTION member,
|
|
1336
|
+
* because that is how `Object.prototype.toString` is kept from counting as an option. Copying
|
|
1337
|
+
* therefore dropped an inherited `transport`, and dropping a transport is not a smaller result: it
|
|
1338
|
+
* falls through to opening a real connection where the caller passed a fake one. What makes the
|
|
1339
|
+
* repeated reads safe instead is that this door refuses a bag whose properties are not plain values,
|
|
1340
|
+
* and that each option below is taken into a local ONCE, before anything that could run caller code.
|
|
1341
|
+
* EVERY DECLARED OPTION IS TAKEN HERE, and the ones a verdict rests on are made immune, but the
|
|
1342
|
+
* request is NOT shaped here. Shaping it here was tried and is wrong: copying the request headers
|
|
1343
|
+
* runs whatever getters the caller's header object carries, and doing that before a verb reads its
|
|
1344
|
+
* own options put every verification input at the mercy of one. A header getter swapped `logKey` for
|
|
1345
|
+
* an attacker's SPKI, then overwrote the same buffer in place once the reference was held, then
|
|
1346
|
+
* overwrote `sth.rootHash` so an empty audit proof verified. Each was worse than the wrong-host case
|
|
1347
|
+
* the move was meant to close. The verb's inputs are the more valuable thing, so the headers are
|
|
1348
|
+
* copied at send time as they always were, and the destination is protected by making it immune
|
|
1349
|
+
* instead: a string url cannot be rewritten, and a flattened tls with its anchor list copied cannot
|
|
1350
|
+
* be emptied. Taking the top level is safe because the door has already refused a bag whose
|
|
1351
|
+
* properties are not plain values, so no read here runs caller code. */
|
|
1352
|
+
return { o: _settleDecisive(_takeTopLevel(bag, KNOWN_CLIENT_OPTS), keyName) };
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/** @internal Every DECLARED option the bag can answer, read once into a record of plain values, before
|
|
1356
|
+
* anything derives from one. `in` is used rather than an enumeration because the door accepts an option
|
|
1357
|
+
* supplied through a caller's defaults object, and an enumeration omits an inherited FUNCTION member,
|
|
1358
|
+
* which silently dropped a `transport` and fell through to opening a real connection. It consults the
|
|
1359
|
+
* prototype chain exactly as the direct reads it replaces did, so it carries no new exposure, and
|
|
1360
|
+
* absence survives: a name the bag cannot answer is left out rather than materialized as `undefined`,
|
|
1361
|
+
* which a guard that tells absent from present-and-undefined depends on. */
|
|
1362
|
+
function _takeTopLevel(bag, known) {
|
|
1363
|
+
var o = intrinsic.create(null);
|
|
1364
|
+
intrinsic.forEach(intrinsic.keys(known), function (k) {
|
|
1365
|
+
if (k in bag) o[k] = bag[k];
|
|
1366
|
+
});
|
|
1367
|
+
return o;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/** @internal The two values a verdict rests on, made IMMUNE rather than merely taken. Holding a
|
|
1371
|
+
* reference is not holding a value: a header getter copied an attacker's SPKI OVER the caller's
|
|
1372
|
+
* `logKey` buffer in place and an attacker-signed tree head was accepted, and a getter inside `tls`
|
|
1373
|
+
* rewrote an object-valued url's `hostname` and the request went to another host. Both happened after
|
|
1374
|
+
* the option had been taken, because what was taken could still be changed from outside. A string and
|
|
1375
|
+
* a copied buffer cannot be, so the destination and the pin stop depending on what the caller does
|
|
1376
|
+
* next. Done here, before anything nested is read, for the same reason the taking is. */
|
|
1377
|
+
function _settleDecisive(o, keyName) {
|
|
1378
|
+
/** @internal There is no order that fixes this by itself, which is what two rounds of reordering
|
|
1379
|
+
* showed: converting the url asks an object for its text and that runs the caller's code, which
|
|
1380
|
+
* emptied the anchor list not yet flattened; flattening the anchors first reads `tls.anchors` and
|
|
1381
|
+
* that runs the caller's code too, which rewrote the url not yet converted. So the caller's code is
|
|
1382
|
+
* refused HERE instead of ordered around. A url has to be a string, which needs no conversion, and
|
|
1383
|
+
* a tls carries no accessor, which makes reading its members inert. After that nothing in settling
|
|
1384
|
+
* runs caller code and the order stops mattering. */
|
|
1385
|
+
if (o.url != null && typeof o.url !== "string") {
|
|
1386
|
+
throw _ctErr("ct/bad-input", "opts.url must be a string -- an object is asked for its text form, " +
|
|
1387
|
+
"and asking runs the caller's code, which is how a request reached a host its URL had not named");
|
|
1388
|
+
}
|
|
1389
|
+
/** @internal The detached-signature URL is a destination too, and only the log-list fetch declares
|
|
1390
|
+
* it, so the rule is the same one: a value that has to be converted is a value whose conversion
|
|
1391
|
+
* runs here, after everything else has been settled, where it could still rewrite what was. */
|
|
1392
|
+
if (o.sigUrl != null && typeof o.sigUrl !== "string") {
|
|
1393
|
+
throw _ctErr("ct/bad-input", "opts.sigUrl must be a string -- an object is asked for its text " +
|
|
1394
|
+
"form, and asking runs the caller's code after the other options have been settled");
|
|
1395
|
+
}
|
|
1396
|
+
if (o.tls != null) {
|
|
1397
|
+
guard.identifier.refuseAccessorFields(o.tls,
|
|
1398
|
+
guard.identifier.readableNames(o.tls, _ctErr, "ct/bad-input", "opts.tls"),
|
|
1399
|
+
_ctErr, "ct/bad-input", "opts.tls");
|
|
1400
|
+
/** @internal And the ELEMENTS of the anchor list, which refusing accessors on `tls` itself does not
|
|
1401
|
+
* reach. An accessor on `anchors[0]` runs when the list is copied, and one that overwrote
|
|
1402
|
+
* `sth.rootHash` with the leaf being proven had an empty audit path verify. The refusal has to go
|
|
1403
|
+
* as deep as the reading does. */
|
|
1404
|
+
var suppliedAnchors = o.tls.anchors;
|
|
1405
|
+
if (suppliedAnchors != null) {
|
|
1406
|
+
guard.identifier.refuseAccessorFields(suppliedAnchors,
|
|
1407
|
+
guard.identifier.readableIndices(suppliedAnchors, _ctErr, "ct/bad-input", "opts.tls.anchors"),
|
|
1408
|
+
_ctErr, "ct/bad-input", "opts.tls.anchors");
|
|
1409
|
+
}
|
|
1410
|
+
/** @internal Flattened HERE, with the anchor list copied, because the caller still holds the object
|
|
1411
|
+
* and a getter anywhere else in the bag emptied the list after the trust-anchor check approved it.
|
|
1412
|
+
* Reading its members and its elements is inert now that both refuse an accessor, so this runs no
|
|
1413
|
+
* caller code and the order it runs in stops mattering. */
|
|
1414
|
+
o.tls = _tlsForFetch(o);
|
|
1415
|
+
}
|
|
1416
|
+
if (keyName !== null && o[keyName] != null) {
|
|
1417
|
+
o[keyName] = _toBuffer(o[keyName], "opts." + keyName, _ctErr, "ct/bad-input");
|
|
1418
|
+
}
|
|
1419
|
+
return o;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1255
1422
|
/** @internal The shared request shape every message uses: the caller's transport or a pinned-anchor
|
|
1256
1423
|
* one, the caps, and the header filter. Factored so a verb adds a path and a body and nothing else,
|
|
1257
|
-
* and so a rule about reaching a log is stated once.
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
var transport = _assertTransport(
|
|
1261
|
-
|
|
1424
|
+
* and so a rule about reaching a log is stated once. `o` has already been through `_clientDoor`, so
|
|
1425
|
+
* every read here is of a plain value. */
|
|
1426
|
+
function _clientRequest(o) {
|
|
1427
|
+
var transport = _assertTransport(o);
|
|
1428
|
+
/** @internal ONE read, and the presence check and the endpoint both decide from it. Read again after
|
|
1429
|
+
* the header copy below, which runs whatever getters the caller's header object carries, it was
|
|
1430
|
+
* whatever that code had since made it: a getter on a header name rewrote `url` and moved the request
|
|
1431
|
+
* to another host after this check had approved the original.
|
|
1432
|
+
* AND CONVERTED HERE, not at the endpoint. Holding the value alone is not enough when it is an
|
|
1433
|
+
* object, because the text form is what names the host and `String()` asks the object for it: a
|
|
1434
|
+
* header getter that rewrote an object-valued url's `href` moved the request just the same, the
|
|
1435
|
+
* conversion being the last reader. The endpoint is built from this string. */
|
|
1436
|
+
if (o.url == null) throw _ctErr("ct/bad-input", "opts.url is required -- the log's base URL (there is no baked-in log)");
|
|
1437
|
+
var url = _stringOf(o.url);
|
|
1438
|
+
/** @internal Flattened BEFORE the header copy below, which invokes whatever getters the caller's
|
|
1439
|
+
* header object carries. One of those emptied the pinned anchors after this check had approved them,
|
|
1440
|
+
* so the check and the connection have to read the same flat record and it has to exist first. */
|
|
1441
|
+
var tls = _tlsForFetch(o);
|
|
1262
1442
|
if (!transport) {
|
|
1263
|
-
var
|
|
1264
|
-
|
|
1265
|
-
if (!hasAnchors && t.useSystemStore !== true) {
|
|
1443
|
+
var hasAnchors = tls.anchors !== undefined && tls.anchors !== null && !(intrinsic.isArray(tls.anchors) && tls.anchors.length === 0);
|
|
1444
|
+
if (!hasAnchors && tls.useSystemStore !== true) {
|
|
1266
1445
|
throw _ctErr("ct/no-trust-anchors", "no explicit trust anchor and tls.useSystemStore not set to true -- refusing an unpinned CT log server (RFC 6962 sec. 3.2)");
|
|
1267
1446
|
}
|
|
1268
1447
|
transport = httpTransport.https({ E: _ctErr, errPrefix: "ct" });
|
|
1269
1448
|
}
|
|
1270
|
-
var timeout = guard.limits.cap(
|
|
1271
|
-
var maxResponseBytes = guard.limits.cap(
|
|
1449
|
+
var timeout = guard.limits.cap(o.timeout, "timeout", DEFAULT_FETCH_TIMEOUT, { E: _ctErr, code: "ct/bad-input", min: 1, max: MAX_FETCH_TIMEOUT });
|
|
1450
|
+
var maxResponseBytes = guard.limits.cap(o.maxResponseBytes, "maxResponseBytes", C.LIMITS.CT_LOG_LIST_MAX_BYTES, { E: _ctErr, code: "ct/bad-input", min: 1, max: C.LIMITS.CT_LOG_LIST_MAX_BYTES });
|
|
1272
1451
|
var headers = intrinsic.create(null);
|
|
1273
|
-
|
|
1452
|
+
var supplied = o.headers;
|
|
1453
|
+
intrinsic.forEach(guard.identifier.optionNames(supplied), function (k) {
|
|
1274
1454
|
var lk = _toLowerCase(k);
|
|
1275
|
-
if (lk !== "content-length" && lk !== "transfer-encoding") headers[k] =
|
|
1455
|
+
if (lk !== "content-length" && lk !== "transfer-encoding") headers[k] = supplied[k];
|
|
1276
1456
|
});
|
|
1277
|
-
return { transport: transport, req: { headers: headers, tls:
|
|
1278
|
-
maxResponseBytes: maxResponseBytes }
|
|
1457
|
+
return { transport: transport, url: url, req: { headers: headers, tls: tls, timeout: timeout,
|
|
1458
|
+
maxResponseBytes: maxResponseBytes } };
|
|
1279
1459
|
}
|
|
1280
1460
|
|
|
1281
1461
|
/** @internal A sec. 4 message, fetched and read as JSON. The specification returns every error as a
|
|
1282
1462
|
* 4xx or 5xx with a human-readable message, which `_fetchBody` already turns into a typed refusal,
|
|
1283
1463
|
* so a non-200 never reaches the reader. */
|
|
1284
|
-
async function _clientJson(
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1464
|
+
async function _clientJson(door, message, label, query, body) {
|
|
1465
|
+
/** @internal Shaped at send time, from the record the door settled. Shaping it at the door instead
|
|
1466
|
+
* ran the caller's header getters before any verb had captured its verification inputs. */
|
|
1467
|
+
var ctx = _clientRequest(door.o);
|
|
1468
|
+
var url = _endpoint(ctx.url, message);
|
|
1469
|
+
if (query) intrinsic.keys(query).forEach(function (k) { _searchSet(_urlSearchParams(url), k, query[k]); });
|
|
1288
1470
|
var req = ctx.req;
|
|
1289
1471
|
if (body !== undefined) {
|
|
1290
1472
|
req = intrinsic.assign(intrinsic.create(null), req);
|
|
@@ -1295,7 +1477,7 @@ async function _clientJson(opts, message, label, query, body) {
|
|
|
1295
1477
|
h["content-type"] = "application/json";
|
|
1296
1478
|
req.headers = h;
|
|
1297
1479
|
}
|
|
1298
|
-
var res = await _fetchBody(ctx.transport, url
|
|
1480
|
+
var res = await _fetchBody(ctx.transport, _urlHref(url), req, label);
|
|
1299
1481
|
/** @internal The exact-integer rule applies to the NAMED members, the ones `_uintField` reads. RFC 6962
|
|
1300
1482
|
* responses are ordinary JSON, where `1e3` is legal, so the stricter `integersOnly` would refuse a
|
|
1301
1483
|
* document a conforming log may emit; what this refuses is a spelling whose value is not exactly its
|
|
@@ -1466,7 +1648,8 @@ async function verifySth(sth, logKey) {
|
|
|
1466
1648
|
* sth.treeSize; // -> 3n
|
|
1467
1649
|
*/
|
|
1468
1650
|
async function getSth(opts) {
|
|
1469
|
-
|
|
1651
|
+
var door = _clientDoor(opts || {}, "logKey");
|
|
1652
|
+
opts = door.o;
|
|
1470
1653
|
/** @internal ONE read of the option, and the presence check and the copy both decide from it. Reading
|
|
1471
1654
|
* it twice let an accessor answer the two separately: a non-null value for the check and an
|
|
1472
1655
|
* attacker's SPKI for the copy, and the tree head was then verified against a key the caller never
|
|
@@ -1478,7 +1661,7 @@ async function getSth(opts) {
|
|
|
1478
1661
|
throw _ctErr("ct/bad-input", "opts.logKey is required -- the caller-pinned log SPKI; an unverified tree head is not a result");
|
|
1479
1662
|
}
|
|
1480
1663
|
var pinnedKey = _toBuffer(suppliedLogKey, "opts.logKey", _ctErr, "ct/bad-input");
|
|
1481
|
-
var doc = await _clientJson(
|
|
1664
|
+
var doc = await _clientJson(door,"get-sth", "sth");
|
|
1482
1665
|
var sth = {
|
|
1483
1666
|
treeSize: _uintField(doc, "tree_size", "ct/bad-sth", "sth"),
|
|
1484
1667
|
timestamp: _uintField(doc, "timestamp", "ct/bad-sth", "sth"),
|
|
@@ -1524,7 +1707,8 @@ async function getSth(opts) {
|
|
|
1524
1707
|
* p.verified; // -> true
|
|
1525
1708
|
*/
|
|
1526
1709
|
async function getProofByHash(opts) {
|
|
1527
|
-
|
|
1710
|
+
var door = _clientDoor(opts || {}, null);
|
|
1711
|
+
opts = door.o;
|
|
1528
1712
|
var sth = _assertSth(opts.sth, "opts.sth");
|
|
1529
1713
|
/** @internal Copied for the reason the tree head is: it is sent on the wire and then folded against the
|
|
1530
1714
|
* head after the fetch returns, so a view would let the leaf being proved differ from the leaf asked
|
|
@@ -1533,7 +1717,7 @@ async function getProofByHash(opts) {
|
|
|
1533
1717
|
if (leafHash.length !== 32) {
|
|
1534
1718
|
throw _ctErr("ct/bad-input", "opts.leafHash must be exactly 32 bytes (a v1 leaf hash), got " + leafHash.length);
|
|
1535
1719
|
}
|
|
1536
|
-
var doc = await _clientJson(
|
|
1720
|
+
var doc = await _clientJson(door,"get-proof-by-hash", "audit proof", {
|
|
1537
1721
|
hash: _bufToString(leafHash, "base64"), tree_size: _stringOf(sth.treeSize),
|
|
1538
1722
|
});
|
|
1539
1723
|
var leafIndex = _uintField(doc, "leaf_index", "ct/bad-proof", "audit proof");
|
|
@@ -1579,14 +1763,15 @@ async function getProofByHash(opts) {
|
|
|
1579
1763
|
* c.verified; // -> true
|
|
1580
1764
|
*/
|
|
1581
1765
|
async function getSthConsistency(opts) {
|
|
1582
|
-
|
|
1766
|
+
var door = _clientDoor(opts || {}, null);
|
|
1767
|
+
opts = door.o;
|
|
1583
1768
|
var oldSth = _assertSth(opts.oldSth, "opts.oldSth");
|
|
1584
1769
|
var newSth = _assertSth(opts.newSth, "opts.newSth");
|
|
1585
1770
|
if (oldSth.treeSize > newSth.treeSize) {
|
|
1586
1771
|
throw _ctErr("ct/bad-input", "opts.oldSth names tree size " + oldSth.treeSize +
|
|
1587
1772
|
", which is larger than opts.newSth's " + newSth.treeSize);
|
|
1588
1773
|
}
|
|
1589
|
-
var doc = await _clientJson(
|
|
1774
|
+
var doc = await _clientJson(door,"get-sth-consistency", "consistency proof", {
|
|
1590
1775
|
first: _stringOf(oldSth.treeSize), second: _stringOf(newSth.treeSize),
|
|
1591
1776
|
});
|
|
1592
1777
|
var consistency = _b64Array(doc, "consistency", "ct/bad-consistency", "consistency proof", 32, C.LIMITS.MERKLE_MAX_PROOF_NODES);
|
|
@@ -1636,7 +1821,8 @@ async function getSthConsistency(opts) {
|
|
|
1636
1821
|
* e.entries[0].leafInput; // -> <Buffer ...>
|
|
1637
1822
|
*/
|
|
1638
1823
|
async function getEntries(opts) {
|
|
1639
|
-
|
|
1824
|
+
var door = _clientDoor(opts || {}, null);
|
|
1825
|
+
opts = door.o;
|
|
1640
1826
|
var start = guard.range.uint64(opts.start, _ctErr, "ct/bad-input", "opts.start");
|
|
1641
1827
|
var end = guard.range.uint64(opts.end, _ctErr, "ct/bad-input", "opts.end");
|
|
1642
1828
|
if (end < start) {
|
|
@@ -1647,7 +1833,7 @@ async function getEntries(opts) {
|
|
|
1647
1833
|
throw _ctErr("ct/bad-input", "a range of " + requested + " entries exceeds the " +
|
|
1648
1834
|
C.LIMITS.CT_MAX_ENTRIES_PER_REQUEST + "-entry request cap");
|
|
1649
1835
|
}
|
|
1650
|
-
var doc = await _clientJson(
|
|
1836
|
+
var doc = await _clientJson(door,"get-entries", "entries", { start: _stringOf(start), end: _stringOf(end) });
|
|
1651
1837
|
var raw = doc.entries;
|
|
1652
1838
|
if (!intrinsic.isArray(raw)) throw _ctErr("ct/bad-entries", "the entries field is not an array");
|
|
1653
1839
|
if (intrinsic.BigInt(raw.length) > requested) {
|
|
@@ -1692,8 +1878,9 @@ async function getEntries(opts) {
|
|
|
1692
1878
|
* r.certificates.length; // -> 2
|
|
1693
1879
|
*/
|
|
1694
1880
|
async function getRoots(opts) {
|
|
1695
|
-
|
|
1696
|
-
var
|
|
1881
|
+
/** @internal This message carries no options of its own, so the door's record is all that is used. */
|
|
1882
|
+
var door = _clientDoor(opts || {}, null);
|
|
1883
|
+
var doc = await _clientJson(door, "get-roots", "roots");
|
|
1697
1884
|
return guard.verdict.of({ certificates: _b64Array(doc, "certificates", "ct/bad-roots", "roots", undefined, C.LIMITS.CT_MAX_ROOTS) });
|
|
1698
1885
|
}
|
|
1699
1886
|
|
|
@@ -1775,7 +1962,8 @@ async function addPreChain(opts) {
|
|
|
1775
1962
|
* The chain encoding, the response shape, and the refusal when the receipt does not verify are one
|
|
1776
1963
|
* rule stated once. */
|
|
1777
1964
|
async function _addChain(opts, message, entryType) {
|
|
1778
|
-
|
|
1965
|
+
var door = _clientDoor(opts || {}, "logKey");
|
|
1966
|
+
opts = door.o;
|
|
1779
1967
|
/** @internal ONE read of the option, used by the presence check here and by the copy below. Reading it
|
|
1780
1968
|
* twice let an accessor answer the two separately, presenting a non-null value to the check and an
|
|
1781
1969
|
* attacker's SPKI to the copy, and the returned receipt was then verified against a key the caller
|
|
@@ -1808,7 +1996,7 @@ async function _addChain(opts, message, entryType) {
|
|
|
1808
1996
|
: { entryType: 1, tbsCertificate: _snap(opts.tbsCertificate, "opts.tbsCertificate"),
|
|
1809
1997
|
issuerKeyHash: _snap(opts.issuerKeyHash, "opts.issuerKeyHash") };
|
|
1810
1998
|
var pinnedKey = _snap(suppliedLogKey, "opts.logKey");
|
|
1811
|
-
var doc = await _clientJson(
|
|
1999
|
+
var doc = await _clientJson(door,message, "SCT", null, _jsonStringify({ chain: chain }));
|
|
1812
2000
|
var version = doc.sct_version;
|
|
1813
2001
|
if (version !== 0) {
|
|
1814
2002
|
throw _ctErr("ct/bad-sct", "the log returned sct_version " + guard.text.showValue(version) +
|
|
@@ -2000,11 +2188,21 @@ function _b64Array(obj, name, code, label, exactLen, maxCount) {
|
|
|
2000
2188
|
* r.logs[0] && r.logs[0].trusted; // the verified, trusted-log set (the detached signature checked first)
|
|
2001
2189
|
*/
|
|
2002
2190
|
async function fetchLogList(opts) {
|
|
2003
|
-
|
|
2191
|
+
/** @internal The same door the sec. 4 verbs enter by, over this verb's own option set. Checking the
|
|
2192
|
+
* NAMES alone left every value still to be read from the caller's object, and `url` was read twice,
|
|
2193
|
+
* once to parse and once to fetch, so an accessor sent the request somewhere the parse never saw. */
|
|
2194
|
+
opts = guard.identifier.optionsObject(opts, _ctErr, "ct/bad-input", "pki.ct.fetchLogList options");
|
|
2004
2195
|
guard.identifier.assertKnownKeys(opts, KNOWN_FETCH_OPTS, _ctErr, "ct/bad-input", "unknown opts field ");
|
|
2196
|
+
/** @internal Every declared option taken before anything derives from one, for the reason
|
|
2197
|
+
* `_clientDoor` does it: there is no safe order, because reading a NESTED value inside any of them
|
|
2198
|
+
* runs the caller's code and that code can rewrite the ones not yet taken. Parsing the url first let
|
|
2199
|
+
* its `toString` empty the pinned anchor list; taking the tls first let a getter on `tls.anchors`
|
|
2200
|
+
* rewrite the url and the signer key. Taking the top level first closes both. */
|
|
2201
|
+
opts = _settleDecisive(_takeTopLevel(opts, KNOWN_FETCH_OPTS), "signerKey");
|
|
2005
2202
|
/** @internal Read once, at the door, and carried from here. Asking again would let an option that
|
|
2006
2203
|
* answers differently on each read hand back something other than the value that was checked. */
|
|
2007
2204
|
var transport = _assertTransport(opts);
|
|
2205
|
+
var fetchTls = _tlsForFetch(opts);
|
|
2008
2206
|
/** @internal ONE read, and both the presence check and the copy decide from it. Reading it twice let an
|
|
2009
2207
|
* accessor present a non-null value to the check and an attacker's SPKI to the copy, so the list was
|
|
2010
2208
|
* verified under a distributor key the caller never pinned.
|
|
@@ -2016,23 +2214,31 @@ async function fetchLogList(opts) {
|
|
|
2016
2214
|
var pinnedSigner = _toBuffer(suppliedSignerKey, "opts.signerKey", _ctErr, "ct/bad-input");
|
|
2017
2215
|
if (opts.url == null) throw _ctErr("ct/bad-input", "opts.url is required -- the log_list.json URL (there is no baked-in vendor URL)");
|
|
2018
2216
|
var jsonParsed = _parseHttpsUrl(opts.url, "URL");
|
|
2019
|
-
var jsonUrl = jsonParsed
|
|
2020
|
-
|
|
2021
|
-
|
|
2217
|
+
var jsonUrl = _urlHref(jsonParsed);
|
|
2218
|
+
/** @internal ONE read: the presence test and the parse decide from the same value. */
|
|
2219
|
+
var suppliedSigUrl = opts.sigUrl;
|
|
2220
|
+
/** @internal Through the captured accessor like every other read of a parsed destination here. No
|
|
2221
|
+
* caller code can run between the settle above and this line, the bag refusing an accessor and both
|
|
2222
|
+
* urls having to be strings, so this one has no vector of its own; it is read this way because a
|
|
2223
|
+
* site that reads a destination off the prototype is the shape that has to stay absent. */
|
|
2224
|
+
var sigUrl = suppliedSigUrl != null
|
|
2225
|
+
? _urlHref(_parseHttpsUrl(suppliedSigUrl, "signature URL"))
|
|
2226
|
+
: _sigUrlFor(jsonParsed);
|
|
2227
|
+
if (!_sameOrigin(jsonUrl, sigUrl)) throw _ctErr("ct/bad-input", "opts.sigUrl must share the log-list URL's origin (" + _urlOrigin(jsonParsed) + "); a cross-origin detached-signature host is not supported");
|
|
2022
2228
|
if (!transport) {
|
|
2023
|
-
var
|
|
2024
|
-
|
|
2025
|
-
if (!hasAnchors && t.useSystemStore !== true) throw _ctErr("ct/no-trust-anchors", "no explicit trust anchor and tls.useSystemStore not set to true -- refusing an unpinned CT server (RFC 6962 sec. 3.2)");
|
|
2229
|
+
var hasAnchors = fetchTls.anchors !== undefined && fetchTls.anchors !== null && !(intrinsic.isArray(fetchTls.anchors) && fetchTls.anchors.length === 0);
|
|
2230
|
+
if (!hasAnchors && fetchTls.useSystemStore !== true) throw _ctErr("ct/no-trust-anchors", "no explicit trust anchor and tls.useSystemStore not set to true -- refusing an unpinned CT server (RFC 6962 sec. 3.2)");
|
|
2026
2231
|
transport = httpTransport.https({ E: _ctErr, errPrefix: "ct" });
|
|
2027
2232
|
}
|
|
2028
2233
|
var timeout = guard.limits.cap(opts.timeout, "timeout", DEFAULT_FETCH_TIMEOUT, { E: _ctErr, code: "ct/bad-input", min: 1, max: MAX_FETCH_TIMEOUT });
|
|
2029
2234
|
var maxResponseBytes = guard.limits.cap(opts.maxResponseBytes, "maxResponseBytes", C.LIMITS.CT_LOG_LIST_MAX_BYTES, { E: _ctErr, code: "ct/bad-input", min: 1, max: C.LIMITS.CT_LOG_LIST_MAX_BYTES });
|
|
2030
2235
|
var headers = intrinsic.create(null);
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2236
|
+
var suppliedHeaders = opts.headers;
|
|
2237
|
+
intrinsic.forEach(guard.identifier.optionNames(suppliedHeaders), function (k) {
|
|
2238
|
+
var lk = _toLowerCase(k);
|
|
2239
|
+
if (lk !== "content-length" && lk !== "transfer-encoding") headers[k] = suppliedHeaders[k];
|
|
2034
2240
|
});
|
|
2035
|
-
var req = { headers: headers, tls:
|
|
2241
|
+
var req = { headers: headers, tls: fetchTls, timeout: timeout, maxResponseBytes: maxResponseBytes };
|
|
2036
2242
|
/** @internal Read before the request goes out, so the option that decides the check is the one the call
|
|
2037
2243
|
* was made with rather than whatever it says once the response is back. */
|
|
2038
2244
|
var requireJsonContentType = opts.requireJsonContentType === true;
|
package/lib/est.js
CHANGED
|
@@ -80,6 +80,11 @@ var _toLowerCase = intrinsic.uncurry(String.prototype.toLowerCase);
|
|
|
80
80
|
* and both build from the RECEIVER, so reading the global binding at the call decides whether the body
|
|
81
81
|
* runs and whether a refusal stays a refusal. Taken at load. */
|
|
82
82
|
var _Promise = intrinsic.Promise;
|
|
83
|
+
/** @internal The URL parser, taken at load: what it returns names the host an enrollment request goes
|
|
84
|
+
* to, and a redirect is resolved against the current operation URL with it. The conversion the parser
|
|
85
|
+
* is handed is taken with it, since a replacement answers with a host of its own. */
|
|
86
|
+
var _URL = intrinsic.URL;
|
|
87
|
+
var _stringOf = intrinsic.String;
|
|
83
88
|
var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
84
89
|
var _promiseReject = intrinsic.uncurry(intrinsic.promiseReject);
|
|
85
90
|
var pkix = require("./schema-pkix");
|
|
@@ -803,21 +808,22 @@ function _csrDer(input) {
|
|
|
803
808
|
|
|
804
809
|
function _parseUrl(urlStr) {
|
|
805
810
|
var url;
|
|
806
|
-
try { url = new
|
|
811
|
+
try { url = new _URL(_stringOf(urlStr)); }
|
|
807
812
|
catch (e) { throw E("est/bad-url", "the EST server URL did not parse: " + String(urlStr), e); }
|
|
808
813
|
if (url.protocol !== "https:") throw E("est/insecure-url", "EST requires https (RFC 7030 sec. 3.3), got " + url.protocol + " for " + urlStr);
|
|
809
814
|
return url;
|
|
810
815
|
}
|
|
811
816
|
|
|
817
|
+
/** @internal The settings this client forwards to its transport. */
|
|
818
|
+
var TLS_SETTINGS = ["anchors", "useSystemStore", "cert", "key", "minVersion", "servername", "checkServerIdentity"];
|
|
812
819
|
function _tlsForRequest(opts) {
|
|
813
|
-
|
|
814
|
-
return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
|
|
820
|
+
return guard.identifier.pickSettings(opts.tls, TLS_SETTINGS);
|
|
815
821
|
}
|
|
816
822
|
|
|
817
823
|
function _redirectTarget(current, location, method, opts) {
|
|
818
824
|
if (location == null || String(location).trim() === "") throw E("est/http-error", "a redirect response carried no Location header");
|
|
819
825
|
var resolved;
|
|
820
|
-
try { resolved = new
|
|
826
|
+
try { resolved = new _URL(_stringOf(location), current.href); }
|
|
821
827
|
catch (e) { throw E("est/bad-url", "a redirect Location did not parse: " + location, e); }
|
|
822
828
|
if (resolved.protocol !== "https:") throw E("est/insecure-redirect", "a redirect to a non-https URL is refused (RFC 7030 sec. 3.2.1): " + resolved.protocol);
|
|
823
829
|
var safeMethod = method === "GET" || method === "HEAD";
|
|
@@ -980,13 +986,13 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
980
986
|
* answer, so a verb that holds one passes it and this reads the option only when none was held. */
|
|
981
987
|
function _client(op, method, baseUrl, body, headers, opts, proxySnap, onAttempt, checked) {
|
|
982
988
|
var base;
|
|
983
|
-
try { base = new
|
|
989
|
+
try { base = new _URL(_stringOf(baseUrl)); }
|
|
984
990
|
catch (e) { throw E("est/bad-url", "the EST base URL did not parse: " + guard.text.showValue(baseUrl), e); }
|
|
985
991
|
if (base.search || base.hash) throw E("est/bad-url", "the EST base URL must not carry a query or fragment component (RFC 7030 sec. 3.2.2)");
|
|
986
992
|
var url = _parseUrl(paths(baseUrl, { label: opts.label })[op]);
|
|
987
993
|
var transport = checked !== undefined ? checked : _assertTransport(opts);
|
|
988
994
|
if (!transport) {
|
|
989
|
-
var t = opts.tls
|
|
995
|
+
var t = opts.tls != null ? opts.tls : intrinsic.create(null);
|
|
990
996
|
var hasAnchors = t.anchors !== undefined && t.anchors !== null && !(Array.isArray(t.anchors) && t.anchors.length === 0);
|
|
991
997
|
if (!hasAnchors && t.useSystemStore !== true) throw E("est/no-trust-anchors", "no explicit trust anchor and tls.useSystemStore not set to true -- refusing an unpinned server (RFC 7030 sec. 3.6)");
|
|
992
998
|
transport = httpTransport.https({ E: E, errPrefix: "est" });
|
package/lib/guard-bytes.js
CHANGED
|
@@ -247,14 +247,29 @@ function source(input, ErrorClass, code, label) {
|
|
|
247
247
|
throw _raise(ErrorClass, code, label + ": expected a BufferSource (ArrayBuffer / TypedArray / Buffer)");
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/** @internal Into a store of its own, not through `Buffer.from`. A small `Buffer.from` takes its result
|
|
251
|
+
* out of the 64 KiB allocation pool, so a caller whose own input came from that pool holds a view of
|
|
252
|
+
* the whole store and the copy lands inside it at another offset. MEASURED through a shipped verb: a
|
|
253
|
+
* caller's transport, which runs after the key is copied and before the signature is checked, found
|
|
254
|
+
* the copy in the pool it shared, wrote an attacker's SPKI over it, and the forged tree head was
|
|
255
|
+
* accepted. Every consumer of this door relies on the copy being out of the caller's reach, which is
|
|
256
|
+
* the whole reason the copy is taken. The deep copy below has always allocated its own store; this is
|
|
257
|
+
* the same allocation. */
|
|
258
|
+
function _ownedCopy(src) {
|
|
259
|
+
var len = _lengthOf(src);
|
|
260
|
+
var owned = new _ArrayBuffer(len);
|
|
261
|
+
_taSet(new _Uint8Array(owned), src);
|
|
262
|
+
return _bufferFrom(owned, 0, len);
|
|
263
|
+
}
|
|
264
|
+
|
|
250
265
|
// @enforced-by behavioral -- a copy has no rename-proof code shape to detect (any
|
|
251
266
|
function snapshot(input, ErrorClass, code, label) {
|
|
252
|
-
return
|
|
267
|
+
return _ownedCopy(view(input, ErrorClass, code, label));
|
|
253
268
|
}
|
|
254
269
|
|
|
255
270
|
// @enforced-by behavioral -- a copy has no rename-proof shape to detect; the guard
|
|
256
271
|
function snapshotSource(input, ErrorClass, code, label) {
|
|
257
|
-
return
|
|
272
|
+
return _ownedCopy(source(input, ErrorClass, code, label));
|
|
258
273
|
}
|
|
259
274
|
|
|
260
275
|
// @enforced-by behavioral -- a copy has no rename-proof code shape to detect (any `Buffer.from(x)`
|
package/lib/guard-identifier.js
CHANGED
|
@@ -124,6 +124,27 @@ function optionNames(obj, known) {
|
|
|
124
124
|
return out;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/** @internal The named settings of a caller's nested record, taken into a record with NO prototype.
|
|
128
|
+
* Each module declares its own `names`, because which settings a verb forwards is that verb's
|
|
129
|
+
* statement and a shared list would name settings for a verb that does not read them; what is shared
|
|
130
|
+
* is that the result cannot inherit. An absent record yields the same shape with every name
|
|
131
|
+
* undefined, where the empty object literal it replaces inherited from `Object.prototype`: MEASURED,
|
|
132
|
+
* a `useSystemStore` installed there was read as a caller's opt-in and the refusal of a server with
|
|
133
|
+
* no pinned anchor was skipped. A name is read off the record itself, so a caller's own defaults
|
|
134
|
+
* object on the chain still answers for it.
|
|
135
|
+
*
|
|
136
|
+
* @enforced-by behavioral -- a read into a fresh record has no rename-proof shape of its own; the
|
|
137
|
+
* conformance vectors are each module's own refusal of an unpinned server under a polluted prototype.
|
|
138
|
+
*/
|
|
139
|
+
function pickSettings(record, names) {
|
|
140
|
+
var out = _create(null);
|
|
141
|
+
var src = record === null || record === undefined ? null : record;
|
|
142
|
+
for (var i = 0; i < names.length; i++) {
|
|
143
|
+
out[names[i]] = src === null ? undefined : src[names[i]];
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
127
148
|
/** @internal A caller's options bag copied onto a prototype-less object, keyed by every own string
|
|
128
149
|
* property, not the enumerable ones alone. Each is read once, so an accessor-backed option
|
|
129
150
|
* answers the copy and not the later reads. Inherited properties are left behind: the copy is
|
|
@@ -609,6 +630,7 @@ module.exports = intrinsic.freeze({
|
|
|
609
630
|
snapshotOptions: snapshotOptions,
|
|
610
631
|
optionNames: optionNames,
|
|
611
632
|
ownOptions: ownOptions,
|
|
633
|
+
pickSettings: pickSettings,
|
|
612
634
|
readableNames: readableNames,
|
|
613
635
|
readableIndices: readableIndices
|
|
614
636
|
});
|
package/lib/guard-intrinsic.js
CHANGED
|
@@ -68,6 +68,30 @@ var _promiseAny = Promise.any;
|
|
|
68
68
|
* present on the supported runtime floor. */
|
|
69
69
|
var _promiseTry = Promise["try"];
|
|
70
70
|
var _promiseWithResolvers = Promise.withResolvers;
|
|
71
|
+
/** @internal The URL parser, captured for the reason the promise constructor is: what it returns names
|
|
72
|
+
* the host a request is sent to. A replacement installed while a call is in flight answers with a
|
|
73
|
+
* genuine URL of its own, and an endpoint built from that answer reaches a host the caller never
|
|
74
|
+
* wrote while the url the caller passed is still the one the call reports. `canParse` is captured
|
|
75
|
+
* beside it because a module that decides a form with it is deciding from the same binding. */
|
|
76
|
+
var _URL = URL;
|
|
77
|
+
var _urlCanParse = URL.canParse;
|
|
78
|
+
/** @internal And its accessors, because capturing a constructor does not capture the prototype the
|
|
79
|
+
* value it returns reads through. MEASURED: with the constructor captured, a header getter that
|
|
80
|
+
* redefined `URL.prototype.href` had every request go to the host its own getter named while the url
|
|
81
|
+
* the caller passed was still the one the call reported. The parts of a destination are read through
|
|
82
|
+
* these, and the two that build an endpoint are written through the captured setters. */
|
|
83
|
+
var _urlHref = getter(URL.prototype, "href", "URL.prototype");
|
|
84
|
+
var _urlOrigin = getter(URL.prototype, "origin", "URL.prototype");
|
|
85
|
+
var _urlProtocol = getter(URL.prototype, "protocol", "URL.prototype");
|
|
86
|
+
var _urlPathname = getter(URL.prototype, "pathname", "URL.prototype");
|
|
87
|
+
var _urlSearch = getter(URL.prototype, "search", "URL.prototype");
|
|
88
|
+
var _urlHostname = getter(URL.prototype, "hostname", "URL.prototype");
|
|
89
|
+
var _urlPort = getter(URL.prototype, "port", "URL.prototype");
|
|
90
|
+
var _urlUsername = getter(URL.prototype, "username", "URL.prototype");
|
|
91
|
+
var _urlPassword = getter(URL.prototype, "password", "URL.prototype");
|
|
92
|
+
var _urlSearchParams = getter(URL.prototype, "searchParams", "URL.prototype");
|
|
93
|
+
var _urlSetPathname = setter(URL.prototype, "pathname", "URL.prototype");
|
|
94
|
+
var _urlSetSearch = setter(URL.prototype, "search", "URL.prototype");
|
|
71
95
|
var _asyncIterator = Symbol.asyncIterator;
|
|
72
96
|
|
|
73
97
|
var _arrForEach = uncurry(Array.prototype.forEach);
|
|
@@ -160,6 +184,18 @@ function getter(proto, name, who) {
|
|
|
160
184
|
return uncurry(d.get);
|
|
161
185
|
}
|
|
162
186
|
|
|
187
|
+
/** @internal The write half of the same question: a replaced setter can drop the write and leave the
|
|
188
|
+
* value it was given to change unchanged. */
|
|
189
|
+
// @enforced-by behavioral -- reading a descriptor's `set` is not distinguishable from testing one
|
|
190
|
+
function setter(proto, name, who) {
|
|
191
|
+
var d = _getOwnPropertyDescriptor(proto, name);
|
|
192
|
+
if (!d || typeof d.set !== "function") {
|
|
193
|
+
throw new TypeError("guard.intrinsic.setter: this runtime has no intrinsic " + who + "." +
|
|
194
|
+
name + " accessor, so the value's own would have to be invoked instead");
|
|
195
|
+
}
|
|
196
|
+
return uncurry(d.set);
|
|
197
|
+
}
|
|
198
|
+
|
|
163
199
|
var _taByteLength = getter(_getPrototypeOf(Uint8Array.prototype), "byteLength", "%TypedArray%.prototype");
|
|
164
200
|
var _dvByteLength = getter(DataView.prototype, "byteLength", "DataView.prototype");
|
|
165
201
|
|
|
@@ -231,6 +267,21 @@ module.exports = Object.freeze({
|
|
|
231
267
|
promiseAny: _promiseAny,
|
|
232
268
|
promiseTry: _promiseTry,
|
|
233
269
|
promiseWithResolvers: _promiseWithResolvers,
|
|
270
|
+
setter: setter,
|
|
271
|
+
URL: _URL,
|
|
272
|
+
urlCanParse: _urlCanParse,
|
|
273
|
+
urlHref: _urlHref,
|
|
274
|
+
urlOrigin: _urlOrigin,
|
|
275
|
+
urlProtocol: _urlProtocol,
|
|
276
|
+
urlPathname: _urlPathname,
|
|
277
|
+
urlSearch: _urlSearch,
|
|
278
|
+
urlHostname: _urlHostname,
|
|
279
|
+
urlPort: _urlPort,
|
|
280
|
+
urlUsername: _urlUsername,
|
|
281
|
+
urlPassword: _urlPassword,
|
|
282
|
+
urlSearchParams: _urlSearchParams,
|
|
283
|
+
urlSetPathname: _urlSetPathname,
|
|
284
|
+
urlSetSearch: _urlSetSearch,
|
|
234
285
|
asyncIterator: _asyncIterator,
|
|
235
286
|
forEach: _arrForEach,
|
|
236
287
|
map: _arrMap,
|
package/lib/http-digest.js
CHANGED
|
@@ -7,6 +7,8 @@ var crypto = require("crypto");
|
|
|
7
7
|
var constants = require("./constants");
|
|
8
8
|
var intrinsic = require("./guard-intrinsic");
|
|
9
9
|
var _hasOwn = intrinsic.hasOwn;
|
|
10
|
+
/** @internal The URL parser, taken at load: the origin it reports is what a challenge is bound to. */
|
|
11
|
+
var _URL = intrinsic.URL;
|
|
10
12
|
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
11
13
|
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
12
14
|
var _fromCharCode = intrinsic.fromCharCode;
|
|
@@ -293,7 +295,7 @@ function answer(challenge, params, E) {
|
|
|
293
295
|
function _domainEntry(d) {
|
|
294
296
|
d = String(d == null ? "" : d);
|
|
295
297
|
if (d.charAt(0) === "/") return { origin: null, full: d };
|
|
296
|
-
try { var u = new
|
|
298
|
+
try { var u = new _URL(d); return { origin: u.origin.toLowerCase(), full: (u.pathname || "/") + (u.search || "") }; }
|
|
297
299
|
catch (_e) { return null; }
|
|
298
300
|
}
|
|
299
301
|
|
package/lib/http-transport.js
CHANGED
|
@@ -79,6 +79,21 @@ var intrinsic = require("./guard-intrinsic");
|
|
|
79
79
|
* step rejected proceed, and `peerChain`'s refusal to tunnel through a proxy is one of those. Taken at
|
|
80
80
|
* load. */
|
|
81
81
|
var _Promise = intrinsic.Promise;
|
|
82
|
+
/** @internal The URL parser, taken at load: what it returns names the host the socket is opened to,
|
|
83
|
+
* for the request itself and for a proxy it is tunneled through. The conversion it is handed is taken
|
|
84
|
+
* with it, since a replacement answers with a host of its own. */
|
|
85
|
+
var _URL = intrinsic.URL;
|
|
86
|
+
var _stringOf = intrinsic.String;
|
|
87
|
+
/** @internal The parts of that destination are read through the accessors captured at load, not off the
|
|
88
|
+
* prototype the parsed value reads through: a replaced `URL.prototype.hostname` names the host this
|
|
89
|
+
* module then opens a socket to. */
|
|
90
|
+
var _urlProtocol = intrinsic.urlProtocol;
|
|
91
|
+
var _urlHostname = intrinsic.urlHostname;
|
|
92
|
+
var _urlPort = intrinsic.urlPort;
|
|
93
|
+
var _urlPathname = intrinsic.urlPathname;
|
|
94
|
+
var _urlSearch = intrinsic.urlSearch;
|
|
95
|
+
var _urlUsername = intrinsic.urlUsername;
|
|
96
|
+
var _urlPassword = intrinsic.urlPassword;
|
|
82
97
|
var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
83
98
|
var _promiseReject = intrinsic.uncurry(intrinsic.promiseReject);
|
|
84
99
|
var frameworkError = require("./framework-error");
|
|
@@ -331,7 +346,10 @@ function snapshotProxy(p) {
|
|
|
331
346
|
}
|
|
332
347
|
}
|
|
333
348
|
|
|
334
|
-
|
|
349
|
+
/** @internal `originHost`, `statedPort` and `scheme` are the values the request path already read off
|
|
350
|
+
* the parsed origin; they are passed in rather than read again here, so the authority this builds for
|
|
351
|
+
* the CONNECT target is the same authority the socket options were built from. */
|
|
352
|
+
function _validateProxy(request, defaults, E, C, originHost, originStatedPort, originScheme) {
|
|
335
353
|
var proxy = request.proxy !== undefined ? request.proxy : defaults.proxy;
|
|
336
354
|
if (proxy === undefined || proxy === null) return null;
|
|
337
355
|
guard.identifier.assertPlainRecord(proxy, E, C("bad-proxy"), "opts.proxy");
|
|
@@ -339,11 +357,12 @@ function _validateProxy(request, defaults, E, C, url) {
|
|
|
339
357
|
guard.identifier.assertKnownKeys(proxy, PROXY_KEYS, E, C("bad-proxy"), "opts.proxy has an unknown option ");
|
|
340
358
|
if (typeof proxy.url !== "string") throw E(C("bad-proxy"), "opts.proxy.url must be a string, got " + guard.text.showValue(proxy.url));
|
|
341
359
|
var purl;
|
|
342
|
-
try { purl = new
|
|
360
|
+
try { purl = new _URL(proxy.url); }
|
|
343
361
|
catch (e) { throw E(C("bad-proxy"), "opts.proxy.url did not parse: " + guard.text.showValue(proxy.url), e); }
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
362
|
+
var pscheme = _urlProtocol(purl);
|
|
363
|
+
if (pscheme !== "http:" && pscheme !== "https:") throw E(C("bad-proxy"), "opts.proxy.url must be an http or https URL, got " + pscheme);
|
|
364
|
+
if (_urlUsername(purl) || _urlPassword(purl)) throw E(C("bad-proxy"), "opts.proxy.url must not carry credentials in its userinfo; supply opts.proxy.auth over an https proxy");
|
|
365
|
+
var secure = pscheme === "https:";
|
|
347
366
|
if (!secure && proxy.tls !== undefined && proxy.tls !== null) throw E(C("bad-proxy"), "opts.proxy.tls applies only to an https proxy; an http proxy connection is plaintext (a supplied tls policy would be silently ignored)");
|
|
348
367
|
|
|
349
368
|
var auth = null;
|
|
@@ -396,14 +415,14 @@ function _validateProxy(request, defaults, E, C, url) {
|
|
|
396
415
|
proxyTls = { ca: pCa.length ? pCa : undefined, minVersion: pMinVersion, servername: (ptls && ptls.servername) || null };
|
|
397
416
|
}
|
|
398
417
|
|
|
399
|
-
var originHost = _stripIpv6Brackets(url.hostname);
|
|
400
418
|
var originAuthHost = nodeNet.isIP(originHost) === 6 ? "[" + originHost + "]" : originHost;
|
|
401
|
-
var phost = _stripIpv6Brackets(purl
|
|
419
|
+
var phost = _stripIpv6Brackets(_urlHostname(purl));
|
|
402
420
|
/** @internal The CONNECT target is the origin's own authority, so an origin URL that states no
|
|
403
421
|
* port takes the default for the origin's scheme, not the proxy's. Defaulting it to 443 opens
|
|
404
422
|
* the tunnel to a port an `http:` origin does not listen on. */
|
|
405
|
-
var originPort =
|
|
406
|
-
|
|
423
|
+
var originPort = originStatedPort || (originScheme === "http:" ? 80 : 443);
|
|
424
|
+
var pport = _urlPort(purl);
|
|
425
|
+
return { secure: secure, host: phost, port: pport ? +pport : (secure ? 443 : 80), originHost: originHost, originAuthority: originAuthHost + ":" + originPort, auth: auth, proxyTls: proxyTls };
|
|
407
426
|
}
|
|
408
427
|
|
|
409
428
|
function _basicProxyHeader(auth) {
|
|
@@ -484,11 +503,14 @@ function httpsTransport(defaults) {
|
|
|
484
503
|
var E = typeof defaults.E === "function" ? defaults.E : defaultE;
|
|
485
504
|
var prefix = defaults.errPrefix || "transport";
|
|
486
505
|
function C(name) { return prefix + "/" + name; }
|
|
487
|
-
|
|
506
|
+
/** @internal No prototype where no TLS was bound: an empty object literal inherits from
|
|
507
|
+
* `Object.prototype`, so a value installed there would answer for a setting nobody set, and one of
|
|
508
|
+
* the settings read from this record is the opt-in that turns away an unpinned server. */
|
|
509
|
+
var tlsDefaults = defaults.tls != null ? defaults.tls : intrinsic.create(null);
|
|
488
510
|
|
|
489
511
|
function _prepare(request) {
|
|
490
512
|
var url;
|
|
491
|
-
try { url = new
|
|
513
|
+
try { url = new _URL(_stringOf(request.url)); }
|
|
492
514
|
catch (e) { throw E(C("bad-url"), "the request URL did not parse: " + String(request.url), e); }
|
|
493
515
|
/** @internal https is the floor, and `request.allowPlaintextHttp` is the one thing that moves
|
|
494
516
|
* it, per request and never as a default. It exists for the two objects whose integrity does
|
|
@@ -499,11 +521,16 @@ function httpsTransport(defaults) {
|
|
|
499
521
|
* timeout, and the refusal to follow a redirect, which is what would otherwise turn one
|
|
500
522
|
* plaintext hop into a downgrade of the next. */
|
|
501
523
|
var plaintextAllowed = request.allowPlaintextHttp === true;
|
|
502
|
-
|
|
503
|
-
|
|
524
|
+
/** @internal Read ONCE, through the captured accessor, and every decision below made from that
|
|
525
|
+
* string. The scheme decides whether the request is allowed at all, whether an anchor is
|
|
526
|
+
* required and which port it defaults to, so a prototype whose getter answers each read
|
|
527
|
+
* differently would have those three agree on nothing. */
|
|
528
|
+
var scheme = _urlProtocol(url);
|
|
529
|
+
if (scheme !== "https:" && !(plaintextAllowed && scheme === "http:")) {
|
|
530
|
+
throw E(C("insecure-url"), "transport requires https (RFC 7030 sec. 3.3), got " + scheme);
|
|
504
531
|
}
|
|
505
532
|
|
|
506
|
-
var reqTls = request.tls
|
|
533
|
+
var reqTls = request.tls != null ? request.tls : intrinsic.create(null);
|
|
507
534
|
var anchors = reqTls.anchors !== undefined ? reqTls.anchors : tlsDefaults.anchors;
|
|
508
535
|
var useSystem = (reqTls.useSystemStore !== undefined ? reqTls.useSystemStore : tlsDefaults.useSystemStore) === true;
|
|
509
536
|
var hasAnchors = anchors !== undefined && anchors !== null && !(Array.isArray(anchors) && anchors.length === 0);
|
|
@@ -511,7 +538,7 @@ function httpsTransport(defaults) {
|
|
|
511
538
|
* neither an anchor nor the system store is refused rather than left unpinned. A plaintext hop
|
|
512
539
|
* negotiates no certificate, so there is nothing for an anchor to pin; what protects the object
|
|
513
540
|
* fetched over one is its own signature, checked after the fetch by the verb that asked. */
|
|
514
|
-
if (
|
|
541
|
+
if (scheme === "https:" && !hasAnchors && !useSystem) {
|
|
515
542
|
throw E(C("no-trust-anchors"), "no explicit trust anchor and useSystemStore not set -- refusing an unpinned server (RFC 7030 sec. 3.6)");
|
|
516
543
|
}
|
|
517
544
|
|
|
@@ -534,13 +561,14 @@ function httpsTransport(defaults) {
|
|
|
534
561
|
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
535
562
|
}
|
|
536
563
|
|
|
537
|
-
var host = _stripIpv6Brackets(url
|
|
538
|
-
var plaintext =
|
|
564
|
+
var host = _stripIpv6Brackets(_urlHostname(url));
|
|
565
|
+
var plaintext = scheme === "http:";
|
|
566
|
+
var statedPort = _urlPort(url);
|
|
539
567
|
var options = {
|
|
540
568
|
method: request.method || "GET",
|
|
541
569
|
hostname: host,
|
|
542
|
-
port:
|
|
543
|
-
path: url
|
|
570
|
+
port: statedPort || (plaintext ? 80 : 443),
|
|
571
|
+
path: _urlPathname(url) + _urlSearch(url),
|
|
544
572
|
headers: reqHeaders,
|
|
545
573
|
minVersion: minVersion,
|
|
546
574
|
rejectUnauthorized: true,
|
|
@@ -566,7 +594,7 @@ function httpsTransport(defaults) {
|
|
|
566
594
|
};
|
|
567
595
|
}
|
|
568
596
|
var proxyInfo;
|
|
569
|
-
try { proxyInfo = _validateProxy(request, defaults, E, C,
|
|
597
|
+
try { proxyInfo = _validateProxy(request, defaults, E, C, host, statedPort, scheme); }
|
|
570
598
|
catch (e) {
|
|
571
599
|
var typedProxyError = false;
|
|
572
600
|
try {
|
package/lib/ocsp.js
CHANGED
|
@@ -32,6 +32,8 @@ var _ocspIntrinsic = require("./guard-intrinsic");
|
|
|
32
32
|
* the construction to whatever that binding holds, and one settling without awaiting its argument runs
|
|
33
33
|
* the continuation before the check has finished. */
|
|
34
34
|
var _Promise = _ocspIntrinsic.Promise;
|
|
35
|
+
/** @internal The URL parser, taken at load: the href it reports is the responder a request is sent to. */
|
|
36
|
+
var _URL = _ocspIntrinsic.URL;
|
|
35
37
|
var _promiseResolve = _ocspIntrinsic.uncurry(_ocspIntrinsic.promiseResolve);
|
|
36
38
|
/** @internal A refusal returned as a REJECTED promise is a verdict too: read from the global binding, a
|
|
37
39
|
* replacement whose `reject` resolves instead turns each `ocsp/bad-input` below into a pass. */
|
|
@@ -921,7 +923,7 @@ function httpRequest(der, responderUrl, opts) {
|
|
|
921
923
|
* the string the sec. 5 size bound has to be measured on. The appended segment is base64 under
|
|
922
924
|
* the sec. 2.3 unreserved set, which holds no `.`, so it cannot become a dot segment itself. */
|
|
923
925
|
var normalized;
|
|
924
|
-
try { normalized = new
|
|
926
|
+
try { normalized = new _URL(responderUrl).href; }
|
|
925
927
|
catch (e) { throw E("ocsp/bad-input", "httpRequest: the responder URL does not parse as a URL: " + guard.text.describeThrown(e), e); }
|
|
926
928
|
var base = normalized.charAt(normalized.length - 1) === "/"
|
|
927
929
|
? normalized : normalized + "/";
|
package/lib/path-validate.js
CHANGED
|
@@ -67,6 +67,9 @@ var _reject = intrinsic.uncurry(intrinsic.promiseReject);
|
|
|
67
67
|
* whatever the binding holds: a descriptor failure answering `false` through a replaced constructor is
|
|
68
68
|
* not a refusal, and this module's verdicts are what a caller acts on. */
|
|
69
69
|
var _Promise = intrinsic.Promise;
|
|
70
|
+
/** @internal The URL parser, taken at load: the host it reports from an access descriptor is the host a
|
|
71
|
+
* revocation fetch would be directed to. */
|
|
72
|
+
var _URL = intrinsic.URL;
|
|
70
73
|
var constants = require("./constants");
|
|
71
74
|
var validator = require("./validator-all");
|
|
72
75
|
var cms = require("./schema-cms");
|
|
@@ -3146,7 +3149,7 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3146
3149
|
if (ad.accessMethod !== OID.caIssuers) continue;
|
|
3147
3150
|
if (!ad.accessLocation || ad.accessLocation.tag !== 6) continue;
|
|
3148
3151
|
var u;
|
|
3149
|
-
try { u = new
|
|
3152
|
+
try { u = new _URL(ad.accessLocation.value); }
|
|
3150
3153
|
catch (_e) { continue; }
|
|
3151
3154
|
if (u.protocol !== "https:") continue;
|
|
3152
3155
|
if (_isBlockedAiaHost(u.hostname)) continue;
|
|
@@ -3348,7 +3351,7 @@ async function build(leaf, opts) {
|
|
|
3348
3351
|
* hostname taken from a certificate would be opened on that promise.
|
|
3349
3352
|
* @guard-via guard.identifier.declaresOwn */
|
|
3350
3353
|
transportGuardsAddresses: guard.identifier.declaresOwn(aiaTransport, "blocksPrivateAddresses"),
|
|
3351
|
-
tls: opts.tls
|
|
3354
|
+
tls: opts.tls != null ? opts.tls : intrinsic.create(null),
|
|
3352
3355
|
timeout: opts.aiaTimeout,
|
|
3353
3356
|
maxResponseBytes: guard.limits.cap(opts.maxResponseBytes, "build: opts.maxResponseBytes", constants.LIMITS.PATH_AIA_MAX_RESPONSE_BYTES, { E: E, code: "path/bad-input", min: 1, max: constants.LIMITS.PATH_AIA_MAX_RESPONSE_BYTES }),
|
|
3354
3357
|
maxPerCert: guard.limits.cap(opts.maxAiaPerCert, "build: opts.maxAiaPerCert", constants.LIMITS.PATH_AIA_MAX_PER_CERT, { E: E, code: "path/bad-input", min: 0 }),
|
package/lib/revocation-fetch.js
CHANGED
|
@@ -31,6 +31,9 @@ var OID_AD_OCSP = oid.byName("ocsp");
|
|
|
31
31
|
var OID_OCSP_NONCE = oid.byName("ocspNonce");
|
|
32
32
|
|
|
33
33
|
var _append = guard.list.append;
|
|
34
|
+
/** @internal The URL parser, taken at load: the host it reports is the distribution point a fetch is
|
|
35
|
+
* directed to. */
|
|
36
|
+
var _URL = intrinsic.URL;
|
|
34
37
|
var LIMITS = constants.LIMITS;
|
|
35
38
|
|
|
36
39
|
var CHECKER_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
@@ -162,7 +165,7 @@ function _ocspCandidates(descriptions, net_, skips) {
|
|
|
162
165
|
function _acceptableUrl(value, net_) {
|
|
163
166
|
if (typeof value !== "string" || value === "") return _no("is not one this client fetches");
|
|
164
167
|
var u;
|
|
165
|
-
try { u = new
|
|
168
|
+
try { u = new _URL(value); }
|
|
166
169
|
catch (_e) { return _no("is not one this client fetches"); } // allow:swallow-unverified a value that is not a URL names no location
|
|
167
170
|
if (u.protocol !== "https:" && !(net_.allowHttp && u.protocol === "http:")) return _no("is not one this client fetches");
|
|
168
171
|
var host = u.hostname;
|
package/lib/scep.js
CHANGED
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
|
|
38
38
|
var frameworkError = require("./framework-error");
|
|
39
39
|
var ScepError = frameworkError.ScepError;
|
|
40
|
+
var intrinsic = require("./guard-intrinsic");
|
|
41
|
+
/** @internal The URL parser, taken at load: what it returns names the host an operation is sent to,
|
|
42
|
+
* and a redirect is resolved against the current operation URL with it. The conversion the parser is
|
|
43
|
+
* handed is taken with it, since a replacement answers with a host of its own. */
|
|
44
|
+
var _URL = intrinsic.URL;
|
|
45
|
+
var _stringOf = intrinsic.String;
|
|
40
46
|
var oid = require("./oid");
|
|
41
47
|
var asn1 = require("./asn1-der");
|
|
42
48
|
var b = asn1.build;
|
|
@@ -661,14 +667,15 @@ function _responseLen(body) {
|
|
|
661
667
|
if (Buffer.isBuffer(body)) return guard.bytes.lengthOf(body);
|
|
662
668
|
return guard.bytes.lengthOf(guard.bytes.source(body, ScepError, "scep/bad-response", "the response body"));
|
|
663
669
|
}
|
|
670
|
+
/** @internal The settings this client forwards to its transport. */
|
|
671
|
+
var TLS_SETTINGS = ["anchors", "useSystemStore", "cert", "key", "minVersion", "servername", "checkServerIdentity"];
|
|
664
672
|
function _tlsForRequest(opts) {
|
|
665
|
-
|
|
666
|
-
return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
|
|
673
|
+
return guard.identifier.pickSettings(opts.tls, TLS_SETTINGS);
|
|
667
674
|
}
|
|
668
675
|
function _redirectTarget(current, location, method, allowCrossOrigin) {
|
|
669
676
|
if (location == null || _trim(String(location)) === "") throw _err("scep/bad-response", "a redirect response carried no Location header");
|
|
670
677
|
var resolved;
|
|
671
|
-
try { resolved = new
|
|
678
|
+
try { resolved = new _URL(_stringOf(location), current.href); }
|
|
672
679
|
catch (e) { throw _err("scep/bad-url", "a redirect Location did not parse: " + location, e); }
|
|
673
680
|
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") throw _err("scep/bad-url", "a redirect to a non-HTTP scheme is refused: " + resolved.protocol);
|
|
674
681
|
if (current.protocol === "https:" && resolved.protocol !== "https:") throw _err("scep/insecure-redirect", "a redirect from https to " + resolved.protocol + " is refused");
|
|
@@ -678,7 +685,7 @@ function _redirectTarget(current, location, method, allowCrossOrigin) {
|
|
|
678
685
|
}
|
|
679
686
|
function _scepUrl(baseUrl, operation) {
|
|
680
687
|
var url;
|
|
681
|
-
try { url = new
|
|
688
|
+
try { url = new _URL(_stringOf(baseUrl)); }
|
|
682
689
|
catch (e) { throw _err("scep/bad-url", "the SCEP base URL did not parse: " + _showValue(baseUrl), e); }
|
|
683
690
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw _err("scep/bad-url", "the SCEP base URL must be http or https, got " + url.protocol);
|
|
684
691
|
if (url.hash) throw _err("scep/bad-url", "the SCEP base URL must not carry a fragment");
|
|
@@ -693,7 +700,7 @@ function _pkiMethod(opts) {
|
|
|
693
700
|
}
|
|
694
701
|
function _pkiRequest(method, opUrl, reqMsg, clientOpts) {
|
|
695
702
|
if (method === "GET") {
|
|
696
|
-
var getUrl = new
|
|
703
|
+
var getUrl = new _URL(opUrl.href);
|
|
697
704
|
getUrl.searchParams.set("message", Buffer.from(reqMsg).toString("base64"));
|
|
698
705
|
return _client("GET", getUrl, null, {}, clientOpts);
|
|
699
706
|
}
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:4d5a42e5-0e36-46fe-8ef5-f6f19d7c2347",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
8
|
+
"timestamp": "2026-10-05T11:51:03.115Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.8.50",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.50",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.8.50",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.8.
|
|
57
|
+
"ref": "@blamejs/pki@0.8.50",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|