Skip to content

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.

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.
  1. Node requests a challenge, sending its base58 libp2p peer ID:

    { "peerId": "12D3KooW..." }
  2. 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.

  3. 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.

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:

  1. Validate inputs. Peer-ID shape (base58btc, ≤128 chars) and challenge-ID shape (112 hex chars) are checked; the quote is base64-decoded. Without sealToB64 the request is refused (MERO_KMS_REQUIRE_SEALED_KEY_RELEASE, on in every image).

  2. Root gate. If the replica does not hold its cluster’s root yet, key release is refused with 503 policy_not_ready before anything else — /get-key fails closed while /attest still works.

  3. Check the challenge. The replica checks the ID’s tag against this peerId and its expiry, and recomputes the nonce. A forged, foreign or expired challenge → 401 invalid_challenge. Nothing is recorded yet.

  4. 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 yields peer_identity_mismatch or invalid_signature.

  5. Verify the attestation. The quote is checked cryptographically, and its report_data must carry the issued nonce in bytes 0–31 and, in bytes 32–63, a binding over the peer ID and the sealToB64 key (SHA-256(peerId) for a legacy unsealed request). A failed nonce check → 401 invalid_challenge; a failed identity binding → peer_id_mismatch; otherwise attestation_verification_failed.

  6. Enforce the measurement policy. The reported TCB status must be in allowed_tcb_statuses (403 tcb_status_rejected otherwise), and each of MRTD and RTMR0–3 must match its allowlist (403 measurement_policy_rejected otherwise). An empty allowlist for any enforced register is treated as a rejection, so a misconfigured policy fails closed. See Policy Management.

  7. 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.

  8. 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.

  9. Return the key, sealed to the node’s one-time sealToB64 key (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 }──────────────▶ KMS
node ◀─{ challengeId, nonceB64, expiresAt }───── KMS
node: build report_data = nonce || binding(peerId, sealTo)
node: quote = TDX quote over report_data
node: 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 }─────────── KMS
  • 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 second report_data half re-binds SHA-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.