Skip to content

feat(issuer): implement the dstack-kms key provider (+ deploy/verify runbook)#25

Merged
NubsCarson merged 3 commits into
mainfrom
feat/dstack-kms-client
Jun 8, 2026
Merged

feat(issuer): implement the dstack-kms key provider (+ deploy/verify runbook)#25
NubsCarson merged 3 commits into
mainfrom
feat/dstack-kms-client

Conversation

@NubsCarson

Copy link
Copy Markdown
Owner

What

Turns the reserved/fails-closed TESSERA_KEY_PROVIDER=dstack-kms seam into a
working client that derives the ARC server key from the dstack guest agent
inside an Intel TDX CVM, sealed to the enclave (never on disk) — plus the
deploy/verify runbook for actually standing it up.

The client (crates/tessera-issuer/src/dstack_kms.rs)

  • std-only HTTP/1.1 + JSON over UnixStream (mirrors the existing
    mint::eth_call reader) — POST /GetKey to /var/run/dstack.sock, hex-decode
    the derived secret. Zero new deps; no SDK/async/protobuf (the "pRPC" socket
    speaks JSON via Content-Type: application/json, verified against real dstack
    source).
  • Expands the 32-byte secret into the ARC ServerPrivateKey by seeding the
    canonical SetupServer() keygen via a SHAKE256 XOF DRBG
    — the raw bytes are
    never used as a curve scalar. Deterministic in app-identity + key path, so an
    issuer and its exit in the same dstack app converge on one key with no shared
    key file
    .
  • Fail-closed on every transport/status/parse/length error (preflight
    rejects an unreachable socket). Probes the dstack socket fallbacks; bounds the
    read. The dstack compose now wires the exit to this provider.

Adversarial review applied (4 lenses)

The protocol-faithfulness lens verified the client matches real dstack source
(ship-as-is). The security lens's hardening is applied: exact HTTP status-code
match
, reject an all-zero secret (a predictable seed → predictable KVAC key
that both forges and verifies), reject chunked encoding, case-insensitive
0x. Tests went 11 → 21, adding: non-hex key, 0x-prefix equivalence,
all-zero rejection, key_id JSON-escaping proven through the live request path
(the mock now captures the body), HTTP-framing edge cases (missing separator /
empty body / no Content-Length / chunked), socket-fallback gating, and a KAT
that freezes the seed→key derivation
so an accidental change can't silently break
issuer↔exit convergence across separately-built binaries.

Runbook (DEPLOY.md §2 "Verifying a deployment")

Where to run (Phala ~$40/mo, or GCP/Azure TDX), the dstack simulator for
off-TEE testing (labeled wire-only, not security), and the client/auditor
quote-verification steps (dcap-qvl, MRTD/RTMR, compose_hash, pin digests).

Honest scope (unchanged posture)

Validated only against a mock + the dstack simulator — NOT real Intel TDX
hardware + a live KMS
, so a simulator/mock key carries no security guarantee; a
client-side quote-check before routing is still not built. Every touched doc
(DEPLOY / KEY_MANAGEMENT / DEPLOYMENT_TOPOLOGY / TRUST_MODEL / STATUS /
NEXT_STEPS / CLEAN_ONION_EGRESS / ONION_EGRESS / CHANGELOG / the compose)
now says implemented · fail-closed off-TEE · mock/simulator-proven, not
silicon-proven
— no residual "reserved" contradiction.

Research-grade · UNAUDITED.

…gent client)

Turn the reserved/fails-closed `TESSERA_KEY_PROVIDER=dstack-kms` seam into a
working client that derives the ARC server key from the dstack guest agent inside
an Intel TDX CVM, sealed to the enclave (never on disk).

- `dstack_kms.rs` (NEW): a std-only HTTP/1.1+JSON client over `UnixStream`
  (mirrors the existing `mint::eth_call` reader) — POST `/GetKey` to the
  guest-agent socket, hex-decode the derived secret, and expand it into the ARC
  `ServerPrivateKey` by SEEDING the canonical `SetupServer()` keygen via a SHAKE256
  XOF DRBG (the raw bytes are never used as a curve scalar). Deterministic in the
  app identity + key path, so an issuer and its exit in the same app converge on
  one key with no shared key file. ZERO new deps (reuses sha3/hex/rand_core/zeroize).
- Probes the dstack SDK socket fallbacks; bounds the response read; fail-closed on
  any transport/status/parse/length error (a wrong KVAC key both forges AND
  verifies, so never fall back to ephemeral/disk).
- Wire into `KeyProviderConfig::{preflight,establish}`; drop the reserved-error
  const. preflight now validates the socket is reachable (fail-closed off-TEE).
- Tests: 11 unit tests against a faithful in-process mock guest agent (determinism,
  issuer/exit convergence, and fail-closed on absent socket / non-200 / short
  secret / missing field) + the two `--check` integration tests updated.

Research-grade, UNAUDITED: validated only against a mock + the dstack simulator,
NOT yet proven on real TDX silicon. Docs updated separately in this PR.
…ilicon-proven)

The dstack-kms provider went from reserved/fails-closed-stub to an implemented
guest-agent client, so update every site that claimed otherwise — consistently and
without overclaiming (it's tested vs a mock + the dstack simulator, NOT real TDX):
DEPLOY.md §2 (+ a new deploy/verify runbook: where to run — Phala/GCP/Azure TDX —
the simulator for off-TEE testing, and the client/auditor quote-verification steps),
KEY_MANAGEMENT.md §4.2, DEPLOYMENT_TOPOLOGY.md §3, STATUS.md row 13, TRUST_MODEL.md
§4.1/§4.2/§7, NEXT_STEPS.md (track + guardrail), CLEAN_ONION_EGRESS.md, the dstack
compose comment (+ wire the exit to dstack-kms), and a CHANGELOG entry.
Security hardening (defense-in-depth on the catastrophic-if-broken key path):
- match the HTTP status code token exactly (a loose " 200" substring could be
  satisfied by a non-200 reason phrase);
- reject an all-zero derived secret (a predictable seed → predictable ARC key,
  which under keyed verification both forges AND verifies);
- reject chunked transfer-encoding loudly instead of silently mis-framing;
- strip a `0x`/`0X` key prefix case-insensitively.

Tests (now 21 in dstack_kms): + non-hex key, + 0x-prefix equivalence, + all-zero
rejection, + key_id JSON-escaping proven through the LIVE request path (mock now
captures the request body), + missing-separator / empty-body / no-Content-Length
framing, + chunked rejection, + socket_candidates fallback gating, and a **KAT**
that freezes the seed→key derivation so an accidental change to the domain
separator / SHAKE order / endianness can't silently break issuer↔exit convergence
across separately-built binaries.

Docs: fix every remaining "reserved/not-wired/fails-closed-until" contradiction the
review found (KEY_MANAGEMENT §4.2 heading, STATUS reconciliations, DEPLOYMENT_TOPOLOGY
§7, TRUST_MODEL §8, DEPLOY env table, CLEAN_ONION_EGRESS, ONION_EGRESS, the issuer
binary's doc header, CHANGELOG) → consistently "implemented; fail-closed off-TEE;
mock/simulator-proven, not silicon-proven."

Protocol-faithfulness lens verified the client matches real dstack source (JSON via
Content-Type alone, field names/hex, path=key_id determinism) — ship-as-is there.
@NubsCarson NubsCarson merged commit 4357d88 into main Jun 8, 2026
11 of 12 checks passed
@NubsCarson NubsCarson deleted the feat/dstack-kms-client branch June 8, 2026 03:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant