@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,435 @@
|
|
|
1
|
+
// Copyright (c) Mike Argento. All rights reserved. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bitgraph-core Constructor
|
|
5
|
+
*
|
|
6
|
+
* The Constructor is the sole write path in bitgraph-core. It enforces a strict
|
|
7
|
+
* atomic commit invariant:
|
|
8
|
+
*
|
|
9
|
+
* authorize → bind → sign → (authorization consumed)
|
|
10
|
+
*
|
|
11
|
+
* No partial outputs are produced. If any step fails the entire commit
|
|
12
|
+
* is aborted and an error is thrown without returning any proof fragment.
|
|
13
|
+
*
|
|
14
|
+
* Commit steps (in order):
|
|
15
|
+
* 1. Obtain monotonic counter (if host supports it)
|
|
16
|
+
* 2. Obtain fresh boundary nonce from host
|
|
17
|
+
* 3. Obtain secure time (if host supports it)
|
|
18
|
+
* 4. Compute SHA-256 of input bytes
|
|
19
|
+
* 5. Retrieve enclave measurement and public key
|
|
20
|
+
* 6. Build canonical signed body (includes enforcement + measurement +
|
|
21
|
+
* attestationFormat when present)
|
|
22
|
+
* 7. Canonicalize signed body to UTF-8 bytes
|
|
23
|
+
* 8. Sign canonical bytes via host
|
|
24
|
+
* 9. Optionally obtain attestation report bound to this commit
|
|
25
|
+
* 10. Assemble and return the complete BitGraphProof
|
|
26
|
+
*
|
|
27
|
+
* Steps 1-9 are performed inside a single async critical section; any
|
|
28
|
+
* rejection causes the function to throw with no returned value.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
32
|
+
import { canonicalize } from "@mikeargento/bitgraph-verify";
|
|
33
|
+
import type { HostCapabilities } from "./host.js";
|
|
34
|
+
import type { BitGraphPolicy, BitGraphProof, SignedBody, AgencyEnvelope, Attribution, PolicyBinding } from "@mikeargento/bitgraph-verify";
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Constructor class
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
export class Constructor {
|
|
41
|
+
readonly #host: HostCapabilities;
|
|
42
|
+
readonly #policy: Required<BitGraphPolicy>;
|
|
43
|
+
readonly #epochId: string | undefined;
|
|
44
|
+
|
|
45
|
+
private constructor(
|
|
46
|
+
host: HostCapabilities,
|
|
47
|
+
policy: Required<BitGraphPolicy>,
|
|
48
|
+
epochId: string | undefined,
|
|
49
|
+
) {
|
|
50
|
+
this.#host = host;
|
|
51
|
+
this.#policy = policy;
|
|
52
|
+
this.#epochId = epochId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Initialize a Constructor bound to a host adapter.
|
|
57
|
+
*
|
|
58
|
+
* Performs a preflight check to confirm the host can produce a measurement
|
|
59
|
+
* and a public key. If either fails the returned promise rejects and no
|
|
60
|
+
* Constructor is created.
|
|
61
|
+
*
|
|
62
|
+
* @param opts.host - Host capabilities implementation (TEE adapter)
|
|
63
|
+
* @param opts.policy - Optional policy constraints
|
|
64
|
+
* @param opts.epochId - Optional epoch identifier (generated at enclave boot).
|
|
65
|
+
* When provided, included in every proof's commit.epochId field
|
|
66
|
+
* (signed, tamper-evident). Verifiers use epochId to detect
|
|
67
|
+
* enclave lifecycle boundaries.
|
|
68
|
+
*/
|
|
69
|
+
static async initialize(opts: {
|
|
70
|
+
host: HostCapabilities;
|
|
71
|
+
policy?: BitGraphPolicy;
|
|
72
|
+
epochId?: string;
|
|
73
|
+
}): Promise<Constructor> {
|
|
74
|
+
const { host, policy = {}, epochId } = opts;
|
|
75
|
+
|
|
76
|
+
const resolvedPolicy: Required<BitGraphPolicy> = {
|
|
77
|
+
requireCounter: policy.requireCounter ?? false,
|
|
78
|
+
requireTime: policy.requireTime ?? false,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Preflight: confirm the host can serve a measurement and a public key.
|
|
82
|
+
await Promise.all([host.getMeasurement(), host.getPublicKey()]).catch(
|
|
83
|
+
(cause: unknown) => {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"bitgraph-core: host preflight failed — getMeasurement() or getPublicKey() rejected",
|
|
86
|
+
{ cause }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Policy validation: if counter is required, confirm the host has it.
|
|
92
|
+
if (resolvedPolicy.requireCounter && typeof host.nextCounter !== "function") {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"bitgraph-core: policy requires a monotonic counter but host does not implement nextCounter()"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (resolvedPolicy.requireTime && typeof host.secureTime !== "function") {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"bitgraph-core: policy requires secure time but host does not implement secureTime()"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return new Constructor(host, resolvedPolicy, epochId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Produce a tamper-evident, signed commit proof for a pre-computed digest.
|
|
109
|
+
*
|
|
110
|
+
* Same atomic guarantees as `commit()`, but accepts a Base64-encoded
|
|
111
|
+
* SHA-256 digest instead of raw bytes. This enables digest-only mode
|
|
112
|
+
* where the raw data never leaves the caller (e.g. an iPhone sending
|
|
113
|
+
* only the SHA-256 of a photo to the enclave).
|
|
114
|
+
*
|
|
115
|
+
* @param input.digestB64 - Base64-standard SHA-256 digest of the data
|
|
116
|
+
* @param input.metadata - Advisory metadata (NOT signed)
|
|
117
|
+
* @param input.prevProofHashB64 - Optional base64 hash of a prior proof for chaining
|
|
118
|
+
* @param input.agency - Optional agency envelope (actor + authorization)
|
|
119
|
+
*/
|
|
120
|
+
async commitDigest(input: {
|
|
121
|
+
digestB64: string;
|
|
122
|
+
metadata?: Record<string, unknown>;
|
|
123
|
+
prevProofHashB64?: string;
|
|
124
|
+
agency?: AgencyEnvelope;
|
|
125
|
+
attribution?: Attribution;
|
|
126
|
+
policy?: PolicyBinding;
|
|
127
|
+
}): Promise<BitGraphProof> {
|
|
128
|
+
const { digestB64, metadata, prevProofHashB64 } = input;
|
|
129
|
+
|
|
130
|
+
// Validate the provided digest
|
|
131
|
+
const digestBytes = fromBase64Digest(digestB64);
|
|
132
|
+
if (digestBytes.length !== 32) {
|
|
133
|
+
throw new RangeError(
|
|
134
|
+
`bitgraph-core: digestB64 decodes to ${digestBytes.length} bytes; expected 32 (SHA-256)`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Delegate to the internal commit flow, skipping step 4 (hashing)
|
|
139
|
+
return this.#commitInternal({ digestB64, metadata, prevProofHashB64, agency: input.agency, attribution: input.attribution, policy: input.policy });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Produce a tamper-evident, signed commit proof for `input.bytes`.
|
|
144
|
+
*
|
|
145
|
+
* The call is atomic: it either returns a complete BitGraphProof or throws.
|
|
146
|
+
* No partial proof is ever returned.
|
|
147
|
+
*
|
|
148
|
+
* @param input.bytes - The raw bytes to commit
|
|
149
|
+
* @param input.metadata - Advisory metadata (NOT signed)
|
|
150
|
+
* @param input.prevProofHashB64 - Optional base64 hash of a prior proof for chaining
|
|
151
|
+
* @param input.agency - Optional agency envelope (actor + authorization)
|
|
152
|
+
*/
|
|
153
|
+
async commit(input: {
|
|
154
|
+
bytes: Uint8Array;
|
|
155
|
+
metadata?: Record<string, unknown>;
|
|
156
|
+
prevProofHashB64?: string;
|
|
157
|
+
agency?: AgencyEnvelope;
|
|
158
|
+
attribution?: Attribution;
|
|
159
|
+
policy?: PolicyBinding;
|
|
160
|
+
}): Promise<BitGraphProof> {
|
|
161
|
+
const { bytes, metadata, prevProofHashB64 } = input;
|
|
162
|
+
|
|
163
|
+
// Step 4: SHA-256 digest of input bytes
|
|
164
|
+
const digest = sha256(bytes);
|
|
165
|
+
const digestB64 = toBase64(digest);
|
|
166
|
+
|
|
167
|
+
// Delegate to shared internal flow (steps 1-3, 5-10)
|
|
168
|
+
return this.#commitInternal({ digestB64, metadata, prevProofHashB64, agency: input.agency, attribution: input.attribution, policy: input.policy });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ------------------------------------------------------------------
|
|
172
|
+
// Internal shared commit flow (steps 1-3, 5-10)
|
|
173
|
+
// ------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
async #commitInternal(input: {
|
|
176
|
+
digestB64: string;
|
|
177
|
+
metadata: Record<string, unknown> | undefined;
|
|
178
|
+
prevProofHashB64: string | undefined;
|
|
179
|
+
agency: AgencyEnvelope | undefined;
|
|
180
|
+
attribution: Attribution | undefined;
|
|
181
|
+
policy: PolicyBinding | undefined;
|
|
182
|
+
}): Promise<BitGraphProof> {
|
|
183
|
+
const { digestB64, metadata, prevProofHashB64, agency, attribution, policy } = input;
|
|
184
|
+
|
|
185
|
+
// ------------------------------------------------------------------
|
|
186
|
+
// Step 1: Monotonic counter (optional, policy-gated)
|
|
187
|
+
// ------------------------------------------------------------------
|
|
188
|
+
let counter: string | undefined;
|
|
189
|
+
if (typeof this.#host.nextCounter === "function") {
|
|
190
|
+
counter = await this.#host.nextCounter().catch((cause: unknown) => {
|
|
191
|
+
throw new Error("bitgraph-core: host.nextCounter() rejected", { cause });
|
|
192
|
+
});
|
|
193
|
+
assertNonEmptyString(counter, "counter");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ------------------------------------------------------------------
|
|
197
|
+
// Step 2: Fresh boundary nonce
|
|
198
|
+
// ------------------------------------------------------------------
|
|
199
|
+
const nonceBytes = await this.#host.getFreshNonce().catch((cause: unknown) => {
|
|
200
|
+
throw new Error("bitgraph-core: host.getFreshNonce() rejected", { cause });
|
|
201
|
+
});
|
|
202
|
+
assertUint8Array(nonceBytes, "nonce", 16); // minimum 128-bit entropy
|
|
203
|
+
|
|
204
|
+
// ------------------------------------------------------------------
|
|
205
|
+
// Step 3: Secure time (optional, policy-gated)
|
|
206
|
+
// ------------------------------------------------------------------
|
|
207
|
+
let time: number | undefined;
|
|
208
|
+
if (typeof this.#host.secureTime === "function") {
|
|
209
|
+
time = await this.#host.secureTime().catch((cause: unknown) => {
|
|
210
|
+
throw new Error("bitgraph-core: host.secureTime() rejected", { cause });
|
|
211
|
+
});
|
|
212
|
+
if (typeof time !== "number" || !Number.isFinite(time) || time < 0) {
|
|
213
|
+
throw new TypeError(
|
|
214
|
+
"bitgraph-core: host.secureTime() returned an invalid value; expected a non-negative finite number"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ------------------------------------------------------------------
|
|
220
|
+
// Step 5: Enclave identity (measurement + public key)
|
|
221
|
+
// ------------------------------------------------------------------
|
|
222
|
+
const [measurement, publicKeyBytes] = await Promise.all([
|
|
223
|
+
this.#host.getMeasurement().catch((cause: unknown) => {
|
|
224
|
+
throw new Error("bitgraph-core: host.getMeasurement() rejected", { cause });
|
|
225
|
+
}),
|
|
226
|
+
this.#host.getPublicKey().catch((cause: unknown) => {
|
|
227
|
+
throw new Error("bitgraph-core: host.getPublicKey() rejected", { cause });
|
|
228
|
+
}),
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
assertNonEmptyString(measurement, "measurement");
|
|
232
|
+
assertUint8Array(publicKeyBytes, "publicKey", 32); // Ed25519 public key
|
|
233
|
+
|
|
234
|
+
// ------------------------------------------------------------------
|
|
235
|
+
// Step 6: Build canonical signed body
|
|
236
|
+
// ------------------------------------------------------------------
|
|
237
|
+
const commitFields: BitGraphProof["commit"] = {
|
|
238
|
+
nonceB64: toBase64(nonceBytes),
|
|
239
|
+
};
|
|
240
|
+
if (counter !== undefined) commitFields.counter = counter;
|
|
241
|
+
if (time !== undefined) commitFields.time = time;
|
|
242
|
+
if (prevProofHashB64 !== undefined) commitFields.prevB64 = prevProofHashB64;
|
|
243
|
+
if (this.#epochId !== undefined) commitFields.epochId = this.#epochId;
|
|
244
|
+
|
|
245
|
+
const signedBody: SignedBody = {
|
|
246
|
+
version: "bitgraph/1",
|
|
247
|
+
artifact: {
|
|
248
|
+
hashAlg: "sha256",
|
|
249
|
+
digestB64,
|
|
250
|
+
},
|
|
251
|
+
commit: commitFields,
|
|
252
|
+
publicKeyB64: toBase64(publicKeyBytes),
|
|
253
|
+
enforcement: this.#host.enforcementTier,
|
|
254
|
+
measurement,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Include actor identity in signed body when agency is present
|
|
258
|
+
if (agency !== undefined) {
|
|
259
|
+
signedBody.actor = agency.actor;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Include policy binding in signed body when present (cryptographically sealed)
|
|
263
|
+
if (policy !== undefined) {
|
|
264
|
+
signedBody.policy = policy;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Include attribution in signed body when present (cryptographically sealed)
|
|
268
|
+
if (attribution !== undefined) {
|
|
269
|
+
signedBody.attribution = attribution;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ------------------------------------------------------------------
|
|
273
|
+
// Step 7: Canonicalize
|
|
274
|
+
// ------------------------------------------------------------------
|
|
275
|
+
const canonicalBytes = canonicalize(signedBody);
|
|
276
|
+
|
|
277
|
+
// ------------------------------------------------------------------
|
|
278
|
+
// Step 8: Sign
|
|
279
|
+
// ------------------------------------------------------------------
|
|
280
|
+
const signatureBytes = await this.#host.sign(canonicalBytes).catch(
|
|
281
|
+
(cause: unknown) => {
|
|
282
|
+
throw new Error("bitgraph-core: host.sign() rejected", { cause });
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
assertUint8Array(signatureBytes, "signature", 64); // Ed25519 signature
|
|
286
|
+
|
|
287
|
+
// ------------------------------------------------------------------
|
|
288
|
+
// Step 9: Optional attestation report bound to this commit
|
|
289
|
+
// ------------------------------------------------------------------
|
|
290
|
+
let attestation: BitGraphProof["environment"]["attestation"];
|
|
291
|
+
if (typeof this.#host.getAttestation === "function") {
|
|
292
|
+
// Bind the attestation to the canonical body hash so that a verifier
|
|
293
|
+
// can confirm the report covers this specific commit.
|
|
294
|
+
const bodyHash = sha256(canonicalBytes);
|
|
295
|
+
const result = await this.#host.getAttestation(bodyHash).catch(
|
|
296
|
+
(cause: unknown) => {
|
|
297
|
+
throw new Error("bitgraph-core: host.getAttestation() rejected", { cause });
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
if (
|
|
301
|
+
typeof result !== "object" ||
|
|
302
|
+
result === null ||
|
|
303
|
+
typeof result.format !== "string" ||
|
|
304
|
+
result.format.length === 0 ||
|
|
305
|
+
!(result.report instanceof Uint8Array) ||
|
|
306
|
+
result.report.length === 0
|
|
307
|
+
) {
|
|
308
|
+
throw new TypeError(
|
|
309
|
+
"bitgraph-core: host.getAttestation() must return { format: string, report: Uint8Array }"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
attestation = {
|
|
313
|
+
format: result.format,
|
|
314
|
+
reportB64: toBase64(result.report),
|
|
315
|
+
};
|
|
316
|
+
// Also add attestationFormat to the signed body (already canonicalized
|
|
317
|
+
// above without it — we must add it BEFORE signing. Re-do steps 6-8
|
|
318
|
+
// with attestationFormat included so the format is covered by the sig.
|
|
319
|
+
signedBody.attestationFormat = result.format;
|
|
320
|
+
const canonicalBytesWithAttestation = canonicalize(signedBody);
|
|
321
|
+
const signatureBytesWithAttestation = await this.#host.sign(canonicalBytesWithAttestation).catch(
|
|
322
|
+
(cause: unknown) => {
|
|
323
|
+
throw new Error("bitgraph-core: host.sign() rejected (attestation body)", { cause });
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
assertUint8Array(signatureBytesWithAttestation, "signature", 64);
|
|
327
|
+
|
|
328
|
+
// ------------------------------------------------------------------
|
|
329
|
+
// Step 10: Assemble proof (with attestation)
|
|
330
|
+
// ------------------------------------------------------------------
|
|
331
|
+
const proof: BitGraphProof = {
|
|
332
|
+
version: "bitgraph/1",
|
|
333
|
+
artifact: signedBody.artifact,
|
|
334
|
+
commit: signedBody.commit,
|
|
335
|
+
signer: {
|
|
336
|
+
publicKeyB64: signedBody.publicKeyB64,
|
|
337
|
+
signatureB64: toBase64(signatureBytesWithAttestation),
|
|
338
|
+
},
|
|
339
|
+
environment: {
|
|
340
|
+
enforcement: this.#host.enforcementTier,
|
|
341
|
+
measurement: signedBody.measurement,
|
|
342
|
+
attestation,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
if (agency !== undefined) proof.agency = agency;
|
|
347
|
+
if (policy !== undefined) proof.policy = policy;
|
|
348
|
+
if (attribution !== undefined) proof.attribution = attribution;
|
|
349
|
+
if (metadata !== undefined) proof.metadata = metadata;
|
|
350
|
+
return proof;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ------------------------------------------------------------------
|
|
354
|
+
// Step 10: Assemble proof (without attestation)
|
|
355
|
+
// ------------------------------------------------------------------
|
|
356
|
+
const proof: BitGraphProof = {
|
|
357
|
+
version: "bitgraph/1",
|
|
358
|
+
artifact: signedBody.artifact,
|
|
359
|
+
commit: signedBody.commit,
|
|
360
|
+
signer: {
|
|
361
|
+
publicKeyB64: signedBody.publicKeyB64,
|
|
362
|
+
signatureB64: toBase64(signatureBytes),
|
|
363
|
+
},
|
|
364
|
+
environment: {
|
|
365
|
+
enforcement: this.#host.enforcementTier,
|
|
366
|
+
measurement: signedBody.measurement,
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
if (agency !== undefined) proof.agency = agency;
|
|
371
|
+
if (policy !== undefined) proof.policy = policy;
|
|
372
|
+
if (attribution !== undefined) proof.attribution = attribution;
|
|
373
|
+
if (metadata !== undefined) proof.metadata = metadata;
|
|
374
|
+
return proof;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// Internal guards
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
function assertUint8Array(
|
|
383
|
+
value: unknown,
|
|
384
|
+
name: string,
|
|
385
|
+
minLength: number
|
|
386
|
+
): asserts value is Uint8Array {
|
|
387
|
+
if (!(value instanceof Uint8Array)) {
|
|
388
|
+
throw new TypeError(
|
|
389
|
+
`bitgraph-core: host returned non-Uint8Array for ${name}`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
if (value.length < minLength) {
|
|
393
|
+
throw new RangeError(
|
|
394
|
+
`bitgraph-core: host returned ${name} with insufficient length ` +
|
|
395
|
+
`(got ${value.length}, expected >= ${minLength})`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function assertNonEmptyString(
|
|
401
|
+
value: unknown,
|
|
402
|
+
name: string
|
|
403
|
+
): asserts value is string {
|
|
404
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
405
|
+
throw new TypeError(
|
|
406
|
+
`bitgraph-core: host returned invalid ${name}: expected a non-empty string`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// Encoding utility
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Encode a Uint8Array as standard Base64 (RFC 4648 §4, with padding).
|
|
417
|
+
*/
|
|
418
|
+
function toBase64(bytes: Uint8Array): string {
|
|
419
|
+
return Buffer.from(bytes).toString("base64");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Decode a Base64 string to Uint8Array, validating round-trip fidelity.
|
|
424
|
+
* Used by commitDigest() to validate the caller-provided digest.
|
|
425
|
+
*/
|
|
426
|
+
function fromBase64Digest(b64: string): Uint8Array {
|
|
427
|
+
if (typeof b64 !== "string" || b64.length === 0) {
|
|
428
|
+
throw new TypeError("bitgraph-core: digestB64 must be a non-empty string");
|
|
429
|
+
}
|
|
430
|
+
const buf = Buffer.from(b64, "base64");
|
|
431
|
+
if (buf.toString("base64") !== b64) {
|
|
432
|
+
throw new TypeError(`bitgraph-core: digestB64 is not valid base64: "${b64}"`);
|
|
433
|
+
}
|
|
434
|
+
return new Uint8Array(buf);
|
|
435
|
+
}
|
package/src/host.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Copyright (c) Mike Argento. All rights reserved. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bitgraph-core host abstraction
|
|
5
|
+
*
|
|
6
|
+
* `HostCapabilities` is the sole interface between this library and any
|
|
7
|
+
* Trusted Execution Environment. All TEE-specific behavior (attestation,
|
|
8
|
+
* key management, monotonic counters, secure clocks) is supplied by a
|
|
9
|
+
* caller-provided implementation of this interface.
|
|
10
|
+
*
|
|
11
|
+
* Design goals:
|
|
12
|
+
* - TEE-agnostic: no vendor types leak into this file
|
|
13
|
+
* - Fail-closed: every capability is async and may reject
|
|
14
|
+
* - Extensible: optional capabilities use the ? modifier so that
|
|
15
|
+
* adapters can be progressively enhanced without breaking the core
|
|
16
|
+
* - Self-describing: enforcementTier declares the adapter's trust class
|
|
17
|
+
*
|
|
18
|
+
* Adapters (e.g. aws-nitro, sgx-dcap, sev-snp) live in separate packages
|
|
19
|
+
* and implement this interface. A minimal in-process stub for testing is
|
|
20
|
+
* provided in @bitgraph/stub.
|
|
21
|
+
*
|
|
22
|
+
* IMPORTANT — adapter authors:
|
|
23
|
+
* enforcementTier is self-reported and signed into every proof.
|
|
24
|
+
* Claiming "measured-tee" requires that ALL of the following execute
|
|
25
|
+
* inside the attested enclave boundary:
|
|
26
|
+
* - key generation and sealing
|
|
27
|
+
* - nonce generation (getFreshNonce)
|
|
28
|
+
* - monotonic counter (nextCounter)
|
|
29
|
+
* - commit gate logic
|
|
30
|
+
* - signing (sign)
|
|
31
|
+
* If any of these run outside the boundary, the correct tier is "hw-key"
|
|
32
|
+
* or "stub". Misrepresenting the tier is an architectural violation of
|
|
33
|
+
* BitGraph's atomic causality invariant.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { EnforcementTier } from "@mikeargento/bitgraph-verify";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Core interface
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export interface HostCapabilities {
|
|
43
|
+
/**
|
|
44
|
+
* Declares the enforcement tier of this adapter.
|
|
45
|
+
*
|
|
46
|
+
* This value is written into every proof's environment.enforcement field
|
|
47
|
+
* and is included in the signed body — making it tamper-evident in transit.
|
|
48
|
+
*
|
|
49
|
+
* Must be one of: "stub" | "hw-key" | "measured-tee"
|
|
50
|
+
* See EnforcementTier in types.ts for full semantics.
|
|
51
|
+
*
|
|
52
|
+
* This is a plain property (not async) because it is a static adapter
|
|
53
|
+
* declaration, not a runtime capability.
|
|
54
|
+
*/
|
|
55
|
+
readonly enforcementTier: EnforcementTier;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Return the current enclave measurement as an opaque string.
|
|
59
|
+
*
|
|
60
|
+
* The format is adapter-defined (e.g. hex-encoded PCR0 for Nitro,
|
|
61
|
+
* MRENCLAVE for SGX). The verifier stores and compares this value
|
|
62
|
+
* verbatim; it does not parse or interpret it.
|
|
63
|
+
*
|
|
64
|
+
* Must not return a cached value from outside the TEE boundary.
|
|
65
|
+
*
|
|
66
|
+
* Tier expectations:
|
|
67
|
+
* stub: MAY return a synthetic sentinel
|
|
68
|
+
* hw-key: SHOULD return a string identifying the key environment
|
|
69
|
+
* measured-tee: MUST return the attested enclave image identifier
|
|
70
|
+
*/
|
|
71
|
+
getMeasurement(): Promise<string>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate a fresh, boundary-local nonce.
|
|
75
|
+
*
|
|
76
|
+
* The nonce MUST be:
|
|
77
|
+
* - generated inside the TEE boundary (not passed in from the host OS)
|
|
78
|
+
* - at least 128 bits of entropy
|
|
79
|
+
* - never reused across calls
|
|
80
|
+
*
|
|
81
|
+
* Returns raw bytes; the library base64-encodes them for the proof.
|
|
82
|
+
*/
|
|
83
|
+
getFreshNonce(): Promise<Uint8Array>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sign `data` with the enclave's private signing key.
|
|
87
|
+
*
|
|
88
|
+
* The key MUST be:
|
|
89
|
+
* - generated or provisioned inside the TEE boundary
|
|
90
|
+
* - an Ed25519 key pair
|
|
91
|
+
* - not exportable from the TEE
|
|
92
|
+
*
|
|
93
|
+
* Adapter note: when using @noble/ed25519, prefer signAsync() over sign()
|
|
94
|
+
* to avoid requiring a synchronous SHA-512 shim in Node.js environments.
|
|
95
|
+
*
|
|
96
|
+
* Returns the raw 64-byte Ed25519 signature.
|
|
97
|
+
*/
|
|
98
|
+
sign(data: Uint8Array): Promise<Uint8Array>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Return the Ed25519 public key corresponding to the signing key.
|
|
102
|
+
*
|
|
103
|
+
* Returns raw 32-byte compressed public key bytes.
|
|
104
|
+
* Must be stable across calls within a single enclave lifecycle.
|
|
105
|
+
*/
|
|
106
|
+
getPublicKey(): Promise<Uint8Array>;
|
|
107
|
+
|
|
108
|
+
// -------------------------------------------------------------------------
|
|
109
|
+
// Optional capabilities
|
|
110
|
+
// -------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Atomically advance and return a monotonic counter.
|
|
114
|
+
*
|
|
115
|
+
* The counter MUST be:
|
|
116
|
+
* - monotonically increasing across restarts (hardware-backed preferred)
|
|
117
|
+
* - returned as a decimal string (no leading zeros unless "0", ASCII digits only)
|
|
118
|
+
* - never the same value twice (even after a crash/restart)
|
|
119
|
+
*
|
|
120
|
+
* Compared as BigInt by the verifier to avoid IEEE-754 precision loss.
|
|
121
|
+
* Absence of this capability degrades ordering guarantees to nonce-only.
|
|
122
|
+
*/
|
|
123
|
+
nextCounter?(): Promise<string>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Return a TEE-local attestation report that binds the current enclave
|
|
127
|
+
* state to an optional nonce or arbitrary user data.
|
|
128
|
+
*
|
|
129
|
+
* `userData` is adapter-defined; typically a hash of the canonical proof
|
|
130
|
+
* body so the report is bound to this specific commit.
|
|
131
|
+
*
|
|
132
|
+
* The returned object includes:
|
|
133
|
+
* - format — adapter-defined format identifier (e.g. "aws-nitro", "sgx-dcap")
|
|
134
|
+
* - report — raw attestation report bytes; the library does not parse them
|
|
135
|
+
*
|
|
136
|
+
* format is stored in environment.attestation.format and IS signed.
|
|
137
|
+
* report is stored in environment.attestation.reportB64 and is NOT signed
|
|
138
|
+
* (it is vendor-signed and self-authenticating).
|
|
139
|
+
*/
|
|
140
|
+
getAttestation?(userData?: Uint8Array): Promise<{ format: string; report: Uint8Array }>;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Return the current time as Unix epoch milliseconds from a TEE-trusted
|
|
144
|
+
* source.
|
|
145
|
+
*
|
|
146
|
+
* Software clocks inside TEEs can be skewed or forged by a malicious host.
|
|
147
|
+
* This capability is marked optional because not all TEE platforms provide
|
|
148
|
+
* a trusted clock. When absent, commit.time is omitted from the proof.
|
|
149
|
+
*
|
|
150
|
+
* Callers must not use time as the sole ordering mechanism.
|
|
151
|
+
*/
|
|
152
|
+
secureTime?(): Promise<number>;
|
|
153
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright (c) Mike Argento. All rights reserved. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bitgraph-core — BitGraph
|
|
5
|
+
*
|
|
6
|
+
* Portable cryptographic proof at finalization.
|
|
7
|
+
* Hardware TEE enforcement via AWS Nitro Enclaves.
|
|
8
|
+
*
|
|
9
|
+
* The verification side (verify, proof schema, canonicalization, proofHash)
|
|
10
|
+
* lives in @mikeargento/bitgraph-verify (MIT) and is re-exported here for
|
|
11
|
+
* compatibility. Verification of BitGraph proofs is permissionless.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Read side — re-exported from the permissive verifier package
|
|
15
|
+
export type {
|
|
16
|
+
BitGraphProof,
|
|
17
|
+
BitGraphPolicy,
|
|
18
|
+
VerificationPolicy,
|
|
19
|
+
SignedBody,
|
|
20
|
+
EnforcementTier,
|
|
21
|
+
Attribution,
|
|
22
|
+
PolicyBinding,
|
|
23
|
+
SlotAllocation,
|
|
24
|
+
ActorIdentity,
|
|
25
|
+
AuthorizationPayload,
|
|
26
|
+
WebAuthnAuthorization,
|
|
27
|
+
AgencyEnvelope,
|
|
28
|
+
} from "@mikeargento/bitgraph-verify";
|
|
29
|
+
export { verify, resetEpochLinkState } from "@mikeargento/bitgraph-verify";
|
|
30
|
+
export type { VerifyResult } from "@mikeargento/bitgraph-verify";
|
|
31
|
+
export { computeProofHash } from "@mikeargento/bitgraph-verify";
|
|
32
|
+
export { canonicalize, canonicalizeToString, constantTimeEqual } from "@mikeargento/bitgraph-verify";
|
|
33
|
+
|
|
34
|
+
// Host interface
|
|
35
|
+
export type { HostCapabilities } from "./host.js";
|
|
36
|
+
|
|
37
|
+
// Constructor (write path)
|
|
38
|
+
export { Constructor } from "./constructor.js";
|
|
39
|
+
|
|
40
|
+
// Policy parsing, hashing, and validation
|
|
41
|
+
export {
|
|
42
|
+
parsePolicy,
|
|
43
|
+
hashPolicy,
|
|
44
|
+
createPolicyBinding,
|
|
45
|
+
validateAction,
|
|
46
|
+
} from "./policy.js";
|
|
47
|
+
export type {
|
|
48
|
+
PolicyDocument,
|
|
49
|
+
PolicyRules,
|
|
50
|
+
ActionValidationResult,
|
|
51
|
+
} from "./policy.js";
|