@forestrie/receipt-verify 1.1.0 → 2.1.0
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/dist/attach-transparent-statement-receipt.d.ts +5 -1
- package/dist/attach-transparent-statement-receipt.d.ts.map +1 -1
- package/dist/attach-transparent-statement-receipt.js +7 -3
- package/dist/build-receipt-offline.d.ts +8 -1
- package/dist/build-receipt-offline.d.ts.map +1 -1
- package/dist/build-receipt-offline.js +31 -24
- package/dist/checkpoint-chain.d.ts +130 -17
- package/dist/checkpoint-chain.d.ts.map +1 -1
- package/dist/checkpoint-chain.js +216 -117
- package/dist/decode-checkpoint-consistency-proof.d.ts +38 -0
- package/dist/decode-checkpoint-consistency-proof.d.ts.map +1 -0
- package/dist/decode-checkpoint-consistency-proof.js +108 -0
- package/dist/freshen-receipt.d.ts +23 -6
- package/dist/freshen-receipt.d.ts.map +1 -1
- package/dist/freshen-receipt.js +29 -17
- package/dist/grant-codec.d.ts.map +1 -1
- package/dist/grant-codec.js +24 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/parse-receipt.d.ts.map +1 -1
- package/dist/parse-receipt.js +2 -3
- package/package.json +3 -3
- package/src/attach-transparent-statement-receipt.ts +10 -3
- package/src/build-receipt-offline.ts +41 -22
- package/src/checkpoint-chain.ts +279 -136
- package/src/decode-checkpoint-consistency-proof.ts +136 -0
- package/src/freshen-receipt.ts +56 -25
- package/src/grant-codec.ts +30 -1
- package/src/index.ts +2 -0
- package/src/parse-receipt.ts +2 -4
package/src/checkpoint-chain.ts
CHANGED
|
@@ -1,145 +1,213 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Retained-checkpoint chain verification (FOR-368 Phase 3, plan-2607-29
|
|
2
|
+
* Retained-checkpoint chain verification (FOR-368 Phase 3, plan-2607-29;
|
|
3
|
+
* FOR-568/ADR-0066 signed size-2, plan-2609-10 §4.4, amended 2026-09-20).
|
|
3
4
|
*
|
|
4
5
|
* Post-FOR-410 (ADR-0056) every checkpoint's embedded consistency proof
|
|
5
6
|
* spans its massif's ENTRY BOUNDARY to its seal, so the store's retained
|
|
6
7
|
* `.sth` objects form a contiguous chain `0 → S₁ → S₂ → …`. Folding the
|
|
7
|
-
* chain
|
|
8
|
-
* each link's tree-size-2 accumulator —
|
|
9
|
-
* payload its signature covers (ADR-0046:
|
|
10
|
-
* descending height order). The fold therefore
|
|
11
|
-
* and NO RPC: an authenticated accumulator at
|
|
12
|
-
* final state to check a receipt's recomputed
|
|
8
|
+
* chain via the SIZE-DRIVEN {@link consistentRootsForSizes} (ADR-0066 D5;
|
|
9
|
+
* `@forestrie/merklelog`) reconstructs each link's tree-size-2 accumulator —
|
|
10
|
+
* which is exactly the detached payload its signature covers (ADR-0046:
|
|
11
|
+
* concat of the accumulator in descending height order). The fold therefore
|
|
12
|
+
* yields, with NO tile access and NO RPC: an authenticated accumulator at
|
|
13
|
+
* every retained seal, and the final state to check a receipt's recomputed
|
|
14
|
+
* peak against.
|
|
15
|
+
*
|
|
16
|
+
* Only `tree-size-2` is SIGNED (ADR-0066 D1 as amended: protected header
|
|
17
|
+
* label -65933) — the value folded here is the one the checkpoint's own
|
|
18
|
+
* signature covers, not merely the unprotected consistency proof's declared
|
|
19
|
+
* value, which an unsigned checkpoint could otherwise restate freely (the
|
|
20
|
+
* "keyless first checkpoint" case). `tree-size-1` stays unsigned prover
|
|
21
|
+
* context: the publisher relays several sealed steps and may re-base a step
|
|
22
|
+
* under the head checkpoint's signature, so the declared base of a
|
|
23
|
+
* checkpoint can differ from what the sealer had (D2 chain semantics is
|
|
24
|
+
* withdrawn) — a signed size-1 comparison would reject every re-based
|
|
25
|
+
* publish and every multi-link catch-up. A checkpoint without the signed
|
|
26
|
+
* size-2 label, or whose signed size-2 disagrees with its declared proof,
|
|
27
|
+
* is rejected before any fold is attempted.
|
|
13
28
|
*
|
|
14
29
|
* This rung depends only on the public log store — the complement of the
|
|
15
30
|
* `CheckpointPublished` event scan (public chain data only); see the
|
|
16
31
|
* recorded both-paths decision in plan-2607-29.
|
|
17
32
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
33
|
+
* No pre-FOR-410 state is supported (ADR-0066 D6): the affected logs are
|
|
34
|
+
* re-anchored, so there is no drift condition to signal and no fallback to
|
|
35
|
+
* select. A declared `tree-size-1` that does not continue the state being
|
|
36
|
+
* folded is `size_mismatch` like any other size disagreement — and it has to
|
|
37
|
+
* be, because that value is unsigned: a relaying party can set it without
|
|
38
|
+
* the key, so no reason string chosen from it may mean anything more than
|
|
39
|
+
* "these two sizes differ".
|
|
21
40
|
*/
|
|
22
|
-
import {
|
|
23
|
-
|
|
41
|
+
import {
|
|
42
|
+
COSE_ALG_ES256,
|
|
43
|
+
extractAlgFromProtected,
|
|
44
|
+
isLowS,
|
|
45
|
+
readProtectedTreeSize2,
|
|
46
|
+
} from "@forestrie/encoding";
|
|
47
|
+
import {
|
|
48
|
+
consistentRootsForSizes,
|
|
49
|
+
mmrSizeForLeafCount,
|
|
50
|
+
peakMMRIndexes,
|
|
51
|
+
peaksBitmap,
|
|
52
|
+
} from "@forestrie/merklelog";
|
|
24
53
|
import { SubtleHasher } from "./subtle-hasher.js";
|
|
25
54
|
import { parseCheckpoint } from "./build-receipt-offline.js";
|
|
55
|
+
import { decodeConsistencyProofFromUnprotected } from "./decode-checkpoint-consistency-proof.js";
|
|
26
56
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
/** Draft-bryce consistency proof embedded in a v3 checkpoint. `treeSize2` is
|
|
58
|
+
* cross-checked against the checkpoint's SIGNED tree-size-2 (ADR-0066 D1 as
|
|
59
|
+
* amended); `treeSize1` is unsigned prover context, not cross-checked here —
|
|
60
|
+
* see {@link verifyCheckpointChain}, which compares it with the trusted
|
|
61
|
+
* origin instead. */
|
|
31
62
|
export type CheckpointConsistencyProof = {
|
|
32
63
|
treeSize1: bigint;
|
|
33
64
|
treeSize2: bigint;
|
|
65
|
+
/** Signed `tree-size-2` (protected header label -65933); equal to
|
|
66
|
+
* {@link treeSize2} — {@link checkpointConsistencyProof} enforces this. */
|
|
67
|
+
signedTreeSize2: bigint;
|
|
34
68
|
/** One inclusion path per tree-size-1 peak, proven at tree-size-2. */
|
|
35
69
|
paths: Uint8Array[][];
|
|
36
70
|
/** New peaks not covered by the proven roots (draft `right-peaks`). */
|
|
37
71
|
rightPeaks: Uint8Array[];
|
|
38
72
|
};
|
|
39
73
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The checkpoint's SIGNED `tree-size-2` (protected header, ADR-0066 D1 as
|
|
76
|
+
* amended) differs from the declared `tree-size-2` of its embedded
|
|
77
|
+
* (unprotected) consistency proof. Distinct from a structurally malformed
|
|
78
|
+
* proof: {@link verifyCheckpointChain} reports this as `"size_mismatch"`,
|
|
79
|
+
* not `"proof_malformed"`.
|
|
80
|
+
*/
|
|
81
|
+
export class CheckpointSignedSizeMismatchError extends Error {
|
|
82
|
+
constructor(message: string) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = "CheckpointSignedSizeMismatchError";
|
|
51
85
|
}
|
|
52
|
-
throw new Error(`${what}: expected an unsigned integer`);
|
|
53
86
|
}
|
|
54
87
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
/**
|
|
89
|
+
* The checkpoint's ES256 signature is the malleable high-s twin (`s > n/2`,
|
|
90
|
+
* `n` the P-256 group order): go-merklelog rejects these for checkpoint
|
|
91
|
+
* COSE_Sign1 signatures because the univocity contract's P-256 verifier
|
|
92
|
+
* does, so a receipt that verified here while carrying a high-s signature
|
|
93
|
+
* could be one the chain refuses (FOR-568 rollout item 4). Checked here,
|
|
94
|
+
* before any WebCrypto verify is attempted, so a rejected signature never
|
|
95
|
+
* reaches {@link verifyCheckpointChain}'s `verifySignature` callback. Scoped
|
|
96
|
+
* to the checkpoint receipt path only — this module never touches the WebAuthn
|
|
97
|
+
* (-65800) or session-key-endorsement (-65801) signature paths, which stay
|
|
98
|
+
* governed by their own canonical-form rules.
|
|
99
|
+
*/
|
|
100
|
+
export class CheckpointHighSSignatureError extends Error {
|
|
101
|
+
constructor(message: string) {
|
|
102
|
+
super(message);
|
|
103
|
+
this.name = "CheckpointHighSSignatureError";
|
|
61
104
|
}
|
|
62
|
-
return v as Uint8Array[];
|
|
63
105
|
}
|
|
64
106
|
|
|
65
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Decode the embedded consistency proof (`vdp` 396 key -2) and require its
|
|
109
|
+
* declared `tree-size-2` to equal the checkpoint's SIGNED `tree-size-2` from
|
|
110
|
+
* the protected header (ADR-0066 D1 as amended, D5.5, label -65933).
|
|
111
|
+
* `tree-size-1` is not signed and is not checked here (D2 is withdrawn); see
|
|
112
|
+
* {@link verifyCheckpointChain} for its comparison against the trusted
|
|
113
|
+
* origin.
|
|
114
|
+
*
|
|
115
|
+
* Also rejects a malleable high-s ES256 signature (see
|
|
116
|
+
* {@link CheckpointHighSSignatureError}) before any fold or WebCrypto verify
|
|
117
|
+
* work — a checkpoint signed with any other algorithm (e.g. KS256) is not
|
|
118
|
+
* subject to this check, since it does not go through the P-256 WebCrypto
|
|
119
|
+
* path this guards.
|
|
120
|
+
*
|
|
121
|
+
* @throws {Error} when the protected header carries no consistency proof,
|
|
122
|
+
* the proof is structurally malformed (see
|
|
123
|
+
* {@link decodeConsistencyProofFromUnprotected}), or the protected header
|
|
124
|
+
* carries no signed tree-size-2 label
|
|
125
|
+
* @throws {CheckpointSignedSizeMismatchError} when the signed tree-size-2
|
|
126
|
+
* differs from the declared proof's tree-size-2
|
|
127
|
+
* @throws {CheckpointHighSSignatureError} when the checkpoint is ES256-signed
|
|
128
|
+
* with a high-s (malleable) signature
|
|
129
|
+
*/
|
|
66
130
|
export function checkpointConsistencyProof(
|
|
67
131
|
checkpointBytes: Uint8Array,
|
|
68
132
|
): CheckpointConsistencyProof {
|
|
69
|
-
const { unprotected } = parseCheckpoint(checkpointBytes);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
133
|
+
const { coseSign1, unprotected } = parseCheckpoint(checkpointBytes);
|
|
134
|
+
const alg = extractAlgFromProtected(coseSign1[0]);
|
|
135
|
+
const signature = coseSign1[3];
|
|
136
|
+
if (alg === COSE_ALG_ES256 && signature.length === 64 && !isLowS(signature)) {
|
|
137
|
+
throw new CheckpointHighSSignatureError(
|
|
138
|
+
"checkpoint ES256 signature is not low-s canonical (s > n/2); rejected " +
|
|
139
|
+
"to match the univocity contract's P-256 verifier and go-merklelog",
|
|
140
|
+
);
|
|
73
141
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
);
|
|
77
|
-
if (!(proofBstr instanceof Uint8Array)) {
|
|
142
|
+
const declared = decodeConsistencyProofFromUnprotected(unprotected);
|
|
143
|
+
if (declared === null) {
|
|
78
144
|
throw new Error("checkpoint carries no consistency proof (vdp key -2)");
|
|
79
145
|
}
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
146
|
+
const signedTreeSize2 = readProtectedTreeSize2(coseSign1[0]);
|
|
147
|
+
if (signedTreeSize2 === null) {
|
|
82
148
|
throw new Error(
|
|
83
|
-
"
|
|
149
|
+
"checkpoint protected header carries no signed tree-size-2 (-65933)",
|
|
84
150
|
);
|
|
85
151
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
pathsRaw.some(
|
|
90
|
-
(p) => !Array.isArray(p) || p.some((n) => !(n instanceof Uint8Array)),
|
|
91
|
-
)
|
|
92
|
-
) {
|
|
93
|
-
throw new Error("consistency paths must be arrays of byte strings");
|
|
94
|
-
}
|
|
95
|
-
const treeSize1 = asBigint(proof[0], "tree-size-1");
|
|
96
|
-
const treeSize2 = asBigint(proof[1], "tree-size-2");
|
|
97
|
-
// A consistency proof strictly grows the tree; enforce `0 <= ts1 < ts2`
|
|
98
|
-
// (ts1 == 0 is a legitimate base-0 first link). This is the primary guard
|
|
99
|
-
// that keeps `treeSize2 - 1` / `treeSize1 - 1` non-negative before they
|
|
100
|
-
// reach `peakMMRIndexes` (FOR-414); the unsigned check in `asBigint` and
|
|
101
|
-
// the `posHeight` guard in @forestrie/merklelog are defence-in-depth.
|
|
102
|
-
if (treeSize2 <= treeSize1) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`consistency proof must grow the tree: tree-size-1 ${treeSize1} < tree-size-2 ${treeSize2}`,
|
|
152
|
+
if (signedTreeSize2 !== declared.treeSize2) {
|
|
153
|
+
throw new CheckpointSignedSizeMismatchError(
|
|
154
|
+
`signed tree-size-2 (-65933) ${signedTreeSize2} != declared consistency-proof tree-size-2 ${declared.treeSize2}`,
|
|
105
155
|
);
|
|
106
156
|
}
|
|
107
157
|
return {
|
|
108
|
-
treeSize1,
|
|
109
|
-
treeSize2,
|
|
110
|
-
|
|
111
|
-
|
|
158
|
+
treeSize1: declared.treeSize1,
|
|
159
|
+
treeSize2: declared.treeSize2,
|
|
160
|
+
signedTreeSize2,
|
|
161
|
+
paths: declared.paths,
|
|
162
|
+
rightPeaks: declared.rightPeaks,
|
|
112
163
|
};
|
|
113
164
|
}
|
|
114
165
|
|
|
115
166
|
/**
|
|
116
|
-
* One fold step: from the
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
167
|
+
* One fold step: from the CALLER-TRUSTED accumulator at `sizeFrom`, produce
|
|
168
|
+
* the `proof.treeSize2` accumulator via the size-driven
|
|
169
|
+
* {@link consistentRootsForSizes} (ADR-0066 D5) — `roots` (the proven
|
|
170
|
+
* prefix) followed by the proof's supplied right-peaks (the target peaks no
|
|
171
|
+
* path reaches).
|
|
172
|
+
*
|
|
173
|
+
* `sizeFrom` is a parameter, not read off `proof`, because the fold must run
|
|
174
|
+
* against a size the CALLER already trusts (the previous link's verified
|
|
175
|
+
* `treeSize2`, or the caller's anchor for a first link) — reading it from
|
|
176
|
+
* the proof instead would let an unsigned or substituted proof dictate its
|
|
177
|
+
* own starting point. `proof.treeSize1` must equal it regardless: the two
|
|
178
|
+
* disagreeing means this proof does not continue from the state being
|
|
179
|
+
* folded, not a mere shape defect, so it is checked before any fold work.
|
|
180
|
+
*
|
|
181
|
+
* @throws {Error} when `proof.treeSize1 !== sizeFrom`, or when the proof
|
|
182
|
+
* supplies a right-peaks count other than
|
|
183
|
+
* {@link consistentRootsForSizes}'s `expectedRight`
|
|
184
|
+
* @throws {ConsistencyShapeError} (`@forestrie/merklelog`) when the proof
|
|
185
|
+
* does not have the shape MMR(sizeFrom) -> MMR(proof.treeSize2) implies
|
|
120
186
|
*/
|
|
121
187
|
export async function computeCheckpointAccumulator(
|
|
122
188
|
proof: CheckpointConsistencyProof,
|
|
123
189
|
accumulatorFrom: Uint8Array[],
|
|
190
|
+
sizeFrom: bigint,
|
|
124
191
|
): Promise<Uint8Array[]> {
|
|
192
|
+
if (proof.treeSize1 !== sizeFrom) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`consistency proof base tree-size-1 ${proof.treeSize1} does not match the trusted size ${sizeFrom}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
125
197
|
const hasher = new SubtleHasher();
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
);
|
|
135
|
-
const accumulator = [...proven, ...proof.rightPeaks];
|
|
136
|
-
const expected = peakMMRIndexes(proof.treeSize2 - 1n).length;
|
|
137
|
-
if (accumulator.length !== expected) {
|
|
198
|
+
const { roots, expectedRight } = await consistentRootsForSizes(
|
|
199
|
+
hasher,
|
|
200
|
+
sizeFrom,
|
|
201
|
+
proof.treeSize2,
|
|
202
|
+
accumulatorFrom,
|
|
203
|
+
proof.paths,
|
|
204
|
+
);
|
|
205
|
+
if (proof.rightPeaks.length !== expectedRight) {
|
|
138
206
|
throw new Error(
|
|
139
|
-
`
|
|
207
|
+
`checkpoint supplies ${proof.rightPeaks.length} right-peaks; size ${proof.treeSize2} requires ${expectedRight}`,
|
|
140
208
|
);
|
|
141
209
|
}
|
|
142
|
-
return
|
|
210
|
+
return [...roots, ...proof.rightPeaks];
|
|
143
211
|
}
|
|
144
212
|
|
|
145
213
|
/** Detached payload the checkpoint signature covers (ADR-0046): the raw
|
|
@@ -157,7 +225,11 @@ export function accumulatorPayload(accumulator: Uint8Array[]): Uint8Array {
|
|
|
157
225
|
export type CheckpointChainLink = {
|
|
158
226
|
treeSize1: bigint;
|
|
159
227
|
treeSize2: bigint;
|
|
228
|
+
/** Signed `tree-size-2` (protected header label -65933; ADR-0066 D1 as
|
|
229
|
+
* amended). */
|
|
230
|
+
signedTreeSize2: bigint;
|
|
160
231
|
accumulator: Uint8Array[];
|
|
232
|
+
/** Always `true`: a link is recorded only after its signature verified. */
|
|
161
233
|
signatureOk: boolean;
|
|
162
234
|
};
|
|
163
235
|
|
|
@@ -167,9 +239,25 @@ export type CheckpointChainResult =
|
|
|
167
239
|
ok: false;
|
|
168
240
|
reason:
|
|
169
241
|
| "empty_chain"
|
|
170
|
-
| "legacy_chain_break"
|
|
171
242
|
| "signature"
|
|
172
|
-
|
|
243
|
+
/**
|
|
244
|
+
* The checkpoint's ES256 signature is the malleable high-s twin (see
|
|
245
|
+
* {@link CheckpointHighSSignatureError}) — rejected before any
|
|
246
|
+
* WebCrypto verify, so it is distinct from `"signature"` (a
|
|
247
|
+
* canonical-form low-s signature that did not verify).
|
|
248
|
+
*/
|
|
249
|
+
| "signature_malleable"
|
|
250
|
+
| "proof_malformed"
|
|
251
|
+
/**
|
|
252
|
+
* Two sizes that must be equal are not. Either a checkpoint's SIGNED
|
|
253
|
+
* `tree-size-2` disagrees with its declared consistency-proof
|
|
254
|
+
* `tree-size-2` (ADR-0066 D1 as amended, D5.5), or a link's declared
|
|
255
|
+
* `tree-size-1` disagrees with the size the fold starts from — the
|
|
256
|
+
* caller's `trustedBase.size` (0 with no `trustedBase`) for the
|
|
257
|
+
* first link, the previous link's `tree-size-2` after that.
|
|
258
|
+
* `detail` names both sizes.
|
|
259
|
+
*/
|
|
260
|
+
| "size_mismatch";
|
|
173
261
|
/** Index of the offending checkpoint. */
|
|
174
262
|
at: number;
|
|
175
263
|
detail: string;
|
|
@@ -180,15 +268,34 @@ export type CheckpointChainResult =
|
|
|
180
268
|
* Verify a retained checkpoint chain (ascending massif order) and fold out
|
|
181
269
|
* the final authenticated accumulator.
|
|
182
270
|
*
|
|
183
|
-
* - The first link
|
|
184
|
-
*
|
|
185
|
-
* chain rooted in an
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
271
|
+
* - The first link's trusted starting size is `trustedBase?.size ?? 0n`
|
|
272
|
+
* (base 0 for a whole-log chain) with `trustedBase?.accumulator ?? []`
|
|
273
|
+
* as the fold's starting accumulator (a suffix chain rooted in an
|
|
274
|
+
* already-trusted accumulator supplies both).
|
|
275
|
+
* - A supplied `trustedBase` must describe a state an MMR can be in: its
|
|
276
|
+
* `size` a complete MMR size, and its `accumulator` holding one peak per
|
|
277
|
+
* peak of that size. `peaksBitmap` rounds an incomplete size DOWN to the
|
|
278
|
+
* largest MMR below it, so without the completeness check a size of 5
|
|
279
|
+
* folds the 4 -> N shape while every link reports a base of 5 — a node
|
|
280
|
+
* count no MMR has. Both are `proof_malformed`.
|
|
281
|
+
* - The first link's declared `tree-size-1` must equal that trusted
|
|
282
|
+
* starting size, and every subsequent link's must equal the previous
|
|
283
|
+
* link's sealed `tree-size-2`; either disagreement is `size_mismatch`.
|
|
284
|
+
* `tree-size-1` itself is never compared with a signed value (D2 is
|
|
285
|
+
* withdrawn): only this trusted-origin comparison applies. Because it is
|
|
286
|
+
* unsigned, the reason it produces carries no more meaning than the size
|
|
287
|
+
* disagreement itself (ADR-0066 D6: no pre-FOR-410 state is supported, so
|
|
288
|
+
* there is no drift condition to fall back from).
|
|
289
|
+
* - Each checkpoint's SIGNED `tree-size-2` (ADR-0066 D1 as amended) must
|
|
290
|
+
* equal its declared consistency-proof `tree-size-2`
|
|
291
|
+
* ({@link checkpointConsistencyProof}); a disagreement is
|
|
292
|
+
* `size_mismatch`.
|
|
189
293
|
* - Each link's signature is checked over its computed accumulator via
|
|
190
294
|
* the injected verifier (the caller owns trust resolution — genesis
|
|
191
|
-
* roots, caller-known keys, or the label-1000 delegation path).
|
|
295
|
+
* roots, caller-known keys, or the label-1000 delegation path). A link
|
|
296
|
+
* joins `links` only once its signature has verified, so on any failure
|
|
297
|
+
* `links` is the verified prefix and never holds an accumulator nothing
|
|
298
|
+
* attested.
|
|
192
299
|
*/
|
|
193
300
|
export async function verifyCheckpointChain(opts: {
|
|
194
301
|
checkpoints: Uint8Array[];
|
|
@@ -196,8 +303,9 @@ export async function verifyCheckpointChain(opts: {
|
|
|
196
303
|
checkpointBytes: Uint8Array,
|
|
197
304
|
detachedPayload: Uint8Array,
|
|
198
305
|
) => Promise<boolean>;
|
|
199
|
-
/** Trusted base
|
|
200
|
-
|
|
306
|
+
/** Trusted base for a suffix chain (a whole-log chain has base size 0 and
|
|
307
|
+
* an empty accumulator, which is also the default when this is absent). */
|
|
308
|
+
trustedBase?: { size: bigint; accumulator: Uint8Array[] };
|
|
201
309
|
}): Promise<CheckpointChainResult> {
|
|
202
310
|
const links: CheckpointChainLink[] = [];
|
|
203
311
|
if (opts.checkpoints.length === 0) {
|
|
@@ -209,60 +317,91 @@ export async function verifyCheckpointChain(opts: {
|
|
|
209
317
|
links,
|
|
210
318
|
};
|
|
211
319
|
}
|
|
212
|
-
|
|
213
|
-
|
|
320
|
+
const trustedBase = opts.trustedBase;
|
|
321
|
+
if (trustedBase !== undefined) {
|
|
322
|
+
// A size that is not a complete MMR size describes no state: the fold
|
|
323
|
+
// would silently use the largest MMR below it (`peaksBitmap` rounds
|
|
324
|
+
// down) while every link reported the supplied value as its base.
|
|
325
|
+
if (
|
|
326
|
+
trustedBase.size < 0n ||
|
|
327
|
+
mmrSizeForLeafCount(peaksBitmap(trustedBase.size)) !== trustedBase.size
|
|
328
|
+
) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
reason: "proof_malformed",
|
|
332
|
+
at: 0,
|
|
333
|
+
detail: `trusted base size ${trustedBase.size} is not a complete MMR size`,
|
|
334
|
+
links,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// …and the accumulator must hold exactly the peaks that size has, which
|
|
338
|
+
// the fold otherwise only compares against the rounded-down count.
|
|
339
|
+
const basePeaks =
|
|
340
|
+
trustedBase.size === 0n
|
|
341
|
+
? 0
|
|
342
|
+
: peakMMRIndexes(trustedBase.size - 1n).length;
|
|
343
|
+
if (trustedBase.accumulator.length !== basePeaks) {
|
|
344
|
+
return {
|
|
345
|
+
ok: false,
|
|
346
|
+
reason: "proof_malformed",
|
|
347
|
+
at: 0,
|
|
348
|
+
detail: `trusted base accumulator has ${trustedBase.accumulator.length} peaks; size ${trustedBase.size} has ${basePeaks}`,
|
|
349
|
+
links,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
let accumulator = trustedBase?.accumulator ?? [];
|
|
354
|
+
let expectedBase = trustedBase?.size ?? 0n;
|
|
214
355
|
for (let i = 0; i < opts.checkpoints.length; i++) {
|
|
215
356
|
const bytes = opts.checkpoints[i]!;
|
|
216
357
|
let proof: CheckpointConsistencyProof;
|
|
217
358
|
try {
|
|
218
359
|
proof = checkpointConsistencyProof(bytes);
|
|
219
360
|
} catch (err) {
|
|
361
|
+
const reason =
|
|
362
|
+
err instanceof CheckpointSignedSizeMismatchError
|
|
363
|
+
? "size_mismatch"
|
|
364
|
+
: err instanceof CheckpointHighSSignatureError
|
|
365
|
+
? "signature_malleable"
|
|
366
|
+
: "proof_malformed";
|
|
220
367
|
return {
|
|
221
368
|
ok: false,
|
|
222
|
-
reason
|
|
369
|
+
reason,
|
|
223
370
|
at: i,
|
|
224
371
|
detail: err instanceof Error ? err.message : String(err),
|
|
225
372
|
links,
|
|
226
373
|
};
|
|
227
374
|
}
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
if (accumulator.length !== wanted) {
|
|
243
|
-
return {
|
|
244
|
-
ok: false,
|
|
245
|
-
reason: "proof_malformed",
|
|
246
|
-
at: i,
|
|
247
|
-
detail: `trusted base accumulator has ${accumulator.length} peaks; base size ${proof.treeSize1} requires ${wanted}`,
|
|
248
|
-
links,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
375
|
+
if (proof.treeSize1 !== expectedBase) {
|
|
376
|
+
if (i === 0) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
reason: "size_mismatch",
|
|
380
|
+
at: i,
|
|
381
|
+
detail:
|
|
382
|
+
trustedBase === undefined
|
|
383
|
+
? `first checkpoint declared tree-size-1 ${proof.treeSize1} != whole-log base size ${expectedBase} and no trusted base was supplied`
|
|
384
|
+
: `first checkpoint declared tree-size-1 ${proof.treeSize1} != trusted base size ${expectedBase}`,
|
|
385
|
+
links,
|
|
386
|
+
};
|
|
251
387
|
}
|
|
252
|
-
} else if (proof.treeSize1 !== expectedBase) {
|
|
253
388
|
return {
|
|
254
389
|
ok: false,
|
|
255
|
-
reason: "
|
|
390
|
+
reason: "size_mismatch",
|
|
256
391
|
at: i,
|
|
257
392
|
detail:
|
|
258
|
-
`checkpoint ${i}
|
|
259
|
-
"
|
|
393
|
+
`checkpoint ${i} declared tree-size-1 ${proof.treeSize1} != the previous link's tree-size-2 ${expectedBase} — ` +
|
|
394
|
+
"the declared origin is unsigned, so the chain is treated as not continuous",
|
|
260
395
|
links,
|
|
261
396
|
};
|
|
262
397
|
}
|
|
263
398
|
let computed: Uint8Array[];
|
|
264
399
|
try {
|
|
265
|
-
computed = await computeCheckpointAccumulator(
|
|
400
|
+
computed = await computeCheckpointAccumulator(
|
|
401
|
+
proof,
|
|
402
|
+
accumulator,
|
|
403
|
+
expectedBase,
|
|
404
|
+
);
|
|
266
405
|
} catch (err) {
|
|
267
406
|
return {
|
|
268
407
|
ok: false,
|
|
@@ -276,13 +415,10 @@ export async function verifyCheckpointChain(opts: {
|
|
|
276
415
|
bytes,
|
|
277
416
|
accumulatorPayload(computed),
|
|
278
417
|
);
|
|
279
|
-
links.push({
|
|
280
|
-
treeSize1: proof.treeSize1,
|
|
281
|
-
treeSize2: proof.treeSize2,
|
|
282
|
-
accumulator: computed,
|
|
283
|
-
signatureOk,
|
|
284
|
-
});
|
|
285
418
|
if (!signatureOk) {
|
|
419
|
+
// `links` stays the verified prefix: the computed accumulator of a
|
|
420
|
+
// link whose signature did not verify is attested by nothing, so it
|
|
421
|
+
// is not handed back.
|
|
286
422
|
return {
|
|
287
423
|
ok: false,
|
|
288
424
|
reason: "signature",
|
|
@@ -291,6 +427,13 @@ export async function verifyCheckpointChain(opts: {
|
|
|
291
427
|
links,
|
|
292
428
|
};
|
|
293
429
|
}
|
|
430
|
+
links.push({
|
|
431
|
+
treeSize1: proof.treeSize1,
|
|
432
|
+
treeSize2: proof.treeSize2,
|
|
433
|
+
signedTreeSize2: proof.signedTreeSize2,
|
|
434
|
+
accumulator: computed,
|
|
435
|
+
signatureOk,
|
|
436
|
+
});
|
|
294
437
|
accumulator = computed;
|
|
295
438
|
expectedBase = proof.treeSize2;
|
|
296
439
|
}
|