Skip to content

guest-agent: freeze the v0 API at v0.5.11 and add the versioned v1 API - #1116

Draft
kvinwang wants to merge 22 commits into
nextfrom
feat/guest-api-v1
Draft

guest-agent: freeze the v0 API at v0.5.11 and add the versioned v1 API#1116
kvinwang wants to merge 22 commits into
nextfrom
feat/guest-api-v1

Conversation

@kvinwang

@kvinwang kvinwang commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Single PR for the dstack 0.6.0 guest-agent API: freezes the unversioned surface at exactly v0.5.11, introduces the versioned v1 API, and switches the four SDKs to mirror both surfaces. Supersedes #1113/#1114.

Problem

next had broken the unversioned paths that 0.5.x clients call (Verify/EmitEvent deleted, Worker.GetAttestationForAppKey renamed), while the unversioned API carried design debt it could not shed without further breakage: GetTlsKey names a key what is really certificate issuance; GetKey's purpose/algorithm are accepted but do not domain-separate the derived key (one 32-byte secret serves both curves); Sign/Verify are pure computation granting no capability GetKey does not already grant; Info returns triple-nested JSON duplicating its own top-level fields; and the chain claim keccak256("{purpose}:{hex(pubkey)}") lets a caller-chosen purpose mint nearly arbitrary signed strings.

Fix

Every surface becomes a frozen v0 fossil plus a v1, selected by URL path only (no header negotiation):

Listener v0 (frozen = v0.5.11) v1
dstack.sock / (alias), /v0: 9 v0.5.11 methods /v1: IssueCert, GetKey, Attest, AttestGpu, Info, Version
external /prpc (alias), /prpc/v0: Info, Version, GetAttestationForAppKey /prpc/v1: Info, Version, Health
tappd.sock Tappd unversioned (predates v0)

v0: Verify restored byte-identical to v0.5.11; EmitEvent restored as a deliberate always-failing stub (RTMR3 runtime events are system-owned; the message is the signal, since prpc reports handler failure and unknown method differently only in the body); GetAttestationForAppKey restored TDX-only. Never-released post-0.5.11 additions removed. A descriptor-digest test pins the frozen shape so even a wire-compatible field addition fails CI.

