/** * Unit tests for SGX DCAP attestation. * * Mirrors the Python suite in ../nomyo/test/test_attestation.py: a mock router * serves /pki/attestation with a quote whose report_data is the real * SHA-512(server_pubkey_der [|| challenge]) binding, and a "faithful" verifier * decodes that quote back into a VerifiedQuote. */ import { AttestationPolicy, SgxAttestationVerifier, CallableQuoteVerifier, JwtQuoteVerifier, } from '../../src/core/attestation'; import { VerifiedQuote } from '../../src/types/attestation'; import { AttestationError, SecurityError } from '../../src/errors'; import { SecureCompletionClient } from '../../src/core/SecureCompletionClient'; import { RSAOperations } from '../../src/core/crypto/rsa'; import { bytesToHex, hexToBytes, constantTimeEqual, sha512, stringToArrayBuffer, arrayBufferToBase64, arrayBufferToString, } from '../../src/core/crypto/utils'; // ---- fixtures -------------------------------------------------------------- const MRENCLAVE = 'a'.repeat(64); const MRSIGNER = 'b'.repeat(64); interface HttpResponseLike { statusCode: number; headers: Record; body: ArrayBuffer; } function jsonResponse(statusCode: number, body: object): HttpResponseLike { return { statusCode, headers: { 'content-type': 'application/json' }, body: stringToArrayBuffer(JSON.stringify(body)), }; } /** * Mock router: serves the server public key and an attestation quote binding * that key (plus any challenge) via SHA-512, exactly as the real enclave does. */ class MockServer { pubkeyPem!: string; pubkeyDer!: ArrayBuffer; /** Overrides for negative-path tests */ isAvailable = true; reason = 'sgx not supported'; bindToWrongKey = false; mrenclave = MRENCLAVE; mrsigner = MRSIGNER; isvProdId = 1; isvSvn = 3; tcbStatus = 'UpToDate'; /** Challenges observed on /pki/attestation */ seenChallenges: (string | null)[] = []; attestationCalls = 0; async init(): Promise { const rsa = new RSAOperations(); const pair = await rsa.generateKeyPair(2048); this.pubkeyPem = await rsa.exportPublicKey(pair.publicKey); this.pubkeyDer = await rsa.exportPublicKeyDer(pair.publicKey); } /** * The quote is opaque to the orchestrator; we encode the claims a real DCAP * verifier would recover from it, so `faithfulVerify` can play that role. */ private async quoteB64(challenge: Uint8Array): Promise { const keyBytes = this.bindToWrongKey ? new Uint8Array(this.pubkeyDer.byteLength).fill(0x41) : new Uint8Array(this.pubkeyDer); const bound = new Uint8Array(keyBytes.length + challenge.length); bound.set(keyBytes, 0); bound.set(challenge, keyBytes.length); const reportData = new Uint8Array(await sha512(bound)); const claims = { mrenclave: this.mrenclave, mrsigner: this.mrsigner, isv_prod_id: this.isvProdId, isv_svn: this.isvSvn, tcb_status: this.tcbStatus, report_data: bytesToHex(reportData), }; return arrayBufferToBase64(stringToArrayBuffer(JSON.stringify(claims))); } /** Drop-in replacement for the HttpClient interface. */ client() { const handler = async (url: string): Promise => { const parsed = new URL(url); if (parsed.pathname === '/pki/public_key') { return { statusCode: 200, headers: {}, body: stringToArrayBuffer(this.pubkeyPem), }; } if (parsed.pathname === '/pki/attestation') { this.attestationCalls += 1; const challengeHex = parsed.searchParams.get('challenge'); this.seenChallenges.push(challengeHex); if (!this.isAvailable) { return jsonResponse(200, { is_available: false, reason: this.reason }); } const challenge = challengeHex ? hexToBytes(challengeHex) : new Uint8Array(0); return jsonResponse(200, { is_available: true, quote_b64: await this.quoteB64(challenge), }); } return jsonResponse(404, { detail: 'not found' }); }; return { get: jest.fn((url: string) => handler(url)), post: jest.fn((url: string) => handler(url)), }; } } /** Plays the role of a real DCAP verifier: recovers claims from the quote. */ async function faithfulVerify(quote: Uint8Array): Promise { const claims = JSON.parse(arrayBufferToString(quote.buffer as ArrayBuffer)) as Record; return { mrenclave: String(claims.mrenclave), mrsigner: String(claims.mrsigner), isvProdId: Number(claims.isv_prod_id), isvSvn: Number(claims.isv_svn), tcbStatus: String(claims.tcb_status), reportData: hexToBytes(String(claims.report_data)), }; } function makeOrchestrator(server: MockServer, policy: AttestationPolicy): SgxAttestationVerifier { const orchestrator = new SgxAttestationVerifier({ routerUrl: 'https://router.test', policy, verifier: new CallableQuoteVerifier(faithfulVerify), }); (orchestrator as unknown as { httpClient: unknown }).httpClient = server.client(); return orchestrator; } /** Run the §3 algorithm against the mock server at the "high" tier. */ async function ensure( server: MockServer, policy: AttestationPolicy, tier = 'high' ): Promise { return makeOrchestrator(server, policy).ensureVerified(server.pubkeyDer, tier); } let server: MockServer; let warnSpy: jest.SpyInstance; beforeEach(async () => { server = new MockServer(); await server.init(); warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); }); afterEach(() => { warnSpy.mockRestore(); }); // ---- crypto helpers -------------------------------------------------------- describe('crypto helpers', () => { test('hex round-trips', () => { const bytes = new Uint8Array([0x00, 0x0f, 0xff, 0xa5]); expect(bytesToHex(bytes)).toBe('000fffa5'); expect(Array.from(hexToBytes('000fffa5'))).toEqual(Array.from(bytes)); }); test('hexToBytes rejects malformed input', () => { expect(() => hexToBytes('abc')).toThrow('odd length'); expect(() => hexToBytes('zzzz')).toThrow('non-hex'); }); test('constantTimeEqual compares correctly', () => { expect(constantTimeEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]))).toBe(true); expect(constantTimeEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4]))).toBe(false); expect(constantTimeEqual(new Uint8Array([1, 2]), new Uint8Array([1, 2, 3]))).toBe(false); }); }); // ---- policy ---------------------------------------------------------------- describe('AttestationPolicy', () => { test('normalises pins to lowercase and applies defaults', () => { const policy = new AttestationPolicy({ pinnedMrenclave: ['ABCDEF'], pinnedMrsigner: 'FEDCBA', }); expect(policy.pinnedMrenclave.has('abcdef')).toBe(true); expect(policy.pinnedMrsigner).toBe('fedcba'); expect(Array.from(policy.allowedTcbStatuses)).toEqual(['UpToDate']); expect(policy.requireAttestationForTier.has('high')).toBe(true); expect(policy.requireAttestationForTier.has('maximum')).toBe(true); expect(policy.enforce).toBe(false); expect(policy.liveness).toBe(false); }); }); // ---- orchestrator §3 algorithm --------------------------------------------- describe('SgxAttestationVerifier', () => { test('happy path returns the verified quote', async () => { const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true, }); const v = await ensure(server, policy); expect(v).not.toBeNull(); expect(v!.mrenclave).toBe(MRENCLAVE); expect(v!.tcbStatus).toBe('UpToDate'); }); test('standard tier skips attestation entirely', async () => { const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true }); const v = await ensure(server, policy, 'standard'); expect(v).toBeNull(); expect(server.attestationCalls).toBe(0); }); test('wrong key binding throws in enforce mode', async () => { server.bindToWrongKey = true; const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true }); await expect(ensure(server, policy)).rejects.toThrow(AttestationError); await expect(ensure(server, policy)).rejects.toThrow('key binding mismatch'); }); test('wrong key binding warns and proceeds when enforce is off', async () => { server.bindToWrongKey = true; const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: false }); await expect(ensure(server, policy)).resolves.toBeNull(); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('key binding mismatch')); }); test('MRENCLAVE outside the pinned set fails', async () => { server.mrenclave = 'c'.repeat(64); const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true }); await expect(ensure(server, policy)).rejects.toThrow('not in pinned set'); }); test('disallowed TCB status fails', async () => { server.tcbStatus = 'OutOfDate'; const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true }); await expect(ensure(server, policy)).rejects.toThrow("TCB status 'OutOfDate' not in allowed"); }); test('MRSIGNER, prod id and SVN checks are enforced', async () => { const base = { pinnedMrenclave: [MRENCLAVE], enforce: true }; await expect( ensure(server, new AttestationPolicy({ ...base, pinnedMrsigner: 'd'.repeat(64) })) ).rejects.toThrow('MRSIGNER'); await expect( ensure(server, new AttestationPolicy({ ...base, pinnedProdId: 99 })) ).rejects.toThrow('isvProdId'); await expect( ensure(server, new AttestationPolicy({ ...base, minIsvSvn: 10 })) ).rejects.toThrow('isvSvn 3 < minimum 10'); // All satisfied await expect( ensure(server, new AttestationPolicy({ ...base, pinnedMrsigner: MRSIGNER.toUpperCase(), pinnedProdId: 1, minIsvSvn: 3, })) ).resolves.not.toBeNull(); }); test('warns when no MRENCLAVE is pinned', async () => { await ensure(server, new AttestationPolicy({ enforce: true })); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('enclave identity is NOT verified')); }); test('unavailable attestation throws in enforce mode', async () => { server.isAvailable = false; const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true }); await expect(ensure(server, policy)).rejects.toThrow('attestation unavailable: sgx not supported'); }); test('unavailable attestation warns and proceeds when enforce is off', async () => { server.isAvailable = false; const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE] }); await expect(ensure(server, policy)).resolves.toBeNull(); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('attestation unavailable')); }); test('missing quote verifier throws in enforce mode', async () => { const orchestrator = new SgxAttestationVerifier({ routerUrl: 'https://router.test', policy: new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true }), }); (orchestrator as unknown as { httpClient: unknown }).httpClient = server.client(); await expect(orchestrator.ensureVerified(server.pubkeyDer, 'high')) .rejects.toThrow('no quote verifier configured'); }); test('liveness sends a fresh challenge and binds to it', async () => { const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true, liveness: true, }); const orchestrator = makeOrchestrator(server, policy); await expect(orchestrator.ensureVerified(server.pubkeyDer, 'high')).resolves.not.toBeNull(); await expect(orchestrator.ensureVerified(server.pubkeyDer, 'high')).resolves.not.toBeNull(); // Cache bypassed: both calls hit the server with distinct 32-byte challenges expect(server.attestationCalls).toBe(2); expect(server.seenChallenges).toHaveLength(2); expect(server.seenChallenges[0]).toHaveLength(64); expect(server.seenChallenges[0]).not.toBe(server.seenChallenges[1]); }); test('session cache skips the second verification', async () => { const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true }); const orchestrator = makeOrchestrator(server, policy); await expect(orchestrator.ensureVerified(server.pubkeyDer, 'high')).resolves.not.toBeNull(); // Second call is served from the session cache (null, no network) await expect(orchestrator.ensureVerified(server.pubkeyDer, 'high')).resolves.toBeNull(); expect(server.attestationCalls).toBe(1); expect(server.seenChallenges[0]).toBeNull(); }); test('refuses to fetch attestation over plain HTTP', async () => { const orchestrator = new SgxAttestationVerifier({ routerUrl: 'http://router.test', policy: new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE] }), verifier: new CallableQuoteVerifier(faithfulVerify), }); (orchestrator as unknown as { httpClient: unknown }).httpClient = server.client(); // Throws regardless of enforce — a MITM-able channel defeats attestation await expect(orchestrator.ensureVerified(server.pubkeyDer, 'high')) .rejects.toThrow('must be fetched over HTTPS'); }); test('allows plain HTTP when explicitly opted in', async () => { const orchestrator = new SgxAttestationVerifier({ routerUrl: 'http://router.test', policy: new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true }), verifier: new CallableQuoteVerifier(faithfulVerify), allowHttp: true, }); (orchestrator as unknown as { httpClient: unknown }).httpClient = server.client(); await expect(orchestrator.ensureVerified(server.pubkeyDer, 'high')).resolves.not.toBeNull(); }); test('AttestationError is a SecurityError', async () => { server.isAvailable = false; const policy = new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true }); await expect(ensure(server, policy)).rejects.toBeInstanceOf(SecurityError); }); }); // ---- verifiers ------------------------------------------------------------- describe('CallableQuoteVerifier', () => { test('accepts a sync callable', async () => { const quote: VerifiedQuote = { mrenclave: MRENCLAVE, mrsigner: MRSIGNER, isvProdId: 1, isvSvn: 1, tcbStatus: 'UpToDate', reportData: new Uint8Array(64), }; await expect(new CallableQuoteVerifier(() => quote).verify(new Uint8Array(0))) .resolves.toEqual(quote); }); test('rejects a callable that returns the wrong shape', async () => { const bad = new CallableQuoteVerifier(() => ({ mrenclave: 'x' } as unknown as VerifiedQuote)); await expect(bad.verify(new Uint8Array(0))) .rejects.toThrow('must return a VerifiedQuote'); }); }); describe('JwtQuoteVerifier', () => { test('throws a helpful AttestationError when jose is not installed', async () => { const verifier = new JwtQuoteVerifier('https://verify.test/quote', 'https://verify.test/jwks'); await expect(verifier.verify(new Uint8Array([1, 2, 3]))) .rejects.toThrow(/requires the 'jose' package/); }); test('accepts an injected jose module (CommonJS + ESM-only jose)', async () => { const claims = { sgx_mrenclave: MRENCLAVE.toUpperCase(), sgx_mrsigner: MRSIGNER, sgx_isvprodid: 1, sgx_isvsvn: '4', tcb_status: 'UpToDate', sgx_report_data: bytesToHex(new Uint8Array(64).fill(7)), }; const jose = { createRemoteJWKSet: jest.fn(() => ({ kid: 'stub' })), jwtVerify: jest.fn(async () => ({ payload: claims })), }; const verifier = new JwtQuoteVerifier( 'https://verify.test/quote', 'https://verify.test/jwks', { jose, issuer: 'https://verify.test', algorithms: ['ES256'] } ); (verifier as unknown as { httpClient: unknown }).httpClient = { post: jest.fn(async () => jsonResponse(200, { token: 'header.payload.sig' })), get: jest.fn(), }; const v = await verifier.verify(new Uint8Array([1, 2, 3])); expect(v.mrenclave).toBe(MRENCLAVE); // normalised to lowercase expect(v.isvSvn).toBe(4); // string claim coerced to int expect(v.reportData).toHaveLength(64); expect(jose.jwtVerify).toHaveBeenCalledWith( 'header.payload.sig', expect.anything(), expect.objectContaining({ issuer: 'https://verify.test', algorithms: ['ES256'] }) ); }); test('rejects a verifier response without a token', async () => { const verifier = new JwtQuoteVerifier('https://verify.test/quote', 'https://verify.test/jwks', { jose: { createRemoteJWKSet: jest.fn(), jwtVerify: jest.fn() }, }); (verifier as unknown as { httpClient: unknown }).httpClient = { post: jest.fn(async () => jsonResponse(200, { nope: true })), get: jest.fn(), }; await expect(verifier.verify(new Uint8Array([1]))).rejects.toThrow("missing JWT 'token'"); }); test('surfaces a non-200 from the verification service', async () => { const verifier = new JwtQuoteVerifier('https://verify.test/quote', 'https://verify.test/jwks', { jose: { createRemoteJWKSet: jest.fn(), jwtVerify: jest.fn() }, }); (verifier as unknown as { httpClient: unknown }).httpClient = { post: jest.fn(async () => jsonResponse(503, { detail: 'down' })), get: jest.fn(), }; await expect(verifier.verify(new Uint8Array([1]))).rejects.toThrow('returned HTTP 503'); }); }); // ---- end-to-end through SecureCompletionClient ------------------------------ describe('SecureCompletionClient with attestation', () => { test('enforce failure never sends plaintext', async () => { server.bindToWrongKey = true; const client = new SecureCompletionClient({ routerUrl: 'https://router.test', keyRotationInterval: 0, keySize: 2048, attestationPolicy: new AttestationPolicy({ pinnedMrenclave: [MRENCLAVE], enforce: true, }), quoteVerifier: new CallableQuoteVerifier(faithfulVerify), }); const http = server.client(); (client as unknown as { httpClient: unknown }).httpClient = http; (client as unknown as { attestation: { httpClient: unknown } }).attestation.httpClient = http; await expect( client.sendSecureRequest({ model: 'm', messages: [] }, 'pid', undefined, 'high') ).rejects.toThrow(AttestationError); // The completion endpoint was never POSTed to expect(http.post).not.toHaveBeenCalled(); client.dispose(); }); test('no attestation configured leaves behaviour unchanged', async () => { const client = new SecureCompletionClient({ routerUrl: 'https://router.test', keyRotationInterval: 0, keySize: 2048, }); expect((client as unknown as { attestation?: unknown }).attestation).toBeUndefined(); const http = server.client(); (client as unknown as { httpClient: unknown }).httpClient = http; // Reaches the router (mock returns 404 for the completion endpoint) — // i.e. no attestation step ran and no attestation request was made. await expect( client.sendSecureRequest({ model: 'm', messages: [] }, 'pid', undefined, 'high') ).rejects.toThrow('Endpoint not found'); expect(server.attestationCalls).toBe(0); client.dispose(); }); });