Skip to content

Trust Model

Trust in the storage-key flow is bidirectional. Before the KMS releases a node’s storage key, the node proves it runs an approved image inside genuine TDX hardware; and the KMS exposes its own quote so the node can prove the KMS is genuine first. Neither side relies on TLS identity or operator trust alone — the root of trust is the Intel TDX quote signed by the CPU.

A node should confirm it is talking to a genuine KMS of its own release before sending a key request. The KMS provides POST /attest for exactly this: the caller sends a fresh 32-byte nonce, and the KMS returns a TDX quote (from configfs-tsm) whose report_data is nonce ‖ binding (mero-kms/src/handlers/attest.rs). The caller then checks the quote’s signature, the nonce binding, and the KMS’s own measurements against known-good values.

report_data[0..32] = client nonce (freshness)
report_data[32..64] = binding (defaults to SHA-256("mero-kms-attest-v1"))

The same endpoint powers the attestation verifier.

merod enforces it whenever a node is pointed at a release (tee-release-version on the node images): the KMS’s quote must not be a debug TD, and its TCB status and MRTD/RTMR0–3 must be in the kms_allowed_* allowlists of the signed kms-attestation-policy[.<profile>].json in release mero-kms-v<version>. The five registers pin the whole KMS image: MRTD and RTMR0–2 cover firmware, kernel and the dm-verity root hash on the kernel command line, and kms-init extends RTMR3 with the image’s role, profile and root hash at boot. Because the KMS image bakes in its node allowlist, pinning its measurements also pins which nodes it will release keys to.

