@forestrie/receipt-verify 2.1.0 → 2.2.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.
@@ -39,6 +39,21 @@ export declare class CheckpointSignedSizeMismatchError extends Error {
39
39
  export declare class CheckpointHighSSignatureError extends Error {
40
40
  constructor(message: string);
41
41
  }
42
+ /**
43
+ * The checkpoint's protected header carries no integer `alg` (label 1).
44
+ *
45
+ * The univocity contract rejects such a header outright — the structural
46
+ * walk finds the size, and the `alg` requirement in the same call raises
47
+ * `ClaimNotFound(1)` or `UnexpectedMajorType`. Off-chain the header used to
48
+ * read as "no algorithm stated", which both turned OFF the high-s rejection
49
+ * below (gated on the algorithm being ES256) and still yielded a signed size
50
+ * to fold from — so a checkpoint the chain will never anchor verified here
51
+ * under weaker rules than a well-formed one (review finding S-1). Reported
52
+ * as `"proof_malformed"` by {@link verifyCheckpointChain}.
53
+ */
54
+ export declare class CheckpointProtectedHeaderAlgError extends Error {
55
+ constructor(message: string);
56
+ }
42
57
  /**
43
58
  * Decode the embedded consistency proof (`vdp` 396 key -2) and require its
44
59
  * declared `tree-size-2` to equal the checkpoint's SIGNED `tree-size-2` from
@@ -61,6 +76,8 @@ export declare class CheckpointHighSSignatureError extends Error {
61
76
  * differs from the declared proof's tree-size-2
62
77
  * @throws {CheckpointHighSSignatureError} when the checkpoint is ES256-signed
63
78
  * with a high-s (malleable) signature
79
+ * @throws {CheckpointProtectedHeaderAlgError} when the protected header
80
+ * carries no integer `alg` (label 1)
64
81
  */
65
82
  export declare function checkpointConsistencyProof(checkpointBytes: Uint8Array): CheckpointConsistencyProof;
66
83
  /**
@@ -136,11 +153,14 @@ export type CheckpointChainResult = {
136
153
  * as the fold's starting accumulator (a suffix chain rooted in an
137
154
  * already-trusted accumulator supplies both).
138
155
  * - A supplied `trustedBase` must describe a state an MMR can be in: its
139
- * `size` a complete MMR size, and its `accumulator` holding one peak per
140
- * peak of that size. `peaksBitmap` rounds an incomplete size DOWN to the
141
- * largest MMR below it, so without the completeness check a size of 5
142
- * folds the 4 -> N shape while every link reports a base of 5 — a node
143
- * count no MMR has. Both are `proof_malformed`.
156
+ * `size` a complete MMR size, its `accumulator` holding one peak per peak
157
+ * of that size, and every one of those peaks a 32-byte node value.
158
+ * `peaksBitmap` rounds an incomplete size DOWN to the largest MMR below
159
+ * it, so without the completeness check a size of 5 folds the 4 -> N shape
160
+ * while every link reports a base of 5 — a node count no MMR has; and
161
+ * without the byte-length check an origin peak of any length is copied
162
+ * through the empty-path branch into the detached payload. All three are
163
+ * `proof_malformed`.
144
164
  * - The first link's declared `tree-size-1` must equal that trusted
145
165
  * starting size, and every subsequent link's must equal the previous
146
166
  * link's sealed `tree-size-2`; either disagreement is `size_mismatch`.
@@ -1 +1 @@
1
- {"version":3,"file":"checkpoint-chain.d.ts","sourceRoot":"","sources":["../src/checkpoint-chain.ts"],"names":[],"mappings":"AAwDA;;;;qBAIqB;AACrB,MAAM,MAAM,0BAA0B,GAAG;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;+EAC2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;IACtB,uEAAuE;IACvE,UAAU,EAAE,UAAU,EAAE,CAAC;CAC1B,CAAC;AAEF;;;;;;GAMG;AACH,qBAAa,iCAAkC,SAAQ,KAAK;gBAC9C,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,UAAU,GAC1B,0BAA0B,CAgC5B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,0BAA0B,EACjC,eAAe,EAAE,UAAU,EAAE,EAC7B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,EAAE,CAAC,CAoBvB;AAED;+DAC+D;AAC/D,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,UAAU,CAQxE;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;mBACe;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,2EAA2E;IAC3E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,GACrE;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EACF,aAAa,GACb,WAAW;IACb;;;;;OAKG;OACD,qBAAqB,GACrB,iBAAiB;IACnB;;;;;;;;OAQG;OACD,eAAe,CAAC;IACpB,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B,CAAC;AAEN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,eAAe,EAAE,CACf,eAAe,EAAE,UAAU,EAC3B,eAAe,EAAE,UAAU,KACxB,OAAO,CAAC,OAAO,CAAC,CAAC;IACtB;+EAC2E;IAC3E,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;CAC3D,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAoIjC"}
1
+ {"version":3,"file":"checkpoint-chain.d.ts","sourceRoot":"","sources":["../src/checkpoint-chain.ts"],"names":[],"mappings":"AAyDA;;;;qBAIqB;AACrB,MAAM,MAAM,0BAA0B,GAAG;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;+EAC2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;IACtB,uEAAuE;IACvE,UAAU,EAAE,UAAU,EAAE,CAAC;CAC1B,CAAC;AAEF;;;;;;GAMG;AACH,qBAAa,iCAAkC,SAAQ,KAAK;gBAC9C,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,iCAAkC,SAAQ,KAAK;gBAC9C,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,UAAU,GAC1B,0BAA0B,CA6C5B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,0BAA0B,EACjC,eAAe,EAAE,UAAU,EAAE,EAC7B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,EAAE,CAAC,CAoBvB;AAED;+DAC+D;AAC/D,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,UAAU,CAQxE;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;mBACe;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,2EAA2E;IAC3E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,GACrE;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EACF,aAAa,GACb,WAAW;IACb;;;;;OAKG;OACD,qBAAqB,GACrB,iBAAiB;IACnB;;;;;;;;OAQG;OACD,eAAe,CAAC;IACpB,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B,CAAC;AAEN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,eAAe,EAAE,CACf,eAAe,EAAE,UAAU,EAC3B,eAAe,EAAE,UAAU,KACxB,OAAO,CAAC,OAAO,CAAC,CAAC;IACtB;+EAC2E;IAC3E,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;CAC3D,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAuJjC"}
@@ -38,7 +38,7 @@
38
38
  * the key, so no reason string chosen from it may mean anything more than
39
39
  * "these two sizes differ".
40
40
  */
41
- import { COSE_ALG_ES256, extractAlgFromProtected, isLowS, readProtectedTreeSize2, } from "@forestrie/encoding";
41
+ import { COSE_ALG_ES256, ProtectedHeaderAlgError, isLowS, readProtectedAlg, readProtectedTreeSize2, } from "@forestrie/encoding";
42
42
  import { consistentRootsForSizes, mmrSizeForLeafCount, peakMMRIndexes, peaksBitmap, } from "@forestrie/merklelog";
43
43
  import { SubtleHasher } from "./subtle-hasher.js";
44
44
  import { parseCheckpoint } from "./build-receipt-offline.js";
@@ -74,6 +74,24 @@ export class CheckpointHighSSignatureError extends Error {
74
74
  this.name = "CheckpointHighSSignatureError";
75
75
  }
76
76
  }
