@blamejs/pki 0.8.30 → 0.8.48

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +411 -0
  2. package/README.md +67 -16
  3. package/bin/pki.js +716 -30
  4. package/examples/issue-a-certificate.js +90 -0
  5. package/examples/post-quantum.js +66 -0
  6. package/examples/read-a-certificate.js +56 -0
  7. package/examples/revoke-and-check.js +86 -0
  8. package/examples/sign-and-verify-cms.js +63 -0
  9. package/index.js +18 -5
  10. package/lib/acme.js +223 -74
  11. package/lib/alt-sig.js +441 -0
  12. package/lib/asn1-der.js +48 -25
  13. package/lib/attrcert-sign.js +27 -19
  14. package/lib/cbor-det.js +24 -14
  15. package/lib/cmc-build.js +9 -2
  16. package/lib/cmc-verify.js +14 -3
  17. package/lib/cmp-build.js +17 -5
  18. package/lib/cmp-session.js +24 -11
  19. package/lib/cmp-verify.js +28 -19
  20. package/lib/cms-compress.js +3 -1
  21. package/lib/cms-decrypt.js +102 -7
  22. package/lib/cms-encrypt.js +59 -7
  23. package/lib/cms-sign.js +31 -9
  24. package/lib/cms-verify.js +22 -18
  25. package/lib/composite-kem.js +30 -18
  26. package/lib/composite-sig.js +29 -14
  27. package/lib/constants.js +28 -0
  28. package/lib/crl-sign.js +111 -19
  29. package/lib/crmf-sign.js +167 -15
  30. package/lib/csr-sign.js +117 -6
  31. package/lib/ct.js +939 -48
  32. package/lib/est.js +29 -16
  33. package/lib/framework-error.js +12 -0
  34. package/lib/guard-async.js +1 -1
  35. package/lib/guard-bytes.js +103 -20
  36. package/lib/guard-crypto.js +205 -3
  37. package/lib/guard-encoding.js +6 -1
  38. package/lib/guard-identifier.js +55 -8
  39. package/lib/guard-intrinsic.js +43 -0
  40. package/lib/guard-json.js +122 -9
  41. package/lib/guard-limits.js +12 -1
  42. package/lib/guard-list.js +63 -8
  43. package/lib/guard-name.js +167 -20
  44. package/lib/guard-range.js +43 -5
  45. package/lib/guard-text.js +41 -0
  46. package/lib/guard-verdict.js +39 -8
  47. package/lib/hpke.js +688 -42
  48. package/lib/http-transport.js +156 -4
  49. package/lib/identity-match.js +4 -4
  50. package/lib/inspect.js +448 -2
  51. package/lib/jose.js +93 -25
  52. package/lib/key.js +18 -13
  53. package/lib/lint.js +165 -1
  54. package/lib/merkle.js +233 -27
  55. package/lib/ocsp.js +27 -14
  56. package/lib/oid.js +14 -4
  57. package/lib/path-validate.js +143 -61
  58. package/lib/pkcs12-build.js +7 -2
  59. package/lib/pki-build.js +31 -7
  60. package/lib/possession.js +575 -0
  61. package/lib/related-cert.js +708 -0
  62. package/lib/revocation-fetch.js +6 -6
  63. package/lib/scep.js +15 -5
  64. package/lib/schema-c509.js +5 -2
  65. package/lib/schema-cmc.js +1 -1
  66. package/lib/schema-cmp.js +8 -8
  67. package/lib/schema-cms.js +2 -15
  68. package/lib/schema-csr.js +69 -0
  69. package/lib/schema-engine.js +10 -3
  70. package/lib/schema-pkix.js +267 -4
  71. package/lib/schema-tsp.js +7 -6
  72. package/lib/sign-scheme.js +22 -9
  73. package/lib/sigstore.js +684 -168
  74. package/lib/sleep.js +8 -1
  75. package/lib/smime.js +22 -1
  76. package/lib/tlog.js +1235 -0
  77. package/lib/tls-cert-compress.js +25 -8
  78. package/lib/tsp-sign.js +120 -35
  79. package/lib/tuf.js +1395 -0
  80. package/lib/validator-tpm.js +4 -1
  81. package/lib/verify-seam.js +7 -1
  82. package/lib/webauthn-mds.js +41 -4
  83. package/lib/webauthn.js +43 -25
  84. package/lib/webcrypto.js +48 -46
  85. package/lib/x509-sign.js +225 -19
  86. package/package.json +2 -1
  87. package/sbom.cdx.json +6 -6
package/lib/ct.js CHANGED
@@ -47,6 +47,27 @@ var guard = require("./guard-all.js");
47
47
  var intrinsic = require("./guard-intrinsic.js");
48
48
  var _hasOwn = intrinsic.hasOwn;
49
49
  var _strSlice = intrinsic.uncurry(String.prototype.slice);
50
+ /** @internal The log fetch is gated on a resolved promise, and `resolve` builds from the RECEIVER, so
51
+ * reading the global binding at the call decides whether the request is made at all. Taken at load. */
52
+ var _Promise = intrinsic.Promise;
53
+ var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
54
+ /** @internal The operations the RFC 6962 sec. 4 client reads, taken at load. A log's response decides
55
+ * a verdict, so which function answers "these bytes as base64", "this number as text" or "is this
56
+ * the pinned log id" is fixed before any caller code runs. */
57
+ var _stringOf = intrinsic.String;
58
+ var _jsonStringify = intrinsic.stringify;
59
+ var _bufferAlloc = intrinsic.bufferAlloc;
60
+ var _bufferFrom = intrinsic.bufferFrom;
61
+ var _byteLength = intrinsic.byteLength;
62
+ var _bufToString = intrinsic.bufToString;
63
+ var _bufferCompare = intrinsic.compare;
64
+ var _subarray = intrinsic.subarray;
65
+ var _toLowerCase = intrinsic.toLowerCase;
66
+ var _bufCopy = intrinsic.uncurry(Buffer.prototype.copy);
67
+ var _writeU64BE = intrinsic.uncurry(Buffer.prototype.writeBigUInt64BE);
68
+ var _searchSet = intrinsic.uncurry(URLSearchParams.prototype.set);
69
+ var _numberOf = intrinsic.Number;
70
+ var _URL = URL;
50
71
  var _JSON_SUFFIX = ".json";
51
72
  function _endsWithJson(s) { return s.length >= _JSON_SUFFIX.length && _strSlice(s, s.length - _JSON_SUFFIX.length) === _JSON_SUFFIX; }
52
73
  var ByteReader = require("./byte-reader.js");
@@ -56,7 +77,14 @@ var webcrypto = require("./webcrypto.js");
56
77
  var validator = require("./validator-all.js");
57
78
  var rfc3339 = require("./rfc3339.js");
58
79
  var httpTransport = require("./http-transport.js");
80
+ var merkle = require("./merkle.js");
59
81
  var subtle = webcrypto.webcrypto.subtle;
82
+ /** @internal The two operations every signature check here runs, captured at load. The object was held
83
+ * and its METHODS fetched per call, which leaves them replaceable: a `verify` answering true accepts a
84
+ * signed tree head, a log list or an SCT that no log signed, and the import beside it decides which key
85
+ * the check runs under. `path-validate` holds the same pair the same way. */
86
+ var _subtleImportKey = intrinsic.uncurry(subtle.importKey);
87
+ var _subtleVerify = intrinsic.uncurry(subtle.verify);
60
88
 
61
89
  var CtError = frameworkError.CtError;
62
90
  var PkiError = frameworkError.PkiError;
@@ -83,8 +111,53 @@ function _peelInner(extValue) {
83
111
  catch (e) { throw new CtError("ct/bad-der", "the SCT-list extension value must be a DER OCTET STRING wrapping the TLS list (RFC 6962 sec. 3.3)", e); }
84
112
  }
85
113
 
