@blamejs/pki 0.6.13 → 0.6.15

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 (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/README.md +1 -1
  3. package/index.js +1 -1
  4. package/lib/acme.js +59 -815
  5. package/lib/asn1-der.js +7 -245
  6. package/lib/attrcert-sign.js +12 -260
  7. package/lib/attrcert-verify.js +1 -7
  8. package/lib/byte-reader.js +0 -8
  9. package/lib/byte-writer.js +0 -11
  10. package/lib/cbor-det.js +0 -54
  11. package/lib/cmc-build.js +15 -357
  12. package/lib/cmc-verify.js +6 -239
  13. package/lib/cmp-build.js +41 -317
  14. package/lib/cmp-session.js +73 -760
  15. package/lib/cmp-verify.js +32 -488
  16. package/lib/cms-compress.js +3 -32
  17. package/lib/cms-decrypt.js +29 -294
  18. package/lib/cms-encrypt.js +14 -234
  19. package/lib/cms-sign.js +29 -324
  20. package/lib/cms-verify.js +9 -500
  21. package/lib/composite-kem.js +2 -102
  22. package/lib/composite-sig.js +1 -77
  23. package/lib/constants.js +0 -249
  24. package/lib/crl-sign.js +9 -237
  25. package/lib/crl-verify.js +1 -10
  26. package/lib/crmf-sign.js +19 -198
  27. package/lib/crmf-verify.js +1 -12
  28. package/lib/csr-sign.js +3 -34
  29. package/lib/csr-verify.js +1 -12
  30. package/lib/ct.js +28 -253
  31. package/lib/edwards-point.js +10 -44
  32. package/lib/est.js +28 -764
  33. package/lib/framework-error.js +4 -182
  34. package/lib/guard-all.js +1 -58
  35. package/lib/guard-async.js +1 -36
  36. package/lib/guard-bytes.js +6 -650
  37. package/lib/guard-compress.js +5 -102
  38. package/lib/guard-crypto.js +1 -40
  39. package/lib/guard-der.js +2 -93
  40. package/lib/guard-encoding.js +2 -60
  41. package/lib/guard-header.js +1 -36
  42. package/lib/guard-identifier.js +7 -531
  43. package/lib/guard-intrinsic.js +1 -150
  44. package/lib/guard-json.js +10 -73
  45. package/lib/guard-limits.js +1 -77
  46. package/lib/guard-list.js +1 -49
  47. package/lib/guard-name.js +3 -209
  48. package/lib/guard-parsed.js +7 -335
  49. package/lib/guard-range.js +1 -71
  50. package/lib/guard-secret.js +1 -102
  51. package/lib/guard-text.js +1 -64
  52. package/lib/guard-time.js +1 -97
  53. package/lib/hpke.js +3 -105
  54. package/lib/http-digest.js +14 -142
  55. package/lib/http-retry-after.js +1 -47
  56. package/lib/http-transport.js +21 -131
  57. package/lib/inspect.js +30 -284
  58. package/lib/ip-utils.js +6 -47
  59. package/lib/jose.js +7 -142
  60. package/lib/key.js +2 -67
  61. package/lib/lint.js +9 -194
  62. package/lib/merkle.js +1 -27
  63. package/lib/mime.js +9 -82
  64. package/lib/ocsp-request-verify.js +1 -14
  65. package/lib/ocsp-verify.js +1 -82
  66. package/lib/ocsp.js +11 -169
  67. package/lib/oid.js +5 -337
  68. package/lib/path-validate.js +103 -1492
  69. package/lib/pbes2.js +2 -89
  70. package/lib/pkcs12-build.js +31 -379
  71. package/lib/pki-build.js +64 -290
  72. package/lib/rc2.js +2 -18
  73. package/lib/rfc3339.js +4 -27
  74. package/lib/scep.js +59 -233
  75. package/lib/schema-all.js +0 -122
  76. package/lib/schema-attrcert.js +4 -146
  77. package/lib/schema-c509.js +153 -802
  78. package/lib/schema-cmc.js +5 -220
  79. package/lib/schema-cmp.js +2 -223
  80. package/lib/schema-cms.js +13 -427
  81. package/lib/schema-crl.js +5 -68
  82. package/lib/schema-crmf.js +6 -167
  83. package/lib/schema-csr.js +1 -54
  84. package/lib/schema-csrattrs.js +2 -87
  85. package/lib/schema-engine.js +0 -152
  86. package/lib/schema-ocsp.js +0 -117
  87. package/lib/schema-pkcs12.js +2 -93
  88. package/lib/schema-pkcs8.js +3 -35
  89. package/lib/schema-pkix.js +24 -655
  90. package/lib/schema-smime.js +0 -54
  91. package/lib/schema-tsp.js +3 -99
  92. package/lib/schema-x509.js +4 -63
  93. package/lib/shbs.js +11 -95
  94. package/lib/sign-scheme.js +2 -97
  95. package/lib/sigstore.js +9 -169
  96. package/lib/sleep.js +2 -8
  97. package/lib/smime.js +15 -408
  98. package/lib/tls-cert-compress.js +0 -62
  99. package/lib/trust.js +19 -185
  100. package/lib/tsp-sign.js +16 -270
  101. package/lib/validator-all.js +1 -30
  102. package/lib/validator-attcert.js +2 -46
  103. package/lib/validator-cose.js +2 -153
  104. package/lib/validator-keydesc.js +1 -37
  105. package/lib/validator-sig.js +1 -46
  106. package/lib/validator-tls.js +1 -88
  107. package/lib/validator-tpm.js +19 -122
  108. package/lib/verify-seam.js +1 -24
  109. package/lib/webauthn-mds.js +3 -400
  110. package/lib/webauthn.js +12 -833
  111. package/lib/webcrypto.js +5 -485
  112. package/lib/x509-sign.js +4 -108
  113. package/package.json +1 -1
  114. package/sbom.cdx.json +6 -6
package/lib/ct.js CHANGED
@@ -45,13 +45,9 @@ var constants = require("./constants.js");
45
45
  var frameworkError = require("./framework-error.js");
46
46
  var guard = require("./guard-all.js");
47
47
  var intrinsic = require("./guard-intrinsic.js");
48
- // Own-key membership through an operation taken at load. Written out, the question reads two
49
- // replaceable properties to answer one thing, and here it decides whether a log state is one this
50
- // toolkit trusts -- answering true for a name the table never carried trusts an untrusted log.
51
48
  var _hasOwn = intrinsic.hasOwn;
52
49
  var _strSlice = intrinsic.uncurry(String.prototype.slice);
53
50
  var _JSON_SUFFIX = ".json";
54
- // A pathname ending with the literal ".json" suffix (matching /\.json$/), by suffix compare (no regex).
55
51
  function _endsWithJson(s) { return s.length >= _JSON_SUFFIX.length && _strSlice(s, s.length - _JSON_SUFFIX.length) === _JSON_SUFFIX; }
56
52
  var ByteReader = require("./byte-reader.js");
57
53
  var ByteWriter = require("./byte-writer.js");
@@ -66,35 +62,19 @@ var CtError = frameworkError.CtError;
66
62
  var PkiError = frameworkError.PkiError;
67
63
  var C = constants;
68
64
 
69
- // (code, message[, cause]) -> CtError, the factory the composed guards + the shared transport throw
70
- // through so a malformed value keeps the ct/* typed verdict (the optional cause carries the underlying
71
- // transport / evaluation error; existing 2-arg callers pass undefined, so the change preserves behavior).
72
65
  function _ctErr(c, m, cause) { return new CtError(c, m, cause); }
73
66
 
74
- // RFC 5246 sec. 7.4.1.4.1 code points: 1-byte values, never OIDs. Surfaced named; an
75
- // unknown code surfaces as its numeric byte with a null name (never rejected --
76
- // off-profile-pair rejection is a verifier-tier log-conformance concern).
77
67
  var HASH_ALGORITHMS = {
78
68
  0: "none", 1: "md5", 2: "sha1", 3: "sha224", 4: "sha256", 5: "sha384", 6: "sha512",
79
69
  };
80
70
  var SIGNATURE_ALGORITHMS = { 0: "anonymous", 1: "rsa", 2: "dsa", 3: "ecdsa" };
81
71
 
82
- // A minimum viable v1 SCT body: version(1) + LogID(32) + timestamp(8) +
83
- // empty-extensions(2) + digitally-signed{ hash(1) + sig(1) + empty-sig(2) } = 47.
84
72
  var SCT_MIN_BODY = 47;
85
73
  var LOGID_BYTES = 32;
86
- var MAX_SAFE = 9007199254740991n; // 2^53 - 1; above this a Number loses precision
74
+ var MAX_SAFE = 9007199254740991n;
87
75
 
88
- // ---- TlsReader: the RFC 6962 TLS-vector cursor, the shared bounded big-endian
89
- // ByteReader engine primitive bound to the ct/* fault domain. A lying inner length
90
- // can overrun only the current sub-reader's `end`, never the parent buffer, so
91
- // bounds-before-slice is structural (see lib/byte-reader.js).
92
76
  function TlsReader(buf, start, end) { return new ByteReader(buf, start, end, CtError, "ct/truncated"); }
93
77
 
94
- // Peel the RFC 6962 sec. 3.3 inner DER OCTET STRING (the certificate/OCSP layer
95
- // already peeled the outer extnValue OCTET STRING). Rides the strict codec, so
96
- // an indefinite length / constructed OCTET STRING / trailing bytes / single
97
- // wrap all fail closed here; the asn1/* fault attaches as `.cause`.
98
78
  function _peelInner(extValue) {
99
79
  var node;
100
80
  try { node = asn1.decode(extValue); }
@@ -107,16 +87,9 @@ function _toBuffer(v, field) {
107
87
  return guard.bytes.view(v, CtError, "ct/bad-input", field);
108
88
  }
109
89
 
110
- // Parse one SerializedSCT body inside a sub-reader bounded to the element (so a
111
- // lying extensions/signature length overruns the element, never the list). A
112
- // version this parser does not define is preserved OPAQUE, not rejected: RFC 6962
113
- // sec. 3.3 gives every SerializedSCT its own length prefix precisely so a client "can
114
- // still parse old SCTs while skipping over new SCTs whose versions they don't
115
- // understand", so an unknown version yields { unknown, version, rawSct } and the
116
- // v1-specific field decode (and the 47-byte floor) is skipped.
117
90
  function _parseSct(r, sctLen) {
118
91
  var bodyStart = r.pos;
119
- var version = r.u8(); // sctLen >= 1 is guaranteed by the SerializedSCT<1..> check
92
+ var version = r.u8();
120
93
  if (version !== 0) {
121
94
  return { unknown: true, version: version, rawSct: r.buf.subarray(bodyStart, r.end) };
122
95
  }
@@ -217,8 +190,6 @@ function parseSctList(extValue) {
217
190
  throw new CtError("ct/empty-list", "an SCT list must contain at least one SCT (RFC 6962 sec. 3.3)");
218
191
  }
219
192
  var scts = [], unknownScts = [], all = [];
220
- // The shared item counter bounds the TOTAL element count (known + preserved-
221
- // unknown) before it can drive unbounded per-element work (RFC 6962 sec. 3.3).
222
193
  var sctCount = guard.limits.counter(C.LIMITS.SCT_MAX_COUNT, _ctErr, "ct/too-many-scts", "SCT");
223
194
  while (!outer.atEnd()) {
224
195
  if (outer.remaining() < 2) {
@@ -236,8 +207,6 @@ function parseSctList(extValue) {
236
207
  if (one.unknown) { var u = { version: one.version, rawSct: one.rawSct }; unknownScts.push(u); all.push(u); }
237
208
  else { scts.push(one); all.push(one); }
238
209
  }
239
- // `all` preserves the exact wire order across known + unknown entries, so encodeSctList(all)
240
- // reproduces the list byte-identically even when the two kinds were interleaved.
241
210
  return { scts: scts, unknownScts: unknownScts, all: all };
242
211
  }
243
212
 
@@ -292,23 +261,18 @@ function reconstructSignedData(entry, sct) {
292
261
  if (entryType !== 0 && entryType !== 1) {
293
262
  throw new CtError("ct/bad-entry-type", "entryType must be x509_entry(0) or precert_entry(1), got " + entryType + " (RFC 6962 sec. 3.1)");
294
263
  }
295
- // A fully decoded v1 SCT (from parseSctList().scts[]) -- not an opaque
296
- // unknownScts entry, whose body layout is undefined and cannot be signed over.
297
264
  if (!sct || typeof sct.timestamp !== "bigint" || sct.version !== 0) {
298
265
  throw new CtError("ct/bad-input", "reconstructSignedData expects a decoded v1 SCT from parseSctList().scts[]");
299
266
  }
300
- // RFC 6962 3.2: the timestamp is a uint64. The BigInt-preserving range guard
301
- // refuses a value that cannot fill the fixed 8-byte field, typed, rather than
302
- // letting it escape as a raw RangeError from the Buffer write.
303
267
  var tsVal = guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp (RFC 6962 3.2)");
304
268
  var parts = [];
305
- parts.push(Buffer.from([sct.version & 0xff])); // Version -- v1(0)
306
- parts.push(Buffer.from([0])); // SignatureType -- certificate_timestamp(0)
307
- var ts = Buffer.alloc(8); ts.writeBigUInt64BE(tsVal); parts.push(ts); // uint64 timestamp
308
- parts.push(Buffer.from([(entryType >> 8) & 0xff, entryType & 0xff])); // LogEntryType (2 bytes BE)
269
+ parts.push(Buffer.from([sct.version & 0xff]));
270
+ parts.push(Buffer.from([0]));
271
+ var ts = Buffer.alloc(8); ts.writeBigUInt64BE(tsVal); parts.push(ts);
272
+ parts.push(Buffer.from([(entryType >> 8) & 0xff, entryType & 0xff]));
309
273
  if (entryType === 0) {
310
274
  var cert = _toBuffer(entry.leafCert, "leafCert");
311
- parts.push(_u24Bytes(cert.length)); parts.push(cert); // ASN.1Cert<1..2^24-1>
275
+ parts.push(_u24Bytes(cert.length)); parts.push(cert);
312
276
  } else {
313
277
  var ikh = _toBuffer(entry.issuerKeyHash, "issuerKeyHash");
314
278
  if (ikh.length !== 32) {
@@ -316,32 +280,20 @@ function reconstructSignedData(entry, sct) {
316
280
  }
317
281
  var tbs = _toBuffer(entry.tbsCertificate, "tbsCertificate");
318
282
  parts.push(ikh);
319
- parts.push(_u24Bytes(tbs.length)); parts.push(tbs); // PreCert.tbs_certificate<1..2^24-1>
283
+ parts.push(_u24Bytes(tbs.length)); parts.push(tbs);
320
284
  }
321
- var ext = _toBuffer(sct.extensions, "sct.extensions"); // reuse the parsed raw bytes, never re-encode
322
- // RFC 6962 3.2: CtExtensions is opaque<0..2^16-1>. A longer value is
323
- // unrepresentable -- its 2-byte prefix would silently truncate mod 65536,
324
- // an internally inconsistent preimage -- so it is refused, matching the
325
- // range check the u24 certificate lengths get.
285
+ var ext = _toBuffer(sct.extensions, "sct.extensions");
326
286
  if (ext.length > 0xffff) {
327
287
  throw new CtError("ct/bad-extensions", "CtExtensions must be 0..65535 bytes, got " + ext.length + " (RFC 6962 3.2)");
328
288
  }
329
- parts.push(Buffer.from([(ext.length >> 8) & 0xff, ext.length & 0xff])); parts.push(ext); // CtExtensions
289
+ parts.push(Buffer.from([(ext.length >> 8) & 0xff, ext.length & 0xff])); parts.push(ext);
330
290
  return Buffer.concat(parts);
331
291
  }
332
292
 
333
- // RFC 6962 sec. 2.1.4 constrains an SCT to SHA-256 with either ECDSA (NIST P-256) or RSA;
334
- // the verifier enforces exactly that rather than accepting an off-profile hash / curve a
335
- // conformant log never uses. The SCT hash name -> the WebCrypto digest.
336
293
  var CT_HASH = { sha256: "SHA-256" };
337
- // The one EC named curve an SCT log key may use (P-256), keyed by the curve OID its SPKI
338
- // carries -> the WebCrypto curve name + the r/s coordinate width.
339
294
  var CT_EC_CURVE = {};
340
295
  CT_EC_CURVE[oid.byName("prime256v1")] = { curve: "P-256", coordLen: 32 };
341
296
 
342
- // Read the SubjectPublicKeyInfo algorithm OID (+ the EC curve OID) off a log key SPKI so
343
- // the verifier picks the WebCrypto import descriptor from the KEY, not just the SCT's
344
- // self-declared signature type. Fail-closed: a non-SPKI shape throws a typed ct/bad-input.
345
297
  function _spkiAlg(spki) {
346
298
  var node;
347
299
  try { node = asn1.decode(spki); } catch (e) { throw new CtError("ct/bad-input", "the CT log public key is not a well-formed SubjectPublicKeyInfo", e); }
@@ -355,19 +307,13 @@ function _spkiAlg(spki) {
355
307
  try { out.curveOid = asn1.read.oid(algId.children[1]); }
356
308
  catch (e2) { throw new CtError("ct/bad-input", "the EC log key SPKI curve parameters are not a named-curve OID", e2); }
357
309
  } else if (out.algOid === oid.byName("rsaEncryption")) {
358
- // RFC 6962 sec. 2.1.4 requires an RSA log key of at least 2048 bits -- size it from the
359
- // RSAPublicKey modulus in the subjectPublicKey BIT STRING.
360
310
  var mod, exp;
361
311
  try {
362
312
  var rsaSeq = asn1.decode(asn1.read.bitString(node.children[1]).bytes);
363
313
  mod = asn1.read.integer(rsaSeq.children[0]);
364
314
  exp = asn1.read.integer(rsaSeq.children[1]);
365
315
  } catch (e3) { throw new CtError("ct/bad-input", "the RSA log key SPKI is not a well-formed RSAPublicKey", e3); }
366
- // A non-positive modulus is malformed -- fail closed rather than size its absolute value.
367
316
  if (mod <= 0n) throw new CtError("ct/bad-input", "the RSA log key modulus is not a positive integer");
368
- // A small or even public exponent makes RSASSA-PKCS1-v1_5 verification forgeable (with
369
- // e=1 the signature is simply the encoded DigestInfo). Require an odd exponent >= 3 --
370
- // critical when the log key is taken from an untrusted log list rather than a pinned key.
371
317
  if (exp < 3n || (exp & 1n) === 0n) throw new CtError("ct/bad-input", "the RSA log key public exponent must be an odd integer >= 3");
372
318
  out.rsaBits = mod.toString(2).length;
373
319
  }
@@ -412,12 +358,8 @@ function _spkiAlg(spki) {
412
358
  * var ok = await pki.ct.verifySct({ entryType: 0, leafCert: certDer }, sct, logKey);
413
359
  */
414
360
  async function verifySct(entry, sct, logPublicKey) {
415
- // reconstructSignedData validates the entry + SCT and rebuilds the exact preimage.
416
361
  var message = reconstructSignedData(entry, sct);
417
362
  var spki = _toBuffer(logPublicKey, "the CT log public key (SPKI)");
418
- // RFC 6962 sec. 3.2: an SCT names its log by LogID = SHA-256(log SPKI). When the SCT
419
- // carries a logId, it MUST match the key it is verified against, so an SCT is never
420
- // accepted against a different log's key (a key-confusion the bare signature check misses).
421
363
  if (sct.logId != null) {
422
364
  var keyId = Buffer.from(await subtle.digest("SHA-256", spki));
423
365
  if (!keyId.equals(_toBuffer(sct.logId, "sct.logId"))) {
@@ -435,9 +377,6 @@ async function verifySct(entry, sct, logPublicKey) {
435
377
  if (!ec) throw new CtError("ct/unsupported-algorithm", "unsupported SCT log EC curve (RFC 6962 sec. 2.1.4 mandates NIST P-256)");
436
378
  imp = { name: "ECDSA", namedCurve: ec.curve };
437
379
  ver = { name: "ECDSA", hash: hashName };
438
- // The SCT signature is a DER ECDSA-Sig-Value; route it through the ORDER-AWARE conformance gate
439
- // (primitive, minimal, and r,s in [1, n-1] per FIPS 186-5 sec. 6.4.2 -- rejecting an out-of-range
440
- // r/s >= the curve order n, not only the r=s=0 shape) before converting to raw r||s.
441
380
  sig = validator.sig.ecdsaDerToP1363(sig, ec.curve, CtError, "ct/bad-signature");
442
381
  } else if (sigInfo.signatureName === "rsa") {
443
382
  if (alg.algOid !== oid.byName("rsaEncryption")) throw new CtError("ct/bad-input", "the SCT declares an RSA signature but the log key is not an RSA key");
@@ -447,8 +386,6 @@ async function verifySct(entry, sct, logPublicKey) {
447
386
  } else {
448
387
  throw new CtError("ct/unsupported-algorithm", "unsupported SCT signature algorithm " + JSON.stringify(sigInfo.signatureName) + " (RFC 6962 sec. 2.1.4 supports ecdsa/rsa)");
449
388
  }
450
- // A wrong signature resolves false from subtle.verify (a verdict); a structural failure
451
- // -- an unimportable key, an algorithm/key mismatch -- is re-thrown fail-closed.
452
389
  try {
453
390
  var key = await subtle.importKey("spki", spki, imp, false, ["verify"]);
454
391
  return await subtle.verify(ver, key, sig, message);
@@ -457,24 +394,14 @@ async function verifySct(entry, sct, logPublicKey) {
457
394
  }
458
395
  }
459
396
 
460
- // The TLS-vector WRITER bound to the ct/* fault domain -- the encode twin of TlsReader,
461
- // over the shared ByteWriter engine primitive (see lib/byte-writer.js).
462
397
  function TlsWriter() { return new ByteWriter(CtError, "ct/bad-input"); }
463
398
 
464
- // Encode one SerializedSCT body. A v1 SCT is rebuilt from its fields in the exact _parseSct
465
- // order (so it round-trips byte-identically); an opaque non-v1 entry re-emits its rawSct
466
- // verbatim (RFC 6962 sec. 3.3 forward-compat symmetry -- encode preserves what parse preserved).
467
399
  function _encodeSctBody(sct) {
468
400
  if (!sct || typeof sct !== "object") throw new CtError("ct/bad-input", "each SCT must be an object");
469
401
  if (sct.version !== 0) {
470
- // The SerializedSCT version is a single byte (RFC 6962 sec. 3.2), so a declared version must be a
471
- // 0..255 integer -- reject a non-byte value rather than masking it (a masked 263 would spoof v7).
472
402
  if (typeof sct.version !== "number" || !Number.isInteger(sct.version) || sct.version < 0 || sct.version > 255) {
473
403
  throw new CtError("ct/bad-input", "an SCT version must be a byte in 0..255 (RFC 6962 sec. 3.2)");
474
404
  }
475
- // An opaque entry re-emits its rawSct verbatim, but the SerializedSCT's on-wire version is
476
- // rawSct[0] -- refuse a rawSct whose leading byte disagrees with the declared version, so encode
477
- // never emits a list whose framing our own parser would reject or re-classify (RFC 6962 sec. 3.3).
478
405
  var raw = _toBuffer(sct.rawSct, "sct.rawSct");
479
406
  if (raw.length < 1 || raw[0] !== sct.version) {
480
407
  throw new CtError("ct/bad-input", "an opaque SCT's rawSct[0] must equal its declared version (RFC 6962 sec. 3.3)");
@@ -482,15 +409,15 @@ function _encodeSctBody(sct) {
482
409
  return raw;
483
410
  }
484
411
  var w = new TlsWriter();
485
- w.u8(0, "ct/bad-input"); // Version -- v1(0)
412
+ w.u8(0, "ct/bad-input");
486
413
  var logId = _toBuffer(sct.logId, "sct.logId");
487
414
  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)");
488
415
  w.bytes(logId);
489
- w.u64(guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp"), "ct/bad-input"); // uint64 timestamp
490
- w.vector(2, 0, 0xffff, _toBuffer(sct.extensions, "sct.extensions"), "ct/bad-input"); // CtExtensions<0..2^16-1>
491
- w.u8(sct.hashAlg, "ct/bad-input"); // digitally-signed hash
492
- w.u8(sct.sigAlg, "ct/bad-input"); // digitally-signed signature
493
- w.vector(2, 0, 0xffff, _toBuffer(sct.signature, "sct.signature"), "ct/bad-input"); // signature<0..2^16-1>
416
+ w.u64(guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp"), "ct/bad-input");
417
+ w.vector(2, 0, 0xffff, _toBuffer(sct.extensions, "sct.extensions"), "ct/bad-input");
418
+ w.u8(sct.hashAlg, "ct/bad-input");
419
+ w.u8(sct.sigAlg, "ct/bad-input");
420
+ w.vector(2, 0, 0xffff, _toBuffer(sct.signature, "sct.signature"), "ct/bad-input");
494
421
  return w.build();
495
422
  }
496
423
 
@@ -536,26 +463,18 @@ function encodeSctList(scts) {
536
463
  for (var i = 0; i < scts.length; i++) {
537
464
  sctCount.tick();
538
465
  var ew = new TlsWriter();
539
- ew.vector(2, 1, 0xffff, _encodeSctBody(scts[i]), "ct/bad-input"); // SerializedSCT<1..2^16-1>
466
+ ew.vector(2, 1, 0xffff, _encodeSctBody(scts[i]), "ct/bad-input");
540
467
  var el = ew.build();
541
468
  total += el.length;
542
- // Enforce the sct_list<1..2^16-1> body cap INCREMENTALLY, so an over-long list fails at the first
543
- // element that crosses the bound instead of accumulating the whole (potentially large) set first.
544
469
  if (total > 0xffff) throw new CtError("ct/too-large", "the SCT list body exceeds the 65535-byte maximum (RFC 6962 sec. 3.3)");
545
470
  elements.push(el);
546
471
  }
547
472
  var lw = new TlsWriter();
548
- lw.vector(2, 1, 0xffff, Buffer.concat(elements, total), "ct/too-large"); // sct_list<1..2^16-1>
549
- return asn1.build.octetString(lw.build()); // RFC 6962 sec. 3.3 DER OCTET STRING wrap
473
+ lw.vector(2, 1, 0xffff, Buffer.concat(elements, total), "ct/too-large");
474
+ return asn1.build.octetString(lw.build());
550
475
  }
551
476
 
552
- // A CT log private key -> its PKCS#8 DER (for WebCrypto sign) + the SPKI DER (for the LogID +
553
- // the sec. 2.1.4 algorithm profile). Accepts a PKCS#8 DER Buffer, a PEM string, a node
554
- // KeyObject, or a { key, format, type } input.
555
477
  function _logKeyMaterial(logKey) {
556
- // Every key operation runs inside the guard: a duck-typed impostor (an object carrying an
557
- // asymmetricKeyType but not a real KeyObject) fails at export/createPublicKey and surfaces the
558
- // typed ct/bad-input, never a raw TypeError. The private-key check keeps its own CtError.
559
478
  try {
560
479
  var keyObj;
561
480
  if (logKey && typeof logKey === "object" && logKey.asymmetricKeyType) keyObj = logKey;
@@ -628,7 +547,6 @@ async function signSct(entry, logKey, opts) {
628
547
  if (opts.logId != null && !_toBuffer(opts.logId, "opts.logId").equals(logId)) {
629
548
  throw new CtError("ct/bad-input", "opts.logId does not match SHA-256 of the log key (RFC 6962 sec. 3.4)");
630
549
  }
631
- // Reuse the ONE preimage builder the verifier uses -- sign and verify cannot diverge.
632
550
  var preimage = reconstructSignedData(entry, { version: 0, timestamp: timestamp, extensions: extensions });
633
551
  var priv = await subtle.importKey("pkcs8", mat.pkcs8, imp, false, ["sign"]);
634
552
  var sigRaw = Buffer.from(await subtle.sign(sign, priv, preimage));
@@ -639,23 +557,14 @@ async function signSct(entry, logKey, opts) {
639
557
  timestamp: timestamp,
640
558
  extensions: extensions,
641
559
  hashAlg: hashAlg, sigAlg: sigAlg,
642
- // The `|| null` fallbacks mirror parseSctList's shape; unreachable here since signSct only ever
643
- // emits the in-map sha256(4) + ecdsa(3)/rsa(1) code points (coverage residual).
644
560
  signatureAlgorithm: { hash: hashAlg, hashName: HASH_ALGORITHMS[hashAlg] || null, signature: sigAlg, signatureName: SIGNATURE_ALGORITHMS[sigAlg] || null },
645
561
  signature: signature,
646
562
  };
647
563
  }
648
564
 
649
- // ---- CT log-list trust surface (RFC 6962 sec. 3.2 + the CT log-list v3 JSON schema) --------------
650
565
 
651
- // The recognized CT log states (the schema `state` oneOf) mapped to a trust decision (the deployed
652
- // Chrome/Apple CT policy): usable/qualified/readonly are trusted; retired is CONDITIONALLY trusted
653
- // (only for an SCT timestamped before the retirement instant, checked at verify); pending/rejected
654
- // are not trusted.
655
566
  var LOG_STATE_TRUST = { pending: "no", qualified: "yes", usable: "yes", readonly: "yes", retired: "conditional", rejected: "no" };
656
567
 
657
- // Decode a log's `state` oneOf: EXACTLY ONE recognized member, whose timestamp is an RFC 3339 instant.
658
- // Zero, multiple, or an unrecognized member -> ct/bad-state (fail-closed, never a silent default-trust).
659
568
  function _parseLogState(state) {
660
569
  if (state == null || typeof state !== "object") throw _ctErr("ct/bad-state", "a CT log entry is missing its state");
661
570
  var keys = Object.keys(state);
@@ -669,7 +578,6 @@ function _parseLogState(state) {
669
578
  return { name: name, since: since, trusted: trust === "yes", conditional: trust === "conditional" };
670
579
  }
671
580
 
672
- // Decode a log's `temporal_interval` (both bounds RFC 3339, start strictly before end) or null.
673
581
  function _parseTemporalInterval(ti) {
674
582
  if (ti == null) return null;
675
583
  if (typeof ti !== "object") throw _ctErr("ct/bad-log-list", "a CT log temporal_interval must be an object");
@@ -680,17 +588,13 @@ function _parseTemporalInterval(ti) {
680
588
  return { startInclusive: start, endExclusive: end };
681
589
  }
682
590
 
683
- // One log-list entry -> a constraint-carrying trusted-log record. The load-bearing M-BIND step: the
684
- // log-id is RECOMPUTED as SHA-256(SPKI) and MUST equal the stated log_id (RFC 6962 sec. 3.2), so the
685
- // trusted set is keyed by the authoritative binding, never the document's self-assertion -- a swapped
686
- // key or a flipped id is refused. The key is also validated as a well-formed, on-profile SPKI at ingest.
687
591
  function _parseLog(log, operatorName) {
688
592
  if (log == null || typeof log !== "object") throw _ctErr("ct/bad-log-list", "a CT log entry is not an object");
689
593
  if (typeof log.key !== "string" || typeof log.log_id !== "string") throw _ctErr("ct/bad-log-list", "a CT log entry is missing its key or log_id");
690
594
  var spki = guard.encoding.base64(log.key, C.LIMITS.CT_LOG_LIST_MAX_BYTES, _ctErr, "ct/bad-log-list", "the CT log key");
691
595
  var statedId = guard.encoding.base64(log.log_id, 64, _ctErr, "ct/bad-log-list", "the CT log id");
692
596
  if (statedId.length !== 32) throw _ctErr("ct/bad-log-list", "a CT log_id must be 32 bytes (SHA-256), got " + statedId.length);
693
- _spkiAlg(spki); // fail-fast: a non-SPKI / malformed key (or a forgeable RSA e < 3) throws ct/bad-input; the full EC-curve / RSA-size profile is enforced by verifySct at verify time
597
+ _spkiAlg(spki);
694
598
  var logId = nodeCrypto.createHash("sha256").update(spki).digest();
695
599
  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)");
696
600
  return {
@@ -699,16 +603,12 @@ function _parseLog(log, operatorName) {
699
603
  url: typeof log.url === "string" ? log.url : (typeof log.submission_url === "string" ? log.submission_url : null),
700
604
  mmd: typeof log.mmd === "number" ? log.mmd : null,
701
605
  operator: operatorName, state: _parseLogState(log.state), temporalInterval: _parseTemporalInterval(log.temporal_interval),
702
- trusted: false, // set below from the state (usable/qualified/readonly), so a consumer reads a plain bool
606
+ trusted: false,
703
607
  };
704
608
  }
705
609
 
706
- // Two entries for one recomputed log-id must be byte-identical (collapse) or the list is inconsistent.
707
610
  function _sameTemporal(a, b) {
708
611
  if (a == null && b == null) return true;
709
- // Coverage residual: the || guard is exercised across null/interval, interval/null, and both-interval
710
- // inputs; the remaining c8 sub-arm is a short-circuit path (when `a == null` is true, `b == null` is
711
- // never evaluated) with no additional reachable input.
712
612
  if (a == null || b == null) return false;
713
613
  return guard.time.instantOf(a.startInclusive) === guard.time.instantOf(b.startInclusive) &&
714
614
  guard.time.instantOf(a.endExclusive) === guard.time.instantOf(b.endExclusive);
@@ -772,40 +672,31 @@ function parseLogList(json, opts) {
772
672
  if (!Array.isArray(arr)) throw _ctErr("ct/bad-log-list", "a CT log-list operator's logs / tiled_logs must be an array");
773
673
  for (var j = 0; j < arr.length; j++) {
774
674
  var rec = _parseLog(arr[j], op.name);
775
- rec.trusted = rec.state.trusted; // usable/qualified/readonly; retired stays false (conditional at verify)
675
+ rec.trusted = rec.state.trusted;
776
676
  var prev = byLogId[rec.logIdHex];
777
677
  if (prev) {
778
678
  if (!_logsAgree(prev, rec)) throw _ctErr("ct/duplicate-log", "two CT log entries share log-id " + rec.logIdHex + " but disagree");
779
- continue; // a byte-identical duplicate collapses to the one already recorded
679
+ continue;
780
680
  }
781
681
  byLogId[rec.logIdHex] = rec;
782
682
  logs.push(rec);
783
683
  }
784
684
  }
785
685
  }
786
- // Surface the document's own version + timestamp additively (the staleness surface the live-fetch client
787
- // returns for the caller to police freshness). Read LENIENTLY -- a missing / non-string / unparseable
788
- // scalar yields null, never a new throw, so every existing caller of the { logs, byLogId } shape is
789
- // behavior-preserving.
790
686
  var version = typeof doc.version === "string" ? doc.version : null;
791
687
  var timestamp = (typeof doc.log_list_timestamp === "string" && rfc3339.isValid(doc.log_list_timestamp))
792
688
  ? rfc3339.parse(doc.log_list_timestamp, _ctErr, "ct/bad-date", "log_list_timestamp") : null;
793
689
  return { logs: logs, byLogId: byLogId, version: version, timestamp: timestamp };
794
690
  }
795
691
 
796
- // The covered certificate's notAfter for the temporal gate: an explicit opts.certNotAfter, else derived
797
- // from a leafCert (entryType 0) via the shipped x509 parser, else null (a precert TBS can't be parsed).
798
692
  function _resolveNotAfter(entry, opts) {
799
693
  if (guard.time.isDate(opts.certNotAfter)) return opts.certNotAfter;
800
694
  if (entry && entry.entryType === 0 && entry.leafCert != null) {
801
- // Inline require (circular-load): ct.js is loaded before schema-x509 finishes initializing in the
802
- // index chain (schema-x509 -> schema-pkix is mid-init when ct.js is first required), so a top-level
803
- // require would see a half-built pkix. Deferred to first use, when every module is initialized.
804
695
  var x509 = require("./schema-x509.js"); // allow:inline-require -- circular load with schema-x509 -> schema-pkix (see note above)
805
696
  try {
806
697
  return x509.parse(_toBuffer(entry.leafCert, "entry.leafCert")).validity.notAfter;
807
698
  } catch (_e) {
808
- return null; // a malformed leafCert -> no resolvable notAfter -> fail-closed ct/temporal-interval at the caller
699
+ return null;
809
700
  }
810
701
  }
811
702
  return null;
@@ -850,33 +741,18 @@ function _resolveNotAfter(entry, opts) {
850
741
  * state: { usable: { timestamp: "2026-01-01T00:00:00Z" } } }] }] }), "utf8"));
851
742
  * var res = await pki.ct.verifySctWithLogList(sctEntry, embeddedSct, logList); // { valid, logId, logIdHex, operator, logState, timestamp }
852
743
  */
853
- // The pure, NON-THROWING log resolution + state + temporal classifier shared by verifySctWithLogList
854
- // (which throws the mapped code on a non-ok status) and verifySctList (which records the code into a
855
- // result row and continues). Returns { status, log?, code?, message? } where status is one of
856
- // ok | bad-input | not-found | untrusted | temporal. A mis-shaped sct.timestamp (in the retired-log
857
- // gate) or a malformed opts.certNotAfter (in the temporal gate, via within) still THROWS -- those are
858
- // caller mis-shapes, not per-SCT trust outcomes (the three-tier rule: bad input throws, a per-SCT
859
- // verdict is recorded). The state and temporal gate semantics are verbatim from the original
860
- // verifySctWithLogList body, so its shipped RED vectors are the behavior guard for this refactor.
861
744
  function _classifyLog(sct, logList, entry, opts) {
862
745
  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" };
863
746
  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" };
864
747
  var log = logList.byLogId[sct.logIdHex];
865
748
  if (log == null) return { status: "not-found", code: "ct/log-not-found", message: "no trusted CT log matches the SCT's logId " + sct.logIdHex };
866
- // State gate (fail-closed): trusted proceed; retired only before its retirement; else untrusted.
867
749
  if (!log.state.trusted) {
868
750
  if (!log.state.conditional) return { status: "untrusted", log: log, code: "ct/log-untrusted", message: "the CT log state '" + log.state.name + "' is not trusted" };
869
751
  var ts = guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp");
870
752
  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() + ")" };
871
753
  }
872
- // Temporal gate (fail-closed): the covered cert's notAfter must be in [start_inclusive, end_exclusive).
873
754
  if (log.temporalInterval) {
874
755
  var notAfter = _resolveNotAfter(entry, opts);
875
- // An unresolvable notAfter is a failure for this windowed SCT, never a silent skip. within() then
876
- // validates notAfter fail-closed before comparing: an Invalid Date is instanceof Date yet NaN, and
877
- // NaN < x / NaN >= x are both false, so an unvalidated notAfter would BYPASS the containment. A
878
- // malformed opts.certNotAfter throws inside within (a caller mis-shape); an out-of-window one is
879
- // this recorded temporal status.
880
756
  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)" };
881
757
  if (!guard.time.within(notAfter, log.temporalInterval.startInclusive, log.temporalInterval.endExclusive, _ctErr, "ct/temporal-interval", "the covered certificate notAfter (pass a valid opts.certNotAfter)")) {
882
758
  return { status: "temporal", log: log, code: "ct/temporal-interval", message: "the covered certificate's notAfter is outside the CT log's temporal_interval" };
@@ -889,10 +765,7 @@ async function verifySctWithLogList(entry, sct, logList, opts) {
889
765
  opts = opts || {};
890
766
  var c = _classifyLog(sct, logList, entry, opts);
891
767
  if (c.status !== "ok") throw _ctErr(c.code, c.message);
892
- // The crypto verdict alone hid WHICH trusted log accepted the SCT and when. Surface the resolved log
893
- // record so a caller can record the operator / log state / timestamp for a policy decision instead of
894
- // re-deriving them, rather than reading a bare boolean that drops the whole record it just resolved.
895
- var valid = await verifySct(entry, sct, c.log.key); // re-checks the logId binding
768
+ var valid = await verifySct(entry, sct, c.log.key);
896
769
  return {
897
770
  valid: valid,
898
771
  logId: c.log.logId,
@@ -903,10 +776,8 @@ async function verifySctWithLogList(entry, sct, logList, opts) {
903
776
  };
904
777
  }
905
778
 
906
- // Options verifySctList reads. All optional; the defaults are the RFC 6962 sec. 3.3 floor (>= 1).
907
779
  var _VERIFY_SCT_LIST_OPTS = { minScts: 1, minOperators: 1, certNotAfter: 1, at: 1 };
908
780
 
909
- // A caller-supplied policy threshold: a positive integer, or null when omitted (defaulted by the caller).
910
781
  function _policyCount(v, name) {
911
782
  if (v == null) return null;
912
783
  if (typeof v !== "number" || !intrinsic.isInteger(v) || v < 1) throw _ctErr("ct/bad-input", name + " must be a positive integer");
@@ -965,32 +836,18 @@ async function verifySctList(entry, list, logList, opts) {
965
836
  var minOperators = _policyCount(opts.minOperators, "opts.minOperators"); if (minOperators == null) minOperators = 1;
966
837
  if (logList == null || typeof logList !== "object" || logList.byLogId == null) throw _ctErr("ct/bad-input", "logList must be a pki.ct.parseLogList result");
967
838
  if (entry == null || (entry.entryType !== 0 && entry.entryType !== 1)) throw _ctErr("ct/bad-entry-type", "entryType must be x509_entry(0) or precert_entry(1) (RFC 6962 sec. 3.1)");
968
- // The validation time, when supplied, arms the RFC 6962 sec. 5.2 rule that a future-dated SCT is
969
- // rejected: a log cannot have issued an SCT after the moment we verify it, so its timestamp must not
970
- // be later than `at`. Held as a millisecond Number and compared against the SCT's BigInt uint64
971
- // timestamp (a BigInt-vs-Number relational comparison is exact).
972
839
  var atMs = null;
973
840
  if (opts.at != null) { guard.time.assertValid(opts.at, _ctErr, "ct/bad-input", "opts.at (the validation time)"); atMs = guard.time.instantOf(opts.at); }
974
- // A present certNotAfter must be a valid Date: a non-Date is a caller mis-shape that throws, never a
975
- // value silently ignored (entryType 0 would fall back to the certificate's own notAfter) or demoted to
976
- // a per-SCT temporal failure (entryType 1) -- the malformed-options contract, matching opts.at.
977
841
  if (opts.certNotAfter != null) guard.time.assertValid(opts.certNotAfter, _ctErr, "ct/bad-input", "opts.certNotAfter (the covered certificate's notAfter)");
978
- // Validate the shared entry's required fields up front: a mis-shaped entry is a caller error that
979
- // throws (not a per-SCT row or a verdict rendered without examining it), even when the SCT list is
980
- // empty. The same fields reconstructSignedData reads for the preimage.
981
842
  if (entry.entryType === 0) { _toBuffer(entry.leafCert, "entry.leafCert"); }
982
843
  else {
983
844
  if (_toBuffer(entry.issuerKeyHash, "entry.issuerKeyHash").length !== 32) throw _ctErr("ct/bad-issuer-key-hash", "entry.issuerKeyHash must be exactly 32 bytes (SHA-256 of the issuer SPKI, RFC 6962 sec. 3.2)");
984
845
  _toBuffer(entry.tbsCertificate, "entry.tbsCertificate");
985
846
  }
986
- // Normalize the list into the known v1 SCTs plus the count of forward-compat unknown-version entries
987
- // (M7 -- their body layout is undefined, so they are unverifiable, never a valid SCT, never a throw).
988
847
  var scts, unknownScts;
989
848
  if (list != null && intrinsic.isArray(list.scts)) { scts = list.scts; unknownScts = intrinsic.isArray(list.unknownScts) ? list.unknownScts.length : 0; }
990
849
  else if (intrinsic.isArray(list)) { scts = list; unknownScts = 0; }
991
850
  else throw _ctErr("ct/bad-input", "list must be a pki.ct.parseSctList result, its .scts array, or an array of decoded v1 SCTs");
992
- // Bound the per-SCT verification work a caller-supplied array can demand to the same cap parseSctList
993
- // enforces on wire input, so a bare oversized array cannot force an unbounded run of signature checks.
994
851
  if (scts.length > C.LIMITS.SCT_MAX_COUNT) throw _ctErr("ct/too-many-scts", "the SCT list has " + scts.length + " entries, exceeding the SCT_MAX_COUNT cap of " + C.LIMITS.SCT_MAX_COUNT + " (RFC 6962 sec. 3.3)");
995
852
 
996
853
  var results = [], validScts = 0, operators = [], seenOps = intrinsic.create(null), seenLogs = intrinsic.create(null);
@@ -999,9 +856,6 @@ async function verifySctList(entry, list, logList, opts) {
999
856
  var row = { logIdHex: (sct && typeof sct.logIdHex === "string") ? sct.logIdHex : null, valid: false,
1000
857
  operator: null, logState: null,
1001
858
  timestamp: (sct && typeof sct.timestamp === "bigint") ? sct.timestamp : null, code: undefined, reason: undefined };
1002
- // Classification can THROW for a malformed per-SCT field (a bad timestamp on a retired-log match):
1003
- // that is a recorded per-SCT outcome, never a thrown aggregate abort, so it matches the usable-log
1004
- // malformed-SCT path below and one bad SCT cannot sink the verdict or a valid sibling.
1005
859
  var c;
1006
860
  try { c = _classifyLog(sct, logList, entry, opts); }
1007
861
  catch (e) {
@@ -1010,29 +864,17 @@ async function verifySctList(entry, list, logList, opts) {
1010
864
  }
1011
865
  if (c.log) { row.operator = c.log.operator; row.logState = c.log.state.name; }
1012
866
  if (c.status !== "ok") { row.code = c.code; row.reason = c.message; intrinsic.push(results, row); continue; }
1013
- // A trust exclusion is a recorded code above; the crypto check below is false-is-a-verdict. But a
1014
- // structurally-defective SCT from a trusted log (a malformed signature encoding, an unsupported hash
1015
- // or curve) makes verifySct THROW a typed CtError -- for the aggregate that is a recorded per-SCT
1016
- // outcome (M10, the verdict-collector tier), never a thrown call that would sink the verdict or a
1017
- // valid sibling. A non-PkiError (an unexpected bug) is re-thrown, never swallowed into a verdict.
1018
867
  var ok;
1019
868
  try { ok = await verifySct(entry, sct, c.log.key); }
1020
869
  catch (e) {
1021
870
  if (!(e && e.isPkiError)) throw e;
1022
871
  row.code = e.code; row.reason = e.message; intrinsic.push(results, row); continue;
1023
872
  }
1024
- // RFC 6962 sec. 5.2: reject a future-dated SCT even when its signature verifies -- a log cannot have
1025
- // issued it after the validation time, so it is recorded as a rejection and never counted. An SCT
1026
- // dated after the certificate's OWN notAfter is deliberately NOT rejected: RFC 6962 sec. 5.1 permits
1027
- // a log to accept an already-expired certificate, so such an SCT is legitimate.
1028
873
  if (ok === true && atMs != null && sct.timestamp > atMs) {
1029
874
  row.code = "ct/future-timestamp"; row.reason = "the SCT timestamp is later than the validation time (RFC 6962 sec. 5.2)";
1030
875
  intrinsic.push(results, row); continue;
1031
876
  }
1032
877
  row.valid = ok === true;
1033
- // Count each distinct trusted log once: a duplicated SCT, or a second SCT from the same log, is one
1034
- // attestation, so a repeat cannot inflate validScts past a minScts policy (the count is over the set
1035
- // of logs, not raw SCT copies). The duplicate row is still recorded for diagnostics.
1036
878
  if (row.valid && !seenLogs[sct.logIdHex]) {
1037
879
  seenLogs[sct.logIdHex] = true;
1038
880
  validScts++;
@@ -1043,7 +885,7 @@ async function verifySctList(entry, list, logList, opts) {
1043
885
  var operatorCount = operators.length;
1044
886
  var policyOk = validScts >= minScts && operatorCount >= minOperators;
1045
887
  return {
1046
- valid: policyOk, // canonical toolkit-wide verdict alias, beside this verb's `policyOk`
888
+ valid: policyOk,
1047
889
  policyOk: policyOk, totalScts: scts.length + unknownScts, validScts: validScts, unknownScts: unknownScts,
1048
890
  operatorCount: operatorCount, operators: operators, required: { minScts: minScts, minOperators: minOperators },
1049
891
  results: results,
@@ -1051,12 +893,7 @@ async function verifySctList(entry, list, logList, opts) {
1051
893
  };
1052
894
  }
1053
895
 
1054
- // A cert / issuer argument as a parsed x509: a DER Buffer, a PEM string, or an unmodified
1055
- // pki.schema.x509.parse result. Routed through guard.parsed (provenance-based, getter-free) so a
1056
- // caller object cannot spoof "already parsed" through a lying accessor; a malformed one is a typed
1057
- // ct/bad-cert-entry, never a silently-skipped input.
1058
896
  function _asParsedCert(v, field) {
1059
- // Inline require (circular load with schema-x509 -> schema-pkix), matching _resolveNotAfter above.
1060
897
  var x509 = require("./schema-x509.js"); // allow:inline-require -- circular load (see _resolveNotAfter)
1061
898
  return guard.parsed.acceptDerived(v, "certificate", x509.parse, _ctErr, "ct/bad-cert-entry", field);
1062
899
  }
@@ -1097,14 +934,8 @@ function _asParsedCert(v, field) {
1097
934
  * var verdict = await pki.ct.verifySctList(entry, pki.ct.parseSctList(sctExtValue), logList,
1098
935
  * { certNotAfter: pki.schema.x509.parse(leafCertDer).validity.notAfter });
1099
936
  */
1100
- // The issuer contributes only its SubjectPublicKeyInfo (issuerKeyHash = SHA-256 of it), so it accepts
1101
- // anything x509CertEntry's cert does PLUS a bare { subjectPublicKeyInfo: { bytes } } -- a trust anchor
1102
- // carries its key without a full certificate, and requiring a whole cert there would refuse a leaf
1103
- // validated directly under an anchor.
1104
937
  function _issuerSpki(v, field) {
1105
938
  if (v != null && typeof v === "object" && !intrinsic.isBuffer(v) && v.subjectPublicKeyInfo != null && v.subjectPublicKeyInfo.bytes != null) {
1106
- // Validate the bare-form bytes as a BufferSource so a malformed value fails through the typed CT
1107
- // error contract, not a native TypeError from the SHA-256 update downstream.
1108
939
  return guard.bytes.view(v.subjectPublicKeyInfo.bytes, CtError, "ct/bad-cert-entry", field + " subjectPublicKeyInfo.bytes");
1109
940
  }
1110
941
  return _asParsedCert(v, field).subjectPublicKeyInfo.bytes;
@@ -1113,27 +944,20 @@ function _issuerSpki(v, field) {
1113
944
  function x509CertEntry(cert, issuer) {
1114
945
  var leaf = _asParsedCert(cert, "cert"), issSpki = _issuerSpki(issuer, "issuer");
1115
946
  var tbs = asn1.decode(leaf.tbsBytes);
1116
- // extensions [3] EXPLICIT is the last TBSCertificate field (context-constructed tag 3).
1117
947
  var wrapIdx = -1;
1118
948
  for (var i = 0; i < tbs.children.length; i++) {
1119
949
  if (tbs.children[i].tagClass === "context" && tbs.children[i].tagNumber === 3) { wrapIdx = i; break; }
1120
950
  }
1121
951
  if (wrapIdx < 0) throw _ctErr("ct/no-sct-extension", "the certificate has no extensions, so it carries no embedded SCT list (RFC 6962 sec. 3.2)");
1122
- var extsSeq = tbs.children[wrapIdx].children[0]; // the SEQUENCE OF Extension
952
+ var extsSeq = tbs.children[wrapIdx].children[0];
1123
953
  var sctOid = asn1.build.oid(oid.byName("signedCertificateTimestampList"));
1124
954
  var kept = [], removed = 0;
1125
955
  for (var j = 0; j < extsSeq.children.length; j++) {
1126
956
  var ext = extsSeq.children[j];
1127
- // ext.children[0] is the extnID OID node; compare its exact TLV bytes to the SCT-list OID.
1128
957
  if (ext.children[0] && intrinsic.bufferEquals(ext.children[0].bytes, sctOid)) { removed++; continue; }
1129
958
  intrinsic.push(kept, ext.bytes);
1130
959
  }
1131
960
  if (removed === 0) throw _ctErr("ct/no-sct-extension", "the certificate carries no signedCertificateTimestampList extension (RFC 6962 sec. 3.2)");
1132
- // Rebuild the TBSCertificate SEQUENCE from raw child bytes; canonical DER lengths reproduce the
1133
- // original framing minus the removed extension. When the SCT-list was the ONLY extension, `kept` is
1134
- // empty and the extensions field is dropped ENTIRELY -- RFC 5280 Extensions is SIZE (1..MAX) and the
1135
- // [3] field is OPTIONAL, so an empty [3] SEQUENCE {} would be invalid and would not match the signed
1136
- // precert TBS, whose extensions field is likewise absent once its sole poison extension is removed.
1137
961
  var tbsChildren = [];
1138
962
  for (var k = 0; k < tbs.children.length; k++) {
1139
963
  if (k !== wrapIdx) { intrinsic.push(tbsChildren, tbs.children[k].bytes); continue; }
@@ -1176,12 +1000,10 @@ function x509CertEntry(cert, issuer) {
1176
1000
  */
1177
1001
  async function verifyLogListSignature(json, signature, publicKey) {
1178
1002
  var message = typeof json === "string" ? Buffer.from(json) : _toBuffer(json, "the CT log list JSON");
1179
- // Bound the signed message before the digest/verify (the same cap parseLogList enforces) so a hostile
1180
- // caller cannot force unbounded hashing work on an oversized input (CWE-400).
1181
1003
  guard.limits.byteCap(message, C.LIMITS.CT_LOG_LIST_MAX_BYTES, _ctErr, "ct/too-large", "the CT log list");
1182
1004
  var sig = _toBuffer(signature, "the CT log list signature");
1183
1005
  var spki = _toBuffer(publicKey, "the CT log list signer public key (SPKI)");
1184
- var alg = _spkiAlg(spki); // fail-closed: a non-SPKI key or a forgeable RSA e < 3 throws ct/bad-input
1006
+ var alg = _spkiAlg(spki);
1185
1007
  var imp, ver;
1186
1008
  if (alg.algOid === oid.byName("rsaEncryption")) {
1187
1009
  if (!(alg.rsaBits >= 2048)) throw new CtError("ct/unsupported-algorithm", "the CT log-list signer RSA key is below the 2048-bit minimum");
@@ -1192,15 +1014,10 @@ async function verifyLogListSignature(json, signature, publicKey) {
1192
1014
  if (!ec) throw new CtError("ct/unsupported-algorithm", "unsupported CT log-list signer EC curve (only NIST P-256)");
1193
1015
  imp = { name: "ECDSA", namedCurve: ec.curve };
1194
1016
  ver = { name: "ECDSA", hash: "SHA-256" };
1195
- // A DER ECDSA-Sig-Value: route through the ORDER-AWARE conformance gate (primitive, minimal, and
1196
- // r,s in [1, n-1] per FIPS 186-5 sec. 6.4.2 -- defeating the CVE-2022-21449 r=s=0 forgery AND an
1197
- // out-of-range r/s >= the curve order n) before the raw r||s conversion.
1198
1017
  sig = validator.sig.ecdsaDerToP1363(sig, ec.curve, CtError, "ct/bad-signature");
1199
1018
  } else {
1200
1019
  throw new CtError("ct/unsupported-algorithm", "unsupported CT log-list signer key algorithm (only rsaEncryption / ecPublicKey P-256)");
1201
1020
  }
1202
- // A wrong signature resolves false from subtle.verify (a verdict); a structural failure -- an
1203
- // unimportable key -- is re-thrown fail-closed.
1204
1021
  try {
1205
1022
  var key = await subtle.importKey("spki", spki, imp, false, ["verify"]);
1206
1023
  return await subtle.verify(ver, key, sig, message);
@@ -1209,30 +1026,20 @@ async function verifyLogListSignature(json, signature, publicKey) {
1209
1026
  }
1210
1027
  }
1211
1028
 
1212
- // ---- CT log-list live-fetch client (RFC 6962 sec. 3.2; the Chrome/Apple detached log_list.sig model) ----
1213
1029
 
1214
1030
  var DEFAULT_FETCH_TIMEOUT = C.TIME.seconds(30);
1215
1031
  var MAX_FETCH_TIMEOUT = C.TIME.seconds(600);
1216
1032
  var KNOWN_FETCH_OPTS = { url: 1, signerKey: 1, sigUrl: 1, transport: 1, tls: 1, headers: 1, timeout: 1, maxResponseBytes: 1, requireJsonContentType: 1 };
1217
1033
 
1218
- // Map opts.tls (operator-facing) to the transport request.tls shape. rejectUnauthorized is NOT here -- the
1219
- // transport forces it on unconditionally; the verb never disables server verification.
1220
1034
  function _tlsForFetch(opts) {
1221
1035
  var t = opts.tls || {};
1222
1036
  return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
1223
1037
  }
1224
1038
 
1225
- // Do the two URLs share an origin (scheme + host + port)? Both args are already-validated absolute URL
1226
- // strings (each came from `new URL(...).href` after the https gate), so the parse cannot fail here.
1227
1039
  function _sameOrigin(a, b) { return new URL(a).origin === new URL(b).origin; }
1228
1040
 
1229
- // The response content-type token (before any ;-parameters), lowercased.
1230
1041
  function _fetchCtypeToken(headers) { return String((headers || {})["content-type"] || "").split(";")[0].trim().toLowerCase(); }
1231
1042
 
1232
- // Parse a URL and require https BEFORE any request -- runs even when a transport is INJECTED, so an insecure
1233
- // URL never reaches the wire (the CT log list is fetched over TLS; the detached signature is the authenticity
1234
- // gate, but the fetch stays confined to https, consistent with the est/acme clients). A parse failure is
1235
- // ct/bad-url; a non-https scheme is ct/insecure-url.
1236
1043
  function _parseHttpsUrl(u, label) {
1237
1044
  var parsed;
1238
1045
  try { parsed = new URL(String(u)); }
@@ -1241,10 +1048,6 @@ function _parseHttpsUrl(u, label) {
1241
1048
  return parsed;
1242
1049
  }
1243
1050
 
1244
- // Derive the detached-signature URL from the (already parsed, https) log_list.json URL by the Chrome/Apple
1245
- // convention: the final ".json" path suffix rewritten to ".sig". A path that does NOT end in ".json" cannot
1246
- // yield an unambiguous sig URL, so the caller must pass opts.sigUrl explicitly (guards never guess a value).
1247
- // The derived URL inherits the json URL's https origin.
1248
1051
  function _sigUrlFor(parsed) {
1249
1052
  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");
1250
1053
  var sig = new URL(parsed.href);
@@ -1252,15 +1055,7 @@ function _sigUrlFor(parsed) {
1252
1055
  return sig.href;
1253
1056
  }
1254
1057
 
1255
- // One bounded GET returning { body: Buffer, status, contentType, tls }. The body is re-viewed through the
1256
- // byte guard (a detached backing buffer fails as ct/bad-input, never a silent empty body); a string body
1257
- // falls back to a UTF-8 encode -- the width a real socket already counts -- so the size cap and verify/parse
1258
- // see the same bytes, never a typed array stringified to "48,130,..." garbage. An oversized body ->
1259
- // ct/response-too-large (the post-hoc twin of the transport's streaming abort); a non-200 -> ct/http-error
1260
- // carrying the status; an empty 200 body -> ct/empty-response. Both GETs must succeed before verify.
1261
1058
  function _fetchBody(transport, url, req, label) {
1262
- // Wrap the transport call in a Promise.resolve().then so a SYNCHRONOUS throw from an injected transport
1263
- // becomes a rejection the catch below can type (not an uncaught throw crossing the API boundary).
1264
1059
  return Promise.resolve().then(function () {
1265
1060
  return transport({ method: "GET", url: url, headers: req.headers, tls: req.tls, timeout: req.timeout, maxResponseBytes: req.maxResponseBytes });
1266
1061
  }).then(function (res) {
@@ -1275,10 +1070,6 @@ function _fetchBody(transport, url, req, label) {
1275
1070
  if (body.length === 0) throw _ctErr("ct/empty-response", "the CT server returned a 200 with an empty " + label);
1276
1071
  return { body: body, status: res.status, contentType: _fetchCtypeToken(h), tls: res.tls || null };
1277
1072
  }).catch(function (e) {
1278
- // An already-typed PkiError (the default transport's ct/*, this function's own ct/* verdicts, or a
1279
- // caller-thrown typed error) propagates UNCHANGED; any other throw from an injected transport -- a raw
1280
- // network Error, a synchronous throw -- is wrapped as a typed ct/transport-error so a fetch failure never
1281
- // crosses the API boundary untyped (the documented typed-error contract).
1282
1073
  if (e instanceof PkiError) throw e;
1283
1074
  throw _ctErr("ct/transport-error", "the CT " + label + " request failed in the transport", e);
1284
1075
  });
@@ -1327,17 +1118,9 @@ async function fetchLogList(opts) {
1327
1118
  guard.identifier.assertKnownKeys(opts, KNOWN_FETCH_OPTS, _ctErr, "ct/bad-input", "unknown opts field ");
1328
1119
  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)");
1329
1120
  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)");
1330
- // Parse + https-gate both URLs BEFORE any transport call -- the gate runs across the injectable seam so an
1331
- // http URL never reaches even an injected transport (the documented HTTPS guarantee holds either way).
1332
1121
  var jsonParsed = _parseHttpsUrl(opts.url, "URL");
1333
1122
  var jsonUrl = jsonParsed.href;
1334
1123
  var sigUrl = opts.sigUrl != null ? _parseHttpsUrl(opts.sigUrl, "signature URL").href : _sigUrlFor(jsonParsed);
1335
- // The detached signature MUST share the log-list URL's origin. The log-list trust rests on the pinned-key
1336
- // signature, not on where the signature is hosted; requiring one origin means both GETs carry the same
1337
- // per-origin credentials + TLS identity with NO cross-origin leak (an Authorization/Cookie header or an
1338
- // mTLS client certificate configured for the log-list host can never reach a different signature host). A
1339
- // caller with a genuinely separate signature host fetches it itself and composes verifyLogListSignature +
1340
- // parseLogList directly.
1341
1124
  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");
1342
1125
  var transport = opts.transport;
1343
1126
  if (!transport) {
@@ -1348,23 +1131,15 @@ async function fetchLogList(opts) {
1348
1131
  }
1349
1132
  var timeout = guard.limits.cap(opts.timeout, "timeout", DEFAULT_FETCH_TIMEOUT, { E: _ctErr, code: "ct/bad-input", min: 1, max: MAX_FETCH_TIMEOUT });
1350
1133
  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 });
1351
- // Forward custom opts.headers but STRIP the request-framing headers so a caller cannot desync the request
1352
- // framing; the verb owns the GET method (no body).
1353
1134
  var headers = {};
1354
1135
  Object.keys(opts.headers || {}).forEach(function (k) {
1355
1136
  var lk = k.toLowerCase();
1356
1137
  if (lk !== "content-length" && lk !== "transfer-encoding") headers[k] = opts.headers[k];
1357
1138
  });
1358
1139
  var req = { headers: headers, tls: _tlsForFetch(opts), timeout: timeout, maxResponseBytes: maxResponseBytes };
1359
- // Two bounded GETs; BOTH must succeed before verify. The JSON is fetched first (its content-type is the one
1360
- // opts.requireJsonContentType polices). Both requests carry the same per-origin credentials + TLS identity,
1361
- // which is sound because the signature shares the log-list origin (gated above).
1362
1140
  var jsonRes = await _fetchBody(transport, jsonUrl, req, "CT log-list JSON");
1363
1141
  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)");
1364
1142
  var sigRes = await _fetchBody(transport, sigUrl, req, "CT log-list signature");
1365
- // VERIFY BEFORE PARSE: the fetched JSON is untrusted until the detached signature verifies over the RAW
1366
- // bytes against the caller-pinned key. parseLogList runs ONLY on a strict === true verdict, over the SAME
1367
- // Buffer -- nothing re-serialized between verify and parse.
1368
1143
  var ok = await verifyLogListSignature(jsonRes.body, sigRes.body, opts.signerKey);
1369
1144
  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");
1370
1145
  var parsed = parseLogList(jsonRes.body);