@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 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 URL(v); }
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 URL(s); }
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 URL(loc); }
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 URL(loc, base); }
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
- var t = o.tls || {};
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 !URL.canParse("http://[" + interior + "]/");
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 URL(base), baseOrigin = baseUrl.origin, baseHref = baseUrl.href;
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 URL(lv.anchor, base); }
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 URL(resolved);
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 URL(dirUrl).origin;
1959
+ var trustedOrigin = new _URL(dirUrl).origin;
1954
1960
  function _tlsFor(url) {
1955
1961
  var t = budgets.tls;
1956
- if (new URL(url).origin !== trustedOrigin) {
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 URL(h["location"], url).href; }
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 URL(base);
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
- var t = opts.tls || {};
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 URL(String(url)); }
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 URL(String(base)); }
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
- var t = opts.tls || {};
1176
- return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
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 URL(a).origin === new URL(b).origin; }
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
- try { parsed = new URL(String(u)); }
1192
- catch (e) { throw _ctErr("ct/bad-url", "the CT log-list " + label + " did not parse: " + String(u), e); }
1193
- if (parsed.protocol !== "https:") throw _ctErr("ct/insecure-url", "the CT log-list " + label + " must be https, got " + parsed.protocol + " for " + String(u));
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
- if (!_endsWithJson(parsed.pathname)) throw _ctErr("ct/bad-input", "cannot derive the detached-signature URL from a non-.json path (" + JSON.stringify(parsed.pathname) + "); pass opts.sigUrl explicitly");
1199
- var sig = new URL(parsed.href);
1200
- sig.pathname = _strSlice(parsed.pathname, 0, parsed.pathname.length - _JSON_SUFFIX.length) + ".sig";
1201
- return sig.href;
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.pathname;
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.href);
1250
- out.pathname = path + tail + message;
1251
- out.search = "";
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
- function _clientRequest(opts, label) {
1259
- guard.identifier.assertKnownKeys(opts, KNOWN_CLIENT_OPTS, _ctErr, "ct/bad-input", "unknown opts field ");
1260
- var transport = _assertTransport(opts);
1261
- if (opts.url == null) throw _ctErr("ct/bad-input", "opts.url is required -- the log's base URL (there is no baked-in log)");
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 t = opts.tls || {};
1264
- var hasAnchors = t.anchors !== undefined && t.anchors !== null && !(intrinsic.isArray(t.anchors) && t.anchors.length === 0);
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(opts.timeout, "timeout", DEFAULT_FETCH_TIMEOUT, { E: _ctErr, code: "ct/bad-input", min: 1, max: MAX_FETCH_TIMEOUT });
1271
- 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 });
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
- guard.identifier.optionNames(opts.headers).forEach(function (k) {
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] = opts.headers[k];
1455
+ if (lk !== "content-length" && lk !== "transfer-encoding") headers[k] = supplied[k];
1276
1456
  });
1277
- return { transport: transport, req: { headers: headers, tls: _tlsForFetch(opts), timeout: timeout,
1278
- maxResponseBytes: maxResponseBytes }, label: label };
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(opts, message, label, query, body) {
1285
- var ctx = _clientRequest(opts, label);
1286
- var url = _endpoint(opts.url, message);
1287
- if (query) intrinsic.keys(query).forEach(function (k) { _searchSet(url.searchParams, k, query[k]); });
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.href, req, label);
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
- opts = opts || {};
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(opts, "get-sth", "sth");
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
- opts = opts || {};
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(opts, "get-proof-by-hash", "audit proof", {
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
- opts = opts || {};
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(opts, "get-sth-consistency", "consistency proof", {
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
- opts = opts || {};
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(opts, "get-entries", "entries", { start: _stringOf(start), end: _stringOf(end) });
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
- opts = opts || {};
1696
- var doc = await _clientJson(opts, "get-roots", "roots");
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
- opts = opts || {};
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(opts, message, "SCT", null, _jsonStringify({ chain: chain }));
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
- opts = opts || {};
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.href;
2020
- var sigUrl = opts.sigUrl != null ? _parseHttpsUrl(opts.sigUrl, "signature URL").href : _sigUrlFor(jsonParsed);
2021
- if (!_sameOrigin(jsonUrl, sigUrl)) throw _ctErr("ct/bad-input", "opts.sigUrl must share the log-list URL's origin (" + jsonParsed.origin + "); a cross-origin detached-signature host is not supported");
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 t = opts.tls || {};
2024
- var hasAnchors = t.anchors !== undefined && t.anchors !== null && !(Array.isArray(t.anchors) && t.anchors.length === 0);
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
- guard.identifier.optionNames(opts.headers).forEach(function (k) {
2032
- var lk = k.toLowerCase();
2033
- if (lk !== "content-length" && lk !== "transfer-encoding") headers[k] = opts.headers[k];
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: _tlsForFetch(opts), timeout: timeout, maxResponseBytes: maxResponseBytes };
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 URL(String(urlStr)); }
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
- var t = opts.tls || {};
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 URL(String(location), current.href); }
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 URL(String(baseUrl)); }
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" });
@@ -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 _bufferFrom(view(input, ErrorClass, code, label));
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 _bufferFrom(source(input, ErrorClass, code, label));
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)`
@@ -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
  });
@@ -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,
@@ -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 URL(d); return { origin: u.origin.toLowerCase(), full: (u.pathname || "/") + (u.search || "") }; }
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
 
@@ -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
- function _validateProxy(request, defaults, E, C, url) {
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 URL(proxy.url); }
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
- if (purl.protocol !== "http:" && purl.protocol !== "https:") throw E(C("bad-proxy"), "opts.proxy.url must be an http or https URL, got " + purl.protocol);
345
- if (purl.username || purl.password) throw E(C("bad-proxy"), "opts.proxy.url must not carry credentials in its userinfo; supply opts.proxy.auth over an https proxy");
346
- var secure = purl.protocol === "https:";
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.hostname);
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 = url.port || (url.protocol === "http:" ? 80 : 443);
406
- return { secure: secure, host: phost, port: purl.port ? +purl.port : (secure ? 443 : 80), originHost: originHost, originAuthority: originAuthHost + ":" + originPort, auth: auth, proxyTls: proxyTls };
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
- var tlsDefaults = defaults.tls || {};
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 URL(String(request.url)); }
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
- if (url.protocol !== "https:" && !(plaintextAllowed && url.protocol === "http:")) {
503
- throw E(C("insecure-url"), "transport requires https (RFC 7030 sec. 3.3), got " + url.protocol);
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 (url.protocol === "https:" && !hasAnchors && !useSystem) {
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.hostname);
538
- var plaintext = url.protocol === "http:";
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: url.port || (plaintext ? 80 : 443),
543
- path: url.pathname + url.search,
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, url); }
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 URL(responderUrl).href; }
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 + "/";
@@ -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 URL(ad.accessLocation.value); }
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 }),
@@ -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 URL(value); }
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
- var t = opts.tls || {};
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 URL(String(location), current.href); }
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 URL(String(baseUrl)); }
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 URL(opUrl.href);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.48",
3
+ "version": "0.8.50",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:0f0eb9a3-9ce3-42bb-ba73-3ad3528eccf7",
5
+ "serialNumber": "urn:uuid:4d5a42e5-0e36-46fe-8ef5-f6f19d7c2347",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-05T06:06:36.873Z",
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.48",
22
+ "bom-ref": "@blamejs/pki@0.8.50",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.48",
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.48",
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.48",
57
+ "ref": "@blamejs/pki@0.8.50",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]