diff --git a/doc/policy_v2_measurements.md b/doc/policy_v2_measurements.md new file mode 100644 index 000000000..92328cf34 --- /dev/null +++ b/doc/policy_v2_measurements.md @@ -0,0 +1,451 @@ +# MigTD Policy v2 — MRTD / RTMR Measurements (`build-igvm-get-quote`) + +This document summarizes exactly **what is measured into `MRTD` and `RTMR0`–`RTMR3`** +for a MigTD image produced by the `build-igvm-get-quote` target in +[`sh_script/Azure/Makefile`](../sh_script/Azure/Makefile) — i.e. build with the same features as the regular +**Azure** build (IGVM format) with **Policy v2** and real `GetQuote` attestation. + +For each register it describes the measured artifact, *who* measures it +(TDX module, td-shim firmware, or the MigTD runtime), *when*, the hashing +algorithm, and reference / reproduction values. + +> The equivalent **non-IGVM (TDVF `.bin`) Linux build** — `cargo image` without +> `--image-format igvm` — is covered in [§7](#7-the-linux-non-igvm-tdvf-bin-build): +> only the **MRTD derivation** differs; **RTMR0–RTMR3 are identical**. + +--- + +## 1. The build target + +```make +IGVM_FEATURES_BASE = vmcall-raw,stack-guard,main,vmcall-interrupt,oneshot-apic,spdm_attestation +IGVM_FEATURES_GET_QUOTE = $(IGVM_FEATURES_BASE),igvm-attest + +build-igvm-get-quote: + cargo image --no-default-features --features $(IGVM_FEATURES_GET_QUOTE) ... \ + --image-format igvm --output target/release/migtd.igvm --debug \ + --policy-v2 --policy config/templates/policy_v2_signed.json \ + --policy-issuer-chain config/templates/policy_issuer_chain.pem \ + --root-ca config/Intel_SGX_Provisioning_Certification_RootCA_preproduction.cer +``` + +`generate-hash-get-quote` then runs the reference tool +`migtd-hash --policy-v2 --manifest config/Azure/servtd_info.json` to reproduce the +register values offline (see §6). + +### What `--policy-v2` enrolls into the image (CFV) + +The build runs `td-shim-enroll` to place two raw files into the **Configuration +Firmware Volume (CFV)** (see `xtask/src/build.rs:267`): + +| CFV file (FFS GUID) | Content | +| ----------------------------------------------------- | -------------------------------- | +| `0BE92DC3-…-8EEFFD70DE5A` (`MIGTD_POLICY_FFS_GUID`) | `policy_v2_signed.json` | +| `3F2FB27A-…-D3EAB39F8AEB` (`MIGTD_POLICY_ISSUER_CHAIN`) | `policy_issuer_chain.pem` | + +> Note: in Policy v2 the **root CA is *not* enrolled** (the `--root-ca` argument is +> ignored — it is only used by the v1 path). The SGX root CA is delivered at runtime +> through the policy *collaterals* instead, so it is **not** measured into any RTMR. + +--- + +## 2. Measurement model + +A TDX `TDREPORT.TD_INFO` exposes one static register and four runtime registers, +all **SHA-384 (48 bytes)**: + +- **`MRTD`** — build-time measurement of the initial TD memory, finalized by the + **TDX module** at `TDH.MR.FINALIZE` (before the guest runs). It is *not* + runtime-extendable. +- **`RTMR0`–`RTMR3`** — runtime registers extended by guest software via + `TDG.MR.RTMR.EXTEND`. Every extension is also appended to the TDX event log + (CCEL / `TDEL`) so a verifier can replay it. + +**RTMR extend formula** (one event): + +``` +RTMR_new = SHA384( RTMR_old(48B) || SHA384(event_data) ) +``` + +**Event-log `mr_index` → register** mapping used throughout MigTD +(`src/migtd/src/event_log.rs:164`): + +| `mr_index` | 1 | 2 | 3 | 4 | +| ---------- | ----- | ----- | ----- | ----- | +| register | RTMR0 | RTMR1 | RTMR2 | RTMR3 | + +MigTD fills the registers in two stages: + +1. **td-shim firmware** runs first and writes only an `EV_SEPARATOR` into RTMR0/RTMR1. +2. **MigTD core** (`main.rs::do_measurements`, gated by the `policy_v2` feature) + then extends RTMR1 and RTMR2. + +--- + +## 3. Summary + +| Register | Measured content (Policy v2) | Measured by | Stage | +| -------- | ------------------------------------------------------------------------ | ------------------ | --------- | +| `MRTD` | Initial TD image: **td-shim BFV** + **MigTD core Payload** page contents, plus the GPAs of all added private pages. (CFV content **excluded**.) | TDX module (static) | TD build | +| `RTMR0` | One `EV_SEPARATOR` event (`u32` `0x0000_0000`). Nothing else. | td-shim firmware | Boot | +| `RTMR1` | `EV_SEPARATOR`, **then the policy issuer chain** (`policy_issuer_chain.pem`). | td-shim, then MigTD | Boot | +| `RTMR2` | **The migration policy** (`policy_v2_signed.json`). No root CA in v2. | MigTD core | Boot | +| `RTMR3` | *Nothing* — stays all-zero. | — | — | + +--- + +## 4. Per-register detail + +### MRTD — the MigTD firmware identity + +The TDX module measures every private page the VMM adds before launch. The +reference tool reproduces this from the IGVM file +(`TdInfoStruct::build_igvmmrtd`, `deps/td-shim/td-shim-tools/src/tee_info_hash.rs:376`): + +- For **each** non-shared page → `TDH.MEM.PAGE.ADD`: the page **GPA** is hashed + (a 128-byte `"MEM.PAGE.ADD"` + GPA buffer). +- For each **measured** page (not flagged "unmeasured") → `TDH.MR.EXTEND`: the page + **content** is hashed 256 bytes at a time. + +Which sections are content-extended is driven by `config/metadata.json` +(`Attributes = 0x1` ⇒ `PAGE.ADD + MR.EXTEND`): + +| Section | Attributes | In MRTD? | +| --------- | ---------- | ----------------------------------------- | +| `BFV` | `0x1` | ✅ GPA **and** content (td-shim firmware) | +| `Payload` | `0x1` | ✅ GPA **and** content (MigTD core binary) | +| `CFV` | `0x0` | ⚠️ GPA only — **content not extended** | +| `TempMem` | `0x0` | GPA only (pre-added zero pages) | +| `PermMem` | `0x2` | ❌ not measured (PAGE.AUG — accepted dynamically after launch) | + +**Consequence:** `MRTD` is the identity of the td-shim firmware + the MigTD core +code and its fixed memory layout. Because the CFV **content** is excluded, changing +the policy or issuer chain does **not** change `MRTD`. + +### RTMR0 — firmware separator only + +td-shim calls `create_seperator()`, which extends **RTMR0 and RTMR1** with the +digest of the 4-byte value `0x0000_0000` +(`deps/td-shim/cc-measurement/src/log.rs:58`). For MigTD nothing else reaches RTMR0: + +- The TD-HOB is logged into RTMR0 *only if* td-shim consumes one + (`main.rs:131`). MigTD uses a pre-allocated `PermMem` region and consumes no + TD-HOB, so this is skipped. +- The payload binary is *not* re-measured into an RTMR (`payload_extend_rtmr` is + false — the payload is already covered by `MRTD`). + +`RTMR0` is therefore a **constant** for every MigTD build: + +``` +RTMR0 = SHA384( 0x00*48 || SHA384(0x00000000) ) + = 518923B0F955D08DA077C96AABA522B9DECEDE61C599CEA6C41889CFBEA4AE4D50529D96FE4D1AFDAFB65E7F95BF23C4 +``` + +(verified against `config/templates/tcb_mapping.json`). + +### RTMR1 — separator + policy issuer chain + +After the firmware separator, the MigTD core measures the **policy issuer chain** +read from the CFV (`get_policy_issuer_chain_and_measure`, `src/migtd/src/bin/migtd/main.rs:312`) +into `mr_index = 2` (RTMR1), tagged `POLICY_ISSUER_CHAIN`: + +``` +RTMR1 = SHA384( RTMR0_separator(48B) || SHA384(policy_issuer_chain.pem) ) +``` + +This step exists **only** under the `policy_v2` feature. + +### RTMR2 — migration policy + +The MigTD core measures the **migration policy** read from the CFV +(`get_policy_and_measure`, `src/migtd/src/bin/migtd/main.rs:262`) into +`mr_index = 3` (RTMR2), tagged `POLICY`. RTMR2 starts from zero (no separator): + +``` +RTMR2 = SHA384( 0x00*48 || SHA384(policy_v2_signed.json) ) +``` + +> The bytes **extended** are the full signed policy file. The event-log *payload* +> for this entry is just the policy SVN/version string, but the RTMR digest is over +> the whole policy. +> In Policy v1 the root CA would also be extended into RTMR2; in v2 it is not. + +### RTMR3 — unused + +No MigTD measurement targets `mr_index = 4`. `RTMR3` remains all-zero +(`00…00`) in the normal Policy v2 flow. +(The `test_disable_ra_and_accept_all` debug feature is **not** part of this target.) + +--- + +## 5. Reference values for this target + +`RTMR0` is constant. `RTMR1`/`RTMR2` depend on the exact bytes of the enrolled +files, so they are shown here as reproduced from the current +`config/templates/*` artifacts (regenerate whenever the policy or issuer chain +changes): + +| Register | Value | +| -------- | -------------------------------------------------------------------------------------------------- | +| `RTMR0` | `518923B0F955D08DA077C96AABA522B9DECEDE61C599CEA6C41889CFBEA4AE4D50529D96FE4D1AFDAFB65E7F95BF23C4` | +| `RTMR1` | `279EB652F7D7B7D15EA1E593B29EEEB20C6AFD33BE432C66A7B237107A00F5276919AEF490A8DC000886552F79748B0F` | +| `RTMR2` | `07AF01E95CEFCDC4885A5DC5C5BB1CBE05913FD9486BCD1141C195C3C399939D5127F9E0D5F2F0E09D62B571B562EC36` | +| `RTMR3` | `000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000` | +| `MRTD` | build-specific (measures the firmware image; e.g. `E2C7DA7C…` in the committed template). | + +> ⚠️ The `rtmr0`/`rtmr1` placeholders committed in `config/templates/tcb_mapping.json` +> are both `518923B0…`. That is only the firmware-separator stage; the live +> `RTMR1` of a Policy v2 TD additionally includes the issuer chain (as shown above) +> and is what `TdTcbMapping::get_engine_svn_by_report` compares against the TDREPORT +> (`src/policy/src/v2/servtd_collateral.rs:192`). Regenerate the mapping with +> `migtd-hash --policy-v2` before signing a deployable policy. + +The non-register `TD_INFO` fields (`ATTRIBUTES`, `XFAM`, `MRCONFIGID`, `MROWNER`, +`MROWNERCONFIG`) come from the manifest `config/Azure/servtd_info.json`, not from +the measured registers. + +--- + +## 6. Reproducing the values + +```sh +# Build the image (writes target/release/migtd.igvm) +make -C sh_script/Azure build-igvm-get-quote + +# Print MRTD + RTMR0..3 from the image and CFV +cargo run -p migtd-hash -- \ + --image target/release/migtd.igvm \ + --manifest config/Azure/servtd_info.json \ + --policy-v2 --verbose +``` + +The tool's `build_td_info` (`tools/migtd-hash/src/lib.rs:37`) implements exactly the +flow above: `build_igvmmrtd` (MRTD), `build_rtmr_with_seperator(0)` (RTMR0/RTMR1 +seed), then `rtmr1()` (+ issuer chain) and `rtmr2()` (policy) for Policy v2. + +--- + +## 7. The Linux non-IGVM (TDVF `.bin`) build + +The standard Linux / KVM build produces a **TDVF flat image** `target/release/migtd.bin` +instead of an IGVM file — `cargo image` defaults to `--image-format tdvf` +(`xtask/src/build.rs:19,439`): + +```sh +# Non-IGVM build (default format = tdvf) -> target/release/migtd.bin +cargo image --policy-v2 \ + --policy config/templates/policy_v2_signed.json \ + --policy-issuer-chain config/templates/policy_issuer_chain.pem + +# Reproduce MRTD + RTMR0..3 (the .bin extension auto-selects the TDVF path) +cargo hash --image target/release/migtd.bin --policy-v2 +``` + +(`cargo image` / `cargo hash` are aliases for `xtask image` / `xtask hash`; +`hash` supports the same `--policy-v2` flag — `xtask/src/servtd_info_hash.rs:50`.) + +### What changes vs the IGVM build + +| Register | TDVF `.bin` build | Same value as IGVM? | +| ------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | +| `MRTD` | Derived by `TdInfoStruct::build_mrtd` — walks the OVMF GUID table / TDVF metadata in the 16 MB image and replays `TDH.MEM.PAGE.ADD` / `TDH.MR.EXTEND` per `config/metadata.json` | **Derivation differs** — see below | +| `RTMR0` | separator only | ✅ identical (`518923B0…`) | +| `RTMR1` | separator + policy issuer chain | ✅ identical (same `policy_issuer_chain.pem`) | +| `RTMR2` | policy | ✅ identical (same `policy_v2_signed.json`) | +| `RTMR3` | all-zero | ✅ identical | + +- **RTMR0–RTMR3 are unchanged.** The firmware separator, the runtime + `do_measurements` flow, and the CFV content (policy + issuer chain, enrolled + identically by `td-shim-enroll`) are all image-format-independent. In the `.bin` + the CFV is simply the first `TD_SHIM_CONFIG_SIZE` bytes, which the reference tool + reads directly instead of de-duplicating IGVM pages + (`tools/migtd-hash/src/lib.rs:66`). So the §5 values for RTMR0–RTMR3 apply + verbatim to the non-IGVM build. +- **MRTD is the only difference — and it is structural, not content.** For a given + feature set the BFV (td-shim) and Payload (MigTD core) are the *same compiled + binaries* in both formats; only `td-shim-ld -i ` (`xtask/src/build.rs:246`) + packages them differently. `MRTD` is a single SHA-384 over an **ordered** stream of + per-page `TDH.MEM.PAGE.ADD` / `TDH.MR.EXTEND` records (each carries the page GPA, + and for extended pages the 256-byte content). The two formats feed that stream in a + **different order**: + - TDVF `build_mrtd` replays `config/metadata.json` sections in order: + **BFV → Payload → CFV → TempMem**. + - IGVM `build_igvmmrtd` replays the linker's `PageData` directives, emitted as + **CFV → mailbox → temp-stack → temp-heap → Payload → BFV** + (`deps/td-shim/td-shim-tools/src/linker.rs:429`, `build_igvm`). + + BFV is measured *first* under TDVF but *last* under IGVM, so even with byte-identical + firmware/payload content and identical GPAs the two digests differ. `build_td_info` + selects the algorithm from the file extension (`tools/migtd-hash/src/lib.rs:56`). + Always derive `MRTD` from the exact image you deploy. + +> There is no `build-igvm-get-quote` equivalent for `.bin` in `sh_script/Azure/Makefile` +> (that Makefile is IGVM-only). Use `cargo image --policy-v2 …` as shown above, or +> the general build helpers under `sh_script/` (e.g. `build_final.sh`). + +--- + +## 8. Design note: why the MigTD core is in MRTD, not RTMR1 + +A common assumption (true for *generic* td-shim / TDVF images) is: **MRTD measures +only the firmware, then the firmware loads the payload and extends it into RTMR1.** +**This is *not* how MigTD is configured** — for MigTD the core (Payload) is part of +`MRTD`, and td-shim does **not** extend any RTMR with it. + +### What MigTD does + +`config/metadata.json` marks **both** firmware and core sections with the `MR.EXTEND` +attribute `0x1`, so the **VMM / TDX module** measures both into `MRTD` via +`TDH.MEM.PAGE.ADD` + `TDH.MR.EXTEND` during TD build — *before* td-shim runs and +finalizes at `TDH.MR.FINALIZE`. (MRTD is produced by the host/TDX module, not by +td-shim; td-shim is itself one of the measured payloads, and so is the migtd core.) + +| Section | `Attributes` | Lands in | +| ---------------------- | ------------ | -------- | +| `BFV` (td-shim) | `0x1` | `MRTD` | +| `Payload` (MigTD core) | `0x1` | `MRTD` | + +### Why td-shim does not extend RTMR1 with the payload + +td-shim *supports* the "measure payload into RTMR1" behaviour — +`log_payload_binary()` extends the payload blob into `mr_index = 2` → **RTMR1** +(`deps/td-shim/td-shim/src/event_log.rs:76`; `mr_index 2 → RTMR1` via +`deps/td-shim/td-shim/src/bin/td-shim/td/tdx.rs:53`). But it is **gated** on the +payload *not* already being in MRTD (`deps/td-shim/td-shim/src/bin/td-shim/shim_info.rs:91`): + +```rust +// payload_extend_rtmr is true ONLY when the Payload section is not measured into MRTD +if section.r#type == TDX_METADATA_SECTION_TYPE_PAYLOAD && section.attributes == 0 { + payload_extend_rtmr = true; +} +``` + +MigTD's Payload attribute is `0x1` (not `0`), so `payload_extend_rtmr()` is **false** +and the RTMR1 extension is **skipped** — preventing the core from being measured +twice (`src/migtd/src/bin/migtd/main.rs` boot flow calls `log_payload_binary` only +under this flag). + +### Rationale + +1. **MigTD ships as one fixed image.** Firmware + core are bundled into a single + `migtd.bin` / `migtd.igvm`, fully known at build time and placed in guest memory + by the VMM before launch. Measuring the whole image into `MRTD` yields one + **static identity** for the complete MigTD. +2. **The RTMR1-payload model is for *separately-loaded* payloads.** The generic + td-shim path exists when the payload is a distinct, variable artifact loaded at + boot (e.g. a Linux kernel) and therefore absent from the launch image — it *must* + then be measured dynamically into an RTMR. MigTD has no such separation. +3. **It matches ServTD binding.** A user TD binds to MigTD by its measurement + (`SERVTD_HASH` derived from `TD_INFO`); a static `MRTD` that already pins the exact + core is the natural, immutable anchor, independent of runtime ordering. +4. **No double-counting.** The attribute gate guarantees the core is measured into + exactly one register (`MRTD`), never both. + +**Net:** for MigTD, `MRTD` = td-shim (BFV) **+** migtd core (Payload); `RTMR1` = +firmware separator (+ policy issuer chain at runtime under Policy v2). The core is +**never** in `RTMR1`. + +--- + +## 9. Experimental evidence: MRTD covers the MigTD core + +To confirm that the MigTD core (the `Payload` section) really is part of `MRTD` — and +that it is *not* in any RTMR — each image was built **twice** with a single +optimization-proof change to the core, and the registers were compared. The +experiment was run for **both** image formats: the standard KVM TDVF `.bin` build and +the regular Azure IGVM Policy v2 build (`build-igvm-get-quote`). + +**Environment** (both experiments) + +- Commit: `intel/main` @ `e29440454028ea5eab6180e21f521cb9d32e5db6` (clean tree; `HEAD == intel/main`) +- Toolchain: Rust `1.88.0` + +**The change** — identical for both — in the MigTD core, +`src/migtd/src/bin/migtd/main.rs`: + +```diff + fn basic_info() { ++ core::hint::black_box(0xA5A5_5A5A_DEAD_BEEFu64); // temporary marker, reverted after + info!("MigTD Version - {}\n", MIGTD_VERSION); + } +``` + +`core::hint::black_box` forces the constant into the compiled payload, so the core +binary differs by a few bytes while nothing else (firmware, CFV, layout) changes. +**A** = baseline (clean), **B** = with the change, **C** = change reverted. + +### 9.1 Standard KVM build (TDVF `.bin`) + +- Build: `cargo image` → `target/release/migtd.bin` +- Measure: `cargo run -p migtd-hash -- --image target/release/migtd.bin --manifest config/servtd_info.json --output-td-info .json` + +| Build | MRTD | +| ----- | -------------------------------------------------------------------------------------------------- | +| **A** | `560703c6259a4efebf5dc13de6220e0ab2b2b85a838a441a38b0bc6971908d0211a4d0a0398fa440c7310cc5803f37d8` | +| **B** | `71956de175eca87f8b958a8ac283ea8bc45bbd638ecfe0d51818ad8989261912ad2f4eec3aac6974a48ebea14683beaf` | +| **C** | `560703c6259a4efebf5dc13de6220e0ab2b2b85a838a441a38b0bc6971908d0211a4d0a0398fa440c7310cc5803f37d8` | + +RTMRs — **identical** across A, B, C (this default build is the **v1** image, so +`rtmr1` is the separator only and `rtmr2` carries policy + root CA): + +``` +rtmr0 = 518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4 +rtmr1 = 518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4 +rtmr2 = 00738709463174735612b421f112c600a153ad659d54c1ffdfe58967904996a1ef1ed7d130acbee7ea861b70c15454f3 +rtmr3 = 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +``` + +### 9.2 Regular Azure build (IGVM, Policy v2) — `build-igvm-get-quote` + +- Build: `make -C sh_script/Azure build-igvm-get-quote` → `target/release/migtd.igvm` +- Measure: `cargo run -p migtd-hash -- --image target/release/migtd.igvm --manifest config/Azure/servtd_info.json --policy-v2 --output-td-info .json` + +| Build | MRTD | +| ----- | -------------------------------------------------------------------------------------------------- | +| **A** | `582f87da119c826a56af55891450f8e26627114929b137d66b60d951f0b0297fca81421d3818f720852257e790ed5a76` | +| **B** | `c359d539c5e758f6a28b973bc11d2014bc327103a808c699917c2fafe7dc9cd994c1cac7d25a602ec30817a1915bc4b1` | +| **C** | `582f87da119c826a56af55891450f8e26627114929b137d66b60d951f0b0297fca81421d3818f720852257e790ed5a76` | + +RTMRs — **identical** across A, B, C. This is the **Policy v2** image, so `rtmr1` +carries the issuer chain and `rtmr2` the policy — and these match the reference values +computed in §5: + +``` +rtmr0 = 518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4 +rtmr1 = 279eb652f7d7b7d15ea1e593b29eeeb20c6afd33be432c66a7b237107a00f5276919aef490a8dc000886552f79748b0f +rtmr2 = 07af01e95cefcdc4885a5dc5c5bb1cbe05913fd9486bcd1141c195c3c399939d5127f9e0d5f2f0e09d62b571b562ec36 +rtmr3 = 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +``` + +### Conclusion (both builds) + +- **A ≠ B** — a one-line change to the MigTD core changes `MRTD` ⇒ **`MRTD` covers the + MigTD core (Payload)**, not just the td-shim firmware. Holds for the TDVF *and* the + Azure IGVM Policy v2 image. +- **RTMR0–3 unchanged between A and B** ⇒ the core is **not** measured into any RTMR — + the firmware does not extend the payload into RTMR1 (confirming §8). +- **A == C** — reverting restores the exact baseline `MRTD`, confirming the build is + deterministic and the difference was caused solely by the core change. +- The two formats yield **different** `MRTD` baselines (§7); note the `.bin` build here + is release+v1 and the IGVM build is debug+v2, so their absolute `MRTD` values are not + directly comparable — each experiment is self-contained (same format, A vs B). + +--- + +## 10. Source references + +| Concern | Location | +| ----------------------------- | -------------------------------------------------------------- | +| Build target / CFV enrollment | `sh_script/Azure/Makefile`, `xtask/src/build.rs:267` | +| MRTD (IGVM) computation | `deps/td-shim/td-shim-tools/src/tee_info_hash.rs:376` | +| MRTD (TDVF `.bin`) computation | `deps/td-shim/td-shim-tools/src/tee_info_hash.rs:196` | +| IGVM page emission order | `deps/td-shim/td-shim-tools/src/linker.rs:429` (`build_igvm`) | +| RTMR0/RTMR1 separator | `deps/td-shim/cc-measurement/src/log.rs:58` | +| Reference RTMR1/RTMR2 build | `tools/migtd-hash/src/lib.rs:123` (`rtmr1`/`rtmr2`) | +| MRTD format selection | `tools/migtd-hash/src/lib.rs:56` (`build_td_info`) | +| Runtime measurement flow | `src/migtd/src/bin/migtd/main.rs:184` (`do_measurements`, v2) | +| Core in MRTD (Payload attr) | `config/metadata.json` (`Payload` `Attributes 0x1`) | +| Payload→RTMR1 gate | `deps/td-shim/td-shim/src/bin/td-shim/shim_info.rs:91` (`payload_extend_rtmr`) | +| Payload→RTMR1 extension | `deps/td-shim/td-shim/src/event_log.rs:76` (`log_payload_binary`) | +| `mr_index` → RTMR mapping | `src/migtd/src/event_log.rs:164` (`extend_rtmr`) | +| TCB-mapping comparison | `src/policy/src/v2/servtd_collateral.rs:192` | diff --git a/doc/rtmr1_signer_anchor_proposal.md b/doc/rtmr1_signer_anchor_proposal.md new file mode 100644 index 000000000..4fade9f04 --- /dev/null +++ b/doc/rtmr1_signer_anchor_proposal.md @@ -0,0 +1,257 @@ +RTMR1 Signer-Anchor Measurement +================================================ + +> The scope of this proposal is limited to **what MigTD measures into RTMR1**. The +> RTMR2 / TCB-mapping circular-dependency work is covered separately in +> [tcb_mapping_design_proposal.md](./tcb_mapping_design_proposal.md); this proposal +> is the companion change called out there under *"RTMR1 signer anchor for key +> rotation"*. +> For the concrete current measurement values see +> [policy_v2_measurements.md](./policy_v2_measurements.md). + +# Current design — RTMR1 measures the raw issuer cert chain + +Today RTMR1 is, after the firmware boot separator, a **runtime extend over the raw +bytes of the policy/identity issuer certificate chain** (`policy_issuer_chain.pem`), +loaded from the CFV slot `MIGTD_POLICY_ISSUER_CHAIN_FFS_GUID`. The MigTD core measures +it at boot into `mr_index = 2` → RTMR1 (`get_policy_issuer_chain_and_measure`, +`src/migtd/src/bin/migtd/main.rs`; tag `TAGGED_EVENT_ID_POLICY_ISSUER_CHAIN`). The +offline reference tool reproduces it as `rtmr1()` in `tools/migtd-hash/src/lib.rs`. + +``` +RTMR1_1 = SHA384( 0x00*48 || SHA384(separator 0x00000000) ) (td-shim) +RTMR1_final = SHA384( RTMR1_1 || SHA384(issuer_chain_bytes) ) (MigTD core) + ▲ + └── the ENTIRE PEM chain, byte-for-byte +``` + +RTMR1 is part of `TDINFO`, so it folds into `tdinfo_hash = SHA384(TDINFO)` — the value +the TCB-mapping proposal uses as the `svnMappings` key and as the endorsed +`init/cur_servtd_info_hash`. + +**What the chain is actually for.** The chain establishes the *trust anchor* for the +policy/identity signer. At runtime, MigTD-to-MigTD peer validation +(`validate_peer_cert_chain`, `src/crypto/src/lib.rs:290`) enforces only: + +1. the peer chain's internal signatures are valid, +2. **the root CA matches by DER byte comparison**, +3. **the leaf certificate Subject Name matches**, and +4. every issuer in the chain is a CA (non-CA issuers rejected). + +Note what the trust model does **not** require: an identical *leaf certificate* or an +identical *leaf public key*. Two MigTDs trust each other as long as they share the same +root CA and the same leaf Subject — the leaf key may differ. + +``` + Runtime trust model (peer validation) RTMR1 measurement (today) + ─────────────────────────────────── ───────────────────────── + cares about: root DER + leaf Subject hashes: the WHOLE chain + ignores: leaf key, leaf cert bytes (every byte, incl. leaf key) + + ⇒ RTMR1 is far MORE sensitive than the trust model it encodes +``` + +# Problem 1: leaf-key rotation churns RTMR1 + +Issuers rotate the leaf signing key periodically (routine key rotation), issuing a new +leaf certificate under the *same root CA and same leaf Subject*. + +**Peer-to-peer attestation already supports this.** As described above, peer validation +keys on the root CA and leaf Subject — not the leaf key — so old- and new-key builds +interoperate in a rolling deployment (commit `2d238cf3`). + +**The attestation service does not.** Because RTMR1 hashes the *raw chain bytes*, the new +leaf changes RTMR1 → changes `tdinfo_hash`, so each rotation forces: + +- a new MigTD **build** (the rotated chain is baked into the measured image), and +- a new **endorsement** entry keyed on the new `tdinfo_hash`, published to the attestation + service so tenant TDs bound to the rotated MigTD still attest successfully. + +So a rotation the runtime treats as a no-op becomes a build-and-endorsement update the +attestation service must track — deployment complexity for a change that does not touch the +trust anchor. RTMR1 is measuring the wrong granularity: the leaf key, not the trust anchor. + +# Problem 2: regional leaf certificates fragment the RTMR1 measurement + +Independently of the attestation format, the issuer may use a **different leaf certificate +per region** (regional keys / HSMs) while keeping the same root CA and same leaf Subject. +The runtime trust model treats all of these as the *same* anchor. But raw-chain RTMR1 +hashes the exact chain bytes, so each region produces a *different* RTMR1 — and therefore a +different RTMR1 contribution to `tdinfo_hash` — for identical MigTD code and an identical +trust anchor: + +``` + region A leaf ─┐ + region B leaf ─┼─ same root + same leaf Subject, different leaf cert/chain + region C leaf ─┘ + raw chain in RTMR1: 3 different RTMR1 (chain bytes differ per region) + signer anchor: 1 RTMR1 anchor (root + leaf Subject identical) +``` + +So the trust-anchor measurement fragments by region for no trust-relevant reason — each +region needs its own `svnMappings` / endorsement entry even though the MigTD code and the +trust anchor are identical. + +# Problem 3: CoRIM reuse for Azure attestation duplicates the cert chain + +A goal of the TCB-mapping proposal is to make the signed `servtdTcbMapping` reusable +as-is by the tenant attestation service — instead of relying on separate out-of-band +endorsements. To realize that reuse in the **Microsoft Azure** environment, the mapping +must be reformatted as a **CoRIM** endorsement — the endorsement / reference-value format +consumed by the **Microsoft Azure Attestation (MAA)** service. A CoRIM endorsement +**embeds the signer's certificate chain** (the COSE `x5chain` parameter) so a verifier can +establish the signer trust anchor from the artifact itself. + +If RTMR1 *also* folds the raw chain bytes into `tdinfo_hash`, the same chain is carried +twice — once inside the CoRIM, once inside the measurement — and the two copies must be +kept byte-consistent forever (two sources of truth for one signer). + +``` + CoRIM endorsement (signed) RTMR1 → tdinfo_hash + ┌──────────────────────────────┐ ┌──────────────────────────────┐ + │ svnMappings / measurements │ │ SHA384( … || SHA384(chain) ) │ + │ x5chain: [leaf, …, root] ◄───┼── same │ full chain bytes again ◄───┤ + └──────────────────────────────┘ chain └──────────────────────────────┘ + ▲ ▲ + └──── duplicated, must stay in sync ┘ +``` + +# Proposal — measure a stable signer anchor + +Replace the raw-chain RTMR1 extend with an extend over a **signer anchor** `A` that +commits to *exactly the trust-anchor identity the runtime enforces* — the root CA and +the leaf Subject — and nothing else. + +| | Today | Proposed | +|---|-------|----------| +| **RTMR1 extend input** | `SHA384(raw issuer chain PEM bytes)` | `SHA384(A)` where `A` is the signer anchor below | +| **CFV slot `MIGTD_POLICY_ISSUER_CHAIN_FFS_GUID`** | full signing cert chain (unchanged) | full signing cert chain (**unchanged**) | +| **What RTMR1 is sensitive to** | every byte of the chain (incl. leaf key) | root CA DER + leaf Subject DER only | + +The CFV still ships the **full** chain (peer validation and policy/identity signature +verification still need it); only **what is hashed into RTMR1** changes — a small, +stable anchor derived from the chain rather than the chain's raw bytes. + +## RTMR1 signer-anchor formula + +Define `H(x) = SHA384(x)`. + +1. Root component: `R = H(DER(root_certificate))` +2. Leaf-subject component: `S = H(DER(leaf_certificate.tbsCertificate.subject))` +3. Domain-separated anchor: `A = H("MIGTD-RTMR1-ANCHOR-V1" || 0x00 || R || 0x00 || S)` +4. RTMR extend chain: + - `RTMR1_0 = 48-byte zero` + - `RTMR1_1 = H( RTMR1_0 || H(separator_event_payload) )` *(td-shim boot separator, unchanged)* + - `RTMR1_final = H( RTMR1_1 || H(A) )` *(MigTD core, anchor event)* + +`DER(...subject)` is the raw DER encoding of the leaf `tbsCertificate.subject`, used +(rather than a text rendering of the Distinguished Name) to avoid encoding ambiguity. +The `"MIGTD-RTMR1-ANCHOR-V1"` tag provides domain separation and a version hook for +future formula changes. + +# Benefits + +- **No rotation churn** — `A` depends on the root CA and leaf Subject, not the leaf public + key, so a leaf re-issue under the same root + Subject leaves RTMR1 **unchanged**. With + the companion RTMR2 measuring policy without TCBMapping, the whole `tdinfo_hash` is then + unchanged when nothing else changes — a key rotation needs no new endorsement / + `svnMappings` entry. +- **Region-independent measurement** — regional leaf certificates that share the root + + Subject produce the **same** RTMR1, and the same `tdinfo_hash` when nothing else differs, + so one endorsement covers all such regions instead of one per region. +- **No CoRIM duplication** — RTMR1 commits to the *anchor identity* (root + Subject), + not the chain bytes, so the CoRIM remains the single carrier of the full chain. No + two-sources-of-truth synchronization burden. +- **Measurement matches the trust model** — RTMR1's sensitivity becomes exactly that of + `validate_peer_cert_chain` (root DER + leaf Subject). The measured value answers the + same question the runtime asks. +- **Trust-anchor changes stay visible** — changing the **root CA** DER changes `R` and + therefore RTMR1 (intended); only leaf-key churn is decoupled. + +# Design details + +## Alignment with runtime peer validation + +The anchor is the measured projection of the two equality checks already enforced by +`validate_peer_cert_chain` (`src/crypto/src/lib.rs:290`): + +| Peer-validation check | Anchor component | +|-----------------------|------------------| +| Root CA must match (DER byte comparison) | `R = H(DER(root))` | +| Leaf Subject Name must match | `S = H(DER(leaf subject))` | +| Chain internal signatures valid; non-CA issuers rejected | enforced at runtime; not folded into `A` (integrity, not identity) | + +Contrast with `get_policy_signer_key_hash` (`src/crypto/src/lib.rs:105`), which hashes +the **leaf public key** and therefore *does* change on rotation. The anchor deliberately +avoids the leaf key so that rotation is measurement-stable. + +## What changes when the leaf signing key rotates + +Assumption: only the leaf signing key rotates — MigTD code, policy rules, root CA, and +leaf Subject are unchanged. + +| Component | Changes? | Why | +|-----------|----------|-----| +| **MRTD** | No | Cert chain lives in the CFV (unmeasured content of the IGVM image) | +| **RTMR0** | No | MigTD binary code unchanged | +| **RTMR1** | **No** | `A` depends on root DER + leaf Subject DER, not the leaf key | +| **RTMR2** | No¹ | the companion RTMR2 (policy without TCBMapping) is unchanged here | +| **`tdinfo_hash` / endorsement** | No | no register changed, so the hash — and its existing endorsement — still apply | +| **IGVM rebuild** | No | only the CFV leaf cert is swapped (`td-shim-enroll`); the measurement is unchanged | + +¹ RTMR2 is the companion [TCB-mapping proposal](./tcb_mapping_design_proposal.md)'s domain; +this proposal changes only RTMR1. RTMR2 redacts TCBMapping, so rotating the TCBMapping +signing leaf — the trust authority RTMR1 anchors — leaves RTMR2 (and the hash) unchanged. + +## Regional leaf certificates + +Regional leaf certificates are just the spatial version of rotation: every region whose +leaf shares the root + Subject produces the **same RTMR1**, and the **same `tdinfo_hash`** when +nothing else differs. Operators provision region-specific leaf certs into each region's +CFV slot (`MIGTD_POLICY_ISSUER_CHAIN_FFS_GUID`); the authority then publishes **one** +`svnMappings` entry for all of them instead of one per region. Peer migration across +regions passes because the runtime check keys on root + Subject. + +## Boot & offline measurement flow + +- **Boot (MigTD core):** load the chain from CFV → parse the root certificate and the + leaf `tbsCertificate.subject` → compute `R`, `S`, `A` → extend RTMR1 with `H(A)` and + emit one event-log entry. The full chain remains available for signature verification + and peer validation. +- **Offline (`migtd-hash` `rtmr1()`):** compute the identical `A` from the same CFV + chain so the reproduced `tdinfo_hash` matches the running TD. This replaces the current + "extend over raw chain bytes" path. + +# Notes + +- **This proposal changes only RTMR1.** RTMR2 (the policy) is the companion + [TCB-mapping proposal](./tcb_mapping_design_proposal.md)'s concern. Together the two keep + `tdinfo_hash` the same across leaf rotation and across regions whenever the code, policy + content, and trust anchor (root + Subject) are unchanged, while still binding the exact + policy content. (A genuine content change — e.g. re-issuing `servtdIdentity` — does + change RTMR2 and the hash, as intended.) +- **Orthogonal to the TCB-mapping proposal's *Future considerations*.** Both *dropping the + outer policy signature* and *dropping `servtdIdentity`* affect only **RTMR2** (the signed + `policyData`), not RTMR1; this RTMR1-only change is therefore orthogonal to either and can + ship before, after, or without them. +- **Security trade-off — anchor binds identity, not the leaf key.** `A` commits to the + root CA and the leaf Subject, **not** the leaf public key. A leaf key compromised under + the same root + Subject is therefore *not* distinguished by RTMR1 alone. This matches + the existing runtime trust model (which also keys on root + Subject) and pushes + leaf-level revocation to its proper layers: the issuer/root CA's control of issuance, + chain/CRL validation at runtime, and — if a specific build must be revoked — removing + that build's `tdinfo_hash` from `svnMappings[]`. Making the root CA the unit of trust + is the intended, explicit trade-off. +- **Root rotation still visible.** Rotating or adding a *root* CA changes `R` and thus + RTMR1 — intended, since that is a genuine trust-anchor change that should re-endorse. + +# Current RTMR1 implementation (reference) + +| Concern | Location | +|---------|----------| +| RTMR1 runtime extend (raw chain today) | `src/migtd/src/bin/migtd/main.rs` (`get_policy_issuer_chain_and_measure`) | +| `mr_index 2 → RTMR1`, tag id | `src/migtd/src/event_log.rs` (`MR_INDEX_POLICY_ISSUER_CHAIN`, `TAGGED_EVENT_ID_POLICY_ISSUER_CHAIN`) | +| Offline RTMR1 reproduction | `tools/migtd-hash/src/lib.rs` (`rtmr1`) | +| Peer trust model (root DER + leaf Subject) | `src/crypto/src/lib.rs:290` (`validate_peer_cert_chain`) | +| Leaf-public-key hash (changes on rotation; not used by anchor) | `src/crypto/src/lib.rs:105` (`get_policy_signer_key_hash`) | +| CFV slot holding the chain | `MIGTD_POLICY_ISSUER_CHAIN_FFS_GUID` (`src/migtd/src/config.rs`) | diff --git a/doc/tcb_mapping_design_proposal.md b/doc/tcb_mapping_design_proposal.md new file mode 100644 index 000000000..22e541063 --- /dev/null +++ b/doc/tcb_mapping_design_proposal.md @@ -0,0 +1,416 @@ +TCB Mapping Design for One-Hash Endorsement +=================================================== +# Current TCB Mapping inside Policy V2 + + +**Current TCBMapping without full measurement of MigTD and policy in svnMappings** + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Signed Policy Blob │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ policyData │ │ +│ │ ├── policy (migration rules) │ │ +│ │ ├── collaterals (platform TCB info) │ │ +│ │ └── servtdCollateral │ │ +│ │ ├── servtdIdentity {tdIdentity, signature} │ │ +│ │ └── servtdTcbMapping │ │ +│ │ └── svnMappings[]: │ │ +│ │ {[MRTD, RTMR0, RTMR1], isvsvn} │ │ +│ │ ───────────────────── │ │ +│ │ RTMR2, RTMR3 excluded to avoid circularity │ │ +│ └─────────────────────┬─────────────────────────────────────────┘ │ +│ │ │ +│ signature │ │ +└────────────────────────┼────────────────────────────────────────────┘ + │ entire blob measured into + ▼ + ┌─────────────────────┐ + │ RTMR2 │ ← depends on svnMappings content + └─────────────────────┘ (inside the measured blob) + + Result: svnMappings cannot include RTMR2 without creating a + circular dependency, so RTMR2 is excluded — leaving the TCB + mapping unable to fully bind MigTD identity to policy content. +``` + +Policy v2 bundles `{policy, collaterals, servtdCollateral (signed TCB mapping + signed identity)}` into one signed blob that is measured into RTMR2. This creates a **circular dependency**: binding RTMR2 into `svnMappings` requires RTMR2 to be known before the TCB mapping is generated, yet RTMR2 is computed over policy content that already contains that TCB mapping. + +To avoid the cycle, today's `svnMappings` exclude RTMR2 and key only on `[MRTD, RTMR0, RTMR1]`. The signed TCB mapping therefore binds the MigTD code measurement and policy-signer anchor but **not** the policy content measured into RTMR2. This results in two problems described below. + + +# Problem 1: source MigTD cannot map the init hash to an SVN locally + +The source MigTD cannot map `init_servtd_info_hash` (= `SHA384(TDINFO)`) to an SVN directly, so it must accept the full init TDINFO from the untrusted VMM on every request and re-derive the registers after verifying the init TDINFO. + +**Init MigTD (rebinding/migration) TCB evaluation - current svnMappings require init TDINFO from VMM:** + +``` + VMM / Host OS Current MigTD (source) + ┌─────────────────────┐ ┌──────────────────────────────────┐ + │ │ │ │ + │ TDX Module provides│ │ Needs to determine TCB level │ + │ init_servtd_info_ │ │ of init MigTD bound to target │ + │ hash to MigTD │ │ │ + │ (from servtd_ext) │ │ svnMappings only has: │ + │ But svnMappings │ │ {[MRTD, RTMR0, RTMR1], isvsvn}│ + │ uses [MRTD,RTMR0, │ │ │ + │ RTMR1] not full │ │ Cannot derive [MRTD, RTMR0, │ + │ tdinfo_hash │ │ RTMR1] from init_servtd_info_ │ + │ │ │ hash alone! │ + │ │ │ │ + │ ┌───────────────┐ │ per-request │ │ + │ │ init TDINFO │──┼──────────────►│ Verify: │ + │ │ (full struct) │ │ VMM carries │ SHA384(TDINFO) == │ + │ └───────────────┘ │ untrusted │ init_servtd_info_hash? ✓ │ + │ │ input │ │ + │ │ │ Extract [MRTD, RTMR0, RTMR1] │ + │ │ │ from verified TDINFO │ + │ │ │ │ │ + │ │ │ ▼ │ + │ │ │ Look up svnMappings → isvsvn │ + └─────────────────────┘ └──────────────────────────────────┘ + + Problem: VMM must supply full init TDINFO struct on every migration + request. MigTD verifies it against init_servtd_info_hash, then + extracts individual registers to look up SVN. This adds: + - VMM implementation complexity (carry and supply TDINFO per request) + - Larger untrusted input surface per migration handshake +``` + +# Problem 2: attestation service cannot match the info hash to svnMappings + +The tenant TD attestation service holds only `init/cur_servtd_info_hash` (hashes over *all* registers) and cannot match them against the subset-keyed `svnMappings`, forcing reliance on separate hash-based endorsements. + + +**Tenant TD attestation — current svnMappings not useful:** + +``` + TD Quote (authenticated by QE signature) + ┌──────────────────────────────────────────────────────────┐ + │ tdinfo │ + │ ├── MRTD, RTMR0, RTMR1, RTMR2, RTMR3, ... │ + │ └── Servtd_hash = SHA384(SERVTD_EXT_STRUCT) ───────┐ │ + └──────────────────────────────────────────────────────┼───┘ + │ + SERVTD_EXT_STRUCT (carried alongside quote) │ + ┌──────────────────────────────────────────────────┐ │ + │ init_servtd_info_hash (48 bytes) │◄──┘ authenticated + │ init_servtd_attr │ by Servtd_hash + │ cur_servtd_info_hash (48 bytes) │ + │ cur_servtd_attr │ + └──────────────┬──────────────────┬────────────────┘ + │ │ + ▼ ▼ + init_servtd_info_hash cur_servtd_info_hash + = SHA384(init TDINFO) = SHA384(cur TDINFO) + │ │ + ▼ ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ Attestation Service │ + │ │ + │ Has: init_servtd_info_hash, cur_servtd_info_hash │ + │ (single hashes of full TDINFO including ALL registers) │ + │ │ + │ svnMappings provides: │ + │ {[MRTD, RTMR0, RTMR1], isvsvn} │ + │ ───────────────────────────── │ + │ Incomplete! Missing RTMR2, RTMR3. │ + │ │ + │ ✗ Cannot match init/cur_servtd_info_hash against │ + │ svnMappings — the hash covers ALL registers but │ + │ svnMappings only lists a subset. │ + │ │ + │ ✗ Cannot reconstruct tdinfo_hash from partial registers │ + │ without knowing RTMR2 (which svnMappings excludes). │ + │ │ + │ → Must rely on separate endorsements (CoRIM) that │ + │ directly map tdinfo_hash → SVN, bypassing svnMappings. │ + └──────────────────────────────────────────────────────────────┘ +``` + +*Note:* `SERVTD_EXT_STRUCT` is constructed by the TDX module at runtime using the tenant's TDCS and is not directly included in the TD report. Its hash, `SHA384(SERVTD_EXT_STRUCT)`, is included as `tdinfo.Servtd_hash`. The structure is read by the host OS and supplied to the Quoting service (QTD/QE), which verifies it against the hash and includes it in the TD Quote. MigTD can also read it from the bound target tenant TD's TDCS and use the hash to verify the tdinfo from VMM. + +```rust +struct ServtdExt { + init_servtd_info_hash: [u8; 48], + init_servtd_attr: [u8; 8], + reserved: [u8; 8], + init_cpusvn: [u8; 16], + init_tee_tcb_svn: [u8; 16], + init_tee_model: [u8; 12], + reserved1: [u8; 4], + cur_servtd_info_hash: [u8; 48], + cur_servtd_attr: [u8; 8], + reserved2: [u8; 104], +} +``` + +# Proposal + + +Break policy content into independent measured components so RTMR2 no longer depends on TCB mapping content: + +**Measurement register layout** (RTMR extends): + +| Register | Before | Proposed | +|----------|--------|----------| +| **RTMR1** | Policy issuer cert chain | TCBMapping issuer cert chain | +| **RTMR2** | Signed policy blob (contains policy rules + collaterals + signed TCB mapping + signed identity) | **Single canonical-bytes extend** of `policyData` with `servtdCollateral.servtdTcbMapping` removed. By construction this binds every other top-level `policyData` field — `version`, `id`, `policySvn`, `policy`, `forwardPolicy`, `backwardPolicy`, `collaterals`, and the rest of `servtdCollateral` (including the issuer-signed `{tdIdentity, signature}` and both issuer chains). See "RTMR2 single redacted extend" below. | + +**IGVM CFV file layout** (configuration firmware volume slots loaded at boot): + +| CFV slot | Before | Proposed | Measured into | +|----------|--------|----------|---------------| +| `MIGTD_POLICY_ISSUER_CHAIN_FFS_GUID` | Policy issuer cert chain | TCBMapping issuer cert chain | **RTMR1** | +| `MIGTD_POLICY_FFS_GUID` | Signed policy with collaterals | Signed policy with collaterals, updated `svnMappings` semantics | **RTMR2** | + +With this split: +- RTMR2 = measurement of canonical `policyData` with `servtdCollateral.servtdTcbMapping` redacted — every other field is automatically bound by being inside the canonical object. The redaction is the only escape hatch and permits `servtdTcbMapping` to be re-signed after the IGVM is shipped, preserving circularity-freedom. +- TCB mapping can bind `tdinfo_hash` (= `init_servtd_info_hash` = `SHA384(TDINFO)` for attr=0) to SVN without circularity. (See "Schema note" at the end.) +- RTMR1 = measurement of TCB Mapping issuer cert chain instead of policy issuer chain. +- Policy is still signed to keep current file format not changed, but signing is not required as TCBMapping now includes and authenticates the policy measurement. + +**New design — full tdinfo hash in svnMappings but unmeasured, removing circular dependency:** + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ Signed Policy Blob │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ policyData │ │ +│ │ ├── policy, version, id, policySvn, collaterals, ... │ │ +│ │ └── servtdCollateral │ │ +│ │ ├── servtdIdentity {tdIdentity, signature} ──┐ │ │ +│ │ ├── servtdIdentityIssuerChain │ measured │ │ +│ │ ├── servtdTcbMappingIssuerChain │ │ │ +│ │ └── servtdTcbMapping ◄────── NOT measured ──┐ │ │ │ +│ │ └── svnMappings[]: │ │ │ │ +│ │ {tdinfo_hash, isvsvn} │ │ │ │ +│ └───────────────────────────────────────────────────┼─┼────────────┘ │ +│ signature ─────────────────────────────────────────┼─┤ │ +└──────────────────────────────────────────────────────┼─┼───────────────┘ + │ │ + RTMR2 = SHA384(canonical(policyData │ │ + minus servtdTcbMapping)) ◄──────┘ │ + │ │ + ┌──────────┼──────────────────┐ │ + │ ▼ │ │ + │ tdinfo_hash = SHA384(TDINFO)│ │ + │ MRTD, RTMR0, RTMR1, RTMR2 │ │ + └──────────┬──────────────────┘ │ + │ │ + ▼ │ + svnMappings[].tdinfo_hash ─────────────────┘ + populated AFTER + measurement + (no circularity) + +``` + + +# Benefits + +- **Breaks the circular dependency** — `tdinfo_hash` is computable from build inputs before the TCB mapping is signed. +- **Problem 1 solved with simpler rebind/migration** — MigTD maps `servtd_ext.init_servtd_info_hash` to an SVN locally; the VMM no longer supplies init TDINFO per request. +- **Problem 2 solved with TCB Mapping reused for attestation** — the service matches `init/cur_servtd_info_hash` directly against `svnMappings`, needing no out-of-band endorsements. + +# Design details + +## RTMR2 single redacted extend + +RTMR2 is extended **once** with the canonical bytes of `policyData` with +`servtdCollateral.servtdTcbMapping` removed. Every other `policyData` field +— including `version`, `id`, `policySvn`, `policy`, `forwardPolicy`, +`backwardPolicy`, `collaterals`, and the rest of `servtdCollateral` +(`majorVersion`, `minorVersion`, the issuer-signed +`{tdIdentity, signature}` object, `servtdIdentityIssuerChain`, +`servtdTcbMappingIssuerChain`) — is bound into RTMR2 by virtue of being +inside the canonical object bytes. The redaction is the only escape hatch +and is what makes `servtdTcbMapping` updateable after the IGVM is shipped. + +This single extend folds together two security properties: +detecting drift between the bytes that were signed and the bytes loaded +into the running MigTD (covered by canonicalizing the whole `policyData` +sub-tree), and — whenever `servtdIdentity` is used for policy — defeating +its playback / TCB-downgrade attacks (covered by including +`servtdCollateral.servtdIdentity` in that sub-tree; see below). + +**`servtdIdentity` — measured for free, retained for compatibility:** + +- `servtdIdentity.tcbLevels` is an optional enrichment layer on top of the `tdinfo_hash → SVN` mapping: it translates a resolved SVN into a `tcbStatus` / `tcbDate`, enabling richer recovery policy (status labels, date thresholds, per-SVN revocation) that pure SVN ordering cannot express. The core identity and anti-downgrade guarantee comes from the TCB mapping and holds with or without it. +- **Initial implementation:** retain `servtdIdentity` unchanged so existing `tcbDate` / `tcbStatus` policies keep working; it is measured into RTMR2 for free by the redacted-`policyData` extend — no extra code, tag, or event-log entry. +- **Must be measured whenever used:** unmeasured, an attacker could boot a peer with an obsolete-but-still-signed `servtdIdentity` and present revoked SVNs as `UpToDate` (playback / downgrade). Measured, a different `servtdIdentity` yields a different `tdinfo_hash` that falls outside the authority's `svnMappings`, so migration fails closed. + +**Why include the signature too:** + +- Hash scope = **full canonical `policyData` minus `servtdTcbMapping`**, which includes the `{tdIdentity, signature}` object verbatim (canonical bytes, sorted keys, no whitespace). +- Including the signature means that **any** authority re-signing event (even of byte-identical content) changes RTMR2. This is intentional: operators must re-release the MigTD image whenever the issuer re-issues `servtdIdentity`, and `svnMappings[]` for the new image must be re-computed by the authority. This eliminates ambiguity over "which issuance is bound here". + +**Why `servtdTcbMapping` is the only redacted field:** + +- Measuring it would defeat the entire purpose of the proposal: `servtdTcbMapping` carries `svnMappings[].tdinfo_hash` (which is what `tdinfo_hash` itself derives from), and so binding it back into RTMR2 would re-introduce the circular dependency. +- The redaction is also what enables the authority to re-issue `servtdTcbMapping` (adding/removing `svnMappings[]` entries, bumping `nextUpdate`, etc.) without forcing a new IGVM release. Operators just swap the signed TCB mapping artifact alongside the existing IGVM. + +**Why measure by construction:** + +- The single redacted-`policyData` extend automatically binds every top-level `policyData` field, including any added in the future, without requiring an explicit whitelist update. +- Both issuer chains are covered for free: `servtdIdentityIssuerChain` and `servtdTcbMappingIssuerChain`. An attacker who could substitute either chain could weaponise it to validate an arbitrary identity or mapping; this scheme rules that out by construction. +- Optional blocks (`forwardPolicy` / `backwardPolicy`) are covered the same way — no separate extend, no separate tag, no separate event-log entry. + +**Alternatives considered** + +| Scheme | Result | Why chosen / rejected | +|--------|--------|-----------------------| +| **Single canonical extend over `policyData` with `servtdTcbMapping` redacted** *(chosen)* | One RTMR2 extend, one tag, one event-log entry. | Breaks the circular dependency by redacting exactly the field that contains `tdinfo_hash`; binds every other field by construction. | +| **Per-field extends** | N RTMR2 extends, each with own tag and event-log entry. | Requires discipline to add a new extend for every new `policyData` field — easy to forget, silently leaving fields unmeasured. Rejected. | +| **Single extend over raw (non-canonical) bytes** | One extend, no canonicalization. | Brittle: any whitespace or key-order difference between policy generator, CFV, and offline hash tool produces a different digest. Rejected. | +| **Single canonical extend over full `policyData` (no redaction)** | One extend covering `servtdTcbMapping` too. | Re-introduces the circular dependency. Rejected. | + +## Build flow + +The release artifact is produced in two stages: a build stage that compiles the MigTD binary into a *base IGVM* with a dummy CFV, and a release stage that signs the policy artifacts and enrolls the production bytes into the base IGVM's CFV via `td-shim-enroll` (a byte-level FFS slot replacement — no Rust rebuild). + +1. **Build stage — base IGVM.** Compile MigTD and embed a dummy CFV containing the same canonical `policyData` content the final policy will carry, so the single redacted-`policyData` RTMR2 extend matches the final image byte-for-byte. The production signing chain is also enrolled into the `MIGTD_POLICY_ISSUER_CHAIN` CFV slot so RTMR1 already matches the final IGVM. The embedded `servtdIdentity` is signed by an ephemeral build-time key (the build environment has no access to production signing). This yields the base IGVM and a *preview* `tdinfo_hash`. + +2. **Release stage — pre-final IGVM (CFV swap).** Re-sign `servtdIdentity` under production signing. Assemble a *pre-final* `policyData` with an empty `servtdTcbMapping` sentinel (the redacted RTMR2 extend ignores this field). Run `td-shim-enroll` to overwrite the CFV slots. Measure the re-enrolled binary to obtain the production `tdinfo_hash`. + +3. **Release stage — TCB mapping.** Create `svnMappings: [{tdinfo_hash, isvsvn}]` using the production `tdinfo_hash`, then sign the TCB mapping. + +4. **Release stage — final IGVM.** Assemble the final signed policy (now including the signed TCB mapping) and re-run `td-shim-enroll`. Verify its `tdinfo_hash` equals the pre-final value — a CI gate enforcing the "`tcbMapping` is not measured" invariant. + +5. **Endorsements.** Compute endorsed `tdinfo_hash` (= `init_servtd_info_hash` = `SHA384(TDINFO)`) from the final image. This hash captures policy content (via the single RTMR2 extend). + +## Init_servTD verification - how problem 1 solved + +With `svnMappings` keyed on the full `tdinfo_hash`, the source MigTD maps `servtd_ext.init_servtd_info_hash` to an SVN entirely from its locally-measured TCB mapping — the VMM no longer supplies the init TDINFO struct per request. + +**Init MigTD (rebinding/migration) TCB evaluation — proposed svnMappings need no TDINFO from VMM:** + +``` + VMM / Host OS Proposed MigTD (source) + ┌─────────────────────┐ ┌──────────────────────────────────┐ + │ │ │ │ + │ TDX Module provides│ │ Needs to determine TCB level │ + │ init_servtd_info_ │ │ of init MigTD bound to target │ + │ hash to MigTD │ │ │ + │ (from servtd_ext) │ │ svnMappings now keyed on full │ + │ │ no per- │ tdinfo_hash: │ + │ │ request │ {tdinfo_hash, isvsvn} │ + │ (no init TDINFO │ TDINFO │ │ + │ struct needed) │──────────────►│ Direct lookup: │ + │ │ │ init_servtd_info_hash == │ + │ │ │ svnMappings[].tdinfo_hash? ✓ │ + │ │ │ │ │ + │ │ │ ▼ │ + │ │ │ → isvsvn │ + │ │ │ (no VMM input, no register │ + │ │ │ re-derivation) │ + └─────────────────────┘ └──────────────────────────────────┘ + + Result: MigTD maps init_servtd_info_hash → SVN from its locally-measured + TCB mapping. The VMM supplies nothing per request, removing the + untrusted-input surface and VMM implementation complexity. +``` + + +## Attestation verification - how problem 2 solved + +The attestation service receives the Tenant TD Quote, which includes for each bound MigTD: + +* `init_migtd_hash` ← `servtd_ext.init_servtd_info_hash` — the hash of the MigTD originally bound to the tenant TD. +* `cur_migtd_hash` ← `servtd_ext.cur_servtd_info_hash` — the hash of the currently bound MigTD. + +Both values are authenticated by `tdinfo.Servtd_hash` (the `SHA384(SERVTD_EXT_STRUCT)` carried in the quote). + +The service consults two signed endorsement artifacts: + +1. **Authorization endorsement** (`servtd_info_hash → SVN`) — translates `init_migtd_hash` and `cur_migtd_hash` into `init_migtd_svn` and `cur_migtd_svn`. Cumulative across releases — must include historical entries so past `init_migtd_hash` values still resolve. + +2. **Trust / baseline endorsement** — declares the minimum acceptable MigTD SVN. The service evaluates **both** initial and current bound MigTDs against this baseline (`init_migtd_svn >= min_migtd_svn` and `cur_migtd_svn >= min_migtd_svn`). A failure on either fails the attestation — catching both "originally bound to a now-revoked MigTD" and "currently bound to an out-of-date MigTD" cases. + +**Proposed tenant TD attestation — self-contained reverse lookup:** + +``` + TD Quote (authenticated by QE signature) + ┌──────────────────────────────────────────────────────────┐ + │ tdinfo │ + │ └── Servtd_hash = SHA384(SERVTD_EXT_STRUCT) ───────┐ │ + └──────────────────────────────────────────────────────┼───┘ + │ + SERVTD_EXT_STRUCT (carried alongside quote) │ + ┌──────────────────────────────────────────────────┐ │ + │ init_servtd_info_hash ─────────────────────┐ │◄──┘ authenticated + │ cur_servtd_info_hash ──────────────────┐ │ │ by Servtd_hash + └───────────────────────────────────────────┼──┼───┘ + │ │ + ▼ ▼ + ┌───────────────────────────────────────────────────────────────────┐ + │ Attestation Service │ + │ │ + │ Step 1: Authorization endorsement (svnMappings in TCB mapping) │ + │ ┌─────────────────────────────────────────────────────────────┐ │ + │ │ svnMappings[]: │ │ + │ │ {tdinfo_hash: "abc123...", isvsvn: 3} │ │ + │ │ {tdinfo_hash: "def456...", isvsvn: 2} ← historical │ │ + │ │ {tdinfo_hash: "ghi789...", isvsvn: 1} ← historical │ │ + │ │ │ │ + │ │ ✓ Direct lookup: │ │ + │ │ init_servtd_info_hash == tdinfo_hash? → init_migtd_svn │ │ + │ │ cur_servtd_info_hash == tdinfo_hash? → cur_migtd_svn │ │ + │ └─────────────────────────────────────────────────────────────┘ │ + │ │ │ + │ ▼ │ + │ Step 2: Trust baseline endorsement │ + │ ┌─────────────────────────────────────────────────────────────┐ │ + │ │ min_migtd_svn = 2 │ │ + │ │ │ │ + │ │ init_migtd_svn >= min_migtd_svn? (e.g. 3 >= 2 ✓) │ │ + │ │ cur_migtd_svn >= min_migtd_svn? (e.g. 3 >= 2 ✓) │ │ + │ │ │ │ + │ │ Both pass → attestation succeeds │ │ + │ │ Either fails → attestation denied │ │ + │ └─────────────────────────────────────────────────────────────┘ │ + └───────────────────────────────────────────────────────────────────┘ + + Key improvement: svnMappings now uses tdinfo_hash (= SHA384(full TDINFO)) + as the lookup key. The attestation service matches init/cur_servtd_info_hash + directly against svnMappings — no out-of-band endorsements needed. +``` + +This design enables self-contained reverse lookup: the attestation service can derive MigTD identity and trustworthiness entirely from the `tdinfo_hash` → SVN mapping and the trust baseline, without requiring additional out-of-band endorsements. + +# Future considerations + +These items are out of scope for the circular-dependency fix above but are enabled by it. + +## Mig-NRX support + +In NRX arch, `SERVTD_EXT.{INIT,CUR}_INFO_HASH` will measure the policy only, so we just need to align the `tdinfo_hash` in svnMappings by redefining it as the hash of the policy only. + +## Dropping `servtdIdentity` (pure-SVN policy) + +If migration policy is expressed purely as SVN comparisons, `servtdIdentity` can be dropped: the peer's SVN is derived solely from the TCB mapping (`tdinfo_hash → SVN`), independent of `servtdIdentity`. Trade-off: loses the `tcbStatus` / `tcbDate` axes and non-monotonic per-SVN revocation (mark SVN N `Revoked` while keeping N−1), and requires SVN monotonicity ("higher SVN ≥ as trustworthy"); build-specific revocation still works by removing that build's `tdinfo_hash` entry from the mapping. + +## Dropping the outer policy signature + +Once RTMR2 binds the canonical `policyData` content directly (this proposal), the outer policy-blob signature is redundant for integrity: the hardware-rooted RTMR2 measurement already authenticates the exact bytes loaded into MigTD. A future revision can drop policy signing entirely, removing the policy-signing key and its rotation burden. The issuer signatures on `servtdTcbMapping` and `servtdIdentity` still remain — those artifacts are redacted/updateable and are verified by their own issuer chains, not by RTMR2. + +## RTMR1 signer anchor for key rotation + +Today RTMR1 measures the full issuer cert chain, so any leaf re-issuance (e.g. routine key rotation) changes RTMR1 — and therefore `tdinfo_hash` — forcing a new `svnMappings` entry and IGVM release per rotation. A future change can measure a stable *signer anchor* instead of the raw chain bytes — e.g. RTMR1 = `SHA384(root-CA identity || leaf subject)` rather than the DER chain — so rotating the leaf key while keeping the same root and subject leaves RTMR1 (and `tdinfo_hash`) unchanged, decoupling key rotation from measurement churn. + +# Schema note — flat `tdinfo_hash` vs measurement registers (MRs) + +Throughout this document `svnMappings[]` entries are written in the flattened form `{tdinfo_hash, isvsvn}` for readability. In the actual CoRIM/`policyData` schema the measurement is nested under `tdMeasurements` (e.g. `svnMappings[].tdMeasurements.tdinfo_hash`, see `src/policy/src/v2/servtd_collateral.rs`), and `tdMeasurements` is the place that can also carry the individual measurement registers / MRs (`MRTD`, `RTMR0`–`RTMR3`). This proposal keys the mapping on the single composite `tdinfo_hash` (= `SHA384(TDINFO)`, which already folds in all MRs) rather than the per-register subset used today; the implementation should populate `tdMeasurements.tdinfo_hash` accordingly. + +# MRTD / RTMR measurements -current implementation + +| Register | Measured content (Policy v2) | Measured by | Stage | +| -------- | ------------------------------------------------------------------------ | ------------------ | --------- | +| `MRTD` | Initial TD image: **td-shim BFV** + **MigTD core Payload** page contents, plus the GPAs of all added private pages. (CFV content **excluded**.) | TDX module (static) | TD build | +| `RTMR0` | One `EV_SEPARATOR` event (`u32` `0x0000_0000`). Nothing else. | td-shim firmware | Boot | +| `RTMR1` | `EV_SEPARATOR`, **then the policy issuer chain** (`policy_issuer_chain.pem`). | td-shim, then MigTD | Boot | +| `RTMR2` | **The migration policy** (`policy_v2_signed.json`). No root CA in v2. | MigTD core | Boot | +| `RTMR3` | *Nothing* — stays all-zero. | — | — | + +See [policy_v2_measurements.md](./policy_v2_measurements.md) for details. \ No newline at end of file