@mikeargento/bitgraph 1.8.0 → 1.9.1
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/__tests__/audit-anchors.test.js +1 -1
- package/dist/__tests__/audit-anchors.test.js.map +1 -1
- package/dist/__tests__/audit-anomalies.test.js +1 -1
- package/dist/__tests__/audit-anomalies.test.js.map +1 -1
- package/dist/__tests__/audit-attestation.test.js +1 -1
- package/dist/__tests__/audit-attestation.test.js.map +1 -1
- package/dist/__tests__/audit-bench-smoke.test.js +1 -1
- package/dist/__tests__/audit-bench-smoke.test.js.map +1 -1
- package/dist/__tests__/audit-cli.test.js +1 -1
- package/dist/__tests__/audit-cli.test.js.map +1 -1
- package/dist/__tests__/audit-dependencies.test.js +1 -1
- package/dist/__tests__/audit-dependencies.test.js.map +1 -1
- package/dist/__tests__/audit-fixtures.js +1 -1
- package/dist/__tests__/audit-fixtures.js.map +1 -1
- package/dist/__tests__/audit-ingest.test.js +1 -1
- package/dist/__tests__/audit-ingest.test.js.map +1 -1
- package/dist/__tests__/audit-property.test.js +1 -1
- package/dist/__tests__/audit-property.test.js.map +1 -1
- package/dist/__tests__/audit-real-fixtures.test.js +1 -1
- package/dist/__tests__/audit-real-fixtures.test.js.map +1 -1
- package/dist/__tests__/audit-real-live.test.js +1 -1
- package/dist/__tests__/audit-real-live.test.js.map +1 -1
- package/dist/__tests__/audit-reconstruct.test.js +1 -1
- package/dist/__tests__/audit-reconstruct.test.js.map +1 -1
- package/dist/__tests__/audit-report.test.js +1 -1
- package/dist/__tests__/audit-report.test.js.map +1 -1
- package/dist/__tests__/audit-roundtrip.test.js +1 -1
- package/dist/__tests__/audit-roundtrip.test.js.map +1 -1
- package/dist/__tests__/audit-temporal.test.js +1 -1
- package/dist/__tests__/audit-temporal.test.js.map +1 -1
- package/dist/__tests__/audit-tiers.test.js +1 -1
- package/dist/__tests__/audit-tiers.test.js.map +1 -1
- package/dist/__tests__/audit-witness.test.js +1 -1
- package/dist/__tests__/audit-witness.test.js.map +1 -1
- package/dist/__tests__/canonical.test.js +1 -1
- package/dist/__tests__/canonical.test.js.map +1 -1
- package/dist/__tests__/commit-service.integration.test.js +1 -1
- package/dist/__tests__/commit-service.integration.test.js.map +1 -1
- package/dist/__tests__/constructor.test.js +1 -1
- package/dist/__tests__/constructor.test.js.map +1 -1
- package/dist/__tests__/fuse-inline.test.d.ts +2 -0
- package/dist/__tests__/fuse-inline.test.d.ts.map +1 -0
- package/dist/__tests__/fuse-inline.test.js +181 -0
- package/dist/__tests__/fuse-inline.test.js.map +1 -0
- package/dist/__tests__/fuse-placement.test.js +1 -1
- package/dist/__tests__/fuse-placement.test.js.map +1 -1
- package/dist/__tests__/fuse-roundtrip.test.js +1 -1
- package/dist/__tests__/fuse-roundtrip.test.js.map +1 -1
- package/dist/__tests__/fuse-sdk.test.js +1 -1
- package/dist/__tests__/fuse-sdk.test.js.map +1 -1
- package/dist/__tests__/fuse-set-sdk.test.js +1 -1
- package/dist/__tests__/fuse-set-sdk.test.js.map +1 -1
- package/dist/__tests__/fuse-set.test.js +24 -3
- package/dist/__tests__/fuse-set.test.js.map +1 -1
- package/dist/__tests__/fuse-set2.test.js +1 -1
- package/dist/__tests__/fuse-set2.test.js.map +1 -1
- package/dist/__tests__/fuse-vectors.test.js +1 -1
- package/dist/__tests__/fuse-vectors.test.js.map +1 -1
- package/dist/__tests__/fuse-verify.test.js +1 -1
- package/dist/__tests__/fuse-verify.test.js.map +1 -1
- package/dist/__tests__/hello-world.test.js +1 -1
- package/dist/__tests__/hello-world.test.js.map +1 -1
- package/dist/__tests__/policy-enforcement.test.js +1 -1
- package/dist/__tests__/policy-enforcement.test.js.map +1 -1
- package/dist/__tests__/proof-integrity.test.js +30 -2
- package/dist/__tests__/proof-integrity.test.js.map +1 -1
- package/dist/__tests__/run.test.d.ts +2 -0
- package/dist/__tests__/run.test.d.ts.map +1 -0
- package/dist/__tests__/run.test.js +178 -0
- package/dist/__tests__/run.test.js.map +1 -0
- package/dist/__tests__/verifier.test.js +1 -1
- package/dist/__tests__/verifier.test.js.map +1 -1
- 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 +16 -4
- package/src/__tests__/audit-anchors.test.ts +1 -1
- package/src/__tests__/audit-anomalies.test.ts +1 -1
- package/src/__tests__/audit-attestation.test.ts +1 -1
- package/src/__tests__/audit-bench-smoke.test.ts +1 -1
- package/src/__tests__/audit-cli.test.ts +1 -1
- package/src/__tests__/audit-dependencies.test.ts +1 -1
- package/src/__tests__/audit-fixtures.ts +1 -1
- package/src/__tests__/audit-ingest.test.ts +1 -1
- package/src/__tests__/audit-property.test.ts +1 -1
- package/src/__tests__/audit-real-fixtures.test.ts +1 -1
- package/src/__tests__/audit-real-live.test.ts +1 -1
- package/src/__tests__/audit-reconstruct.test.ts +1 -1
- package/src/__tests__/audit-report.test.ts +1 -1
- package/src/__tests__/audit-roundtrip.test.ts +1 -1
- package/src/__tests__/audit-temporal.test.ts +1 -1
- package/src/__tests__/audit-tiers.test.ts +1 -1
- package/src/__tests__/audit-witness.test.ts +1 -1
- package/src/__tests__/canonical.test.ts +1 -1
- package/src/__tests__/commit-service.integration.test.ts +1 -1
- package/src/__tests__/constructor.test.ts +1 -1
- package/src/__tests__/fuse-inline.test.ts +211 -0
- package/src/__tests__/fuse-placement.test.ts +1 -1
- package/src/__tests__/fuse-roundtrip.test.ts +1 -1
- package/src/__tests__/fuse-sdk.test.ts +1 -1
- package/src/__tests__/fuse-set-sdk.test.ts +1 -1
- package/src/__tests__/fuse-set.test.ts +25 -3
- package/src/__tests__/fuse-set2.test.ts +1 -1
- package/src/__tests__/fuse-vectors.test.ts +1 -1
- package/src/__tests__/fuse-verify.test.ts +1 -1
- package/src/__tests__/hello-world.test.ts +1 -1
- package/src/__tests__/policy-enforcement.test.ts +1 -1
- package/src/__tests__/proof-integrity.test.ts +36 -1
- package/src/__tests__/verifier.test.ts +1 -1
- 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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The marker's title in its second vocabulary: an ENCODING rather than a
|
|
5
|
+
* placement, for an artifact MADE with the commitment inside it.
|
|
6
|
+
*
|
|
7
|
+
* A placement is a recipe, so a holder of the original rebuilds the committed
|
|
8
|
+
* artifact byte for byte. An encoding says there is no recipe and no original:
|
|
9
|
+
* the commitment was in the thing while it was being made (a prompt, a build
|
|
10
|
+
* id, a commit message), so the only thing to do is look for it. One marker
|
|
11
|
+
* answers both, which is why this file tests verifyFuse and not a second
|
|
12
|
+
* function. It was briefly a second profile, bitgraph-run/1, on 2026-09-07.
|
|
13
|
+
*
|
|
14
|
+
* The positive case is real: the canonical inline artifact minted on
|
|
15
|
+
* production, enclave v8, slot 2072 consumed at commit 2075. Categories that
|
|
16
|
+
* need a key under this test's control mint their own proofs, because every one
|
|
17
|
+
* of them is reached by changing something the enclave signed.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, test } from "node:test";
|
|
21
|
+
import * as assert from "node:assert/strict";
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
25
|
+
import { getPublicKeyAsync, signAsync, utils } from "@noble/ed25519";
|
|
26
|
+
import {
|
|
27
|
+
verifyFuse,
|
|
28
|
+
inlineAttribution,
|
|
29
|
+
isCarryEncoding,
|
|
30
|
+
findCommitment,
|
|
31
|
+
computeSlotCommitment,
|
|
32
|
+
canonicalize,
|
|
33
|
+
bytesToBase64,
|
|
34
|
+
ENCODING_BASE64URL,
|
|
35
|
+
} from "@mikeargento/bitgraph-verify";
|
|
36
|
+
import type { BitGraphProof } from "@mikeargento/bitgraph-verify";
|
|
37
|
+
|
|
38
|
+
const FIX = fileURLToPath(new URL("../../src/__tests__/fuse-fixtures/", import.meta.url));
|
|
39
|
+
const b64 = (b: Uint8Array) => bytesToBase64(b);
|
|
40
|
+
const b64u = (b: Uint8Array) => b64(b).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
41
|
+
const utf8 = (s: string) => new TextEncoder().encode(s);
|
|
42
|
+
|
|
43
|
+
const liveProof = JSON.parse(readFileSync(FIX + "inline.proof.json", "utf8")) as BitGraphProof;
|
|
44
|
+
const liveBytes = new Uint8Array(readFileSync(FIX + "inline.artifact.txt"));
|
|
45
|
+
|
|
46
|
+
/** Mint a proof the way the enclave does, so the signature covers the marker. */
|
|
47
|
+
async function mint(
|
|
48
|
+
makeBytes: (commitment: string) => Uint8Array,
|
|
49
|
+
opts: { title?: string; message?: string } = {},
|
|
50
|
+
) {
|
|
51
|
+
const seed = utils.randomPrivateKey();
|
|
52
|
+
const publicKeyB64 = b64(await getPublicKeyAsync(seed));
|
|
53
|
+
const slotBody = {
|
|
54
|
+
version: "bitgraph/slot/1" as const,
|
|
55
|
+
nonceB64: b64(new Uint8Array(32).fill(7)),
|
|
56
|
+
counter: "41",
|
|
57
|
+
epochId: "inline-test-epoch",
|
|
58
|
+
publicKeyB64,
|
|
59
|
+
};
|
|
60
|
+
const slotAllocation = { ...slotBody, signatureB64: b64(await signAsync(canonicalize(slotBody), seed)) };
|
|
61
|
+
const commitment = b64u(computeSlotCommitment(slotAllocation));
|
|
62
|
+
const bytes = makeBytes(commitment);
|
|
63
|
+
const digestB64 = b64(sha256(bytes));
|
|
64
|
+
const attribution: Record<string, string> = { name: "bitgraph-fuse/1", title: opts.title ?? ENCODING_BASE64URL };
|
|
65
|
+
if (opts.message !== undefined) attribution["message"] = opts.message;
|
|
66
|
+
const commit = {
|
|
67
|
+
nonceB64: slotBody.nonceB64,
|
|
68
|
+
counter: "42",
|
|
69
|
+
slotCounter: "41",
|
|
70
|
+
slotHashB64: b64(sha256(canonicalize(slotBody))),
|
|
71
|
+
epochId: "inline-test-epoch",
|
|
72
|
+
};
|
|
73
|
+
const signedBody = {
|
|
74
|
+
version: "bitgraph/1" as const,
|
|
75
|
+
artifact: { hashAlg: "sha256" as const, digestB64 },
|
|
76
|
+
commit,
|
|
77
|
+
publicKeyB64,
|
|
78
|
+
enforcement: "measured-tee" as const,
|
|
79
|
+
measurement: "inline-test-measurement",
|
|
80
|
+
attribution,
|
|
81
|
+
};
|
|
82
|
+
const proof = {
|
|
83
|
+
version: "bitgraph/1",
|
|
84
|
+
artifact: signedBody.artifact,
|
|
85
|
+
commit,
|
|
86
|
+
signer: { publicKeyB64, signatureB64: b64(await signAsync(canonicalize(signedBody), seed)) },
|
|
87
|
+
environment: { enforcement: "measured-tee", measurement: "inline-test-measurement" },
|
|
88
|
+
slotAllocation,
|
|
89
|
+
attribution,
|
|
90
|
+
} as unknown as BitGraphProof;
|
|
91
|
+
return { proof, bytes, commitment };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe("carry encodings", () => {
|
|
95
|
+
test("base64url is registered; a placement id and nonsense are not", () => {
|
|
96
|
+
assert.equal(isCarryEncoding(ENCODING_BASE64URL), true);
|
|
97
|
+
for (const id of ["trailer/1", "container/2", "set/1", "hex/1", "", null, undefined]) {
|
|
98
|
+
assert.equal(isCarryEncoding(id as string), false, `${String(id)} should not be an encoding`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("inlineAttribution declares the encoding and no origin, because there is none", () => {
|
|
103
|
+
const a = inlineAttribution();
|
|
104
|
+
assert.deepEqual(a, { name: "bitgraph-fuse/1", title: ENCODING_BASE64URL });
|
|
105
|
+
assert.equal("message" in a, false, "a 32-byte message would be read as an origin digest");
|
|
106
|
+
assert.deepEqual(a, liveProof.attribution);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("findCommitment locates every occurrence, and none for an unregistered encoding", () => {
|
|
110
|
+
const c = new Uint8Array(32).fill(3);
|
|
111
|
+
const text = b64u(c);
|
|
112
|
+
assert.deepEqual(findCommitment(utf8(`a${text}b${text}`), c, ENCODING_BASE64URL), [1, 1 + text.length + 1]);
|
|
113
|
+
assert.deepEqual(findCommitment(utf8("nothing"), c, ENCODING_BASE64URL), []);
|
|
114
|
+
assert.deepEqual(findCommitment(utf8(text), c, "hex/1"), []);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("the canonical inline artifact (slot 2072, commit 2075)", () => {
|
|
119
|
+
test("CARRIED_INLINE, located, and named as carried by an encoding", async () => {
|
|
120
|
+
const r = await verifyFuse({ proof: liveProof, bytes: liveBytes });
|
|
121
|
+
assert.equal(r.category, "CARRIED_INLINE");
|
|
122
|
+
assert.equal(r.proof.valid, true);
|
|
123
|
+
assert.equal(r.reason, null);
|
|
124
|
+
assert.deepEqual(r.carriedBy, { kind: "encoding", id: ENCODING_BASE64URL });
|
|
125
|
+
assert.equal(r.placement, null, "an inline artifact has no placement: there is no recipe");
|
|
126
|
+
assert.equal(r.originDigestB64, null, "and no original");
|
|
127
|
+
assert.equal(r.offsets.length, 1);
|
|
128
|
+
// The bytes at the reported offset really are the commitment.
|
|
129
|
+
const at = Buffer.from(liveBytes.slice(r.offsets[0]!, r.offsets[0]! + 43)).toString("utf8");
|
|
130
|
+
assert.equal(at, r.slotCommitmentB64!.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("the span is the real one, and the statements name it", async () => {
|
|
134
|
+
const r = await verifyFuse({ proof: liveProof, bytes: liveBytes });
|
|
135
|
+
assert.equal(r.span?.slotCounter, "2072");
|
|
136
|
+
assert.equal(r.span?.commitCounter, "2075");
|
|
137
|
+
assert.match(r.statements[0] ?? "", /slot at position 2072/);
|
|
138
|
+
assert.match(r.statements[1] ?? "", /committed at position 2075/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("the limit rides with the claim and is carried by nothing else", async () => {
|
|
142
|
+
const r = await verifyFuse({ proof: liveProof, bytes: liveBytes });
|
|
143
|
+
assert.equal(r.limits.length, 1);
|
|
144
|
+
assert.match(r.limits[0] ?? "", /appended to a finished file pass the same check/);
|
|
145
|
+
const { proof, bytes } = await mint(() => utf8("no commitment in here"));
|
|
146
|
+
assert.deepEqual((await verifyFuse({ proof, bytes })).limits, [], "a result that establishes nothing carries no limits");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("one edited byte is refused on the proof, not on the search", async () => {
|
|
150
|
+
const edited = new Uint8Array(liveBytes);
|
|
151
|
+
edited[edited.length - 2] = edited[edited.length - 2]! ^ 0x01;
|
|
152
|
+
const r = await verifyFuse({ proof: liveProof, bytes: edited });
|
|
153
|
+
assert.equal(r.category, "NO_MATCH", "the bytes are neither the artifact nor a declared original");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a measurement outside the pinned list is refused", async () => {
|
|
157
|
+
const r = await verifyFuse({ proof: liveProof, bytes: liveBytes, trustAnchors: { allowedMeasurements: ["0".repeat(96)] } });
|
|
158
|
+
assert.equal(r.category, "INVALID_UNDERLYING_PROOF");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("the other inline outcomes", () => {
|
|
163
|
+
test("every occurrence is counted and the first is the one the statement names", async () => {
|
|
164
|
+
const { proof, bytes } = await mint((c) => utf8(`${c}\nmiddle\n${c}\n`));
|
|
165
|
+
const r = await verifyFuse({ proof, bytes });
|
|
166
|
+
assert.equal(r.category, "CARRIED_INLINE");
|
|
167
|
+
assert.deepEqual(r.offsets.length, 2);
|
|
168
|
+
assert.equal(r.offsets[0], 0);
|
|
169
|
+
assert.match(r.statements[1] ?? "", /byte 0/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("COMMITMENT_ABSENT: the proof is valid, the bytes simply do not carry it", async () => {
|
|
173
|
+
const { proof, bytes } = await mint(() => utf8("nothing of the sort in here\n"));
|
|
174
|
+
const r = await verifyFuse({ proof, bytes });
|
|
175
|
+
assert.equal(r.category, "COMMITMENT_ABSENT");
|
|
176
|
+
assert.equal(r.proof.valid, true);
|
|
177
|
+
assert.deepEqual(r.offsets, []);
|
|
178
|
+
assert.deepEqual(r.statements, []);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("an inline marker that declares an origin is refused: there is no original to name", async () => {
|
|
182
|
+
const { proof, bytes } = await mint((c) => utf8(`token ${c}\n`), { message: b64(new Uint8Array(32).fill(9)) });
|
|
183
|
+
const r = await verifyFuse({ proof, bytes });
|
|
184
|
+
assert.equal(r.category, "INVALID_ORIGIN_ATTRIBUTION");
|
|
185
|
+
assert.match(r.reason ?? "", /has no original/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("an unregistered title is UNDETERMINED_PLACEMENT, neither a pass nor a failure", async () => {
|
|
189
|
+
const { proof, bytes } = await mint((c) => utf8(`token ${c}\n`), { title: "hex/1" });
|
|
190
|
+
const r = await verifyFuse({ proof, bytes });
|
|
191
|
+
assert.equal(r.category, "UNDETERMINED_PLACEMENT");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("a commitment appended to finished work passes, and the limit says so", async () => {
|
|
195
|
+
const { proof, bytes } = await mint((c) => utf8(`the work was already done\n${c}\n`));
|
|
196
|
+
const r = await verifyFuse({ proof, bytes });
|
|
197
|
+
assert.equal(r.category, "CARRIED_INLINE");
|
|
198
|
+
assert.ok((r.offsets[0] ?? 0) > 0);
|
|
199
|
+
assert.match(r.limits[0] ?? "", /producer's claim/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("a fused artifact still reports a placement, and carries no limits", async () => {
|
|
203
|
+
const fused = JSON.parse(readFileSync(FIX + "trailer.proof.json", "utf8")) as BitGraphProof;
|
|
204
|
+
const bytes = new Uint8Array(readFileSync(FIX + "fused-trailer.bin"));
|
|
205
|
+
const r = await verifyFuse({ proof: fused, bytes });
|
|
206
|
+
assert.equal(r.category, "FUSED_DIRECT");
|
|
207
|
+
assert.deepEqual(r.carriedBy, { kind: "placement", id: r.placement! });
|
|
208
|
+
assert.deepEqual(r.limits, []);
|
|
209
|
+
assert.deepEqual(r.offsets, []);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c)
|
|
1
|
+
// Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import * as assert from "node:assert/strict";
|
|
4
4
|
import { placementForBytes, fusedNamesFor } from "../fuse.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c)
|
|
1
|
+
// Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* BitGraph Sets, phase 1: the set manifest codec, the set/1 placement,
|
|
@@ -52,6 +52,28 @@ import type {
|
|
|
52
52
|
import { makeKey, signBody, b64, utf8 } from "./audit-fixtures.js";
|
|
53
53
|
import type { ManualKey } from "./audit-fixtures.js";
|
|
54
54
|
|
|
55
|
+
/* ⚠️ UPPERCASE THE WHOLE DIGEST, NOT ITS FIRST EIGHT CHARACTERS.
|
|
56
|
+
*
|
|
57
|
+
* Both call sites used to do `.replace(/"digest":"([0-9a-f]{8})/, upper)`, and
|
|
58
|
+
* that is a COIN FLIP. A manifest is sorted ascending by artifact digest, so
|
|
59
|
+
* the first digest in the text is the minimum of the set — which skews toward
|
|
60
|
+
* leading zeros and digits — and when all eight of those characters happen to
|
|
61
|
+
* be 0-9, uppercasing them changes nothing at all. The bytes stay canonical,
|
|
62
|
+
* parseSetManifest correctly accepts them, and the assertion that it should
|
|
63
|
+
* have REFUSED them fails. Measured: about 3-4% of runs, so roughly one red CI
|
|
64
|
+
* run in twenty-five, on a test that is not wrong about anything.
|
|
65
|
+
*
|
|
66
|
+
* A full 64-character hex digest with no letter in it has probability ~1e-15.
|
|
67
|
+
* And the assertion below turns the remaining impossibility into a loud
|
|
68
|
+
* failure rather than a silent no-op that passes for the wrong reason — which
|
|
69
|
+
* is what the old version did every time it did not flake.
|
|
70
|
+
*/
|
|
71
|
+
function withUppercasedDigest(text: string): string {
|
|
72
|
+
const upper = text.replace(/"digest":"([0-9a-f]{64})"/, (_m, h: string) => `"digest":"${h.toUpperCase()}"`);
|
|
73
|
+
assert.notEqual(upper, text, "the fixture has no lowercase hex digest to uppercase; this check would pass vacuously");
|
|
74
|
+
return upper;
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
const FIX = fileURLToPath(new URL("../../src/__tests__/fuse-fixtures/", import.meta.url));
|
|
56
78
|
const bytes = (name: string) => new Uint8Array(readFileSync(FIX + name));
|
|
57
79
|
const proofOf = (name: string) => JSON.parse(readFileSync(FIX + name, "utf8")) as BitGraphProof;
|
|
@@ -296,7 +318,7 @@ describe("codec: parseSetManifest", () => {
|
|
|
296
318
|
|
|
297
319
|
test("8. digests: uppercase hex, 63- and 65-char hex, and other algorithm spellings are refused", () => {
|
|
298
320
|
const text = dec(A.manifest);
|
|
299
|
-
assert.equal(parseSetManifest(enc(text
|
|
321
|
+
assert.equal(parseSetManifest(enc(withUppercasedDigest(text))), null, "uppercase");
|
|
300
322
|
const o = asObject(A.manifest);
|
|
301
323
|
const short = structuredClone(o); short.members[0]!.artifact.digest = short.members[0]!.artifact.digest.slice(0, 63);
|
|
302
324
|
assert.equal(parseSetManifest(compact(short)), null, "63 chars");
|
|
@@ -770,7 +792,7 @@ describe("verifyFuseMember: manifest binding", () => {
|
|
|
770
792
|
const text = dec(A.manifest);
|
|
771
793
|
const bad: Array<[string, Uint8Array]> = [
|
|
772
794
|
["duplicate keys", enc(text.replace('"type":"bitgraph-fuse/1"', '"type":"bitgraph-fuse/1","type":"bitgraph-fuse/1"'))],
|
|
773
|
-
["uppercase hex", enc(text
|
|
795
|
+
["uppercase hex", enc(withUppercasedDigest(text))],
|
|
774
796
|
["whitespace", enc(JSON.stringify(asObject(A.manifest), null, 1))],
|
|
775
797
|
];
|
|
776
798
|
for (const [label, explicit] of bad) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c)
|
|
1
|
+
// Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Tests for verifyProofIntegrity, the bytes-free proof integrity API in
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
verify,
|
|
18
18
|
verifyProofIntegrity,
|
|
19
19
|
resetEpochLinkState,
|
|
20
|
+
createVerificationContext,
|
|
20
21
|
canonicalize,
|
|
21
22
|
} from "@mikeargento/bitgraph-verify";
|
|
22
23
|
import type { BitGraphProof, ProofIntegrityResult } from "@mikeargento/bitgraph-verify";
|
|
@@ -388,6 +389,40 @@ describe("verifyProofIntegrity: epochLink", () => {
|
|
|
388
389
|
assert.match(result.reason ?? "", /different keys/);
|
|
389
390
|
});
|
|
390
391
|
|
|
392
|
+
/* A rejected proof must not change what the verifier accepts afterwards.
|
|
393
|
+
Before 2026-09-20 the single-successor map was written during the epoch
|
|
394
|
+
link check, which runs BEFORE the policy check, so a proof refused for an
|
|
395
|
+
untrusted measurement still claimed its predecessor and the next honest
|
|
396
|
+
proof was reported as a fork. Verification is a function of its inputs. */
|
|
397
|
+
test("a proof rejected by policy does not consume its predecessor", async () => {
|
|
398
|
+
const rejected = await makeEpochLinkFixture({ toEpochId: "epoch-successor-a" });
|
|
399
|
+
const honest = await makeEpochLinkFixture({ toEpochId: "epoch-successor-b" });
|
|
400
|
+
|
|
401
|
+
const r1 = await verifyProofIntegrity({
|
|
402
|
+
proof: rejected.proof,
|
|
403
|
+
trustAnchors: { allowedMeasurements: ["some-other-measurement"] },
|
|
404
|
+
});
|
|
405
|
+
assert.equal(r1.valid, false);
|
|
406
|
+
|
|
407
|
+
const r2 = await verifyProofIntegrity({ proof: honest.proof });
|
|
408
|
+
assert.equal(r2.valid, true, r2.valid ? "" : r2.reason);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("an explicit context keeps one verification's history out of another's", async () => {
|
|
412
|
+
const a = await makeEpochLinkFixture({ toEpochId: "epoch-successor-a" });
|
|
413
|
+
const b = await makeEpochLinkFixture({ toEpochId: "epoch-successor-b" });
|
|
414
|
+
const one = createVerificationContext();
|
|
415
|
+
const two = createVerificationContext();
|
|
416
|
+
|
|
417
|
+
assert.equal((await verifyProofIntegrity({ proof: a.proof, context: one })).valid, true);
|
|
418
|
+
assert.equal((await verifyProofIntegrity({ proof: b.proof, context: two })).valid, true);
|
|
419
|
+
|
|
420
|
+
// The same context still catches the fork.
|
|
421
|
+
const forked = await verifyProofIntegrity({ proof: b.proof, context: one });
|
|
422
|
+
assert.equal(forked.valid, false);
|
|
423
|
+
assert.match(forked.reason ?? "", /FORK DETECTED/);
|
|
424
|
+
});
|
|
425
|
+
|
|
391
426
|
test("detects a fork: same predecessor consumed by two successor epochs", async () => {
|
|
392
427
|
const first = await makeEpochLinkFixture({ toEpochId: "epoch-successor-a" });
|
|
393
428
|
const second = await makeEpochLinkFixture({ toEpochId: "epoch-successor-b" });
|
package/src/constructor.ts
CHANGED
package/src/fuse-cli.ts
CHANGED
package/src/fuse.ts
CHANGED
package/src/host.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c)
|
|
1
|
+
// Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* bitgraph-core — BitGraph
|
|
@@ -26,7 +26,8 @@ export type {
|
|
|
26
26
|
WebAuthnAuthorization,
|
|
27
27
|
AgencyEnvelope,
|
|
28
28
|
} from "@mikeargento/bitgraph-verify";
|
|
29
|
-
export { verify, resetEpochLinkState } from "@mikeargento/bitgraph-verify";
|
|
29
|
+
export { verify, resetEpochLinkState, createVerificationContext } from "@mikeargento/bitgraph-verify";
|
|
30
|
+
export type { VerificationContext } from "@mikeargento/bitgraph-verify";
|
|
30
31
|
export type { VerifyResult } from "@mikeargento/bitgraph-verify";
|
|
31
32
|
export { computeProofHash } from "@mikeargento/bitgraph-verify";
|
|
32
33
|
export { canonicalize, canonicalizeToString, constantTimeEqual } from "@mikeargento/bitgraph-verify";
|
package/src/policy.ts
CHANGED