@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/tlog.js
ADDED
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.tlog
|
|
6
|
+
* @nav Transparency
|
|
7
|
+
* @title Tlog
|
|
8
|
+
* @fullname Transparency-log envelope: signed notes, checkpoints, tiles (C2SP)
|
|
9
|
+
* @order 221
|
|
10
|
+
* @slug tlog
|
|
11
|
+
*
|
|
12
|
+
* @intro
|
|
13
|
+
* The C2SP envelope every tiled transparency log speaks, over the hashes
|
|
14
|
+
* `pki.merkle` already verifies: a signed note (`c2sp.org/signed-note`), the
|
|
15
|
+
* checkpoint body a log signs into one (`c2sp.org/tlog-checkpoint`), and the
|
|
16
|
+
* tile addressing a static log serves them beside (`c2sp.org/tlog-tiles`).
|
|
17
|
+
* Static-CT, Merkle Tree Certificates and Rekor v2 differ in what they put in
|
|
18
|
+
* the origin line and what they do with the root, not in this layer.
|
|
19
|
+
*
|
|
20
|
+
* A note is text, a blank line, then one signature line per signer. The
|
|
21
|
+
* signature is over the text INCLUDING its trailing newline, so `parseNote`
|
|
22
|
+
* surfaces `signedBytes` as a view of the input, never a re-serialization:
|
|
23
|
+
* a verifier that rebuilt the text from its parsed lines would accept a note
|
|
24
|
+
* altered in the delimiter. A key ID is four bytes and the specification says
|
|
25
|
+
* it identifies rather than proves, so `verifyNote` tries every supplied key
|
|
26
|
+
* whose ID matches and reports a verdict only after all of them. An unknown
|
|
27
|
+
* signature is ignored; a note carrying no signature from a supplied key is
|
|
28
|
+
* rejected.
|
|
29
|
+
*
|
|
30
|
+
* A checkpoint is that note with a body of origin, tree size and root hash,
|
|
31
|
+
* plus opaque extension lines. The tree size is carried as a `BigInt`, the
|
|
32
|
+
* root as 32 raw bytes ready for `pki.merkle.verifyInclusion`. Decoding is
|
|
33
|
+
* fail-closed: a leading zero in the tree size, an empty origin, an empty
|
|
34
|
+
* extension line, a root that is not 32 bytes, or non-canonical base64
|
|
35
|
+
* anywhere is a typed `tlog/*` refusal.
|
|
36
|
+
*
|
|
37
|
+
* `tilePath` / `entryBundlePath` build the paths a log serves, and
|
|
38
|
+
* `parseTilePath` reads one back. Verifying only: nothing here signs, and
|
|
39
|
+
* nothing fetches.
|
|
40
|
+
*
|
|
41
|
+
* @card
|
|
42
|
+
* Signed notes, checkpoints and tile paths over `pki.merkle`, fail-closed and
|
|
43
|
+
* transport-free.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
var constants = require("./constants");
|
|
47
|
+
var frameworkError = require("./framework-error");
|
|
48
|
+
var guard = require("./guard-all");
|
|
49
|
+
var subtle = require("./webcrypto").webcrypto.subtle;
|
|
50
|
+
var edwardsPoint = require("./edwards-point");
|
|
51
|
+
var merkle = require("./merkle");
|
|
52
|
+
var intrinsic = require("./guard-intrinsic");
|
|
53
|
+
|
|
54
|
+
/** @internal The two SubtleCrypto operations an Ed25519 note verdict rests on, captured at load. The
|
|
55
|
+
* object was held and its METHODS fetched per call, which leaves them replaceable: a `verify`
|
|
56
|
+
* answering true accepts a note no key signed. */
|
|
57
|
+
var _subtleImportKey = intrinsic.uncurry(subtle.importKey);
|
|
58
|
+
var _subtleVerify = intrinsic.uncurry(subtle.verify);
|
|
59
|
+
|
|
60
|
+
/** @internal The string and array operations this module parses with, taken at load. A note, a
|
|
61
|
+
* checkpoint body and a tile path are all split and sliced to reach the values a verdict rests on,
|
|
62
|
+
* and `String.prototype` `split` / `indexOf` / `slice` are replaceable: one swapped after this
|
|
63
|
+
* module loads would otherwise be asked at the moment of the parse and could steer which line reads
|
|
64
|
+
* as the origin, the tree size or the root. `pki.sigstore` composes this module inside a verifier
|
|
65
|
+
* already hardened that way, so the strictest consumer sets the rule here. */
|
|
66
|
+
var _split = intrinsic.uncurry(String.prototype.split);
|
|
67
|
+
var _strIndexOf = intrinsic.stringIndexOf;
|
|
68
|
+
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
69
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
70
|
+
var _arrSlice = intrinsic.arraySlice;
|
|
71
|
+
var _join = intrinsic.join;
|
|
72
|
+
var _push = intrinsic.push;
|
|
73
|
+
var _pop = intrinsic.pop;
|
|
74
|
+
var _unshift = intrinsic.unshift;
|
|
75
|
+
var _bufToString = intrinsic.bufToString;
|
|
76
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
77
|
+
var _byteLength = intrinsic.byteLength;
|
|
78
|
+
var _bufferConcat = intrinsic.bufferConcat;
|
|
79
|
+
var _bufferCompare = intrinsic.compare;
|
|
80
|
+
var _subarray = intrinsic.subarray;
|
|
81
|
+
var _create = intrinsic.create;
|
|
82
|
+
var _assign = intrinsic.assign;
|
|
83
|
+
var _isArray = intrinsic.isArray;
|
|
84
|
+
var _isInteger = intrinsic.isInteger;
|
|
85
|
+
var _fromCharCode = intrinsic.fromCharCode;
|
|
86
|
+
var _jsonStringify = intrinsic.stringify;
|
|
87
|
+
var _stringOf = intrinsic.String;
|
|
88
|
+
var _numberOf = intrinsic.Number;
|
|
89
|
+
var _bigIntOf = intrinsic.BigInt;
|
|
90
|
+
var _numToString = intrinsic.uncurry(Number.prototype.toString);
|
|
91
|
+
|
|
92
|
+
var TlogError = frameworkError.TlogError;
|
|
93
|
+
function _err(code, message, cause) { return new TlogError(code, message, cause); }
|
|
94
|
+
|
|
95
|
+
var SHA256_BYTES = 32;
|
|
96
|
+
var KEY_ID_BYTES = 4;
|
|
97
|
+
var ED25519_ALG_ID = 0x01;
|
|
98
|
+
var ED25519_RAW_BYTES = 32;
|
|
99
|
+
var TILE_HEIGHT_HASHES = 256;
|
|
100
|
+
var TILE_SEGMENT_DIGITS = 3;
|
|
101
|
+
var TILE_MAX_LEVEL = 63;
|
|
102
|
+
var MIN_CHECKPOINT_LINES = 3;
|
|
103
|
+
|
|
104
|
+
/** @internal The signature line's first character is an em dash, U+2014, which the wire format
|
|
105
|
+
* requires. It is built from its codepoint so the byte cannot drift through an editor, and it is
|
|
106
|
+
* the only place in this codebase where that character is data rather than prose. */
|
|
107
|
+
var SIG_LINE_PREFIX = _fromCharCode(0x2014) + " ";
|
|
108
|
+
|
|
109
|
+
var LF = 0x0a;
|
|
110
|
+
|
|
111
|
+
/** @internal A caller's byte source as a byte VIEW, before anything measures or copies it. Every door here
|
|
112
|
+
* admits what `guard.bytes.isByteSource` admits and says so, which is Buffer, TypedArray, DataView and
|
|
113
|
+
* ArrayBuffer. `guard.bytes.lengthOf` and `guard.bytes.snapshot` do not take that whole set, so measuring
|
|
114
|
+
* or copying the raw input left the admitted set wider than the handled one: an ArrayBuffer reached
|
|
115
|
+
* `lengthOf` and raised a bare TypeError carrying no code, and a DataView over valid bytes was refused as
|
|
116
|
+
* though its content were wrong. `Response.arrayBuffer()` is the ordinary way a tile arrives, so the gap
|
|
117
|
+
* was on the path a caller actually takes.
|
|
118
|
+
*
|
|
119
|
+
* `guard.bytes.source` shares the caller's backing store rather than copying it, so normalizing FIRST
|
|
120
|
+
* leaves the cap ahead of every allocation, which is the order `_binary` explains and depends on. */
|
|
121
|
+
function _normalized(input, field) {
|
|
122
|
+
return guard.bytes.source(input, TlogError, "tlog/bad-input", field);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function _bytes(input, field, maxBytes) {
|
|
126
|
+
/** @internal The cap is checked BEFORE either conversion allocates, for the reason at `_binary`. A string
|
|
127
|
+
* is measured as the UTF-8 it will encode to rather than as its code-unit count. */
|
|
128
|
+
if (typeof input === "string") {
|
|
129
|
+
var utf8 = _byteLength(input, "utf8");
|
|
130
|
+
if (utf8 > maxBytes) throw _err("tlog/bad-input", field + " " + utf8 + " bytes exceeds the cap " + maxBytes);
|
|
131
|
+
return _bufferFrom(input, "utf8");
|
|
132
|
+
}
|
|
133
|
+
var view = _normalized(input, field);
|
|
134
|
+
var size = guard.bytes.lengthOf(view);
|
|
135
|
+
if (size > maxBytes) throw _err("tlog/bad-input", field + " " + size + " bytes exceeds the cap " + maxBytes);
|
|
136
|
+
/** @internal A note's `signedBytes` is a view of this, which the docstring states, and the note itself is
|
|
137
|
+
* re-read by nothing after parsing, so this one keeps its view. */
|
|
138
|
+
return view;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @internal The same coercion, but a COPY, for a verb that reads the input more than once or holds a
|
|
142
|
+
* view of it across an await. `guard.bytes.source` returns a view of the caller's buffer: a caller that
|
|
143
|
+
* reuses or overwrites that buffer while an Ed25519 key import is pending changes the bytes between the
|
|
144
|
+
* decode and the verification, so the verdict describes one document and the values reported come from
|
|
145
|
+
* another. A string is already immutable and is copied by the UTF-8 decode. Taking the copy ONCE at the
|
|
146
|
+
* entry point is what makes the decode and the signature check read the same bytes. */
|
|
147
|
+
function _bytesSnapshot(input, field, maxBytes) {
|
|
148
|
+
/** @internal The cap is checked BEFORE the copy, for the reason at `_binary`: the verify verbs snapshot
|
|
149
|
+
* their note so the bytes verified are the bytes reported, and an oversized one was copied in full before
|
|
150
|
+
* the parser read the limit that refuses it. A string is measured as the UTF-8 it will encode to. */
|
|
151
|
+
if (typeof input === "string") {
|
|
152
|
+
var utf8 = _byteLength(input, "utf8");
|
|
153
|
+
if (utf8 > maxBytes) throw _err("tlog/bad-input", field + " " + utf8 + " bytes exceeds the cap " + maxBytes);
|
|
154
|
+
return _bufferFrom(input, "utf8");
|
|
155
|
+
}
|
|
156
|
+
var view = _normalized(input, field);
|
|
157
|
+
var size = guard.bytes.lengthOf(view);
|
|
158
|
+
if (size > maxBytes) throw _err("tlog/bad-input", field + " " + size + " bytes exceeds the cap " + maxBytes);
|
|
159
|
+
return guard.bytes.snapshot(view, _err, "tlog/bad-input", field);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @internal A tile, an entry bundle and a key are binary, where a note is text. Reading a string
|
|
163
|
+
* as one of them decodes it as UTF-8, which rewrites every byte above 0x7f, so the result is never
|
|
164
|
+
* the data it stands for; a 32-character string is the length of one hash and would parse as one.
|
|
165
|
+
* A string here is a caller that read a response as text rather than bytes, and is told so. */
|
|
166
|
+
function _binary(input, field, maxBytes, tooLargeCode) {
|
|
167
|
+
if (typeof input === "string") {
|
|
168
|
+
throw _err("tlog/bad-input", field + " is binary and must be a byte source, not a string: " +
|
|
169
|
+
"read the response as bytes rather than as text");
|
|
170
|
+
}
|
|
171
|
+
/** @internal COPIES rather than views. A tile's hashes and an entry bundle's entries are returned as
|
|
172
|
+
* slices of what arrives here, and the bytes arrive from a caller's `read` callback: a callback reusing
|
|
173
|
+
* one scratch buffer across fetches would have a later tile overwrite hashes and proof nodes already
|
|
174
|
+
* collected, so the proof verified would not be the proof that was served.
|
|
175
|
+
* The cap is checked BEFORE the copy, on the input's authoritative byte length. Copying first would cost
|
|
176
|
+
* a second allocation the size of a hostile response before the limit that refuses it was read: a 64 MiB
|
|
177
|
+
* buffer handed to a parser whose tiles are 8192 bytes. Same order as `guard.text.decode`. */
|
|
178
|
+
var view = _normalized(input, field);
|
|
179
|
+
var size = guard.bytes.lengthOf(view);
|
|
180
|
+
if (size > maxBytes) {
|
|
181
|
+
/** @internal The CALLER's code, so moving the cap ahead of the copy did not move the verdict: an
|
|
182
|
+
* over-wide tile is still `tlog/bad-tile` and not the door's own error. */
|
|
183
|
+
throw _err(tooLargeCode, field + " " + size + " bytes exceeds the cap " + maxBytes);
|
|
184
|
+
}
|
|
185
|
+
return guard.bytes.snapshot(view, TlogError, "tlog/bad-input", field);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @internal A note is UTF-8 text carrying no ASCII control byte other than newline. The scan is by
|
|
189
|
+
* character code, not a pattern, and it runs before anything is split, so a control byte is
|
|
190
|
+
* refused instead of being carried into a line. */
|
|
191
|
+
function _assertNoteText(buf) {
|
|
192
|
+
for (var i = 0; i < buf.length; i++) {
|
|
193
|
+
var c = buf[i];
|
|
194
|
+
if (c < 0x20 && c !== LF) {
|
|
195
|
+
throw _err("tlog/bad-note", "a note carries no ASCII control byte other than newline; found 0x" +
|
|
196
|
+
_numToString(c, 16) + " at offset " + i);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @internal Canonical base64 through the shared encoding guard, which is where the alphabet, the
|
|
202
|
+
* group-length rule and the re-encode check live; a bare decode accepts text that is not the text
|
|
203
|
+
* it claims to be. Every base64 field in this module reads through here. */
|
|
204
|
+
function _base64(text, field) {
|
|
205
|
+
return guard.encoding.base64(text, constants.LIMITS.TLOG_MAX_NOTE_BYTES, _err, "tlog/bad-note", field);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** @internal An ASCII decimal string with no leading zero, as a BigInt. "0" alone is the empty
|
|
209
|
+
* tree and is the one value whose first digit may be zero. Bounded to a uint64, which is the
|
|
210
|
+
* width pki.merkle carries a tree coordinate at, so a size this accepts is one the fold takes. */
|
|
211
|
+
function _decimal(text, field, code) {
|
|
212
|
+
return guard.range.decimalUint64(text, _err, code, field);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @primitive pki.tlog.keyId
|
|
217
|
+
* @signature pki.tlog.keyId(keyName, publicKey) -> Buffer
|
|
218
|
+
* @since 0.8.33
|
|
219
|
+
* @status stable
|
|
220
|
+
* @spec C2SP signed-note
|
|
221
|
+
* @related pki.tlog.verifyNote
|
|
222
|
+
*
|
|
223
|
+
* The four-byte key ID a signed note carries in front of its signature. The derivation is fixed per
|
|
224
|
+
* algorithm and they are not variations on one form, so it is read from the key material rather than
|
|
225
|
+
* from anything the caller declares:
|
|
226
|
+
*
|
|
227
|
+
* - **Ed25519:** `SHA-256(keyName || 0x0A || 0x01 || raw32)[:4]`. Pass the raw 32 bytes or an
|
|
228
|
+
* Ed25519 SubjectPublicKeyInfo; both give the same ID, because one key has one ID.
|
|
229
|
+
* - **ECDSA** over P-256, P-384 or P-521: `SHA-256(spkiDer)[:4]`, with neither the key name nor a
|
|
230
|
+
* type byte in the preimage. The same key therefore has one ID under any name.
|
|
231
|
+
* - **RSA:** `SHA-256(keyName || 0x0A || 0xFF || "PKIX-RSA-PKCS#1v1.5" || spkiDer)[:4]`.
|
|
232
|
+
*
|
|
233
|
+
* An algorithm with no stated derivation is `tlog/bad-input` rather than hashed under an assumed
|
|
234
|
+
* one, which would produce an identifier no log would ever state.
|
|
235
|
+
*
|
|
236
|
+
* The specification calls these identifiers rather than cryptographically strong hashes, and four
|
|
237
|
+
* bytes is short enough that two keys can share one. A match therefore selects a candidate and
|
|
238
|
+
* never decides a verdict; `pki.tlog.verifyNote` checks every key whose ID matches.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
242
|
+
* var spki = await pki.key.export(pair.publicKey);
|
|
243
|
+
* var raw = pki.asn1.decode(spki).children[1].content.subarray(1); // the 32 key bytes
|
|
244
|
+
* pki.tlog.keyId("example.com/log", raw).length; // -> 4
|
|
245
|
+
*/
|
|
246
|
+
function keyId(keyName, publicKey) {
|
|
247
|
+
return _resolveKey(keyName, publicKey).id;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var ECDSA_ALG_ID = 0x02;
|
|
251
|
+
var RSA_ALG_ID = 0xff;
|
|
252
|
+
var RSA_FORMAT_TAG = _bufferFrom("PKIX-RSA-PKCS#1v1.5", "utf8");
|
|
253
|
+
/** @internal The curves signature type 0x02 admits. c2sp.org/signed-note states "The ECDSA curve MUST be
|
|
254
|
+
* one of NIST P-256, NIST P-384, or NIST P-521", so a key on any other curve has no type to be carried
|
|
255
|
+
* under and is refused rather than signed for under an assumed one.
|
|
256
|
+
*
|
|
257
|
+
* The DIGEST is not read from this list. The same clause defines the type as "ECDSA signatures as
|
|
258
|
+
* implemented by github.com/transparency-dev/witness", whose constant for the byte is named
|
|
259
|
+
* `algECDSAWithSHA256` and whose verifier computes `sha256.Sum256(msg)` for every key it accepts, with no
|
|
260
|
+
* branch on the curve. The digest belongs to the signature TYPE, and taking it from the curve instead
|
|
261
|
+
* rejected every conforming P-384 and P-521 note with `tlog/bad-signature`. */
|
|
262
|
+
var EC_CURVES = _assign(_create(null), {
|
|
263
|
+
prime256v1: 1, secp384r1: 1, secp521r1: 1,
|
|
264
|
+
});
|
|
265
|
+
var EC_HASH = "sha256";
|
|
266
|
+
|
|
267
|
+
/** @internal A supplied key resolved to the one identity the specification gives it: its signature
|
|
268
|
+
* type, its SubjectPublicKeyInfo, and the four-byte ID a note would carry for it.
|
|
269
|
+
*
|
|
270
|
+
* The algorithm is read off the key material through the engine rather than taken from the caller,
|
|
271
|
+
* so a key cannot be hashed under a type it is not. A 32-byte input is raw Ed25519: no
|
|
272
|
+
* SubjectPublicKeyInfo is that short, the shortest being 44 bytes for this same algorithm, so the
|
|
273
|
+
* two readings cannot collide. Anything else must be an SPKI the engine accepts.
|
|
274
|
+
*
|
|
275
|
+
* The ECDSA preimage is the SPKI alone, which is the specification's own departure from the other
|
|
276
|
+
* two forms, so the name never reaches the hash for that algorithm. */
|
|
277
|
+
function _resolveKey(keyName, publicKey) {
|
|
278
|
+
if (typeof keyName !== "string" || keyName.length === 0) {
|
|
279
|
+
throw _err("tlog/bad-input", "keyName must be a non-empty string");
|
|
280
|
+
}
|
|
281
|
+
/** @internal COPIED. This is the one door every key enters through, and what it returns is BOTH the
|
|
282
|
+
* material the key ID is derived from and the material a signature is later verified under. A note with
|
|
283
|
+
* several signature lines awaits a key import per line, so a view would let a caller replace a later
|
|
284
|
+
* key's bytes after its ID was computed: the ID that decides which key a line is checked against would
|
|
285
|
+
* come from one key and the verification from another, and a line could be attributed to a signer whose
|
|
286
|
+
* key never signed it. Copying here makes the ID and the verification one read of one value. */
|
|
287
|
+
/** @internal A verifier key is a raw Ed25519 key or an SPKI, so the note cap is far above any real one and
|
|
288
|
+
* is here only so an oversized input is refused before it is copied. `_binary` already snapshots, so it
|
|
289
|
+
* is not wrapped a second time. */
|
|
290
|
+
var bytes = _binary(publicKey, "publicKey", constants.LIMITS.TLOG_MAX_NOTE_BYTES, "tlog/bad-input");
|
|
291
|
+
var name = _bufferFrom(keyName, "utf8");
|
|
292
|
+
if (bytes.length === ED25519_RAW_BYTES) {
|
|
293
|
+
return _edKey(name, _bufferConcat([ED25519_SPKI_PREFIX, bytes]), bytes);
|
|
294
|
+
}
|
|
295
|
+
var key;
|
|
296
|
+
try { key = guard.crypto.publicKey({ key: bytes, format: "der", type: "spki" }); }
|
|
297
|
+
catch (e) {
|
|
298
|
+
throw _err("tlog/bad-input", "publicKey must be the raw " + ED25519_RAW_BYTES +
|
|
299
|
+
" Ed25519 bytes or a SubjectPublicKeyInfo, got " + bytes.length + " bytes", e);
|
|
300
|
+
}
|
|
301
|
+
var type = key.asymmetricKeyType;
|
|
302
|
+
if (type === "ed25519") {
|
|
303
|
+
return _edKey(name, bytes, _subarray(bytes, bytes.length - ED25519_RAW_BYTES));
|
|
304
|
+
}
|
|
305
|
+
if (type === "ec") {
|
|
306
|
+
var curve = key.asymmetricKeyDetails && key.asymmetricKeyDetails.namedCurve;
|
|
307
|
+
if (EC_CURVES[curve] !== 1) {
|
|
308
|
+
throw _err("tlog/bad-input", "a signed note names no key-ID derivation for the elliptic curve " +
|
|
309
|
+
guard.text.showValue(_stringOf(curve)));
|
|
310
|
+
}
|
|
311
|
+
return { type: ECDSA_ALG_ID, spki: bytes, raw: null, hash: EC_HASH, key: key,
|
|
312
|
+
id: _idOf([bytes]) };
|
|
313
|
+
}
|
|
314
|
+
if (type === "rsa") {
|
|
315
|
+
return { type: RSA_ALG_ID, spki: bytes, raw: null, hash: "sha256", key: key,
|
|
316
|
+
id: _idOf([name, _bufferFrom([LF, RSA_ALG_ID]), RSA_FORMAT_TAG, bytes]) };
|
|
317
|
+
}
|
|
318
|
+
throw _err("tlog/bad-input", "a signed note names no key-ID derivation for a " +
|
|
319
|
+
guard.text.showValue(_stringOf(type)) + " key");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function _edKey(name, spki, raw) {
|
|
323
|
+
/** @internal The shared Edwards-point gate runs where the key is resolved, not only where it is
|
|
324
|
+
* used: Node imports a low-order key without complaint and such a key verifies a FORGED
|
|
325
|
+
* signature, so one must never become a candidate or be given an identifier at all. */
|
|
326
|
+
edwardsPoint.validateSpki(spki, ED25519_CRV, TlogError, "tlog/bad-input");
|
|
327
|
+
return { type: ED25519_ALG_ID, spki: spki, raw: raw, hash: null, key: null,
|
|
328
|
+
id: _idOf([name, _bufferFrom([LF, ED25519_ALG_ID]), raw]) };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** @internal The identifier is what matches a signature line to a caller's key, so the digest over its
|
|
332
|
+
* parts goes through the operations captured at load: a replaced `update` changed the four bytes a
|
|
333
|
+
* line is matched on, and a note a key really did sign then reported `verified: false`. */
|
|
334
|
+
function _idOf(parts) { return _subarray(guard.crypto.digestParts("sha256", parts), 0, KEY_ID_BYTES); }
|
|
335
|
+
|
|
336
|
+
/** @internal The offset one past the text's terminating newline, which is where the blank line
|
|
337
|
+
* begins. The text runs to the first "\n\n", so the signed range ENDS with that first newline and
|
|
338
|
+
* the second one opens the signature block. Returns -1 when the note carries no blank line. */
|
|
339
|
+
function _textEnd(buf) {
|
|
340
|
+
/** @internal Scanned from the END. The specification says "the note text MAY contain empty lines;
|
|
341
|
+
* the text is separated from the signatures by the LAST empty line in the note", so taking the
|
|
342
|
+
* first would make the signed range a PREFIX of what the signer signed: the remainder would then
|
|
343
|
+
* be read as signature lines and a conforming note refused, and a verifier that did accept the
|
|
344
|
+
* prefix would be checking a signature over less text than the log committed to. */
|
|
345
|
+
for (var i = buf.length - 2; i >= 0; i--) {
|
|
346
|
+
if (buf[i] === LF && buf[i + 1] === LF) return i + 1;
|
|
347
|
+
}
|
|
348
|
+
return -1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @primitive pki.tlog.parseNote
|
|
353
|
+
* @signature pki.tlog.parseNote(input) -> { text, signedBytes, signatures }
|
|
354
|
+
* @since 0.8.33
|
|
355
|
+
* @status stable
|
|
356
|
+
* @spec C2SP signed-note
|
|
357
|
+
* @defends signature-bypass (CWE-347)
|
|
358
|
+
* @related pki.tlog.verifyNote, pki.tlog.parseCheckpoint
|
|
359
|
+
*
|
|
360
|
+
* Read a signed note: the text, the exact bytes a signature covers, and one entry per signature
|
|
361
|
+
* line carrying `keyName`, the four-byte `keyId` and the `signature` after it.
|
|
362
|
+
*
|
|
363
|
+
* `signedBytes` is a view of the input, not a re-serialization. The signature covers the text
|
|
364
|
+
* including its trailing newline, so a parser that rebuilt that text from its parsed lines would
|
|
365
|
+
* verify a note against bytes the signer never saw. Everything a verifier hashes comes from here.
|
|
366
|
+
*
|
|
367
|
+
* A note carrying an ASCII control byte other than newline, no blank line, a signature line that is
|
|
368
|
+
* not an em dash, a space, a key name, a space and canonical base64, a signature shorter than its
|
|
369
|
+
* key ID, or more signatures than `C.LIMITS.TLOG_MAX_SIGNATURES` is refused with `tlog/bad-note`.
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* var DASH = String.fromCharCode(0x2014); // the em dash a signature line opens with
|
|
373
|
+
* var noteText = "example.com/log\n5\n" + Buffer.alloc(32).toString("base64") + "\n" +
|
|
374
|
+
* "\n" + DASH + " example.com/log " + Buffer.alloc(68).toString("base64") + "\n";
|
|
375
|
+
* var note = pki.tlog.parseNote(noteText);
|
|
376
|
+
* note.signatures[0].keyName; // -> "example.com/log"
|
|
377
|
+
* note.signedBytes.length === Buffer.byteLength(note.text); // -> true
|
|
378
|
+
*/
|
|
379
|
+
function parseNote(input) {
|
|
380
|
+
var buf = _bytes(input, "note", constants.LIMITS.TLOG_MAX_NOTE_BYTES);
|
|
381
|
+
/** @internal The WHOLE note is decoded strictly before any of it is read as text. C2SP signed-note is
|
|
382
|
+
* UTF-8, and a lossy conversion replaces a malformed byte with U+FFFD: a note beginning 0xFF verified
|
|
383
|
+
* with its origin reported as U+FFFD, and the text reported did not encode back to the bytes signed. */
|
|
384
|
+
guard.text.decode(buf, constants.LIMITS.TLOG_MAX_NOTE_BYTES, TlogError, {
|
|
385
|
+
charset: "utf-8", fatal: true, tooLarge: "tlog/bad-input", badDecode: "tlog/bad-note",
|
|
386
|
+
badInput: "tlog/bad-input", label: "the note",
|
|
387
|
+
});
|
|
388
|
+
_assertNoteText(buf);
|
|
389
|
+
var end = _textEnd(buf);
|
|
390
|
+
if (end < 0) throw _err("tlog/bad-note", "a note separates its text from its signatures with a blank line");
|
|
391
|
+
var signedBytes = _subarray(buf, 0, end);
|
|
392
|
+
var rest = _bufToString(_subarray(buf, end + 1), "utf8");
|
|
393
|
+
var signatures = [];
|
|
394
|
+
var lines = rest.length === 0 ? [] : _split(rest, "\n");
|
|
395
|
+
/** @internal A note ends with a newline, so the split leaves one empty tail element; anything
|
|
396
|
+
* else after it is a line the format does not have. The drop goes through the capture: dispatching
|
|
397
|
+
* through `Array.prototype.pop` let a replacement pop TWICE, taking the last signature line with the
|
|
398
|
+
* tail element, and a forged signature under a known key was then never checked. */
|
|
399
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") _pop(lines);
|
|
400
|
+
else if (lines.length > 0) throw _err("tlog/bad-note", "a note's last signature line ends with a newline");
|
|
401
|
+
if (lines.length > constants.LIMITS.TLOG_MAX_SIGNATURES) {
|
|
402
|
+
throw _err("tlog/bad-note", lines.length + " signature lines exceeds the cap " +
|
|
403
|
+
constants.LIMITS.TLOG_MAX_SIGNATURES);
|
|
404
|
+
}
|
|
405
|
+
for (var i = 0; i < lines.length; i++) {
|
|
406
|
+
_push(signatures, _parseSignatureLine(lines[i]));
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
text: _bufToString(signedBytes, "utf8"),
|
|
410
|
+
signedBytes: signedBytes,
|
|
411
|
+
signatures: signatures,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** @internal One signature line: the em-dash prefix, a key name with no space in it, a space, and
|
|
416
|
+
* canonical base64 whose first four bytes are the key ID. */
|
|
417
|
+
function _parseSignatureLine(line) {
|
|
418
|
+
if (_strIndexOf(line, SIG_LINE_PREFIX) !== 0) {
|
|
419
|
+
throw _err("tlog/bad-note", "a signature line begins with an em dash and a space");
|
|
420
|
+
}
|
|
421
|
+
var body = _strSlice(line, SIG_LINE_PREFIX.length);
|
|
422
|
+
var space = _strIndexOf(body, " ");
|
|
423
|
+
if (space <= 0 || space === body.length - 1) {
|
|
424
|
+
throw _err("tlog/bad-note", "a signature line is an em dash, a space, a key name, a space, and base64");
|
|
425
|
+
}
|
|
426
|
+
var name = _strSlice(body, 0, space);
|
|
427
|
+
var encoded = _strSlice(body, space + 1);
|
|
428
|
+
if (_strIndexOf(encoded, " ") !== -1) {
|
|
429
|
+
throw _err("tlog/bad-note", "a signature line carries one key name and one base64 field");
|
|
430
|
+
}
|
|
431
|
+
var raw = _base64(encoded, "a signature");
|
|
432
|
+
if (raw.length <= KEY_ID_BYTES) {
|
|
433
|
+
throw _err("tlog/bad-note", "a signature carries a " + KEY_ID_BYTES +
|
|
434
|
+
"-byte key id and a signature after it, got " + raw.length + " bytes");
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
keyName: name,
|
|
438
|
+
keyId: _subarray(raw, 0, KEY_ID_BYTES),
|
|
439
|
+
signature: _subarray(raw, KEY_ID_BYTES),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @primitive pki.tlog.verifyNote
|
|
445
|
+
* @signature pki.tlog.verifyNote(input, keys) -> Promise<{ verified, signers, note }>
|
|
446
|
+
* @since 0.8.33
|
|
447
|
+
* @status stable
|
|
448
|
+
* @spec C2SP signed-note, RFC 8032
|
|
449
|
+
* @defends signature-bypass (CWE-347)
|
|
450
|
+
* @related pki.tlog.parseNote, pki.tlog.verifyCheckpoint
|
|
451
|
+
*
|
|
452
|
+
* Verify a signed note against a list of `{ name, publicKey }` keys, where `publicKey` is the raw
|
|
453
|
+
* 32 Ed25519 bytes or a SubjectPublicKeyInfo for any algorithm `pki.tlog.keyId` derives an ID for.
|
|
454
|
+
* Resolves `{ verified, signers, note }`: `signers` is one entry per signature that verified under a
|
|
455
|
+
* supplied key, and `verified` is whether there was at least one.
|
|
456
|
+
*
|
|
457
|
+
* The specification states two separate rules about failure and this draws the line between them:
|
|
458
|
+
*
|
|
459
|
+
* - A signature from a key not in the list is **ignored**, which is what a verifier must do with a
|
|
460
|
+
* co-signature it does not know. It carries no claim to check.
|
|
461
|
+
* - A note carrying no signature from a supplied key resolves **`verified: false`** without
|
|
462
|
+
* throwing, because that is a verdict about the note and not a fault in it.
|
|
463
|
+
* - A signature line naming a supplied key by BOTH name and ID, which no such key verifies,
|
|
464
|
+
* **rejects the whole note** with `tlog/bad-signature`. Reporting another line's success instead
|
|
465
|
+
* would let a forged line from a known signer pass unnoticed behind a valid one.
|
|
466
|
+
*
|
|
467
|
+
* Every supplied key whose ID matches is tried before that refusal. A key ID is four bytes and
|
|
468
|
+
* identifies rather than proves, so stopping at the first candidate would reject a note a later key
|
|
469
|
+
* with the same ID verifies.
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
473
|
+
* var raw = pki.asn1.decode(await pki.key.export(pair.publicKey)).children[1].content.subarray(1);
|
|
474
|
+
* var text = "example.com/log\n0\n" + pki.merkle.emptyRootHash().toString("base64") + "\n";
|
|
475
|
+
* var sig = Buffer.from(await pki.webcrypto.subtle.sign({ name: "Ed25519" }, pair.privateKey,
|
|
476
|
+
* Buffer.from(text, "utf8")));
|
|
477
|
+
* var line = String.fromCharCode(0x2014) + " example.com/log " +
|
|
478
|
+
* Buffer.concat([pki.tlog.keyId("example.com/log", raw), sig]).toString("base64");
|
|
479
|
+
* var v = await pki.tlog.verifyNote(text + "\n" + line + "\n",
|
|
480
|
+
* [{ name: "example.com/log", publicKey: raw }]);
|
|
481
|
+
* v.verified; // -> true
|
|
482
|
+
*/
|
|
483
|
+
async function verifyNote(input, keys) {
|
|
484
|
+
/** @internal Each caller key's `name` and `publicKey` are read ONCE, into the candidate the rest of this
|
|
485
|
+
* function uses. They were read by the validator, again to resolve the key, and again to record the
|
|
486
|
+
* candidate's name, and a member reached through an accessor answers every read separately: the name the
|
|
487
|
+
* identifier was derived under need not be the name a signature line is matched against. `_resolveKey`
|
|
488
|
+
* copies the key bytes, so what the candidate holds cannot change afterwards either. */
|
|
489
|
+
if (!_isArray(keys)) throw _err("tlog/bad-input", "keys must be an array");
|
|
490
|
+
/** @internal The note is copied BEFORE any key record is read. A key's `name` or `publicKey` reached
|
|
491
|
+
* through an accessor runs while the caller's buffer is still the caller's, and it can overwrite the
|
|
492
|
+
* note with another one: the verdict then reports on a document that was not the argument. Measured,
|
|
493
|
+
* a getter that copied a validly signed note over an invalidly signed one turned `tlog/bad-signature`
|
|
494
|
+
* into `verified`. Reading each key once bounds what a key can say about ITSELF and says nothing
|
|
495
|
+
* about what reading it can do to the subject, so the subject is taken first. */
|
|
496
|
+
var noteBytes = _bytesSnapshot(input, "note", constants.LIMITS.TLOG_MAX_NOTE_BYTES);
|
|
497
|
+
var candidates = [];
|
|
498
|
+
for (var k = 0; k < keys.length; k++) {
|
|
499
|
+
var entry = keys[k];
|
|
500
|
+
if (entry === null || typeof entry !== "object") throw _err("tlog/bad-input", "each key is an object");
|
|
501
|
+
var kName = entry.name, kPublic = entry.publicKey;
|
|
502
|
+
if (typeof kName !== "string" || kName.length === 0) {
|
|
503
|
+
throw _err("tlog/bad-input", "each key carries a non-empty name");
|
|
504
|
+
}
|
|
505
|
+
if (kPublic === undefined || kPublic === null) {
|
|
506
|
+
throw _err("tlog/bad-input", "each key carries a publicKey");
|
|
507
|
+
}
|
|
508
|
+
var resolved = _resolveKey(kName, kPublic);
|
|
509
|
+
_push(candidates, { name: kName, resolved: resolved, id: resolved.id });
|
|
510
|
+
}
|
|
511
|
+
/** @internal Parsed from the snapshot taken above, so the signed range this holds cannot change under
|
|
512
|
+
* the key import the verification awaits. */
|
|
513
|
+
var note = parseNote(noteBytes);
|
|
514
|
+
var signers = [];
|
|
515
|
+
for (var s = 0; s < note.signatures.length; s++) {
|
|
516
|
+
var sig = note.signatures[s];
|
|
517
|
+
var matched = false, lineVerified = false;
|
|
518
|
+
for (var c = 0; c < candidates.length; c++) {
|
|
519
|
+
var cand = candidates[c];
|
|
520
|
+
if (cand.name !== sig.keyName || _bufferCompare(cand.id, sig.keyId) !== 0) continue;
|
|
521
|
+
/** @internal Every matching candidate is tried rather than the first: a four-byte id can be
|
|
522
|
+
* shared, and reporting the first one's failure would refuse a note a later key signed. */
|
|
523
|
+
matched = true;
|
|
524
|
+
if (await _verifyUnder(cand.resolved, sig.signature, note.signedBytes)) { lineVerified = true; break; }
|
|
525
|
+
}
|
|
526
|
+
/** @internal A line naming a key the caller supplied, by BOTH name and id, that no such key
|
|
527
|
+
* verifies rejects the whole note. Skipping it and reporting another line's success would let a
|
|
528
|
+
* forged line from a known signer pass unnoticed behind a valid one. A line naming a key the
|
|
529
|
+
* caller did not supply is ignored, which is a different case: it carries no claim to check. */
|
|
530
|
+
if (matched && !lineVerified) {
|
|
531
|
+
throw _err("tlog/bad-signature", "the note carries a signature from " +
|
|
532
|
+
guard.text.showValue(sig.keyName) + " with a matching key id that does not verify");
|
|
533
|
+
}
|
|
534
|
+
if (lineVerified) _push(signers, { keyName: sig.keyName, keyId: sig.keyId });
|
|
535
|
+
}
|
|
536
|
+
return guard.verdict.of({ verified: signers.length > 0, signers: signers, note: note });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** @internal An Ed25519 verify over the raw public key, which the engine takes as a
|
|
540
|
+
* SubjectPublicKeyInfo: the 12 bytes below are the fixed header of an Ed25519 one, so the key
|
|
541
|
+
* becomes an SPKI by concatenation, with no builder involved.
|
|
542
|
+
*
|
|
543
|
+
* An import failure is NOT caught. A key the caller supplied that does not import is a caller
|
|
544
|
+
* fault, and turning it into `false` would report "this note is not signed by you" for a key that
|
|
545
|
+
* was never usable. Only the verify itself answers false, which is a verdict about the signature. */
|
|
546
|
+
var ED25519_SPKI_PREFIX = _bufferFrom([0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00]);
|
|
547
|
+
var ED25519_CRV = 6;
|
|
548
|
+
var ED25519_ALG = _assign(_create(null), { name: "Ed25519" });
|
|
549
|
+
/** @internal The verify for a resolved key, under the algorithm its material named. An ECDSA
|
|
550
|
+
* signature is DER, which is what a log writes, and the digest comes from the signature TYPE rather
|
|
551
|
+
* than from the curve, so a P-384 and a P-521 log key are both checked with SHA-256. */
|
|
552
|
+
async function _verifyUnder(resolved, signature, message) {
|
|
553
|
+
if (resolved.type === ED25519_ALG_ID) return await _verifyEd25519(resolved.raw, signature, message);
|
|
554
|
+
if (resolved.type === ECDSA_ALG_ID) {
|
|
555
|
+
return guard.crypto.verify(resolved.hash, message, { key: resolved.key, dsaEncoding: "der" }, signature);
|
|
556
|
+
}
|
|
557
|
+
return guard.crypto.verify(resolved.hash, message, resolved.key, signature);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function _verifyEd25519(raw, signature, message) {
|
|
561
|
+
var spki = _bufferConcat([ED25519_SPKI_PREFIX, raw]);
|
|
562
|
+
/** @internal The shared Edwards-point gate runs before the key reaches a verify. Node imports a
|
|
563
|
+
* low-order key without complaint and such a key verifies a FORGED signature, so a caller whose
|
|
564
|
+
* trusted key list picked one up would have every note verify under it. */
|
|
565
|
+
edwardsPoint.validateSpki(spki, ED25519_CRV, TlogError, "tlog/bad-input");
|
|
566
|
+
var key;
|
|
567
|
+
try {
|
|
568
|
+
key = await _subtleImportKey(subtle, "spki", spki, ED25519_ALG, true, ["verify"]);
|
|
569
|
+
} catch (e) {
|
|
570
|
+
throw _err("tlog/bad-input", "a supplied publicKey is not an Ed25519 key", e);
|
|
571
|
+
}
|
|
572
|
+
return await _subtleVerify(subtle, ED25519_ALG, key, signature, message);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* @primitive pki.tlog.parseCheckpoint
|
|
577
|
+
* @signature pki.tlog.parseCheckpoint(input) -> { origin, treeSize, rootHash, extensions, note }
|
|
578
|
+
* @since 0.8.33
|
|
579
|
+
* @status stable
|
|
580
|
+
* @spec C2SP tlog-checkpoint, RFC 6962
|
|
581
|
+
* @related pki.tlog.verifyCheckpoint, pki.merkle.verifyInclusion
|
|
582
|
+
*
|
|
583
|
+
* Read the checkpoint a log signed into a note: the `origin` that names the log, the `treeSize` as
|
|
584
|
+
* a `BigInt`, the 32-byte `rootHash` ready for `pki.merkle`, any opaque `extensions`, and the
|
|
585
|
+
* `note` beneath, whose `signedBytes` remain reachable for verification.
|
|
586
|
+
*
|
|
587
|
+
* The origin is not validated as a URL. The specification recommends a schema-less one and then
|
|
588
|
+
* says outright that a client must not assume the origin follows that shape or names a reachable
|
|
589
|
+
* endpoint, so only an empty origin is refused.
|
|
590
|
+
*
|
|
591
|
+
* A tree size with a leading zero, a root hash that is not 32 bytes, an empty extension line, or a
|
|
592
|
+
* body of fewer than three lines is refused with `tlog/bad-checkpoint`.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* var DASH = String.fromCharCode(0x2014);
|
|
596
|
+
* var checkpointText = "example.com/log\n5\n" + Buffer.alloc(32, 1).toString("base64") + "\n" +
|
|
597
|
+
* "\n" + DASH + " example.com/log " + Buffer.alloc(68).toString("base64") + "\n";
|
|
598
|
+
* var cp = pki.tlog.parseCheckpoint(checkpointText);
|
|
599
|
+
* cp.treeSize; // -> 5n
|
|
600
|
+
* cp.rootHash.length; // -> 32, ready for pki.merkle.verifyInclusion
|
|
601
|
+
*/
|
|
602
|
+
function parseCheckpoint(input) {
|
|
603
|
+
var note = parseNote(input);
|
|
604
|
+
var text = note.text;
|
|
605
|
+
/** @internal The text ends with a newline, so the split leaves an empty tail that is not a line. */
|
|
606
|
+
var lines = _split(text, "\n");
|
|
607
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") _pop(lines);
|
|
608
|
+
if (lines.length < MIN_CHECKPOINT_LINES) {
|
|
609
|
+
throw _err("tlog/bad-checkpoint", "a checkpoint body is an origin, a tree size and a root hash, got " +
|
|
610
|
+
lines.length + " line(s)");
|
|
611
|
+
}
|
|
612
|
+
var origin = lines[0];
|
|
613
|
+
if (origin.length === 0) throw _err("tlog/bad-checkpoint", "the origin line is non-empty");
|
|
614
|
+
var treeSize = _decimal(lines[1], "the tree size", "tlog/bad-checkpoint");
|
|
615
|
+
var rootHash;
|
|
616
|
+
try { rootHash = _base64(lines[2], "the root hash"); }
|
|
617
|
+
catch (e) { throw _err("tlog/bad-checkpoint", "the root hash is not canonical base64", e); }
|
|
618
|
+
if (rootHash.length !== SHA256_BYTES) {
|
|
619
|
+
throw _err("tlog/bad-checkpoint", "the root hash is " + SHA256_BYTES + " bytes, got " + rootHash.length);
|
|
620
|
+
}
|
|
621
|
+
var extensions = _arrSlice(lines, MIN_CHECKPOINT_LINES);
|
|
622
|
+
if (extensions.length > constants.LIMITS.TLOG_MAX_EXTENSION_LINES) {
|
|
623
|
+
throw _err("tlog/bad-checkpoint", extensions.length + " extension lines exceeds the cap " +
|
|
624
|
+
constants.LIMITS.TLOG_MAX_EXTENSION_LINES);
|
|
625
|
+
}
|
|
626
|
+
/** @internal "Extension lines, if any, MUST be non-empty." A note's text is separated from its
|
|
627
|
+
* signatures at the LAST empty line, so an empty line where an extension would sit is inside the
|
|
628
|
+
* text and reaches here as an empty extension. Reading it as one would let a checkpoint carry a
|
|
629
|
+
* line the specification forbids. */
|
|
630
|
+
for (var x = 0; x < extensions.length; x++) {
|
|
631
|
+
if (extensions[x].length === 0) {
|
|
632
|
+
throw _err("tlog/bad-checkpoint", "extension line " + (x + 1) + " is empty, and an extension " +
|
|
633
|
+
"line must not be");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return { origin: origin, treeSize: treeSize, rootHash: rootHash, extensions: extensions, note: note };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* @primitive pki.tlog.verifyCheckpoint
|
|
641
|
+
* @signature pki.tlog.verifyCheckpoint(input, keys, opts?) -> Promise<{ verified, signers, checkpoint }>
|
|
642
|
+
* @since 0.8.33
|
|
643
|
+
* @status stable
|
|
644
|
+
* @spec C2SP tlog-checkpoint
|
|
645
|
+
* @defends signature-bypass (CWE-347)
|
|
646
|
+
* @related pki.tlog.verifyNote, pki.merkle.verifyInclusion
|
|
647
|
+
*
|
|
648
|
+
* Verify a checkpoint's signatures and read its body in one call: `pki.tlog.verifyNote` over the
|
|
649
|
+
* note, and `pki.tlog.parseCheckpoint` over the text it signed. A `verified: false` result still
|
|
650
|
+
* carries the parsed `checkpoint`, since a caller auditing a log needs to see what was claimed.
|
|
651
|
+
*
|
|
652
|
+
* The root hash it returns is what `pki.merkle.verifyInclusion` checks a proof against, and that is
|
|
653
|
+
* the whole point of the pairing: a proof against an unverified root proves nothing.
|
|
654
|
+
*
|
|
655
|
+
* @example
|
|
656
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
657
|
+
* var raw = pki.asn1.decode(await pki.key.export(pair.publicKey)).children[1].content.subarray(1);
|
|
658
|
+
* var leaves = [pki.merkle.leafHash(Buffer.from("a")), pki.merkle.leafHash(Buffer.from("b"))];
|
|
659
|
+
* var body = "example.com/log\n2\n" +
|
|
660
|
+
* pki.merkle.nodeHash(leaves[0], leaves[1]).toString("base64") + "\n";
|
|
661
|
+
* var sig = Buffer.from(await pki.webcrypto.subtle.sign({ name: "Ed25519" }, pair.privateKey,
|
|
662
|
+
* Buffer.from(body, "utf8")));
|
|
663
|
+
* var note = body + "\n" + String.fromCharCode(0x2014) + " example.com/log " +
|
|
664
|
+
* Buffer.concat([pki.tlog.keyId("example.com/log", raw), sig]).toString("base64") + "\n";
|
|
665
|
+
* var v = await pki.tlog.verifyCheckpoint(note, [{ name: "example.com/log", publicKey: raw }]);
|
|
666
|
+
* v.verified && pki.merkle.verifyInclusion({ leafHash: leaves[0], leafIndex: 0n,
|
|
667
|
+
* treeSize: v.checkpoint.treeSize, rootHash: v.checkpoint.rootHash, proof: [leaves[1]] });
|
|
668
|
+
* // -> true
|
|
669
|
+
*/
|
|
670
|
+
var _CHECKPOINT_OPTS = _assign(_create(null), { origin: 1 });
|
|
671
|
+
async function verifyCheckpoint(input, keys, opts) {
|
|
672
|
+
opts = opts || {};
|
|
673
|
+
guard.identifier.assertKnownKeys(opts, _CHECKPOINT_OPTS, _err,
|
|
674
|
+
"tlog/bad-input", "unknown verifyCheckpoint option ");
|
|
675
|
+
/** @internal ONE read of the caller's input, shared by the decode and the verification below. Reading
|
|
676
|
+
* it twice let the checkpoint be decoded from one document and its signature checked over another,
|
|
677
|
+
* since the verification awaits a key import and the caller's buffer can change in that window. */
|
|
678
|
+
var bytes = _bytesSnapshot(input, "note", constants.LIMITS.TLOG_MAX_NOTE_BYTES);
|
|
679
|
+
var checkpoint = parseCheckpoint(bytes);
|
|
680
|
+
/** @internal ONE read of the pinned origin, which the checks and the comparison both read. An accessor
|
|
681
|
+
* answers every read separately, and this was read four times: whether it was supplied, its type, its
|
|
682
|
+
* length, and the comparison. A getter could therefore pass all three checks as one value and be
|
|
683
|
+
* compared as another, so the origin the caller pinned was not the origin enforced and the verdict
|
|
684
|
+
* reported a match against a log the checkpoint does not name. */
|
|
685
|
+
var expectedOrigin = opts.origin;
|
|
686
|
+
if (expectedOrigin !== undefined) {
|
|
687
|
+
if (typeof expectedOrigin !== "string" || expectedOrigin.length === 0) {
|
|
688
|
+
throw _err("tlog/bad-input", "opts.origin must be a non-empty string");
|
|
689
|
+
}
|
|
690
|
+
/** @internal A valid signature from one log over its own tree is not evidence about another
|
|
691
|
+
* log's tree, and with more than one log pinned that is the whole difference. The expected
|
|
692
|
+
* origin is the caller's value and never the key name off the signature line: a Rekor v1 origin
|
|
693
|
+
* appends the tree ID after a space, which a key name may not contain, so reading one from the
|
|
694
|
+
* other would refuse every v1 checkpoint. */
|
|
695
|
+
if (checkpoint.origin !== expectedOrigin) {
|
|
696
|
+
throw _err("tlog/origin-mismatch", "the checkpoint states origin " +
|
|
697
|
+
guard.text.showValue(checkpoint.origin) + " where the caller pinned " +
|
|
698
|
+
guard.text.showValue(expectedOrigin));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
var v = await verifyNote(bytes, keys);
|
|
702
|
+
return guard.verdict.of({ verified: v.verified, signers: v.signers, checkpoint: checkpoint });
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* @primitive pki.tlog.parseVkey
|
|
707
|
+
* @signature pki.tlog.parseVkey(text) -> { name, keyId, signatureType, publicKey }
|
|
708
|
+
* @since 0.8.37
|
|
709
|
+
* @status stable
|
|
710
|
+
* @spec C2SP signed-note
|
|
711
|
+
* @related pki.tlog.keyId, pki.tlog.verifyNote
|
|
712
|
+
*
|
|
713
|
+
* Read the verifier-key text form a log publishes to identify itself,
|
|
714
|
+
* `<name>+<hex key ID>+<base64 of signature type || public key>`, as in
|
|
715
|
+
* `example.com/foo+530d903a+Aeky...`. Returns the name, the four-byte `keyId`, the `signatureType`
|
|
716
|
+
* byte, and the `publicKey`: the raw 32 bytes for Ed25519, or the SubjectPublicKeyInfo for ECDSA and
|
|
717
|
+
* RSA. The `publicKey` is what `verifyNote` takes for that key.
|
|
718
|
+
*
|
|
719
|
+
* The stated key ID is checked against the one the key material derives, and the stated signature
|
|
720
|
+
* type against the algorithm that material actually is, so a vkey cannot name a key it does not
|
|
721
|
+
* carry or claim an algorithm its key is not. A key name may not contain a plus, so the name ends at
|
|
722
|
+
* the first separator; a text with three of them is refused rather than read as a name containing
|
|
723
|
+
* one.
|
|
724
|
+
*
|
|
725
|
+
* @example
|
|
726
|
+
* var v = pki.tlog.parseVkey("example.com/foo+530d903a+AekyeRrm56hApGFkyQR4ZCbV54Id2LKaANYcrnKv3U2k");
|
|
727
|
+
* v.name; // "example.com/foo"
|
|
728
|
+
* v.keyId.toString("hex"); // "530d903a"
|
|
729
|
+
* v.signatureType; // 1
|
|
730
|
+
*/
|
|
731
|
+
function parseVkey(text) {
|
|
732
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
733
|
+
throw _err("tlog/bad-input", "a verifier key is a non-empty string");
|
|
734
|
+
}
|
|
735
|
+
var first = _strIndexOf(text, "+");
|
|
736
|
+
if (first <= 0) {
|
|
737
|
+
throw _err("tlog/bad-input", "a verifier key is <name>+<hex key id>+<base64 key>, and the name " +
|
|
738
|
+
"must be non-empty");
|
|
739
|
+
}
|
|
740
|
+
var second = _strIndexOf(text, "+", first + 1);
|
|
741
|
+
if (second < 0) throw _err("tlog/bad-input", "a verifier key carries two plus separators");
|
|
742
|
+
/** @internal Only the FIRST TWO pluses are separators, and a later one belongs to the key material:
|
|
743
|
+
* standard base64's alphabet includes `+`, and a 44-character Ed25519 blob carries one more often than
|
|
744
|
+
* not, so refusing a third plus rejects most real verifier keys. A name containing a plus is still
|
|
745
|
+
* unparseable, which is what the format requires: everything between the first two separators has to
|
|
746
|
+
* be the eight hex digits of a key ID, and a name fragment is not. The blob is left to the base64
|
|
747
|
+
* decoder, which is what decides whether the remaining field is well formed. */
|
|
748
|
+
var name = _strSlice(text, 0, first);
|
|
749
|
+
var hexId = _strSlice(text, first + 1, second);
|
|
750
|
+
var blobText = _strSlice(text, second + 1);
|
|
751
|
+
if (hexId.length !== KEY_ID_BYTES * 2) {
|
|
752
|
+
throw _err("tlog/bad-input", "a verifier key states its key ID as " + (KEY_ID_BYTES * 2) +
|
|
753
|
+
" hex digits, got " + hexId.length);
|
|
754
|
+
}
|
|
755
|
+
var stated = guard.encoding.hex(hexId, KEY_ID_BYTES, _err, "tlog/bad-input", "the stated key ID");
|
|
756
|
+
if (blobText.length === 0) throw _err("tlog/bad-input", "a verifier key carries key material");
|
|
757
|
+
var blob = guard.encoding.base64(blobText, constants.LIMITS.TLOG_MAX_NOTE_BYTES, _err,
|
|
758
|
+
"tlog/bad-input", "the verifier key material");
|
|
759
|
+
if (blob.length < 2) throw _err("tlog/bad-input", "a verifier key is a signature type byte then a key");
|
|
760
|
+
var type = blob[0];
|
|
761
|
+
var publicKey = _subarray(blob, 1);
|
|
762
|
+
var resolved = _resolveKey(name, publicKey);
|
|
763
|
+
if (resolved.type !== type) {
|
|
764
|
+
throw _err("tlog/bad-input", "the verifier key states signature type " + type +
|
|
765
|
+
" where its key material is type " + resolved.type);
|
|
766
|
+
}
|
|
767
|
+
if (_bufferCompare(resolved.id, stated) !== 0) {
|
|
768
|
+
throw _err("tlog/bad-input", "the verifier key states key ID " + _bufToString(stated, "hex") +
|
|
769
|
+
" where its own key material derives " + _bufToString(resolved.id, "hex"));
|
|
770
|
+
}
|
|
771
|
+
return { name: name, keyId: resolved.id, signatureType: type, publicKey: publicKey };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function _assertLevel(level) {
|
|
775
|
+
if (typeof level !== "number" || !_isInteger(level) || level < 0 || level > TILE_MAX_LEVEL) {
|
|
776
|
+
throw _err("tlog/bad-tile", "a tile level is an integer 0 to " + TILE_MAX_LEVEL + ", got " + level);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function _assertWidth(width) {
|
|
781
|
+
if (width === undefined || width === null) return null;
|
|
782
|
+
if (typeof width !== "number" || !_isInteger(width) || width < 1 || width > TILE_HEIGHT_HASHES - 1) {
|
|
783
|
+
throw _err("tlog/bad-tile", "a partial tile width is an integer 1 to " + (TILE_HEIGHT_HASHES - 1) +
|
|
784
|
+
", got " + width);
|
|
785
|
+
}
|
|
786
|
+
return width;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** @internal The index path: zero-padded three-digit segments, every one but the last carrying an
|
|
790
|
+
* `x`. The specification's own example is 1234067 -> x001/x234/067. Built from the low digits up,
|
|
791
|
+
* so an index of any size produces the right number of segments. */
|
|
792
|
+
/** @internal How many three-digit segments the largest index a writer emits occupies, DERIVED from the
|
|
793
|
+
* writer itself rather than written down: a uint64 index is at most 20 decimal digits, and the segments
|
|
794
|
+
* carry three each. Deriving it means the reader's bound cannot drift from the writer's range. */
|
|
795
|
+
var MAX_INDEX_SEGMENTS = (function () {
|
|
796
|
+
var n = 18446744073709551615n, count = 1;
|
|
797
|
+
while (n >= 1000n) { n = n / 1000n; count += 1; }
|
|
798
|
+
return count;
|
|
799
|
+
})();
|
|
800
|
+
|
|
801
|
+
function _indexPath(index) {
|
|
802
|
+
var n = guard.range.uint64(index, _err, "tlog/bad-tile", "a tile index");
|
|
803
|
+
var segments = [];
|
|
804
|
+
var rest = n;
|
|
805
|
+
var thousand = 1000n;
|
|
806
|
+
for (;;) {
|
|
807
|
+
var part = rest % thousand;
|
|
808
|
+
var text = _stringOf(part);
|
|
809
|
+
while (text.length < TILE_SEGMENT_DIGITS) text = "0" + text;
|
|
810
|
+
_unshift(segments, text);
|
|
811
|
+
rest = rest / thousand;
|
|
812
|
+
if (rest === 0n) break;
|
|
813
|
+
}
|
|
814
|
+
for (var i = 0; i < segments.length - 1; i++) segments[i] = "x" + segments[i];
|
|
815
|
+
return _join(segments, "/");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* @primitive pki.tlog.tilePath
|
|
820
|
+
* @signature pki.tlog.tilePath(level, index, width?) -> string
|
|
821
|
+
* @since 0.8.33
|
|
822
|
+
* @status stable
|
|
823
|
+
* @spec C2SP tlog-tiles
|
|
824
|
+
* @related pki.tlog.parseTilePath, pki.tlog.entryBundlePath
|
|
825
|
+
*
|
|
826
|
+
* The path a tiled log serves one Merkle tile at, relative to the log's prefix:
|
|
827
|
+
* `tile/<level>/<index>` with the index in zero-padded three-digit segments, every one but the last
|
|
828
|
+
* carrying an `x`. A `width` of 1 to 255 appends the `.p/<width>` suffix a partial tile has.
|
|
829
|
+
*
|
|
830
|
+
* The segmented form is what lets a log hold its tiles in a filesystem without a directory and a
|
|
831
|
+
* file colliding on one name.
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
* pki.tlog.tilePath(0, 1234067n); // "tile/0/x001/x234/067"
|
|
835
|
+
* pki.tlog.tilePath(0, 1234067n, 42); // "tile/0/x001/x234/067.p/42"
|
|
836
|
+
*/
|
|
837
|
+
function tilePath(level, index, width) {
|
|
838
|
+
_assertLevel(level);
|
|
839
|
+
var w = _assertWidth(width);
|
|
840
|
+
return "tile/" + level + "/" + _indexPath(index) + (w === null ? "" : ".p/" + w);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* @primitive pki.tlog.entryBundlePath
|
|
845
|
+
* @signature pki.tlog.entryBundlePath(index, width?) -> string
|
|
846
|
+
* @since 0.8.33
|
|
847
|
+
* @status stable
|
|
848
|
+
* @spec C2SP tlog-tiles
|
|
849
|
+
* @related pki.tlog.parseEntryBundle
|
|
850
|
+
*
|
|
851
|
+
* The path a tiled log serves one entry bundle at: `tile/entries/<index>`, with the same segmented
|
|
852
|
+
* index and `.p/<width>` suffix a tile path uses. The bundle holds the entries whose leaf hashes
|
|
853
|
+
* are the level-0 tile at the same index.
|
|
854
|
+
*
|
|
855
|
+
* @example
|
|
856
|
+
* pki.tlog.entryBundlePath(1234067n); // "tile/entries/x001/x234/067"
|
|
857
|
+
*/
|
|
858
|
+
function entryBundlePath(index, width) {
|
|
859
|
+
var w = _assertWidth(width);
|
|
860
|
+
return "tile/entries/" + _indexPath(index) + (w === null ? "" : ".p/" + w);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* @primitive pki.tlog.checkpointPath
|
|
865
|
+
* @signature pki.tlog.checkpointPath() -> string
|
|
866
|
+
* @since 0.8.33
|
|
867
|
+
* @status stable
|
|
868
|
+
* @spec C2SP tlog-tiles
|
|
869
|
+
* @related pki.tlog.verifyCheckpoint
|
|
870
|
+
*
|
|
871
|
+
* The path a tiled log serves its checkpoint at, relative to the log's prefix. Fixed by the
|
|
872
|
+
* specification, and given as a verb so a caller composes a URL without writing the literal.
|
|
873
|
+
*
|
|
874
|
+
* @example
|
|
875
|
+
* pki.tlog.checkpointPath(); // "checkpoint"
|
|
876
|
+
*/
|
|
877
|
+
function checkpointPath() { return "checkpoint"; }
|
|
878
|
+
|
|
879
|
+
/** @internal Three ASCII digits, optionally behind an `x`. Scanned by character code, not
|
|
880
|
+
* matched, and the `x` is required on every segment but the last. */
|
|
881
|
+
function _segmentDigits(segment, expectX) {
|
|
882
|
+
var body = segment;
|
|
883
|
+
if (expectX) {
|
|
884
|
+
if (_charCodeAt(body, 0) !== 0x78) throw _err("tlog/bad-tile", "a leading index segment begins with x");
|
|
885
|
+
body = _strSlice(body, 1);
|
|
886
|
+
} else if (_charCodeAt(segment, 0) === 0x78) {
|
|
887
|
+
throw _err("tlog/bad-tile", "the final index segment carries no x prefix");
|
|
888
|
+
}
|
|
889
|
+
if (body.length !== TILE_SEGMENT_DIGITS) {
|
|
890
|
+
throw _err("tlog/bad-tile", "an index segment is " + TILE_SEGMENT_DIGITS + " digits, got " +
|
|
891
|
+
_jsonStringify(segment));
|
|
892
|
+
}
|
|
893
|
+
for (var i = 0; i < body.length; i++) {
|
|
894
|
+
var c = _charCodeAt(body, i);
|
|
895
|
+
if (c < 0x30 || c > 0x39) throw _err("tlog/bad-tile", "an index segment is digits, got " + _jsonStringify(segment));
|
|
896
|
+
}
|
|
897
|
+
return _bigIntOf(body);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* @primitive pki.tlog.parseTilePath
|
|
902
|
+
* @signature pki.tlog.parseTilePath(path) -> { level, index, width }
|
|
903
|
+
* @since 0.8.33
|
|
904
|
+
* @status stable
|
|
905
|
+
* @spec C2SP tlog-tiles
|
|
906
|
+
* @related pki.tlog.tilePath
|
|
907
|
+
*
|
|
908
|
+
* Read a tile path back into its `level`, its `index` as a `BigInt`, and its `width`, which is
|
|
909
|
+
* `null` for a full tile. The inverse of `pki.tlog.tilePath`.
|
|
910
|
+
*
|
|
911
|
+
* Fail-closed on every shape the specification forbids: a level with a leading zero or above 63, a
|
|
912
|
+
* segment that is not three digits, a leading segment without its `x`, a final segment with one, or
|
|
913
|
+
* a partial width outside 1 to 255.
|
|
914
|
+
*
|
|
915
|
+
* @example
|
|
916
|
+
* pki.tlog.parseTilePath("tile/0/x001/x234/067.p/42");
|
|
917
|
+
* // { level: 0, index: 1234067n, width: 42 }
|
|
918
|
+
*/
|
|
919
|
+
function parseTilePath(path) {
|
|
920
|
+
if (typeof path !== "string") throw _err("tlog/bad-input", "a tile path is a string");
|
|
921
|
+
var parts = _split(path, "/");
|
|
922
|
+
if (parts.length < 3 || parts[0] !== "tile") {
|
|
923
|
+
throw _err("tlog/bad-tile", "a tile path begins tile/<level>/<index>");
|
|
924
|
+
}
|
|
925
|
+
var width = null;
|
|
926
|
+
/** @internal The partial suffix is its own two segments: ".p" attached to the final index
|
|
927
|
+
* segment, then the width. Reading it off the tail first leaves a clean index to walk. */
|
|
928
|
+
var tailSeg = parts.length >= 2 ? parts[parts.length - 2] : "";
|
|
929
|
+
if (parts.length >= 2 && _strIndexOf(tailSeg, ".p") === tailSeg.length - 2 && tailSeg.length > 2) {
|
|
930
|
+
var widthText = parts[parts.length - 1];
|
|
931
|
+
width = _numberOf(_decimal(widthText, "a partial tile width", "tlog/bad-tile"));
|
|
932
|
+
_assertWidth(width);
|
|
933
|
+
parts = _arrSlice(parts, 0, parts.length - 1);
|
|
934
|
+
parts[parts.length - 1] = _strSlice(parts[parts.length - 1], 0, -2);
|
|
935
|
+
}
|
|
936
|
+
var levelText = parts[1];
|
|
937
|
+
var level = _numberOf(_decimal(levelText, "a tile level", "tlog/bad-tile"));
|
|
938
|
+
_assertLevel(level);
|
|
939
|
+
var segments = _arrSlice(parts, 2);
|
|
940
|
+
if (segments.length === 0) throw _err("tlog/bad-tile", "a tile path carries an index");
|
|
941
|
+
/** @internal The reader is bounded to the range the writer emits. `_indexPath` holds an index to
|
|
942
|
+
* uint64, so the widest path a writer can produce has MAX_INDEX_SEGMENTS segments; accepting more both
|
|
943
|
+
* admits an index no writer could emit and multiplies an ever-growing BigInt once per segment, so a
|
|
944
|
+
* long untrusted path becomes superlinear synchronous work. The count is checked BEFORE the loop, so
|
|
945
|
+
* the work a hostile path can ask for is bounded rather than merely rejected afterwards. */
|
|
946
|
+
if (segments.length > MAX_INDEX_SEGMENTS) {
|
|
947
|
+
throw _err("tlog/bad-tile", "a tile path carries at most " + MAX_INDEX_SEGMENTS +
|
|
948
|
+
" index segments, which is what a uint64 index occupies; this one has " + segments.length);
|
|
949
|
+
}
|
|
950
|
+
var index = 0n;
|
|
951
|
+
for (var i = 0; i < segments.length; i++) {
|
|
952
|
+
index = index * 1000n + _segmentDigits(segments[i], i < segments.length - 1);
|
|
953
|
+
}
|
|
954
|
+
/** @internal And the value itself, since the segment count allows a little more than uint64 does. */
|
|
955
|
+
guard.range.uint64(index, _err, "tlog/bad-tile", "a tile index");
|
|
956
|
+
return { level: level, index: index, width: width };
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* @primitive pki.tlog.parseTile
|
|
961
|
+
* @signature pki.tlog.parseTile(bytes, opts?) -> Buffer[]
|
|
962
|
+
* @since 0.8.33
|
|
963
|
+
* @status stable
|
|
964
|
+
* @spec C2SP tlog-tiles
|
|
965
|
+
* @related pki.tlog.parseTilePath, pki.merkle.nodeHash
|
|
966
|
+
*
|
|
967
|
+
* Split a tile into its 32-byte hashes. A full tile is exactly 256 of them, 8192 bytes; a partial
|
|
968
|
+
* tile carries 1 to 255. Bytes that are not a whole number of hashes, or more than a full tile, are
|
|
969
|
+
* refused with `tlog/bad-tile`.
|
|
970
|
+
*
|
|
971
|
+
* A tile is binary. A string is refused with `tlog/bad-input` rather than decoded, because reading
|
|
972
|
+
* a response as text rewrites every byte above 0x7f.
|
|
973
|
+
*
|
|
974
|
+
* Pass `opts.full` when the caller asked the log for a full tile: a short answer is then refused
|
|
975
|
+
* and is not read as a partial one, which is the difference between a truncated response and a
|
|
976
|
+
* tile the log meant to serve.
|
|
977
|
+
*
|
|
978
|
+
* @example
|
|
979
|
+
* var tileBytes = Buffer.concat([pki.merkle.leafHash(Buffer.from("a")),
|
|
980
|
+
* pki.merkle.leafHash(Buffer.from("b"))]);
|
|
981
|
+
* var hashes = pki.tlog.parseTile(tileBytes);
|
|
982
|
+
* hashes.length; // -> 2, a partial tile; a full one holds 256
|
|
983
|
+
* hashes[0].length; // -> 32
|
|
984
|
+
*/
|
|
985
|
+
function parseTile(bytes, opts) {
|
|
986
|
+
var buf = _binary(bytes, "a tile", TILE_HEIGHT_HASHES * SHA256_BYTES, "tlog/bad-tile");
|
|
987
|
+
if (buf.length === 0) throw _err("tlog/bad-tile", "a tile carries at least one hash");
|
|
988
|
+
if (buf.length % SHA256_BYTES !== 0) {
|
|
989
|
+
throw _err("tlog/bad-tile", "a tile is a whole number of " + SHA256_BYTES + "-byte hashes, got " +
|
|
990
|
+
buf.length + " bytes");
|
|
991
|
+
}
|
|
992
|
+
var count = buf.length / SHA256_BYTES;
|
|
993
|
+
if (count > TILE_HEIGHT_HASHES) {
|
|
994
|
+
throw _err("tlog/bad-tile", "a tile is at most " + TILE_HEIGHT_HASHES + " hashes, got " + count);
|
|
995
|
+
}
|
|
996
|
+
if (opts && opts.full === true && count !== TILE_HEIGHT_HASHES) {
|
|
997
|
+
throw _err("tlog/bad-tile", "a full tile is exactly " + TILE_HEIGHT_HASHES + " hashes, got " + count);
|
|
998
|
+
}
|
|
999
|
+
var out = [];
|
|
1000
|
+
for (var i = 0; i < count; i++) _push(out, _subarray(buf, i * SHA256_BYTES, (i + 1) * SHA256_BYTES));
|
|
1001
|
+
return out;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* @primitive pki.tlog.parseEntryBundle
|
|
1006
|
+
* @signature pki.tlog.parseEntryBundle(bytes) -> Buffer[]
|
|
1007
|
+
* @since 0.8.33
|
|
1008
|
+
* @status stable
|
|
1009
|
+
* @spec C2SP tlog-tiles
|
|
1010
|
+
* @defends parser-DoS (CWE-400)
|
|
1011
|
+
* @related pki.tlog.entryBundlePath, pki.merkle.leafHash
|
|
1012
|
+
*
|
|
1013
|
+
* Split an entry bundle into its entries. The wire form is big-endian uint16 length-prefixed
|
|
1014
|
+
* entries, and each entry's `pki.merkle.leafHash` is the corresponding hash in the level-0 tile at
|
|
1015
|
+
* the same index, which is what makes a bundle checkable against the tree.
|
|
1016
|
+
*
|
|
1017
|
+
* A length prefix that overruns the buffer, or a trailing byte that is not a whole prefix, is
|
|
1018
|
+
* refused with `tlog/bad-bundle` rather than read as a short final entry. An empty bundle is an
|
|
1019
|
+
* empty list.
|
|
1020
|
+
*
|
|
1021
|
+
* @example
|
|
1022
|
+
* var first = Buffer.from("the first entry");
|
|
1023
|
+
* var len = Buffer.alloc(2);
|
|
1024
|
+
* len.writeUInt16BE(first.length, 0);
|
|
1025
|
+
* var entries = pki.tlog.parseEntryBundle(Buffer.concat([len, first]));
|
|
1026
|
+
* entries[0].toString(); // -> "the first entry"
|
|
1027
|
+
* pki.merkle.leafHash(entries[0]).equals(pki.merkle.leafHash(first)); // -> true
|
|
1028
|
+
*/
|
|
1029
|
+
function parseEntryBundle(bytes) {
|
|
1030
|
+
var buf = _binary(bytes, "an entry bundle", constants.LIMITS.TLOG_MAX_ENTRY_BUNDLE_BYTES, "tlog/bad-input");
|
|
1031
|
+
var out = [];
|
|
1032
|
+
var p = 0;
|
|
1033
|
+
while (p < buf.length) {
|
|
1034
|
+
if (p + 2 > buf.length) {
|
|
1035
|
+
throw _err("tlog/bad-bundle", "a trailing byte at offset " + p + " is not a whole length prefix");
|
|
1036
|
+
}
|
|
1037
|
+
var len = buf.readUInt16BE(p);
|
|
1038
|
+
p += 2;
|
|
1039
|
+
if (p + len > buf.length) {
|
|
1040
|
+
throw _err("tlog/bad-bundle", "the entry at offset " + (p - 2) + " declares " + len +
|
|
1041
|
+
" bytes and " + (buf.length - p) + " remain");
|
|
1042
|
+
}
|
|
1043
|
+
/** @internal The COUNT is capped, not only the byte length. A zero-length entry costs two bytes on the
|
|
1044
|
+
* wire and an object in memory, so the byte cap alone admits half a million of them from a megabyte. */
|
|
1045
|
+
if (out.length >= constants.LIMITS.TLOG_MAX_ENTRY_BUNDLE_ENTRIES) {
|
|
1046
|
+
throw _err("tlog/bad-bundle", "an entry bundle carries at most " +
|
|
1047
|
+
constants.LIMITS.TLOG_MAX_ENTRY_BUNDLE_ENTRIES + " entries, one tile's width (C2SP tlog-tiles)");
|
|
1048
|
+
}
|
|
1049
|
+
_push(out, _subarray(buf, p, p + len));
|
|
1050
|
+
p += len;
|
|
1051
|
+
}
|
|
1052
|
+
return out;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* @primitive pki.tlog.tileWidth
|
|
1057
|
+
* @signature pki.tlog.tileWidth(treeSize, level, index) -> number | null
|
|
1058
|
+
* @since 0.8.36
|
|
1059
|
+
* @status stable
|
|
1060
|
+
* @spec C2SP tlog-tiles
|
|
1061
|
+
* @related pki.tlog.tilePath, pki.tlog.inclusionProof
|
|
1062
|
+
*
|
|
1063
|
+
* How wide the tile at `level` and `index` is in a tree of `treeSize` leaves: `null` when it is
|
|
1064
|
+
* full, which is the form `tilePath` takes for a full tile, or 1 to 255 when it is partial. The two
|
|
1065
|
+
* compose directly, so `tilePath(l, i, tileWidth(size, l, i))` is the path the log serves.
|
|
1066
|
+
*
|
|
1067
|
+
* A client has to know this before it asks. A tiled log serves a full tile and a partial tile at
|
|
1068
|
+
* different paths, so requesting the full path for a tile that is partial is a request for a
|
|
1069
|
+
* resource the log does not have.
|
|
1070
|
+
*
|
|
1071
|
+
* A tile the tree does not reach is `tlog/bad-tile` rather than a width of zero: the specification
|
|
1072
|
+
* says empty tiles must not be served, so there is no path to build.
|
|
1073
|
+
*
|
|
1074
|
+
* @example
|
|
1075
|
+
* pki.tlog.tileWidth(300n, 0, 0n); // null, a full tile
|
|
1076
|
+
* pki.tlog.tileWidth(300n, 0, 1n); // 44
|
|
1077
|
+
*/
|
|
1078
|
+
function tileWidth(treeSize, level, index) {
|
|
1079
|
+
var size = guard.range.uint64(treeSize, _err, "tlog/bad-tile", "a tree size");
|
|
1080
|
+
_assertLevel(level);
|
|
1081
|
+
var n = guard.range.uint64(index, _err, "tlog/bad-tile", "a tile index");
|
|
1082
|
+
/** @internal A level-l unit is a COMPLETE subtree of 256**l leaves, so the units that exist at
|
|
1083
|
+
* that level are the whole ones the tree reaches. What the tile holds is however many of its
|
|
1084
|
+
* own 256 units remain. */
|
|
1085
|
+
var units = size / (_bigIntOf(TILE_HEIGHT_HASHES) ** _bigIntOf(level));
|
|
1086
|
+
var avail = units - n * _bigIntOf(TILE_HEIGHT_HASHES);
|
|
1087
|
+
if (avail <= 0n) {
|
|
1088
|
+
throw _err("tlog/bad-tile", "a tree of " + size + " leaves has no tile " + n + " at level " +
|
|
1089
|
+
level + ", and an empty tile is not served");
|
|
1090
|
+
}
|
|
1091
|
+
return avail >= _bigIntOf(TILE_HEIGHT_HASHES) ? null : _numberOf(avail);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** @internal One level-l unit hash, read out of the tile that carries it. Tiles are cached for the
|
|
1095
|
+
* life of one assembly: a tile holds 256 unit hashes and an audit path walks neighbors, so reading
|
|
1096
|
+
* per unit would ask the log for the same tile over and over.
|
|
1097
|
+
*
|
|
1098
|
+
* The slot check below cannot fire while the width check above it holds: a full tile is held to
|
|
1099
|
+
* exactly 256 hashes and a slot is `unit % 256`, and a partial tile is held to the width the tree
|
|
1100
|
+
* size requires, which is `units - tile * 256` and so is greater than the slot of any unit the
|
|
1101
|
+
* recursion asks for. It is kept because the width and the slot are checked by separate steps, and
|
|
1102
|
+
* a change to either should not turn a short tile into a hash read from the wrong position. */
|
|
1103
|
+
function _unitReader(size, read) {
|
|
1104
|
+
var cache = _create(null);
|
|
1105
|
+
return async function (level, unit) {
|
|
1106
|
+
var tile = unit / _bigIntOf(TILE_HEIGHT_HASHES);
|
|
1107
|
+
var slot = _numberOf(unit % _bigIntOf(TILE_HEIGHT_HASHES));
|
|
1108
|
+
var key = level + ":" + tile;
|
|
1109
|
+
var hashes = cache[key];
|
|
1110
|
+
if (hashes === undefined) {
|
|
1111
|
+
var width = tileWidth(size, level, tile);
|
|
1112
|
+
var bytes = await read(level, tile, width);
|
|
1113
|
+
hashes = parseTile(bytes, { full: width === null });
|
|
1114
|
+
if (width !== null && hashes.length !== width) {
|
|
1115
|
+
throw _err("tlog/bad-tile", "the tile at level " + level + " index " + tile + " was served " +
|
|
1116
|
+
hashes.length + " hashes where the tree size requires " + width);
|
|
1117
|
+
}
|
|
1118
|
+
cache[key] = hashes;
|
|
1119
|
+
}
|
|
1120
|
+
if (slot >= hashes.length) {
|
|
1121
|
+
throw _err("tlog/bad-tile", "the tile at level " + level + " index " + tile +
|
|
1122
|
+
" carries no unit at position " + slot);
|
|
1123
|
+
}
|
|
1124
|
+
return hashes[slot];
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/** @internal MTH(D[lo:hi]) with a tile read standing in for a whole subtree. A range that is
|
|
1129
|
+
* exactly one COMPLETE level-l unit is one read; anything else splits at the largest power of two
|
|
1130
|
+
* below its length, which is the RFC 6962 sec. 2.1 shape. Every leaf is a level-0 unit, so the
|
|
1131
|
+
* recursion always terminates on a read. */
|
|
1132
|
+
async function _subtree(lo, hi, size, unitAt) {
|
|
1133
|
+
var n = hi - lo;
|
|
1134
|
+
if (hi <= size) {
|
|
1135
|
+
var span = 1n, level = 0;
|
|
1136
|
+
while (span < n && level < TILE_MAX_LEVEL) { span *= _bigIntOf(TILE_HEIGHT_HASHES); level += 1; }
|
|
1137
|
+
if (span === n && lo % n === 0n) return await unitAt(level, lo / n);
|
|
1138
|
+
}
|
|
1139
|
+
var k = 1n;
|
|
1140
|
+
while (k * 2n < n) k *= 2n;
|
|
1141
|
+
var left = await _subtree(lo, lo + k, size, unitAt);
|
|
1142
|
+
var right = await _subtree(lo + k, hi, size, unitAt);
|
|
1143
|
+
return merkle.nodeHash(left, right);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/** @internal PATH(m, D[lo:hi]) of RFC 6962 sec. 2.1.1 over the same tile-backed subtrees. */
|
|
1147
|
+
async function _tilePath(lo, hi, m, size, unitAt) {
|
|
1148
|
+
if (hi - lo === 1n) return [];
|
|
1149
|
+
var k = 1n;
|
|
1150
|
+
while (k * 2n < hi - lo) k *= 2n;
|
|
1151
|
+
if (m < k) {
|
|
1152
|
+
var a = await _tilePath(lo, lo + k, m, size, unitAt);
|
|
1153
|
+
_push(a, await _subtree(lo + k, hi, size, unitAt));
|
|
1154
|
+
return a;
|
|
1155
|
+
}
|
|
1156
|
+
var b = await _tilePath(lo + k, hi, m - k, size, unitAt);
|
|
1157
|
+
_push(b, await _subtree(lo, lo + k, size, unitAt));
|
|
1158
|
+
return b;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* @primitive pki.tlog.inclusionProof
|
|
1163
|
+
* @signature pki.tlog.inclusionProof(opts) -> Promise<Buffer[]>
|
|
1164
|
+
* @since 0.8.36
|
|
1165
|
+
* @status stable
|
|
1166
|
+
* @spec C2SP tlog-tiles, RFC 6962
|
|
1167
|
+
* @related pki.merkle.verifyInclusion, pki.tlog.tileWidth, pki.tlog.verifyCheckpoint
|
|
1168
|
+
*
|
|
1169
|
+
* Assemble the RFC 6962 audit path for one leaf of a tiled log. A tiled log serves no proof
|
|
1170
|
+
* endpoint, so a client fetches tiles and computes the path itself; this is that computation, with
|
|
1171
|
+
* the fetching left to `read`. The returned array is what `pki.merkle.verifyInclusion` folds
|
|
1172
|
+
* against the root a verified checkpoint carries.
|
|
1173
|
+
*
|
|
1174
|
+
* `read(level, index, width)` takes the same three values `tilePath` takes and returns that tile's
|
|
1175
|
+
* bytes, or a promise of them, so a caller wires it straight to a fetch of
|
|
1176
|
+
* `tilePath(level, index, width)`. Tiles are cached for the life of one call.
|
|
1177
|
+
*
|
|
1178
|
+
* Take `size` from a checkpoint you have verified. The specification says a client must not fetch
|
|
1179
|
+
* arbitrary partial tiles without a checkpoint whose size requires them, and the width of every
|
|
1180
|
+
* tile this reads follows from that size.
|
|
1181
|
+
*
|
|
1182
|
+
* A tile served narrower than the tree size requires is `tlog/bad-tile` rather than a path folded
|
|
1183
|
+
* from short data; an `index` at or above `size` is `tlog/index-out-of-range`, refused before any
|
|
1184
|
+
* tile is read.
|
|
1185
|
+
*
|
|
1186
|
+
* @opts
|
|
1187
|
+
* index: number | bigint, // 0-based leaf position to prove
|
|
1188
|
+
* size: number | bigint, // tree size from a VERIFIED checkpoint
|
|
1189
|
+
* read: function, // (level, index, width) -> Buffer | Promise<Buffer>
|
|
1190
|
+
*
|
|
1191
|
+
* @example
|
|
1192
|
+
* var leaves = [];
|
|
1193
|
+
* for (var i = 0; i < 5; i++) leaves.push(pki.merkle.leafHash(Buffer.from([i])));
|
|
1194
|
+
* var tile = Buffer.concat(leaves);
|
|
1195
|
+
* var proof = await pki.tlog.inclusionProof({ index: 2n, size: 5n,
|
|
1196
|
+
* read: function () { return tile; } });
|
|
1197
|
+
* pki.merkle.verifyInclusion({ leafIndex: 2, treeSize: 5, leafHash: leaves[2],
|
|
1198
|
+
* proof: proof, rootHash: pki.merkle.root(leaves) }); // -> true
|
|
1199
|
+
*/
|
|
1200
|
+
var _INCLUSION_PROOF_KEYS = _assign(_create(null), { index: 1, size: 1, read: 1 });
|
|
1201
|
+
async function inclusionProof(opts) {
|
|
1202
|
+
opts = opts || {};
|
|
1203
|
+
guard.identifier.assertKnownKeys(opts, _INCLUSION_PROOF_KEYS, _err,
|
|
1204
|
+
"tlog/bad-input", "unknown inclusionProof option ");
|
|
1205
|
+
var size = guard.range.uint64(opts.size, _err, "tlog/bad-input", "size");
|
|
1206
|
+
var index = guard.range.uint64(opts.index, _err, "tlog/bad-input", "index");
|
|
1207
|
+
/** @internal ONE read of the callback, which the type check and the reader both use. Read twice, the
|
|
1208
|
+
* function held to being a function need not be the function that serves the tiles: an accessor
|
|
1209
|
+
* answers each read separately, so the proof returned would be assembled from a source the check never
|
|
1210
|
+
* saw. */
|
|
1211
|
+
var read = opts.read;
|
|
1212
|
+
if (typeof read !== "function") throw _err("tlog/bad-input", "read must be a function");
|
|
1213
|
+
if (size === 0n) throw _err("tlog/empty-tree", "an empty tree has no leaf to prove");
|
|
1214
|
+
if (index >= size) {
|
|
1215
|
+
throw _err("tlog/index-out-of-range", "index " + index + " is not less than the tree size " + size);
|
|
1216
|
+
}
|
|
1217
|
+
return await _tilePath(0n, size, index, size, _unitReader(size, read));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
module.exports = {
|
|
1221
|
+
keyId: keyId,
|
|
1222
|
+
parseNote: parseNote,
|
|
1223
|
+
verifyNote: verifyNote,
|
|
1224
|
+
parseCheckpoint: parseCheckpoint,
|
|
1225
|
+
verifyCheckpoint: verifyCheckpoint,
|
|
1226
|
+
tilePath: tilePath,
|
|
1227
|
+
entryBundlePath: entryBundlePath,
|
|
1228
|
+
checkpointPath: checkpointPath,
|
|
1229
|
+
parseTilePath: parseTilePath,
|
|
1230
|
+
parseTile: parseTile,
|
|
1231
|
+
parseEntryBundle: parseEntryBundle,
|
|
1232
|
+
tileWidth: tileWidth,
|
|
1233
|
+
inclusionProof: inclusionProof,
|
|
1234
|
+
parseVkey: parseVkey,
|
|
1235
|
+
};
|