@blamejs/pki 0.8.49 → 0.8.51

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/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");
@@ -1177,14 +1187,42 @@ var KNOWN_FETCH_OPTS = intrinsic.assign(intrinsic.create(null), { url: 1, signer
1177
1187
  * and rewrite the servername after the check had passed: the anchors checked were then not the anchors
1178
1188
  * the connection used. Copying the list is what closes that, since emptying an array the caller still
1179
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
+
1180
1204
  function _tlsForFetch(opts) {
1181
- var t = opts.tls || {};
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);
1182
1213
  var anchors = t.anchors;
1183
- if (intrinsic.isArray(anchors)) anchors = intrinsic.arraySlice(anchors);
1184
- return { anchors: anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
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 };
1185
1223
  }
1186
1224
 
1187
- 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)); }
1188
1226
 
1189
1227
  /** @internal Refused at the door and again where the value is used. */
1190
1228
  function _assertTransport(opts) {
@@ -1196,17 +1234,20 @@ function _fetchCtypeToken(headers) { return String((headers || {})["content-type
1196
1234
 
1197
1235
  function _parseHttpsUrl(u, label) {
1198
1236
  var parsed;
1199
- try { parsed = new URL(String(u)); }
1200
- catch (e) { throw _ctErr("ct/bad-url", "the CT log-list " + label + " did not parse: " + String(u), e); }
1201
- 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);
1202
1242
  return parsed;
1203
1243
  }
1204
1244
 
1205
1245
  function _sigUrlFor(parsed) {
1206
- 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");
1207
- var sig = new URL(parsed.href);
1208
- sig.pathname = _strSlice(parsed.pathname, 0, parsed.pathname.length - _JSON_SUFFIX.length) + ".sig";
1209
- 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);
1210
1251
  }
1211
1252
 
1212
1253
  function _fetchBody(transport, url, req, label) {
@@ -1239,6 +1280,10 @@ var STH_PREIMAGE_BYTES = 50;
1239
1280
  var CT_VERSION_V1 = 0;
1240
1281
  var SIG_TYPE_TREE_HASH = 1;
1241
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. */
1242
1287
  var KNOWN_CLIENT_OPTS = intrinsic.assign(intrinsic.create(null), {
1243
1288
  url: 1, logKey: 1, transport: 1, tls: 1, headers: 1, timeout: 1, maxResponseBytes: 1,
1244
1289
  sth: 1, oldSth: 1, newSth: 1, leafHash: 1, start: 1, end: 1, chain: 1,
@@ -1249,14 +1294,14 @@ var KNOWN_CLIENT_OPTS = intrinsic.assign(intrinsic.create(null), {
1249
1294
  * with or without the `ct/v1/` prefix the specification fixes, and neither spelling doubles it. */
1250
1295
  function _endpoint(url, message) {
1251
1296
  var parsed = _parseHttpsUrl(url, "log URL");
1252
- var path = parsed.pathname;
1297
+ var path = _urlPathname(parsed);
1253
1298
  if (path.length === 0 || path[path.length - 1] !== "/") path += "/";
1254
1299
  var tail = CT_V1_PREFIX;
1255
1300
  if (path.length >= CT_V1_PREFIX.length + 1 &&
1256
1301
  _strSlice(path, path.length - CT_V1_PREFIX.length) === CT_V1_PREFIX) tail = "";
1257
- var out = new _URL(parsed.href);
1258
- out.pathname = path + tail + message;
1259
- out.search = "";
1302
+ var out = new _URL(_urlHref(parsed));
1303
+ _urlSetPathname(out, path + tail + message);
1304
+ _urlSetSearch(out, "");
1260
1305
  return out;
1261
1306
  }
1262
1307
 
@@ -1268,7 +1313,13 @@ function _endpoint(url, message) {
1268
1313
  * would be a pin approved on one object and a socket opened under another. Reading from the record
1269
1314
  * afterwards means the value that was checked is the value that is used. Every verb calls this at its
1270
1315
  * own entry and passes the record down, rather than relying on a check further in. */
1271
- function _clientDoor(opts) {
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) {
1272
1323
  /** @internal `optionsObject` is the door `signSct` in this module already enters by, and it REFUSES
1273
1324
  * an options bag carrying an accessor rather than tolerating one: a bag whose properties are not
1274
1325
  * plain values is a shape no set of checks can answer for, and it also refuses one that changes
@@ -1286,15 +1337,93 @@ function _clientDoor(opts) {
1286
1337
  * therefore dropped an inherited `transport`, and dropping a transport is not a smaller result: it
1287
1338
  * falls through to opening a real connection where the caller passed a fake one. What makes the
1288
1339
  * repeated reads safe instead is that this door refuses a bag whose properties are not plain values,
1289
- * and that each option below is taken into a local ONCE, before anything that could run caller code. */
1290
- return bag;
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;
1291
1420
  }
1292
1421
 
1293
1422
  /** @internal The shared request shape every message uses: the caller's transport or a pinned-anchor
1294
1423
  * one, the caps, and the header filter. Factored so a verb adds a path and a body and nothing else,
1295
1424
  * and so a rule about reaching a log is stated once. `o` has already been through `_clientDoor`, so
1296
1425
  * every read here is of a plain value. */
1297
- function _clientRequest(o, label) {
1426
+ function _clientRequest(o) {
1298
1427
  var transport = _assertTransport(o);
1299
1428
  /** @internal ONE read, and the presence check and the endpoint both decide from it. Read again after
1300
1429
  * the header copy below, which runs whatever getters the caller's header object carries, it was
@@ -1326,16 +1455,18 @@ function _clientRequest(o, label) {
1326
1455
  if (lk !== "content-length" && lk !== "transfer-encoding") headers[k] = supplied[k];
1327
1456
  });
1328
1457
  return { transport: transport, url: url, req: { headers: headers, tls: tls, timeout: timeout,
1329
- maxResponseBytes: maxResponseBytes }, label: label };
1458
+ maxResponseBytes: maxResponseBytes } };
1330
1459
  }
1331
1460
 
1332
1461
  /** @internal A sec. 4 message, fetched and read as JSON. The specification returns every error as a
1333
1462
  * 4xx or 5xx with a human-readable message, which `_fetchBody` already turns into a typed refusal,
1334
1463
  * so a non-200 never reaches the reader. */
1335
- async function _clientJson(o, message, label, query, body) {
1336
- var ctx = _clientRequest(o, label);
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);
1337
1468
  var url = _endpoint(ctx.url, message);
1338
- if (query) intrinsic.keys(query).forEach(function (k) { _searchSet(url.searchParams, k, query[k]); });
1469
+ if (query) intrinsic.keys(query).forEach(function (k) { _searchSet(_urlSearchParams(url), k, query[k]); });
1339
1470
  var req = ctx.req;
1340
1471
  if (body !== undefined) {
1341
1472
  req = intrinsic.assign(intrinsic.create(null), req);
@@ -1346,7 +1477,7 @@ async function _clientJson(o, message, label, query, body) {
1346
1477
  h["content-type"] = "application/json";
1347
1478
  req.headers = h;
1348
1479
  }
1349
- var res = await _fetchBody(ctx.transport, url.href, req, label);
1480
+ var res = await _fetchBody(ctx.transport, _urlHref(url), req, label);
1350
1481
  /** @internal The exact-integer rule applies to the NAMED members, the ones `_uintField` reads. RFC 6962
1351
1482
  * responses are ordinary JSON, where `1e3` is legal, so the stricter `integersOnly` would refuse a
1352
1483
  * document a conforming log may emit; what this refuses is a spelling whose value is not exactly its
@@ -1517,7 +1648,8 @@ async function verifySth(sth, logKey) {
1517
1648
  * sth.treeSize; // -> 3n
1518
1649
  */
1519
1650
  async function getSth(opts) {
1520
- opts = _clientDoor(opts || {});
1651
+ var door = _clientDoor(opts || {}, "logKey");
1652
+ opts = door.o;
1521
1653
  /** @internal ONE read of the option, and the presence check and the copy both decide from it. Reading
1522
1654
  * it twice let an accessor answer the two separately: a non-null value for the check and an
1523
1655
  * attacker's SPKI for the copy, and the tree head was then verified against a key the caller never
@@ -1529,7 +1661,7 @@ async function getSth(opts) {
1529
1661
  throw _ctErr("ct/bad-input", "opts.logKey is required -- the caller-pinned log SPKI; an unverified tree head is not a result");
1530
1662
  }
1531
1663
  var pinnedKey = _toBuffer(suppliedLogKey, "opts.logKey", _ctErr, "ct/bad-input");
1532
- var doc = await _clientJson(opts, "get-sth", "sth");
1664
+ var doc = await _clientJson(door,"get-sth", "sth");
1533
1665
  var sth = {
1534
1666
  treeSize: _uintField(doc, "tree_size", "ct/bad-sth", "sth"),
1535
1667
  timestamp: _uintField(doc, "timestamp", "ct/bad-sth", "sth"),
@@ -1575,7 +1707,8 @@ async function getSth(opts) {
1575
1707
  * p.verified; // -> true
1576
1708
  */
1577
1709
  async function getProofByHash(opts) {
1578
- opts = _clientDoor(opts || {});
1710
+ var door = _clientDoor(opts || {}, null);
1711
+ opts = door.o;
1579
1712
  var sth = _assertSth(opts.sth, "opts.sth");
1580
1713
  /** @internal Copied for the reason the tree head is: it is sent on the wire and then folded against the
1581
1714
  * head after the fetch returns, so a view would let the leaf being proved differ from the leaf asked
@@ -1584,7 +1717,7 @@ async function getProofByHash(opts) {
1584
1717
  if (leafHash.length !== 32) {
1585
1718
  throw _ctErr("ct/bad-input", "opts.leafHash must be exactly 32 bytes (a v1 leaf hash), got " + leafHash.length);
1586
1719
  }
1587
- var doc = await _clientJson(opts, "get-proof-by-hash", "audit proof", {
1720
+ var doc = await _clientJson(door,"get-proof-by-hash", "audit proof", {
1588
1721
  hash: _bufToString(leafHash, "base64"), tree_size: _stringOf(sth.treeSize),
1589
1722
  });
1590
1723
  var leafIndex = _uintField(doc, "leaf_index", "ct/bad-proof", "audit proof");
@@ -1630,14 +1763,15 @@ async function getProofByHash(opts) {
1630
1763
  * c.verified; // -> true
1631
1764
  */
1632
1765
  async function getSthConsistency(opts) {
1633
- opts = _clientDoor(opts || {});
1766
+ var door = _clientDoor(opts || {}, null);
1767
+ opts = door.o;
1634
1768
  var oldSth = _assertSth(opts.oldSth, "opts.oldSth");
1635
1769
  var newSth = _assertSth(opts.newSth, "opts.newSth");
1636
1770
  if (oldSth.treeSize > newSth.treeSize) {
1637
1771
  throw _ctErr("ct/bad-input", "opts.oldSth names tree size " + oldSth.treeSize +
1638
1772
  ", which is larger than opts.newSth's " + newSth.treeSize);
1639
1773
  }
1640
- var doc = await _clientJson(opts, "get-sth-consistency", "consistency proof", {
1774
+ var doc = await _clientJson(door,"get-sth-consistency", "consistency proof", {
1641
1775
  first: _stringOf(oldSth.treeSize), second: _stringOf(newSth.treeSize),
1642
1776
  });
1643
1777
  var consistency = _b64Array(doc, "consistency", "ct/bad-consistency", "consistency proof", 32, C.LIMITS.MERKLE_MAX_PROOF_NODES);
@@ -1687,7 +1821,8 @@ async function getSthConsistency(opts) {
1687
1821
  * e.entries[0].leafInput; // -> <Buffer ...>
1688
1822
  */
1689
1823
  async function getEntries(opts) {
1690
- opts = _clientDoor(opts || {});
1824
+ var door = _clientDoor(opts || {}, null);
1825
+ opts = door.o;
1691
1826
  var start = guard.range.uint64(opts.start, _ctErr, "ct/bad-input", "opts.start");
1692
1827
  var end = guard.range.uint64(opts.end, _ctErr, "ct/bad-input", "opts.end");
1693
1828
  if (end < start) {
@@ -1698,7 +1833,7 @@ async function getEntries(opts) {
1698
1833
  throw _ctErr("ct/bad-input", "a range of " + requested + " entries exceeds the " +
1699
1834
  C.LIMITS.CT_MAX_ENTRIES_PER_REQUEST + "-entry request cap");
1700
1835
  }
1701
- 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) });
1702
1837
  var raw = doc.entries;
1703
1838
  if (!intrinsic.isArray(raw)) throw _ctErr("ct/bad-entries", "the entries field is not an array");
1704
1839
  if (intrinsic.BigInt(raw.length) > requested) {
@@ -1743,8 +1878,9 @@ async function getEntries(opts) {
1743
1878
  * r.certificates.length; // -> 2
1744
1879
  */
1745
1880
  async function getRoots(opts) {
1746
- opts = _clientDoor(opts || {});
1747
- 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");
1748
1884
  return guard.verdict.of({ certificates: _b64Array(doc, "certificates", "ct/bad-roots", "roots", undefined, C.LIMITS.CT_MAX_ROOTS) });
1749
1885
  }
1750
1886
 
@@ -1826,7 +1962,8 @@ async function addPreChain(opts) {
1826
1962
  * The chain encoding, the response shape, and the refusal when the receipt does not verify are one
1827
1963
  * rule stated once. */
1828
1964
  async function _addChain(opts, message, entryType) {
1829
- opts = _clientDoor(opts || {});
1965
+ var door = _clientDoor(opts || {}, "logKey");
1966
+ opts = door.o;
1830
1967
  /** @internal ONE read of the option, used by the presence check here and by the copy below. Reading it
1831
1968
  * twice let an accessor answer the two separately, presenting a non-null value to the check and an
1832
1969
  * attacker's SPKI to the copy, and the returned receipt was then verified against a key the caller
@@ -1859,7 +1996,7 @@ async function _addChain(opts, message, entryType) {
1859
1996
  : { entryType: 1, tbsCertificate: _snap(opts.tbsCertificate, "opts.tbsCertificate"),
1860
1997
  issuerKeyHash: _snap(opts.issuerKeyHash, "opts.issuerKeyHash") };
1861
1998
  var pinnedKey = _snap(suppliedLogKey, "opts.logKey");
1862
- var doc = await _clientJson(opts, message, "SCT", null, _jsonStringify({ chain: chain }));
1999
+ var doc = await _clientJson(door,message, "SCT", null, _jsonStringify({ chain: chain }));
1863
2000
  var version = doc.sct_version;
1864
2001
  if (version !== 0) {
1865
2002
  throw _ctErr("ct/bad-sct", "the log returned sct_version " + guard.text.showValue(version) +
@@ -2056,9 +2193,16 @@ async function fetchLogList(opts) {
2056
2193
  * once to parse and once to fetch, so an accessor sent the request somewhere the parse never saw. */
2057
2194
  opts = guard.identifier.optionsObject(opts, _ctErr, "ct/bad-input", "pki.ct.fetchLogList options");
2058
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");
2059
2202
  /** @internal Read once, at the door, and carried from here. Asking again would let an option that
2060
2203
  * answers differently on each read hand back something other than the value that was checked. */
2061
2204
  var transport = _assertTransport(opts);
2205
+ var fetchTls = _tlsForFetch(opts);
2062
2206
  /** @internal ONE read, and both the presence check and the copy decide from it. Reading it twice let an
2063
2207
  * accessor present a non-null value to the check and an attacker's SPKI to the copy, so the list was
2064
2208
  * verified under a distributor key the caller never pinned.
@@ -2070,14 +2214,17 @@ async function fetchLogList(opts) {
2070
2214
  var pinnedSigner = _toBuffer(suppliedSignerKey, "opts.signerKey", _ctErr, "ct/bad-input");
2071
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)");
2072
2216
  var jsonParsed = _parseHttpsUrl(opts.url, "URL");
2073
- var jsonUrl = jsonParsed.href;
2217
+ var jsonUrl = _urlHref(jsonParsed);
2074
2218
  /** @internal ONE read: the presence test and the parse decide from the same value. */
2075
2219
  var suppliedSigUrl = opts.sigUrl;
2076
- var sigUrl = suppliedSigUrl != null ? _parseHttpsUrl(suppliedSigUrl, "signature URL").href : _sigUrlFor(jsonParsed);
2077
- 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");
2078
- /** @internal Flattened once, for the reason `_clientRequest` does it: the header copy below runs the
2079
- * caller's getters, and the check and the connection have to read the same flat record. */
2080
- var fetchTls = _tlsForFetch(opts);
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");
2081
2228
  if (!transport) {
2082
2229
  var hasAnchors = fetchTls.anchors !== undefined && fetchTls.anchors !== null && !(intrinsic.isArray(fetchTls.anchors) && fetchTls.anchors.length === 0);
2083
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)");
package/lib/est.js CHANGED
@@ -80,6 +80,20 @@ 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;
88
+ /** @internal And the accessors its result is read through: capturing the constructor does not capture
89
+ * the prototype the value reads its parts off, and each part here decides something, the scheme
90
+ * whether the request is allowed, the origin whether credentials may still be sent after a redirect. */
91
+ var _urlHref = intrinsic.urlHref;
92
+ var _urlOrigin = intrinsic.urlOrigin;
93
+ var _urlProtocol = intrinsic.urlProtocol;
94
+ var _urlPathname = intrinsic.urlPathname;
95
+ var _urlSearch = intrinsic.urlSearch;
96
+ var _urlHash = intrinsic.urlHash;
83
97
  var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
84
98
  var _promiseReject = intrinsic.uncurry(intrinsic.promiseReject);
85
99
  var pkix = require("./schema-pkix");
@@ -803,25 +817,28 @@ function _csrDer(input) {
803
817
 
804
818
  function _parseUrl(urlStr) {
805
819
  var url;
806
- try { url = new URL(String(urlStr)); }
820
+ try { url = new _URL(_stringOf(urlStr)); }
807
821
  catch (e) { throw E("est/bad-url", "the EST server URL did not parse: " + String(urlStr), e); }
808
- if (url.protocol !== "https:") throw E("est/insecure-url", "EST requires https (RFC 7030 sec. 3.3), got " + url.protocol + " for " + urlStr);
822
+ var scheme = _urlProtocol(url);
823
+ if (scheme !== "https:") throw E("est/insecure-url", "EST requires https (RFC 7030 sec. 3.3), got " + scheme + " for " + urlStr);
809
824
  return url;
810
825
  }
811
826
 
827
+ /** @internal The settings this client forwards to its transport. */
828
+ var TLS_SETTINGS = ["anchors", "useSystemStore", "cert", "key", "minVersion", "servername", "checkServerIdentity"];
812
829
  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 };
830
+ return guard.identifier.pickSettings(opts.tls, TLS_SETTINGS);
815
831
  }
816
832
 
817
833
  function _redirectTarget(current, location, method, opts) {
818
834
  if (location == null || String(location).trim() === "") throw E("est/http-error", "a redirect response carried no Location header");
819
835
  var resolved;
820
- try { resolved = new URL(String(location), current.href); }
836
+ try { resolved = new _URL(_stringOf(location), _urlHref(current)); }
821
837
  catch (e) { throw E("est/bad-url", "a redirect Location did not parse: " + location, e); }
822
- 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);
838
+ var resolvedScheme = _urlProtocol(resolved);
839
+ if (resolvedScheme !== "https:") throw E("est/insecure-redirect", "a redirect to a non-https URL is refused (RFC 7030 sec. 3.2.1): " + resolvedScheme);
823
840
  var safeMethod = method === "GET" || method === "HEAD";
824
- if (resolved.origin !== current.origin && !safeMethod && !opts.allowCrossOriginRedirect) {
841
+ if (_urlOrigin(resolved) !== _urlOrigin(current) && !safeMethod && !opts.allowCrossOriginRedirect) {
825
842
  throw E("est/cross-origin-redirect", "a cross-origin redirect on a " + method + " needs opts.allowCrossOriginRedirect (RFC 7030 sec. 3.2.1)");
826
843
  }
827
844
  return resolved;
@@ -884,10 +901,10 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
884
901
  var staleRetries = 0;
885
902
  var authSpaces = 0;
886
903
  var lastDigestNonce = null;
887
- var initialOrigin = url.origin;
904
+ var initialOrigin = _urlOrigin(url);
888
905
  function _tlsFor(u) {
889
906
  var t = budgets.tls;
890
- if (t && u.origin !== initialOrigin) {
907
+ if (t && _urlOrigin(u) !== initialOrigin) {
891
908
  t = Object.assign({}, t);
892
909
  t.cert = null; t.key = null; t.servername = null;
893
910
  }
@@ -899,20 +916,26 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
899
916
  var digestSent = false;
900
917
  function _headersFor(u) {
901
918
  digestSent = false;
902
- if (u.origin !== initialOrigin || (!authValue && !digestChallenge)) return headers;
919
+ /** @internal The origin and the request target are each read ONCE and both the protection-space
920
+ * test and the answer are built from those locals: the credential is bound to the target it is
921
+ * computed over, so a part answering two reads differently would answer for one target and be
922
+ * sent to another. */
923
+ var uOrigin = _urlOrigin(u);
924
+ var uTarget = _urlPathname(u) + _urlSearch(u);
925
+ if (uOrigin !== initialOrigin || (!authValue && !digestChallenge)) return headers;
903
926
  var hh = guard.identifier.ownOptions(headers);
904
927
  if (digestChallenge) {
905
- if (!httpDigest.inProtectionSpace(digestChallenge, u.origin, u.pathname + u.search)) return headers;
928
+ if (!httpDigest.inProtectionSpace(digestChallenge, uOrigin, uTarget)) return headers;
906
929
  var nkey = digestChallenge.nonce;
907
930
  digestNcByNonce[nkey] = (digestNcByNonce[nkey] || 0) + 1;
908
- hh.authorization = httpDigest.answer(digestChallenge, { method: method, uri: u.pathname + u.search, username: _authUser(opts), password: _authPass(opts), body: body, nc: digestNcByNonce[nkey], policy: _digestPolicy(opts) }, E);
931
+ hh.authorization = httpDigest.answer(digestChallenge, { method: method, uri: uTarget, username: _authUser(opts), password: _authPass(opts), body: body, nc: digestNcByNonce[nkey], policy: _digestPolicy(opts) }, E);
909
932
  digestSent = true;
910
933
  } else { hh.authorization = authValue; }
911
934
  return hh;
912
935
  }
913
936
  function step() {
914
937
  var hopBody = (typeof body === "function" && budgets.onAttempt) ? budgets.onAttempt() : body;
915
- return guard.async.invoked(transport, [{ method: method, url: url.href, headers: _headersFor(url), body: hopBody, tls: _tlsFor(url), proxy: budgets.proxy, timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }], E, "est/bad-input", "opts.transport").then(function (res) {
938
+ return guard.async.invoked(transport, [{ method: method, url: _urlHref(url), headers: _headersFor(url), body: hopBody, tls: _tlsFor(url), proxy: budgets.proxy, timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }], E, "est/bad-input", "opts.transport").then(function (res) {
916
939
  res = res || {};
917
940
  var blen = _bodyLen(res);
918
941
  if (blen > budgets.maxResponseBytes) throw E("est/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap (RFC 7030 sec. 6)");
@@ -933,7 +956,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
933
956
  return step();
934
957
  }
935
958
  if (status === 401) {
936
- if (url.origin !== initialOrigin) throw E("est/auth-required", "refusing to send HTTP credentials to a redirected origin (RFC 7030 sec. 3.6)");
959
+ if (_urlOrigin(url) !== initialOrigin) throw E("est/auth-required", "refusing to send HTTP credentials to a redirected origin (RFC 7030 sec. 3.6)");
937
960
  var www = String(h["www-authenticate"] || "");
938
961
  if (www.length > constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES) throw E("est/auth-required", "the WWW-Authenticate header exceeds the " + constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES + "-byte cap (RFC 7030 sec. 3.2.3)");
939
962
  var scheme = _authScheme(opts);
@@ -945,8 +968,8 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
945
968
  selPol.preferStale = digestSent;
946
969
  selPol.priorNonce = lastDigestNonce;
947
970
  selPol.priorRealm = digestChallenge && digestChallenge.realm;
948
- selPol.requestOrigin = url.origin;
949
- selPol.requestTarget = url.pathname + url.search;
971
+ selPol.requestOrigin = _urlOrigin(url);
972
+ selPol.requestTarget = _urlPathname(url) + _urlSearch(url);
950
973
  var ch = httpDigest.parseChallenge(www, E, "est/digest-bad-challenge", selPol);
951
974
  if (!ch) throw E("est/auth-required", "the server offered no usable Digest challenge: " + www);
952
975
  if (digestSent && digestChallenge && ch.realm === digestChallenge.realm) {
@@ -980,13 +1003,13 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
980
1003
  * answer, so a verb that holds one passes it and this reads the option only when none was held. */
981
1004
  function _client(op, method, baseUrl, body, headers, opts, proxySnap, onAttempt, checked) {
982
1005
  var base;
983
- try { base = new URL(String(baseUrl)); }
1006
+ try { base = new _URL(_stringOf(baseUrl)); }
984
1007
  catch (e) { throw E("est/bad-url", "the EST base URL did not parse: " + guard.text.showValue(baseUrl), e); }
985
- 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)");
1008
+ if (_urlSearch(base) || _urlHash(base)) throw E("est/bad-url", "the EST base URL must not carry a query or fragment component (RFC 7030 sec. 3.2.2)");
986
1009
  var url = _parseUrl(paths(baseUrl, { label: opts.label })[op]);
987
1010
  var transport = checked !== undefined ? checked : _assertTransport(opts);
988
1011
  if (!transport) {
989
- var t = opts.tls || {};
1012
+ var t = opts.tls != null ? opts.tls : intrinsic.create(null);
990
1013
  var hasAnchors = t.anchors !== undefined && t.anchors !== null && !(Array.isArray(t.anchors) && t.anchors.length === 0);
991
1014
  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
1015
  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
  });