nomyo/doc/attestation.md

135 lines
5.6 KiB
Markdown
Raw Permalink Normal View History

2026-06-16 16:26:30 +02:00
# 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: `tcb_status`, pinned `MRENCLAVE` (and optionally
`MRSIGNER` / `prod_id` / minimum `isv_svn`).
5. Checks the **key binding**: `SHA-512(server_pubkey_der [|| challenge])` must
equal the `report_data` 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 raises and nothing is sent.
## 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 dependency:
```bash
pip install nomyo[attestation]
```
```python
from nomyo import SecureChatCompletion, AttestationPolicy, JwtQuoteVerifier
verifier = JwtQuoteVerifier(
verify_url="https://attest.example.com/appraisal/v1/attest",
jwks_url="https://attest.example.com/certs",
issuer="https://attest.example.com", # optional
audience="nomyo-client", # optional
# claim_map=... # override if your service uses different claim names
)
policy = AttestationPolicy(
pinned_mrenclave={"<hex from gramine/build-enclave.sh>"},
allowed_tcb_statuses={"UpToDate"},
enforce=False, # rollout: warn and proceed. Set True to hard-fail.
)
client = SecureChatCompletion(
base_url="https://api.nomyo.ai",
api_key="...",
attestation_policy=policy,
quote_verifier=verifier,
)
resp = await client.create(
model="...", messages=[...], security_tier="high",
)
```
The default JWT claim names are in `DEFAULT_CLAIM_MAP`; override `claim_map` if
your service differs (e.g. Veraison vs ITA).
### `CallableQuoteVerifier` — bring your own
```python
from nomyo import CallableQuoteVerifier, VerifiedQuote
async def my_verify(quote: bytes) -> VerifiedQuote:
# call your verifier (local QVL FFI, another service, ...)
return VerifiedQuote(mrenclave=..., mrsigner=..., isv_prod_id=...,
isv_svn=..., tcb_status="UpToDate", report_data=...)
client = SecureChatCompletion(..., attestation_policy=policy,
quote_verifier=CallableQuoteVerifier(my_verify))
```
`my_verify` may be sync or async and must raise on any verification failure.
## Policy (`AttestationPolicy`)
| Field | Meaning | Default |
|---|---|---|
| `pinned_mrenclave` | Allowed enclave measurements (a **set** so you can roll builds) | `frozenset()` |
| `pinned_mrsigner` | Allowed signer (optional) | `None` |
| `pinned_prod_id` / `min_isv_svn` | Product id / minimum security version (optional) | `None` |
| `allowed_tcb_statuses` | Accepted platform TCB levels | `{"UpToDate"}` |
| `enforce` | Hard-fail vs warn-and-proceed | `False` |
| `require_attestation_for_tier` | Tiers that must be operator-blind | `{"high","maximum"}` |
| `liveness` | Use a fresh per-request challenge (see below) | `False` |
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. Do **not** silently allow `OutOfDate` or
`Revoked` TCB statuses.
## Rollout vs enforce (spec §6)
- **`standard` tier:** attestation not expected — proceeds normally.
- **`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**.
## Liveness (spec §7, optional)
The default quote is bound to the key only and is 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.
## 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: <JWT>}` plus a JWKS endpoint. Veraison is an
open-source option. Run that one service and point `JwtQuoteVerifier` at it.