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.
Plane 1 — the node verifies the KMS
Section titled “Plane 1 — the node verifies the KMS”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.
The derivation root: the cluster root
Section titled “The derivation root: the cluster root”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.
Plane 2 — the KMS verifies the node
Section titled “Plane 2 — the KMS verifies the node”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:
- Challenge. The node calls
POST /challenge { peerId }. The KMS issues a single-use 32-byte nonce under achallengeIdwith 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 }. - Quote + signature. The node generates a TDX quote whose
report_dataisnonce ‖ SHA-256(peerId), and signs a canonical payload binding thechallengeId, nonce, a hash of the quote, and thepeerIdwith its libp2p private key. - Key request. The node calls
POST /get-key { challengeId, quoteB64, peerId, peerPublicKeyB64, signatureB64 }. - 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.
- Signature check. The KMS confirms the supplied public key derives to the
claimed
peerId, then verifies the signature over the canonical payload. - Quote check. The quote is verified (DCAP signature), and its
report_datamust contain the issued nonce andSHA-256(peerId). - Policy check. The quote’s TCB status and all five measurement registers (MRTD, RTMR0–3) are checked against the loaded attestation policy.
- Key derivation. On success the KMS derives the key by HKDF from the
cluster root on path
{KEY_NAMESPACE_PREFIX}/{profile}/{peerId}(default prefixmerod/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 attestation policy
Section titled “The attestation policy”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.
What each measurement pins
Section titled “What each measurement pins”| 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. |
RTMR3 profile cohort separation
Section titled “RTMR3 profile cohort separation”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 mitigations
Section titled “Threat mitigations”| 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.