@zeke-02/tinfoil 0.0.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.
Files changed (65) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +169 -0
  3. package/dist/__tests__/test-utils.d.ts +1 -0
  4. package/dist/__tests__/test-utils.js +44 -0
  5. package/dist/ai-sdk-provider.d.ts +7 -0
  6. package/dist/ai-sdk-provider.js +23 -0
  7. package/dist/config.d.ts +17 -0
  8. package/dist/config.js +20 -0
  9. package/dist/encrypted-body-fetch.d.ts +8 -0
  10. package/dist/encrypted-body-fetch.js +93 -0
  11. package/dist/env.d.ts +5 -0
  12. package/dist/env.js +20 -0
  13. package/dist/esm/__tests__/test-utils.d.ts +1 -0
  14. package/dist/esm/__tests__/test-utils.js +38 -0
  15. package/dist/esm/ai-sdk-provider.d.ts +7 -0
  16. package/dist/esm/ai-sdk-provider.js +20 -0
  17. package/dist/esm/config.d.ts +17 -0
  18. package/dist/esm/config.js +17 -0
  19. package/dist/esm/encrypted-body-fetch.d.ts +8 -0
  20. package/dist/esm/encrypted-body-fetch.js +86 -0
  21. package/dist/esm/env.d.ts +5 -0
  22. package/dist/esm/env.js +17 -0
  23. package/dist/esm/fetch-adapter.d.ts +21 -0
  24. package/dist/esm/fetch-adapter.js +23 -0
  25. package/dist/esm/index.browser.d.ts +7 -0
  26. package/dist/esm/index.browser.js +8 -0
  27. package/dist/esm/index.d.ts +8 -0
  28. package/dist/esm/index.js +12 -0
  29. package/dist/esm/pinned-tls-fetch.d.ts +1 -0
  30. package/dist/esm/pinned-tls-fetch.js +110 -0
  31. package/dist/esm/secure-client.d.ts +20 -0
  32. package/dist/esm/secure-client.js +123 -0
  33. package/dist/esm/secure-fetch.browser.d.ts +1 -0
  34. package/dist/esm/secure-fetch.browser.js +10 -0
  35. package/dist/esm/secure-fetch.d.ts +1 -0
  36. package/dist/esm/secure-fetch.js +22 -0
  37. package/dist/esm/tinfoilai.d.ts +54 -0
  38. package/dist/esm/tinfoilai.js +134 -0
  39. package/dist/esm/unverified-client.d.ts +18 -0
  40. package/dist/esm/unverified-client.js +33 -0
  41. package/dist/esm/verifier.d.ts +141 -0
  42. package/dist/esm/verifier.js +741 -0
  43. package/dist/esm/wasm-exec.js +668 -0
  44. package/dist/fetch-adapter.d.ts +21 -0
  45. package/dist/fetch-adapter.js +27 -0
  46. package/dist/index.browser.d.ts +7 -0
  47. package/dist/index.browser.js +29 -0
  48. package/dist/index.d.ts +8 -0
  49. package/dist/index.js +49 -0
  50. package/dist/pinned-tls-fetch.d.ts +1 -0
  51. package/dist/pinned-tls-fetch.js +116 -0
  52. package/dist/secure-client.d.ts +20 -0
  53. package/dist/secure-client.js +127 -0
  54. package/dist/secure-fetch.browser.d.ts +1 -0
  55. package/dist/secure-fetch.browser.js +13 -0
  56. package/dist/secure-fetch.d.ts +1 -0
  57. package/dist/secure-fetch.js +25 -0
  58. package/dist/tinfoilai.d.ts +54 -0
  59. package/dist/tinfoilai.js +141 -0
  60. package/dist/unverified-client.d.ts +18 -0
  61. package/dist/unverified-client.js +37 -0
  62. package/dist/verifier.d.ts +141 -0
  63. package/dist/verifier.js +781 -0
  64. package/dist/wasm-exec.js +668 -0
  65. package/package.json +97 -0
