@blamejs/pki 0.6.14 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +140 -286
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- 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;
|
|
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();
|
|
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]));
|
|
306
|
-
parts.push(Buffer.from([0]));
|
|
307
|
-
var ts = Buffer.alloc(8); ts.writeBigUInt64BE(tsVal); parts.push(ts);
|
|
308
|
-
parts.push(Buffer.from([(entryType >> 8) & 0xff, entryType & 0xff]));
|
|
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);
|
|
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);
|
|
283
|
+
parts.push(_u24Bytes(tbs.length)); parts.push(tbs);
|
|
320
284
|
}
|
|
321
|
-
var ext = _toBuffer(sct.extensions, "sct.extensions");
|
|
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);
|
|
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");
|
|
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");
|
|
490
|
-
w.vector(2, 0, 0xffff, _toBuffer(sct.extensions, "sct.extensions"), "ct/bad-input");
|
|
491
|
-
w.u8(sct.hashAlg, "ct/bad-input");
|
|
492
|
-
w.u8(sct.sigAlg, "ct/bad-input");
|
|
493
|
-
w.vector(2, 0, 0xffff, _toBuffer(sct.signature, "sct.signature"), "ct/bad-input");
|
|
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");
|
|
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");
|
|
549
|
-
return asn1.build.octetString(lw.build());
|
|
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);
|
|
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,
|
|
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;
|
|
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;
|
|
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;
|
|
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
|
-
|
|
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,
|
|
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];
|
|
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);
|
|
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);
|