@mikeargento/bitgraph-verify 1.1.2 → 1.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.
package/dist/index.d.ts CHANGED
@@ -8,6 +8,6 @@
8
8
  export type { BitGraphProof, BitGraphPolicy, VerificationPolicy, SignedBody, EnforcementTier, Attribution, PolicyBinding, SlotAllocation, ActorIdentity, AuthorizationPayload, WebAuthnAuthorization, AgencyEnvelope, } from "./types.js";
9
9
  export { verify, verifyProofIntegrity, resetEpochLinkState } from "./verifier.js";
10
10
  export type { VerifyResult, ProofIntegrityResult } from "./verifier.js";
11
- export { computeProofHash, computeChainHash } from "./proof-hash.js";
11
+ export { computeProofHash, computeChainHash, buildSignedBody, computeSignedBodyHash } from "./proof-hash.js";
12
12
  export { canonicalize, canonicalizeToString, constantTimeEqual } from "./canonical.js";
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,YAAY,EACV,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,UAAU,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACrB,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAClF,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAExE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAErE,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,YAAY,EACV,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,UAAU,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACrB,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAClF,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAExE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7G,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
2
  export { verify, verifyProofIntegrity, resetEpochLinkState } from "./verifier.js";
3
- export { computeProofHash, computeChainHash } from "./proof-hash.js";
3
+ export { computeProofHash, computeChainHash, buildSignedBody, computeSignedBodyHash } from "./proof-hash.js";
4
4
  export { canonicalize, canonicalizeToString, constantTimeEqual } from "./canonical.js";
5
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAyBrF,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGlF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAErE,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAyBrF,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGlF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7G,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -1,6 +1,10 @@
1
1
  import type { BitGraphProof } from "./types.js";
2
2
  /**
3
- * Compute the canonical hash of a BitGraph proof's signed body.
3
+ * Compute the ledger identity hash of a BitGraph proof.
4
+ *
5
+ * Hashes the frozen signed-body subset described at the top of this file.
6
+ * This is NOT the value the Ed25519 signature covers and NOT the attestation
7
+ * user_data; for that, reconstruct the full signed body (verifier.ts step 3).
4
8
  *
5
9
  * @param proof - The full BitGraphProof object (or equivalent Record)
6
10
  * @returns Base64-standard encoded SHA-256 hash
@@ -27,4 +31,29 @@ export declare function computeProofHash(proof: BitGraphProof | Record<string, u
27
31
  * @returns Base64-standard encoded SHA-256 hash
28
32
  */
29
33
  export declare function computeChainHash(proof: BitGraphProof | Record<string, unknown>): string;
34
+ /**
35
+ * Reconstruct the FULL canonical signed body: the bytes the Ed25519 signature
36
+ * actually covers, and the bytes whose SHA-256 the enclave places in the Nitro
37
+ * attestation's `user_data`.
38
+ *
39
+ * ⚠️ THIS IS HASH (2) IN THE LIST AT THE TOP OF THIS FILE, AND IT IS NOT
40
+ * computeProofHash. For a proof carrying neither `actor` nor `policy` the two
41
+ * coincide, which is why substituting one for the other survives every
42
+ * ordinary proof and fails only on agency or policy proofs — a bug that hides
43
+ * until the day the feature ships. That is exactly what happened: the offline
44
+ * auditor compared the attestation's user_data against computeProofHash and
45
+ * reported every declared recording as belonging to "some other proof",
46
+ * turning a valid proof into a FALSE (2026-08-18, found on the first real
47
+ * declaration, ledger position #12,010).
48
+ *
49
+ * Signature verification and attestation binding MUST both use this function
50
+ * so they cannot drift apart again.
51
+ */
52
+ export declare function buildSignedBody(proof: BitGraphProof | Record<string, unknown>): Record<string, unknown>;
53
+ /**
54
+ * base64(SHA-256(canonical full signed body)). The value the enclave writes
55
+ * into the attestation's user_data, and the digest the Ed25519 signature
56
+ * covers. See buildSignedBody for why this is never computeProofHash.
57
+ */
58
+ export declare function computeSignedBodyHash(proof: BitGraphProof | Record<string, unknown>): string;
30
59
  //# sourceMappingURL=proof-hash.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proof-hash.d.ts","sourceRoot":"","sources":["../src/proof-hash.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CA2BvF;AAsBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAavF"}