@@ -0,0 +1,741 @@
1
+ /**
2
+ * VERIFIER COMPONENT OVERVIEW
3
+ * ==========================
4
+ *
5
+ * This implementation performs three security checks entirely on the client using
6
+ * a Go WebAssembly module, and exposes a small TypeScript API around it.
7
+ *
8
+ * 1) REMOTE ATTESTATION (Enclave Verification)
9
+ * - Invokes Go WASM `verifyEnclave(host)` against the target enclave hostname
10
+ * - Verifies vendor certificate chains inside WASM (AMD SEV-SNP / Intel TDX)
11
+ * - Returns the enclave's runtime measurement and at least one public key (TLS fingerprint and/or HPKE)
12
+ * - Falls back to TLS-only verification if only TLS key is available (Node.js only)
13
+ *
14
+ * 2) CODE INTEGRITY (Release Verification)
15
+ * - Fetches the latest release notes via the Tinfoil GitHub proxy and extracts a digest
16
+ * (endpoint: https://api-github-proxy.tinfoil.sh)
17
+ * - Invokes Go WASM `verifyCode(configRepo, digest)` to obtain the expected code measurement
18
+ * - The Go implementation verifies provenance using Sigstore/Rekor for GitHub Actions builds
19
+ *
20
+ * 3) CODE CONSISTENCY (Measurement Comparison)
21
+ * - Compares the runtime measurement with the expected code measurement using
22
+ * platform-aware rules implemented in `compareMeasurements()`
23
+ *
24
+ * RUNTIME AND DELIVERY
25
+ * - All verification executes locally via WebAssembly (Go → WASM)
26
+ * - WASM loader: `wasm-exec.js`
27
+ * - WASM module URL: https://tinfoilsh.github.io/verifier-js/tinfoil-verifier.wasm
28
+ * - Works in Node 20+ and modern browsers with lightweight polyfills for
29
+ * `performance`, `TextEncoder`/`TextDecoder`, and `crypto.getRandomValues`
30
+ * - Go stdout/stderr is suppressed by default; toggle via `suppressWasmLogs()`
31
+ * - Module auto-initializes the WASM runtime on import
32
+ *
33
+ * PROXIES AND TRUST
34
+ * - GitHub proxy is used only to avoid rate limits; the WASM logic independently
35
+ * validates release provenance via Sigstore transparency logs
36
+ * - AMD KDS access may be proxied within the WASM for availability; AMD roots are
37
+ * embedded and the full chain is verified in Go to prevent forgery
38
+ *
39
+ * SUPPORTED PLATFORMS AND PREDICATES
40
+ * - Predicate types supported by this client: SNP/TDX multi-platform v1,
41
+ * TDX guest v1, SEV-SNP guest v1
42
+ * - See `compareMeasurements()` for exact register mapping rules
43
+ *
44
+ * PUBLIC API (this module)
45
+ * - `new Verifier({ serverURL?, configRepo? })`
46
+ * - `verify()` → full end-to-end verification and attestation response
47
+ * - `verifyEnclave(host?)` → runtime attestation only
48
+ * - `verifyCode(configRepo, digest)` → expected measurement for a specific release
49
+ * - `compareMeasurements(code, runtime)` → predicate-based comparison
50
+ * - `fetchLatestDigest(configRepo?)` → release digest via proxy
51
+ * - `suppressWasmLogs(suppress?)` → control WASM log output
52
+ */
53
+ import { TINFOIL_CONFIG } from "./config";
54
+ import { getFetch } from "./fetch-adapter";
55
+ let cachedTextEncoder = null;
56
+ function getTextEncoder() {
57
+ if (!cachedTextEncoder) {
58
+ if (typeof globalThis.TextEncoder !== "function") {
59
+ throw new Error("TextEncoder is not available in this environment");
60
+ }
61
+ cachedTextEncoder = globalThis.TextEncoder;
62
+ }
63
+ return cachedTextEncoder;
64
+ }
65
+ let cachedTextDecoder = null;
66
+ function getTextDecoder() {
67
+ if (!cachedTextDecoder) {
68
+ if (typeof globalThis.TextDecoder !== "function") {
69
+ throw new Error("TextDecoder is not available in this environment");
70
+ }
71
+ cachedTextDecoder = globalThis.TextDecoder;
72
+ }
73
+ return cachedTextDecoder;
74
+ }
75
+ const nodeRequire = createNodeRequire();
76
+ let wasmExecLoader = null;
77
+ // Platform type constants
78
+ // See https://github.com/tinfoilsh/verifier/
79
+ const PLATFORM_TYPES = {
80
+ SNP_TDX_MULTI_PLATFORM_V1: "https://tinfoil.sh/predicate/snp-tdx-multiplatform/v1",
81
+ TDX_GUEST_V1: "https://tinfoil.sh/predicate/tdx-guest/v1",
82
+ TDX_GUEST_V2: "https://tinfoil.sh/predicate/tdx-guest/v2",
83
+ SEV_GUEST_V1: "https://tinfoil.sh/predicate/sev-snp-guest/v1",
84
+ SEV_GUEST_V2: "https://tinfoil.sh/predicate/sev-snp-guest/v2",
85
+ HARDWARE_MEASUREMENTS_V1: "https://tinfoil.sh/predicate/hardware-measurements/v1",
86
+ };
87
+ const MEASUREMENT_ERROR_MESSAGES = {
88
+ FORMAT_MISMATCH: "attestation format mismatch",
89
+ MEASUREMENT_MISMATCH: "measurement mismatch",
90
+ RTMR1_MISMATCH: "RTMR1 mismatch",
91
+ RTMR2_MISMATCH: "RTMR2 mismatch",
92
+ FEW_REGISTERS: "fewer registers than expected",
93
+ MULTI_PLATFORM_MISMATCH: "multi-platform measurement mismatch",
94
+ MULTI_PLATFORM_SEV_SNP_MISMATCH: "multi-platform SEV-SNP measurement mismatch",
95
+ };
96
+ function registersEqual(a, b) {
97
+ if (a.length !== b.length) {
98
+ return false;
99
+ }
100
+ return a.every((value, index) => value === b[index]);
101
+ }
102
+ function compareMeasurementsError(codeMeasurement, runtimeMeasurement) {
103
+ if (codeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1 &&
104
+ runtimeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1) {
105
+ if (!registersEqual(codeMeasurement.registers, runtimeMeasurement.registers)) {
106
+ return new Error(MEASUREMENT_ERROR_MESSAGES.MULTI_PLATFORM_MISMATCH);
107
+ }
108
+ return null;
109
+ }
110
+ if (runtimeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1) {
111
+ return compareMeasurementsError(runtimeMeasurement, codeMeasurement);
112
+ }
113
+ if (codeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1) {
114
+ switch (runtimeMeasurement.type) {
115
+ case PLATFORM_TYPES.TDX_GUEST_V1: {
116
+ if (codeMeasurement.registers.length < 3 ||
117
+ runtimeMeasurement.registers.length < 4) {
118
+ return new Error(MEASUREMENT_ERROR_MESSAGES.FEW_REGISTERS);
119
+ }
120
+ const expectedRtmr1 = codeMeasurement.registers[1];
121
+ const expectedRtmr2 = codeMeasurement.registers[2];
122
+ const actualRtmr1 = runtimeMeasurement.registers[2];
123
+ const actualRtmr2 = runtimeMeasurement.registers[3];
124
+ if (expectedRtmr1 !== actualRtmr1) {
125
+ return new Error(MEASUREMENT_ERROR_MESSAGES.RTMR1_MISMATCH);
126
+ }
127
+ if (expectedRtmr2 !== actualRtmr2) {
128
+ return new Error(MEASUREMENT_ERROR_MESSAGES.RTMR2_MISMATCH);
129
+ }
130
+ return null;
131
+ }
132
+ case PLATFORM_TYPES.SEV_GUEST_V1:
133
+ case PLATFORM_TYPES.SEV_GUEST_V2: {
134
+ if (codeMeasurement.registers.length < 1 ||
135
+ runtimeMeasurement.registers.length < 1) {
136
+ return new Error(MEASUREMENT_ERROR_MESSAGES.FEW_REGISTERS);
137
+ }
138
+ const expectedSevSnp = codeMeasurement.registers[0];
139
+ const actualSevSnp = runtimeMeasurement.registers[0];
140
+ if (expectedSevSnp !== actualSevSnp) {
141
+ return new Error(MEASUREMENT_ERROR_MESSAGES.MULTI_PLATFORM_SEV_SNP_MISMATCH);
142
+ }
143
+ return null;
144
+ }
145
+ default:
146
+ return new Error(`unsupported enclave platform for multi-platform code measurements: ${runtimeMeasurement.type}`);
147
+ }
148
+ }
149
+ if (codeMeasurement.type !== runtimeMeasurement.type) {
150
+ return new Error(MEASUREMENT_ERROR_MESSAGES.FORMAT_MISMATCH);
151
+ }
152
+ if (!registersEqual(codeMeasurement.registers, runtimeMeasurement.registers)) {
153
+ return new Error(MEASUREMENT_ERROR_MESSAGES.MEASUREMENT_MISMATCH);
154
+ }
155
+ return null;
156
+ }
157
+ export function compareMeasurementsDetailed(codeMeasurement, runtimeMeasurement) {
158
+ const error = compareMeasurementsError(codeMeasurement, runtimeMeasurement);
159
+ if (error) {
160
+ return { match: false, error };
161
+ }
162
+ return { match: true };
163
+ }
164
+ /**
165
+ * Compare two measurements according to platform-specific rules
166
+ * This is predicate function for comparing attestation measurements
167
+ * taken from https://github.com/tinfoilsh/verifier/blob/main/attestation/attestation.go
168
+ *
169
+ * @param codeMeasurement - Expected measurement from code attestation
170
+ * @param runtimeMeasurement - Actual measurement from runtime attestation
171
+ * @returns true if measurements match according to platform rules
172
+ */
173
+ export function compareMeasurements(codeMeasurement, runtimeMeasurement) {
174
+ return compareMeasurementsError(codeMeasurement, runtimeMeasurement) === null;
175
+ }
176
+ /**
177
+ * Verifier performs attestation verification for Tinfoil enclaves
178
+ *
179
+ * The verifier loads a WebAssembly module that:
180
+ * 1. Fetches the latest code release digest from GitHub
181
+ * 2. Performs runtime attestation against the enclave
182
+ * 3. Performs code attestation using the digest
183
+ * 4. Compares measurements using platform-specific logic
184
+ */
185
+ export class Verifier {
186
+ constructor(options) {
187
+ const serverURL = options?.serverURL ?? TINFOIL_CONFIG.INFERENCE_BASE_URL;
188
+ this.serverURL = new URL(serverURL).hostname;
189
+ this.configRepo =
190
+ options?.configRepo ?? TINFOIL_CONFIG.INFERENCE_PROXY_REPO;
191
+ }
192
+ /**
193
+ * Execute a function with a fresh WASM instance that auto-cleans up
194
+ * This ensures Go runtime doesn't keep the process alive
195
+ */
196
+ static async executeWithWasm(fn) {
197
+ await initializeWasmGlobals();
198
+ const goInstance = new globalThis.Go();
199
+ // Load WASM module
200
+ const fetchFn = getFetch();
201
+ const wasmResponse = await fetchFn(Verifier.defaultWasmUrl);
202
+ if (!wasmResponse.ok) {
203
+ throw new Error(`Failed to fetch WASM: ${wasmResponse.status} ${wasmResponse.statusText}`);
204
+ }
205
+ const wasmBuffer = await wasmResponse.arrayBuffer();
206
+ const result = await WebAssembly.instantiate(wasmBuffer, goInstance.importObject);
207
+ // Start the Go instance in the background
208
+ // We don't await this - it runs continuously
209
+ goInstance.run(result.instance);
210
+ // Wait for WASM functions to be available
211
+ await new Promise((resolve) => setTimeout(resolve, 100));
212
+ if (typeof globalThis.verifyCode === "undefined" ||
213
+ typeof globalThis.verifyEnclave === "undefined") {
214
+ await new Promise((resolve) => setTimeout(resolve, 500));
215
+ }
216
+ // Apply log suppression if requested
217
+ if (Verifier.wasmLogsSuppressed && globalThis.fs?.writeSync) {
218
+ const fsObj = globalThis.fs;
219
+ if (!Verifier.originalFsWriteSync) {
220
+ Verifier.originalFsWriteSync = fsObj.writeSync.bind(fsObj);
221
+ }
222
+ fsObj.writeSync = function (_fd, buf) {
223
+ return buf.length;
224
+ };
225
+ }
226
+ try {
227
+ // Execute the user's function
228
+ const result = await fn();
229
+ return result;
230
+ }
231
+ finally {
232
+ // Clean up the Go instance
233
+ if (goInstance._scheduledTimeouts instanceof Map) {
234
+ for (const timeoutId of goInstance._scheduledTimeouts.values()) {
235
+ clearTimeout(timeoutId);
236
+ }
237
+ goInstance._scheduledTimeouts.clear();
238
+ }
239
+ if (typeof goInstance.exit === "function") {
240
+ try {
241
+ goInstance.exit(0);
242
+ }
243
+ catch (e) {
244
+ // Ignore exit errors
245
+ }
246
+ }
247
+ }
248
+ }
249
+ /**
250
+ * Fetch the latest release digest from GitHub
251
+ * @param configRepo - Repository name (e.g., "tinfoilsh/confidential-inference-proxy")
252
+ * @returns The digest hash
253
+ */
254
+ async fetchLatestDigest(configRepo) {
255
+ // GitHub Proxy Note:
256
+ // We use api-github-proxy.tinfoil.sh instead of the direct GitHub API to avoid
257
+ // rate limiting that could degrade UX. The proxy caches responses while the
258
+ // integrity of the data is independently verified in `verifyCode` via
259
+ // Sigstore transparency logs (Rekor). Using the proxy therefore does not
260
+ // weaken security.
261
+ const targetRepo = configRepo || this.configRepo;
262
+ const fetchFn = getFetch();
263
+ const releaseResponse = await fetchFn(`https://api-github-proxy.tinfoil.sh/repos/${targetRepo}/releases/latest`, {
264
+ headers: {
265
+ Accept: "application/vnd.github.v3+json",
266
+ "User-Agent": "tinfoil-node-client",
267
+ },
268
+ });
269
+ if (!releaseResponse.ok) {
270
+ throw new Error(`GitHub API request failed: ${releaseResponse.status} ${releaseResponse.statusText}`);
271
+ }
272
+ const releaseData = (await releaseResponse.json());
273
+ // Extract digest from release notes
274
+ const digestRegex = /Digest: `([a-f0-9]{64})`/;
275
+ const digestMatch = releaseData.body?.match(digestRegex);
276
+ if (!digestMatch) {
277
+ throw new Error("Could not find digest in release notes");
278
+ }
279
+ const digest = digestMatch[1];
280
+ return digest;
281
+ }
282
+ /**
283
+ * Perform runtime attestation on the enclave
284
+ * @param enclaveHost - The enclave hostname
285
+ * @returns Attestation response with measurement and keys
286
+ */
287
+ async verifyEnclave(enclaveHost) {
288
+ // Expose errors via explicit Promise rejection and add a timeout
289
+ return new Promise(async (resolve, reject) => {
290
+ try {
291
+ const targetHost = enclaveHost || this.serverURL;
292
+ if (typeof globalThis.verifyEnclave !== "function") {
293
+ reject(new Error("WASM verifyEnclave function not available"));
294
+ return;
295
+ }
296
+ let attestationResponse;
297
+ let timeoutHandle;
298
+ try {
299
+ const timeoutPromise = new Promise((_, timeoutReject) => {
300
+ timeoutHandle = setTimeout(() => timeoutReject(new Error("WASM verifyEnclave timed out after 10 seconds")), 10000);
301
+ });
302
+ attestationResponse = await Promise.race([
303
+ globalThis.verifyEnclave(targetHost),
304
+ timeoutPromise,
305
+ ]);
306
+ // Clear timeout on success
307
+ if (timeoutHandle !== undefined) {
308
+ clearTimeout(timeoutHandle);
309
+ }
310
+ }
311
+ catch (error) {
312
+ // Clear timeout on error
313
+ if (timeoutHandle !== undefined) {
314
+ clearTimeout(timeoutHandle);
315
+ }
316
+ reject(new Error(`WASM verifyEnclave failed: ${error}`));
317
+ return;
318
+ }
319
+ // Validate required fields - fail fast with explicit rejection
320
+ // At least one key must be present (TLS or HPKE)
321
+ if (!attestationResponse?.tls_public_key &&
322
+ !attestationResponse?.hpke_public_key) {
323
+ reject(new Error("Missing both tls_public_key and hpke_public_key in attestation response"));
324
+ return;
325
+ }
326
+ // Parse runtime measurement
327
+ let parsedRuntimeMeasurement;
328
+ try {
329
+ if (attestationResponse.measurement &&
330
+ typeof attestationResponse.measurement === "string") {
331
+ parsedRuntimeMeasurement = JSON.parse(attestationResponse.measurement);
332
+ }
333
+ else if (attestationResponse.measurement &&
334
+ typeof attestationResponse.measurement === "object") {
335
+ parsedRuntimeMeasurement = attestationResponse.measurement;
336
+ }
337
+ else {
338
+ reject(new Error("Invalid runtime measurement format"));
339
+ return;
340
+ }
341
+ }
342
+ catch (parseError) {
343
+ reject(new Error(`Failed to parse runtime measurement: ${parseError}`));
344
+ return;
345
+ }
346
+ const result = {
347
+ measurement: parsedRuntimeMeasurement,
348
+ };
349
+ // Include keys if available
350
+ if (attestationResponse.tls_public_key) {
351
+ result.tlsPublicKeyFingerprint = attestationResponse.tls_public_key;
352
+ }
353
+ if (attestationResponse.hpke_public_key) {
354
+ result.hpkePublicKey = attestationResponse.hpke_public_key;
355
+ }
356
+ resolve(result);
357
+ }
358
+ catch (outerError) {
359
+ reject(outerError);
360
+ }
361
+ });
362
+ }
363
+ /**
364
+ * Perform code attestation
365
+ * @param configRepo - Repository name
366
+ * @param digest - Code digest hash
367
+ * @returns Code measurement
368
+ */
369
+ async verifyCode(configRepo, digest) {
370
+ if (typeof globalThis.verifyCode !== "function") {
371
+ throw new Error("WASM verifyCode function not available");
372
+ }
373
+ const rawMeasurement = await globalThis.verifyCode(configRepo, digest);
374
+ const normalizedMeasurement = typeof rawMeasurement === "string"
375
+ ? (() => {
376
+ try {
377
+ return JSON.parse(rawMeasurement);
378
+ }
379
+ catch (error) {
380
+ throw new Error(`Invalid code measurement format: ${error.message}`);
381
+ }
382
+ })()
383
+ : rawMeasurement;
384
+ if (!normalizedMeasurement || typeof normalizedMeasurement !== "object") {
385
+ throw new Error("Invalid code measurement format");
386
+ }
387
+ const measurementObject = normalizedMeasurement;
388
+ if (typeof measurementObject.type !== "string" ||
389
+ !Array.isArray(measurementObject.registers)) {
390
+ throw new Error("Invalid code measurement format");
391
+ }
392
+ const parsedCodeMeasurement = {
393
+ type: measurementObject.type,
394
+ registers: measurementObject.registers.map((value) => String(value)),
395
+ };
396
+ return { measurement: parsedCodeMeasurement };
397
+ }
398
+ /**
399
+ * Perform attestation verification
400
+ *
401
+ * This method:
402
+ * 1. Fetches the latest code digest from GitHub releases
403
+ * 2. Calls verifyCode to get the expected measurement for the code
404
+ * 3. Calls verifyEnclave to get the actual runtime measurement
405
+ * 4. Compares measurements using platform-specific logic (see `compareMeasurements()`)
406
+ * 5. Returns the attestation response if verification succeeds
407
+ *
408
+ * The WASM runtime is automatically initialized and cleaned up within this method.
409
+ *
410
+ * @throws Error if measurements don't match or verification fails
411
+ */
412
+ async verify() {
413
+ return Verifier.executeWithWasm(async () => {
414
+ return this.verifyInternal();
415
+ });
416
+ }
417
+ /**
418
+ * Internal verification logic that runs within WASM context
419
+ */
420
+ async verifyInternal() {
421
+ const steps = {
422
+ fetchDigest: { status: "pending" },
423
+ verifyCode: { status: "pending" },
424
+ verifyEnclave: { status: "pending" },
425
+ compareMeasurements: { status: "pending" },
426
+ };
427
+ let releaseDigest;
428
+ let codeMeasurement;
429
+ let attestation;
430
+ try {
431
+ releaseDigest = await this.fetchLatestDigest(this.configRepo);
432
+ steps.fetchDigest = { status: "success" };
433
+ }
434
+ catch (error) {
435
+ steps.fetchDigest = { status: "failed", error: error.message };
436
+ this.lastVerificationDocument = {
437
+ configRepo: this.configRepo,
438
+ enclaveHost: this.serverURL,
439
+ releaseDigest: "",
440
+ codeMeasurement: { type: "", registers: [] },
441
+ enclaveMeasurement: { measurement: { type: "", registers: [] } },
442
+ securityVerified: false,
443
+ steps,
444
+ };
445
+ throw error;
446
+ }
447
+ try {
448
+ const results = await Promise.all([
449
+ this.verifyCode(this.configRepo, releaseDigest).then((result) => {
450
+ steps.verifyCode = { status: "success" };
451
+ return result;
452
+ }, (error) => {
453
+ steps.verifyCode = {
454
+ status: "failed",
455
+ error: error.message,
456
+ };
457
+ throw error;
458
+ }),
459
+ this.verifyEnclave(this.serverURL).then((result) => {
460
+ steps.verifyEnclave = { status: "success" };
461
+ return result;
462
+ }, (error) => {
463
+ steps.verifyEnclave = {
464
+ status: "failed",
465
+ error: error.message,
466
+ };
467
+ throw error;
468
+ }),
469
+ ]);
470
+ codeMeasurement = results[0].measurement;
471
+ attestation = results[1];
472
+ }
473
+ catch (error) {
474
+ this.lastVerificationDocument = {
475
+ configRepo: this.configRepo,
476
+ enclaveHost: this.serverURL,
477
+ releaseDigest: releaseDigest,
478
+ codeMeasurement: codeMeasurement,
479
+ enclaveMeasurement: attestation,
480
+ securityVerified: false,
481
+ steps,
482
+ };
483
+ throw error;
484
+ }
485
+ const measurementsMatchError = compareMeasurementsError(codeMeasurement, attestation.measurement);
486
+ if (measurementsMatchError) {
487
+ steps.compareMeasurements = {
488
+ status: "failed",
489
+ error: measurementsMatchError.message,
490
+ };
491
+ this.lastVerificationDocument = {
492
+ configRepo: this.configRepo,
493
+ enclaveHost: this.serverURL,
494
+ releaseDigest: releaseDigest,
495
+ codeMeasurement,
496
+ enclaveMeasurement: attestation,
497
+ securityVerified: false,
498
+ steps,
499
+ };
500
+ throw new Error(`Verification failed: measurements did not match.\nCode measurement (${codeMeasurement.type}: ${codeMeasurement.registers})\n` +
501
+ `Runtime measurement (${attestation.measurement.type}: ${attestation.measurement.registers}:)\n ${measurementsMatchError.message}`);
502
+ }
503
+ steps.compareMeasurements = { status: "success" };
504
+ this.lastVerificationDocument = {
505
+ configRepo: this.configRepo,
506
+ enclaveHost: this.serverURL,
507
+ releaseDigest: releaseDigest,
508
+ codeMeasurement,
509
+ enclaveMeasurement: attestation,
510
+ securityVerified: true,
511
+ steps,
512
+ };
513
+ return attestation;
514
+ }
515
+ /**
516
+ * Returns the full verification document from the last successful verify() call
517
+ */
518
+ getVerificationDocument() {
519
+ return this.lastVerificationDocument;
520
+ }
521
+ }
522
+ Verifier.goInstance = null;
523
+ Verifier.initializationPromise = null;
524
+ Verifier.defaultWasmUrl = "https://tinfoilsh.github.io/verifier-js/tinfoil-verifier.wasm";
525
+ Verifier.originalFsWriteSync = null;
526
+ Verifier.wasmLogsSuppressed = true;
527
+ Verifier.globalsInitialized = false;
528
+ // Start initialization as soon as the module loads
529
+ function shouldAutoInitializeWasm() {
530
+ const globalAny = globalThis;
531
+ const env = globalAny.process?.env;
532
+ const autoInitFlag = env?.TINFOIL_SKIP_WASM_AUTO_INIT ?? env?.TINFOIL_DISABLE_WASM_AUTO_INIT;
533
+ if (typeof autoInitFlag === "string") {
534
+ const normalized = autoInitFlag.toLowerCase();
535
+ if (normalized === "1" || normalized === "true") {
536
+ return false;
537
+ }
538
+ }
539
+ if (env?.NODE_ENV === "test") {
540
+ return false;
541
+ }
542
+ const isNode = typeof globalAny.process?.versions?.node === "string";
543
+ if (isNode) {
544
+ return false;
545
+ }
546
+ const hasBrowserGlobals = typeof globalAny.window === "object" &&
547
+ typeof globalAny.document === "object";
548
+ return hasBrowserGlobals;
549
+ }
550
+ /**
551
+ * Control WASM log output
552
+ *
553
+ * The Go WASM runtime outputs logs through a polyfilled fs.writeSync.
554
+ * This function allows suppressing those logs without affecting other console output.
555
+ *
556
+ * @param suppress - Whether to suppress WASM logs (default: true)
557
+ */
558
+ export function suppressWasmLogs(suppress = true) {
559
+ globalThis.__tinfoilSuppressWasmLogs = suppress;
560
+ Verifier.wasmLogsSuppressed = suppress;
561
+ const fsObj = globalThis.fs;
562
+ if (!fsObj || typeof fsObj.writeSync !== "function")
563
+ return;
564
+ if (suppress) {
565
+ if (!Verifier.originalFsWriteSync) {
566
+ Verifier.originalFsWriteSync = fsObj.writeSync.bind(fsObj);
567
+ }
568
+ fsObj.writeSync = function (_fd, buf) {
569
+ return buf.length;
570
+ };
571
+ }
572
+ else if (Verifier.originalFsWriteSync) {
573
+ fsObj.writeSync = Verifier.originalFsWriteSync;
574
+ }
575
+ }
576
+ /**
577
+ * Initialize globals needed for Go WASM runtime
578
+ * This function sets up browser-like globals that the Go WASM runtime expects
579
+ */
580
+ async function initializeWasmGlobals() {
581
+ // Only initialize once
582
+ if (Verifier.globalsInitialized) {
583
+ return;
584
+ }
585
+ const root = globalThis;
586
+ // Performance API (Go runtime expects a few methods to exist)
587
+ if (!root.performance) {
588
+ root.performance = {
589
+ now: () => Date.now(),
590
+ markResourceTiming: () => { },
591
+ mark: () => { },
592
+ measure: () => { },
593
+ clearMarks: () => { },
594
+ clearMeasures: () => { },
595
+ getEntriesByName: () => [],
596
+ getEntriesByType: () => [],
597
+ getEntries: () => [],
598
+ };
599
+ }
600
+ else {
601
+ root.performance.now = root.performance.now ?? (() => Date.now());
602
+ root.performance.markResourceTiming =
603
+ root.performance.markResourceTiming ?? (() => { });
604
+ root.performance.mark = root.performance.mark ?? (() => { });
605
+ root.performance.measure = root.performance.measure ?? (() => { });
606
+ root.performance.clearMarks = root.performance.clearMarks ?? (() => { });
607
+ root.performance.clearMeasures =
608
+ root.performance.clearMeasures ?? (() => { });
609
+ root.performance.getEntriesByName =
610
+ root.performance.getEntriesByName ?? (() => []);
611
+ root.performance.getEntriesByType =
612
+ root.performance.getEntriesByType ?? (() => []);
613
+ root.performance.getEntries = root.performance.getEntries ?? (() => []);
614
+ }
615
+ // Text encoding
616
+ if (!root.TextEncoder) {
617
+ root.TextEncoder = getTextEncoder();
618
+ }
619
+ if (!root.TextDecoder) {
620
+ root.TextDecoder = getTextDecoder();
621
+ }
622
+ // Crypto API (needed by Go WASM)
623
+ ensureCrypto(root);
624
+ // Default: suppress WASM (Go) stdout/stderr logs unless explicitly enabled by caller
625
+ if (typeof root.__tinfoilSuppressWasmLogs === "undefined") {
626
+ root.__tinfoilSuppressWasmLogs = true;
627
+ }
628
+ // Force process to stay running (prevent Go from exiting Node process)
629
+ // This is a common issue with Go WASM in Node - it calls process.exit()
630
+ if (root.process &&
631
+ typeof root.process.exit === "function" &&
632
+ !root.__tinfoilProcessExitPatched) {
633
+ root.__tinfoilProcessExitPatched = true;
634
+ const originalExit = root.process.exit.bind(root.process);
635
+ root.__tinfoilOriginalProcessExit = originalExit;
636
+ // Replace process.exit to prevent the Go WASM runtime from terminating the Node.js process.
637
+ // When wasm log suppression is enabled, suppress the informational log about the ignored exit
638
+ // so callers can silence only the WASM-related noise while keeping application logs intact.
639
+ root.process.exit = ((code) => {
640
+ if (!root.__tinfoilSuppressWasmLogs) {
641
+ console.log(`Process exit called with code ${code} - ignoring to keep runtime alive`);
642
+ }
643
+ return undefined;
644
+ });
645
+ }
646
+ await loadWasmExec();
647
+ Verifier.globalsInitialized = true;
648
+ }
649
+ function ensureCrypto(root) {
650
+ const hasWorkingGetRandomValues = root.crypto && typeof root.crypto.getRandomValues === "function"
651
+ ? root.crypto
652
+ : resolveWindowCrypto(root);
653
+ if (hasWorkingGetRandomValues) {
654
+ if (!root.crypto) {
655
+ root.crypto = hasWorkingGetRandomValues;
656
+ }
657
+ return;
658
+ }
659
+ const nodeRandomBytes = resolveNodeRandomBytes();
660
+ if (!nodeRandomBytes) {
661
+ throw new Error("crypto.getRandomValues is not available in this environment");
662
+ }
663
+ const fallbackCrypto = {
664
+ getRandomValues: (buffer) => {
665
+ const bytes = nodeRandomBytes(buffer.length);
666
+ buffer.set(bytes);
667
+ return buffer;
668
+ },
669
+ };
670
+ try {
671
+ root.crypto = fallbackCrypto;
672
+ }
673
+ catch {
674
+ Object.defineProperty(root, "crypto", {
675
+ configurable: true,
676
+ enumerable: false,
677
+ value: fallbackCrypto,
678
+ writable: false,
679
+ });
680
+ }
681
+ }
682
+ function resolveWindowCrypto(root) {
683
+ const maybeWindow = root.window ?? (typeof window !== "undefined" ? window : undefined);
684
+ if (maybeWindow?.crypto &&
685
+ typeof maybeWindow.crypto.getRandomValues === "function") {
686
+ return maybeWindow.crypto;
687
+ }
688
+ return undefined;
689
+ }
690
+ function resolveNodeRandomBytes() {
691
+ if (!nodeRequire) {
692
+ return undefined;
693
+ }
694
+ try {
695
+ const cryptoModule = nodeRequire("crypto");
696
+ const randomBytes = typeof cryptoModule?.randomBytes === "function"
697
+ ? cryptoModule.randomBytes
698
+ : undefined;
699
+ if (randomBytes) {
700
+ return (size) => {
701
+ const result = randomBytes(size);
702
+ return result instanceof Uint8Array ? result : new Uint8Array(result);
703
+ };
704
+ }
705
+ }
706
+ catch {
707
+ return undefined;
708
+ }
709
+ return undefined;
710
+ }
711
+ function loadWasmExec() {
712
+ if (!wasmExecLoader) {
713
+ wasmExecLoader = (async () => {
714
+ // Prefer a dynamic import so bundlers (Next/Webpack/Vite) include the file.
715
+ // If that fails (e.g., pure Node without bundler), fall back to require.
716
+ try {
717
+ // @ts-expect-error: Local JS helper has no TS types; ambient module declared in wasm-exec.d.ts
718
+ await import("./wasm-exec.js");
719
+ }
720
+ catch {
721
+ if (nodeRequire) {
722
+ nodeRequire("./wasm-exec.js");
723
+ return;
724
+ }
725
+ throw new Error("Failed to load wasm-exec.js via dynamic import, and require() is unavailable");
726
+ }
727
+ })();
728
+ wasmExecLoader.catch(() => {
729
+ wasmExecLoader = null;
730
+ });
731
+ }
732
+ return wasmExecLoader;
733
+ }
734
+ function createNodeRequire() {
735
+ try {
736
+ return typeof require === "function" ? require : undefined;
737
+ }
738
+ catch {
739
+ return undefined;
740
+ }
741
+ }