Key Release
The KMS releases a merod node’s storage-encryption key only after the node proves,
against the hardware, that it is running an approved measurement. This is a two-round
challenge-response: the node first gets a fresh nonce, then presents a TDX quote and a
signature binding that nonce to its identity. The KMS verifies the quote, enforces the
measurement policy, and only then derives the key from its
cluster root and returns it sealed to the node. This is the trust plane where the KMS verifies the node (see the
trust model).
The quote itself is produced as described in Attestation Flow; here we cover the gate that consumes it.
Endpoints
Section titled “Endpoints”The KMS serves the release protocol over two unauthenticated routes plus a health
check. Any replica of the cluster can serve either route: a node’s /get-key rarely
reaches the replica that served its /challenge, and it does not need to.
| Method · Path | Body | Purpose |
|---|---|---|
POST /challenge |
{ peerId } |
Issue a single-use nonce challenge bound to a peer ID. |
POST /get-key |
{ challengeId, quoteB64, peerId, peerPublicKeyB64, signatureB64, sealToB64 } |
Verify attestation + policy, derive the key and return it sealed. |
GET /health |
— | Liveness probe, plus clusterRootReady. |
Round 1 — challenge
Section titled “Round 1 — challenge”-
Node requests a challenge, sending its base58 libp2p peer ID:
{ "peerId": "12D3KooW..." } -
KMS mints a stateless challenge. The challenge ID is 16 random bytes and the expiry, hex-encoded (48 characters). The nonce is an HMAC-SHA256 over the ID and the peer ID under a challenge key HKDF’d from the cluster root. Nothing is stored.
-
KMS responds:
{"challengeId": "<112 hex chars>","nonceB64": "<base64 of the 32-byte nonce>","expiresAt": 1700000000}
Because the nonce is recomputed from the challenge ID with a key only the cluster
holds, any replica can recompute it, and only for the peer that asked for it
(mero-kms/src/stateless_challenge.rs). A forged ID yields a nonce its forger cannot
compute, so the node’s signature and quote refuse it. Challenges expire after
CHALLENGE_TTL_SECS (default 60 seconds). A replica without its root cannot issue
one and answers 503 policy_not_ready.
Round 2 — get-key
Section titled “Round 2 — get-key”The node generates a TDX quote whose report_data is nonce || SHA-256(peer_id),
signs a payload binding the challenge to its key, and calls /get-key:
{ "challengeId": "<112 hex chars>", "quoteB64": "<base64 raw TDX quote>", "peerId": "12D3KooW...", "peerPublicKeyB64": "<base64 protobuf libp2p public key>", "signatureB64": "<base64 signature over the canonical payload>", "sealToB64": "<base64 one-time X25519 public key>"}The KMS runs a fixed pipeline. The challenge is checked first and redeemed last, just before the key is derived: a made-up challenge costs one MAC, and only a request that verified can take a place in the replay set, so a request replayed to the same replica after a release always fails:
-
Validate inputs. Peer-ID shape (base58btc, ≤128 chars) and challenge-ID shape (112 hex chars) are checked; the quote is base64-decoded. Without
sealToB64the request is refused (MERO_KMS_REQUIRE_SEALED_KEY_RELEASE, on in every image). -
Root gate. If the replica does not hold its cluster’s root yet, key release is refused with
503 policy_not_readybefore anything else —/get-keyfails closed while/atteststill works. -
Check the challenge. The replica checks the ID’s tag against this
peerIdand its expiry, and recomputes the nonce. A forged, foreign or expired challenge →401 invalid_challenge. Nothing is recorded yet. -
Verify the peer signature. The submitted public key is decoded, confirmed to derive to the claimed
peerId, and used to verify a signature over the canonical JSON payload{ challengeId, challengeNonceHex, quoteHashHex, peerId }(the quote is hashed, not embedded). A mismatch yieldspeer_identity_mismatchorinvalid_signature. -
Verify the attestation. The quote is checked cryptographically, and its
report_datamust carry the issued nonce in bytes 0–31 and, in bytes 32–63, a binding over the peer ID and thesealToB64key (SHA-256(peerId)for a legacy unsealed request). A failed nonce check →401 invalid_challenge; a failed identity binding →peer_id_mismatch; otherwiseattestation_verification_failed. -
Enforce the measurement policy. The reported TCB status must be in
allowed_tcb_statuses(403 tcb_status_rejectedotherwise), and each of MRTD and RTMR0–3 must match its allowlist (403 measurement_policy_rejectedotherwise). An empty allowlist for any enforced register is treated as a rejection, so a misconfigured policy fails closed. See Policy Management. -
Redeem the challenge (single-use per replica). Only a request that passed every check above is entered in the replay set. An already-redeemed challenge →
401 invalid_challenge; a full replay set (MAX_CONSUMED_CHALLENGES) →429 rate_limited. -
Derive the key. Only now does the KMS derive the key by HKDF from the cluster root along the path
{KEY_NAMESPACE_PREFIX}/{profile}/{peerId}(e.g.merod/storage/<profile>/<peerId>). The derivation is deterministic in those inputs, so every replica of the cluster derives the same key. -
Return the key, sealed to the node’s one-time
sealToB64key (AES-GCM under an X25519 agreement with the KMS’s root-derived transport key):{ "sealedKeyB64": "<base64>", "sealNonceB64": "<base64 12-byte nonce>" }
node ──POST /challenge { peerId }──────────────▶ KMSnode ◀─{ challengeId, nonceB64, expiresAt }───── KMS
node: build report_data = nonce || binding(peerId, sealTo)node: quote = TDX quote over report_datanode: sig = sign({challengeId, nonceHex, quoteHash, peerId})
node ──POST /get-key { challengeId, quoteB64, peerId, peerPublicKeyB64, signatureB64, sealToB64 }▶ KMS (any replica) KMS: redeem challenge (tag, expiry, replay set) KMS: verify signature -> identity KMS: verify quote + nonce + peer bind KMS: enforce TCB + MRTD + RTMR0-3 KMS: key = HKDF(root, path)node ◀─{ sealedKeyB64, sealNonceB64 }─────────── KMSSecurity properties
Section titled “Security properties”- Single-use challenges, per replica. Each replica remembers the challenges it has redeemed until they expire. The same request sent to a second replica within the TTL can pass the challenge check there, but it still needs the node’s signature and a quote bound to the node’s one-time key, and what it releases is sealed to that key, which a replayer does not hold.
- Freshness. The nonce lives in the quote’s
report_data, proving the quote was generated for this specific challenge; stale challenges are TTL-pruned. - Identity binding. The signature ties the request to a key that provably owns the
peerId, and the quote’s secondreport_datahalf re-bindsSHA-256(peerId)— a quote cannot be replayed for a different node. - Deterministic keys. Because derivation is a pure function of the cluster root, namespace, profile, and peer ID, a node receives the same key from any replica of its release’s cluster — and a different one from any other cluster.
- Fail-closed policy. No root yet, or an empty allowlist for an enforced register, blocks release rather than allowing it.