1
+ {"version":3,"file":"proof-hash.d.ts","sourceRoot":"","sources":["../src/proof-hash.ts"],"names":[],"mappings":"AAoEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CA2BvF;AAsBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAavF;AAcD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqBvG;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAE5F"}
@@ -4,28 +4,53 @@
4
4
  *
5
5
  * Canonical, deterministic proof hash computation.
6
6
  *
7
- * The proof hash covers the SIGNED BODY the fields that are
8
- * cryptographically signed by the enclave. This matches what the
9
- * Ed25519 signature covers, making the hash verifiable.
10
- *
11
- * Signed body fields:
7
+ * This is the LEDGER IDENTITY of a proof. It hashes a fixed SUBSET of the
8
+ * signed body:
12
9
  * - version, artifact, commit
13
10
  * - publicKeyB64 (from signer)
14
11
  * - enforcement, measurement (from environment)
15
12
  * - attribution (if present)
16
13
  * - attestationFormat (if attestation present)
17
14
  *
18
- * This hash (the signed-body hash) is used for:
19
- * - S3 ledger key generation
15
+ * Used for:
16
+ * - S3 ledger key generation — proofs/{epoch}/{counter}-{proofHash}.json
20
17
  * - Ethereum anchor binding
21
18
  * - Proof deduplication and canonical identity
22
19
  *
23
- * It is NOT the value referenced by commit.prevB64 or by
24
- * epochLink.prevProofHashB64. Those reference the CHAIN hash: SHA-256 over
25
- * the canonicalized WHOLE proof (every field, minus the ledger-added
26
- * proofHash), computed by computeChainHash below. Matching prevB64 against
27
- * computeProofHash instead of computeChainHash silently fails to link real
28
- * chains.
20
+ * ── THE FIELD LIST ABOVE IS FROZEN. DO NOT ADD TO IT. ──
21
+ *
22
+ * It is tempting to "fix" this function by adding `actor` and `policy`, which
23
+ * ARE in the signed body the enclave signs. Doing so is a silent, permanent
24
+ * break: this value is baked into every S3 object key already written, and
25
+ * ledger verify() compares a recomputed value against the stored `proofHash`
26
+ * field (packages/ledger/src/verify.ts). Widening the subset changes the hash
27
+ * of every existing agency-bearing or policy-bearing proof, orphaning its key
28
+ * and failing its hash check forever. The subset is a compatibility contract,
29
+ * not an approximation of the signature.
30
+ *
31
+ * BitGraph has THREE distinct proof hashes. Confusing them is the single
32
+ * easiest way to break the system, so they are spelled out here:
33
+ *
34
+ * 1. computeProofHash (this function) — signed-body SUBSET.
35
+ * Ledger identity and S3 key. Frozen. Not a signature-equivalent value.
36
+ *
37
+ * 2. SHA-256 of the FULL canonical signed body — what the Ed25519 signature
38
+ * actually covers, and what the enclave puts in the Nitro attestation's
39
+ * user_data. Reconstructed by verifier.ts (step 3) and, in the browser,
40
+ * by buildSignedBody()/proofHashB64() in website/src/lib/bitgraph.ts.
41
+ * For a proof with no `actor` and no `policy` this coincides with (1);
42
+ * for agency or policy proofs it does NOT. Use this for signature and
43
+ * attestation checks. Never use (1).
44
+ *
45
+ * 3. computeChainHash (below) — the WHOLE proof, minus ledger-added fields.
46
+ * This is what the enclave writes into the next proof's commit.prevB64
47
+ * and into epochLink.prevProofHashB64. Matching prevB64 against (1)
48
+ * silently fails to link real chains.
49
+ *
50
+ * packages/hosted/src/bitcoin-anchor.ts carries a deliberate inline copy of
51
+ * (1) because Railway cannot resolve the monorepo package. It mirrors this
52
+ * field list exactly and must continue to; if this list ever does change,
53
+ * that copy changes with it in the same commit.
29
54
  *
30
55
  * Algorithm:
31
56
  * 1. Extract signed body fields
@@ -40,7 +65,11 @@
40
65
  import { canonicalize } from "./canonical.js";
41
66
  import { sha256 } from "@noble/hashes/sha256";