A node’s storage and data-disk keys are HKDF(root, "{namespace}/{profile}/{peerId}") (mero-kms/src/backend.rs), where root is a random 32-byte value that exists only in the memory of one release’s KMS cluster. So whoever holds the root can derive every node key of that release. The design makes sure nobody does (design, #338):

Property How
The root is born inside a TD The bootstrap replica (kms-bootstrap=true) generates it at boot, in RAM. A replica without kms-bootstrap never generates one, so a partition cannot split the cluster into two roots.
It never leaves a TD except to an identical TD A replica gives the root only to a joiner whose quote carries exactly its own MRTD and RTMR0–3 (“same as me”), is not a debug TD, and has an allowed TCB status. The joiner checks the giver the same way before accepting it. There is no allowlist, config or owner that could admit different code.
It is never written Read-only dm-verity root, /tmp and /var on tmpfs, no swap, no core dumps, volatile journal. There is no backup.
The code that holds it cannot be changed The image is frozen and sealed behind dm-verity; changing anything (including the node allowlist) makes a different image, with different measurements, that the cluster refuses. A bug is fixed by the next release, never patched in place.
No shell The KMS image carries the node image’s locked-read-only lockdown.

Sealed key release protects a key in transit; the transport key it seals under is also derived from the root, so every replica holds the same one. Every upgrade brings a new KMS image, a new cluster and a new root, and retiring a release deletes its cluster, after which no copy of an old node disk can be opened.

What remains trusted is Intel TDX (and Intel PCS collateral), Google’s measured TDX firmware, and the release workflow that builds the image — it defines what “same as me” is. See Security.

Key release (POST /get-key, mero-kms/src/handlers/get_key.rs) never trusts a request without a fresh, signed, hardware-attested proof bound to the node’s peer identity. The steps, in order:

  1. Challenge. The node calls POST /challenge { peerId }. The KMS issues a single-use 32-byte nonce under a challengeId with a TTL (CHALLENGE_TTL_SECS, default 60s), MAC’d with a key derived from the cluster root so any replica can check it, and returns { challengeId, nonceB64, expiresAt }.
  2. Quote + signature. The node generates a TDX quote whose report_data is nonce ‖ SHA-256(peerId), and signs a canonical payload binding the challengeId, nonce, a hash of the quote, and the peerId with its libp2p private key.
  3. Key request. The node calls POST /get-key { challengeId, quoteB64, peerId, peerPublicKeyB64, signatureB64 }.
  4. Challenge redeemed. The challenge is checked (MAC, expiry, peer ID) and entered into the replica’s replay set before any crypto check, so a replay always fails on the second attempt regardless of where the first failed.
  5. Signature check. The KMS confirms the supplied public key derives to the claimed peerId, then verifies the signature over the canonical payload.
  6. Quote check. The quote is verified (DCAP signature), and its report_data must contain the issued nonce and SHA-256(peerId).
  7. Policy check. The quote’s TCB status and all five measurement registers (MRTD, RTMR0–3) are checked against the loaded attestation policy.
  8. Key derivation. On success the KMS derives the key by HKDF from the cluster root on path {KEY_NAMESPACE_PREFIX}/{profile}/{peerId} (default prefix merod/storage) and returns it sealed to the node’s one-time key. Derivation is deterministic — the same node always receives the same key from any replica of its release’s cluster.

Because the profile is part of the derivation path, a debug node and a production node with the same peerId receive different keys even from the same KMS.

The node policy is baked into the KMS image (ALLOWED_* in /etc/mero-kms/kms.env, from the node release’s published-mrtds.json at build time), read at startup, and modelled as (mero-kms/src/policy.rs):

struct AttestationPolicy {
enforce_measurement_policy: bool,
allowed_tcb_statuses: Vec<String>, // normalized lowercase, e.g. ["uptodate"]
allowed_mrtd: Vec<HexMeasurement>,
allowed_rtmr0: Vec<HexMeasurement>,
allowed_rtmr1: Vec<HexMeasurement>,
allowed_rtmr2: Vec<HexMeasurement>,
allowed_rtmr3: Vec<HexMeasurement>,
}

The release publishes the same lists as node_allowed_mrtd, node_allowed_rtmr0..3, and node_allowed_tcb_statuses in kms-attestation-policy[.<profile>].json, for audit; the KMS never fetches them. Each measurement is a 48-byte (96-hex-char) SHA-384 value.

Register What it measures Notes for this system
MRTD SHA-384 of the initial VM image at launch Pins the exact node/KMS image.
RTMR0 Firmware (TDVF) measurement Extended during early boot.
RTMR1 OS kernel / boot measurement Extended during kernel boot.
RTMR2 Application / kernel-cmdline measurement The node image injects calimero.root_hash (a hash of /etc/calimero, baked binaries, and grub config) into the kernel cmdline, measured here (mero-tee/playbook.yml). It is computed at build time and measured as a string, so it names the build. The cmdline also carries the dm-verity root hash: the root filesystem is a read-only EROFS image sealed as the build’s last step, and every block the booted system reads is checked against that hash, so an offline edit of the boot disk is a read error, not different code (#334; verity-root role).
RTMR3 Runtime events after boot Carries the profile/role separation: kms-init (KMS image) and calimero-init (node image) each extend it with role + profile + root hash, fatally on locked-read-only.

Each image profile (debug, debug-read-only, locked-read-only) produces a distinct RTMR3 value. A production KMS policy lists only the production RTMR3, so a debug node’s quote is rejected. The separation is enforced by hardware — there is no software path to forge an RTMR3 value — and reinforced by the profile being baked into the key-derivation path.

Threat Mitigation
Rogue KMS A fake KMS cannot produce a valid quote with the expected KMS measurements; Plane 1 verification via /attest rejects it.
Modified KMS obtaining the root A replica gives the root only to a TD with exactly its own MRTD and RTMR0–3; dm-verity makes an offline edit of the image a read error rather than different code.
Debug TD holding the root Refused in the join whatever it measures as, and a replica refuses to start in one.
Debug KMS sharing the locked KMS’s root Each profile is a different image, so the locked cluster refuses a debug replica’s join.
Genuine cluster with a different root Leaks nothing (the root exists only inside genuine TDs), but nodes pointed at it would lose their data when it stops. The KMS URL comes from metadata, so this is an availability risk, not a confidentiality one.
Rogue / tampered node A node outside TDX cannot produce a valid quote; a tampered image has different MRTD/RTMR values that the policy rejects.
Replay The challenge nonce is single-use and TTL-bound and is redeemed before verification; the /attest nonce prevents quote reuse.
Peer-ID spoofing get-key requires a signature by the key that derives to the claimed peerId, and binds SHA-256(peerId) into the quote’s report_data.
Profile escalation Distinct hardware-measured RTMR3 per profile; the policy rejects any RTMR3 not on its allowlist.
Host reads or edits a node’s disk The data disk is LUKS2 with dm-integrity, keyed by merod kms disk-key from the KMS under the same attestation as the storage key; the disk-unlock identity sits in the LUKS2 header and is useless without a genuine TD. merod’s store keeps its own KMS key on top.
Policy downgrade via metadata tee-release-version is operator-set, and every release’s policy is validly signed. The image bakes its own version as a floor (/etc/calimero/min-tee-release-version → MERO_TEE_MIN_VERSION); calimero-init and merod both refuse an older release.
Policy tampering The KMS’s node allowlist is part of its image and so of its measurements; nothing is fetched at runtime. merod verifies the Sigstore signature of the KMS policy it fetches.

Rejections surface as typed errors — tcb_status_rejected and measurement_policy_rejected return 403, challenge/signature/attestation failures return 401, and a replica without its root returns 503. See error handling for the full mapping.