77
+ /**
78
+ * The checkpoint's protected header carries no integer `alg` (label 1).
79
+ *
80
+ * The univocity contract rejects such a header outright — the structural
81
+ * walk finds the size, and the `alg` requirement in the same call raises
82
+ * `ClaimNotFound(1)` or `UnexpectedMajorType`. Off-chain the header used to
83
+ * read as "no algorithm stated", which both turned OFF the high-s rejection
84
+ * below (gated on the algorithm being ES256) and still yielded a signed size
85
+ * to fold from — so a checkpoint the chain will never anchor verified here
86
+ * under weaker rules than a well-formed one (review finding S-1). Reported
87
+ * as `"proof_malformed"` by {@link verifyCheckpointChain}.
88
+ */
89
+ export class CheckpointProtectedHeaderAlgError extends Error {
90
+ constructor(message) {
91
+ super(message);
92
+ this.name = "CheckpointProtectedHeaderAlgError";
93
+ }
94
+ }
77
95
  /**
78
96
  * Decode the embedded consistency proof (`vdp` 396 key -2) and require its
79
97
  * declared `tree-size-2` to equal the checkpoint's SIGNED `tree-size-2` from
@@ -96,10 +114,24 @@ export class CheckpointHighSSignatureError extends Error {
96
114
  * differs from the declared proof's tree-size-2
97
115
  * @throws {CheckpointHighSSignatureError} when the checkpoint is ES256-signed
98
116
  * with a high-s (malleable) signature
117
+ * @throws {CheckpointProtectedHeaderAlgError} when the protected header
118
+ * carries no integer `alg` (label 1)
99
119
  */