42
67
  /**
43
- * Compute the canonical hash of a BitGraph proof's signed body.
68
+ * Compute the ledger identity hash of a BitGraph proof.
69
+ *
70
+ * Hashes the frozen signed-body subset described at the top of this file.
71
+ * This is NOT the value the Ed25519 signature covers and NOT the attestation
72
+ * user_data; for that, reconstruct the full signed body (verifier.ts step 3).
44
73
  *
45
74
  * @param proof - The full BitGraphProof object (or equivalent Record)
46
75
  * @returns Base64-standard encoded SHA-256 hash
@@ -134,4 +163,53 @@ function base64(hash) {
134
163
  }
135
164
  return btoa(binary);
136
165
  }
166
+ /**
167
+ * Reconstruct the FULL canonical signed body: the bytes the Ed25519 signature
168
+ * actually covers, and the bytes whose SHA-256 the enclave places in the Nitro
169
+ * attestation's `user_data`.
170
+ *
171
+ * ⚠️ THIS IS HASH (2) IN THE LIST AT THE TOP OF THIS FILE, AND IT IS NOT
172
+ * computeProofHash. For a proof carrying neither `actor` nor `policy` the two
173
+ * coincide, which is why substituting one for the other survives every
174
+ * ordinary proof and fails only on agency or policy proofs — a bug that hides
175
+ * until the day the feature ships. That is exactly what happened: the offline
176
+ * auditor compared the attestation's user_data against computeProofHash and
177
+ * reported every declared recording as belonging to "some other proof",
178
+ * turning a valid proof into a FALSE (2026-08-18, found on the first real
179
+ * declaration, ledger position #12,010).
180
+ *
181
+ * Signature verification and attestation binding MUST both use this function
182
+ * so they cannot drift apart again.
183
+ */
184
+ export function buildSignedBody(proof) {
185
+ const p = proof;
186
+ const signer = p.signer;
187
+ const env = p.environment;
188
+ const body = {
189
+ version: p.version,
190
+ artifact: p.artifact,
191
+ commit: p.commit,
192
+ publicKeyB64: signer?.publicKeyB64,
193
+ enforcement: env?.enforcement,
194
+ measurement: env?.measurement,
195
+ };
196
+ if (env?.attestation !== undefined)
197
+ body.attestationFormat = env.attestation.format;
198
+ const agency = p.agency;
199
+ if (agency !== undefined)
200
+ body.actor = agency.actor;
201
+ if (p.policy !== undefined)
202
+ body.policy = p.policy;
203
+ if (p.attribution !== undefined)
204
+ body.attribution = p.attribution;
205
+ return body;
206
+ }
207
+ /**
208
+ * base64(SHA-256(canonical full signed body)). The value the enclave writes
209
+ * into the attestation's user_data, and the digest the Ed25519 signature
210
+ * covers. See buildSignedBody for why this is never computeProofHash.
211
+ */
212
+ export function computeSignedBodyHash(proof) {
213
+ return base64(sha256(canonicalize(buildSignedBody(proof))));
214
+ }
137
215
  //# sourceMappingURL=proof-hash.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"proof-hash.js","sourceRoot":"","sources":["../src/proof-hash.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAErF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAG9C;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAA8C;IAC7E,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,MAAM,MAAM,GAAG,CAAC,CAAC,MAA8C,CAAC;IAChE,MAAM,GAAG,GAAG,CAAC,CAAC,WAAyG,CAAC;IAExH,MAAM,UAAU,GAA4B;QAC1C,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,YAAY,EAAE,MAAM,EAAE,YAAY;QAClC,WAAW,EAAE,GAAG,EAAE,WAAW;QAC7B,WAAW,EAAE,GAAG,EAAE,WAAW;KAC9B,CAAC;IAEF,iCAAiC;IACjC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QAClB,UAAU,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW,CAAC;IACzC,CAAC;IAED,wCAAwC;IACxC,IAAI,GAAG,EAAE,WAAW,EAAE,CAAC;QACrB,UAAU,CAAC,iBAAiB,GAAG,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC;IACxD,CAAC;IAED,MAAM,KAAK,GAAG,YAAY,CAAC,UAAsC,CAAC,CAAC;IACnE,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,mBAAmB,GAAsB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAA8C;IAC7E,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,IAAI,QAAQ,GAA4B,CAAC,CAAC;IAC1C,IAAI,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChF,2EAA2E;QAC3E,kCAAkC;QAClC,QAAQ,GAAG,EAAE,CAAC;QACd,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,YAAY,CAAC,QAAoC,CAAC,CAAC;IACjE,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED,8DAA8D;AAC9D,SAAS,MAAM,CAAC,IAAgB;IAC9B,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC"}
