@mikeargento/bitgraph 1.9.0 → 1.9.2
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 +1 -1
- package/README.md +58 -35
- package/dist/constructor.js +1 -1
- package/dist/constructor.js.map +1 -1
- package/dist/fuse-cli.js +1 -1
- package/dist/fuse-cli.js.map +1 -1
- package/dist/fuse.js +1 -1
- package/dist/fuse.js.map +1 -1
- package/dist/host.js +1 -1
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/policy.js +1 -1
- package/dist/policy.js.map +1 -1
- package/package.json +23 -8
- package/src/constructor.ts +1 -1
- package/src/fuse-cli.ts +1 -1
- package/src/fuse.ts +1 -1
- package/src/host.ts +1 -1
- package/src/index.ts +3 -2
- package/src/policy.ts +1 -1
- package/dist/__tests__/audit-anchors.test.d.ts +0 -2
- package/dist/__tests__/audit-anchors.test.d.ts.map +0 -1
- package/dist/__tests__/audit-anchors.test.js +0 -152
- package/dist/__tests__/audit-anchors.test.js.map +0 -1
- package/dist/__tests__/audit-anomalies.test.d.ts +0 -2
- package/dist/__tests__/audit-anomalies.test.d.ts.map +0 -1
- package/dist/__tests__/audit-anomalies.test.js +0 -663
- package/dist/__tests__/audit-anomalies.test.js.map +0 -1
- package/dist/__tests__/audit-attestation.test.d.ts +0 -2
- package/dist/__tests__/audit-attestation.test.d.ts.map +0 -1
- package/dist/__tests__/audit-attestation.test.js +0 -403
- package/dist/__tests__/audit-attestation.test.js.map +0 -1
- package/dist/__tests__/audit-bench-smoke.test.d.ts +0 -2
- package/dist/__tests__/audit-bench-smoke.test.d.ts.map +0 -1
- package/dist/__tests__/audit-bench-smoke.test.js +0 -87
- package/dist/__tests__/audit-bench-smoke.test.js.map +0 -1
- package/dist/__tests__/audit-boundary.test.d.ts +0 -20
- package/dist/__tests__/audit-boundary.test.d.ts.map +0 -1
- package/dist/__tests__/audit-boundary.test.js +0 -84
- package/dist/__tests__/audit-boundary.test.js.map +0 -1
- package/dist/__tests__/audit-cli.test.d.ts +0 -2
- package/dist/__tests__/audit-cli.test.d.ts.map +0 -1
- package/dist/__tests__/audit-cli.test.js +0 -239
- package/dist/__tests__/audit-cli.test.js.map +0 -1
- package/dist/__tests__/audit-dependencies.test.d.ts +0 -2
- package/dist/__tests__/audit-dependencies.test.d.ts.map +0 -1
- package/dist/__tests__/audit-dependencies.test.js +0 -116
- package/dist/__tests__/audit-dependencies.test.js.map +0 -1
- package/dist/__tests__/audit-fixtures.d.ts +0 -265
- package/dist/__tests__/audit-fixtures.d.ts.map +0 -1
- package/dist/__tests__/audit-fixtures.js +0 -511
- package/dist/__tests__/audit-fixtures.js.map +0 -1
- package/dist/__tests__/audit-ingest.test.d.ts +0 -2
- package/dist/__tests__/audit-ingest.test.d.ts.map +0 -1
- package/dist/__tests__/audit-ingest.test.js +0 -473
- package/dist/__tests__/audit-ingest.test.js.map +0 -1
- package/dist/__tests__/audit-memory-ingest.test.d.ts +0 -12
- package/dist/__tests__/audit-memory-ingest.test.d.ts.map +0 -1
- package/dist/__tests__/audit-memory-ingest.test.js +0 -135
- package/dist/__tests__/audit-memory-ingest.test.js.map +0 -1
- package/dist/__tests__/audit-property.test.d.ts +0 -2
- package/dist/__tests__/audit-property.test.d.ts.map +0 -1
- package/dist/__tests__/audit-property.test.js +0 -771
- package/dist/__tests__/audit-property.test.js.map +0 -1
- package/dist/__tests__/audit-real-fixtures.test.d.ts +0 -2
- package/dist/__tests__/audit-real-fixtures.test.d.ts.map +0 -1
- package/dist/__tests__/audit-real-fixtures.test.js +0 -132
- package/dist/__tests__/audit-real-fixtures.test.js.map +0 -1
- package/dist/__tests__/audit-real-live.test.d.ts +0 -2
- package/dist/__tests__/audit-real-live.test.d.ts.map +0 -1
- package/dist/__tests__/audit-real-live.test.js +0 -107
- package/dist/__tests__/audit-real-live.test.js.map +0 -1
- package/dist/__tests__/audit-reconstruct.test.d.ts +0 -2
- package/dist/__tests__/audit-reconstruct.test.d.ts.map +0 -1
- package/dist/__tests__/audit-reconstruct.test.js +0 -287
- package/dist/__tests__/audit-reconstruct.test.js.map +0 -1
- package/dist/__tests__/audit-report.test.d.ts +0 -2
- package/dist/__tests__/audit-report.test.d.ts.map +0 -1
- package/dist/__tests__/audit-report.test.js +0 -186
- package/dist/__tests__/audit-report.test.js.map +0 -1
- package/dist/__tests__/audit-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/audit-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/audit-roundtrip.test.js +0 -368
- package/dist/__tests__/audit-roundtrip.test.js.map +0 -1
- package/dist/__tests__/audit-temporal.test.d.ts +0 -2
- package/dist/__tests__/audit-temporal.test.d.ts.map +0 -1
- package/dist/__tests__/audit-temporal.test.js +0 -364
- package/dist/__tests__/audit-temporal.test.js.map +0 -1
- package/dist/__tests__/audit-tiers.test.d.ts +0 -2
- package/dist/__tests__/audit-tiers.test.d.ts.map +0 -1
- package/dist/__tests__/audit-tiers.test.js +0 -282
- package/dist/__tests__/audit-tiers.test.js.map +0 -1
- package/dist/__tests__/audit-witness-real.test.d.ts +0 -19
- package/dist/__tests__/audit-witness-real.test.d.ts.map +0 -1
- package/dist/__tests__/audit-witness-real.test.js +0 -82
- package/dist/__tests__/audit-witness-real.test.js.map +0 -1
- package/dist/__tests__/audit-witness.test.d.ts +0 -2
- package/dist/__tests__/audit-witness.test.d.ts.map +0 -1
- package/dist/__tests__/audit-witness.test.js +0 -308
- package/dist/__tests__/audit-witness.test.js.map +0 -1
- package/dist/__tests__/canonical.test.d.ts +0 -2
- package/dist/__tests__/canonical.test.d.ts.map +0 -1
- package/dist/__tests__/canonical.test.js +0 -162
- package/dist/__tests__/canonical.test.js.map +0 -1
- package/dist/__tests__/chain-hash-anchor.test.d.ts +0 -22
- package/dist/__tests__/chain-hash-anchor.test.d.ts.map +0 -1
- package/dist/__tests__/chain-hash-anchor.test.js +0 -55
- package/dist/__tests__/chain-hash-anchor.test.js.map +0 -1
- package/dist/__tests__/commit-service.integration.test.d.ts +0 -2
- package/dist/__tests__/commit-service.integration.test.d.ts.map +0 -1
- package/dist/__tests__/commit-service.integration.test.js +0 -95
- package/dist/__tests__/commit-service.integration.test.js.map +0 -1
- package/dist/__tests__/constructor.test.d.ts +0 -2
- package/dist/__tests__/constructor.test.d.ts.map +0 -1
- package/dist/__tests__/constructor.test.js +0 -267
- package/dist/__tests__/constructor.test.js.map +0 -1
- package/dist/__tests__/declared-proof-fixture.d.ts +0 -66
- package/dist/__tests__/declared-proof-fixture.d.ts.map +0 -1
- package/dist/__tests__/declared-proof-fixture.js +0 -66
- package/dist/__tests__/declared-proof-fixture.js.map +0 -1
- package/dist/__tests__/fuse-placement.test.d.ts +0 -2
- package/dist/__tests__/fuse-placement.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-placement.test.js +0 -31
- package/dist/__tests__/fuse-placement.test.js.map +0 -1
- package/dist/__tests__/fuse-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/fuse-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-roundtrip.test.js +0 -241
- package/dist/__tests__/fuse-roundtrip.test.js.map +0 -1
- package/dist/__tests__/fuse-sdk.test.d.ts +0 -2
- package/dist/__tests__/fuse-sdk.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-sdk.test.js +0 -117
- package/dist/__tests__/fuse-sdk.test.js.map +0 -1
- package/dist/__tests__/fuse-set-sdk.test.d.ts +0 -2
- package/dist/__tests__/fuse-set-sdk.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-set-sdk.test.js +0 -721
- package/dist/__tests__/fuse-set-sdk.test.js.map +0 -1
- package/dist/__tests__/fuse-set.test.d.ts +0 -2
- package/dist/__tests__/fuse-set.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-set.test.js +0 -868
- package/dist/__tests__/fuse-set.test.js.map +0 -1
- package/dist/__tests__/fuse-set2.test.d.ts +0 -2
- package/dist/__tests__/fuse-set2.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-set2.test.js +0 -294
- package/dist/__tests__/fuse-set2.test.js.map +0 -1
- package/dist/__tests__/fuse-vectors.test.d.ts +0 -2
- package/dist/__tests__/fuse-vectors.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-vectors.test.js +0 -175
- package/dist/__tests__/fuse-vectors.test.js.map +0 -1
- package/dist/__tests__/fuse-verify.test.d.ts +0 -2
- package/dist/__tests__/fuse-verify.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-verify.test.js +0 -250
- package/dist/__tests__/fuse-verify.test.js.map +0 -1
- package/dist/__tests__/hello-world.test.d.ts +0 -2
- package/dist/__tests__/hello-world.test.d.ts.map +0 -1
- package/dist/__tests__/hello-world.test.js +0 -9
- package/dist/__tests__/hello-world.test.js.map +0 -1
- package/dist/__tests__/mock-proof-fixture.d.ts +0 -15
- package/dist/__tests__/mock-proof-fixture.d.ts.map +0 -1
- package/dist/__tests__/mock-proof-fixture.js +0 -34
- package/dist/__tests__/mock-proof-fixture.js.map +0 -1
- package/dist/__tests__/policy-enforcement.test.d.ts +0 -2
- package/dist/__tests__/policy-enforcement.test.d.ts.map +0 -1
- package/dist/__tests__/policy-enforcement.test.js +0 -188
- package/dist/__tests__/policy-enforcement.test.js.map +0 -1
- package/dist/__tests__/proof-hash-regression.test.d.ts +0 -2
- package/dist/__tests__/proof-hash-regression.test.d.ts.map +0 -1
- package/dist/__tests__/proof-hash-regression.test.js +0 -95
- package/dist/__tests__/proof-hash-regression.test.js.map +0 -1
- package/dist/__tests__/proof-hash.test.d.ts +0 -2
- package/dist/__tests__/proof-hash.test.d.ts.map +0 -1
- package/dist/__tests__/proof-hash.test.js +0 -116
- package/dist/__tests__/proof-hash.test.js.map +0 -1
- package/dist/__tests__/proof-integrity.test.d.ts +0 -2
- package/dist/__tests__/proof-integrity.test.d.ts.map +0 -1
- package/dist/__tests__/proof-integrity.test.js +0 -426
- package/dist/__tests__/proof-integrity.test.js.map +0 -1
- package/dist/__tests__/realistic-proof-fixture.d.ts +0 -56
- package/dist/__tests__/realistic-proof-fixture.d.ts.map +0 -1
- package/dist/__tests__/realistic-proof-fixture.js +0 -57
- package/dist/__tests__/realistic-proof-fixture.js.map +0 -1
- package/dist/__tests__/verifier.test.d.ts +0 -2
- package/dist/__tests__/verifier.test.d.ts.map +0 -1
- package/dist/__tests__/verifier.test.js +0 -531
- package/dist/__tests__/verifier.test.js.map +0 -1
- package/dist/canonical.d.ts +0 -54
- package/dist/canonical.d.ts.map +0 -1
- package/dist/canonical.js +0 -119
- package/dist/canonical.js.map +0 -1
- package/dist/proof-hash.d.ts +0 -9
- package/dist/proof-hash.d.ts.map +0 -1
- package/dist/proof-hash.js +0 -75
- package/dist/proof-hash.js.map +0 -1
- package/dist/types.d.ts +0 -705
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/verifier.d.ts +0 -27
- package/dist/verifier.d.ts.map +0 -1
- package/dist/verifier.js +0 -844
- package/dist/verifier.js.map +0 -1
- package/src/__tests__/audit-anchors.test.ts +0 -188
- package/src/__tests__/audit-anomalies.test.ts +0 -815
- package/src/__tests__/audit-attestation.test.ts +0 -529
- package/src/__tests__/audit-bench-smoke.test.ts +0 -108
- package/src/__tests__/audit-boundary.test.ts +0 -96
- package/src/__tests__/audit-cli.test.ts +0 -288
- package/src/__tests__/audit-dependencies.test.ts +0 -141
- package/src/__tests__/audit-fixtures.ts +0 -736
- package/src/__tests__/audit-ingest.test.ts +0 -575
- package/src/__tests__/audit-memory-ingest.test.ts +0 -156
- package/src/__tests__/audit-property.test.ts +0 -892
- package/src/__tests__/audit-real-fixtures.test.ts +0 -153
- package/src/__tests__/audit-real-live.test.ts +0 -128
- package/src/__tests__/audit-reconstruct.test.ts +0 -353
- package/src/__tests__/audit-report.test.ts +0 -222
- package/src/__tests__/audit-roundtrip.test.ts +0 -464
- package/src/__tests__/audit-temporal.test.ts +0 -446
- package/src/__tests__/audit-tiers.test.ts +0 -343
- package/src/__tests__/audit-witness-real.test.ts +0 -92
- package/src/__tests__/audit-witness.test.ts +0 -345
- package/src/__tests__/canonical.test.ts +0 -253
- package/src/__tests__/chain-hash-anchor.test.ts +0 -67
- package/src/__tests__/commit-service.integration.test.ts +0 -117
- package/src/__tests__/constructor.test.ts +0 -340
- package/src/__tests__/declared-proof-fixture.ts +0 -65
- package/src/__tests__/fuse-placement.test.ts +0 -32
- package/src/__tests__/fuse-roundtrip.test.ts +0 -243
- package/src/__tests__/fuse-sdk.test.ts +0 -142
- package/src/__tests__/fuse-set-sdk.test.ts +0 -852
- package/src/__tests__/fuse-set.test.ts +0 -1005
- package/src/__tests__/fuse-set2.test.ts +0 -343
- package/src/__tests__/fuse-vectors.test.ts +0 -218
- package/src/__tests__/fuse-verify.test.ts +0 -278
- package/src/__tests__/hello-world.test.ts +0 -10
- package/src/__tests__/mock-proof-fixture.ts +0 -36
- package/src/__tests__/policy-enforcement.test.ts +0 -226
- package/src/__tests__/proof-hash-regression.test.ts +0 -107
- package/src/__tests__/proof-hash.test.ts +0 -129
- package/src/__tests__/proof-integrity.test.ts +0 -541
- package/src/__tests__/real-fixtures/anchor-after-112762.json +0 -47
- package/src/__tests__/real-fixtures/anchor-with-ethereum-112758.json +0 -47
- package/src/__tests__/real-fixtures/live-proof-0.json +0 -42
- package/src/__tests__/real-fixtures/live-proof-1.json +0 -43
- package/src/__tests__/real-fixtures/live-proof-2.json +0 -43
- package/src/__tests__/real-fixtures/successor-112760.json +0 -38
- package/src/__tests__/real-fixtures/witness-25525146.json +0 -6
- package/src/__tests__/real-fixtures/witness-25525147.json +0 -6
- package/src/__tests__/realistic-proof-fixture.ts +0 -57
- package/src/__tests__/verifier.test.ts +0 -624
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Mike Argento. All rights reserved. See LICENSE.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* BitGraph Fuse: verifier paths (spec 15.4, 15.5, 15.8) on real fixture
|
|
5
|
-
* proofs minted through the unmodified enclave. Every negative case runs.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, test } from "node:test";
|
|
9
|
-
import * as assert from "node:assert/strict";
|
|
10
|
-
import { readFileSync } from "node:fs";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { sha256 } from "@noble/hashes/sha256";
|
|
13
|
-
import { verifyFuse, assembledAfterCommit, verifyProofIntegrity, bytesToBase64, resetEpochLinkState } from "@mikeargento/bitgraph-verify";
|
|
14
|
-
import type { BitGraphProof, FuseVerifyResult } from "@mikeargento/bitgraph-verify";
|
|
15
|
-
|
|
16
|
-
const FIX = fileURLToPath(new URL("../../src/__tests__/fuse-fixtures/", import.meta.url));
|
|
17
|
-
const bytes = (name: string) => new Uint8Array(readFileSync(FIX + name));
|
|
18
|
-
const proofOf = (name: string) => JSON.parse(readFileSync(FIX + name, "utf8")) as BitGraphProof;
|
|
19
|
-
const text = (name: string) => readFileSync(FIX + name, "utf8");
|
|
20
|
-
|
|
21
|
-
const original = bytes("original.txt");
|
|
22
|
-
const png = bytes("image.png");
|
|
23
|
-
const unrelated = new TextEncoder().encode("a file nobody recorded\n");
|
|
24
|
-
|
|
25
|
-
async function run(proofName: string, file: Uint8Array, extra: Partial<Parameters<typeof verifyFuse>[0]> = {}): Promise<FuseVerifyResult> {
|
|
26
|
-
resetEpochLinkState();
|
|
27
|
-
return verifyFuse({ proof: proofOf(proofName), bytes: file, ...extra });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("path 3: the file is the committed bytes", () => {
|
|
31
|
-
test("an ordinary proof with its file is RECORDED, with the recorded statement", async () => {
|
|
32
|
-
const r = await run("recorded.proof.json", original);
|
|
33
|
-
assert.equal(r.category, "RECORDED");
|
|
34
|
-
assert.equal(r.proof.valid, true);
|
|
35
|
-
assert.equal(r.marker, null);
|
|
36
|
-
assert.match(r.statements[0]!, /existed no later than commit position/);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
for (const [proofName, fusedName, placement] of [
|
|
40
|
-
["trailer.proof.json", "fused-trailer.bin", "trailer/1"],
|
|
41
|
-
["container.proof.json", "fused-container.tar", "container/1"],
|
|
42
|
-
["produced-origin.proof.json", "produced-origin.json", "produced/1"],
|
|
43
|
-
["produced-bare.proof.json", "produced-bare.json", "produced/1"],
|
|
44
|
-
] as const) {
|
|
45
|
-
test(`${placement}: fused bytes verify FUSED_DIRECT`, async () => {
|
|
46
|
-
const r = await run(proofName, bytes(fusedName));
|
|
47
|
-
assert.equal(r.category, "FUSED_DIRECT", r.reason ?? "");
|
|
48
|
-
assert.equal(r.placement, placement);
|
|
49
|
-
assert.equal(r.marker?.source, "attribution");
|
|
50
|
-
assert.ok(r.span);
|
|
51
|
-
assert.ok(BigInt(r.span!.positions) >= 1n);
|
|
52
|
-
assert.equal(r.span!.chainId, "bitgraph:main");
|
|
53
|
-
assert.match(r.statements.at(-1)!, /could not feasibly have been finalized before their signed slot allocation at position/);
|
|
54
|
-
assert.equal(r.policy.spanExceeded, false);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
test("the origin statement names what was checked: consistency on the direct path, reconstruction on the origin path", async () => {
|
|
59
|
-
const direct = await run("trailer.proof.json", bytes("fused-trailer.bin"));
|
|
60
|
-
assert.equal(direct.category, "FUSED_DIRECT");
|
|
61
|
-
assert.equal(direct.statements.length, 2);
|
|
62
|
-
assert.match(direct.statements[0]!, /origin digest that matches the signed marker/);
|
|
63
|
-
assert.match(direct.statements[0]!, /was not supplied and was not checked/);
|
|
64
|
-
assert.doesNotMatch(direct.statements[0]!, /supplied original/, "no original was supplied on the direct path");
|
|
65
|
-
const fromOrigin = await run("trailer.proof.json", original);
|
|
66
|
-
assert.equal(fromOrigin.category, "FUSED_FROM_ORIGIN");
|
|
67
|
-
assert.equal(fromOrigin.statements.length, 2);
|
|
68
|
-
assert.match(fromOrigin.statements[0]!, /supplied original rebuilds the committed fused artifact byte for byte/);
|
|
69
|
-
const bare = await run("produced-bare.proof.json", bytes("produced-bare.json"));
|
|
70
|
-
assert.equal(bare.statements.length, 1);
|
|
71
|
-
assert.equal(bare.originDigestB64, null);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("a fused file committed under a different slot is INVALID_SLOT_COMMITMENT, never RECORDED", async () => {
|
|
75
|
-
const r = await run("wrong-slot.proof.json", bytes("fused-wrong-slot.bin"));
|
|
76
|
-
assert.equal(r.category, "INVALID_SLOT_COMMITMENT");
|
|
77
|
-
assert.equal(r.proof.valid, true, "the underlying proof is sound; only the commitment is wrong");
|
|
78
|
-
assert.match(r.reason!, /does not match the proof's slot record/);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("an unregistered placement id is UNDETERMINED_PLACEMENT", async () => {
|
|
82
|
-
const r = await run("unregistered.proof.json", bytes("fused-unregistered.bin"));
|
|
83
|
-
assert.equal(r.category, "UNDETERMINED_PLACEMENT");
|
|
84
|
-
assert.match(r.reason!, /xmp\/9/);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("a manifest origin hint that contradicts the origin inside the bytes is INVALID_ORIGIN_ATTRIBUTION", async () => {
|
|
88
|
-
// The signed attribution of the undeclared fixture names no origin, so the
|
|
89
|
-
// Frame's manifest supplies the hint. Trailer bytes carry the original as
|
|
90
|
-
// their prefix, and the hint (the PNG) contradicts what the bytes say.
|
|
91
|
-
const frameText = JSON.stringify({
|
|
92
|
-
type: "bitgraph-fuse/1",
|
|
93
|
-
manifest: { placement: "trailer/1", origin: { algorithm: "sha256", digest: Buffer.from(sha256(png)).toString("hex") }, artifact: { algorithm: "sha256", digest: Buffer.from(sha256(bytes("fused-trailer-undeclared.bin"))).toString("hex") }, fusedFile: null },
|
|
94
|
-
proof: proofOf("trailer-undeclared.proof.json"),
|
|
95
|
-
});
|
|
96
|
-
const r = await run("trailer-undeclared.proof.json", bytes("fused-trailer-undeclared.bin"), { frame: frameText });
|
|
97
|
-
assert.equal(r.category, "INVALID_ORIGIN_ATTRIBUTION");
|
|
98
|
-
assert.equal(r.marker?.source, "attribution", "the proof itself is signed fused");
|
|
99
|
-
assert.equal(r.marker?.originSource, "manifest", "the contradicted origin came from the manifest");
|
|
100
|
-
assert.match(r.reason!, /manifest/);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("a manifest marker that agrees with the bytes verifies FUSED_DIRECT", async () => {
|
|
104
|
-
const r = await run("trailer-undeclared.proof.json", bytes("fused-trailer-undeclared.bin"), {
|
|
105
|
-
frame: { type: "bitgraph-fuse/1", manifest: { placement: "trailer/1", origin: { algorithm: "sha256", digest: Buffer.from(sha256(original)).toString("hex") }, artifact: { algorithm: "sha256", digest: "00".repeat(32) }, fusedFile: null }, proof: proofOf("trailer-undeclared.proof.json") },
|
|
106
|
-
});
|
|
107
|
-
assert.equal(r.category, "FUSED_DIRECT");
|
|
108
|
-
assert.equal(r.statements.length, 2);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("path 4: the file is the original", () => {
|
|
113
|
-
test("trailer/1: the original alone rebuilds and verifies FUSED_FROM_ORIGIN", async () => {
|
|
114
|
-
const r = await run("trailer.proof.json", original);
|
|
115
|
-
assert.equal(r.category, "FUSED_FROM_ORIGIN", r.reason ?? "");
|
|
116
|
-
assert.equal(r.placement, "trailer/1");
|
|
117
|
-
assert.equal(r.originDigestB64, bytesToBase64(sha256(original)));
|
|
118
|
-
assert.equal(r.statements.length, 2);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("container/1: the PNG alone rebuilds the archive", async () => {
|
|
122
|
-
const r = await run("container.proof.json", png);
|
|
123
|
-
assert.equal(r.category, "FUSED_FROM_ORIGIN", r.reason ?? "");
|
|
124
|
-
assert.equal(r.placement, "container/1");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("produced/1 names a source but is not rebuildable from it: RECONSTRUCTION_MISMATCH", async () => {
|
|
128
|
-
const r = await run("produced-origin.proof.json", original);
|
|
129
|
-
assert.equal(r.category, "RECONSTRUCTION_MISMATCH");
|
|
130
|
-
assert.equal(r.placement, "produced/1");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("a manifest origin hint on a signed fused proof lets the original rebuild", async () => {
|
|
134
|
-
const r = await run("trailer-undeclared.proof.json", original, {
|
|
135
|
-
frame: { type: "bitgraph-fuse/1", manifest: { placement: "trailer/1", origin: { algorithm: "sha256", digest: Buffer.from(sha256(original)).toString("hex") }, artifact: { algorithm: "sha256", digest: "00".repeat(32) }, fusedFile: null }, proof: proofOf("trailer-undeclared.proof.json") },
|
|
136
|
-
});
|
|
137
|
-
assert.equal(r.category, "FUSED_FROM_ORIGIN");
|
|
138
|
-
assert.equal(r.placement, "trailer/1");
|
|
139
|
-
assert.equal(r.marker?.originSource, "manifest");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("an undeclared placement resolves through the registry in fixed order and reports the match", async () => {
|
|
143
|
-
// A proof whose signed attribution has no title cannot be minted through the
|
|
144
|
-
// parent (it requires one), so undeclared placement arrives only through a
|
|
145
|
-
// Frame: strip the title here to exercise the registry scan on both paths.
|
|
146
|
-
const p = structuredClone(proofOf("trailer-undeclared.proof.json"));
|
|
147
|
-
// The title is signed; removing it breaks the signature, so instead present a
|
|
148
|
-
// Frame with no placement and rely on the signed marker having one. To reach
|
|
149
|
-
// the scan we need BOTH undeclared, which only a hypothetical name-only
|
|
150
|
-
// attribution gives; construct that case by verifying against a Frame whose
|
|
151
|
-
// proof is name-only and whose signature we re-establish is impossible here,
|
|
152
|
-
// so exercise the scan through the registry helper directly instead.
|
|
153
|
-
const frame = { type: "bitgraph-fuse/1", manifest: { origin: { algorithm: "sha256", digest: Buffer.from(sha256(original)).toString("hex") }, artifact: { algorithm: "sha256", digest: "00".repeat(32) }, fusedFile: null }, proof: p };
|
|
154
|
-
const r = await run("trailer-undeclared.proof.json", original, { frame });
|
|
155
|
-
// The signed title (trailer/1) still declares the placement; the scan is not reached.
|
|
156
|
-
assert.equal(r.category, "FUSED_FROM_ORIGIN");
|
|
157
|
-
assert.equal(r.placement, "trailer/1");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("an unsigned manifest with a FALSE origin is RECONSTRUCTION_MISMATCH: reconstruction makes the hint self-proving", async () => {
|
|
161
|
-
// The manifest claims the PNG is the origin of the undeclared trailer fixture. The
|
|
162
|
-
// PNG's hash matches the hint, so path 4 runs, and rebuilding from the PNG cannot
|
|
163
|
-
// reproduce the signed artifact digest.
|
|
164
|
-
const r = await run("trailer-undeclared.proof.json", png, {
|
|
165
|
-
frame: { type: "bitgraph-fuse/1", manifest: { placement: "trailer/1", origin: { algorithm: "sha256", digest: Buffer.from(sha256(png)).toString("hex") }, artifact: { algorithm: "sha256", digest: "00".repeat(32) }, fusedFile: null }, proof: proofOf("trailer-undeclared.proof.json") },
|
|
166
|
-
});
|
|
167
|
-
assert.equal(r.category, "RECONSTRUCTION_MISMATCH");
|
|
168
|
-
assert.equal(r.marker?.source, "attribution", "the proof is signed fused");
|
|
169
|
-
assert.equal(r.marker?.originSource, "manifest", "the false origin was the manifest's hint");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("an unregistered declared placement cannot rebuild: UNDETERMINED_PLACEMENT", async () => {
|
|
173
|
-
const r = await run("unregistered.proof.json", original);
|
|
174
|
-
assert.equal(r.category, "UNDETERMINED_PLACEMENT");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("the wrong-slot fixture from its original: rebuilt bytes carry the proof's own commitment, so the hash differs", async () => {
|
|
178
|
-
const r = await run("wrong-slot.proof.json", original);
|
|
179
|
-
assert.equal(r.category, "RECONSTRUCTION_MISMATCH");
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe("path 5 and the underlying proof", () => {
|
|
184
|
-
test("a file matching neither digest is NO_MATCH", async () => {
|
|
185
|
-
for (const name of ["recorded.proof.json", "trailer.proof.json", "produced-bare.proof.json"]) {
|
|
186
|
-
const r = await run(name, unrelated);
|
|
187
|
-
assert.equal(r.category, "NO_MATCH", name);
|
|
188
|
-
}
|
|
189
|
-
// The fused bytes of one fixture against another fixture's proof: also NO_MATCH.
|
|
190
|
-
assert.equal((await run("container.proof.json", bytes("fused-trailer.bin"))).category, "NO_MATCH");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test("tampering with the nested proof is INVALID_UNDERLYING_PROOF and never downgrades", async () => {
|
|
194
|
-
const proof = proofOf("trailer.proof.json");
|
|
195
|
-
const cases: Array<[string, (p: BitGraphProof) => void]> = [
|
|
196
|
-
["counter", (p) => { p.commit.counter = String(Number(p.commit.counter) + 1); }],
|
|
197
|
-
["attribution.message (origin)", (p) => { p.attribution!.message = bytesToBase64(sha256(png)); }],
|
|
198
|
-
["attribution.title (placement)", (p) => { p.attribution!.title = "container/1"; }],
|
|
199
|
-
["artifact digest", (p) => { p.artifact.digestB64 = bytesToBase64(sha256(unrelated)); }],
|
|
200
|
-
["slot record nonce", (p) => { p.slotAllocation!.nonceB64 = bytesToBase64(new Uint8Array(32)); }],
|
|
201
|
-
["signature", (p) => { p.signer.signatureB64 = bytesToBase64(new Uint8Array(64)); }],
|
|
202
|
-
];
|
|
203
|
-
for (const [label, mutate] of cases) {
|
|
204
|
-
const p = structuredClone(proof);
|
|
205
|
-
mutate(p);
|
|
206
|
-
resetEpochLinkState();
|
|
207
|
-
const r = await verifyFuse({ proof: p, bytes: bytes("fused-trailer.bin") });
|
|
208
|
-
assert.equal(r.category, "INVALID_UNDERLYING_PROOF", label);
|
|
209
|
-
assert.equal(r.proof.valid, false, label);
|
|
210
|
-
assert.equal(r.statements.length, 0, label);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test("tampering with the fused bytes is NO_MATCH (the digest no longer matches)", async () => {
|
|
215
|
-
const f = bytes("fused-trailer.bin");
|
|
216
|
-
f[3] = f[3]! ^ 1;
|
|
217
|
-
assert.equal((await run("trailer.proof.json", f)).category, "NO_MATCH");
|
|
218
|
-
const c = bytes("fused-trailer.bin");
|
|
219
|
-
c[c.length - 1] = c[c.length - 1]! ^ 1; // a commitment byte
|
|
220
|
-
assert.equal((await run("trailer.proof.json", c)).category, "NO_MATCH");
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test("reformatting only the outer Frame changes nothing", async () => {
|
|
224
|
-
const pretty = text("trailer.bitgraph-fuse.json");
|
|
225
|
-
const compact = JSON.stringify(JSON.parse(pretty));
|
|
226
|
-
const a = await run("trailer.proof.json", bytes("fused-trailer.bin"), { frame: pretty });
|
|
227
|
-
const b = await run("trailer.proof.json", bytes("fused-trailer.bin"), { frame: compact });
|
|
228
|
-
assert.deepEqual(a, b);
|
|
229
|
-
assert.equal(a.category, "FUSED_DIRECT");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("the ordinary verifier still accepts every fixture proof unchanged", async () => {
|
|
233
|
-
for (const name of ["recorded", "trailer", "container", "produced-origin", "produced-bare", "trailer-undeclared", "wrong-slot", "unregistered"]) {
|
|
234
|
-
resetEpochLinkState();
|
|
235
|
-
const v = await verifyProofIntegrity({ proof: proofOf(`${name}.proof.json`) });
|
|
236
|
-
assert.equal(v.valid, true, `${name}: ${v.reason ?? ""}`);
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test("the span policy reports separately and never changes the category", async () => {
|
|
241
|
-
const r = await run("trailer.proof.json", bytes("fused-trailer.bin"), { maxPositions: 0 });
|
|
242
|
-
assert.equal(r.category, "FUSED_DIRECT");
|
|
243
|
-
assert.equal(r.policy.spanExceeded, true);
|
|
244
|
-
assert.equal(r.policy.maxPositions, "0");
|
|
245
|
-
const ok = await run("trailer.proof.json", bytes("fused-trailer.bin"), { maxPositions: 1_000_000 });
|
|
246
|
-
assert.equal(ok.policy.spanExceeded, false);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("a trust policy still applies to the underlying proof", async () => {
|
|
250
|
-
const r = await run("trailer.proof.json", bytes("fused-trailer.bin"), { trustAnchors: { allowedMeasurements: ["00".repeat(48)] } });
|
|
251
|
-
assert.equal(r.category, "INVALID_UNDERLYING_PROOF");
|
|
252
|
-
assert.match(r.reason!, /measurement/i);
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
describe("strict ordering (spec 12.2)", () => {
|
|
257
|
-
test("recorded A, then fused B allocated after A committed: B assembled after A", () => {
|
|
258
|
-
const a = proofOf("recorded.proof.json");
|
|
259
|
-
const b = proofOf("trailer.proof.json");
|
|
260
|
-
assert.equal(assembledAfterCommit(a, b), true);
|
|
261
|
-
});
|
|
262
|
-
test("slot held across an ordinary commit: the strict rule does not claim B after A", () => {
|
|
263
|
-
const a = proofOf("order-held.proof.json");
|
|
264
|
-
const b = proofOf("order-fused-during-hold.proof.json");
|
|
265
|
-
assert.ok(BigInt(b.commit.slotCounter!) < BigInt(a.commit.counter!), "fixture: B's slot precedes A's commit");
|
|
266
|
-
assert.ok(BigInt(a.commit.counter!) < BigInt(b.commit.counter!), "fixture: A committed before B");
|
|
267
|
-
assert.equal(assembledAfterCommit(a, b), false);
|
|
268
|
-
});
|
|
269
|
-
test("incomparable proofs answer null", () => {
|
|
270
|
-
const a = proofOf("recorded.proof.json");
|
|
271
|
-
const b = structuredClone(proofOf("trailer.proof.json"));
|
|
272
|
-
b.commit.epochId = bytesToBase64(new Uint8Array(32).fill(5));
|
|
273
|
-
assert.equal(assembledAfterCommit(a, b), null);
|
|
274
|
-
const c = structuredClone(proofOf("trailer.proof.json"));
|
|
275
|
-
c.signer.publicKeyB64 = bytesToBase64(new Uint8Array(32).fill(6));
|
|
276
|
-
assert.equal(assembledAfterCommit(a, c), null);
|
|
277
|
-
});
|
|
278
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Mike Argento. All rights reserved. See LICENSE.
|
|
2
|
-
|
|
3
|
-
import { describe, test } from "node:test";
|
|
4
|
-
import * as assert from "node:assert/strict";
|
|
5
|
-
|
|
6
|
-
describe("hello world", () => {
|
|
7
|
-
test("passes", () => {
|
|
8
|
-
assert.strictEqual("hello world", "hello world");
|
|
9
|
-
});
|
|
10
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared mock proof fixture.
|
|
3
|
-
*
|
|
4
|
-
* Extracted verbatim from proof-hash.test.ts (values unchanged, byte for
|
|
5
|
-
* byte) so other suites can import the fixture without importing a test
|
|
6
|
-
* file (which would re-register its suites under node:test).
|
|
7
|
-
*
|
|
8
|
-
* Placeholder values throughout: the digest, keys, and signatures are not
|
|
9
|
-
* real base64 crypto material. It is a canonical-hash and ingest fixture;
|
|
10
|
-
* it fails full verification by design, always for a precise non-version
|
|
11
|
-
* reason (the placeholder digest is not strict base64), never as a pass.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { BitGraphProof } from "../index.js";
|
|
15
|
-
|
|
16
|
-
export const MOCK_PROOF: BitGraphProof = {
|
|
17
|
-
version: "bitgraph/1",
|
|
18
|
-
artifact: { hashAlg: "sha256", digestB64: "abc123==" },
|
|
19
|
-
commit: {
|
|
20
|
-
nonceB64: "nonce==",
|
|
21
|
-
counter: "42",
|
|
22
|
-
slotCounter: "41",
|
|
23
|
-
epochId: "epoch==",
|
|
24
|
-
prevB64: "prev==",
|
|
25
|
-
},
|
|
26
|
-
signer: {
|
|
27
|
-
publicKeyB64: "pubkey==",
|
|
28
|
-
signatureB64: "sig==",
|
|
29
|
-
},
|
|
30
|
-
environment: {
|
|
31
|
-
enforcement: "measured-tee",
|
|
32
|
-
measurement: "pcr0hash",
|
|
33
|
-
attestation: { format: "aws-nitro", reportB64: "report==" },
|
|
34
|
-
},
|
|
35
|
-
attribution: { name: "test", message: "hello" },
|
|
36
|
-
};
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Mike Argento. All rights reserved. See LICENSE.
|
|
2
|
-
// End-to-end test for the BitGraph policy enforcement pipeline.
|
|
3
|
-
|
|
4
|
-
import { test, describe } from "node:test";
|
|
5
|
-
import * as assert from "node:assert/strict";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import { mkdirSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { Constructor } from "../constructor.js";
|
|
10
|
-
import { canonicalize } from "../index.js";
|
|
11
|
-
import { hashPolicy, createPolicyBinding } from "../policy.js";
|
|
12
|
-
import { sha256 } from "@noble/hashes/sha256";
|
|
13
|
-
|
|
14
|
-
// Inline stub host for test isolation — dynamic import avoids tsc dist conflicts
|
|
15
|
-
async function createTestHost(statePath: string) {
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
|
-
const mod = await import("bitgraph-stub" as string) as any;
|
|
18
|
-
const StubHost = mod.StubHost ?? mod.default?.StubHost;
|
|
19
|
-
return StubHost.createPersistent({
|
|
20
|
-
statePath,
|
|
21
|
-
measurement: "test",
|
|
22
|
-
enableTime: true,
|
|
23
|
-
enableCounter: true,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const TEST_POLICY = `# Policy: Test Policy
|
|
28
|
-
version: 2.0
|
|
29
|
-
|
|
30
|
-
## Allowed Tools
|
|
31
|
-
- search
|
|
32
|
-
- read_file
|
|
33
|
-
|
|
34
|
-
## Limits
|
|
35
|
-
- max_actions: 100
|
|
36
|
-
- rate_limit: 10/min
|
|
37
|
-
`;
|
|
38
|
-
|
|
39
|
-
const TEST_POLICY_REVOKED = `# Policy: Test Policy Revoked
|
|
40
|
-
version: 3.0
|
|
41
|
-
|
|
42
|
-
## Allowed Tools
|
|
43
|
-
- search
|
|
44
|
-
|
|
45
|
-
## Limits
|
|
46
|
-
- max_actions: 50
|
|
47
|
-
`;
|
|
48
|
-
|
|
49
|
-
describe("Policy Enforcement Pipeline", () => {
|
|
50
|
-
test("policy binding is created from markdown", () => {
|
|
51
|
-
const binding = createPolicyBinding(TEST_POLICY);
|
|
52
|
-
assert.ok(binding.digestB64, "should have digestB64");
|
|
53
|
-
assert.equal(binding.name, "Test Policy");
|
|
54
|
-
assert.equal(binding.version, "2.0");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("policy committed as slot 0 with binding in proof", async () => {
|
|
58
|
-
const testDir = join(tmpdir(), `bitgraph-test-${Date.now()}`);
|
|
59
|
-
mkdirSync(testDir, { recursive: true });
|
|
60
|
-
|
|
61
|
-
const stub = await createTestHost(join(testDir, "state.json"));
|
|
62
|
-
|
|
63
|
-
const constructor = await Constructor.initialize({
|
|
64
|
-
host: stub.host,
|
|
65
|
-
policy: { requireCounter: true, requireTime: true },
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const binding = createPolicyBinding(TEST_POLICY);
|
|
69
|
-
const policyDigest = hashPolicy(TEST_POLICY);
|
|
70
|
-
|
|
71
|
-
const policyProof = await constructor.commitDigest({
|
|
72
|
-
digestB64: policyDigest,
|
|
73
|
-
metadata: { kind: "policy-commitment", policyName: binding.name },
|
|
74
|
-
policy: binding,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
assert.ok(policyProof, "should return a proof");
|
|
78
|
-
assert.equal(policyProof.commit.counter, "1");
|
|
79
|
-
assert.ok(policyProof.policy, "proof should have policy field");
|
|
80
|
-
assert.equal(policyProof.policy!.digestB64, policyDigest);
|
|
81
|
-
assert.equal(policyProof.policy!.name, "Test Policy");
|
|
82
|
-
|
|
83
|
-
const proofHash = Buffer.from(sha256(canonicalize(policyProof))).toString("base64");
|
|
84
|
-
binding.authorProofDigestB64 = proofHash;
|
|
85
|
-
assert.ok(binding.authorProofDigestB64);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("action proofs carry the policy binding", async () => {
|
|
89
|
-
const testDir = join(tmpdir(), `bitgraph-test-${Date.now()}`);
|
|
90
|
-
mkdirSync(testDir, { recursive: true });
|
|
91
|
-
|
|
92
|
-
const stub = await createTestHost(join(testDir, "state.json"));
|
|
93
|
-
|
|
94
|
-
const constructor = await Constructor.initialize({
|
|
95
|
-
host: stub.host,
|
|
96
|
-
policy: { requireCounter: true, requireTime: true },
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const binding = createPolicyBinding(TEST_POLICY);
|
|
100
|
-
const policyDigest = hashPolicy(TEST_POLICY);
|
|
101
|
-
|
|
102
|
-
const policyProof = await constructor.commitDigest({
|
|
103
|
-
digestB64: policyDigest,
|
|
104
|
-
metadata: { kind: "policy-commitment" },
|
|
105
|
-
policy: binding,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const proofHash = Buffer.from(sha256(canonicalize(policyProof))).toString("base64");
|
|
109
|
-
binding.authorProofDigestB64 = proofHash;
|
|
110
|
-
|
|
111
|
-
const actionDigest = Buffer.from(sha256(new TextEncoder().encode("search:query"))).toString("base64");
|
|
112
|
-
const actionProof = await constructor.commitDigest({
|
|
113
|
-
digestB64: actionDigest,
|
|
114
|
-
metadata: { kind: "tool-execution", tool: "search" },
|
|
115
|
-
prevProofHashB64: proofHash,
|
|
116
|
-
policy: binding,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
assert.ok(actionProof.policy, "action proof should have policy");
|
|
120
|
-
assert.equal(actionProof.policy!.digestB64, policyDigest);
|
|
121
|
-
assert.equal(actionProof.policy!.authorProofDigestB64, proofHash);
|
|
122
|
-
assert.equal(actionProof.commit.counter, "2");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("policy revocation updates binding", async () => {
|
|
126
|
-
const testDir = join(tmpdir(), `bitgraph-test-${Date.now()}`);
|
|
127
|
-
mkdirSync(testDir, { recursive: true });
|
|
128
|
-
|
|
129
|
-
const stub = await createTestHost(join(testDir, "state.json"));
|
|
130
|
-
|
|
131
|
-
const constructor = await Constructor.initialize({
|
|
132
|
-
host: stub.host,
|
|
133
|
-
policy: { requireCounter: true, requireTime: true },
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Slot 0: original policy
|
|
137
|
-
const originalBinding = createPolicyBinding(TEST_POLICY);
|
|
138
|
-
const originalDigest = hashPolicy(TEST_POLICY);
|
|
139
|
-
|
|
140
|
-
const slot0Proof = await constructor.commitDigest({
|
|
141
|
-
digestB64: originalDigest,
|
|
142
|
-
metadata: { kind: "policy-commitment" },
|
|
143
|
-
policy: originalBinding,
|
|
144
|
-
});
|
|
145
|
-
const slot0Hash = Buffer.from(sha256(canonicalize(slot0Proof))).toString("base64");
|
|
146
|
-
originalBinding.authorProofDigestB64 = slot0Hash;
|
|
147
|
-
|
|
148
|
-
// Action under original policy
|
|
149
|
-
const action1 = await constructor.commitDigest({
|
|
150
|
-
digestB64: Buffer.from(sha256(new TextEncoder().encode("action1"))).toString("base64"),
|
|
151
|
-
metadata: { kind: "tool-execution", tool: "read_file" },
|
|
152
|
-
prevProofHashB64: slot0Hash,
|
|
153
|
-
policy: originalBinding,
|
|
154
|
-
});
|
|
155
|
-
assert.equal(action1.policy!.digestB64, originalDigest);
|
|
156
|
-
|
|
157
|
-
// Revoke: commit new policy
|
|
158
|
-
const revokedBinding = createPolicyBinding(TEST_POLICY_REVOKED);
|
|
159
|
-
const revokedDigest = hashPolicy(TEST_POLICY_REVOKED);
|
|
160
|
-
|
|
161
|
-
const action1Hash = Buffer.from(sha256(canonicalize(action1))).toString("base64");
|
|
162
|
-
const revokeProof = await constructor.commitDigest({
|
|
163
|
-
digestB64: revokedDigest,
|
|
164
|
-
metadata: { kind: "policy-change", removed: ["read_file"] },
|
|
165
|
-
prevProofHashB64: action1Hash,
|
|
166
|
-
policy: revokedBinding,
|
|
167
|
-
});
|
|
168
|
-
const revokeHash = Buffer.from(sha256(canonicalize(revokeProof))).toString("base64");
|
|
169
|
-
revokedBinding.authorProofDigestB64 = revokeHash;
|
|
170
|
-
|
|
171
|
-
// Action under new (revoked) policy
|
|
172
|
-
const action2 = await constructor.commitDigest({
|
|
173
|
-
digestB64: Buffer.from(sha256(new TextEncoder().encode("action2"))).toString("base64"),
|
|
174
|
-
metadata: { kind: "tool-execution", tool: "search" },
|
|
175
|
-
prevProofHashB64: revokeHash,
|
|
176
|
-
policy: revokedBinding,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
assert.equal(action2.policy!.digestB64, revokedDigest);
|
|
180
|
-
assert.equal(action2.policy!.authorProofDigestB64, revokeHash);
|
|
181
|
-
assert.notEqual(action2.policy!.digestB64, originalDigest);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test("causal ordering: policy counter < action counter", async () => {
|
|
185
|
-
const testDir = join(tmpdir(), `bitgraph-test-${Date.now()}`);
|
|
186
|
-
mkdirSync(testDir, { recursive: true });
|
|
187
|
-
|
|
188
|
-
const stub = await createTestHost(join(testDir, "state.json"));
|
|
189
|
-
|
|
190
|
-
const constructor = await Constructor.initialize({
|
|
191
|
-
host: stub.host,
|
|
192
|
-
policy: { requireCounter: true, requireTime: true },
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const binding = createPolicyBinding(TEST_POLICY);
|
|
196
|
-
|
|
197
|
-
const policyProof = await constructor.commitDigest({
|
|
198
|
-
digestB64: hashPolicy(TEST_POLICY),
|
|
199
|
-
metadata: { kind: "policy-commitment" },
|
|
200
|
-
policy: binding,
|
|
201
|
-
});
|
|
202
|
-
const policyCounter = BigInt(policyProof.commit.counter ?? "0");
|
|
203
|
-
|
|
204
|
-
const proofHash = Buffer.from(sha256(canonicalize(policyProof))).toString("base64");
|
|
205
|
-
binding.authorProofDigestB64 = proofHash;
|
|
206
|
-
|
|
207
|
-
for (let i = 1; i <= 3; i++) {
|
|
208
|
-
const actionProof = await constructor.commitDigest({
|
|
209
|
-
digestB64: Buffer.from(sha256(new TextEncoder().encode(`action-${i}`))).toString("base64"),
|
|
210
|
-
metadata: { kind: "tool-execution", tool: "search" },
|
|
211
|
-
policy: binding,
|
|
212
|
-
});
|
|
213
|
-
const actionCounter = BigInt(actionProof.commit.counter ?? "0");
|
|
214
|
-
assert.ok(actionCounter > policyCounter, `action ${i} counter (${actionCounter}) > policy counter (${policyCounter})`);
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test("hashPolicy produces consistent digest", () => {
|
|
219
|
-
const digest1 = hashPolicy(TEST_POLICY);
|
|
220
|
-
const digest2 = hashPolicy(TEST_POLICY);
|
|
221
|
-
assert.equal(digest1, digest2);
|
|
222
|
-
|
|
223
|
-
const digest3 = hashPolicy(TEST_POLICY_REVOKED);
|
|
224
|
-
assert.notEqual(digest1, digest3);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
@@ -1,107 +0,0 @@
|
|
|
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
|
-
// Fixture extracted verbatim to a shared non-test module so other suites
|
|
16
|
-
// (audit attestation honesty tests) can import it without re-registering
|
|
17
|
-
// this file's suites. Values unchanged.
|
|
18
|
-
import { REALISTIC_PROOF } from "./realistic-proof-fixture.js";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Inline signed-body hash — the OLD way ledger s3.ts computed it.
|
|
22
|
-
* This must produce the same result as computeProofHash().
|
|
23
|
-
*/
|
|
24
|
-
function inlineSignedBodyHash(proof: Record<string, unknown>): string {
|
|
25
|
-
const signer = proof.signer as { publicKeyB64: string };
|
|
26
|
-
const env = proof.environment as { enforcement: string; measurement: string; attestation?: { format: string } };
|
|
27
|
-
|
|
28
|
-
const signedBody: Record<string, unknown> = {
|
|
29
|
-
version: proof.version,
|
|
30
|
-
artifact: proof.artifact,
|
|
31
|
-
commit: proof.commit,
|
|
32
|
-
publicKeyB64: signer.publicKeyB64,
|
|
33
|
-
enforcement: env.enforcement,
|
|
34
|
-
measurement: env.measurement,
|
|
35
|
-
};
|
|
36
|
-
if (proof.attribution) signedBody.attribution = proof.attribution;
|
|
37
|
-
if (env.attestation) signedBody.attestationFormat = env.attestation.format;
|
|
38
|
-
|
|
39
|
-
return Buffer.from(sha256(canonicalize(signedBody))).toString("base64");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe("proof hash regression — all paths must agree", () => {
|
|
43
|
-
it("computeProofHash matches inline signed-body hash (with attribution)", () => {
|
|
44
|
-
const canonical = computeProofHash(REALISTIC_PROOF);
|
|
45
|
-
const inline = inlineSignedBodyHash(REALISTIC_PROOF);
|
|
46
|
-
assert.equal(canonical, inline, "hash drift detected: computeProofHash vs inline");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("computeProofHash matches inline signed-body hash (without attribution)", () => {
|
|
50
|
-
const noAttr = { ...REALISTIC_PROOF } as Record<string, unknown>;
|
|
51
|
-
delete noAttr.attribution;
|
|
52
|
-
const canonical = computeProofHash(noAttr);
|
|
53
|
-
const inline = inlineSignedBodyHash(noAttr);
|
|
54
|
-
assert.equal(canonical, inline, "hash drift detected without attribution");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("computeProofHash matches inline signed-body hash (without attestation)", () => {
|
|
58
|
-
const noAttest = {
|
|
59
|
-
...REALISTIC_PROOF,
|
|
60
|
-
environment: {
|
|
61
|
-
enforcement: REALISTIC_PROOF.environment.enforcement,
|
|
62
|
-
measurement: REALISTIC_PROOF.environment.measurement,
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
const canonical = computeProofHash(noAttest);
|
|
66
|
-
const inline = inlineSignedBodyHash(noAttest);
|
|
67
|
-
assert.equal(canonical, inline, "hash drift detected without attestation");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("anchor proof and user proof use the same hash function", () => {
|
|
71
|
-
// User proof (no attribution)
|
|
72
|
-
const userProof = { ...REALISTIC_PROOF } as Record<string, unknown>;
|
|
73
|
-
delete userProof.attribution;
|
|
74
|
-
|
|
75
|
-
// Anchor proof (has attribution)
|
|
76
|
-
const anchorProof = REALISTIC_PROOF;
|
|
77
|
-
|
|
78
|
-
// Both should work with the same function
|
|
79
|
-
const userHash = computeProofHash(userProof);
|
|
80
|
-
const anchorHash = computeProofHash(anchorProof);
|
|
81
|
-
|
|
82
|
-
// They should be different (different inputs) but both valid
|
|
83
|
-
assert.notEqual(userHash, anchorHash, "user and anchor proofs with different data should differ");
|
|
84
|
-
assert.equal(Buffer.from(userHash, "base64").length, 32, "user hash should be SHA-256");
|
|
85
|
-
assert.equal(Buffer.from(anchorHash, "base64").length, 32, "anchor hash should be SHA-256");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("adding slotAllocation or metadata does not change hash", () => {
|
|
89
|
-
const bare = { ...REALISTIC_PROOF } as Record<string, unknown>;
|
|
90
|
-
delete bare.slotAllocation;
|
|
91
|
-
|
|
92
|
-
const withSlot = { ...REALISTIC_PROOF };
|
|
93
|
-
|
|
94
|
-
const withMetadata = {
|
|
95
|
-
...REALISTIC_PROOF,
|
|
96
|
-
metadata: { type: "ethereum-anchor", extra: true },
|
|
97
|
-
timestamps: { artifact: { time: 1234567890 } },
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const bareHash = computeProofHash(bare);
|
|
101
|
-
const withSlotHash = computeProofHash(withSlot);
|
|
102
|
-
const withMetaHash = computeProofHash(withMetadata);
|
|
103
|
-
|
|
104
|
-
assert.equal(bareHash, withSlotHash, "slotAllocation should not affect hash");
|
|
105
|
-
assert.equal(bareHash, withMetaHash, "metadata/timestamps should not affect hash");
|
|
106
|
-
});
|
|
107
|
-
});
|