100
120
  export function checkpointConsistencyProof(checkpointBytes) {
101
121
  const { coseSign1, unprotected } = parseCheckpoint(checkpointBytes);
102
- const alg = extractAlgFromProtected(coseSign1[0]);
122
+ // Strict: a header with no integer alg is one the contract rejects, and
123
+ // reading it leniently would switch the high-s rejection below off while
124
+ // the signed size was still taken from it (S-1).
125
+ let alg;
126
+ try {
127
+ alg = readProtectedAlg(coseSign1[0]);
128
+ }
129
+ catch (err) {
130
+ if (err instanceof ProtectedHeaderAlgError) {
131
+ throw new CheckpointProtectedHeaderAlgError(`checkpoint protected header carries no integer alg (label 1): ${err.message}`);
132
+ }
133
+ throw err;
134
+ }
103
135
  const signature = coseSign1[3];
104
136
  if (alg === COSE_ALG_ES256 && signature.length === 64 && !isLowS(signature)) {
105
137
  throw new CheckpointHighSSignatureError("checkpoint ES256 signature is not low-s canonical (s > n/2); rejected " +
@@ -176,11 +208,14 @@ export function accumulatorPayload(accumulator) {
176
208
  * as the fold's starting accumulator (a suffix chain rooted in an
177
209
  * already-trusted accumulator supplies both).
178
210
  * - A supplied `trustedBase` must describe a state an MMR can be in: its
179
- * `size` a complete MMR size, and its `accumulator` holding one peak per
180
- * peak of that size. `peaksBitmap` rounds an incomplete size DOWN to the
181
- * largest MMR below it, so without the completeness check a size of 5
182
- * folds the 4 -> N shape while every link reports a base of 5 — a node
183
- * count no MMR has. Both are `proof_malformed`.
211
+ * `size` a complete MMR size, its `accumulator` holding one peak per peak
212
+ * of that size, and every one of those peaks a 32-byte node value.
213
+ * `peaksBitmap` rounds an incomplete size DOWN to the largest MMR below
214
+ * it, so without the completeness check a size of 5 folds the 4 -> N shape
215
+ * while every link reports a base of 5 — a node count no MMR has; and
216
+ * without the byte-length check an origin peak of any length is copied
217
+ * through the empty-path branch into the detached payload. All three are
218
+ * `proof_malformed`.
184
219
  * - The first link's declared `tree-size-1` must equal that trusted
185
220
  * starting size, and every subsequent link's must equal the previous
186
221
  * link's sealed `tree-size-2`; either disagreement is `size_mismatch`.
@@ -240,6 +275,25 @@ export async function verifyCheckpointChain(opts) {
240
275
  links,
241
276
  };
242
277
  }
278
+ // …and every peak must be a 32-byte node value, the check arbor's
279
+ // producer applies to both path elements and right-peaks (`toNode32`).
280
+ // The count alone does not reach it: on the empty-path branch an origin
281
+ // peak is copied into the result verbatim, so a 0/31/33/64-byte peak
282
+ // reaches `accumulatorPayload` and shortens or lengthens the detached
283
+ // payload, with only the signature left to reject it (review finding
284
+ // I2, canopy C6).
285
+ for (let i = 0; i < trustedBase.accumulator.length; i++) {
286
+ const peak = trustedBase.accumulator[i];
287
+ if (!(peak instanceof Uint8Array) || peak.length !== 32) {
288
+ return {
289
+ ok: false,
290
+ reason: "proof_malformed",
291
+ at: 0,
292
+ detail: `trusted base accumulator peak ${i} is not a 32-byte node value (${describePeak(peak)})`,
293
+ links,
294
+ };
295
+ }
296
+ }
243
297
  }
244
298
  let accumulator = trustedBase?.accumulator ?? [];
245
299
  let expectedBase = trustedBase?.size ?? 0n;
@@ -322,3 +376,13 @@ export async function verifyCheckpointChain(opts) {
322
376
  }
323
377
  return { ok: true, links, accumulator };
324
378
  }
