@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.
@@ -45,7 +45,7 @@
45
45
  * tested go-merklelog port); this module only assembles node values by index
46
46
  * from the old receipt path + the consistency proofs.
47
47
  */
48
- import { calculateRoot, inclusionProofPath, peakIndexForLeafProof, peakMMRIndexes, } from "@forestrie/merklelog";
48
+ import { calculateRoot, inclusionProofPath, mmrSizeForLeafCount, peakIndexForLeafProof, peakMMRIndexes, peaksBitmap, } from "@forestrie/merklelog";
49
49
  import { assembleReceiptFromProof, parseCheckpoint, } from "./build-receipt-offline.js";
50
50
  import { computeCheckpointAccumulator, } from "./checkpoint-chain.js";
51
51
  import { parseReceipt } from "./parse-receipt.js";
@@ -90,19 +90,26 @@ export async function freshenReceipt(input) {
90
90
  throw new Error("freshen requires at least one consistency proof linking the receipt's era to the checkpoint");
91
91
  }
92
92
  const firstLink = links[0];
93
- // Base: a base-0 chain starts from an empty accumulator; a suffix chain's
94
- // trusted seed must have the peak count of its tree-size-1.
95
- const baseCount = input.accumulatorFrom?.length ?? 0;
96
- if (firstLink.treeSize1 === 0n) {
97
- if (baseCount !== 0) {
98
- throw new Error("base-0 consistency chain must start from an empty accumulator seed");
99
- }
100
- }
101
- else {
102
- const wanted = peakMMRIndexes(firstLink.treeSize1 - 1n).length;
103
- if (baseCount !== wanted) {
104
- throw new Error(`base accumulator has ${baseCount} peaks; first link size ${firstLink.treeSize1} requires ${wanted}`);
105
- }
93
+ // Base: the CALLER's trusted size and its accumulator (size 0 and an empty
94
+ // accumulator for a whole-log chain). The size has to be a size an MMR can
95
+ // have — `peaksBitmap` rounds an incomplete one DOWN, so a size of 5 would
96
+ // fold the 4 -> N shape — and the accumulator has to hold that size's
97
+ // peaks, which is the check the proof's own declared base used to stand in
98
+ // for.
99
+ const baseSize = input.trustedBase?.size ?? 0n;
100
+ const baseAccumulator = input.trustedBase?.accumulator ?? [];
101
+ if (baseSize < 0n ||
102
+ mmrSizeForLeafCount(peaksBitmap(baseSize)) !== baseSize) {
103
+ throw new Error(`trusted base size ${baseSize} is not a complete MMR size`);
104
+ }
105
+ const basePeaks = baseSize === 0n ? 0 : peakMMRIndexes(baseSize - 1n).length;
106
+ if (baseAccumulator.length !== basePeaks) {
107
+ throw new Error(`base accumulator has ${baseAccumulator.length} peaks; trusted base size ${baseSize} has ${basePeaks}`);
108
+ }
109
+ // The first link must continue from that size, not from a size it names
110
+ // itself (ADR-0066 D5.4).
111
+ if (firstLink.treeSize1 !== baseSize) {
112
+ throw new Error(`first consistency proof declares tree-size-1 ${firstLink.treeSize1}; the trusted base size is ${baseSize}`);
106
113
  }
107
114
  // Contiguity: each link continues where the previous one sealed.
108
115
  for (let i = 1; i < links.length; i++) {
@@ -115,10 +122,15 @@ export async function freshenReceipt(input) {
115
122
  if (lastLink.treeSize2 !== sealedSize) {
116
123
  throw new Error(`consistency chain ends at size ${lastLink.treeSize2} but the checkpoint sealed size ${sealedSize}`);
117
124
  }
118
- // Fold the chain to the latest accumulator (self-check target).
119
- let accumulator = input.accumulatorFrom ?? [];
125
+ // Fold the chain to the latest accumulator (self-check target). Each step
126
+ // runs against a size the caller trusts, never one read off the link being
127
+ // folded: the trusted base for the first link, and the size the previous
128
+ // link was just folded TO for every link after it.
129
+ let accumulator = baseAccumulator;
130
+ let sizeFrom = baseSize;
120
131
  for (const p of links) {
121
- accumulator = await computeCheckpointAccumulator(p, accumulator);
132
+ accumulator = await computeCheckpointAccumulator(p, accumulator, sizeFrom);
133
+ sizeFrom = p.treeSize2;
122
134
  }
123
135
  const aLatest = accumulator;
124
136
  // Cross-check the fold against the checkpoint we are borrowing from (F1): the
@@ -1 +1 @@
1
- {"version":3,"file":"grant-codec.d.ts","sourceRoot":"","sources":["../src/grant-codec.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAuDjD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,CAO3D;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG;IACtD,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;CACzB,CAsBA;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,GAAG,UAAU,CAa3D"}
1
+ {"version":3,"file":"grant-codec.d.ts","sourceRoot":"","sources":["../src/grant-codec.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAiFjD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,CAS3D;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG;IACtD,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;CACzB,CAuBA;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,GAAG,UAAU,CAa3D"}
@@ -18,6 +18,26 @@ function leftPad(b, length) {
18
18
  out.set(b, length - b.length);
19
19
  return out;
20
20
  }
21
+ /**
22
+ * Grant wire v0 map keys are 0–6; keys 7 (`signer`) and 8 (`kind`) were
23
+ * retired before this package existed (FOR-580 owner ruling) and must be
24
+ * rejected here the same way the encoding and canopy-api codecs already do
25
+ * on the admission path (`packages/shared/encoding/src/grant-codec.ts`,
26
+ * `packages/apps/canopy-api/src/grant/codec.ts`) — this codec previously
27
+ * decoded by key number and silently ignored them.
28
+ */
29
+ function assertNoObsoleteWireKeys(m) {
30
+ const keys = m instanceof Map
31
+ ? [...m.keys()]
32
+ : Object.keys(m)
33
+ .map(Number)
34
+ .filter((n) => Number.isFinite(n));
35
+ for (const k of keys) {
36
+ if (k === 7 || k === 8) {
37
+ throw new Error("Grant wire v0: obsolete CBOR keys 7 (signer) and 8 (kind) must not be present; use grantData in the commitment only.");
38
+ }
39
+ }
40
+ }
21
41
  function mapToGrant(m) {
22
42
  const get = (k) => m instanceof Map ? m.get(k) : m[k];
23
43
  const requireBstr = (v, minLen = 0) => {
@@ -52,7 +72,9 @@ export function decodeGrantPayload(bytes) {
52
72
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
53
73
  throw new Error("Grant payload must be a CBOR map");
54
74
  }
55
- return mapToGrant(raw);
75
+ const m = raw;
76
+ assertNoObsoleteWireKeys(m);
77
+ return mapToGrant(m);
56
78
  }
57
79
  export function decodeGrantResponse(bytes) {
58
80
  const raw = decodeCborDeterministic(bytes);
@@ -60,6 +82,7 @@ export function decodeGrantResponse(bytes) {
60
82
  throw new Error("Grant response must be a CBOR map");
61
83
  }
62
84
  const m = raw;
85
+ assertNoObsoleteWireKeys(m);
63
86
  const get = (k) => m instanceof Map ? m.get(k) : m[k];
64
87
  const idtimestampVal = get(CBOR_KEY_IDTIMESTAMP);
65
88
  if (!(idtimestampVal instanceof Uint8Array) ||
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, type CheckpointChainLink, type CheckpointChainResult, type CheckpointConsistencyProof, } from "./checkpoint-chain.js";
79
+ export { accumulatorPayload, checkpointConsistencyProof, computeCheckpointAccumulator, verifyCheckpointChain, CheckpointHighSSignatureError, 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, } from "./checkpoint-chain.js";
64
+ export { accumulatorPayload, checkpointConsistencyProof, computeCheckpointAccumulator, verifyCheckpointChain, CheckpointHighSSignatureError, 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");
@@ -1 +1 @@
1
- {"version":3,"file":"parse-receipt.d.ts","sourceRoot":"","sources":["../src/parse-receipt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAIlD,MAAM,MAAM,SAAS,GAAG;IACtB,eAAe,EAAE,UAAU;IAC3B,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,OAAO,EAAE,UAAU,GAAG,IAAI;IAC1B,SAAS,EAAE,UAAU;CACtB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAW1D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAiB1D;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpD,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAUtB;AAED,wBAAgB,YAAY,CAAC,YAAY,EAAE,UAAU,GAAG;IACtD,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,SAAS,EAAE,SAAS,CAAC;CACtB,CAwDA"}
1
+ {"version":3,"file":"parse-receipt.d.ts","sourceRoot":"","sources":["../src/parse-receipt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAElD,MAAM,MAAM,SAAS,GAAG;IACtB,eAAe,EAAE,UAAU;IAC3B,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,OAAO,EAAE,UAAU,GAAG,IAAI;IAC1B,SAAS,EAAE,UAAU;CACtB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAW1D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAiB1D;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpD,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAUtB;AAED,wBAAgB,YAAY,CAAC,YAAY,EAAE,UAAU,GAAG;IACtD,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,SAAS,EAAE,SAAS,CAAC;CACtB,CAwDA"}
@@ -1,5 +1,4 @@
1
- import { decodeCborDeterministic } from "@forestrie/encoding";
2
- const VDS_COSE_RECEIPT_PROOFS_TAG = 396;
1
+ import { COSE_LABEL_VDP, decodeCborDeterministic } from "@forestrie/encoding";
3
2
  export function unwrapCoseSign1Tag(value) {
4
3
  if (value && typeof value === "object" && !(value instanceof Map)) {
5
4
  const tagged = value;
@@ -58,7 +57,7 @@ export function parseReceipt(receiptBytes) {
58
57
  throw new Error("Receipt payload must be nil (detached) or 32-byte peak hash");
59
58
  }
60
59
  const unprotected = toHeaderMap(coseSign1[1]);
61
- const proofsRaw = unprotected.get(VDS_COSE_RECEIPT_PROOFS_TAG);
60
+ const proofsRaw = unprotected.get(COSE_LABEL_VDP);
62
61
  if (!proofsRaw || typeof proofsRaw !== "object") {
63
62
  throw new Error("Receipt missing header 396 (inclusion proof)");
64
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forestrie/receipt-verify",
3
- "version": "1.1.0",
3
+ "version": "2.1.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.3.0",
33
- "@forestrie/encoding": "0.7.0"
32
+ "@forestrie/merklelog": "0.4.0",
33
+ "@forestrie/encoding": "0.8.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "esbuild": "^0.24.0",
@@ -6,11 +6,18 @@
6
6
  * Golden vectors for receipt construction/verification are tracked by
7
7
  * FOR-289.
8
8
  */
9
- import { mergeUnprotectedIntoCoseSign1 } from "@forestrie/encoding";
9
+ import {
10
+ COSE_LABEL_VDP,
11
+ mergeUnprotectedIntoCoseSign1,
12
+ } from "@forestrie/encoding";
10
13
  import { HEADER_IDTIMESTAMP } from "./forest-genesis-labels.js";
11
14
 
12
- /** SCITT transparent statement unprotected receipt label (grants.md §3.2). */
13
- export const HEADER_RECEIPT = 396;
15
+ /**
16
+ * SCITT transparent statement unprotected receipt label (grants.md §3.2).
17
+ * Alias of {@link COSE_LABEL_VDP} — kept exported under this name for
18
+ * existing callers.
19
+ */
20
+ export const HEADER_RECEIPT = COSE_LABEL_VDP;
14
21
 
15
22
  export function attachReceiptAndIdtimestampToTransparentStatement(
16
23
  statementBytes: Uint8Array,
@@ -16,8 +16,11 @@
16
16
  */
17
17
 
18
18
  import {
19
+ COSE_LABEL_PEAK_RECEIPTS,
20
+ COSE_LABEL_VDP,
19
21
  decodeCborDeterministic,
20
22
  encodeCborDeterministic,
23
+ readProtectedTreeSize2,
21
24
  } from "@forestrie/encoding";
22
25
  import {
23
26
  calculateRoot,
@@ -34,12 +37,11 @@ import {
34
37
  unwrapCoseSign1Tag,
35
38
  type CoseSign1,
36
39
  } from "./parse-receipt.js";
40
+ import { decodeConsistencyProofFromUnprotected } from "./decode-checkpoint-consistency-proof.js";
37
41
  import { SubtleHasher } from "./subtle-hasher.js";
38
42
 
39
- const VDS_COSE_RECEIPT_PROOFS_TAG = 396;
40
- const SEAL_PEAK_RECEIPTS_LABEL = -65931;
43
+ /** Not in scope for the shared cose-labels module (FOR-568 §4.1). */
41
44
  const DELEGATION_CERT_LABEL = 1000;
42
- const VDP_CONSISTENCY_PROOF_KEY = -2;
43
45
 
44
46
  /**
45
47
  * Re-exported for compatibility. The MMR proof math and massif node store now
@@ -54,7 +56,14 @@ export type ParsedCheckpoint = {
54
56
  unprotected: Map<number, unknown>;
55
57
  /** Pre-signed peak receipts (label -65931), or null when absent. */
56
58
  peakReceipts: unknown[] | null;
57
- /** Sealed tree size (tree-size-2 of the embedded consistency proof). */
59
+ /**
60
+ * Sealed tree size: the SIGNED `tree-size-2` from the checkpoint's
61
+ * PROTECTED header (ADR-0066 D1 as amended, label -65933), not the
62
+ * unprotected consistency proof's declared value. `null` when the
63
+ * checkpoint carries no embedded consistency proof or no signed
64
+ * tree-size-2 — such a checkpoint is not verifiable and callers must
65
+ * treat it exactly as they would a checkpoint with no proof at all.
66
+ */
58
67
  mmrSize: bigint | null;
59
68
  delegationCert: Uint8Array | null;
60
69
  };
@@ -65,7 +74,7 @@ export function parseCheckpoint(checkpointBytes: Uint8Array): ParsedCheckpoint {
65
74
  const coseSign1 = requireCoseSign1(unwrapCoseSign1Tag(decoded));
66
75
  const unprotected = toHeaderMap(coseSign1[1]);
67
76
 
68
- const peakReceiptsRaw = unprotected.get(SEAL_PEAK_RECEIPTS_LABEL);
77
+ const peakReceiptsRaw = unprotected.get(COSE_LABEL_PEAK_RECEIPTS);
69
78
  const peakReceipts = Array.isArray(peakReceiptsRaw) ? peakReceiptsRaw : null;
70
79
 
71
80
  const delegationCertRaw = unprotected.get(DELEGATION_CERT_LABEL);
@@ -78,7 +87,7 @@ export function parseCheckpoint(checkpointBytes: Uint8Array): ParsedCheckpoint {
78
87
  coseSign1,
79
88
  unprotected,
80
89
  peakReceipts,
81
- mmrSize: sealedSizeFromCheckpoint(unprotected),
90
+ mmrSize: sealedSizeFromCheckpoint(coseSign1, unprotected),
82
91
  delegationCert,
83
92
  };
84
93
  }
@@ -186,7 +195,7 @@ export function assembleReceiptFromProof(
186
195
  const verifiableProofs = new Map<number, unknown>([
187
196
  [-1, [inclusionProofEntry]],
188
197
  ]);
189
- receiptUnprotected.set(VDS_COSE_RECEIPT_PROOFS_TAG, verifiableProofs);
198
+ receiptUnprotected.set(COSE_LABEL_VDP, verifiableProofs);
190
199
 
191
200
  // Peak receipts are signed with detached payload; emit nil so verify uses
192
201
  // the peak derived from the inclusion proof.
@@ -252,24 +261,34 @@ function cborBytes(value: unknown): Uint8Array {
252
261
  }
253
262
 
254
263
  /**
255
- * Sealed mmr size from a format-v3 checkpoint: tree-size-2 of the consistency
256
- * proof (`bstr .cbor [tree-size-1, tree-size-2, paths, right-peaks]`) under
257
- * the verifiable-proofs unprotected header (label 396, key -2).
264
+ * Sealed mmr size from a format-v3 checkpoint: the SIGNED `tree-size-2`
265
+ * (ADR-0066 D1 as amended, label -65933) from the PROTECTED header — not the
266
+ * unprotected consistency proof's declared value, which a checkpoint without
267
+ * a signing key could freely restate (ADR-0066's "keyless first checkpoint"
268
+ * case). A consistency proof must still be present (an unsigned checkpoint
269
+ * with no proof is not sealed at all); its declared sizes are not otherwise
270
+ * used here — {@link checkpointConsistencyProof} in `checkpoint-chain.ts`
271
+ * is the validating decode that requires the two to agree.
272
+ *
273
+ * Lenient by design: an absent proof, a malformed proof, or an absent or
274
+ * malformed signed tree-size-2 all yield `null` rather than a throw, so a
275
+ * caller of {@link parseCheckpoint} sees exactly the same "not verifiable"
276
+ * outcome either way.
258
277
  */
259
278
  function sealedSizeFromCheckpoint(
279
+ coseSign1: CoseSign1,
260
280
  unprotected: Map<number, unknown>,
261
281
  ): bigint | null {
262
- const vdpRaw = unprotected.get(VDS_COSE_RECEIPT_PROOFS_TAG);
263
- if (vdpRaw === undefined || vdpRaw === null) return null;
264
- const vdp = toHeaderMap(vdpRaw as Map<number, unknown>);
265
- const proofBstr = vdp.get(VDP_CONSISTENCY_PROOF_KEY);
266
- if (!(proofBstr instanceof Uint8Array)) return null;
267
- const proof = decodeCborDeterministic(proofBstr);
268
- if (!Array.isArray(proof) || proof.length < 2) return null;
269
- const treeSize2 = proof[1];
270
- if (typeof treeSize2 === "bigint") return treeSize2;
271
- if (typeof treeSize2 === "number" && Number.isSafeInteger(treeSize2)) {
272
- return BigInt(treeSize2);
282
+ let declared;
283
+ try {
284
+ declared = decodeConsistencyProofFromUnprotected(unprotected);
285
+ } catch {
286
+ return null;
287
+ }
288
+ if (declared === null) return null;
289
+ try {
290
+ return readProtectedTreeSize2(coseSign1[0]);
291
+ } catch {
292
+ return null;
273
293
  }
274
- return null;
275
294
  }