@zeke-02/tinfoil 0.0.5 → 0.0.7
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/README.md +38 -24
- package/dist/ai-sdk-provider.d.ts +1 -1
- package/dist/ai-sdk-provider.js +2 -1
- package/dist/esm/ai-sdk-provider.d.ts +1 -1
- package/dist/esm/ai-sdk-provider.mjs +2 -1
- package/dist/esm/secure-client.mjs +9 -0
- package/dist/esm/verifier.d.ts +49 -49
- package/dist/esm/verifier.mjs +150 -362
- package/dist/secure-client.js +9 -0
- package/dist/verifier.d.ts +49 -49
- package/dist/verifier.js +150 -364
- package/package.json +2 -2
package/dist/verifier.js
CHANGED
|
@@ -34,40 +34,55 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.Verifier = void 0;
|
|
37
|
-
exports.compareMeasurementsDetailed = compareMeasurementsDetailed;
|
|
38
|
-
exports.compareMeasurements = compareMeasurements;
|
|
39
37
|
exports.suppressWasmLogs = suppressWasmLogs;
|
|
40
38
|
/**
|
|
41
39
|
* VERIFIER COMPONENT OVERVIEW
|
|
42
40
|
* ==========================
|
|
43
41
|
*
|
|
44
|
-
* This implementation performs
|
|
45
|
-
* a Go WebAssembly module, and exposes a small TypeScript API around it.
|
|
42
|
+
* This implementation performs end-to-end enclave and code verification entirely on the
|
|
43
|
+
* client using a Go WebAssembly module, and exposes a small TypeScript API around it.
|
|
46
44
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
45
|
+
* UNIFIED VERIFICATION FLOW
|
|
46
|
+
* The primary API is `verify()`, which invokes the Go WASM `verify(enclaveHost, repo)`
|
|
47
|
+
* function that performs all verification steps atomically:
|
|
48
|
+
*
|
|
49
|
+
* 1) DIGEST FETCH
|
|
50
|
+
* - Fetches the latest release digest from GitHub
|
|
51
|
+
* - Uses Tinfoil GitHub proxy (https://api-github-proxy.tinfoil.sh) to avoid rate limits
|
|
52
52
|
*
|
|
53
53
|
* 2) CODE INTEGRITY (Release Verification)
|
|
54
|
-
* -
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* - Verifies code provenance using Sigstore/Rekor for GitHub Actions builds
|
|
55
|
+
* - Returns the expected code measurement for the release
|
|
56
|
+
*
|
|
57
|
+
* 3) REMOTE ATTESTATION (Enclave Verification)
|
|
58
|
+
* - Performs runtime attestation against the target enclave hostname
|
|
59
|
+
* - Verifies vendor certificate chains inside WASM (AMD SEV-SNP / Intel TDX)
|
|
60
|
+
* - Returns the enclave's runtime measurement and cryptographic keys (TLS fingerprint and HPKE)
|
|
58
61
|
*
|
|
59
|
-
*
|
|
60
|
-
* -
|
|
61
|
-
*
|
|
62
|
+
* 4) HARDWARE VERIFICATION (for TDX platforms)
|
|
63
|
+
* - Fetches and verifies TDX platform measurements if required
|
|
64
|
+
* - Validates hardware attestation against expected measurements
|
|
65
|
+
*
|
|
66
|
+
* 5) CODE CONSISTENCY (Measurement Comparison)
|
|
67
|
+
* - Compares the runtime measurement with the expected code measurement
|
|
68
|
+
* - Uses platform-aware comparison rules for different TEE types
|
|
69
|
+
*
|
|
70
|
+
* ERROR HANDLING
|
|
71
|
+
* When verification fails, errors are prefixed with the failing step:
|
|
72
|
+
* - `fetchDigest:` - Failed to fetch GitHub release digest
|
|
73
|
+
* - `verifyCode:` - Failed to verify code provenance
|
|
74
|
+
* - `verifyEnclave:` - Failed runtime attestation
|
|
75
|
+
* - `verifyHardware:` - Failed TDX hardware verification
|
|
76
|
+
* - `validateTLS:` - TLS public key validation failed
|
|
77
|
+
* - `measurements:` - Measurement comparison failed
|
|
62
78
|
*
|
|
63
79
|
* RUNTIME AND DELIVERY
|
|
64
80
|
* - All verification executes locally via WebAssembly (Go → WASM)
|
|
65
81
|
* - WASM loader: `wasm-exec.js`
|
|
66
|
-
* - WASM module URL: https://tinfoilsh.github.io/verifier
|
|
82
|
+
* - WASM module URL: https://tinfoilsh.github.io/verifier/tinfoil-verifier.wasm
|
|
67
83
|
* - Works in Node 20+ and modern browsers with lightweight polyfills for
|
|
68
84
|
* `performance`, `TextEncoder`/`TextDecoder`, and `crypto.getRandomValues`
|
|
69
85
|
* - Go stdout/stderr is suppressed by default; toggle via `suppressWasmLogs()`
|
|
70
|
-
* - Module auto-initializes the WASM runtime on import
|
|
71
86
|
*
|
|
72
87
|
* PROXIES AND TRUST
|
|
73
88
|
* - GitHub proxy is used only to avoid rate limits; the WASM logic independently
|
|
@@ -75,19 +90,16 @@ exports.suppressWasmLogs = suppressWasmLogs;
|
|
|
75
90
|
* - AMD KDS access may be proxied within the WASM for availability; AMD roots are
|
|
76
91
|
* embedded and the full chain is verified in Go to prevent forgery
|
|
77
92
|
*
|
|
78
|
-
* SUPPORTED PLATFORMS
|
|
79
|
-
* -
|
|
80
|
-
*
|
|
81
|
-
* -
|
|
93
|
+
* SUPPORTED PLATFORMS
|
|
94
|
+
* - AMD SEV-SNP
|
|
95
|
+
* - Intel TDX (with hardware platform verification)
|
|
96
|
+
* - Predicate types: SNP/TDX multi-platform v1, TDX guest v1/v2, SEV-SNP guest v1
|
|
82
97
|
*
|
|
83
98
|
* PUBLIC API (this module)
|
|
84
|
-
* - `new Verifier({ serverURL
|
|
85
|
-
* - `verify()` → full end-to-end verification and
|
|
86
|
-
* - `
|
|
87
|
-
* - `
|
|
88
|
-
* - `compareMeasurements(code, runtime)` → predicate-based comparison
|
|
89
|
-
* - `fetchLatestDigest(configRepo?)` → release digest via proxy
|
|
90
|
-
* - `suppressWasmLogs(suppress?)` → control WASM log output
|
|
99
|
+
* - `new Verifier({ serverURL, configRepo? })`
|
|
100
|
+
* - `verify()` → Promise<AttestationResponse> - full end-to-end verification returning cryptographic keys and measurement
|
|
101
|
+
* - `getVerificationDocument()` → VerificationDocument | undefined - detailed step-by-step verification results
|
|
102
|
+
* - `suppressWasmLogs(suppress?)` → void - control WASM log output
|
|
91
103
|
*/
|
|
92
104
|
const config_1 = require("./config");
|
|
93
105
|
const fetch_adapter_1 = require("./fetch-adapter");
|
|
@@ -113,114 +125,19 @@ function getTextDecoder() {
|
|
|
113
125
|
}
|
|
114
126
|
const nodeRequire = createNodeRequire();
|
|
115
127
|
let wasmExecLoader = null;
|
|
116
|
-
// Platform type constants
|
|
117
|
-
// See https://github.com/tinfoilsh/verifier/
|
|
118
|
-
const PLATFORM_TYPES = {
|
|
119
|
-
SNP_TDX_MULTI_PLATFORM_V1: "https://tinfoil.sh/predicate/snp-tdx-multiplatform/v1",
|
|
120
|
-
TDX_GUEST_V1: "https://tinfoil.sh/predicate/tdx-guest/v1",
|
|
121
|
-
TDX_GUEST_V2: "https://tinfoil.sh/predicate/tdx-guest/v2",
|
|
122
|
-
SEV_GUEST_V1: "https://tinfoil.sh/predicate/sev-snp-guest/v1",
|
|
123
|
-
SEV_GUEST_V2: "https://tinfoil.sh/predicate/sev-snp-guest/v2",
|
|
124
|
-
HARDWARE_MEASUREMENTS_V1: "https://tinfoil.sh/predicate/hardware-measurements/v1",
|
|
125
|
-
};
|
|
126
|
-
const MEASUREMENT_ERROR_MESSAGES = {
|
|
127
|
-
FORMAT_MISMATCH: "attestation format mismatch",
|
|
128
|
-
MEASUREMENT_MISMATCH: "measurement mismatch",
|
|
129
|
-
RTMR1_MISMATCH: "RTMR1 mismatch",
|
|
130
|
-
RTMR2_MISMATCH: "RTMR2 mismatch",
|
|
131
|
-
FEW_REGISTERS: "fewer registers than expected",
|
|
132
|
-
MULTI_PLATFORM_MISMATCH: "multi-platform measurement mismatch",
|
|
133
|
-
MULTI_PLATFORM_SEV_SNP_MISMATCH: "multi-platform SEV-SNP measurement mismatch",
|
|
134
|
-
};
|
|
135
|
-
function registersEqual(a, b) {
|
|
136
|
-
if (a.length !== b.length) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
return a.every((value, index) => value === b[index]);
|
|
140
|
-
}
|
|
141
|
-
function compareMeasurementsError(codeMeasurement, runtimeMeasurement) {
|
|
142
|
-
if (codeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1 &&
|
|
143
|
-
runtimeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1) {
|
|
144
|
-
if (!registersEqual(codeMeasurement.registers, runtimeMeasurement.registers)) {
|
|
145
|
-
return new Error(MEASUREMENT_ERROR_MESSAGES.MULTI_PLATFORM_MISMATCH);
|
|
146
|
-
}
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
if (runtimeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1) {
|
|
150
|
-
return compareMeasurementsError(runtimeMeasurement, codeMeasurement);
|
|
151
|
-
}
|
|
152
|
-
if (codeMeasurement.type === PLATFORM_TYPES.SNP_TDX_MULTI_PLATFORM_V1) {
|
|
153
|
-
switch (runtimeMeasurement.type) {
|
|
154
|
-
case PLATFORM_TYPES.TDX_GUEST_V1:
|
|
155
|
-
case PLATFORM_TYPES.TDX_GUEST_V2: {
|
|
156
|
-
if (codeMeasurement.registers.length < 3 ||
|
|
157
|
-
runtimeMeasurement.registers.length < 4) {
|
|
158
|
-
return new Error(MEASUREMENT_ERROR_MESSAGES.FEW_REGISTERS);
|
|
159
|
-
}
|
|
160
|
-
const expectedRtmr1 = codeMeasurement.registers[1];
|
|
161
|
-
const expectedRtmr2 = codeMeasurement.registers[2];
|
|
162
|
-
const actualRtmr1 = runtimeMeasurement.registers[2];
|
|
163
|
-
const actualRtmr2 = runtimeMeasurement.registers[3];
|
|
164
|
-
if (expectedRtmr1 !== actualRtmr1) {
|
|
165
|
-
return new Error(MEASUREMENT_ERROR_MESSAGES.RTMR1_MISMATCH);
|
|
166
|
-
}
|
|
167
|
-
if (expectedRtmr2 !== actualRtmr2) {
|
|
168
|
-
return new Error(MEASUREMENT_ERROR_MESSAGES.RTMR2_MISMATCH);
|
|
169
|
-
}
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
case PLATFORM_TYPES.SEV_GUEST_V1:
|
|
173
|
-
case PLATFORM_TYPES.SEV_GUEST_V2: {
|
|
174
|
-
if (codeMeasurement.registers.length < 1 ||
|
|
175
|
-
runtimeMeasurement.registers.length < 1) {
|
|
176
|
-
return new Error(MEASUREMENT_ERROR_MESSAGES.FEW_REGISTERS);
|
|
177
|
-
}
|
|
178
|
-
const expectedSevSnp = codeMeasurement.registers[0];
|
|
179
|
-
const actualSevSnp = runtimeMeasurement.registers[0];
|
|
180
|
-
if (expectedSevSnp !== actualSevSnp) {
|
|
181
|
-
return new Error(MEASUREMENT_ERROR_MESSAGES.MULTI_PLATFORM_SEV_SNP_MISMATCH);
|
|
182
|
-
}
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
default:
|
|
186
|
-
return new Error(`unsupported enclave platform for multi-platform code measurements: ${runtimeMeasurement.type}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (codeMeasurement.type !== runtimeMeasurement.type) {
|
|
190
|
-
return new Error(MEASUREMENT_ERROR_MESSAGES.FORMAT_MISMATCH);
|
|
191
|
-
}
|
|
192
|
-
if (!registersEqual(codeMeasurement.registers, runtimeMeasurement.registers)) {
|
|
193
|
-
return new Error(MEASUREMENT_ERROR_MESSAGES.MEASUREMENT_MISMATCH);
|
|
194
|
-
}
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
function compareMeasurementsDetailed(codeMeasurement, runtimeMeasurement) {
|
|
198
|
-
const error = compareMeasurementsError(codeMeasurement, runtimeMeasurement);
|
|
199
|
-
if (error) {
|
|
200
|
-
return { match: false, error };
|
|
201
|
-
}
|
|
202
|
-
return { match: true };
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Compare two measurements according to platform-specific rules
|
|
206
|
-
* This is predicate function for comparing attestation measurements
|
|
207
|
-
* taken from https://github.com/tinfoilsh/verifier/blob/main/attestation/attestation.go
|
|
208
|
-
*
|
|
209
|
-
* @param codeMeasurement - Expected measurement from code attestation
|
|
210
|
-
* @param runtimeMeasurement - Actual measurement from runtime attestation
|
|
211
|
-
* @returns true if measurements match according to platform rules
|
|
212
|
-
*/
|
|
213
|
-
function compareMeasurements(codeMeasurement, runtimeMeasurement) {
|
|
214
|
-
return compareMeasurementsError(codeMeasurement, runtimeMeasurement) === null;
|
|
215
|
-
}
|
|
216
128
|
/**
|
|
217
129
|
* Verifier performs attestation verification for Tinfoil enclaves
|
|
218
130
|
*
|
|
219
|
-
* The verifier loads a WebAssembly module that
|
|
131
|
+
* The verifier loads a WebAssembly module (compiled from Go) that performs
|
|
132
|
+
* end-to-end attestation verification:
|
|
220
133
|
* 1. Fetches the latest code release digest from GitHub
|
|
221
|
-
* 2.
|
|
222
|
-
* 3. Performs
|
|
223
|
-
* 4.
|
|
134
|
+
* 2. Verifies code provenance using Sigstore/Rekor
|
|
135
|
+
* 3. Performs runtime attestation against the enclave
|
|
136
|
+
* 4. Verifies hardware measurements (for TDX platforms)
|
|
137
|
+
* 5. Compares code and runtime measurements using platform-specific logic
|
|
138
|
+
*
|
|
139
|
+
* Primary method: verify() - Returns AttestationResponse with cryptographic keys
|
|
140
|
+
* Verification details: getVerificationDocument() - Returns step-by-step results
|
|
224
141
|
*/
|
|
225
142
|
class Verifier {
|
|
226
143
|
constructor(options) {
|
|
@@ -289,173 +206,46 @@ class Verifier {
|
|
|
289
206
|
}
|
|
290
207
|
}
|
|
291
208
|
/**
|
|
292
|
-
*
|
|
293
|
-
* @param configRepo - Repository name (e.g., "tinfoilsh/confidential-model-router")
|
|
294
|
-
* @returns The digest hash
|
|
295
|
-
*/
|
|
296
|
-
async fetchLatestDigest(configRepo) {
|
|
297
|
-
// GitHub Proxy Note:
|
|
298
|
-
// We use api-github-proxy.tinfoil.sh instead of the direct GitHub API to avoid
|
|
299
|
-
// rate limiting that could degrade UX. The proxy caches responses while the
|
|
300
|
-
// integrity of the data is independently verified in `verifyCode` via
|
|
301
|
-
// Sigstore transparency logs (Rekor). Using the proxy therefore does not
|
|
302
|
-
// weaken security.
|
|
303
|
-
const targetRepo = configRepo || this.configRepo;
|
|
304
|
-
const fetchFn = (0, fetch_adapter_1.getFetch)();
|
|
305
|
-
const releaseResponse = await fetchFn(`https://api-github-proxy.tinfoil.sh/repos/${targetRepo}/releases/latest`, {
|
|
306
|
-
headers: {
|
|
307
|
-
Accept: "application/vnd.github.v3+json",
|
|
308
|
-
"User-Agent": "tinfoil-node-client",
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
if (!releaseResponse.ok) {
|
|
312
|
-
throw new Error(`GitHub API request failed: ${releaseResponse.status} ${releaseResponse.statusText}`);
|
|
313
|
-
}
|
|
314
|
-
const releaseData = (await releaseResponse.json());
|
|
315
|
-
// Extract digest from release notes
|
|
316
|
-
const digestRegex = /Digest: `([a-f0-9]{64})`/;
|
|
317
|
-
const digestMatch = releaseData.body?.match(digestRegex);
|
|
318
|
-
if (!digestMatch) {
|
|
319
|
-
throw new Error("Could not find digest in release notes");
|
|
320
|
-
}
|
|
321
|
-
const digest = digestMatch[1];
|
|
322
|
-
return digest;
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Perform runtime attestation on the enclave
|
|
326
|
-
* @param enclaveHost - The enclave hostname
|
|
327
|
-
* @returns Attestation response with measurement and keys
|
|
328
|
-
*/
|
|
329
|
-
async verifyEnclave(enclaveHost) {
|
|
330
|
-
// Expose errors via explicit Promise rejection and add a timeout
|
|
331
|
-
return new Promise(async (resolve, reject) => {
|
|
332
|
-
try {
|
|
333
|
-
const targetHost = enclaveHost || this.serverURL;
|
|
334
|
-
if (typeof globalThis.verifyEnclave !== "function") {
|
|
335
|
-
reject(new Error("WASM verifyEnclave function not available"));
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
let attestationResponse;
|
|
339
|
-
let timeoutHandle;
|
|
340
|
-
try {
|
|
341
|
-
const timeoutPromise = new Promise((_, timeoutReject) => {
|
|
342
|
-
timeoutHandle = setTimeout(() => timeoutReject(new Error("WASM verifyEnclave timed out after 10 seconds")), 10000);
|
|
343
|
-
});
|
|
344
|
-
attestationResponse = await Promise.race([
|
|
345
|
-
globalThis.verifyEnclave(targetHost),
|
|
346
|
-
timeoutPromise,
|
|
347
|
-
]);
|
|
348
|
-
// Clear timeout on success
|
|
349
|
-
if (timeoutHandle !== undefined) {
|
|
350
|
-
clearTimeout(timeoutHandle);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
catch (error) {
|
|
354
|
-
// Clear timeout on error
|
|
355
|
-
if (timeoutHandle !== undefined) {
|
|
356
|
-
clearTimeout(timeoutHandle);
|
|
357
|
-
}
|
|
358
|
-
reject(new Error(`WASM verifyEnclave failed: ${error}`));
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
// Validate required fields - fail fast with explicit rejection
|
|
362
|
-
// At least one key must be present (TLS or HPKE)
|
|
363
|
-
if (!attestationResponse?.tls_public_key &&
|
|
364
|
-
!attestationResponse?.hpke_public_key) {
|
|
365
|
-
reject(new Error("Missing both tls_public_key and hpke_public_key in attestation response"));
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
// Parse runtime measurement
|
|
369
|
-
let parsedRuntimeMeasurement;
|
|
370
|
-
try {
|
|
371
|
-
if (attestationResponse.measurement &&
|
|
372
|
-
typeof attestationResponse.measurement === "string") {
|
|
373
|
-
parsedRuntimeMeasurement = JSON.parse(attestationResponse.measurement);
|
|
374
|
-
}
|
|
375
|
-
else if (attestationResponse.measurement &&
|
|
376
|
-
typeof attestationResponse.measurement === "object") {
|
|
377
|
-
parsedRuntimeMeasurement = attestationResponse.measurement;
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
reject(new Error("Invalid runtime measurement format"));
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
catch (parseError) {
|
|
385
|
-
reject(new Error(`Failed to parse runtime measurement: ${parseError}`));
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
const result = {
|
|
389
|
-
measurement: parsedRuntimeMeasurement,
|
|
390
|
-
};
|
|
391
|
-
// Include keys if available
|
|
392
|
-
if (attestationResponse.tls_public_key) {
|
|
393
|
-
result.tlsPublicKeyFingerprint = attestationResponse.tls_public_key;
|
|
394
|
-
}
|
|
395
|
-
if (attestationResponse.hpke_public_key) {
|
|
396
|
-
result.hpkePublicKey = attestationResponse.hpke_public_key;
|
|
397
|
-
}
|
|
398
|
-
resolve(result);
|
|
399
|
-
}
|
|
400
|
-
catch (outerError) {
|
|
401
|
-
reject(outerError);
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Perform code attestation
|
|
407
|
-
* @param configRepo - Repository name
|
|
408
|
-
* @param digest - Code digest hash
|
|
409
|
-
* @returns Code measurement
|
|
410
|
-
*/
|
|
411
|
-
async verifyCode(configRepo, digest) {
|
|
412
|
-
if (typeof globalThis.verifyCode !== "function") {
|
|
413
|
-
throw new Error("WASM verifyCode function not available");
|
|
414
|
-
}
|
|
415
|
-
const rawMeasurement = await globalThis.verifyCode(configRepo, digest);
|
|
416
|
-
const normalizedMeasurement = typeof rawMeasurement === "string"
|
|
417
|
-
? (() => {
|
|
418
|
-
try {
|
|
419
|
-
return JSON.parse(rawMeasurement);
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
throw new Error(`Invalid code measurement format: ${error.message}`);
|
|
423
|
-
}
|
|
424
|
-
})()
|
|
425
|
-
: rawMeasurement;
|
|
426
|
-
if (!normalizedMeasurement || typeof normalizedMeasurement !== "object") {
|
|
427
|
-
throw new Error("Invalid code measurement format");
|
|
428
|
-
}
|
|
429
|
-
const measurementObject = normalizedMeasurement;
|
|
430
|
-
if (typeof measurementObject.type !== "string" ||
|
|
431
|
-
!Array.isArray(measurementObject.registers)) {
|
|
432
|
-
throw new Error("Invalid code measurement format");
|
|
433
|
-
}
|
|
434
|
-
const parsedCodeMeasurement = {
|
|
435
|
-
type: measurementObject.type,
|
|
436
|
-
registers: measurementObject.registers.map((value) => String(value)),
|
|
437
|
-
};
|
|
438
|
-
return { measurement: parsedCodeMeasurement };
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Perform attestation verification
|
|
209
|
+
* Perform end-to-end attestation verification
|
|
442
210
|
*
|
|
443
|
-
* This method:
|
|
211
|
+
* This method performs all verification steps atomically via the Go WASM verify() function:
|
|
444
212
|
* 1. Fetches the latest code digest from GitHub releases
|
|
445
|
-
* 2.
|
|
446
|
-
* 3.
|
|
447
|
-
* 4.
|
|
448
|
-
* 5.
|
|
213
|
+
* 2. Verifies code provenance using Sigstore/Rekor
|
|
214
|
+
* 3. Performs runtime attestation against the enclave
|
|
215
|
+
* 4. Verifies hardware measurements (for TDX platforms)
|
|
216
|
+
* 5. Compares code and runtime measurements using platform-specific logic
|
|
449
217
|
*
|
|
450
218
|
* The WASM runtime is automatically initialized and cleaned up within this method.
|
|
219
|
+
* A detailed verification document is saved and can be accessed via getVerificationDocument().
|
|
451
220
|
*
|
|
452
|
-
* @
|
|
221
|
+
* @returns AttestationResponse containing cryptographic keys (TLS/HPKE) and enclave measurement
|
|
222
|
+
* @throws Error if measurements don't match or verification fails at any step
|
|
453
223
|
*/
|
|
454
224
|
async verify() {
|
|
455
225
|
return Verifier.executeWithWasm(async () => {
|
|
456
226
|
return this.verifyInternal();
|
|
457
227
|
});
|
|
458
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Save a failed verification document
|
|
231
|
+
*/
|
|
232
|
+
saveFailedVerificationDocument(steps) {
|
|
233
|
+
this.lastVerificationDocument = {
|
|
234
|
+
configRepo: this.configRepo,
|
|
235
|
+
enclaveHost: this.serverURL,
|
|
236
|
+
releaseDigest: "",
|
|
237
|
+
codeMeasurement: { type: "", registers: [] },
|
|
238
|
+
enclaveMeasurement: { measurement: { type: "", registers: [] } },
|
|
239
|
+
tlsPublicKey: "",
|
|
240
|
+
hpkePublicKey: "",
|
|
241
|
+
hardwareMeasurement: undefined,
|
|
242
|
+
codeFingerprint: "",
|
|
243
|
+
enclaveFingerprint: "",
|
|
244
|
+
selectedRouterEndpoint: this.serverURL,
|
|
245
|
+
securityVerified: false,
|
|
246
|
+
steps,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
459
249
|
/**
|
|
460
250
|
* Internal verification logic that runs within WASM context
|
|
461
251
|
*/
|
|
@@ -466,96 +256,90 @@ class Verifier {
|
|
|
466
256
|
verifyEnclave: { status: "pending" },
|
|
467
257
|
compareMeasurements: { status: "pending" },
|
|
468
258
|
};
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
releaseDigest = await this.fetchLatestDigest(this.configRepo);
|
|
474
|
-
steps.fetchDigest = { status: "success" };
|
|
475
|
-
}
|
|
476
|
-
catch (error) {
|
|
477
|
-
steps.fetchDigest = { status: "failed", error: error.message };
|
|
478
|
-
this.lastVerificationDocument = {
|
|
479
|
-
configRepo: this.configRepo,
|
|
480
|
-
enclaveHost: this.serverURL,
|
|
481
|
-
releaseDigest: "",
|
|
482
|
-
codeMeasurement: { type: "", registers: [] },
|
|
483
|
-
enclaveMeasurement: { measurement: { type: "", registers: [] } },
|
|
484
|
-
securityVerified: false,
|
|
485
|
-
steps,
|
|
259
|
+
if (typeof globalThis.verify !== "function") {
|
|
260
|
+
steps.fetchDigest = {
|
|
261
|
+
status: "failed",
|
|
262
|
+
error: "WASM verify function not available",
|
|
486
263
|
};
|
|
487
|
-
|
|
264
|
+
this.saveFailedVerificationDocument(steps);
|
|
265
|
+
throw new Error("WASM verify function not available");
|
|
488
266
|
}
|
|
267
|
+
let groundTruth;
|
|
489
268
|
try {
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
error: error.message,
|
|
498
|
-
};
|
|
499
|
-
throw error;
|
|
500
|
-
}),
|
|
501
|
-
this.verifyEnclave(this.serverURL).then((result) => {
|
|
502
|
-
steps.verifyEnclave = { status: "success" };
|
|
503
|
-
return result;
|
|
504
|
-
}, (error) => {
|
|
505
|
-
steps.verifyEnclave = {
|
|
506
|
-
status: "failed",
|
|
507
|
-
error: error.message,
|
|
508
|
-
};
|
|
509
|
-
throw error;
|
|
510
|
-
}),
|
|
511
|
-
]);
|
|
512
|
-
codeMeasurement = results[0].measurement;
|
|
513
|
-
attestation = results[1];
|
|
269
|
+
const groundTruthJSON = await globalThis.verify(this.serverURL, this.configRepo);
|
|
270
|
+
groundTruth = JSON.parse(groundTruthJSON);
|
|
271
|
+
// Mark all steps as successful since WASM verify() succeeded
|
|
272
|
+
steps.fetchDigest = { status: "success" };
|
|
273
|
+
steps.verifyCode = { status: "success" };
|
|
274
|
+
steps.verifyEnclave = { status: "success" };
|
|
275
|
+
steps.compareMeasurements = { status: "success" };
|
|
514
276
|
}
|
|
515
277
|
catch (error) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
278
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
279
|
+
if (errorMessage.startsWith("fetchDigest:")) {
|
|
280
|
+
steps.fetchDigest = { status: "failed", error: errorMessage };
|
|
281
|
+
}
|
|
282
|
+
else if (errorMessage.startsWith("verifyCode:")) {
|
|
283
|
+
steps.fetchDigest = { status: "success" };
|
|
284
|
+
steps.verifyCode = { status: "failed", error: errorMessage };
|
|
285
|
+
}
|
|
286
|
+
else if (errorMessage.startsWith("verifyEnclave:")) {
|
|
287
|
+
steps.fetchDigest = { status: "success" };
|
|
288
|
+
steps.verifyCode = { status: "success" };
|
|
289
|
+
steps.verifyEnclave = { status: "failed", error: errorMessage };
|
|
290
|
+
}
|
|
291
|
+
else if (errorMessage.startsWith("measurements:")) {
|
|
292
|
+
steps.fetchDigest = { status: "success" };
|
|
293
|
+
steps.verifyCode = { status: "success" };
|
|
294
|
+
steps.verifyEnclave = { status: "success" };
|
|
295
|
+
steps.compareMeasurements = { status: "failed", error: errorMessage };
|
|
296
|
+
}
|
|
297
|
+
else if (errorMessage.startsWith("verifyHardware:") ||
|
|
298
|
+
errorMessage.startsWith("validateTLS:")) {
|
|
299
|
+
steps.fetchDigest = { status: "success" };
|
|
300
|
+
steps.verifyCode = { status: "success" };
|
|
301
|
+
steps.verifyEnclave = { status: "success" };
|
|
302
|
+
steps.otherError = { status: "failed", error: errorMessage };
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
steps.otherError = { status: "failed", error: errorMessage };
|
|
306
|
+
}
|
|
307
|
+
this.saveFailedVerificationDocument(steps);
|
|
525
308
|
throw error;
|
|
526
309
|
}
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
};
|
|
533
|
-
this.lastVerificationDocument = {
|
|
534
|
-
configRepo: this.configRepo,
|
|
535
|
-
enclaveHost: this.serverURL,
|
|
536
|
-
releaseDigest: releaseDigest,
|
|
537
|
-
codeMeasurement,
|
|
538
|
-
enclaveMeasurement: attestation,
|
|
539
|
-
securityVerified: false,
|
|
540
|
-
steps,
|
|
541
|
-
};
|
|
542
|
-
throw new Error(`Verification failed: measurements did not match.\nCode measurement (${codeMeasurement.type}: ${codeMeasurement.registers})\n` +
|
|
543
|
-
`Runtime measurement (${attestation.measurement.type}: ${attestation.measurement.registers}:)\n ${measurementsMatchError.message}`);
|
|
544
|
-
}
|
|
545
|
-
steps.compareMeasurements = { status: "success" };
|
|
310
|
+
const attestation = {
|
|
311
|
+
tlsPublicKeyFingerprint: groundTruth.tls_public_key,
|
|
312
|
+
hpkePublicKey: groundTruth.hpke_public_key,
|
|
313
|
+
measurement: groundTruth.enclave_measurement,
|
|
314
|
+
};
|
|
546
315
|
this.lastVerificationDocument = {
|
|
547
316
|
configRepo: this.configRepo,
|
|
548
317
|
enclaveHost: this.serverURL,
|
|
549
|
-
releaseDigest:
|
|
550
|
-
codeMeasurement,
|
|
318
|
+
releaseDigest: groundTruth.digest,
|
|
319
|
+
codeMeasurement: groundTruth.code_measurement,
|
|
551
320
|
enclaveMeasurement: attestation,
|
|
321
|
+
tlsPublicKey: groundTruth.tls_public_key,
|
|
322
|
+
hpkePublicKey: groundTruth.hpke_public_key,
|
|
323
|
+
hardwareMeasurement: groundTruth.hardware_measurement,
|
|
324
|
+
codeFingerprint: groundTruth.code_fingerprint,
|
|
325
|
+
enclaveFingerprint: groundTruth.enclave_fingerprint,
|
|
326
|
+
selectedRouterEndpoint: this.serverURL,
|
|
552
327
|
securityVerified: true,
|
|
553
328
|
steps,
|
|
554
329
|
};
|
|
555
330
|
return attestation;
|
|
556
331
|
}
|
|
557
332
|
/**
|
|
558
|
-
* Returns the
|
|
333
|
+
* Returns the verification document from the last verify() call
|
|
334
|
+
*
|
|
335
|
+
* The document contains detailed step-by-step verification results including:
|
|
336
|
+
* - Step status (pending/success/failed) for each verification phase
|
|
337
|
+
* - Measurements, fingerprints, and cryptographic keys
|
|
338
|
+
* - Error messages for any failed steps
|
|
339
|
+
*
|
|
340
|
+
* Available even if verification failed, allowing inspection of which step failed.
|
|
341
|
+
*
|
|
342
|
+
* @returns VerificationDocument with complete verification details, or undefined if verify() hasn't been called
|
|
559
343
|
*/
|
|
560
344
|
getVerificationDocument() {
|
|
561
345
|
return this.lastVerificationDocument;
|
|
@@ -564,7 +348,7 @@ class Verifier {
|
|
|
564
348
|
exports.Verifier = Verifier;
|
|
565
349
|
Verifier.goInstance = null;
|
|
566
350
|
Verifier.initializationPromise = null;
|
|
567
|
-
Verifier.defaultWasmUrl = "https://tinfoilsh.github.io/verifier
|
|
351
|
+
Verifier.defaultWasmUrl = "https://tinfoilsh.github.io/verifier/tinfoil-verifier.wasm";
|
|
568
352
|
Verifier.originalFsWriteSync = null;
|
|
569
353
|
Verifier.wasmLogsSuppressed = true;
|
|
570
354
|
Verifier.globalsInitialized = false;
|
|
@@ -593,10 +377,12 @@ function shouldAutoInitializeWasm() {
|
|
|
593
377
|
/**
|
|
594
378
|
* Control WASM log output
|
|
595
379
|
*
|
|
596
|
-
* The Go WASM runtime outputs logs through a polyfilled fs.writeSync.
|
|
380
|
+
* The Go WASM runtime outputs logs (stdout/stderr) through a polyfilled fs.writeSync.
|
|
597
381
|
* This function allows suppressing those logs without affecting other console output.
|
|
382
|
+
* By default, WASM logs are suppressed to reduce noise.
|
|
598
383
|
*
|
|
599
384
|
* @param suppress - Whether to suppress WASM logs (default: true)
|
|
385
|
+
* @returns void
|
|
600
386
|
*/
|
|
601
387
|
function suppressWasmLogs(suppress = true) {
|
|
602
388
|
globalThis.__tinfoilSuppressWasmLogs = suppress;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeke-02/tinfoil",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Tinfoil secure OpenAI client wrapper for Tauri v2",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.mjs",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"dependencies": {
|
|
87
87
|
"@ai-sdk/openai-compatible": "^1.0.10",
|
|
88
88
|
"@tauri-apps/plugin-http": "^2.5.2",
|
|
89
|
-
"@zeke-02/ehbp": "^0.0.
|
|
89
|
+
"@zeke-02/ehbp": "^0.0.2",
|
|
90
90
|
"ai": "^5.0.19",
|
|
91
91
|
"dotenv": "^17.2.1",
|
|
92
92
|
"openai": "^5.13.1",
|