# SGX enclave attestation (operator-blind verification) For the `high` and `maximum` security tiers, the NOMYO server runs CPU inference inside an Intel SGX enclave. SGX **attestation** lets the client prove — *before sending any plaintext* — that the RSA public key it is about to encrypt to was generated inside a genuine enclave running the expected (measured) code. This upgrades the channel from "encrypted in transit, operator-trusted" to "operator-blind": only the enclave can decrypt, not the host operator. This is **opt-in** and **additive**. With no attestation configured the client behaves exactly as before. ## How it works On each secure request for a tier that requires attestation, the client: 1. Fetches the server public key (`GET /pki/public_key`). 2. Fetches a hardware-signed DCAP quote (`GET /pki/attestation`). 3. **Verifies the quote** (ECDSA signature + Intel PCK chain + TCB) via a `QuoteVerifier` you provide (step 4). 4. Checks policy: `tcbStatus`, pinned `MRENCLAVE` (and optionally `MRSIGNER` / `prodId` / minimum `isvSvn`). 5. Checks the **key binding**: `SHA-512(server_pubkey_der [|| challenge])` must equal the `reportData` inside the verified quote (constant-time compare). 6. Only if all checks pass does it encrypt the payload **to that exact attested key** and send it. In `enforce` mode any failure throws and nothing is sent. The key fetched in step 1 is the key used in step 6 — it is not re-fetched between verification and encryption, so there is no window in which an attested key could be swapped for another. ## Quote verification (`QuoteVerifier`) Verifying a DCAP quote must not be hand-rolled. The client delegates step 4 to a `QuoteVerifier`. Three ways to supply one: ### `JwtQuoteVerifier` (recommended) Talks to a remote verification service that returns a signed JWT of appraised claims — Intel Trust Authority, [Veraison](https://github.com/veraison), or a self-hosted wrapper around the Intel DCAP Quote Verification Library (QVL). The native/PCCS dependency lives on that one service, not on every client. Requires the optional peer dependency: ```bash npm install jose ``` ```typescript import { SecureChatCompletion, AttestationPolicy, JwtQuoteVerifier, } from 'nomyo-js'; const verifier = new JwtQuoteVerifier( 'https://attest.example.com/appraisal/v1/attest', // verifyUrl 'https://attest.example.com/certs', // jwksUrl { issuer: 'https://attest.example.com', // optional audience: 'nomyo-client', // optional // claimMap: { ... } // override if your service uses different claim names }, ); const policy = new AttestationPolicy({ pinnedMrenclave: [''], allowedTcbStatuses: ['UpToDate'], enforce: false, // rollout: warn and proceed. Set true to hard-fail. }); const client = new SecureChatCompletion({ baseUrl: 'https://api.nomyo.ai', apiKey: process.env.NOMYO_API_KEY, attestationPolicy: policy, quoteVerifier: verifier, }); const response = await client.create({ model: 'Qwen/Qwen3-0.6B', messages: [{ role: 'user', content: 'Hello!' }], security_tier: 'high', }); ``` The default JWT claim names are in `DEFAULT_CLAIM_MAP`; override `claimMap` if your service differs (e.g. Veraison vs ITA). #### CommonJS and ESM-only `jose` `jose` v5+ is ESM-only. Under CommonJS, `require('jose')` fails, and this client deliberately does **not** work around that with a runtime-constructed dynamic import: that requires `new Function`/`eval`, which any Content-Security-Policy without `unsafe-eval` blocks — and the failure would land in the attestation path in browsers. Import the module yourself and pass it in instead: ```typescript import * as jose from 'jose'; const verifier = new JwtQuoteVerifier(verifyUrl, jwksUrl, { jose }); ``` Unlike the Python client there is no `verify_ssl` escape hatch: TLS certificate verification against the appraisal service is always enforced. ### `CallableQuoteVerifier` — bring your own ```typescript import { CallableQuoteVerifier, VerifiedQuote } from 'nomyo-js'; async function myVerify(quote: Uint8Array): Promise { // call your verifier (local QVL binding, another service, ...) return { mrenclave: '...', mrsigner: '...', isvProdId: 1, isvSvn: 3, tcbStatus: 'UpToDate', reportData: new Uint8Array(64), }; } const client = new SecureChatCompletion({ attestationPolicy: policy, quoteVerifier: new CallableQuoteVerifier(myVerify), }); ``` `myVerify` may be sync or async and must throw on any verification failure. ### Any `QuoteVerifier` Implement the interface directly: ```typescript interface QuoteVerifier { verify(quote: Uint8Array): Promise; } ``` ## Policy (`AttestationPolicy`) | Option | Meaning | Default | |---|---|---| | `pinnedMrenclave` | Allowed enclave measurements (a **set** so you can roll builds) | `[]` (not verified) | | `pinnedMrsigner` | Allowed signer (optional) | `undefined` | | `pinnedProdId` / `minIsvSvn` | Product id / minimum security version (optional) | `undefined` | | `allowedTcbStatuses` | Accepted platform TCB levels | `['UpToDate']` | | `enforce` | Hard-fail vs warn-and-proceed | `false` | | `requireAttestationForTier` | Tiers that must be operator-blind | `['high', 'maximum']` | | `liveness` | Use a fresh per-request challenge (see below) | `false` | Hex values are compared case-insensitively; pins are lowercased on construction. Pin MRENCLAVE values as **configuration**, not constants — they change whenever the enclave build or its file paths change. Printed by `gramine/build-enclave.sh` at sign time. With no `pinnedMrenclave` the client logs a warning on every verification, because enclave identity is then not actually being checked. Do **not** silently allow `OutOfDate` or `Revoked` TCB statuses. ## Rollout vs enforce - **`standard` tier:** attestation not expected — proceeds normally, and no attestation request is made at all. - **`high`/`maximum`, `enforce: false` (rollout, default):** if attestation is unavailable (server not yet under SGX) or fails, a loud warning is logged and the request proceeds. Lets you deploy clients before SGX is live everywhere. - **`high`/`maximum`, `enforce: true` (target):** unavailable or failed attestation ⇒ the request is refused with `AttestationError`, **no plaintext fallback**. The payload is never even constructed. `AttestationError` extends `SecurityError`, so existing `catch (e) { if (e instanceof SecurityError) ... }` blocks treat a failed attestation as the security failure it is. Fetching attestation over plain HTTP throws regardless of `enforce` — a MITM-able channel defeats the purpose. Use `allowHttp: true` only for local development. ## Liveness (optional) The default quote is bound to the key only and may be cached server-side (proves binding, not recency). Set `liveness: true` to send a random 32-byte challenge per handshake; the server returns a fresh, uncached quote bound to `SHA-512(pubkey_der || challenge)`, proving the enclave is alive *now*. Liveness bypasses the client-side verification cache, so every request performs a full round trip and quote verification. Without it, a successful verification is cached per server public key for the session and re-verified if that key changes. ## Self-hosting the verifier A self-hosted equivalent of Intel Trust Authority is a small service that runs the Intel DCAP QVL (`libsgx_dcap_quoteverify`) with a reachable PCCS, and exposes `POST /verify {quote} -> {token: }` plus a JWKS endpoint. Veraison is an open-source option. Run that one service and point `JwtQuoteVerifier` at it.