Key Release Flow

Challenge-response protocol for storage encryption key release

Protocol Overview

The key release protocol is a two-phase challenge-response flow. In Phase 1, the node obtains a fresh nonce from the KMS. In Phase 2, the node presents its TDX attestation quote with the nonce to prove its identity and integrity. Only after passing all checks does the KMS derive and release the storage encryption key.

2
phases
5
verification steps
1
key derivation
TTL
bound nonces

Sequence Diagram

merod Node mero-kms-phala dstack PHASE 1: CHALLENGE POST /challenge { peerId } generate nonce store(challengeId, nonce, peerId, expiry) { challengeId, nonce } PHASE 2: KEY RELEASE produce TDX quote bind nonce to report_data sign(nonce, peerKey) POST /get-key { challengeId, quote, signature } consume challenge verify signature verify TDX quote policy check MRTD, RTMR0-3, TCB derive_key(namespace, peerId) HKDF deterministic key material { key } decrypt storage Failure exits: challenge expired → 400 bad signature → 401 invalid quote → 401 policy violation → 403 rate limited → 429 policy not ready → 503
Node
KMS
dstack
Key release
Error paths

Phase 1 — Challenge

Request

POST /challenge
Content-Type: application/json

{
  "peerId": "12D3KooW..." // libp2p peer ID of the requesting node
}

Response

200 OK
Content-Type: application/json

{
  "challengeId": "uuid-v4", // opaque challenge identifier
  "nonce": "hex-encoded-32-bytes" // cryptographically random nonce
}

Server-Side Logic

Nonce Generation

32 bytes from a CSPRNG. The nonce must be unpredictable to prevent pre-computation of TDX quotes.

Challenge Storage

The ChallengeStore maps challengeId → (nonce, peerId, expiry). InMemory uses a DashMap; Redis uses SETEX for automatic TTL cleanup. Max pending challenges are enforced per-peer.

Phase 2 — Key Release

Request

POST /get-key
Content-Type: application/json

{
  "challengeId": "uuid-v4",
  "quote": "base64-encoded-tdx-quote",
  "signature": "base64-encoded-ed25519-sig"
}

Verification Pipeline

1

Consume Challenge

Look up challengeId in the store and remove it atomically (one-time use). If not found or expired, return 400 InvalidChallenge.

2

Verify Signature

Decode the peerId to extract the Ed25519 public key. Verify signature over the original nonce. If invalid, return 401 InvalidSignature.

3

Parse TDX Quote

Decode the base64 quote. Extract MRTD, RTMR0–3, report_data, and TCB status fields from the TDX quote structure.

4

Verify Nonce Binding

Check that the quote’s report_data field contains the expected hash binding to the original nonce. This ensures the quote was freshly generated for this challenge.

5

Policy Evaluation

Compare extracted measurements against the attestation policy allowlists:

  • MRTD ∈ allowed_mrtd
  • RTMR0 ∈ allowed_rtmr0
  • RTMR1 ∈ allowed_rtmr1
  • RTMR2 ∈ allowed_rtmr2
  • RTMR3 ∈ allowed_rtmr3 (profile check)
  • TCB status ∈ allowed_tcb_status

Any mismatch returns 403 PolicyViolation with details about which field failed.

6

Key Derivation

Call dstack via the Unix socket at DSTACK_SOCKET_PATH. The derivation uses KEY_NAMESPACE_PREFIX + peerId as input. dstack’s HKDF produces a deterministic 32-byte key — the same inputs always yield the same key.

Response

200 OK
Content-Type: application/json

{
  "key": "base64-encoded-32-byte-key" // storage encryption key
}

Security Properties

One-Time Use

Each challenge can only be consumed once. Replaying a /get-key request with the same challengeId returns 400 InvalidChallenge.

Deterministic Keys

dstack derives keys deterministically from the namespace + peerId. The node always gets the same key regardless of which KMS instance it contacts (as long as the KMS runs the same dstack root).

TTL Expiry

Challenges expire after CHALLENGE_TTL_SECS (default: 300s). Slow or stale requests are automatically rejected.

Rate Limiting

MAX_PENDING_CHALLENGES limits how many active challenges a single peerId can have. Excess requests return 429 RateLimited.