@mikeargento/bitgraph-verify 1.1.1 → 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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/proof-hash.d.ts +39 -9
- package/dist/proof-hash.d.ts.map +1 -1
- package/dist/proof-hash.js +124 -26
- package/dist/proof-hash.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/proof-hash.ts +126 -26
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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"}
|
package/dist/proof-hash.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { BitGraphProof } from "./types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Compute the
|
|
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
|
|
@@ -8,22 +12,48 @@ import type { BitGraphProof } from "./types.js";
|
|
|
8
12
|
export declare function computeProofHash(proof: BitGraphProof | Record<string, unknown>): string;
|
|
9
13
|
/**
|
|
10
14
|
* Compute the CHAIN hash of a BitGraph proof: SHA-256 over the canonicalized
|
|
11
|
-
* whole proof object, with the ledger-added
|
|
15
|
+
* whole proof object, with the ledger/service-added fields removed.
|
|
12
16
|
*
|
|
13
17
|
* This is the value the enclave writes into the NEXT proof's
|
|
14
18
|
* `commit.prevB64` (and into `epochLink.prevProofHashB64` at an epoch
|
|
15
19
|
* boundary): the enclave hashes the entire assembled proof, before the ledger
|
|
16
|
-
* appends its convenience
|
|
17
|
-
* `commit.prevB64` against `computeChainHash(predecessor)`.
|
|
20
|
+
* or anchor service appends its convenience fields. To link a chain, match a
|
|
21
|
+
* proof's `commit.prevB64` against `computeChainHash(predecessor)`.
|
|
18
22
|
*
|
|
19
23
|
* This differs from computeProofHash, which hashes only the signed-body
|
|
20
|
-
* subset. The chain hash covers every field (signer
|
|
21
|
-
* report, slot allocation, attribution, and so on), so
|
|
22
|
-
* a proof breaks the link from its successor.
|
|
24
|
+
* subset. The chain hash covers every enclave-produced field (signer
|
|
25
|
+
* signature, attestation report, slot allocation, attribution, and so on), so
|
|
26
|
+
* any change anywhere in a proof breaks the link from its successor.
|
|
23
27
|
*
|
|
24
|
-
* @param proof - The full BitGraphProof object (or equivalent Record).
|
|
25
|
-
*
|
|
28
|
+
* @param proof - The full BitGraphProof object (or equivalent Record). Any
|
|
29
|
+
* ledger/service-added field (see LEDGER_ADDED_FIELDS) is excluded before
|
|
30
|
+
* hashing.
|
|
26
31
|
* @returns Base64-standard encoded SHA-256 hash
|
|
27
32
|
*/
|
|
28
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;
|
|
29
59
|
//# sourceMappingURL=proof-hash.d.ts.map
|
package/dist/proof-hash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proof-hash.d.ts","sourceRoot":"","sources":["../src/proof-hash.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/proof-hash.js
CHANGED
|
@@ -4,28 +4,53 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Canonical, deterministic proof hash computation.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
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
|
|
@@ -69,34 +98,54 @@ export function computeProofHash(proof) {
|
|
|
69
98
|
const hash = sha256(bytes);
|
|
70
99
|
return base64(hash);
|
|
71
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Fields that are NOT produced by the enclave. They are appended to a proof
|
|
103
|
+
* AFTER it is signed, by the ledger or the anchor service, purely as read-time
|
|
104
|
+
* convenience. The enclave computed its chain hash before any of these
|
|
105
|
+
* existed, so they MUST be excluded from computeChainHash — otherwise a
|
|
106
|
+
* successor's `commit.prevB64` (computed by the enclave over the clean proof)
|
|
107
|
+
* can never match.
|
|
108
|
+
*
|
|
109
|
+
* proofHash — SHA-256 of the signed-body subset; added by the ledger at
|
|
110
|
+
* write time (see computeProofHash above).
|
|
111
|
+
* ethereum — { blockNumber, blockHash }; added by the anchor service to
|
|
112
|
+
* anchor records after the transaction is mined. Purely
|
|
113
|
+
* redundant: the block hash is already in the signed
|
|
114
|
+
* `attribution.message` and the block number in
|
|
115
|
+
* `attribution.title`. Present only on Ethereum-anchor proofs.
|
|
116
|
+
*
|
|
117
|
+
* Any future ledger/service-added field must be added here as well.
|
|
118
|
+
*/
|
|
119
|
+
const LEDGER_ADDED_FIELDS = ["proofHash", "ethereum"];
|
|
72
120
|
/**
|
|
73
121
|
* Compute the CHAIN hash of a BitGraph proof: SHA-256 over the canonicalized
|
|
74
|
-
* whole proof object, with the ledger-added
|
|
122
|
+
* whole proof object, with the ledger/service-added fields removed.
|
|
75
123
|
*
|
|
76
124
|
* This is the value the enclave writes into the NEXT proof's
|
|
77
125
|
* `commit.prevB64` (and into `epochLink.prevProofHashB64` at an epoch
|
|
78
126
|
* boundary): the enclave hashes the entire assembled proof, before the ledger
|
|
79
|
-
* appends its convenience
|
|
80
|
-
* `commit.prevB64` against `computeChainHash(predecessor)`.
|
|
127
|
+
* or anchor service appends its convenience fields. To link a chain, match a
|
|
128
|
+
* proof's `commit.prevB64` against `computeChainHash(predecessor)`.
|
|
81
129
|
*
|
|
82
130
|
* This differs from computeProofHash, which hashes only the signed-body
|
|
83
|
-
* subset. The chain hash covers every field (signer
|
|
84
|
-
* report, slot allocation, attribution, and so on), so
|
|
85
|
-
* a proof breaks the link from its successor.
|
|
131
|
+
* subset. The chain hash covers every enclave-produced field (signer
|
|
132
|
+
* signature, attestation report, slot allocation, attribution, and so on), so
|
|
133
|
+
* any change anywhere in a proof breaks the link from its successor.
|
|
86
134
|
*
|
|
87
|
-
* @param proof - The full BitGraphProof object (or equivalent Record).
|
|
88
|
-
*
|
|
135
|
+
* @param proof - The full BitGraphProof object (or equivalent Record). Any
|
|
136
|
+
* ledger/service-added field (see LEDGER_ADDED_FIELDS) is excluded before
|
|
137
|
+
* hashing.
|
|
89
138
|
* @returns Base64-standard encoded SHA-256 hash
|
|
90
139
|
*/
|
|
91
140
|
export function computeChainHash(proof) {
|
|
92
141
|
const p = proof;
|
|
93
142
|
let hashable = p;
|
|
94
|
-
if (Object.prototype.hasOwnProperty.call(p,
|
|
95
|
-
// Shallow copy without the ledger-added
|
|
96
|
-
// proof before
|
|
143
|
+
if (LEDGER_ADDED_FIELDS.some((k) => Object.prototype.hasOwnProperty.call(p, k))) {
|
|
144
|
+
// Shallow copy without the ledger/service-added fields; the enclave hashed
|
|
145
|
+
// the proof before these existed.
|
|
97
146
|
hashable = {};
|
|
98
147
|
for (const key of Object.keys(p)) {
|
|
99
|
-
if (key
|
|
148
|
+
if (!LEDGER_ADDED_FIELDS.includes(key))
|
|
100
149
|
hashable[key] = p[key];
|
|
101
150
|
}
|
|
102
151
|
}
|
|
@@ -114,4 +163,53 @@ function base64(hash) {
|
|
|
114
163
|
}
|
|
115
164
|
return btoa(binary);
|
|
116
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
|
+
}
|
|
117
215
|
//# sourceMappingURL=proof-hash.js.map
|
package/dist/proof-hash.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proof-hash.js","sourceRoot":"","sources":["../src/proof-hash.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAErF
|
|
1
|
+
{"version":3,"file":"proof-hash.js","sourceRoot":"","sources":["../src/proof-hash.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
|
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
|
|
@@ -78,34 +107,55 @@ export function computeProofHash(proof: BitGraphProof | Record<string, unknown>)
|
|
|
78
107
|
return base64(hash);
|
|
79
108
|
}
|
|
80
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Fields that are NOT produced by the enclave. They are appended to a proof
|
|
112
|
+
* AFTER it is signed, by the ledger or the anchor service, purely as read-time
|
|
113
|
+
* convenience. The enclave computed its chain hash before any of these
|
|
114
|
+
* existed, so they MUST be excluded from computeChainHash — otherwise a
|
|
115
|
+
* successor's `commit.prevB64` (computed by the enclave over the clean proof)
|
|
116
|
+
* can never match.
|
|
117
|
+
*
|
|
118
|
+
* proofHash — SHA-256 of the signed-body subset; added by the ledger at
|
|
119
|
+
* write time (see computeProofHash above).
|
|
120
|
+
* ethereum — { blockNumber, blockHash }; added by the anchor service to
|
|
121
|
+
* anchor records after the transaction is mined. Purely
|
|
122
|
+
* redundant: the block hash is already in the signed
|
|
123
|
+
* `attribution.message` and the block number in
|
|
124
|
+
* `attribution.title`. Present only on Ethereum-anchor proofs.
|
|
125
|
+
*
|
|
126
|
+
* Any future ledger/service-added field must be added here as well.
|
|
127
|
+
*/
|
|
128
|
+
const LEDGER_ADDED_FIELDS: readonly string[] = ["proofHash", "ethereum"];
|
|
129
|
+
|
|
81
130
|
/**
|
|
82
131
|
* Compute the CHAIN hash of a BitGraph proof: SHA-256 over the canonicalized
|
|
83
|
-
* whole proof object, with the ledger-added
|
|
132
|
+
* whole proof object, with the ledger/service-added fields removed.
|
|
84
133
|
*
|
|
85
134
|
* This is the value the enclave writes into the NEXT proof's
|
|
86
135
|
* `commit.prevB64` (and into `epochLink.prevProofHashB64` at an epoch
|
|
87
136
|
* boundary): the enclave hashes the entire assembled proof, before the ledger
|
|
88
|
-
* appends its convenience
|
|
89
|
-
* `commit.prevB64` against `computeChainHash(predecessor)`.
|
|
137
|
+
* or anchor service appends its convenience fields. To link a chain, match a
|
|
138
|
+
* proof's `commit.prevB64` against `computeChainHash(predecessor)`.
|
|
90
139
|
*
|
|
91
140
|
* This differs from computeProofHash, which hashes only the signed-body
|
|
92
|
-
* subset. The chain hash covers every field (signer
|
|
93
|
-
* report, slot allocation, attribution, and so on), so
|
|
94
|
-
* a proof breaks the link from its successor.
|
|
141
|
+
* subset. The chain hash covers every enclave-produced field (signer
|
|
142
|
+
* signature, attestation report, slot allocation, attribution, and so on), so
|
|
143
|
+
* any change anywhere in a proof breaks the link from its successor.
|
|
95
144
|
*
|
|
96
|
-
* @param proof - The full BitGraphProof object (or equivalent Record).
|
|
97
|
-
*
|
|
145
|
+
* @param proof - The full BitGraphProof object (or equivalent Record). Any
|
|
146
|
+
* ledger/service-added field (see LEDGER_ADDED_FIELDS) is excluded before
|
|
147
|
+
* hashing.
|
|
98
148
|
* @returns Base64-standard encoded SHA-256 hash
|
|
99
149
|
*/
|
|
100
150
|
export function computeChainHash(proof: BitGraphProof | Record<string, unknown>): string {
|
|
101
151
|
const p = proof as Record<string, unknown>;
|
|
102
152
|
let hashable: Record<string, unknown> = p;
|
|
103
|
-
if (Object.prototype.hasOwnProperty.call(p,
|
|
104
|
-
// Shallow copy without the ledger-added
|
|
105
|
-
// proof before
|
|
153
|
+
if (LEDGER_ADDED_FIELDS.some((k) => Object.prototype.hasOwnProperty.call(p, k))) {
|
|
154
|
+
// Shallow copy without the ledger/service-added fields; the enclave hashed
|
|
155
|
+
// the proof before these existed.
|
|
106
156
|
hashable = {};
|
|
107
157
|
for (const key of Object.keys(p)) {
|
|
108
|
-
if (key
|
|
158
|
+
if (!LEDGER_ADDED_FIELDS.includes(key)) hashable[key] = p[key];
|
|
109
159
|
}
|
|
110
160
|
}
|
|
111
161
|
const bytes = canonicalize(hashable as unknown as BitGraphProof);
|
|
@@ -123,3 +173,53 @@ function base64(hash: Uint8Array): string {
|
|
|
123
173
|
}
|
|
124
174
|
return btoa(binary);
|
|
125
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
|
+
}
|