379
+ /** Name what was found where a 32-byte accumulator peak was required. */
380
+ function describePeak(peak) {
381
+ if (peak instanceof Uint8Array)
382
+ return `${peak.length} bytes`;
383
+ if (peak === null)
384
+ return "null";
385
+ if (Array.isArray(peak))
386
+ return "an array";
387
+ return typeof peak;
388
+ }
@@ -16,7 +16,9 @@ export type FreshenReceiptInput = {
16
16
  leafValue: Uint8Array;
17
17
  /** Consistency-proof chain covering [0 or the trusted base] → the latest
18
18
  * sealed size, in ascending contiguous order (the raw per-checkpoint
19
- * proofs, with `paths`). The chain's last link must end at the
19
+ * proofs, with `paths`). Every link's `signedTreeSize2` must equal its
20
+ * `treeSize2`, as `checkpointConsistencyProof` requires of the checkpoint
21
+ * it decoded each link from. The chain's last link must end at the
20
22
  * checkpoint's sealed size. */
21
23
  consistencyProofs: readonly CheckpointConsistencyProof[];
22
24
  /** Trusted base for a suffix chain — the size the caller already trusts
@@ -40,9 +42,11 @@ export type FreshenReceiptResult = {
40
42
  sealedSize: bigint;
41
43
  };
42
44
  /**
43
- * Freshen a stale receipt to the latest sealed state. Throws if the chain is
44
- * not a contiguous cover from the trusted base to the checkpoint's sealed size,
45
- * if a climb node is missing from the supplied proofs, or if the recomputed
45
+ * Freshen a stale receipt to the latest sealed state. Throws if a link's
46
+ * signed `tree-size-2` disagrees with its declared one
47
+ * ({@link CheckpointSignedSizeMismatchError}), if the chain is not a
48
+ * contiguous cover from the trusted base to the checkpoint's sealed size, if
49
+ * a climb node is missing from the supplied proofs, or if the recomputed
46
50
  * peak does not match the folded latest accumulator.
47
51
  */
48
52
  export declare function freshenReceipt(input: FreshenReceiptInput): Promise<FreshenReceiptResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"freshen-receipt.d.ts","sourceRoot":"","sources":["../src/freshen-receipt.ts"],"names":[],"mappings":"AA2DA,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,uBAAuB,CAAC;AAI/B;;;;;;;;GAQG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,iEAAiE;IACjE,eAAe,EAAE,UAAU,CAAC;IAC5B;uEACmE;IACnE,SAAS,EAAE,UAAU,CAAC;IACtB;;;mCAG+B;IAC/B,iBAAiB,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACzD;;;;;kDAK8C;IAC9C,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;IAC1D;wDACoD;IACpD,qBAAqB,EAAE,UAAU,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,wEAAwE;IACxE,OAAO,EAAE,UAAU,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AASF;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAwK/B"}
