@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.
Files changed (80) hide show
  1. package/LICENSE +9 -0
  2. package/NOTICE +14 -0
  3. package/README.md +175 -0
  4. package/dist/__tests__/canonical.test.d.ts +2 -0
  5. package/dist/__tests__/canonical.test.d.ts.map +1 -0
  6. package/dist/__tests__/canonical.test.js +162 -0
  7. package/dist/__tests__/canonical.test.js.map +1 -0
  8. package/dist/__tests__/commit-service.integration.test.d.ts +2 -0
  9. package/dist/__tests__/commit-service.integration.test.d.ts.map +1 -0
  10. package/dist/__tests__/commit-service.integration.test.js +95 -0
  11. package/dist/__tests__/commit-service.integration.test.js.map +1 -0
  12. package/dist/__tests__/constructor.test.d.ts +2 -0
  13. package/dist/__tests__/constructor.test.d.ts.map +1 -0
  14. package/dist/__tests__/constructor.test.js +267 -0
  15. package/dist/__tests__/constructor.test.js.map +1 -0
  16. package/dist/__tests__/hello-world.test.d.ts +2 -0
  17. package/dist/__tests__/hello-world.test.d.ts.map +1 -0
  18. package/dist/__tests__/hello-world.test.js +9 -0
  19. package/dist/__tests__/hello-world.test.js.map +1 -0
  20. package/dist/__tests__/policy-enforcement.test.d.ts +2 -0
  21. package/dist/__tests__/policy-enforcement.test.d.ts.map +1 -0
  22. package/dist/__tests__/policy-enforcement.test.js +188 -0
  23. package/dist/__tests__/policy-enforcement.test.js.map +1 -0
  24. package/dist/__tests__/proof-hash-regression.test.d.ts +2 -0
  25. package/dist/__tests__/proof-hash-regression.test.d.ts.map +1 -0
  26. package/dist/__tests__/proof-hash-regression.test.js +134 -0
  27. package/dist/__tests__/proof-hash-regression.test.js.map +1 -0
  28. package/dist/__tests__/proof-hash.test.d.ts +2 -0
  29. package/dist/__tests__/proof-hash.test.d.ts.map +1 -0
  30. package/dist/__tests__/proof-hash.test.js +134 -0
  31. package/dist/__tests__/proof-hash.test.js.map +1 -0
  32. package/dist/__tests__/verifier.test.d.ts +2 -0
  33. package/dist/__tests__/verifier.test.d.ts.map +1 -0
  34. package/dist/__tests__/verifier.test.js +457 -0
  35. package/dist/__tests__/verifier.test.js.map +1 -0
  36. package/dist/canonical.d.ts +54 -0
  37. package/dist/canonical.d.ts.map +1 -0
  38. package/dist/canonical.js +119 -0
  39. package/dist/canonical.js.map +1 -0
  40. package/dist/constructor.d.ts +66 -0
  41. package/dist/constructor.d.ts.map +1 -0
  42. package/dist/constructor.js +339 -0
  43. package/dist/constructor.js.map +1 -0
  44. package/dist/host.d.ts +138 -0
  45. package/dist/host.d.ts.map +1 -0
  46. package/dist/host.js +3 -0
  47. package/dist/host.js.map +1 -0
  48. package/dist/index.d.ts +20 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +9 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/policy.d.ts +68 -0
  53. package/dist/policy.d.ts.map +1 -0
  54. package/dist/policy.js +176 -0
  55. package/dist/policy.js.map +1 -0
  56. package/dist/proof-hash.d.ts +9 -0
  57. package/dist/proof-hash.d.ts.map +1 -0
  58. package/dist/proof-hash.js +75 -0
  59. package/dist/proof-hash.js.map +1 -0
  60. package/dist/types.d.ts +705 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +3 -0
  63. package/dist/types.js.map +1 -0
  64. package/dist/verifier.d.ts +27 -0
  65. package/dist/verifier.d.ts.map +1 -0
  66. package/dist/verifier.js +844 -0
  67. package/dist/verifier.js.map +1 -0
  68. package/package.json +40 -0
  69. package/src/__tests__/canonical.test.ts +253 -0
  70. package/src/__tests__/commit-service.integration.test.ts +117 -0
  71. package/src/__tests__/constructor.test.ts +340 -0
  72. package/src/__tests__/hello-world.test.ts +10 -0
  73. package/src/__tests__/policy-enforcement.test.ts +226 -0
  74. package/src/__tests__/proof-hash-regression.test.ts +147 -0
  75. package/src/__tests__/proof-hash.test.ts +148 -0
  76. package/src/__tests__/verifier.test.ts +541 -0
  77. package/src/constructor.ts +435 -0
  78. package/src/host.ts +153 -0
  79. package/src/index.ts +51 -0
  80. 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";