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