1
+ {"version":3,"file":"freshen-receipt.d.ts","sourceRoot":"","sources":["../src/freshen-receipt.ts"],"names":[],"mappings":"AA2DA,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,uBAAuB,CAAC;AAI/B;;;;;;;;GAQG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,iEAAiE;IACjE,eAAe,EAAE,UAAU,CAAC;IAC5B;uEACmE;IACnE,SAAS,EAAE,UAAU,CAAC;IACtB;;;;;mCAK+B;IAC/B,iBAAiB,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACzD;;;;;kDAK8C;IAC9C,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;IAC1D;wDACoD;IACpD,qBAAqB,EAAE,UAAU,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,wEAAwE;IACxE,OAAO,EAAE,UAAU,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AASF;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAsL/B"}
@@ -47,7 +47,7 @@
47
47
  */
48
48
  import { calculateRoot, inclusionProofPath, mmrSizeForLeafCount, peakIndexForLeafProof, peakMMRIndexes, peaksBitmap, } from "@forestrie/merklelog";
49
49
  import { assembleReceiptFromProof, parseCheckpoint, } from "./build-receipt-offline.js";
50
- import { computeCheckpointAccumulator, } from "./checkpoint-chain.js";
50
+ import { CheckpointSignedSizeMismatchError, computeCheckpointAccumulator, } from "./checkpoint-chain.js";
51
51
  import { parseReceipt } from "./parse-receipt.js";
52
52
  import { SubtleHasher } from "./subtle-hasher.js";
