@mikeargento/bitgraph 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/NOTICE +14 -0
- package/README.md +175 -0
- package/dist/__tests__/canonical.test.d.ts +2 -0
- package/dist/__tests__/canonical.test.d.ts.map +1 -0
- package/dist/__tests__/canonical.test.js +162 -0
- package/dist/__tests__/canonical.test.js.map +1 -0
- package/dist/__tests__/commit-service.integration.test.d.ts +2 -0
- package/dist/__tests__/commit-service.integration.test.d.ts.map +1 -0
- package/dist/__tests__/commit-service.integration.test.js +95 -0
- package/dist/__tests__/commit-service.integration.test.js.map +1 -0
- package/dist/__tests__/constructor.test.d.ts +2 -0
- package/dist/__tests__/constructor.test.d.ts.map +1 -0
- package/dist/__tests__/constructor.test.js +267 -0
- package/dist/__tests__/constructor.test.js.map +1 -0
- package/dist/__tests__/hello-world.test.d.ts +2 -0
- package/dist/__tests__/hello-world.test.d.ts.map +1 -0
- package/dist/__tests__/hello-world.test.js +9 -0
- package/dist/__tests__/hello-world.test.js.map +1 -0
- package/dist/__tests__/policy-enforcement.test.d.ts +2 -0
- package/dist/__tests__/policy-enforcement.test.d.ts.map +1 -0
- package/dist/__tests__/policy-enforcement.test.js +188 -0
- package/dist/__tests__/policy-enforcement.test.js.map +1 -0
- package/dist/__tests__/proof-hash-regression.test.d.ts +2 -0
- package/dist/__tests__/proof-hash-regression.test.d.ts.map +1 -0
- package/dist/__tests__/proof-hash-regression.test.js +134 -0
- package/dist/__tests__/proof-hash-regression.test.js.map +1 -0
- package/dist/__tests__/proof-hash.test.d.ts +2 -0
- package/dist/__tests__/proof-hash.test.d.ts.map +1 -0
- package/dist/__tests__/proof-hash.test.js +134 -0
- package/dist/__tests__/proof-hash.test.js.map +1 -0
- package/dist/__tests__/verifier.test.d.ts +2 -0
- package/dist/__tests__/verifier.test.d.ts.map +1 -0
- package/dist/__tests__/verifier.test.js +457 -0
- package/dist/__tests__/verifier.test.js.map +1 -0
- package/dist/canonical.d.ts +54 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +119 -0
- package/dist/canonical.js.map +1 -0
- package/dist/constructor.d.ts +66 -0
- package/dist/constructor.d.ts.map +1 -0
- package/dist/constructor.js +339 -0
- package/dist/constructor.js.map +1 -0
- package/dist/host.d.ts +138 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +3 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +68 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +176 -0
- package/dist/policy.js.map +1 -0
- package/dist/proof-hash.d.ts +9 -0
- package/dist/proof-hash.d.ts.map +1 -0
- package/dist/proof-hash.js +75 -0
- package/dist/proof-hash.js.map +1 -0
- package/dist/types.d.ts +705 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/verifier.d.ts +27 -0
- package/dist/verifier.d.ts.map +1 -0
- package/dist/verifier.js +844 -0
- package/dist/verifier.js.map +1 -0
- package/package.json +40 -0
- package/src/__tests__/canonical.test.ts +253 -0
- package/src/__tests__/commit-service.integration.test.ts +117 -0
- package/src/__tests__/constructor.test.ts +340 -0
- package/src/__tests__/hello-world.test.ts +10 -0
- package/src/__tests__/policy-enforcement.test.ts +226 -0
- package/src/__tests__/proof-hash-regression.test.ts +147 -0
- package/src/__tests__/proof-hash.test.ts +148 -0
- package/src/__tests__/verifier.test.ts +541 -0
- package/src/constructor.ts +435 -0
- package/src/host.ts +153 -0
- package/src/index.ts +51 -0
- package/src/policy.ts +239 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test: all code paths must produce identical proofHash.
|
|
3
|
+
*
|
|
4
|
+
* Simulates the hash computation as done by:
|
|
5
|
+
* 1. computeProofHash() from bitgraph (canonical)
|
|
6
|
+
* 2. Inline signed-body + canonicalize + sha256 (how ledger s3.ts USED to do it)
|
|
7
|
+
* 3. Any other path
|
|
8
|
+
*
|
|
9
|
+
* If these ever diverge, this test fails.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it } from "node:test";
|
|
12
|
+
import * as assert from "node:assert/strict";
|
|
13
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
14
|
+
import { computeProofHash, canonicalize } from "../index.js";
|
|
15
|
+
|
|
16
|
+
// Realistic proof — mirrors what the TEE actually produces
|
|
17
|
+
const REALISTIC_PROOF = {
|
|
18
|
+
version: "bitgraph/1" as const,
|
|
19
|
+
artifact: {
|
|
20
|
+
hashAlg: "sha256" as const,
|
|
21
|
+
digestB64: "NkYJQ1Gi0hE0SIhv6c46MlqJdurmKHtbScOprDfoL6A=",
|
|
22
|
+
},
|
|
23
|
+
commit: {
|
|
24
|
+
nonceB64: "z1272XEvkMtibuBP0qG3oY37cewo6cJ/o1AuigOSVOk=",
|
|
25
|
+
counter: "1370",
|
|
26
|
+
slotCounter: "1369",
|
|
27
|
+
slotHashB64: "8IyrBOGB64ppP/RETEZTYyYesF5bfcwJ5dI9+u7r2yY=",
|
|
28
|
+
epochId: "7jU4N9703cm6A2t8YNoGsyOvbdePDpoaD8AKCFs7wko=",
|
|
29
|
+
prevB64: "KsJfA3MinpSwF+t4GuxU9vLmmaqAK8FhzhaepEnJBDc=",
|
|
30
|
+
chainId: "bitgraph:main",
|
|
31
|
+
},
|
|
32
|
+
signer: {
|
|
33
|
+
publicKeyB64: "9XjqKDS+e8NhhNoBqbisCKHpaR/HK6hH0x5AKdwc56w=",
|
|
34
|
+
signatureB64: "wbhAtN1dcF2nESBDB0sCQ4ebuZCav8ziLivYgofh7cXeupw5H97VyovHN4I/R9uS0vNaoPIP8js8uTkZnn/IBA==",
|
|
35
|
+
},
|
|
36
|
+
environment: {
|
|
37
|
+
enforcement: "measured-tee",
|
|
38
|
+
measurement: "638d655ad6091bed5c358628b7780de0cdbe138a37fe09d52bf8021a720680a2b3c730fee9f6bef79c1dbe68ef3cdd94",
|
|
39
|
+
attestation: {
|
|
40
|
+
format: "aws-nitro",
|
|
41
|
+
reportB64: "hEShATgioFkR...(truncated for test)",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
slotAllocation: {
|
|
45
|
+
version: "bitgraph/slot/1" as const,
|
|
46
|
+
nonceB64: "z1272XEvkMtibuBP0qG3oY37cewo6cJ/o1AuigOSVOk=",
|
|
47
|
+
counter: "1369",
|
|
48
|
+
epochId: "7jU4N9703cm6A2t8YNoGsyOvbdePDpoaD8AKCFs7wko=",
|
|
49
|
+
publicKeyB64: "9XjqKDS+e8NhhNoBqbisCKHpaR/HK6hH0x5AKdwc56w=",
|
|
50
|
+
chainId: "bitgraph:main",
|
|
51
|
+
signatureB64: "yO34pHfmkjGY6HgDxcPIaFd/lnt/UFZuHQa10LmUvTHgsQP3S3bIEJX/xcXPRENKZ9duVe7c+81rAWdmhiRMBg==",
|
|
52
|
+
},
|
|
53
|
+
attribution: {
|
|
54
|
+
name: "Ethereum Anchor",
|
|
55
|
+
title: "https://etherscan.io/block/24800448",
|
|
56
|
+
message: "0x28ed3639cd705fb8cb2b915c1991e9f808b40e775bc8eb540702942729fec2c0",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Inline signed-body hash — the OLD way ledger s3.ts computed it.
|
|
62
|
+
* This must produce the same result as computeProofHash().
|
|
63
|
+
*/
|
|
64
|
+
function inlineSignedBodyHash(proof: Record<string, unknown>): string {
|
|
65
|
+
const signer = proof.signer as { publicKeyB64: string };
|
|
66
|
+
const env = proof.environment as { enforcement: string; measurement: string; attestation?: { format: string } };
|
|
67
|
+
|
|
68
|
+
const signedBody: Record<string, unknown> = {
|
|
69
|
+
version: proof.version,
|
|
70
|
+
artifact: proof.artifact,
|
|
71
|
+
commit: proof.commit,
|
|
72
|
+
publicKeyB64: signer.publicKeyB64,
|
|
73
|
+
enforcement: env.enforcement,
|
|
74
|
+
measurement: env.measurement,
|
|
75
|
+
};
|
|
76
|
+
if (proof.attribution) signedBody.attribution = proof.attribution;
|
|
77
|
+
if (env.attestation) signedBody.attestationFormat = env.attestation.format;
|
|
78
|
+
|
|
79
|
+
return Buffer.from(sha256(canonicalize(signedBody))).toString("base64");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("proof hash regression — all paths must agree", () => {
|
|
83
|
+
it("computeProofHash matches inline signed-body hash (with attribution)", () => {
|
|
84
|
+
const canonical = computeProofHash(REALISTIC_PROOF);
|
|
85
|
+
const inline = inlineSignedBodyHash(REALISTIC_PROOF);
|
|
86
|
+
assert.equal(canonical, inline, "hash drift detected: computeProofHash vs inline");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("computeProofHash matches inline signed-body hash (without attribution)", () => {
|
|
90
|
+
const noAttr = { ...REALISTIC_PROOF } as Record<string, unknown>;
|
|
91
|
+
delete noAttr.attribution;
|
|
92
|
+
const canonical = computeProofHash(noAttr);
|
|
93
|
+
const inline = inlineSignedBodyHash(noAttr);
|
|
94
|
+
assert.equal(canonical, inline, "hash drift detected without attribution");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("computeProofHash matches inline signed-body hash (without attestation)", () => {
|
|
98
|
+
const noAttest = {
|
|
99
|
+
...REALISTIC_PROOF,
|
|
100
|
+
environment: {
|
|
101
|
+
enforcement: REALISTIC_PROOF.environment.enforcement,
|
|
102
|
+
measurement: REALISTIC_PROOF.environment.measurement,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
const canonical = computeProofHash(noAttest);
|
|
106
|
+
const inline = inlineSignedBodyHash(noAttest);
|
|
107
|
+
assert.equal(canonical, inline, "hash drift detected without attestation");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("anchor proof and user proof use the same hash function", () => {
|
|
111
|
+
// User proof (no attribution)
|
|
112
|
+
const userProof = { ...REALISTIC_PROOF } as Record<string, unknown>;
|
|
113
|
+
delete userProof.attribution;
|
|
114
|
+
|
|
115
|
+
// Anchor proof (has attribution)
|
|
116
|
+
const anchorProof = REALISTIC_PROOF;
|
|
117
|
+
|
|
118
|
+
// Both should work with the same function
|
|
119
|
+
const userHash = computeProofHash(userProof);
|
|
120
|
+
const anchorHash = computeProofHash(anchorProof);
|
|
121
|
+
|
|
122
|
+
// They should be different (different inputs) but both valid
|
|
123
|
+
assert.notEqual(userHash, anchorHash, "user and anchor proofs with different data should differ");
|
|
124
|
+
assert.equal(Buffer.from(userHash, "base64").length, 32, "user hash should be SHA-256");
|
|
125
|
+
assert.equal(Buffer.from(anchorHash, "base64").length, 32, "anchor hash should be SHA-256");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("adding slotAllocation or metadata does not change hash", () => {
|
|
129
|
+
const bare = { ...REALISTIC_PROOF } as Record<string, unknown>;
|
|
130
|
+
delete bare.slotAllocation;
|
|
131
|
+
|
|
132
|
+
const withSlot = { ...REALISTIC_PROOF };
|
|
133
|
+
|
|
134
|
+
const withMetadata = {
|
|
135
|
+
...REALISTIC_PROOF,
|
|
136
|
+
metadata: { type: "ethereum-anchor", extra: true },
|
|
137
|
+
timestamps: { artifact: { time: 1234567890 } },
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const bareHash = computeProofHash(bare);
|
|
141
|
+
const withSlotHash = computeProofHash(withSlot);
|
|
142
|
+
const withMetaHash = computeProofHash(withMetadata);
|
|
143
|
+
|
|
144
|
+
assert.equal(bareHash, withSlotHash, "slotAllocation should not affect hash");
|
|
145
|
+
assert.equal(bareHash, withMetaHash, "metadata/timestamps should not affect hash");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import * as assert from "node:assert/strict";
|
|
3
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
4
|
+
import { computeProofHash, canonicalize } from "../index.js";
|
|
5
|
+
import type { BitGraphProof } from "../index.js";
|
|
6
|
+
|
|
7
|
+
const MOCK_PROOF: BitGraphProof = {
|
|
8
|
+
version: "bitgraph/1",
|
|
9
|
+
artifact: { hashAlg: "sha256", digestB64: "abc123==" },
|
|
10
|
+
commit: {
|
|
11
|
+
nonceB64: "nonce==",
|
|
12
|
+
counter: "42",
|
|
13
|
+
slotCounter: "41",
|
|
14
|
+
epochId: "epoch==",
|
|
15
|
+
prevB64: "prev==",
|
|
16
|
+
},
|
|
17
|
+
signer: {
|
|
18
|
+
publicKeyB64: "pubkey==",
|
|
19
|
+
signatureB64: "sig==",
|
|
20
|
+
},
|
|
21
|
+
environment: {
|
|
22
|
+
enforcement: "measured-tee",
|
|
23
|
+
measurement: "pcr0hash",
|
|
24
|
+
attestation: { format: "aws-nitro", reportB64: "report==" },
|
|
25
|
+
},
|
|
26
|
+
attribution: { name: "test", message: "hello" },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe("computeProofHash", () => {
|
|
30
|
+
it("produces base64-encoded SHA-256", () => {
|
|
31
|
+
const hash = computeProofHash(MOCK_PROOF);
|
|
32
|
+
// Base64 of SHA-256 is always 44 chars (with padding)
|
|
33
|
+
assert.ok(hash.length > 0 && hash.length <= 44, `unexpected hash length: ${hash.length}`);
|
|
34
|
+
// Valid base64
|
|
35
|
+
const decoded = Buffer.from(hash, "base64");
|
|
36
|
+
assert.equal(decoded.length, 32, "SHA-256 output should be 32 bytes");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("matches manual signed-body computation", () => {
|
|
40
|
+
const signedBody: Record<string, unknown> = {
|
|
41
|
+
version: MOCK_PROOF.version,
|
|
42
|
+
artifact: MOCK_PROOF.artifact,
|
|
43
|
+
commit: MOCK_PROOF.commit,
|
|
44
|
+
publicKeyB64: MOCK_PROOF.signer.publicKeyB64,
|
|
45
|
+
enforcement: MOCK_PROOF.environment.enforcement,
|
|
46
|
+
measurement: MOCK_PROOF.environment.measurement,
|
|
47
|
+
attribution: MOCK_PROOF.attribution,
|
|
48
|
+
attestationFormat: MOCK_PROOF.environment.attestation!.format,
|
|
49
|
+
};
|
|
50
|
+
const expected = Buffer.from(sha256(canonicalize(signedBody))).toString("base64");
|
|
51
|
+
const actual = computeProofHash(MOCK_PROOF);
|
|
52
|
+
assert.equal(actual, expected, "computeProofHash must match manual signed-body hash");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("is deterministic — same input always produces same output", () => {
|
|
56
|
+
const a = computeProofHash(MOCK_PROOF);
|
|
57
|
+
const b = computeProofHash(MOCK_PROOF);
|
|
58
|
+
assert.equal(a, b);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("is not affected by key ordering in input", () => {
|
|
62
|
+
// Construct same proof with keys in different order
|
|
63
|
+
const reordered = {
|
|
64
|
+
environment: MOCK_PROOF.environment,
|
|
65
|
+
version: MOCK_PROOF.version,
|
|
66
|
+
signer: MOCK_PROOF.signer,
|
|
67
|
+
artifact: MOCK_PROOF.artifact,
|
|
68
|
+
commit: MOCK_PROOF.commit,
|
|
69
|
+
attribution: MOCK_PROOF.attribution,
|
|
70
|
+
} as unknown as BitGraphProof;
|
|
71
|
+
assert.equal(computeProofHash(reordered), computeProofHash(MOCK_PROOF));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("is not affected by metadata (outside signed body)", () => {
|
|
75
|
+
const withMetadata = {
|
|
76
|
+
...MOCK_PROOF,
|
|
77
|
+
metadata: { extra: "stuff", nested: { deep: true } },
|
|
78
|
+
timestamps: { artifact: { time: 12345 } },
|
|
79
|
+
} as unknown as BitGraphProof;
|
|
80
|
+
assert.equal(computeProofHash(withMetadata), computeProofHash(MOCK_PROOF));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("is not affected by signatureB64 (not in signed body)", () => {
|
|
84
|
+
const differentSig = {
|
|
85
|
+
...MOCK_PROOF,
|
|
86
|
+
signer: { ...MOCK_PROOF.signer, signatureB64: "completely-different-sig==" },
|
|
87
|
+
} as BitGraphProof;
|
|
88
|
+
assert.equal(computeProofHash(differentSig), computeProofHash(MOCK_PROOF));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("is not affected by attestation reportB64 (not in signed body)", () => {
|
|
92
|
+
const differentReport = {
|
|
93
|
+
...MOCK_PROOF,
|
|
94
|
+
environment: {
|
|
95
|
+
...MOCK_PROOF.environment,
|
|
96
|
+
attestation: { format: "aws-nitro", reportB64: "totally-different==" },
|
|
97
|
+
},
|
|
98
|
+
} as BitGraphProof;
|
|
99
|
+
assert.equal(computeProofHash(differentReport), computeProofHash(MOCK_PROOF));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("changes when artifact changes", () => {
|
|
103
|
+
const different = {
|
|
104
|
+
...MOCK_PROOF,
|
|
105
|
+
artifact: { hashAlg: "sha256" as const, digestB64: "different==" },
|
|
106
|
+
};
|
|
107
|
+
assert.notEqual(computeProofHash(different), computeProofHash(MOCK_PROOF));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("changes when counter changes", () => {
|
|
111
|
+
const different = {
|
|
112
|
+
...MOCK_PROOF,
|
|
113
|
+
commit: { ...MOCK_PROOF.commit, counter: "99" },
|
|
114
|
+
};
|
|
115
|
+
assert.notEqual(computeProofHash(different), computeProofHash(MOCK_PROOF));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("changes when publicKeyB64 changes", () => {
|
|
119
|
+
const different = {
|
|
120
|
+
...MOCK_PROOF,
|
|
121
|
+
signer: { ...MOCK_PROOF.signer, publicKeyB64: "different-key==" },
|
|
122
|
+
} as BitGraphProof;
|
|
123
|
+
assert.notEqual(computeProofHash(different), computeProofHash(MOCK_PROOF));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("includes attribution when present", () => {
|
|
127
|
+
const without = { ...MOCK_PROOF } as Record<string, unknown>;
|
|
128
|
+
delete without.attribution;
|
|
129
|
+
assert.notEqual(computeProofHash(without), computeProofHash(MOCK_PROOF));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("includes attestationFormat when present", () => {
|
|
133
|
+
const withoutAttestation = {
|
|
134
|
+
...MOCK_PROOF,
|
|
135
|
+
environment: {
|
|
136
|
+
enforcement: MOCK_PROOF.environment.enforcement,
|
|
137
|
+
measurement: MOCK_PROOF.environment.measurement,
|
|
138
|
+
},
|
|
139
|
+
} as BitGraphProof;
|
|
140
|
+
assert.notEqual(computeProofHash(withoutAttestation), computeProofHash(MOCK_PROOF));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("works with Record<string, unknown> input (not just BitGraphProof)", () => {
|
|
144
|
+
const record: Record<string, unknown> = { ...MOCK_PROOF };
|
|
145
|
+
const hash = computeProofHash(record);
|
|
146
|
+
assert.equal(hash, computeProofHash(MOCK_PROOF));
|
|
147
|
+
});
|
|
148
|
+
});
|