1
+ {"version":3,"file":"proof-hash.js","sourceRoot":"","sources":["../src/proof-hash.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikeargento/bitgraph-verify",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Offline verification of BitGraph proofs. Permissionless by design.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -26,6 +26,6 @@ export type {
26
26
  export { verify, verifyProofIntegrity, resetEpochLinkState } from "./verifier.js";
27
27
  export type { VerifyResult, ProofIntegrityResult } from "./verifier.js";
28
28
 
29
- export { computeProofHash, computeChainHash } from "./proof-hash.js";
29
+ export { computeProofHash, computeChainHash, buildSignedBody, computeSignedBodyHash } from "./proof-hash.js";
30
30
 
31
31
  export { canonicalize, canonicalizeToString, constantTimeEqual } from "./canonical.js";
package/src/proof-hash.ts CHANGED
@@ -5,28 +5,53 @@
5
5
  *
6
6
  * Canonical, deterministic proof hash computation.
7
7
  *
8
- * The proof hash covers the SIGNED BODY the fields that are
9
- * cryptographically signed by the enclave. This matches what the
10
- * Ed25519 signature covers, making the hash verifiable.
11
- *
12
- * Signed body fields:
8
+ * This is the LEDGER IDENTITY of a proof. It hashes a fixed SUBSET of the
9
+ * signed body:
13
10
  * - version, artifact, commit
14
11
  * - publicKeyB64 (from signer)
15
12
  * - enforcement, measurement (from environment)
16
13
  * - attribution (if present)
17
14
  * - attestationFormat (if attestation present)
18
15
  *
19
- * This hash (the signed-body hash) is used for:
20
- * - S3 ledger key generation
16
+ * Used for:
17
+ * - S3 ledger key generation — proofs/{epoch}/{counter}-{proofHash}.json
21
18
  * - Ethereum anchor binding
22
19
  * - Proof deduplication and canonical identity
23
20
  *
24
- * It is NOT the value referenced by commit.prevB64 or by
25
- * epochLink.prevProofHashB64. Those reference the CHAIN hash: SHA-256 over
26
- * the canonicalized WHOLE proof (every field, minus the ledger-added
27
- * proofHash), computed by computeChainHash below. Matching prevB64 against
28
- * computeProofHash instead of computeChainHash silently fails to link real
29
- * chains.
21
+ * ── THE FIELD LIST ABOVE IS FROZEN. DO NOT ADD TO IT. ──
22
+ *
23
+ * It is tempting to "fix" this function by adding `actor` and `policy`, which
24
+ * ARE in the signed body the enclave signs. Doing so is a silent, permanent
25
+ * break: this value is baked into every S3 object key already written, and
26
+ * ledger verify() compares a recomputed value against the stored `proofHash`
27
+ * field (packages/ledger/src/verify.ts). Widening the subset changes the hash
28
+ * of every existing agency-bearing or policy-bearing proof, orphaning its key
29
+ * and failing its hash check forever. The subset is a compatibility contract,
30
+ * not an approximation of the signature.
31
+ *
32
+ * BitGraph has THREE distinct proof hashes. Confusing them is the single
33
+ * easiest way to break the system, so they are spelled out here:
34
+ *
35
+ * 1. computeProofHash (this function) — signed-body SUBSET.
36
+ * Ledger identity and S3 key. Frozen. Not a signature-equivalent value.
37
+ *
38
+ * 2. SHA-256 of the FULL canonical signed body — what the Ed25519 signature
39
+ * actually covers, and what the enclave puts in the Nitro attestation's
40
+ * user_data. Reconstructed by verifier.ts (step 3) and, in the browser,
41
+ * by buildSignedBody()/proofHashB64() in website/src/lib/bitgraph.ts.
42
+ * For a proof with no `actor` and no `policy` this coincides with (1);
43
+ * for agency or policy proofs it does NOT. Use this for signature and
44
+ * attestation checks. Never use (1).
45
+ *
46
+ * 3. computeChainHash (below) — the WHOLE proof, minus ledger-added fields.
47
+ * This is what the enclave writes into the next proof's commit.prevB64
48
+ * and into epochLink.prevProofHashB64. Matching prevB64 against (1)
49
+ * silently fails to link real chains.
50
+ *
51
+ * packages/hosted/src/bitcoin-anchor.ts carries a deliberate inline copy of
52
+ * (1) because Railway cannot resolve the monorepo package. It mirrors this
53
+ * field list exactly and must continue to; if this list ever does change,
54
+ * that copy changes with it in the same commit.
30
55
  *
31
56
  * Algorithm:
32
57
  * 1. Extract signed body fields
@@ -44,7 +69,11 @@ import { sha256 } from "@noble/hashes/sha256";
44
69
  import type { BitGraphProof } from "./types.js";
45
70
 
46
71
  /**
47
- * Compute the canonical hash of a BitGraph proof's signed body.
72
+ * Compute the ledger identity hash of a BitGraph proof.
73
+ *
74
+ * Hashes the frozen signed-body subset described at the top of this file.
75
+ * This is NOT the value the Ed25519 signature covers and NOT the attestation
76
+ * user_data; for that, reconstruct the full signed body (verifier.ts step 3).
48
77
  *
49
78
  * @param proof - The full BitGraphProof object (or equivalent Record)
50
79
  * @returns Base64-standard encoded SHA-256 hash
@@ -144,3 +173,53 @@ function base64(hash: Uint8Array): string {
144
173
  }
145
174
  return btoa(binary);
146
175
  }
176
+
177
+ /**
178
+ * Reconstruct the FULL canonical signed body: the bytes the Ed25519 signature
179
+ * actually covers, and the bytes whose SHA-256 the enclave places in the Nitro
180
+ * attestation's `user_data`.
181
+ *
182
+ * ⚠️ THIS IS HASH (2) IN THE LIST AT THE TOP OF THIS FILE, AND IT IS NOT
183
+ * computeProofHash. For a proof carrying neither `actor` nor `policy` the two
184
+ * coincide, which is why substituting one for the other survives every
185
+ * ordinary proof and fails only on agency or policy proofs — a bug that hides
186
+ * until the day the feature ships. That is exactly what happened: the offline
187
+ * auditor compared the attestation's user_data against computeProofHash and
188
+ * reported every declared recording as belonging to "some other proof",
189
+ * turning a valid proof into a FALSE (2026-08-18, found on the first real
190
+ * declaration, ledger position #12,010).
191
+ *
192
+ * Signature verification and attestation binding MUST both use this function
193
+ * so they cannot drift apart again.
194
+ */
195
+ export function buildSignedBody(proof: BitGraphProof | Record<string, unknown>): Record<string, unknown> {
196
+ const p = proof as Record<string, unknown>;
197
+ const signer = p.signer as { publicKeyB64?: string } | undefined;
198
+ const env = p.environment as
199
+ | { enforcement?: string; measurement?: string; attestation?: { format?: string } }
200
+ | undefined;
201
+
202
+ const body: Record<string, unknown> = {
203
+ version: p.version,
204
+ artifact: p.artifact,
205
+ commit: p.commit,
206
+ publicKeyB64: signer?.publicKeyB64,
207
+ enforcement: env?.enforcement,
208
+ measurement: env?.measurement,
209
+ };
210
+ if (env?.attestation !== undefined) body.attestationFormat = env.attestation.format;
211
+ const agency = p.agency as { actor?: unknown } | undefined;
212
+ if (agency !== undefined) body.actor = agency.actor;
213
+ if (p.policy !== undefined) body.policy = p.policy;
214
+ if (p.attribution !== undefined) body.attribution = p.attribution;
215
+ return body;
216
+ }
217
+
218
+ /**
219
+ * base64(SHA-256(canonical full signed body)). The value the enclave writes
220
+ * into the attestation's user_data, and the digest the Ed25519 signature
221
+ * covers. See buildSignedBody for why this is never computeProofHash.
222
+ */
223
+ export function computeSignedBodyHash(proof: BitGraphProof | Record<string, unknown>): string {
224
+ return base64(sha256(canonicalize(buildSignedBody(proof) as unknown as BitGraphProof)));
225
+ }