86
- function _toBuffer(v, field) {
87
- return guard.bytes.view(v, CtError, "ct/bad-input", field);
114
+ /** @internal The response members read as integers, and so the members whose number tokens must denote
115
+ * exactly the integer they are read as. These are the names `_uintField` is called with: a tree size, a
116
+ * timestamp and a leaf index, each folded into a reconstructed binary preimage that a log signature is
117
+ * checked over. A spelling that rounds to a different value than it denotes verifies under the log's
118
+ * signature while the value handed back is not the one the log sent. */
119
+ var EXACT_INTEGER_RESPONSE_MEMBERS = intrinsic.assign(intrinsic.create(null), {
120
+ tree_size: 1, timestamp: 1, leaf_index: 1,
121
+ });
122
+
123
+ /** @internal Every caller byte input enters through here, and it COPIES rather than views. A view stays
124
+ * a window onto the caller's buffer, and these verbs import a key with an await before they verify: a
125
+ * tree-head signature held as a view could be overwritten in that window, so the bytes verified would not
126
+ * be the bytes supplied. Copying at the door makes the value checked the value used, on every route. */
127
+ function _toBuffer(v, field, E, code, cap, capCode) {
128
+ /** @internal Normalized to a byte view before it is copied. The docs for these verbs name their byte
129
+ * inputs `BufferSource`, and `guard.bytes.snapshot` takes a Buffer or a Uint8Array only, so the
130
+ * ArrayBuffer `crypto.subtle.exportKey("spki", ...)` returns, which is how a caller holding a WebCrypto
131
+ * key has it, was refused before the transport ran while a Buffer of identical bytes verified.
132
+ * `guard.bytes.source` shares the caller's store, so the copy below is still the only allocation and
133
+ * the copy-before-await reasoning above is unchanged.
134
+ *
135
+ * The comment above said every caller byte input entered here and SEVEN sites called the narrow copy
136
+ * directly, each with its own error factory, so the single door was a claim rather than a fact and the
137
+ * normalization added here would have reached one of them. The factory and the code are parameters so
138
+ * those sites can route through this one and keep the refusal they already gave. */
139
+ var cls = E === undefined ? CtError : E;
140
+ var reason = code === undefined ? "ct/bad-input" : code;
141
+ var viewed = guard.bytes.source(v, cls, reason, field);
142
+ /** @internal `cap`, where a caller passes one, is measured on the VIEW. The view shares the caller's
143
+ * store and allocates nothing, so an input over the cap is refused before the copy below would take
144
+ * it. Every cap in this module used to run after that copy, so a verb refusing a 64 MiB input had
145
+ * already allocated 64 MiB to do it (CWE-770). The size comes off the view rather than off the
146
+ * caller's own `length`, which a caller can shadow independently of the bytes. */
147
+ /** @internal `guard.bytes.*` takes the error CLASS and constructs it; `guard.limits.byteCap` takes a
148
+ * FACTORY and calls it. Handing the class to both throws "Class constructor cannot be invoked without
149
+ * new" instead of the refusal, so the factory is built from whichever class this call is using. */
150
+ /** @internal A site passing NO cap gets the DER ceiling rather than no bound at all. Leaving it
151
+ * optional left most of this module's inputs copied at whatever size arrived: measured, a 32 MiB
152
+ * value supplied as a 32-byte tree head hash allocated 32 MiB, and one supplied as its signature
153
+ * allocated 64 MiB across the two reads, with the same on the chain, pinned-key, leaf-hash and
154
+ * issuer-hash inputs. The default means a call added later is bounded without anyone remembering to
155
+ * bound it, and a site that knows a narrower width still passes one. The refusal is `ct/too-large`,
156
+ * which is what the value is. */
157
+ var bound = cap === undefined ? C.LIMITS.DER_MAX_BYTES : cap;
158
+ var boundCode = cap === undefined ? "ct/too-large" : (capCode === undefined ? reason : capCode);
159
+ guard.limits.byteCap(viewed, bound, function (c, m) { return new cls(c, m); }, boundCode, field);
160
+ return guard.bytes.snapshot(viewed, cls, reason, field);
88
161
  }
89
162
 
90
163
  function _parseSct(r, sctLen) {
@@ -179,7 +252,12 @@ function _parseSct(r, sctLen) {
179
252
  * }
180
253
  */
181
254
  function parseSctList(extValue) {
182
- var blob = _peelInner(_toBuffer(extValue, "the SCT-list extension value"));
255
+ /** @internal The raw value is bounded before it is copied, and then the peeled list is held to the
256
+ * exact cap as before. The two differ by the DER header `_peelInner` may strip, at most five bytes for
257
+ * an OCTET STRING of this size (tag, a 0x83 long-form marker and three length octets), so the outer
258
+ * bound carries that slack and the semantic cap below is unchanged. */
259
+ var blob = _peelInner(_toBuffer(extValue, "the SCT-list extension value", undefined, undefined,
260
+ C.LIMITS.SCT_MAX_BYTES + SCT_LIST_WRAPPER_SLACK, "ct/too-large"));
183
261
  guard.limits.byteCap(blob, C.LIMITS.SCT_MAX_BYTES, _ctErr, "ct/too-large", "SCT list");
184
262
  var outer = new TlsReader(blob, 0, blob.length);
185
263
  var listLen = outer.u16("ct/bad-list");
@@ -207,14 +285,19 @@ function parseSctList(extValue) {
207
285
  if (one.unknown) { var u = { version: one.version, rawSct: one.rawSct }; unknownScts.push(u); all.push(u); }
208
286
  else { scts.push(one); all.push(one); }
209
287
  }
210
- return { scts: scts, unknownScts: unknownScts, all: all };
288
+ return guard.verdict.of({ scts: scts, unknownScts: unknownScts, all: all });
211
289
  }
212
290
 
291
+ /** @internal The DER header `_peelInner` may strip from an SCT-list extension value: an OCTET STRING
292
+ * tag, a long-form length marker and three length octets reach five bytes at this size. The outer bound
293
+ * on the raw value carries this slack so that bounding it cannot refuse a list the exact cap accepts. */
294
+ var SCT_LIST_WRAPPER_SLACK = 8;
295
+
213
296
  function _u24Bytes(n) {
214
297
  if (n < 1 || n > 0xffffff) {
215
298
  throw new CtError("ct/bad-tbs-length", "a certificate / TBSCertificate length must be in 1..2^24-1, got " + n + " (RFC 6962 sec. 3.1)");
216
299
  }
217
- return Buffer.from([(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]);
300
+ return _bufferFrom([(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]);
218
301
  }
219
302
 
220
303
  /**
@@ -273,27 +356,27 @@ function reconstructSignedData(entry, sct) {
273
356
  }
274
357
  var tsVal = guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp (RFC 6962 3.2)");
275
358
  var parts = [];
276
- parts.push(Buffer.from([sct.version & 0xff]));
277
- parts.push(Buffer.from([0]));
278
- var ts = Buffer.alloc(8); ts.writeBigUInt64BE(tsVal); parts.push(ts);
279
- parts.push(Buffer.from([(entryType >> 8) & 0xff, entryType & 0xff]));
359
+ parts.push(_bufferFrom([sct.version & 0xff]));
360
+ parts.push(_bufferFrom([0]));
361
+ var ts = _bufferAlloc(8); ts.writeBigUInt64BE(tsVal); parts.push(ts);
362
+ parts.push(_bufferFrom([(entryType >> 8) & 0xff, entryType & 0xff]));
280
363
  if (entryType === 0) {
281
- var cert = _toBuffer(entry.leafCert, "leafCert");
364
+ var cert = _toBuffer(entry.leafCert, "leafCert", undefined, undefined, 0xffffff, "ct/bad-tbs-length");
282
365
  parts.push(_u24Bytes(cert.length)); parts.push(cert);
283
366
  } else {
284
367
  var ikh = _toBuffer(entry.issuerKeyHash, "issuerKeyHash");
285
368
  if (ikh.length !== 32) {
286
369
  throw new CtError("ct/bad-issuer-key-hash", "issuer_key_hash must be exactly 32 bytes (SHA-256 of the issuer SPKI), got " + ikh.length + " (RFC 6962 sec. 3.2)");
287
370
  }
288
- var tbs = _toBuffer(entry.tbsCertificate, "tbsCertificate");
371
+ var tbs = _toBuffer(entry.tbsCertificate, "tbsCertificate", undefined, undefined, 0xffffff, "ct/bad-tbs-length");
289
372
  parts.push(ikh);
290
373
  parts.push(_u24Bytes(tbs.length)); parts.push(tbs);
291
374
  }
292
- var ext = _toBuffer(sct.extensions, "sct.extensions");
375
+ var ext = _toBuffer(sct.extensions, "sct.extensions", undefined, undefined, 0xffff, "ct/bad-extensions");
293
376
  if (ext.length > 0xffff) {
294
377
  throw new CtError("ct/bad-extensions", "CtExtensions must be 0..65535 bytes, got " + ext.length + " (RFC 6962 3.2)");
295
378
  }
296
- parts.push(Buffer.from([(ext.length >> 8) & 0xff, ext.length & 0xff])); parts.push(ext);
379
+ parts.push(_bufferFrom([(ext.length >> 8) & 0xff, ext.length & 0xff])); parts.push(ext);
297
380
  return Buffer.concat(parts);
298
381
  }
299
382
 
@@ -365,19 +448,35 @@ function _spkiAlg(spki) {
365
448
  * var ok = await pki.ct.verifySct({ entryType: 0, leafCert: certDer }, sct, logKey);
366
449
  */
367
450
  async function verifySct(entry, sct, logPublicKey) {
368
- var message = reconstructSignedData(entry, sct);
451
+ if (sct === null || typeof sct !== "object") {
452
+ throw new CtError("ct/bad-input", "the SCT must be an object from pki.ct.parseSctList or pki.ct.signSct");
453
+ }
454
+ /** @internal Every member of the caller's SCT is taken ONCE, and everything below reads this record. The
455
+ * preimage is built from some of them and the verification runs after an awaited digest, so reading a
456
+ * member on both sides would let the value that shaped the preimage differ from the value checked
457
+ * against it. One read each removes the question rather than answering it per member. */
458
+ var rec = {
459
+ version: sct.version, logId: sct.logId, timestamp: sct.timestamp,
460
+ extensions: sct.extensions, signatureAlgorithm: sct.signatureAlgorithm, signature: sct.signature,
461
+ };
462
+ var message = reconstructSignedData(entry, rec);
463
+ /** @internal COPIED. The logId check digests this key and awaits that digest, and the key is then read
464
+ * again to resolve its algorithm and to import it for the verification. Held as a view, the key whose
465
+ * SHA-256 matched sct.logId need not be the key the signature is checked under: a caller reusing the
466
+ * buffer in that window passes the logId check with one key and verifies with another, so the SCT
467
+ * verifies while naming a log that did not sign it. */
369
468
  var spki = _toBuffer(logPublicKey, "the CT log public key (SPKI)");
370
- if (sct.logId != null) {
469
+ if (rec.logId != null) {
371
470
  var keyId = Buffer.from(await subtle.digest("SHA-256", spki));
372
- if (!keyId.equals(_toBuffer(sct.logId, "sct.logId"))) {
471
+ if (!keyId.equals(_toBuffer(rec.logId, "sct.logId"))) {
373
472
  throw new CtError("ct/log-id-mismatch", "the SCT logId does not match SHA-256 of the provided log key (RFC 6962 sec. 3.2)");
374
473
  }
375
474
  }
376
- var sigInfo = sct.signatureAlgorithm || {};
475
+ var sigInfo = rec.signatureAlgorithm || {};
377
476
  var hashName = CT_HASH[sigInfo.hashName];
378
477
  if (!hashName) throw new CtError("ct/unsupported-algorithm", "unsupported SCT hash algorithm " + JSON.stringify(sigInfo.hashName) + " (RFC 6962 sec. 2.1.4 mandates sha256)");
379
478
  var alg = _spkiAlg(spki);
380
- var imp, ver, sig = _toBuffer(sct.signature, "sct.signature");
479
+ var imp, ver, sig = _toBuffer(rec.signature, "sct.signature");
381
480
  if (sigInfo.signatureName === "ecdsa") {
382
481
  if (alg.algOid !== oid.byName("ecPublicKey")) throw new CtError("ct/bad-input", "the SCT declares an ECDSA signature but the log key is not an EC key");
383
482
  var ec = CT_EC_CURVE[alg.curveOid];
@@ -394,8 +493,8 @@ async function verifySct(entry, sct, logPublicKey) {
394
493
  throw new CtError("ct/unsupported-algorithm", "unsupported SCT signature algorithm " + JSON.stringify(sigInfo.signatureName) + " (RFC 6962 sec. 2.1.4 supports ecdsa/rsa)");
395
494
  }
396
495
  try {
397
- var key = await subtle.importKey("spki", spki, imp, false, ["verify"]);
398
- return await subtle.verify(ver, key, sig, message);
496
+ var key = await _subtleImportKey(subtle, "spki", spki, imp, false, ["verify"]);
497
+ return await _subtleVerify(subtle, ver, key, sig, message);
399
498
  } catch (e) {
400
499
  throw new CtError("ct/verify-error", "the SCT signature could not be evaluated", e);
401
500
  }
@@ -421,10 +520,10 @@ function _encodeSctBody(sct) {
421
520
  if (logId.length !== LOGID_BYTES) throw new CtError("ct/bad-input", "an SCT logId must be exactly " + LOGID_BYTES + " bytes (RFC 6962 sec. 3.2)");
422
521
  w.bytes(logId);
423
522
  w.u64(guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp"), "ct/bad-input");
424
- w.vector(2, 0, 0xffff, _toBuffer(sct.extensions, "sct.extensions"), "ct/bad-input");
523
+ w.vector(2, 0, 0xffff, _toBuffer(sct.extensions, "sct.extensions", undefined, undefined, 0xffff), "ct/bad-input");
425
524
  w.u8(sct.hashAlg, "ct/bad-input");
426
525
  w.u8(sct.sigAlg, "ct/bad-input");
427
- w.vector(2, 0, 0xffff, _toBuffer(sct.signature, "sct.signature"), "ct/bad-input");
526
+ w.vector(2, 0, 0xffff, _toBuffer(sct.signature, "sct.signature", undefined, undefined, 0xffff), "ct/bad-input");
428
527
  return w.build();
429
528
  }
430
529
 
@@ -584,7 +683,7 @@ function _parseLogState(state) {
584
683
  if (member == null || typeof member !== "object") throw _ctErr("ct/bad-state", "the CT log state " + name + " is malformed");
585
684
  var since = rfc3339.parse(member.timestamp, _ctErr, "ct/bad-date", "the CT log state timestamp");
586
685
  var trust = LOG_STATE_TRUST[name];
587
- return { name: name, since: since, trusted: trust === "yes", conditional: trust === "conditional" };
686
+ return guard.verdict.of({ name: name, since: since, trusted: trust === "yes", conditional: trust === "conditional" });
588
687
  }
589
688
 
590
689
  function _parseTemporalInterval(ti) {
@@ -606,14 +705,14 @@ function _parseLog(log, operatorName) {
606
705
  _spkiAlg(spki);
607
706
  var logId = nodeCrypto.createHash("sha256").update(spki).digest();
608
707
  if (!logId.equals(statedId)) throw _ctErr("ct/log-id-mismatch", "the CT log_id does not match SHA-256 of the log key (RFC 6962 sec. 3.2)");
609
- return {
708
+ return guard.verdict.of({
610
709
  logId: logId, logIdHex: logId.toString("hex"), key: spki,
611
710
  description: typeof log.description === "string" ? log.description : null,
612
711
  url: typeof log.url === "string" ? log.url : (typeof log.submission_url === "string" ? log.submission_url : null),
613
712
  mmd: typeof log.mmd === "number" ? log.mmd : null,
614
713
  operator: operatorName, state: _parseLogState(log.state), temporalInterval: _parseTemporalInterval(log.temporal_interval),
615
714
  trusted: false,
616
- };
715
+ });
617
716
  }
618
717
 
619
718
  function _sameTemporal(a, b) {
@@ -664,8 +763,13 @@ function _logsAgree(a, b) {
664
763
  */
665
764
  function parseLogList(json, opts) {
666
765
  void opts;
766
+ /** @internal The named members, for the reason at the response reader: a log list carries tree sizes and
767
+ * state timestamps that are read as integers, and the list is signed, so a spelling that rounds to a
768
+ * different value than it denotes would verify under the distributor's signature while naming
769
+ * something else. */
667
770
  var doc = guard.json.parse(json, _ctErr, {
668
771
  maxBytes: C.LIMITS.CT_LOG_LIST_MAX_BYTES, maxDepth: C.LIMITS.JSON_MAX_DEPTH,
772
+ exactIntegerMembers: EXACT_INTEGER_RESPONSE_MEMBERS,
669
773
  badJson: "ct/bad-json", tooDeep: "ct/too-deep", duplicateMember: "ct/duplicate-member",
670
774
  tooLarge: "ct/too-large", badInput: "ct/bad-input", label: "the CT log list",
671
775
  });
@@ -695,7 +799,7 @@ function parseLogList(json, opts) {
695
799
  var version = typeof doc.version === "string" ? doc.version : null;
696
800
  var timestamp = (typeof doc.log_list_timestamp === "string" && rfc3339.isValid(doc.log_list_timestamp))
697
801
  ? rfc3339.parse(doc.log_list_timestamp, _ctErr, "ct/bad-date", "log_list_timestamp") : null;
698
- return { logs: logs, byLogId: byLogId, version: version, timestamp: timestamp };
802
+ return guard.verdict.of({ logs: logs, byLogId: byLogId, version: version, timestamp: timestamp });
699
803
  }
700
804
 
701
805
  function _resolveNotAfter(entry, opts) {
@@ -751,29 +855,33 @@ function _resolveNotAfter(entry, opts) {
751
855
  * var res = await pki.ct.verifySctWithLogList(sctEntry, embeddedSct, logList); // { valid, logId, logIdHex, operator, logState, timestamp }
752
856
  */
753
857
  function _classifyLog(sct, logList, entry, opts) {
754
- if (logList == null || typeof logList !== "object" || logList.byLogId == null) return { status: "bad-input", code: "ct/bad-input", message: "logList must be a pki.ct.parseLogList result" };
755
- if (sct == null || typeof sct !== "object" || typeof sct.logIdHex !== "string") return { status: "bad-input", code: "ct/bad-input", message: "the SCT is missing its logIdHex" };
858
+ if (logList == null || typeof logList !== "object" || logList.byLogId == null) return guard.verdict.of({ status: "bad-input", code: "ct/bad-input", message: "logList must be a pki.ct.parseLogList result" });
859
+ if (sct == null || typeof sct !== "object" || typeof sct.logIdHex !== "string") return guard.verdict.of({ status: "bad-input", code: "ct/bad-input", message: "the SCT is missing its logIdHex" });
756
860
  var log = logList.byLogId[sct.logIdHex];
757
- if (log == null) return { status: "not-found", code: "ct/log-not-found", message: "no trusted CT log matches the SCT's logId " + sct.logIdHex };
861
+ if (log == null) return guard.verdict.of({ status: "not-found", code: "ct/log-not-found", message: "no trusted CT log matches the SCT's logId " + sct.logIdHex });
758
862
  if (!log.state.trusted) {
759
- if (!log.state.conditional) return { status: "untrusted", log: log, code: "ct/log-untrusted", message: "the CT log state '" + log.state.name + "' is not trusted" };
863
+ if (!log.state.conditional) return guard.verdict.of({ status: "untrusted", log: log, code: "ct/log-untrusted", message: "the CT log state '" + log.state.name + "' is not trusted" });
760
864
  var ts = guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp");
761
- if (ts >= BigInt(guard.time.instantOf(log.state.since))) return { status: "untrusted", log: log, code: "ct/log-untrusted", message: "the CT log is retired and the SCT is not timestamped before its retirement (" + log.state.since.toISOString() + ")" };
865
+ if (ts >= BigInt(guard.time.instantOf(log.state.since))) return guard.verdict.of({ status: "untrusted", log: log, code: "ct/log-untrusted", message: "the CT log is retired and the SCT is not timestamped before its retirement (" + log.state.since.toISOString() + ")" });
762
866
  }
763
867
  if (log.temporalInterval) {
764
868
  var notAfter = _resolveNotAfter(entry, opts);
765
- if (notAfter == null) return { status: "temporal", log: log, code: "ct/temporal-interval", message: "the windowed CT log has no resolvable covered-certificate notAfter (pass a valid opts.certNotAfter)" };
869
+ if (notAfter == null) return guard.verdict.of({ status: "temporal", log: log, code: "ct/temporal-interval", message: "the windowed CT log has no resolvable covered-certificate notAfter (pass a valid opts.certNotAfter)" });
766
870
  if (!guard.time.within(notAfter, log.temporalInterval.startInclusive, log.temporalInterval.endExclusive, _ctErr, "ct/temporal-interval", "the covered certificate notAfter (pass a valid opts.certNotAfter)")) {
767
- return { status: "temporal", log: log, code: "ct/temporal-interval", message: "the covered certificate's notAfter is outside the CT log's temporal_interval" };
871
+ return guard.verdict.of({ status: "temporal", log: log, code: "ct/temporal-interval", message: "the covered certificate's notAfter is outside the CT log's temporal_interval" });
768
872
  }
769
873
  }
770
- return { status: "ok", log: log };
874
+ return guard.verdict.of({ status: "ok", log: log });
771
875
  }
772
876
 
773
877
  async function verifySctWithLogList(entry, sct, logList, opts) {
774
878
  opts = opts || {};
775
879
  var c = _classifyLog(sct, logList, entry, opts);
776
880
  if (c.status !== "ok") throw _ctErr(c.code, c.message);
881
+ /** @internal Read before the verify, so the timestamp the verdict states is the one the signature covered.
882
+ * Read after it, a caller's record could answer the verification with one instant and the report with
883
+ * another, and the verdict would name a time nothing signed. */
884
+ var reportedTimestamp = sct.timestamp;
777
885
  var valid = await verifySct(entry, sct, c.log.key);
778
886
  return guard.verdict.of({
779
887
  valid: valid,
@@ -781,7 +889,7 @@ async function verifySctWithLogList(entry, sct, logList, opts) {
781
889
  logIdHex: c.log.logIdHex,
782
890
  operator: c.log.operator,
783
891
  logState: c.log.state.name,
784
- timestamp: sct.timestamp,
892
+ timestamp: reportedTimestamp,
785
893
  });
786
894
  }
787
895
 
@@ -862,9 +970,11 @@ async function verifySctList(entry, list, logList, opts) {
862
970
  var results = [], validScts = 0, operators = [], seenOps = intrinsic.create(null), seenLogs = intrinsic.create(null);
863
971
  for (var i = 0; i < scts.length; i++) {
864
972
  var sct = scts[i];
865
- var row = { logIdHex: (sct && typeof sct.logIdHex === "string") ? sct.logIdHex : null, valid: false,
973
+ /** @internal The row is a per-SCT verdict a caller reads `valid` off, so it carries the sentinel
974
+ * from the moment it exists rather than at each of the five places it is appended. */
975
+ var row = guard.verdict.of({ logIdHex: (sct && typeof sct.logIdHex === "string") ? sct.logIdHex : null, valid: false,
866
976
  operator: null, logState: null,
867
- timestamp: (sct && typeof sct.timestamp === "bigint") ? sct.timestamp : null, code: undefined, reason: undefined };
977
+ timestamp: (sct && typeof sct.timestamp === "bigint") ? sct.timestamp : null, code: undefined, reason: undefined });
868
978
  var c;
869
979
  try { c = _classifyLog(sct, logList, entry, opts); }
870
980
  catch (e) {
@@ -945,7 +1055,14 @@ function _asParsedCert(v, field) {
945
1055
  */
946
1056
  function _issuerSpki(v, field) {
947
1057
  if (v != null && typeof v === "object" && !intrinsic.isBuffer(v) && v.subjectPublicKeyInfo != null && v.subjectPublicKeyInfo.bytes != null) {
948
- return guard.bytes.view(v.subjectPublicKeyInfo.bytes, CtError, "ct/bad-cert-entry", field + " subjectPublicKeyInfo.bytes");
1058
+ /** @internal A VIEW rather than a copy, as the parsed-certificate branch below also returns, and the
1059
+ * wide guard so the whole BufferSource set this module documents is read. The narrow one took a
1060
+ * Buffer or a Uint8Array only, so an issuer named by a DataView or by the ArrayBuffer a WebCrypto
1061
+ * export returns was refused with `ct/bad-cert-entry`, blaming the certificate for the container its
1062
+ * key arrived in; the issuerKeyHash binds the entry to its issuer, so one key has to give one hash
1063
+ * however it is held. */
1064
+ return guard.bytes.source(v.subjectPublicKeyInfo.bytes, CtError, "ct/bad-cert-entry",
1065
+ field + " subjectPublicKeyInfo.bytes");
949
1066
  }
950
1067
  return _asParsedCert(v, field).subjectPublicKeyInfo.bytes;
951
1068
  }
@@ -1008,8 +1125,22 @@ function x509CertEntry(cert, issuer) {
1008
1125
  * var ok = await pki.ct.verifyLogListSignature(logListJsonBytes, logListSig, googleSignerSpki);
1009
1126
  */
1010
1127
  async function verifyLogListSignature(json, signature, publicKey) {
1011
- var message = typeof json === "string" ? Buffer.from(json) : _toBuffer(json, "the CT log list JSON");
1012
- guard.limits.byteCap(message, C.LIMITS.CT_LOG_LIST_MAX_BYTES, _ctErr, "ct/too-large", "the CT log list");
1128
+ /** @internal Both arms are bounded before they allocate. A string is measured with
1129
+ * `Buffer.byteLength`, which computes the UTF-8 size without encoding it, so a string over the cap is
1130
+ * refused before `Buffer.from` would take it; its character count would not do, a character reaching
1131
+ * four bytes. The byte arm caps the view inside `_toBuffer`, before its copy. */
1132
+ var message;
1133
+ if (typeof json === "string") {
1134
+ var jsonBytes = _byteLength(json);
1135
+ if (jsonBytes > C.LIMITS.CT_LOG_LIST_MAX_BYTES) {
1136
+ throw _ctErr("ct/too-large", "the CT log list is " + jsonBytes + " bytes, over the " +
1137
+ C.LIMITS.CT_LOG_LIST_MAX_BYTES + "-byte cap");
1138
+ }
1139
+ message = _bufferFrom(json, "utf8");
1140
+ } else {
1141
+ message = _toBuffer(json, "the CT log list JSON", undefined, undefined,
1142
+ C.LIMITS.CT_LOG_LIST_MAX_BYTES, "ct/too-large");
1143
+ }
1013
1144
  var sig = _toBuffer(signature, "the CT log list signature");
1014
1145
  var spki = _toBuffer(publicKey, "the CT log list signer public key (SPKI)");
1015
1146
  var alg = _spkiAlg(spki);
@@ -1028,8 +1159,8 @@ async function verifyLogListSignature(json, signature, publicKey) {
1028
1159
  throw new CtError("ct/unsupported-algorithm", "unsupported CT log-list signer key algorithm (only rsaEncryption / ecPublicKey P-256)");
1029
1160
  }
1030
1161
  try {
1031
- var key = await subtle.importKey("spki", spki, imp, false, ["verify"]);
1032
- return await subtle.verify(ver, key, sig, message);
1162
+ var key = await _subtleImportKey(subtle, "spki", spki, imp, false, ["verify"]);
1163
+ return await _subtleVerify(subtle, ver, key, sig, message);
1033
1164
  } catch (e) {
1034
1165
  throw new CtError("ct/verify-error", "the CT log-list signature could not be evaluated", e);
1035
1166
  }
@@ -1071,8 +1202,11 @@ function _sigUrlFor(parsed) {
1071
1202
  }
1072
1203
 
1073
1204
  function _fetchBody(transport, url, req, label) {
1074
- return Promise.resolve().then(function () {
1075
- return transport({ method: "GET", url: url, headers: req.headers, tls: req.tls, timeout: req.timeout, maxResponseBytes: req.maxResponseBytes });
1205
+ return _promiseResolve(_Promise, undefined).then(function () {
1206
+ var request = { method: req.method || "GET", url: url, headers: req.headers, tls: req.tls,
1207
+ timeout: req.timeout, maxResponseBytes: req.maxResponseBytes };
1208
+ if (req.body !== undefined) request.body = req.body;
1209
+ return transport(request);
1076
1210
  }).then(function (res) {
1077
1211
  res = res || {};
1078
1212
  var h = intrinsic.create(null);
@@ -1083,13 +1217,750 @@ function _fetchBody(transport, url, req, label) {
1083
1217
  if (body.length > req.maxResponseBytes) throw _ctErr("ct/response-too-large", "the " + label + " (" + body.length + " bytes) exceeds the " + req.maxResponseBytes + "-byte cap");
1084
1218
  if (res.status !== 200) throw _ctErr("ct/http-error", "the CT server returned HTTP " + JSON.stringify(res.status) + " for the " + label);
1085
1219
  if (body.length === 0) throw _ctErr("ct/empty-response", "the CT server returned a 200 with an empty " + label);
1086
- return { body: body, status: res.status, contentType: _fetchCtypeToken(h), tls: res.tls || null };
1220
+ return guard.verdict.of({ body: body, status: res.status, contentType: _fetchCtypeToken(h), tls: res.tls || null });
1087
1221
  }).catch(function (e) {
1088
1222
  if (e instanceof PkiError) throw e;
1089
1223
  throw _ctErr("ct/transport-error", "the CT " + label + " request failed in the transport", e);
1090
1224
  });
1091
1225
  }
1092
1226
 
1227
+ /** @internal RFC 6962 sec. 3.5 fixes the TreeHeadSignature preimage at these 50 bytes: the version,
1228
+ * the signature type, the timestamp and tree size as big-endian uint64s, then the 32-byte root.
1229
+ * `v1(0)` and `tree_hash(1)` are the sec. 3.2 enum values. */
1230
+ var STH_PREIMAGE_BYTES = 50;
1231
+ var CT_VERSION_V1 = 0;
1232
+ var SIG_TYPE_TREE_HASH = 1;
1233
+ var CT_V1_PREFIX = "ct/v1/";
1234
+ var KNOWN_CLIENT_OPTS = intrinsic.assign(intrinsic.create(null), {
1235
+ url: 1, logKey: 1, transport: 1, tls: 1, headers: 1, timeout: 1, maxResponseBytes: 1,
1236
+ sth: 1, oldSth: 1, newSth: 1, leafHash: 1, start: 1, end: 1, chain: 1,
1237
+ tbsCertificate: 1, issuerKeyHash: 1,
1238
+ });
1239
+
1240
+ /** @internal The endpoint URL for one of the sec. 4 messages. A caller may pin the log's base URL
1241
+ * with or without the `ct/v1/` prefix the specification fixes, and neither spelling doubles it. */
1242
+ function _endpoint(url, message) {
1243
+ var parsed = _parseHttpsUrl(url, "log URL");
1244
+ var path = parsed.pathname;
1245
+ if (path.length === 0 || path[path.length - 1] !== "/") path += "/";
1246
+ var tail = CT_V1_PREFIX;
1247
+ if (path.length >= CT_V1_PREFIX.length + 1 &&
1248
+ _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 = "";
1252
+ return out;
1253
+ }
1254
+
1255
+ /** @internal The shared request shape every message uses: the caller's transport or a pinned-anchor
1256
+ * 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)");
1262
+ 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) {
1266
+ 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
+ }
1268
+ transport = httpTransport.https({ E: _ctErr, errPrefix: "ct" });
1269
+ }
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 });
1272
+ var headers = intrinsic.create(null);
1273
+ guard.identifier.optionNames(opts.headers).forEach(function (k) {
1274
+ var lk = _toLowerCase(k);
1275
+ if (lk !== "content-length" && lk !== "transfer-encoding") headers[k] = opts.headers[k];
1276
+ });
1277
+ return { transport: transport, req: { headers: headers, tls: _tlsForFetch(opts), timeout: timeout,
1278
+ maxResponseBytes: maxResponseBytes }, label: label };
1279
+ }
1280
+
1281
+ /** @internal A sec. 4 message, fetched and read as JSON. The specification returns every error as a
1282
+ * 4xx or 5xx with a human-readable message, which `_fetchBody` already turns into a typed refusal,
1283
+ * 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]); });
1288
+ var req = ctx.req;
1289
+ if (body !== undefined) {
1290
+ req = intrinsic.assign(intrinsic.create(null), req);
1291
+ req.method = "POST";
1292
+ req.body = body;
1293
+ var h = intrinsic.create(null);
1294
+ intrinsic.keys(req.headers).forEach(function (k) { h[k] = req.headers[k]; });
1295
+ h["content-type"] = "application/json";
1296
+ req.headers = h;
1297
+ }
1298
+ var res = await _fetchBody(ctx.transport, url.href, req, label);
1299
+ /** @internal The exact-integer rule applies to the NAMED members, the ones `_uintField` reads. RFC 6962
1300
+ * responses are ordinary JSON, where `1e3` is legal, so the stricter `integersOnly` would refuse a
1301
+ * document a conforming log may emit; what this refuses is a spelling whose value is not exactly its
1302
+ * own integer. `1.0000000000000001` converts to the Number 1, so `_uintField` cannot be asked about it
1303
+ * afterwards, and a tree size or timestamp read that way is folded into the reconstructed binary
1304
+ * preimage a signature is checked over: the rounded spelling verifies, and the value the caller is
1305
+ * handed is not the one the log sent.
1306
+ *
1307
+ * Named rather than applied document-wide even though RFC 6962 closes every schema here, because a
1308
+ * rule over every number is a bet that no field anywhere in the format is fractional, and that bet
1309
+ * reads as safe right up until a field nobody thought about. Naming the members that need it says what
1310
+ * is actually meant and cannot be wrong about the rest. */
1311
+ var parsed = guard.json.parse(res.body, _ctErr, {
1312
+ maxBytes: req.maxResponseBytes, maxDepth: C.LIMITS.JSON_MAX_DEPTH,
1313
+ exactIntegerMembers: EXACT_INTEGER_RESPONSE_MEMBERS,
1314
+ badJson: "ct/bad-json", tooDeep: "ct/too-deep", duplicateMember: "ct/duplicate-member",
1315
+ tooLarge: "ct/too-large", badInput: "ct/bad-input", label: "the " + label + " response",
1316
+ });
1317
+ if (!parsed || typeof parsed !== "object" || intrinsic.isArray(parsed)) {
1318
+ throw _ctErr("ct/bad-json", "the " + label + " is not a JSON object");
1319
+ }
1320
+ return parsed;
1321
+ }
1322
+
1323
+ /** @internal A base64 field, decoded and held to a byte length where the specification fixes one. */
1324
+ function _b64Field(obj, name, code, label, exactLen) {
1325
+ var v = obj[name];
1326
+ if (typeof v !== "string") throw _ctErr(code, "the " + label + " field " + name + " is not a base64 string");
1327
+ var buf = guard.encoding.base64(v, C.LIMITS.CT_LOG_LIST_MAX_BYTES, _ctErr, code, name);
1328
+ if (exactLen !== undefined && buf.length !== exactLen) {
1329
+ throw _ctErr(code, "the " + label + " field " + name + " is " + buf.length + " bytes, expected " + exactLen);
1330
+ }
1331
+ return buf;
1332
+ }
1333
+
1334
+ /** @internal A non-negative integer field the specification states in decimal. */
1335
+ function _uintField(obj, name, code, label) {
1336
+ var v = obj[name];
1337
+ if (typeof v !== "number" || !intrinsic.isSafeInteger(v) || v < 0) {
1338
+ if (typeof v !== "string") {
1339
+ throw _ctErr(code, "the " + label + " field " + name + " is not a non-negative integer");
1340
+ }
1341
+ return guard.range.decimalUint64(v, _ctErr, code, "the " + label + " field " + name);
1342
+ }
1343
+ return intrinsic.BigInt(v);
1344
+ }
1345
+
1346
+ /**
1347
+ * @primitive pki.ct.sthSignedData
1348
+ * @signature pki.ct.sthSignedData(sth) -> Buffer
1349
+ * @since 0.8.40
1350
+ * @status stable
1351
+ * @spec RFC 6962
1352
+ * @related pki.ct.verifySth, pki.ct.getSth
1353
+ *
1354
+ * The 50 bytes a log signs to produce a signed tree head, which RFC 6962 sec. 3.5 fixes as the
1355
+ * `TreeHeadSignature` structure: the version, the signature type `tree_hash`, the timestamp and the
1356
+ * tree size as big-endian uint64s, then the 32-byte root hash. Given as a verb so a caller holding an
1357
+ * STH from anywhere can check it without a fetch.
1358
+ *
1359
+ * @opts
1360
+ * timestamp: number | bigint, // the log's timestamp, milliseconds since the epoch
1361
+ * treeSize: number | bigint, // the number of entries the head commits to
1362
+ * rootHash: Buffer, // the 32-byte Merkle tree head
1363
+ *
1364
+ * @example
1365
+ * pki.ct.sthSignedData({ timestamp: 1800000000000, treeSize: 3,
1366
+ * rootHash: pki.merkle.emptyRootHash() }).length; // -> 50
1367
+ */
1368
+ /** @internal Two of these five are here so that what `pki.ct.getSth` RETURNS is what this verb TAKES. A
1369
+ * client that fetches a tree head and then wants the bytes the log signed has to be able to hand one
1370
+ * verb's result to the other, and both fields the fetch adds were refused as unknown:
1371
+ *
1372
+ * - `raw` is the log's own JSON document, which getSth hands back beside the decoded fields.
1373
+ * - `then` is the sentinel `guard.verdict.of` defines on every verdict, own and non-enumerable, so that
1374
+ * resolving one cannot run an inherited accessor. `guard.identifier.assertKnownKeys` reads own
1375
+ * non-enumerable names deliberately, so it sees it.
1376
+ *
1377
+ * Neither reaches the preimage, which is built from the timestamp, the tree size and the root hash. The
1378
+ * allowlist still catches what it is for: a caller's misspelling of one of those three. */
1379
+ var _STH_KEYS = intrinsic.assign(intrinsic.create(null),
1380
+ { timestamp: 1, treeSize: 1, rootHash: 1, signature: 1, raw: 1, then: 1 });
1381
+ function sthSignedData(sth) {
1382
+ sth = sth || {};
1383
+ guard.identifier.assertKnownKeys(sth, _STH_KEYS, _ctErr, "ct/bad-input", "unknown sth field ");
1384
+ var ts = guard.range.uint64(sth.timestamp, _ctErr, "ct/bad-input", "sth.timestamp");
1385
+ var size = guard.range.uint64(sth.treeSize, _ctErr, "ct/bad-input", "sth.treeSize");
1386
+ var root = _toBuffer(sth.rootHash, "sth.rootHash");
1387
+ if (root.length !== 32) {
1388
+ throw _ctErr("ct/bad-input", "sth.rootHash must be exactly 32 bytes (SHA-256), got " + root.length);
1389
+ }
1390
+ var out = _bufferAlloc(STH_PREIMAGE_BYTES);
1391
+ out[0] = CT_VERSION_V1;
1392
+ out[1] = SIG_TYPE_TREE_HASH;
1393
+ _writeU64BE(out, ts, 2);
1394
+ _writeU64BE(out, size, 10);
1395
+ _bufCopy(root, out, 18);
1396
+ return out;
1397
+ }
1398
+
1399
+ /**
1400
+ * @primitive pki.ct.verifySth
1401
+ * @signature pki.ct.verifySth(sth, logKey) -> Promise<boolean>
1402
+ * @since 0.8.40
1403
+ * @status stable
1404
+ * @spec RFC 6962
1405
+ * @related pki.ct.sthSignedData, pki.ct.getSth
1406
+ *
1407
+ * Verify a signed tree head's `tree_head_signature` over the `sthSignedData` preimage under a log's
1408
+ * SubjectPublicKeyInfo. Resolves `true` or `false`: whether a given key signed a given head is a
1409
+ * verdict about the head, not a fault in it. A malformed input throws.
1410
+ *
1411
+ * `sth.signature` is the `digitally-signed` structure the log returns, hash and signature algorithm
1412
+ * bytes included, which is what `pki.ct.verifySct` also takes.
1413
+ *
1414
+ * @example
1415
+ * var pair = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
1416
+ * var logSpki = await pki.key.export(pair.publicKey);
1417
+ * var head = { timestamp: 1800000000000, treeSize: 1, rootHash: pki.merkle.emptyRootHash() };
1418
+ * var raw = Buffer.from(await pki.webcrypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" },
1419
+ * pair.privateKey, pki.ct.sthSignedData(head)));
1420
+ * var der = pki.asn1.build.sequence([
1421
+ * pki.asn1.build.integer(BigInt("0x" + raw.subarray(0, 32).toString("hex"))),
1422
+ * pki.asn1.build.integer(BigInt("0x" + raw.subarray(32).toString("hex")))]);
1423
+ * head.signature = Buffer.concat([Buffer.from([4, 3, (der.length >> 8) & 0xff, der.length & 0xff]), der]);
1424
+ * await pki.ct.verifySth(head, logSpki);
1425
+ * // -> true
1426
+ */
1427
+ async function verifySth(sth, logKey) {
1428
+ sth = sth || {};
1429
+ var signature = _toBuffer(sth.signature, "sth.signature");
1430
+ var signedData = sthSignedData({ timestamp: sth.timestamp, treeSize: sth.treeSize, rootHash: sth.rootHash });
1431
+ return await _verifyDigitallySigned(signedData, signature, logKey, "the signed tree head");
1432
+ }
1433
+
1434
+ /**
1435
+ * @primitive pki.ct.getSth
1436
+ * @signature pki.ct.getSth(opts) -> Promise<{ treeSize, timestamp, rootHash, signature, raw }>
1437
+ * @since 0.8.40
1438
+ * @status stable
1439
+ * @spec RFC 6962
1440
+ * @defends signature-bypass (CWE-347)
1441
+ * @related pki.ct.verifySth, pki.ct.getProofByHash, pki.merkle.verifyInclusion
1442
+ *
1443
+ * Fetch a log's latest signed tree head (RFC 6962 sec. 4.3) and verify it under the key
1444
+ * `opts.logKey` pins. An STH whose signature does not verify is `ct/sth-untrusted` and no field of it
1445
+ * is returned: a tree head nobody vouched for is not a tree head, and everything a client does next
1446
+ * is measured against its root.
1447
+ *
1448
+ * `opts.logKey` is required. There is no unverified mode and no baked-in key.
1449
+ *
1450
+ * `treeSize` and `timestamp` come back as `BigInt`, since both are uint64 on the wire.
1451
+ *
1452
+ * @opts
1453
+ * url: string, // the log's base URL, with or without the ct/v1/ prefix
1454
+ * logKey: BufferSource, // the log's SubjectPublicKeyInfo, pinned by the caller
1455
+ * transport: function, // optional (request) => Promise<{ status, headers, body }>
1456
+ * tls: object, // optional { anchors, useSystemStore, ... } when no transport is given
1457
+ * headers: object, // optional extra request headers
1458
+ * timeout: number, // optional per-request timeout in milliseconds
1459
+ * maxResponseBytes: number, // optional response cap
1460
+ *
1461
+ * @example
1462
+ * // requires: base -- the CT log's base URL, e.g. https://ct.googleapis.com/logs/us/argon2025h1/
1463
+ * // requires: spki -- the log's SubjectPublicKeyInfo, from a verified log list
1464
+ * // requires: t -- a transport, e.g. the one pki.ct.fetchLogList uses by default
1465
+ * var sth = await pki.ct.getSth({ url: base, logKey: spki, transport: t });
1466
+ * sth.treeSize; // -> 3n
1467
+ */
1468
+ async function getSth(opts) {
1469
+ opts = opts || {};
1470
+ /** @internal ONE read of the option, and the presence check and the copy both decide from it. Reading
1471
+ * it twice let an accessor answer the two separately: a non-null value for the check and an
1472
+ * attacker's SPKI for the copy, and the tree head was then verified against a key the caller never
1473
+ * pinned. The key is COPIED at the call, before the request goes out. Read after the fetch instead,
1474
+ * it is whatever the caller's buffer holds by then: a caller reusing that buffer has the tree head
1475
+ * checked against a key it never pinned, so the log that answered need not be the log it asked. */
1476
+ var suppliedLogKey = opts.logKey;
1477
+ if (suppliedLogKey == null) {
1478
+ throw _ctErr("ct/bad-input", "opts.logKey is required -- the caller-pinned log SPKI; an unverified tree head is not a result");
1479
+ }
1480
+ var pinnedKey = _toBuffer(suppliedLogKey, "opts.logKey", _ctErr, "ct/bad-input");
1481
+ var doc = await _clientJson(opts, "get-sth", "sth");
1482
+ var sth = {
1483
+ treeSize: _uintField(doc, "tree_size", "ct/bad-sth", "sth"),
1484
+ timestamp: _uintField(doc, "timestamp", "ct/bad-sth", "sth"),
1485
+ rootHash: _b64Field(doc, "sha256_root_hash", "ct/bad-sth", "sth", 32),
1486
+ signature: _b64Field(doc, "tree_head_signature", "ct/bad-sth", "sth"),
1487
+ };
1488
+ var ok = await verifySth(sth, pinnedKey);
1489
+ if (ok !== true) {
1490
+ throw _ctErr("ct/sth-untrusted", "the signed tree head did not verify under the pinned log key -- " +
1491
+ "its tree size, timestamp and root are not attested and were not returned");
1492
+ }
1493
+ sth.raw = doc;
1494
+ return guard.verdict.of(sth);
1495
+ }
1496
+
1497
+ /**
1498
+ * @primitive pki.ct.getProofByHash
1499
+ * @signature pki.ct.getProofByHash(opts) -> Promise<{ leafIndex, auditPath, verified }>
1500
+ * @since 0.8.40
1501
+ * @status stable
1502
+ * @spec RFC 6962
1503
+ * @related pki.ct.getSth, pki.merkle.verifyInclusion
1504
+ *
1505
+ * Fetch the Merkle audit proof for a leaf hash (RFC 6962 sec. 4.5) and fold it to the root of the
1506
+ * tree head it was requested at. `opts.sth` is a VERIFIED head, from `pki.ct.getSth`: the tree size
1507
+ * the query needs comes from it, so a size and a head cannot disagree, and its root is what the proof
1508
+ * is folded against. A proof that does not fold, or that does not support the leaf index the log
1509
+ * states, is `ct/proof-mismatch`.
1510
+ *
1511
+ * @opts
1512
+ * url: string, // the log's base URL
1513
+ * leafHash: BufferSource, // the 32-byte v1 leaf hash to prove
1514
+ * sth: object, // a verified { treeSize, rootHash } from pki.ct.getSth
1515
+ * transport, tls, headers, timeout, maxResponseBytes // as pki.ct.getSth takes them
1516
+ *
1517
+ * @example
1518
+ * // requires: base -- the CT log's base URL, e.g. https://ct.googleapis.com/logs/us/argon2025h1/
1519
+ * // requires: spki -- the log's SubjectPublicKeyInfo, from a verified log list
1520
+ * // requires: t -- a transport, e.g. the one pki.ct.fetchLogList uses by default
1521
+ * // requires: lh -- a v1 leaf hash, from pki.ct.getEntries or a certificate you hold
1522
+ * // requires: sth -- a verified tree head from pki.ct.getSth
1523
+ * var p = await pki.ct.getProofByHash({ url: base, leafHash: lh, sth: sth, transport: t });
1524
+ * p.verified; // -> true
1525
+ */
1526
+ async function getProofByHash(opts) {
1527
+ opts = opts || {};
1528
+ var sth = _assertSth(opts.sth, "opts.sth");
1529
+ /** @internal Copied for the reason the tree head is: it is sent on the wire and then folded against the
1530
+ * head after the fetch returns, so a view would let the leaf being proved differ from the leaf asked
1531
+ * about. */
1532
+ var leafHash = _toBuffer(opts.leafHash, "opts.leafHash", _ctErr, "ct/bad-input");
1533
+ if (leafHash.length !== 32) {
1534
+ throw _ctErr("ct/bad-input", "opts.leafHash must be exactly 32 bytes (a v1 leaf hash), got " + leafHash.length);
1535
+ }
1536
+ var doc = await _clientJson(opts, "get-proof-by-hash", "audit proof", {
1537
+ hash: _bufToString(leafHash, "base64"), tree_size: _stringOf(sth.treeSize),
1538
+ });
1539
+ var leafIndex = _uintField(doc, "leaf_index", "ct/bad-proof", "audit proof");
1540
+ var auditPath = _b64Array(doc, "audit_path", "ct/bad-proof", "audit proof", 32, C.LIMITS.MERKLE_MAX_PROOF_NODES);
1541
+ var folded = merkle.verifyInclusion({
1542
+ leafIndex: leafIndex, treeSize: sth.treeSize, leafHash: leafHash,
1543
+ proof: auditPath, rootHash: sth.rootHash,
1544
+ });
1545
+ if (folded !== true) {
1546
+ throw _ctErr("ct/proof-mismatch", "the audit proof the log served does not fold this leaf to the " +
1547
+ "root of the tree head it was requested at");
1548
+ }
1549
+ return guard.verdict.of({ leafIndex: leafIndex, auditPath: auditPath, verified: true });
1550
+ }
1551
+
1552
+ /**
1553
+ * @primitive pki.ct.getSthConsistency
1554
+ * @signature pki.ct.getSthConsistency(opts) -> Promise<{ consistency, verified }>
1555
+ * @since 0.8.40
1556
+ * @status stable
1557
+ * @spec RFC 6962
1558
+ * @related pki.ct.getSth, pki.merkle.verifyConsistency
1559
+ *
1560
+ * Fetch the Merkle consistency proof between two tree heads (RFC 6962 sec. 4.4) and check that the
1561
+ * older is a prefix of the newer. Both `opts.oldSth` and `opts.newSth` are VERIFIED heads, so the two
1562
+ * sizes the query needs and both roots the proof is checked against come from heads a log signed. A
1563
+ * proof that does not reconstruct both roots is `ct/consistency-mismatch`, which is the append-only
1564
+ * failure: the log rewrote history or served a proof for a different pair.
1565
+ *
1566
+ * @opts
1567
+ * url: string, // the log's base URL
1568
+ * oldSth: object, // the earlier verified { treeSize, rootHash }
1569
+ * newSth: object, // the later verified { treeSize, rootHash }
1570
+ * transport, tls, headers, timeout, maxResponseBytes // as pki.ct.getSth takes them
1571
+ *
1572
+ * @example
1573
+ * // requires: base -- the CT log's base URL, e.g. https://ct.googleapis.com/logs/us/argon2025h1/
1574
+ * // requires: spki -- the log's SubjectPublicKeyInfo, from a verified log list
1575
+ * // requires: t -- a transport, e.g. the one pki.ct.fetchLogList uses by default
1576
+ * // requires: a -- an earlier verified tree head from pki.ct.getSth
1577
+ * // requires: b -- a later verified tree head from pki.ct.getSth
1578
+ * var c = await pki.ct.getSthConsistency({ url: base, oldSth: a, newSth: b, transport: t });
1579
+ * c.verified; // -> true
1580
+ */
1581
+ async function getSthConsistency(opts) {
1582
+ opts = opts || {};
1583
+ var oldSth = _assertSth(opts.oldSth, "opts.oldSth");
1584
+ var newSth = _assertSth(opts.newSth, "opts.newSth");
1585
+ if (oldSth.treeSize > newSth.treeSize) {
1586
+ throw _ctErr("ct/bad-input", "opts.oldSth names tree size " + oldSth.treeSize +
1587
+ ", which is larger than opts.newSth's " + newSth.treeSize);
1588
+ }
1589
+ var doc = await _clientJson(opts, "get-sth-consistency", "consistency proof", {
1590
+ first: _stringOf(oldSth.treeSize), second: _stringOf(newSth.treeSize),
1591
+ });
1592
+ var consistency = _b64Array(doc, "consistency", "ct/bad-consistency", "consistency proof", 32, C.LIMITS.MERKLE_MAX_PROOF_NODES);
1593
+ var ok = merkle.verifyConsistency({
1594
+ oldSize: oldSth.treeSize, newSize: newSth.treeSize,
1595
+ oldRoot: oldSth.rootHash, newRoot: newSth.rootHash, proof: consistency,
1596
+ });
1597
+ if (ok !== true) {
1598
+ throw _ctErr("ct/consistency-mismatch", "the consistency proof the log served does not reconstruct " +
1599
+ "both tree heads -- the older is not shown to be a prefix of the newer");
1600
+ }
1601
+ return guard.verdict.of({ consistency: consistency, verified: true });
1602
+ }
1603
+
1604
+ /**
1605
+ * @primitive pki.ct.getEntries
1606
+ * @signature pki.ct.getEntries(opts) -> Promise<{ entries, requested, truncated }>
1607
+ * @since 0.8.40
1608
+ * @status stable
1609
+ * @spec RFC 6962
1610
+ * @related pki.ct.getSth, pki.merkle.root
1611
+ *
1612
+ * Fetch log entries by index range (RFC 6962 sec. 4.6). Each entry comes back as `leafInput` and
1613
+ * `extraData` buffers.
1614
+ *
1615
+ * **This response carries no signature.** The specification states that this message is not signed,
1616
+ * and that the retrieved data is verified by constructing the Merkle Tree Hash corresponding to a
1617
+ * retrieved tree head. So nothing here is verified, and a caller that needs the entries to be the
1618
+ * log's must build the tree and compare it with a head from `pki.ct.getSth`.
1619
+ *
1620
+ * A log may serve fewer entries than the range asks for, which the specification permits: "Logs MAY
1621
+ * restrict the number of entries that can be retrieved per 'get-entries' request." That is reported
1622
+ * as `truncated` beside `requested` rather than treated as a fault, so a caller paginates. MORE
1623
+ * entries than the range asks for is a fault and is refused.
1624
+ *
1625
+ * @opts
1626
+ * url: string, // the log's base URL
1627
+ * start: number | bigint, // the first index, 0-based and inclusive
1628
+ * end: number | bigint, // the last index, inclusive
1629
+ * transport, tls, headers, timeout, maxResponseBytes // as pki.ct.getSth takes them
1630
+ *
1631
+ * @example
1632
+ * // requires: base -- the CT log's base URL, e.g. https://ct.googleapis.com/logs/us/argon2025h1/
1633
+ * // requires: spki -- the log's SubjectPublicKeyInfo, from a verified log list
1634
+ * // requires: t -- a transport, e.g. the one pki.ct.fetchLogList uses by default
1635
+ * var e = await pki.ct.getEntries({ url: base, start: 0, end: 1, transport: t });
1636
+ * e.entries[0].leafInput; // -> <Buffer ...>
1637
+ */
1638
+ async function getEntries(opts) {
1639
+ opts = opts || {};
1640
+ var start = guard.range.uint64(opts.start, _ctErr, "ct/bad-input", "opts.start");
1641
+ var end = guard.range.uint64(opts.end, _ctErr, "ct/bad-input", "opts.end");
1642
+ if (end < start) {
1643
+ throw _ctErr("ct/bad-input", "opts.end (" + end + ") is before opts.start (" + start + ")");
1644
+ }
1645
+ var requested = end - start + 1n;
1646
+ if (requested > intrinsic.BigInt(C.LIMITS.CT_MAX_ENTRIES_PER_REQUEST)) {
1647
+ throw _ctErr("ct/bad-input", "a range of " + requested + " entries exceeds the " +
1648
+ C.LIMITS.CT_MAX_ENTRIES_PER_REQUEST + "-entry request cap");
1649
+ }
1650
+ var doc = await _clientJson(opts, "get-entries", "entries", { start: _stringOf(start), end: _stringOf(end) });
1651
+ var raw = doc.entries;
1652
+ if (!intrinsic.isArray(raw)) throw _ctErr("ct/bad-entries", "the entries field is not an array");
1653
+ if (intrinsic.BigInt(raw.length) > requested) {
1654
+ throw _ctErr("ct/bad-entries", "the log served " + raw.length + " entries for a range of " + requested);
1655
+ }
1656
+ var entries = [];
1657
+ for (var i = 0; i < raw.length; i++) {
1658
+ var e = raw[i];
1659
+ if (!e || typeof e !== "object") throw _ctErr("ct/bad-entries", "entries[" + i + "] is not an object");
1660
+ if (typeof e.leaf_input !== "string" || typeof e.extra_data !== "string") {
1661
+ throw _ctErr("ct/bad-entries", "entries[" + i + "] is missing leaf_input or extra_data");
1662
+ }
1663
+ guard.list.append(entries, {
1664
+ leafInput: guard.encoding.base64(e.leaf_input, C.LIMITS.CT_LOG_LIST_MAX_BYTES, _ctErr, "ct/bad-entries", "entries[" + i + "].leaf_input"),
1665
+ extraData: guard.encoding.base64(e.extra_data, C.LIMITS.CT_LOG_LIST_MAX_BYTES, _ctErr, "ct/bad-entries", "entries[" + i + "].extra_data"),
1666
+ });
1667
+ }
1668
+ return guard.verdict.of({ entries: entries, requested: _numberOf(requested),
1669
+ truncated: intrinsic.BigInt(entries.length) < requested });
1670
+ }
1671
+
1672
+ /**
1673
+ * @primitive pki.ct.getRoots
1674
+ * @signature pki.ct.getRoots(opts) -> Promise<{ certificates }>
1675
+ * @since 0.8.40
1676
+ * @status stable
1677
+ * @spec RFC 6962
1678
+ * @related pki.ct.addChain
1679
+ *
1680
+ * Fetch the root certificates a log accepts (RFC 6962 sec. 4.7), as DER buffers. This response
1681
+ * carries no signature either, so it tells a caller what to submit rather than what to trust.
1682
+ *
1683
+ * @opts
1684
+ * url: string, // the log's base URL
1685
+ * transport, tls, headers, timeout, maxResponseBytes // as pki.ct.getSth takes them
1686
+ *
1687
+ * @example
1688
+ * // requires: base -- the CT log's base URL, e.g. https://ct.googleapis.com/logs/us/argon2025h1/
1689
+ * // requires: spki -- the log's SubjectPublicKeyInfo, from a verified log list
1690
+ * // requires: t -- a transport, e.g. the one pki.ct.fetchLogList uses by default
1691
+ * var r = await pki.ct.getRoots({ url: base, transport: t });
1692
+ * r.certificates.length; // -> 2
1693
+ */
1694
+ async function getRoots(opts) {
1695
+ opts = opts || {};
1696
+ var doc = await _clientJson(opts, "get-roots", "roots");
1697
+ return guard.verdict.of({ certificates: _b64Array(doc, "certificates", "ct/bad-roots", "roots", undefined, C.LIMITS.CT_MAX_ROOTS) });
1698
+ }
1699
+
1700
+ /**
1701
+ * @primitive pki.ct.addChain
1702
+ * @signature pki.ct.addChain(opts) -> Promise<{ sct, verified }>
1703
+ * @since 0.8.40
1704
+ * @status stable
1705
+ * @spec RFC 6962
1706
+ * @defends signature-bypass (CWE-347)
1707
+ * @related pki.ct.addPreChain, pki.ct.verifySct
1708
+ *
1709
+ * Submit a certificate chain to a log (RFC 6962 sec. 4.1) and verify the signed certificate timestamp
1710
+ * it returns. The SCT is the log's promise to include the certificate, so it is checked against the
1711
+ * chain it was issued for and the key `opts.logKey` pins: one that does not verify, or that names a
1712
+ * different log, is `ct/sct-untrusted` and is not returned. A receipt nobody can check is not a
1713
+ * receipt.
1714
+ *
1715
+ * `opts.chain[0]` is the end-entity certificate the SCT covers; the rest are the issuers the log needs
1716
+ * to build a path, and are submitted unread.
1717
+ *
1718
+ * @opts
1719
+ * url: string, // the log's base URL
1720
+ * logKey: BufferSource, // the log's SubjectPublicKeyInfo, pinned by the caller
1721
+ * chain: Array, // [leafDer, ...issuerDers], each a DER Buffer
1722
+ * transport, tls, headers, timeout, maxResponseBytes // as pki.ct.getSth takes them
1723
+ *
1724
+ * @example
1725
+ * // requires: base -- the CT log's base URL, e.g. https://ct.googleapis.com/logs/us/argon2025h1/
1726
+ * // requires: spki -- the log's SubjectPublicKeyInfo, from a verified log list
1727
+ * // requires: t -- a transport, e.g. the one pki.ct.fetchLogList uses by default
1728
+ * // requires: leafDer -- the end-entity certificate to submit (DER)
1729
+ * var r = await pki.ct.addChain({ url: base, logKey: spki, chain: [leafDer], transport: t });
1730
+ * r.verified; // -> true
1731
+ */
1732
+ async function addChain(opts) {
1733
+ return await _addChain(opts, "add-chain", 0);
1734
+ }
1735
+
1736
+ /**
1737
+ * @primitive pki.ct.addPreChain
1738
+ * @signature pki.ct.addPreChain(opts) -> Promise<{ sct, verified }>
1739
+ * @since 0.8.40
1740
+ * @status stable
1741
+ * @spec RFC 6962
1742
+ * @defends signature-bypass (CWE-347)
1743
+ * @related pki.ct.addChain, pki.ct.verifySct
1744
+ *
1745
+ * Submit a precertificate chain to a log (RFC 6962 sec. 4.2) and verify the signed certificate
1746
+ * timestamp it returns. A precertificate SCT covers the `precert_entry` form, whose signed data is the
1747
+ * issuer key hash and the TBSCertificate rather than the certificate itself, so `opts.tbsCertificate`
1748
+ * and `opts.issuerKeyHash` supply what that form needs. Verification is otherwise as `addChain`
1749
+ * describes it.
1750
+ *
1751
+ * @opts
1752
+ * url: string, // the log's base URL
1753
+ * logKey: BufferSource, // the log's SubjectPublicKeyInfo, pinned by the caller
1754
+ * chain: Array, // [precertDer, ...issuerDers]
1755
+ * tbsCertificate: BufferSource, // the TBSCertificate the SCT covers
1756
+ * issuerKeyHash: BufferSource, // SHA-256 of the issuer's SubjectPublicKeyInfo, 32 bytes
1757
+ * transport, tls, headers, timeout, maxResponseBytes // as pki.ct.getSth takes them
1758
+ *
1759
+ * @example
1760
+ * // requires: base -- the CT log's base URL, e.g. https://ct.googleapis.com/logs/us/argon2025h1/
1761
+ * // requires: spki -- the log's SubjectPublicKeyInfo, from a verified log list
1762
+ * // requires: t -- a transport, e.g. the one pki.ct.fetchLogList uses by default
1763
+ * // requires: preDer -- the precertificate to submit (DER)
1764
+ * // requires: tbs -- its TBSCertificate
1765
+ * // requires: ikh -- SHA-256 of the issuer SubjectPublicKeyInfo
1766
+ * var r = await pki.ct.addPreChain({ url: base, logKey: spki, chain: [preDer],
1767
+ * tbsCertificate: tbs, issuerKeyHash: ikh, transport: t });
1768
+ * r.verified; // -> true
1769
+ */
1770
+ async function addPreChain(opts) {
1771
+ return await _addChain(opts, "add-pre-chain", 1);
1772
+ }
1773
+
1774
+ /** @internal The two submission messages differ in their path and in the entry form the SCT covers.
1775
+ * The chain encoding, the response shape, and the refusal when the receipt does not verify are one
1776
+ * rule stated once. */
1777
+ async function _addChain(opts, message, entryType) {
1778
+ opts = opts || {};
1779
+ /** @internal ONE read of the option, used by the presence check here and by the copy below. Reading it
1780
+ * twice let an accessor answer the two separately, presenting a non-null value to the check and an
1781
+ * attacker's SPKI to the copy, and the returned receipt was then verified against a key the caller
1782
+ * never pinned. */
1783
+ var suppliedLogKey = opts.logKey;
1784
+ if (suppliedLogKey == null) {
1785
+ throw _ctErr("ct/bad-input", "opts.logKey is required -- the caller-pinned log SPKI; an unverified SCT is not a receipt");
1786
+ }
1787
+ if (!intrinsic.isArray(opts.chain) || opts.chain.length === 0) {
1788
+ throw _ctErr("ct/bad-input", "opts.chain must be a non-empty array of DER certificates, leaf first");
1789
+ }
1790
+ /** @internal Each element is copied ONCE and both the submitted body and the entry the returned SCT is
1791
+ * verified over are derived from that one copy. Reading `opts.chain[0]` a second time lets an indexed
1792
+ * accessor hand one certificate to the log and another to the check, so the receipt would attest a
1793
+ * certificate that was never submitted. The length is bound for the same reason. */
1794
+ var chainLen = opts.chain.length;
1795
+ var chainDer = [], chain = [];
1796
+ for (var i = 0; i < chainLen; i++) {
1797
+ guard.list.append(chainDer, _toBuffer(opts.chain[i], "opts.chain[" + i + "]"));
1798
+ guard.list.append(chain, _bufToString(chainDer[i], "base64"));
1799
+ }
1800
+ /** @internal The entry and the pinned key are COPIED before the request goes out. Both are read again
1801
+ * after it returns: the entry is the preimage the returned SCT is verified over, and the key is what
1802
+ * the SCT's logId is matched against and what the signature is checked under. Held as views, a caller
1803
+ * reusing either buffer during the request gets a receipt for bytes it did not submit, or one checked
1804
+ * against a log it did not pin. */
1805
+ function _snap(v, label) { return _toBuffer(v, label, _ctErr, "ct/bad-input"); }
1806
+ var entry = entryType === 0
1807
+ ? { entryType: 0, leafCert: chainDer[0] }
1808
+ : { entryType: 1, tbsCertificate: _snap(opts.tbsCertificate, "opts.tbsCertificate"),
1809
+ issuerKeyHash: _snap(opts.issuerKeyHash, "opts.issuerKeyHash") };
1810
+ var pinnedKey = _snap(suppliedLogKey, "opts.logKey");
1811
+ var doc = await _clientJson(opts, message, "SCT", null, _jsonStringify({ chain: chain }));
1812
+ var version = doc.sct_version;
1813
+ if (version !== 0) {
1814
+ throw _ctErr("ct/bad-sct", "the log returned sct_version " + guard.text.showValue(version) +
1815
+ "; this build reads v1(0) (RFC 6962 sec. 3.2)");
1816
+ }
1817
+ var sct = {
1818
+ version: 0,
1819
+ logId: _b64Field(doc, "id", "ct/bad-sct", "sct", 32),
1820
+ timestamp: _uintField(doc, "timestamp", "ct/bad-sct", "sct"),
1821
+ extensions: typeof doc.extensions === "string" && doc.extensions.length === 0
1822
+ ? _bufferAlloc(0) : _b64Field(doc, "extensions", "ct/bad-sct", "sct"),
1823
+ signature: _b64Field(doc, "signature", "ct/bad-sct", "sct"),
1824
+ };
1825
+ var pinnedId = nodeCrypto.createHash("sha256").update(pinnedKey).digest();
1826
+ if (_bufferCompare(sct.logId, pinnedId) !== 0) {
1827
+ throw _ctErr("ct/sct-untrusted", "the SCT names log id " + _bufToString(sct.logId, "hex") +
1828
+ " where the pinned key's id is " + _bufToString(pinnedId, "hex") + " -- it is not this log's receipt");
1829
+ }
1830
+ var signedData = reconstructSignedData(entry, sct);
1831
+ var ok = await _verifyDigitallySigned(signedData, sct.signature, pinnedKey, "the SCT");
1832
+ if (ok !== true) {
1833
+ throw _ctErr("ct/sct-untrusted", "the SCT the log returned did not verify over the chain it was " +
1834
+ "issued for -- it is not a receipt and was not returned");
1835
+ }
1836
+ /** @internal Returned in the SCT shape the rest of this module reads, which is what `parseSctList` and
1837
+ * `signSct` produce: the algorithm bytes decoded into `signatureAlgorithm`, and `signature` the raw
1838
+ * signature rather than the TLS digitally-signed structure it arrived in. A receipt is only useful if
1839
+ * it can be embedded and re-verified, and `encodeSctList` and `verifySct` both take that shape, so
1840
+ * handing back the wire structure made the receipt unusable by every verb that consumes one. The
1841
+ * verification above ran over the wire structure, which is the form the log signed. */
1842
+ var decodedSig = _parseDigitallySigned(sct.signature, "the SCT");
1843
+ var alg = decodedSig.signatureAlgorithm;
1844
+ return guard.verdict.of({
1845
+ sct: {
1846
+ version: sct.version, logId: sct.logId,
1847
+ /** @internal `logIdHex` as well as the bytes, because the log-list verbs key a log by the hex and
1848
+ * not by the Buffer: without it a receipt this verb had just verified was refused outright by
1849
+ * `verifySctWithLogList` and dropped by `verifySctList` as a policy failure. The comment below
1850
+ * claims this is the shape `parseSctList` produces, and that claim is only true with this field,
1851
+ * which `parseSctList` and `signSct` both carry. */
1852
+ logIdHex: _bufToString(sct.logId, "hex"),
1853
+ timestamp: sct.timestamp, extensions: sct.extensions,
1854
+ /** @internal Both spellings, because the module's own consumers read different ones:
1855
+ * `encodeSctList` takes the flat algorithm bytes and `verifySct` the named object. This is the
1856
+ * shape `parseSctList` produces, which is what makes the two round-trip. */
1857
+ hashAlg: alg.hash, sigAlg: alg.signature,
1858
+ signatureAlgorithm: alg, signature: decodedSig.signature,
1859
+ },
1860
+ verified: true,
1861
+ });
1862
+ }
1863
+
1864
+ /** @internal The TLS `digitally-signed` structure a log returns, read into the shape `verifySct`
1865
+ * already takes: a hash-algorithm byte, a signature-algorithm byte, a uint16 length, then that many
1866
+ * signature bytes. The length is held to the bytes that follow it rather than trusted, and trailing
1867
+ * bytes are refused, so a structure claiming less than it carries cannot hide one. */
1868
+ function _parseDigitallySigned(buf, label) {
1869
+ if (buf.length < 4) {
1870
+ throw _ctErr("ct/bad-signature", label + " is shorter than a digitally-signed header (4 bytes), got " + buf.length);
1871
+ }
1872
+ var hashAlg = buf[0], sigAlg = buf[1];
1873
+ var len = (buf[2] << 8) | buf[3];
1874
+ if (buf.length !== 4 + len) {
1875
+ throw _ctErr("ct/bad-signature", label + " states a " + len + "-byte signature but carries " +
1876
+ (buf.length - 4) + " bytes after its header");
1877
+ }
1878
+ return {
1879
+ signatureAlgorithm: { hash: hashAlg, hashName: HASH_ALGORITHMS[hashAlg] || null,
1880
+ signature: sigAlg, signatureName: SIGNATURE_ALGORITHMS[sigAlg] || null },
1881
+ signature: _subarray(buf, 4),
1882
+ };
1883
+ }
1884
+
1885
+ /** @internal A digitally-signed structure verified over a preimage under a log's SPKI, through the
1886
+ * one signature path this module already uses for an SCT. Resolves a boolean: whether a key signed
1887
+ * something is a verdict, and a malformed structure throws before any verify is attempted. */
1888
+ async function _verifyDigitallySigned(preimage, wireSignature, logKey, label) {
1889
+ var parsed = _parseDigitallySigned(_toBuffer(wireSignature, label + " signature"), label);
1890
+ var spki = _toBuffer(logKey, "the CT log public key (SPKI)");
1891
+ var hashName = CT_HASH[parsed.signatureAlgorithm.hashName];
1892
+ if (!hashName) {
1893
+ throw _ctErr("ct/unsupported-algorithm", "unsupported " + label + " hash algorithm " +
1894
+ guard.text.showValue(parsed.signatureAlgorithm.hashName) + " (RFC 6962 sec. 2.1.4 mandates sha256)");
1895
+ }
1896
+ var alg = _spkiAlg(spki);
1897
+ var imp, ver, sig = parsed.signature;
1898
+ if (parsed.signatureAlgorithm.signatureName === "ecdsa") {
1899
+ if (alg.algOid !== oid.byName("ecPublicKey")) throw _ctErr("ct/bad-input", label + " declares an ECDSA signature but the log key is not an EC key");
1900
+ var ec = CT_EC_CURVE[alg.curveOid];
1901
+ if (!ec) throw _ctErr("ct/unsupported-algorithm", "unsupported CT log EC curve (RFC 6962 sec. 2.1.4 mandates NIST P-256)");
1902
+ imp = { name: "ECDSA", namedCurve: ec.curve };
1903
+ ver = { name: "ECDSA", hash: hashName };
1904
+ sig = validator.sig.ecdsaDerToP1363(sig, ec.curve, CtError, "ct/bad-signature");
1905
+ } else if (parsed.signatureAlgorithm.signatureName === "rsa") {
1906
+ if (alg.algOid !== oid.byName("rsaEncryption")) throw _ctErr("ct/bad-input", label + " declares an RSA signature but the log key is not an RSA key");
1907
+ if (!(alg.rsaBits >= 2048)) throw _ctErr("ct/unsupported-algorithm", "the CT log RSA key is below the RFC 6962 sec. 2.1.4 minimum of 2048 bits");
1908
+ imp = { name: "RSASSA-PKCS1-v1_5", hash: hashName };
1909
+ ver = { name: "RSASSA-PKCS1-v1_5" };
1910
+ } else {
1911
+ throw _ctErr("ct/unsupported-algorithm", "unsupported " + label + " signature algorithm " +
1912
+ guard.text.showValue(parsed.signatureAlgorithm.signatureName) + " (RFC 6962 sec. 2.1.4 supports ecdsa/rsa)");
1913
+ }
1914
+ try {
1915
+ var key = await _subtleImportKey(subtle, "spki", spki, imp, false, ["verify"]);
1916
+ return await _subtleVerify(subtle, ver, key, sig, preimage);
1917
+ } catch (e) {
1918
+ throw _ctErr("ct/verify-error", label + " signature could not be evaluated", e);
1919
+ }
1920
+ }
1921
+
1922
+ /** @internal A tree head a caller passes in, held to carrying the two values a proof is checked
1923
+ * against. It is expected to have come from `getSth`, which returns nothing unverified. */
1924
+ function _assertSth(sth, label) {
1925
+ if (!sth || typeof sth !== "object") {
1926
+ throw _ctErr("ct/bad-input", label + " is required -- a verified tree head from pki.ct.getSth; a proof against nothing proves nothing");
1927
+ }
1928
+ /** @internal COPIED, not viewed. Every caller of this holds the result across a network fetch and then
1929
+ * folds a proof against it, so a view would let the trusted root change between the moment the caller
1930
+ * supplied it and the moment the fold reads it. An empty proof folds a one-leaf tree to its own leaf
1931
+ * hash, so a root overwritten with a leaf hash mid-fetch would make an empty proof verify against a
1932
+ * tree head nobody ever supplied. */
1933
+ var root = _toBuffer(sth.rootHash, label + ".rootHash", _ctErr, "ct/bad-input");
1934
+ if (root.length !== 32) throw _ctErr("ct/bad-input", label + ".rootHash must be exactly 32 bytes, got " + root.length);
1935
+ return { treeSize: guard.range.uint64(sth.treeSize, _ctErr, "ct/bad-input", label + ".treeSize"), rootHash: root };
1936
+ }
1937
+
1938
+ /** @internal An array of base64 members, each decoded and held to a byte length where one is fixed. */
1939
+ /** @internal `maxCount` bounds the ELEMENT COUNT, and every caller passes one. The bound used to be keyed
1940
+ * on `exactLen === 32`, the fixed width a proof node has, so the one caller whose entries have no fixed
1941
+ * width, the accepted-roots response, had no bound at all: a configured log is trusted for its own
1942
+ * signatures and not for its resource use, and a response body well inside the transport's byte cap
1943
+ * carries about a million empty strings, each of which becomes its own Buffer here (CWE-770). Keying a
1944
+ * resource bound on an unrelated shape test is what left one caller out of it. */
1945
+ function _b64Array(obj, name, code, label, exactLen, maxCount) {
1946
+ var arr = obj[name];
1947
+ if (!intrinsic.isArray(arr)) throw _ctErr(code, "the " + label + " field " + name + " is not an array");
1948
+ if (arr.length > maxCount) {
1949
+ throw _ctErr(code, "the " + label + " carries " + arr.length + " entries, exceeding " + maxCount);
1950
+ }
1951
+ void exactLen;
1952
+ var out = [];
1953
+ for (var i = 0; i < arr.length; i++) {
1954
+ if (typeof arr[i] !== "string") throw _ctErr(code, name + "[" + i + "] is not a base64 string");
1955
+ var buf = guard.encoding.base64(arr[i], C.LIMITS.CT_LOG_LIST_MAX_BYTES, _ctErr, code, name + "[" + i + "]");
1956
+ if (exactLen !== undefined && buf.length !== exactLen) {
1957
+ throw _ctErr(code, name + "[" + i + "] is " + buf.length + " bytes, expected " + exactLen);
1958
+ }
1959
+ guard.list.append(out, buf);
1960
+ }
1961
+ return out;
1962
+ }
1963
+
1093
1964
  /**
1094
1965
  * @primitive pki.ct.fetchLogList
1095
1966
  * @signature pki.ct.fetchLogList(opts) -> Promise<{ logs, byLogId, version, timestamp, raw, status, contentType, tls }>
@@ -1134,7 +2005,15 @@ async function fetchLogList(opts) {
1134
2005
  /** @internal Read once, at the door, and carried from here. Asking again would let an option that
1135
2006
  * answers differently on each read hand back something other than the value that was checked. */
1136
2007
  var transport = _assertTransport(opts);
1137
- if (opts.signerKey == null) throw _ctErr("ct/bad-input", "opts.signerKey is required -- the caller-pinned CT log-list distributor SPKI (there is no baked-in key)");
2008
+ /** @internal ONE read, and both the presence check and the copy decide from it. Reading it twice let an
2009
+ * accessor present a non-null value to the check and an attacker's SPKI to the copy, so the list was
2010
+ * verified under a distributor key the caller never pinned.
2011
+ * COPIED at the call, before either fetch. The list is verified under this key after BOTH requests
2012
+ * return, so read from the caller's object at that point it is whatever the buffer holds by then: a
2013
+ * caller reusing it has the list checked against a distributor it never pinned. */
2014
+ var suppliedSignerKey = opts.signerKey;
2015
+ if (suppliedSignerKey == null) throw _ctErr("ct/bad-input", "opts.signerKey is required -- the caller-pinned CT log-list distributor SPKI (there is no baked-in key)");
2016
+ var pinnedSigner = _toBuffer(suppliedSignerKey, "opts.signerKey", _ctErr, "ct/bad-input");
1138
2017
  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)");
1139
2018
  var jsonParsed = _parseHttpsUrl(opts.url, "URL");
1140
2019
  var jsonUrl = jsonParsed.href;
@@ -1154,10 +2033,13 @@ async function fetchLogList(opts) {
1154
2033
  if (lk !== "content-length" && lk !== "transfer-encoding") headers[k] = opts.headers[k];
1155
2034
  });
1156
2035
  var req = { headers: headers, tls: _tlsForFetch(opts), timeout: timeout, maxResponseBytes: maxResponseBytes };
2036
+ /** @internal Read before the request goes out, so the option that decides the check is the one the call
2037
+ * was made with rather than whatever it says once the response is back. */
2038
+ var requireJsonContentType = opts.requireJsonContentType === true;
1157
2039
  var jsonRes = await _fetchBody(transport, jsonUrl, req, "CT log-list JSON");
1158
- if (opts.requireJsonContentType === true && jsonRes.contentType !== "application/json") throw _ctErr("ct/bad-content-type", "the CT log-list JSON GET returned content-type " + JSON.stringify(jsonRes.contentType || null) + " (opts.requireJsonContentType is set)");
2040
+ if (requireJsonContentType && jsonRes.contentType !== "application/json") throw _ctErr("ct/bad-content-type", "the CT log-list JSON GET returned content-type " + JSON.stringify(jsonRes.contentType || null) + " (opts.requireJsonContentType is set)");
1159
2041
  var sigRes = await _fetchBody(transport, sigUrl, req, "CT log-list signature");
1160
- var ok = await verifyLogListSignature(jsonRes.body, sigRes.body, opts.signerKey);
2042
+ var ok = await verifyLogListSignature(jsonRes.body, sigRes.body, pinnedSigner);
1161
2043
  if (ok !== true) throw _ctErr("ct/log-list-untrusted", "the CT log-list detached signature did not verify against the pinned distributor key -- the fetched list is untrusted and was not parsed");
1162
2044
  var parsed = parseLogList(jsonRes.body);
1163
2045
  return guard.verdict.of({
@@ -1178,6 +2060,15 @@ module.exports = {
1178
2060
  x509CertEntry: x509CertEntry,
1179
2061
  verifyLogListSignature: verifyLogListSignature,
1180
2062
  fetchLogList: fetchLogList,
2063
+ sthSignedData: sthSignedData,
2064
+ verifySth: verifySth,
2065
+ getSth: getSth,
2066
+ getProofByHash: getProofByHash,
2067
+ getSthConsistency: getSthConsistency,
2068
+ getEntries: getEntries,
2069
+ getRoots: getRoots,
2070
+ addChain: addChain,
2071
+ addPreChain: addPreChain,
1181
2072
  HASH_ALGORITHMS: HASH_ALGORITHMS,
1182
2073
  SIGNATURE_ALGORITHMS: SIGNATURE_ALGORITHMS,
1183
2074
  };