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