53
53
  function bytesEqual(a, b) {
@@ -59,9 +59,11 @@ function bytesEqual(a, b) {
59
59
  return x === 0;
60
60
  }
61
61
  /**
62
- * Freshen a stale receipt to the latest sealed state. Throws if the chain is
63
- * not a contiguous cover from the trusted base to the checkpoint's sealed size,
64
- * if a climb node is missing from the supplied proofs, or if the recomputed
62
+ * Freshen a stale receipt to the latest sealed state. Throws if a link's
63
+ * signed `tree-size-2` disagrees with its declared one
64
+ * ({@link CheckpointSignedSizeMismatchError}), if the chain is not a
65
+ * contiguous cover from the trusted base to the checkpoint's sealed size, if
66
+ * a climb node is missing from the supplied proofs, or if the recomputed
65
67
  * peak does not match the folded latest accumulator.
66
68
  */
67
69
  export async function freshenReceipt(input) {
@@ -106,6 +108,18 @@ export async function freshenReceipt(input) {
106
108
  if (baseAccumulator.length !== basePeaks) {
107
109
  throw new Error(`base accumulator has ${baseAccumulator.length} peaks; trusted base size ${baseSize} has ${basePeaks}`);
108
110
  }
111
+ // Every link's SIGNED tree-size-2 must equal its declared one — the same
112
+ // equality `checkpointConsistencyProof` enforces when it decodes a
113
+ // checkpoint (ADR-0066 D1 as amended, D5.5). These links arrive already
114
+ // decoded, so nothing here had re-read the field the type says was pinned
115
+ // to the signature, and a link carrying `signedTreeSize2: 999n` beside
116
+ // `treeSize2: 7n` freshened (review finding I5).
117
+ for (let i = 0; i < links.length; i++) {
118
+ const link = links[i];
119
+ if (link.signedTreeSize2 !== link.treeSize2) {
120
+ throw new CheckpointSignedSizeMismatchError(`consistency proof ${i}: signed tree-size-2 (-65933) ${link.signedTreeSize2} != declared consistency-proof tree-size-2 ${link.treeSize2}`);
121
+ }
122
+ }
109
123
  // The first link must continue from that size, not from a size it names
110
124
  // itself (ADR-0066 D5.4).
111
125
  if (firstLink.treeSize1 !== baseSize) {
package/dist/index.d.ts CHANGED
@@ -76,7 +76,7 @@ export { findGrantLeafInMassif } from "./find-grant-leaf.js";
76
76
  export type { LocatedLeaf } from "./find-grant-leaf.js";
77
77
  /** Re-exported so callers of findGrantLeafInMassif can catch it (FOR-344). */
78
78
  export { MissingIndexError } from "@forestrie/merklelog";
79
- export { accumulatorPayload, checkpointConsistencyProof, computeCheckpointAccumulator, verifyCheckpointChain, CheckpointHighSSignatureError, CheckpointSignedSizeMismatchError, type CheckpointChainLink, type CheckpointChainResult, type CheckpointConsistencyProof, } from "./checkpoint-chain.js";
79
+ export { accumulatorPayload, checkpointConsistencyProof, computeCheckpointAccumulator, verifyCheckpointChain, CheckpointHighSSignatureError, CheckpointProtectedHeaderAlgError, CheckpointSignedSizeMismatchError, type CheckpointChainLink, type CheckpointChainResult, type CheckpointConsistencyProof, } from "./checkpoint-chain.js";
80
80
  /**
81
81
  * Univocity leaf commitment hash. Was CLI-private (forestrie-cli's own
82
82
  * mirror, "hoist to the library when the FOR-297 multi-hop resolver lands");
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ export { grantCommitmentHashFromGrant } from "./grant-commitment.js";
61
61
  export { findGrantLeafInMassif } from "./find-grant-leaf.js";
62
62
  /** Re-exported so callers of findGrantLeafInMassif can catch it (FOR-344). */
63
63
  export { MissingIndexError } from "@forestrie/merklelog";
64
- export { accumulatorPayload, checkpointConsistencyProof, computeCheckpointAccumulator, verifyCheckpointChain, CheckpointHighSSignatureError, CheckpointSignedSizeMismatchError, } from "./checkpoint-chain.js";
64
+ export { accumulatorPayload, checkpointConsistencyProof, computeCheckpointAccumulator, verifyCheckpointChain, CheckpointHighSSignatureError, CheckpointProtectedHeaderAlgError, CheckpointSignedSizeMismatchError, } from "./checkpoint-chain.js";
65
65
  /**
66
66
  * Univocity leaf commitment hash. Was CLI-private (forestrie-cli's own
67
67
  * mirror, "hoist to the library when the FOR-297 multi-hop resolver lands");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forestrie/receipt-verify",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Offline SCITT grant receipt verification (ADR-0045)",
@@ -29,8 +29,8 @@
29
29
  "dependencies": {
30
30
  "@noble/hashes": "^1.7.1",
31
31
  "@forestrie/chain-rpc": "0.3.0",
32
- "@forestrie/merklelog": "0.4.0",
33
- "@forestrie/encoding": "0.8.0"
32
+ "@forestrie/encoding": "0.9.0",
33
+ "@forestrie/merklelog": "0.5.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "esbuild": "^0.24.0",
@@ -40,8 +40,9 @@
40
40
  */
41
41
  import {
42
42
  COSE_ALG_ES256,
43
- extractAlgFromProtected,
43
+ ProtectedHeaderAlgError,
44
44
  isLowS,
45
+ readProtectedAlg,
45
46
  readProtectedTreeSize2,
46
47
  } from "@forestrie/encoding";
47
48
  import {
@@ -104,6 +105,25 @@ export class CheckpointHighSSignatureError extends Error {
104
105
  }
105
106
  }
106
107
 
108
+ /**
109
+ * The checkpoint's protected header carries no integer `alg` (label 1).
110
+ *
111
+ * The univocity contract rejects such a header outright — the structural
112
+ * walk finds the size, and the `alg` requirement in the same call raises
113
+ * `ClaimNotFound(1)` or `UnexpectedMajorType`. Off-chain the header used to
114
+ * read as "no algorithm stated", which both turned OFF the high-s rejection
115
+ * below (gated on the algorithm being ES256) and still yielded a signed size
116
+ * to fold from — so a checkpoint the chain will never anchor verified here
117
+ * under weaker rules than a well-formed one (review finding S-1). Reported
118
+ * as `"proof_malformed"` by {@link verifyCheckpointChain}.
119
+ */
120
+ export class CheckpointProtectedHeaderAlgError extends Error {
121
+ constructor(message: string) {
122
+ super(message);
123
+ this.name = "CheckpointProtectedHeaderAlgError";
124
+ }
125
+ }
126
+
107
127
  /**
108
128
  * Decode the embedded consistency proof (`vdp` 396 key -2) and require its
109
129
  * declared `tree-size-2` to equal the checkpoint's SIGNED `tree-size-2` from
@@ -126,12 +146,27 @@ export class CheckpointHighSSignatureError extends Error {
126
146
  * differs from the declared proof's tree-size-2
127
147
  * @throws {CheckpointHighSSignatureError} when the checkpoint is ES256-signed
128
148
  * with a high-s (malleable) signature
149
+ * @throws {CheckpointProtectedHeaderAlgError} when the protected header
150
+ * carries no integer `alg` (label 1)
129
151
  */
130
152
  export function checkpointConsistencyProof(
131
153
  checkpointBytes: Uint8Array,
132
154
  ): CheckpointConsistencyProof {
133
155
  const { coseSign1, unprotected } = parseCheckpoint(checkpointBytes);
134
- const alg = extractAlgFromProtected(coseSign1[0]);
156
+ // Strict: a header with no integer alg is one the contract rejects, and
157
+ // reading it leniently would switch the high-s rejection below off while
158
+ // the signed size was still taken from it (S-1).
159
+ let alg: number;
160
+ try {
161
+ alg = readProtectedAlg(coseSign1[0]);
162
+ } catch (err) {
163
+ if (err instanceof ProtectedHeaderAlgError) {
164
+ throw new CheckpointProtectedHeaderAlgError(
165
+ `checkpoint protected header carries no integer alg (label 1): ${err.message}`,
166
+ );
167
+ }
168
+ throw err;
169
+ }
135
170
  const signature = coseSign1[3];
136
171
  if (alg === COSE_ALG_ES256 && signature.length === 64 && !isLowS(signature)) {
137
172
  throw new CheckpointHighSSignatureError(
@@ -273,11 +308,14 @@ export type CheckpointChainResult =
273
308
  * as the fold's starting accumulator (a suffix chain rooted in an
274
309
  * already-trusted accumulator supplies both).
275
310
  * - 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`.
311
+ * `size` a complete MMR size, its `accumulator` holding one peak per peak
312
+ * of that size, and every one of those peaks a 32-byte node value.
313
+ * `peaksBitmap` rounds an incomplete size DOWN to the largest MMR below
314
+ * it, so without the completeness check a size of 5 folds the 4 -> N shape
315
+ * while every link reports a base of 5 — a node count no MMR has; and
316
+ * without the byte-length check an origin peak of any length is copied
317
+ * through the empty-path branch into the detached payload. All three are
318
+ * `proof_malformed`.
281
319
  * - The first link's declared `tree-size-1` must equal that trusted
282
320
  * starting size, and every subsequent link's must equal the previous
283
321
  * link's sealed `tree-size-2`; either disagreement is `size_mismatch`.
@@ -349,6 +387,25 @@ export async function verifyCheckpointChain(opts: {
349
387
  links,
350
388
  };
351
389
  }
390
+ // …and every peak must be a 32-byte node value, the check arbor's
391
+ // producer applies to both path elements and right-peaks (`toNode32`).
392
+ // The count alone does not reach it: on the empty-path branch an origin
393
+ // peak is copied into the result verbatim, so a 0/31/33/64-byte peak
394
+ // reaches `accumulatorPayload` and shortens or lengthens the detached
395
+ // payload, with only the signature left to reject it (review finding
396
+ // I2, canopy C6).
397
+ for (let i = 0; i < trustedBase.accumulator.length; i++) {
398
+ const peak = trustedBase.accumulator[i] as unknown;
399
+ if (!(peak instanceof Uint8Array) || peak.length !== 32) {
400
+ return {
401
+ ok: false,
402
+ reason: "proof_malformed",
403
+ at: 0,
404
+ detail: `trusted base accumulator peak ${i} is not a 32-byte node value (${describePeak(peak)})`,
405
+ links,
406
+ };
407
+ }
408
+ }
352
409
  }
353
410
  let accumulator = trustedBase?.accumulator ?? [];
354
411
  let expectedBase = trustedBase?.size ?? 0n;
@@ -439,3 +496,11 @@ export async function verifyCheckpointChain(opts: {
439
496
  }
440
497
  return { ok: true, links, accumulator };
441
498
  }
499
+
500
+ /** Name what was found where a 32-byte accumulator peak was required. */
501
+ function describePeak(peak: unknown): string {
502
+ if (peak instanceof Uint8Array) return `${peak.length} bytes`;
503
+ if (peak === null) return "null";
504
+ if (Array.isArray(peak)) return "an array";
505
+ return typeof peak;
506
+ }
@@ -58,6 +58,7 @@ import {
58
58
  parseCheckpoint,
59
59
  } from "./build-receipt-offline.js";
60
60
  import {
61
+ CheckpointSignedSizeMismatchError,
61
62
  computeCheckpointAccumulator,
62
63
  type CheckpointConsistencyProof,
63
64
  } from "./checkpoint-chain.js";
@@ -81,7 +82,9 @@ export type FreshenReceiptInput = {
81
82
  leafValue: Uint8Array;
82
83
  /** Consistency-proof chain covering [0 or the trusted base] → the latest
83
84
  * sealed size, in ascending contiguous order (the raw per-checkpoint
84
- * proofs, with `paths`). The chain's last link must end at the
85
+ * proofs, with `paths`). Every link's `signedTreeSize2` must equal its
86
+ * `treeSize2`, as `checkpointConsistencyProof` requires of the checkpoint
87
+ * it decoded each link from. The chain's last link must end at the
85
88
  * checkpoint's sealed size. */
86
89
  consistencyProofs: readonly CheckpointConsistencyProof[];
87
90
  /** Trusted base for a suffix chain — the size the caller already trusts
@@ -111,9 +114,11 @@ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
111
114
  }
112
115
 
113
116
  /**
114
- * Freshen a stale receipt to the latest sealed state. Throws if the chain is
115
- * not a contiguous cover from the trusted base to the checkpoint's sealed size,
116
- * if a climb node is missing from the supplied proofs, or if the recomputed
117
+ * Freshen a stale receipt to the latest sealed state. Throws if a link's
118
+ * signed `tree-size-2` disagrees with its declared one
119
+ * ({@link CheckpointSignedSizeMismatchError}), if the chain is not a
120
+ * contiguous cover from the trusted base to the checkpoint's sealed size, if
121
+ * a climb node is missing from the supplied proofs, or if the recomputed
117
122
  * peak does not match the folded latest accumulator.
118
123
  */
119
124
  export async function freshenReceipt(
@@ -170,6 +175,20 @@ export async function freshenReceipt(
170
175
  `base accumulator has ${baseAccumulator.length} peaks; trusted base size ${baseSize} has ${basePeaks}`,
171
176
  );
172
177
  }
178
+ // Every link's SIGNED tree-size-2 must equal its declared one — the same
179
+ // equality `checkpointConsistencyProof` enforces when it decodes a
180
+ // checkpoint (ADR-0066 D1 as amended, D5.5). These links arrive already
181
+ // decoded, so nothing here had re-read the field the type says was pinned
182
+ // to the signature, and a link carrying `signedTreeSize2: 999n` beside
183
+ // `treeSize2: 7n` freshened (review finding I5).
184
+ for (let i = 0; i < links.length; i++) {
185
+ const link = links[i]!;
186
+ if (link.signedTreeSize2 !== link.treeSize2) {
187
+ throw new CheckpointSignedSizeMismatchError(
188
+ `consistency proof ${i}: signed tree-size-2 (-65933) ${link.signedTreeSize2} != declared consistency-proof tree-size-2 ${link.treeSize2}`,
189
+ );
190
+ }
191
+ }
173
192
  // The first link must continue from that size, not from a size it names
174
193
  // itself (ADR-0066 D5.4).
175
194
  if (firstLink.treeSize1 !== baseSize) {
package/src/index.ts CHANGED
@@ -156,6 +156,7 @@ export {
156
156
  computeCheckpointAccumulator,
157
157
  verifyCheckpointChain,
158
158
  CheckpointHighSSignatureError,
159
+ CheckpointProtectedHeaderAlgError,
159
160
  CheckpointSignedSizeMismatchError,
160
161
  type CheckpointChainLink,
161
162
  type CheckpointChainResult,