v1 keeps only what needs the TEE — derivation, attestation, and KMS-backed cert issuance:

  • GetKey(domain, algorithm): HKDF-SHA256(salt="dstack-guest-v1", IKM=app_root_k256, info=LP("dstack-guest-v1-key")||LP(algorithm)||LP(domain)) — length-prefixed encoding, per-algorithm keys, v1-specific salt so no v0 input can reproduce a v1 key. Chain claim binds (algorithm, domain, raw pubkey) and is structurally unforgeable from the v0 claim format (a v0 preimage's tail is lowercase hex; the v1 length prefix contains 0x00). Constants and vectors live in ra_tls::guest_api_v1, shared by agent and tests.
  • IssueCert: honest name for what GetTlsKey did — certificate issuance via KMS SignCert, generating a fresh key as the integrated one-step mode.
  • Attest (sole CVM attestation entry; optional boot-time GPU evidence), AttestGpu (on-demand, 32-byte nonce), flat identity-and-configuration Info (attestation data deliberately excluded — it belongs to Attest, quote-backed), Version.
  • No Sign/Verify/EmitEvent/GpuInfo/AttestAppKey: signing and verification are the app's and relying party's job per the spec; there is no v1 key the external AttestAppKey could honestly attest.
  • Gateway health polling moves to /prpc/v1/Health. Identity for Info is decoded once at startup (an anonymous external caller can no longer drive hardware quote generation per request).

SDKs (all four): ClientV0 mirrors the frozen surface exactly (v0.5.11 method set, verify() back, emit_event() surfaces the removal error); ClientV1 mirrors the six v1 methods; the never-released local verify_signature/verify_signature_chain helpers and their vectors are removed — the normative spec is the verification reference. Deprecated aliases keep existing code compiling. Versions unified at 0.6.0.

Spec: docs/guest-api-v1.md — byte-level KDF/claim encodings, verification steps, status-code/probe rules (measured: absent mount → 404 HTML, unknown method → 404 JSON, failed handler → 400 JSON), migration contract including that v1 keys deliberately differ from v0 keys for the same inputs.

Verification

  • Acceptance diff vs the v0.5.11 tag (comments stripped): frozen DstackGuest, Worker, Tappd identical; 16/19 messages byte-identical, 3 differ by reserved only.
  • ./dstack/run-tests.sh (agent-backed simulator): 1235 passed, 0 failed. sdk/run-tests.sh: all four suites green (Rust 38, Go 51, Python 156, JS 131).
  • Committed KDF/claim vectors; a_v0_path_cannot_reproduce_a_v1_key; forgery test asserts the structural hex/0x00 property; frozen-descriptor test verified to catch a planted field addition; route tests prove /v0 and unversioned are the same handler and each mount serves only its own surface.
  • fmt/clippy (per CLAUDE.md)/workspace check clean; Go vet/gofmt clean; JS tsc/tsup clean.
  • An 8-angle adversarial self-review ran against this branch; all 26 confirmed findings were fixed here or explicitly dispositioned.

Known follow-ups (out of scope)

  • Remaining 0.6 prerelease checklist: stabilize guest-agent API and SDKs #1094 SDK cleanups (asUint8ArraytoPkcs8Der, Go usage_server_auth default, JS transport status checks).
  • CI job running pinned v0.5.10/v0.5.11 released SDKs against the new agent (compat regression).
  • Pre-existing simulator fixture inconsistency: sdk/simulator/attestation.bin was captured from a different app-compose than app-compose.json (compose_hash mismatch, reproducible on v0 too) — needs a fixture refresh.

Kevin Wang and others added 9 commits August 24, 2026 01:39
0.6.0 dropped the `Verify` RPC because verification needs no key material
and no attestation, and the agent's verdict arrives over the socket
unattested -- the reasoning still holds, and SDKs >= 0.6 verify locally.

But dropping it broke every 0.5.x SDK against a 0.6 agent for a method
those SDKs still call, and prpc answers an unknown method with a bare
HTTP 400, so the caller cannot tell a removed method from a wrong socket.
The unversioned surface is what 0.5.x clients speak; new API design
belongs in `dstack.guest.v1`, not in breaking this one.

Restore the RPC and its messages at the v0.5.11 field numbers, marked
deprecated and legacy-only, and annotate the service as frozen: no
renumbering, no removals, no semantic changes. Semantics are identical to
0.5.11, including k256 rejecting non-canonical high-S signatures outright.
`EmitEvent` is gone for good -- runtime RTMR3 events are system-owned in
0.6.0, so an app extending the measurement chain is no longer something
this API should offer.

Deleting the method outright is the wrong way to say that. prpc answers
both "no such method" and "the handler failed" with HTTP 400 and drops
the message, so a 0.5.x app that emits events gets a bare 400 that reads
identically to a wrong socket path or a broken build, and its author has
no way to learn why the events stopped being recorded.

Keep the method and the args message on the unversioned service, and have
the handler always bail with the reason. Nothing reaches
`emit_runtime_event`; this is a deliberate error, not a code path.
Comment thread dstack/guest-agent/src/rpc_service_v1/keys.rs Fixed
Comment thread dstack/ra-tls/src/kdf.rs Dismissed
Comment thread dstack/ra-tls/src/kdf.rs Dismissed
Comment thread dstack/ra-tls/src/kdf.rs Dismissed
Comment thread dstack/ra-tls/src/kdf.rs Dismissed
Comment thread dstack/ra-tls/src/guest_api_v1.rs Dismissed
Comment thread sdk/rust/tests/test_client_v1.rs Dismissed
Comment thread sdk/rust/tests/test_client_v1.rs Dismissed
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.

2 participants