@alejoamiras/tee-rex 4.0.0-devnet.2-patch.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.
@@ -0,0 +1,140 @@
1
+ import { BBLazyPrivateKernelProver } from "@aztec/bb-prover/client/lazy";
2
+ import { ChonkProofWithPublicInputs } from "@aztec/stdlib/proofs";
3
+ import { schemas } from "@aztec/stdlib/schemas";
4
+ import ky from "ky";
5
+ import ms from "ms";
6
+ import { Base64, Bytes } from "ox";
7
+ import { UnreachableCaseError } from "ts-essentials";
8
+ import { joinURL } from "ufo";
9
+ import { z } from "zod";
10
+ import { verifyNitroAttestation } from "./attestation.js";
11
+ import { encrypt } from "./encrypt.js";
12
+ import { logger } from "./logger.js";
13
+ export const ProvingMode = {
14
+ local: "local",
15
+ remote: "remote",
16
+ };
17
+ /**
18
+ * Aztec private kernel prover that can generate proofs locally or on a remote
19
+ * tee-rex server running inside an AWS Nitro Enclave.
20
+ *
21
+ * In remote mode, witness data is encrypted with the server's attested public
22
+ * key (curve25519 + AES-256-GCM) before being sent over the network.
23
+ */
24
+ export class TeeRexProver extends BBLazyPrivateKernelProver {
25
+ apiUrl;
26
+ #provingMode = ProvingMode.remote;
27
+ #attestationConfig = {};
28
+ constructor(apiUrl, ...args) {
29
+ super(...args);
30
+ this.apiUrl = apiUrl;
31
+ }
32
+ /** Switch between local WASM proving and remote TEE proving. */
33
+ setProvingMode(mode) {
34
+ this.#provingMode = mode;
35
+ }
36
+ /** Update the tee-rex server URL used for remote proving. */
37
+ setApiUrl(url) {
38
+ this.apiUrl = url;
39
+ }
40
+ /** Configure attestation verification (PCR checks, freshness, require TEE). */
41
+ setAttestationConfig(config) {
42
+ this.#attestationConfig = config;
43
+ }
44
+ async createChonkProof(executionSteps) {
45
+ switch (this.#provingMode) {
46
+ case "local": {
47
+ logger.info("Using local prover", {
48
+ steps: executionSteps.length,
49
+ functions: executionSteps.map((s) => s.functionName),
50
+ });
51
+ const start = performance.now();
52
+ const result = await super.createChonkProof(executionSteps);
53
+ logger.info("Local proof completed", {
54
+ durationMs: Math.round(performance.now() - start),
55
+ });
56
+ return result;
57
+ }
58
+ case "remote": {
59
+ logger.info("Using remote prover");
60
+ return this.#remoteCreateChonkProof(executionSteps);
61
+ }
62
+ default: {
63
+ throw new UnreachableCaseError(this.#provingMode);
64
+ }
65
+ }
66
+ }
67
+ async #remoteCreateChonkProof(executionSteps) {
68
+ logger.info("Creating chonk proof", { apiUrl: this.apiUrl });
69
+ const executionStepsSerialized = executionSteps.map((step) => ({
70
+ functionName: step.functionName,
71
+ witness: Array.from(step.witness.entries()),
72
+ bytecode: Base64.fromBytes(step.bytecode),
73
+ vk: Base64.fromBytes(step.vk),
74
+ timings: step.timings,
75
+ }));
76
+ logger.debug("Serialized payload", { chars: JSON.stringify(executionStepsSerialized).length });
77
+ const encryptionPublicKey = await this.#fetchEncryptionPublicKey();
78
+ const encryptedData = Base64.fromBytes(await encrypt({
79
+ data: Bytes.fromString(JSON.stringify({ executionSteps: executionStepsSerialized })),
80
+ encryptionPublicKey,
81
+ })); // TODO(perf): serialize executionSteps -> bytes without intermediate encoding. Needs Aztec to support serialization of the PrivateExecutionStep class.
82
+ const response = await ky
83
+ .post(joinURL(this.apiUrl, "prove"), {
84
+ json: { data: encryptedData },
85
+ timeout: ms("5 min"),
86
+ retry: 2,
87
+ })
88
+ .json();
89
+ const data = z
90
+ .object({
91
+ proof: schemas.Buffer,
92
+ })
93
+ .parse(response);
94
+ return ChonkProofWithPublicInputs.fromBuffer(data.proof);
95
+ }
96
+ async #fetchEncryptionPublicKey() {
97
+ const response = await ky.get(joinURL(this.apiUrl, "attestation"), { retry: 2 }).json();
98
+ const data = z
99
+ .discriminatedUnion("mode", [
100
+ z.object({ mode: z.literal("standard"), publicKey: z.string() }),
101
+ z.object({
102
+ mode: z.literal("nitro"),
103
+ attestationDocument: z.string(),
104
+ publicKey: z.string(),
105
+ }),
106
+ ])
107
+ .parse(response);
108
+ switch (data.mode) {
109
+ case "standard": {
110
+ if (this.#attestationConfig.requireAttestation) {
111
+ throw new Error("Server is running in standard mode but requireAttestation is enabled. " +
112
+ "The server must run inside a TEE to provide attestation.");
113
+ }
114
+ logger.warn("Server is running in standard mode (no TEE attestation)");
115
+ return data.publicKey;
116
+ }
117
+ case "nitro": {
118
+ try {
119
+ const { publicKey } = await verifyNitroAttestation(data.attestationDocument, {
120
+ expectedPCRs: this.#attestationConfig.expectedPCRs,
121
+ maxAgeMs: this.#attestationConfig.maxAgeMs,
122
+ });
123
+ return publicKey;
124
+ }
125
+ catch (err) {
126
+ // In browser environments, node:crypto is unavailable. Fall back to the
127
+ // server-provided public key. The attestation document was still fetched
128
+ // over HTTPS; we just can't verify the COSE_Sign1 chain client-side.
129
+ if (err instanceof Error &&
130
+ (err.message.includes("node:crypto") || err.message.includes("is not a constructor"))) {
131
+ logger.warn("Nitro attestation verification unavailable (browser environment). Using server-provided public key.", { error: err.message });
132
+ return data.publicKey;
133
+ }
134
+ throw err;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ //# sourceMappingURL=tee-rex-prover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tee-rex-prover.js","sourceRoot":"","sources":["../../src/lib/tee-rex-prover.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test-setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-setup.d.ts","sourceRoot":"","sources":["../src/test-setup.ts"],"names":[],"mappings":""}
@@ -0,0 +1,10 @@
1
+ import { expect } from "bun:test";
2
+ // Patch expect for @aztec/foundation compatibility
3
+ // @aztec/foundation checks if expect.addEqualityTesters exists (vitest API)
4
+ if (!expect.addEqualityTesters) {
5
+ expect.addEqualityTesters = () => { };
6
+ }
7
+ if (globalThis.expect && !globalThis.expect.addEqualityTesters) {
8
+ globalThis.expect.addEqualityTesters = () => { };
9
+ }
10
+ //# sourceMappingURL=test-setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-setup.js","sourceRoot":"","sources":["../src/test-setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,mDAAmD;AACnD,4EAA4E;AAC5E,IAAI,CAAE,MAAc,CAAC,kBAAkB,EAAE,CAAC;IACvC,MAAc,CAAC,kBAAkB,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAChD,CAAC;AACD,IAAK,UAAkB,CAAC,MAAM,IAAI,CAAE,UAAkB,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC;IAChF,UAAkB,CAAC,MAAM,CAAC,kBAAkB,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAC3D,CAAC"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@alejoamiras/tee-rex",
3
+ "version": "4.0.0-devnet.2-patch.0",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/alejoamiras/tee-rex",
8
+ "directory": "packages/sdk"
9
+ },
10
+ "exports": "./src/index.ts",
11
+ "publishConfig": {
12
+ "exports": {
13
+ ".": {
14
+ "default": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ }
17
+ },
18
+ "access": "public"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "dev": "tsc -w",
26
+ "build": "rm -rf dist && tsc",
27
+ "test:unit": "bun test src/",
28
+ "test:e2e": "bun test --preload ./e2e/e2e-setup.ts e2e/",
29
+ "test:e2e:nextnet": "AZTEC_NODE_URL=https://nextnet.aztec-labs.com bun test --preload ./e2e/e2e-setup.ts e2e/nextnet.test.ts",
30
+ "test:lint": "tsc --noEmit --emitDeclarationOnly false",
31
+ "prepublishOnly": "bun run test:lint && bun run build"
32
+ },
33
+ "dependencies": {
34
+ "@aztec/bb-prover": "4.0.0-devnet.2-patch.0",
35
+ "@aztec/foundation": "4.0.0-devnet.2-patch.0",
36
+ "@aztec/noir-acvm_js": "4.0.0-devnet.2-patch.0",
37
+ "@aztec/noir-noirc_abi": "4.0.0-devnet.2-patch.0",
38
+ "@aztec/stdlib": "4.0.0-devnet.2-patch.0",
39
+ "@logtape/logtape": "^2.0",
40
+ "@peculiar/x509": "^1.12.0",
41
+ "cbor-x": "^1.6.0",
42
+ "ky": "^1.14.3",
43
+ "ms": "^2.1.3",
44
+ "openpgp": "6.3.0",
45
+ "ox": "^0.12.1",
46
+ "ts-essentials": "^10.1.1",
47
+ "ufo": "^1.6.3",
48
+ "zod": "^3.23.8"
49
+ },
50
+ "devDependencies": {
51
+ "@aztec/accounts": "4.0.0-devnet.2-patch.0",
52
+ "@aztec/aztec.js": "4.0.0-devnet.2-patch.0",
53
+ "@aztec/noir-contracts.js": "4.0.0-devnet.2-patch.0",
54
+ "@aztec/pxe": "4.0.0-devnet.2-patch.0",
55
+ "@aztec/simulator": "4.0.0-devnet.2-patch.0",
56
+ "@aztec/wallets": "4.0.0-devnet.2-patch.0",
57
+ "@types/ms": "^2.1.0"
58
+ }
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type { AttestationVerifyOptions, NitroAttestationDocument } from "./lib/attestation.js";
2
+ export {
3
+ AttestationError,
4
+ AttestationErrorCode,
5
+ verifyNitroAttestation,
6
+ } from "./lib/attestation.js";
7
+ export * from "./lib/tee-rex-prover.js";
@@ -0,0 +1,325 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { execSync } from "node:child_process";
3
+ import crypto from "node:crypto";
4
+ import { AttestationError, AttestationErrorCode, verifyNitroAttestation } from "./attestation.js";
5
+
6
+ /**
7
+ * Generate a P-384 EC key pair and self-signed or CA-signed X.509 certificate
8
+ * using the openssl CLI. Returns PEM strings and DER bytes.
9
+ */
10
+ function generateCert(opts: {
11
+ subject: string;
12
+ issuerKey?: string;
13
+ issuerCert?: string;
14
+ ca?: boolean;
15
+ days?: number;
16
+ }): { keyPem: string; certPem: string; certDer: Uint8Array } {
17
+ const { subject, issuerKey, issuerCert, ca = false, days = 365 } = opts;
18
+
19
+ // Generate EC P-384 private key
20
+ const keyPem = execSync("openssl ecparam -genkey -name secp384r1 -noout 2>/dev/null", {
21
+ encoding: "utf-8",
22
+ });
23
+
24
+ if (issuerKey && issuerCert) {
25
+ // Create a CSR then sign with issuer
26
+ const csrPem = execSync(
27
+ `echo "${keyPem}" | openssl req -new -key /dev/stdin -subj "${subject}" 2>/dev/null`,
28
+ { encoding: "utf-8" },
29
+ );
30
+
31
+ // Write temp files for openssl x509 -req
32
+ const tmpKey = `/tmp/test-issuer-key-${Date.now()}.pem`;
33
+ const tmpCert = `/tmp/test-issuer-cert-${Date.now()}.pem`;
34
+ const tmpCsr = `/tmp/test-csr-${Date.now()}.pem`;
35
+ execSync(`cat > ${tmpKey} << 'ENDKEY'\n${issuerKey}\nENDKEY`);
36
+ execSync(`cat > ${tmpCert} << 'ENDCERT'\n${issuerCert}\nENDCERT`);
37
+ execSync(`cat > ${tmpCsr} << 'ENDCSR'\n${csrPem}\nENDCSR`);
38
+
39
+ let cmd = `openssl x509 -req -in ${tmpCsr} -CA ${tmpCert} -CAkey ${tmpKey} -CAcreateserial -days ${days} -sha384`;
40
+ if (ca) {
41
+ cmd += ` -extfile <(echo "basicConstraints=critical,CA:TRUE")`;
42
+ }
43
+ const certPem = execSync(`bash -c '${cmd} 2>/dev/null'`, { encoding: "utf-8" });
44
+
45
+ // Cleanup
46
+ execSync(`rm -f ${tmpKey} ${tmpCert} ${tmpCsr} ${tmpCert.replace(".pem", ".srl")}`);
47
+
48
+ const certDer = execSync(`echo "${certPem}" | openssl x509 -outform DER 2>/dev/null`);
49
+
50
+ return { keyPem, certPem, certDer: new Uint8Array(certDer) };
51
+ }
52
+
53
+ // Self-signed
54
+ let cmd = `echo "${keyPem}" | openssl req -new -x509 -key /dev/stdin -subj "${subject}" -days ${days} -sha384`;
55
+ if (ca) {
56
+ cmd += ` -addext "basicConstraints=critical,CA:TRUE"`;
57
+ }
58
+ const certPem = execSync(`bash -c '${cmd} 2>/dev/null'`, { encoding: "utf-8" });
59
+ const certDer = execSync(`echo "${certPem}" | openssl x509 -outform DER 2>/dev/null`);
60
+
61
+ return { keyPem, certPem, certDer: new Uint8Array(certDer) };
62
+ }
63
+
64
+ /**
65
+ * Build a synthetic Nitro-style attestation document for testing.
66
+ *
67
+ * Creates a 3-cert chain (root → intermediate → leaf), builds a CBOR attestation
68
+ * payload, and wraps it in a COSE_Sign1 envelope signed by the leaf key.
69
+ */
70
+ async function buildTestAttestation(
71
+ overrides: {
72
+ publicKey?: string;
73
+ nonce?: string;
74
+ pcrs?: Record<number, string>;
75
+ timestamp?: number | bigint;
76
+ } = {},
77
+ ) {
78
+ const { encode: encodeCbor } = await import("cbor-x");
79
+
80
+ // Generate certificate chain: root → intermediate → leaf (all P-384)
81
+ const root = generateCert({ subject: "/CN=Test Root CA", ca: true });
82
+ const intermediate = generateCert({
83
+ subject: "/CN=Test Intermediate",
84
+ issuerKey: root.keyPem,
85
+ issuerCert: root.certPem,
86
+ ca: true,
87
+ });
88
+ const leaf = generateCert({
89
+ subject: "/CN=Test Leaf",
90
+ issuerKey: intermediate.keyPem,
91
+ issuerCert: intermediate.certPem,
92
+ });
93
+
94
+ // Build PCR map
95
+ const pcrs = new Map<number, Uint8Array>();
96
+ if (overrides.pcrs) {
97
+ for (const [index, hex] of Object.entries(overrides.pcrs)) {
98
+ pcrs.set(Number(index), Buffer.from(hex, "hex"));
99
+ }
100
+ } else {
101
+ pcrs.set(0, new Uint8Array(48));
102
+ }
103
+
104
+ const embeddedPublicKey = overrides.publicKey ?? "test-public-key-pem";
105
+ const timestamp = overrides.timestamp ?? Date.now();
106
+
107
+ // Build the attestation document payload
108
+ const attestationPayload: Record<string, unknown> = {
109
+ module_id: "test-module-id",
110
+ timestamp,
111
+ digest: "SHA384",
112
+ pcrs,
113
+ certificate: leaf.certDer,
114
+ cabundle: [root.certDer, intermediate.certDer],
115
+ public_key: new TextEncoder().encode(embeddedPublicKey),
116
+ };
117
+
118
+ if (overrides.nonce) {
119
+ attestationPayload.nonce = Buffer.from(overrides.nonce, "hex");
120
+ }
121
+
122
+ // Encode payload as CBOR
123
+ const payloadBytes = encodeCbor(attestationPayload);
124
+
125
+ // Build COSE_Sign1 protected headers (empty map)
126
+ const protectedHeaders = encodeCbor(new Map());
127
+
128
+ // Build Sig_structure = ["Signature1", protected_headers, external_aad, payload]
129
+ const sigStructure = encodeCbor(["Signature1", protectedHeaders, Buffer.alloc(0), payloadBytes]);
130
+
131
+ // Sign with leaf private key (ECDSA P-384 SHA384, ieee-p1363 format)
132
+ const leafPrivateKey = crypto.createPrivateKey(leaf.keyPem);
133
+ const signer = crypto.createSign("SHA384");
134
+ signer.update(sigStructure);
135
+ const signature = signer.sign({ key: leafPrivateKey, dsaEncoding: "ieee-p1363" });
136
+
137
+ // Wrap in COSE_Sign1: [protected, unprotected, payload, signature]
138
+ const coseSign1 = encodeCbor([protectedHeaders, {}, payloadBytes, signature]);
139
+
140
+ const base64 = Buffer.from(coseSign1).toString("base64");
141
+
142
+ return { base64, rootCaPem: root.certPem, embeddedPublicKey, timestamp };
143
+ }
144
+
145
+ describe("verifyNitroAttestation", () => {
146
+ describe("rejection (invalid input)", () => {
147
+ test("rejects invalid base64 input", async () => {
148
+ await expect(verifyNitroAttestation("not-valid-base64!!!")).rejects.toThrow();
149
+ });
150
+
151
+ test("rejects non-CBOR data", async () => {
152
+ const data = Buffer.from("just plain text").toString("base64");
153
+ await expect(verifyNitroAttestation(data)).rejects.toThrow();
154
+ });
155
+
156
+ test("rejects empty CBOR array", async () => {
157
+ const { encode } = await import("cbor-x");
158
+ const data = Buffer.from(encode([])).toString("base64");
159
+ await expect(verifyNitroAttestation(data)).rejects.toThrow(AttestationError);
160
+ });
161
+
162
+ test("rejects CBOR array with wrong length and sets INVALID_COSE code", async () => {
163
+ const { encode } = await import("cbor-x");
164
+ const data = Buffer.from(encode([new Uint8Array(), {}, new Uint8Array()])).toString("base64");
165
+ try {
166
+ await verifyNitroAttestation(data);
167
+ expect.unreachable("should have thrown");
168
+ } catch (err) {
169
+ expect(err).toBeInstanceOf(AttestationError);
170
+ expect((err as AttestationError).code).toBe(AttestationErrorCode.INVALID_COSE);
171
+ }
172
+ });
173
+ });
174
+
175
+ describe("happy path", () => {
176
+ test("verifies a valid attestation document and returns the public key", async () => {
177
+ const { base64, rootCaPem, embeddedPublicKey } = await buildTestAttestation();
178
+ const result = await verifyNitroAttestation(base64, { rootCaPem });
179
+
180
+ expect(result.publicKey).toBe(embeddedPublicKey);
181
+ expect(result.document.moduleId).toBe("test-module-id");
182
+ expect(result.document.digest).toBe("SHA384");
183
+ expect(result.document.pcrs.get(0)).toBeInstanceOf(Uint8Array);
184
+ });
185
+
186
+ test("verifies attestation with BigInt timestamp (real Nitro encoding)", async () => {
187
+ // Real Nitro enclaves encode timestamp as CBOR uint64, which cbor-x decodes as BigInt
188
+ const { base64, rootCaPem, embeddedPublicKey } = await buildTestAttestation({
189
+ timestamp: BigInt(Date.now()),
190
+ });
191
+ const result = await verifyNitroAttestation(base64, { rootCaPem });
192
+
193
+ expect(result.publicKey).toBe(embeddedPublicKey);
194
+ expect(typeof result.document.timestamp).toBe("number");
195
+ expect(result.document.timestamp).toBeCloseTo(Date.now(), -3);
196
+ });
197
+
198
+ test("verifies with matching PCR values", async () => {
199
+ const pcr0Hex = "aa".repeat(48);
200
+ const { base64, rootCaPem } = await buildTestAttestation({
201
+ pcrs: { 0: pcr0Hex },
202
+ });
203
+ const result = await verifyNitroAttestation(base64, {
204
+ rootCaPem,
205
+ expectedPCRs: { 0: pcr0Hex },
206
+ });
207
+ expect(result.document.pcrs.get(0)).toBeDefined();
208
+ });
209
+
210
+ test("verifies with matching nonce", async () => {
211
+ const nonce = "deadbeef01020304";
212
+ const { base64, rootCaPem, embeddedPublicKey } = await buildTestAttestation({ nonce });
213
+ const result = await verifyNitroAttestation(base64, {
214
+ rootCaPem,
215
+ expectedNonce: nonce,
216
+ });
217
+ expect(result.publicKey).toBe(embeddedPublicKey);
218
+ });
219
+ });
220
+
221
+ describe("verification failures", () => {
222
+ test("rejects expired attestation with EXPIRED code", async () => {
223
+ const { base64, rootCaPem } = await buildTestAttestation({
224
+ timestamp: Date.now() - 10 * 60 * 1000,
225
+ });
226
+ try {
227
+ await verifyNitroAttestation(base64, { rootCaPem, maxAgeMs: 60_000 });
228
+ expect.unreachable("should have thrown");
229
+ } catch (err) {
230
+ expect(err).toBeInstanceOf(AttestationError);
231
+ expect((err as AttestationError).code).toBe(AttestationErrorCode.EXPIRED);
232
+ }
233
+ });
234
+
235
+ test("rejects PCR mismatch with PCR_MISMATCH code", async () => {
236
+ const { base64, rootCaPem } = await buildTestAttestation({
237
+ pcrs: { 0: "aa".repeat(48) },
238
+ });
239
+ try {
240
+ await verifyNitroAttestation(base64, {
241
+ rootCaPem,
242
+ expectedPCRs: { 0: "bb".repeat(48) },
243
+ });
244
+ expect.unreachable("should have thrown");
245
+ } catch (err) {
246
+ expect(err).toBeInstanceOf(AttestationError);
247
+ expect((err as AttestationError).code).toBe(AttestationErrorCode.PCR_MISMATCH);
248
+ }
249
+ });
250
+
251
+ test("rejects nonce mismatch with NONCE_MISMATCH code", async () => {
252
+ const { base64, rootCaPem } = await buildTestAttestation({
253
+ nonce: "deadbeef01020304",
254
+ });
255
+ try {
256
+ await verifyNitroAttestation(base64, {
257
+ rootCaPem,
258
+ expectedNonce: "0000000000000000",
259
+ });
260
+ expect.unreachable("should have thrown");
261
+ } catch (err) {
262
+ expect(err).toBeInstanceOf(AttestationError);
263
+ expect((err as AttestationError).code).toBe(AttestationErrorCode.NONCE_MISMATCH);
264
+ }
265
+ });
266
+
267
+ test("rejects missing nonce with NONCE_MISMATCH code", async () => {
268
+ const { base64, rootCaPem } = await buildTestAttestation();
269
+ try {
270
+ await verifyNitroAttestation(base64, {
271
+ rootCaPem,
272
+ expectedNonce: "deadbeef",
273
+ });
274
+ expect.unreachable("should have thrown");
275
+ } catch (err) {
276
+ expect(err).toBeInstanceOf(AttestationError);
277
+ expect((err as AttestationError).code).toBe(AttestationErrorCode.NONCE_MISMATCH);
278
+ }
279
+ });
280
+
281
+ test("rejects wrong root CA", async () => {
282
+ const { base64 } = await buildTestAttestation();
283
+ // Use default root CA (AWS Nitro) — won't match test chain
284
+ await expect(verifyNitroAttestation(base64)).rejects.toThrow();
285
+ });
286
+
287
+ test("rejects mismatched root CA with CHAIN_FAILED code", async () => {
288
+ const { base64 } = await buildTestAttestation();
289
+ // Generate a different root CA — valid cert but didn't sign our chain
290
+ const otherRoot = generateCert({ subject: "/CN=Other Root CA", ca: true });
291
+ try {
292
+ await verifyNitroAttestation(base64, { rootCaPem: otherRoot.certPem });
293
+ expect.unreachable("should have thrown");
294
+ } catch (err) {
295
+ expect(err).toBeInstanceOf(AttestationError);
296
+ expect((err as AttestationError).code).toBe(AttestationErrorCode.CHAIN_FAILED);
297
+ }
298
+ });
299
+ });
300
+ });
301
+
302
+ /**
303
+ * Integration test against a real Nitro TEE endpoint.
304
+ * Skips when TEE_URL is not set (e.g., local dev without TEE access).
305
+ */
306
+ describe.skipIf(!process.env.TEE_URL)("Real Nitro attestation (integration)", () => {
307
+ test("verifies attestation from production TEE", async () => {
308
+ const res = await fetch(`${process.env.TEE_URL}/attestation`);
309
+ expect(res.ok).toBe(true);
310
+ const body = (await res.json()) as {
311
+ mode: string;
312
+ attestationDocument: string;
313
+ publicKey: string;
314
+ };
315
+ expect(body.mode).toBe("nitro");
316
+ expect(body.attestationDocument).toBeDefined();
317
+
318
+ const { publicKey, document } = await verifyNitroAttestation(body.attestationDocument);
319
+ expect(publicKey).toContain("-----BEGIN PGP PUBLIC KEY BLOCK-----");
320
+ expect(document.moduleId).toBeDefined();
321
+ expect(document.digest).toBe("SHA384");
322
+ expect(document.pcrs.get(0)).toBeInstanceOf(Uint8Array);
323
+ expect(document.timestamp).toBeGreaterThan(0);
324
+ });
325
+ });