From 8166a75f1221109e8fdfb0449b72dcb04338b161 Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Wed, 24 Jun 2026 06:06:17 +0000 Subject: [PATCH 1/8] docs: add proposal for new tcbmapping design Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Haitao Huang --- doc/policy_v2_measurements.md | 451 +++++++++++++++++++++++++++++ doc/tcb_mapping_design_proposal.md | 416 ++++++++++++++++++++++++++ 2 files changed, 867 insertions(+) create mode 100644 doc/policy_v2_measurements.md create mode 100644 doc/tcb_mapping_design_proposal.md 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/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 From a32649f00ab9ec307bcbacc4adfbf3c150e0c2d9 Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Wed, 24 Jun 2026 21:56:49 +0000 Subject: [PATCH 2/8] docs: add RTMR1 signer-anchor measurement proposal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Haitao Huang --- doc/rtmr1_signer_anchor_proposal.md | 257 ++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 doc/rtmr1_signer_anchor_proposal.md 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`) | From a1092a742731702c28167d63c640343544f300f8 Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Thu, 2 Jul 2026 20:07:50 +0000 Subject: [PATCH 3/8] doc: drop outer policy signing and NRX from TCB mapping proposal Promote the outer policy-blob signature removal from a future consideration into the main proposal (agreed with maintainers): RTMR2 authenticates policyData content directly, so no policy-signing key is kept. servtdTcbMapping and servtdIdentity retain their own issuer signatures. Also remove the Mig-NRX future-support section (out of scope) and add an implementation note that the new design replaces the current implementation without feature gating. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Haitao Huang --- doc/tcb_mapping_design_proposal.md | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/doc/tcb_mapping_design_proposal.md b/doc/tcb_mapping_design_proposal.md index 22e541063..756b4a7a7 100644 --- a/doc/tcb_mapping_design_proposal.md +++ b/doc/tcb_mapping_design_proposal.md @@ -163,19 +163,19 @@ Break policy content into independent measured components so RTMR2 no longer dep | 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** | +| `MIGTD_POLICY_FFS_GUID` | Signed policy with collaterals | Policy with collaterals (no outer signature), 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. +- The **outer policy-blob signature is removed**: RTMR2 measures the canonical `policyData` bytes directly, so the hardware-rooted measurement — not a policy-signing key — authenticates exactly what MigTD loads. This eliminates the policy-signing key and its rotation burden. `servtdTcbMapping` and `servtdIdentity` retain their own issuer signatures, verified by their issuer chains (not by RTMR2), which keeps `servtdTcbMapping` updateable after ship. **New design — full tdinfo hash in svnMappings but unmeasured, removing circular dependency:** ``` ┌────────────────────────────────────────────────────────────────────────┐ -│ Signed Policy Blob │ +│ Policy Blob (no outer signature) │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ policyData │ │ │ │ ├── policy, version, id, policySvn, collaterals, ... │ │ @@ -187,7 +187,6 @@ With this split: │ │ └── svnMappings[]: │ │ │ │ │ │ {tdinfo_hash, isvsvn} │ │ │ │ │ └───────────────────────────────────────────────────┼─┼────────────┘ │ -│ signature ─────────────────────────────────────────┼─┤ │ └──────────────────────────────────────────────────────┼─┼───────────────┘ │ │ RTMR2 = SHA384(canonical(policyData │ │ @@ -213,9 +212,14 @@ With this split: - **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. +- **No policy-signing key** — the outer policy-blob signature is removed; RTMR2 authenticates the policy content directly, so there is no policy-signing key to manage or rotate. `servtdTcbMapping` and `servtdIdentity` keep their own issuer signatures. # Design details +## Implementation note + +To minimize code branching, the proposed design **replaces** the current implementation outright — it is **not** feature-gated. No runtime or build-time toggle is kept between the old subset-keyed (`[MRTD, RTMR0, RTMR1]`) scheme and the new full-`tdinfo_hash` scheme; the new behavior is the only path. + ## RTMR2 single redacted extend RTMR2 is extended **once** with the canonical bytes of `policyData` with @@ -229,7 +233,7 @@ 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 +detecting drift between the authority-endorsed `policyData` bytes 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 @@ -268,7 +272,7 @@ its playback / TCB-downgrade attacks (covered by including ## 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). +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 issuer collateral (`servtdIdentity` and `servtdTcbMapping`) 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`. @@ -276,7 +280,7 @@ The release artifact is produced in two stages: a build stage that compiles the 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. +4. **Release stage — final IGVM.** Assemble the final 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). @@ -383,18 +387,10 @@ This design enables self-contained reverse lookup: the attestation service can d 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. From bc75707c54969a49581f57d904767cf4bf524de9 Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Thu, 2 Jul 2026 20:21:48 +0000 Subject: [PATCH 4/8] doc: TCBMapping issuer chain is not measured into RTMR2 Redact servtdTcbMappingIssuerChain from the RTMR2 canonical extend alongside servtdTcbMapping. The chain is redacted only because it is already measured into RTMR1, so measuring it again in RTMR2 would be redundant -- not to make it hash-neutral: swapping the chain still changes RTMR1 and therefore tdinfo_hash, and hash-neutral chain/leaf-key rotation depends on the separate RTMR1 signer-anchor proposal. Only the servtdTcbMapping *content* (measured nowhere) is re-issuable without touching the hash. The TCB-mapping signer remains anchored by RTMR1 (unchanged), which measures the TCBMapping issuer cert chain. Update the measurement diagram, RTMR2 tables, redacted-extend rationale, alternatives table, and build flow accordingly. Also simplify the outer-policy-signature wording and drop the redundant "No policy-signing key" benefit bullet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Haitao Huang --- doc/tcb_mapping_design_proposal.md | 84 ++++++++++++++---------------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/doc/tcb_mapping_design_proposal.md b/doc/tcb_mapping_design_proposal.md index 756b4a7a7..69b0b1718 100644 --- a/doc/tcb_mapping_design_proposal.md +++ b/doc/tcb_mapping_design_proposal.md @@ -156,7 +156,7 @@ Break policy content into independent measured components so RTMR2 no longer dep | 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. | +| **RTMR2** | Signed policy blob (contains policy rules + collaterals + signed TCB mapping + signed identity) | **Single canonical-bytes extend** of `policyData` with `servtdCollateral.servtdTcbMapping` **and** `servtdCollateral.servtdTcbMappingIssuerChain` 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 `servtdIdentityIssuerChain`). The TCBMapping issuer chain is **not** measured here — it is anchored by RTMR1. See "RTMR2 single redacted extend" below. | **IGVM CFV file layout** (configuration firmware volume slots loaded at boot): @@ -166,10 +166,10 @@ Break policy content into independent measured components so RTMR2 no longer dep | `MIGTD_POLICY_FFS_GUID` | Signed policy with collaterals | Policy with collaterals (no outer signature), 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. +- RTMR2 = measurement of canonical `policyData` with `servtdCollateral.servtdTcbMapping` **and** `servtdCollateral.servtdTcbMappingIssuerChain` redacted — every other field is automatically bound by being inside the canonical object. Redacting `servtdTcbMapping` is the escape hatch that preserves circularity-freedom and lets its content be re-issued after the IGVM is shipped without touching `tdinfo_hash`. The issuer chain is redacted only to avoid measuring it twice — it is already measured into RTMR1 (see below). - 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. -- The **outer policy-blob signature is removed**: RTMR2 measures the canonical `policyData` bytes directly, so the hardware-rooted measurement — not a policy-signing key — authenticates exactly what MigTD loads. This eliminates the policy-signing key and its rotation burden. `servtdTcbMapping` and `servtdIdentity` retain their own issuer signatures, verified by their issuer chains (not by RTMR2), which keeps `servtdTcbMapping` updateable after ship. +- The outer policy-blob signature is removed. **New design — full tdinfo hash in svnMappings but unmeasured, removing circular dependency:** @@ -178,32 +178,25 @@ With this split: │ Policy Blob (no outer signature) │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ policyData │ │ -│ │ ├── policy, version, id, policySvn, collaterals, ... │ │ +│ │ ├── policy, version, id, policySvn, collaterals, ... [measured]│ │ │ │ └── servtdCollateral │ │ -│ │ ├── servtdIdentity {tdIdentity, signature} ──┐ │ │ -│ │ ├── servtdIdentityIssuerChain │ measured │ │ -│ │ ├── servtdTcbMappingIssuerChain │ │ │ -│ │ └── servtdTcbMapping ◄────── NOT measured ──┐ │ │ │ -│ │ └── svnMappings[]: │ │ │ │ -│ │ {tdinfo_hash, isvsvn} │ │ │ │ -│ └───────────────────────────────────────────────────┼─┼────────────┘ │ -└──────────────────────────────────────────────────────┼─┼───────────────┘ - │ │ - RTMR2 = SHA384(canonical(policyData │ │ - minus servtdTcbMapping)) ◄──────┘ │ - │ │ - ┌──────────┼──────────────────┐ │ - │ ▼ │ │ - │ tdinfo_hash = SHA384(TDINFO)│ │ - │ MRTD, RTMR0, RTMR1, RTMR2 │ │ - └──────────┬──────────────────┘ │ - │ │ - ▼ │ - svnMappings[].tdinfo_hash ─────────────────┘ - populated AFTER - measurement - (no circularity) - +│ │ ├── servtdIdentity {tdIdentity, signature} [measured]│ │ +│ │ ├── servtdIdentityIssuerChain [measured]│ │ +│ │ ├── servtdTcbMappingIssuerChain [NOT measured]│ │ +│ │ └── servtdTcbMapping [NOT measured]│ │ +│ │ └── svnMappings[]: {tdinfo_hash, isvsvn} │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────────────────┘ + + RTMR2 = SHA384(canonical( policyData + minus servtdTcbMapping + minus servtdTcbMappingIssuerChain )) + │ + ▼ + tdinfo_hash = SHA384(TDINFO) over MRTD, RTMR0, RTMR1, RTMR2 + │ + ▼ + svnMappings[].tdinfo_hash ← populated AFTER measurement (no circularity) ``` @@ -212,7 +205,6 @@ With this split: - **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. -- **No policy-signing key** — the outer policy-blob signature is removed; RTMR2 authenticates the policy content directly, so there is no policy-signing key to manage or rotate. `servtdTcbMapping` and `servtdIdentity` keep their own issuer signatures. # Design details @@ -223,14 +215,17 @@ To minimize code branching, the proposed design **replaces** the current impleme ## 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. +`servtdCollateral.servtdTcbMapping` and +`servtdCollateral.servtdTcbMappingIssuerChain` 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`) — is bound +into RTMR2 by virtue of being inside the canonical object bytes. Redacting +`servtdTcbMapping` is the escape hatch that makes its content updateable +after the IGVM is shipped without perturbing `tdinfo_hash`; +`servtdTcbMappingIssuerChain` is redacted only to avoid measuring it +twice, since it is already measured into RTMR1. This single extend folds together two security properties: detecting drift between the authority-endorsed `policyData` bytes and the bytes loaded @@ -250,22 +245,23 @@ its playback / TCB-downgrade attacks (covered by including - 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:** +**Why `servtdTcbMapping` and its issuer chain are the redacted fields:** -- 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. +- Measuring `servtdTcbMapping` would defeat the entire purpose of the proposal: it 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. +- `servtdTcbMappingIssuerChain` is redacted for a *different* reason: it is **already measured into RTMR1** (which anchors the TCB-mapping signer), so measuring it again in RTMR2 would be redundant. Redacting it from RTMR2 does **not** make the chain hash-neutral — swapping the chain still changes RTMR1 and therefore `tdinfo_hash`; making chain/leaf-key rotation hash-neutral is the separate RTMR1 signer-anchor change (see Future considerations). +- The redaction is what enables the authority to re-issue the `servtdTcbMapping` content (adding/removing `svnMappings[]` entries, bumping `nextUpdate`, etc.) without changing RTMR2. Operators just swap the signed TCB mapping artifact alongside the existing IGVM. (Rotating the TCB-mapping signing key without a new IGVM additionally requires the RTMR1 signer-anchor change — see Future considerations.) **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. +- `servtdIdentityIssuerChain` is covered for free by the RTMR2 extend: an attacker who substituted it could weaponise it to validate an arbitrary identity, and this scheme rules that out by construction. `servtdTcbMappingIssuerChain` is instead measured by **RTMR1** (which anchors the TCB-mapping signer), so it is redacted from RTMR2 only to avoid measuring it twice — not to make it hash-neutral. - 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. | +| **Single canonical extend over `policyData` with `servtdTcbMapping` and its issuer chain redacted** *(chosen)* | One RTMR2 extend, one tag, one event-log entry. | Breaks the circular dependency by redacting the field that contains `tdinfo_hash`; the issuer chain is redacted too, only because it is already measured into RTMR1; 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. | @@ -274,9 +270,9 @@ its playback / TCB-downgrade attacks (covered by including 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 issuer collateral (`servtdIdentity` and `servtdTcbMapping`) 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`. +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 TCBMapping issuer 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`. +2. **Release stage — pre-final IGVM (CFV swap).** Re-sign `servtdIdentity` under production signing. Assemble a *pre-final* `policyData` with empty `servtdTcbMapping` and `servtdTcbMappingIssuerChain` sentinels (the redacted RTMR2 extend ignores these fields). 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. From a4e6fdef040ebbd4d8ff0b75fe97a713f6d9966b Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Thu, 2 Jul 2026 21:14:51 +0000 Subject: [PATCH 5/8] doc: carry intermediate-CA non-pinning reasoning into RTMR1 anchor proposal Copy the reasoning from validate_peer_cert_chain (src/crypto/src/lib.rs) about intentionally not checking intermediate cert identity, and the leaf-Subject uniqueness assumption, into the RTMR1 signer-anchor design doc. The anchor excludes intermediate CAs (matching peer validation), so intermediate-CA rotation under the same root + leaf Subject is measurement-stable. Added in the trust-model description, anchor formula, alignment table, benefits, and the security trade-off note. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Haitao Huang --- doc/rtmr1_signer_anchor_proposal.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/doc/rtmr1_signer_anchor_proposal.md b/doc/rtmr1_signer_anchor_proposal.md index 4fade9f04..7e1cf905a 100644 --- a/doc/rtmr1_signer_anchor_proposal.md +++ b/doc/rtmr1_signer_anchor_proposal.md @@ -42,6 +42,18 @@ Note what the trust model does **not** require: an identical *leaf certificate* 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. +The trust model also **does not pin intermediate-CA identity**: intermediate cert contents +are not compared against the local chain's intermediates, so either side may rotate its +intermediate CA(s) independently — as long as the shared root and the leaf Subject stay +stable and every issuer in the chain is itself a CA (check 4). Intermediates are still +validated *structurally* — signature integrity (check 1) and the CA attribute (check 4) — +just not by identity. + +This rests on an **assumption about the leaf Subject**: the leaf cert's Subject Name +uniquely identifies the intended usage for the product/model — distinct usages must use +distinct Subject Names in their leaf certs. The RTMR1 anchor defined below inherits this +assumption, since it commits to that Subject (`S`). + ``` Runtime trust model (peer validation) RTMR1 measurement (today) ─────────────────────────────────── ───────────────────────── @@ -149,6 +161,11 @@ Define `H(x) = SHA384(x)`. The `"MIGTD-RTMR1-ANCHOR-V1"` tag provides domain separation and a version hook for future formula changes. +`A` deliberately commits to **only** the root CA and the leaf Subject — **not** the +intermediate CAs — matching peer validation, which likewise does not pin intermediate +identity. Intermediate-CA rotation under the same root + leaf Subject therefore leaves +RTMR1 unchanged, exactly as leaf-key rotation does. + # Benefits - **No rotation churn** — `A` depends on the root CA and leaf Subject, not the leaf public @@ -156,6 +173,10 @@ future formula changes. 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. +- **Intermediate-CA rotation is free too** — `A` excludes intermediate CAs (matching peer + validation, which does not pin intermediate identity), so rotating an intermediate CA + under the same root + leaf Subject also leaves RTMR1 unchanged — no rebuild, no new + endorsement. - **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. @@ -179,6 +200,7 @@ The anchor is the measured projection of the two equality checks already enforce |-----------------------|------------------| | Root CA must match (DER byte comparison) | `R = H(DER(root))` | | Leaf Subject Name must match | `S = H(DER(leaf subject))` | +| Intermediate-CA identity **not** pinned (independent rotation allowed) | not folded into `A` — the anchor excludes intermediates, so intermediate rotation is measurement-stable | | 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 @@ -235,9 +257,10 @@ regions passes because the runtime check keys on root + Subject. `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 + root CA and the leaf Subject — **not** the leaf public key, and **not** the intermediate + CAs. A leaf key (or an intermediate CA) 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 does not pin intermediate identity) 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 From a94d5f75f25ff2a65fa85f3843fd082b3de688c0 Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Fri, 3 Jul 2026 01:42:18 +0000 Subject: [PATCH 6/8] doc: add signing-key-compromise analysis and servTD signer revocation Document the security consequence of the RTMR1 root+Subject anchor and the revocation control that addresses it, and broaden the doc title/scope to cover both coupled parts (anchor measurement + signer revocation). Security considerations: - A stolen intermediate CA can mint a same-Subject leaf that is measurement-indistinguishable under the anchor -- the new exposure this proposal introduces. A stolen leaf key reusing its existing certificate is invisible to measurement with or without the anchor (pre-existing). - Mitigations: strict key protection (especially intermediate CA keys) as the primary control; certificate revocation as the in-guest control that closes both cases; and an optional notBefore policy floor as a lightweight complement for the common leaf-key-leak case. Revocation -- the servTD signer CRL: - Deliver an optional CRL via servtdCollateral.servtdCrl, authenticate it against the RTMR1-anchored signer CA, enforce fail-closed on the local and peer signer chains (the peer chain checked against the LOCAL CRL), and gate a monotonic servtd_crl_num floor -- reusing the existing platform pck_crl / root_ca_crl machinery. No trusted clock in-guest, so nextUpdate freshness stays service-side. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Haitao Huang --- doc/rtmr1_signer_anchor_proposal.md | 141 ++++++++++++++++++++++++---- 1 file changed, 124 insertions(+), 17 deletions(-) diff --git a/doc/rtmr1_signer_anchor_proposal.md b/doc/rtmr1_signer_anchor_proposal.md index 7e1cf905a..ff773ca82 100644 --- a/doc/rtmr1_signer_anchor_proposal.md +++ b/doc/rtmr1_signer_anchor_proposal.md @@ -1,11 +1,13 @@ -RTMR1 Signer-Anchor Measurement +RTMR1 Signer-Anchor Measurement & servTD Signer Revocation ================================================ -> 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"*. +> This proposal has two coupled parts: (1) **what MigTD measures into RTMR1** — a +> stable *signer anchor* rather than the raw issuer-chain bytes — and (2) the +> **servTD signer-key revocation** control that mitigates the one security relaxation +> the anchor introduces. The RTMR2 / TCB-mapping circular-dependency work is covered +> separately in [tcb_mapping_design_proposal.md](./tcb_mapping_design_proposal.md); +> the anchor 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). @@ -222,7 +224,7 @@ leaf Subject are unchanged. | **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 +the anchor 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 @@ -244,27 +246,132 @@ regions passes because the runtime check keys on root + Subject. chain so the reproduced `tdinfo_hash` matches the running TD. This replaces the current "extend over raw chain bytes" path. +# Security considerations — signing-key compromise + +The anchor lowers RTMR1's sensitivity from the exact issuer-chain bytes to **root CA + leaf +Subject**. That relaxation is the source of the proposal's rotation/region agility — and of one +new risk. This section states what the change introduces and what it leaves unchanged. + +## Limitation introduced: stolen intermediate cert chain + +Raw-chain RTMR1 (today) hashes the exact chain bytes, so **any** certificate change in the chain +alters RTMR1 — and therefore `tdinfo_hash` — including a leaf freshly minted by a **stolen +intermediate CA** under the same root and same leaf Subject. The signer anchor commits to root + +leaf Subject only, so that same stolen-intermediate-minted leaf becomes +**measurement-indistinguishable**: identical `A`, identical RTMR1, identical `tdinfo_hash`. + +``` + stolen intermediate CA key (same root + same leaf Subject) + │ mints a new leaf, signs + ▼ + forged servtdTcbMapping: { tdinfo_hash(vulnerable_MigTD) → high SVN } + │ + ▼ passes validate_peer_cert_chain (root + Subject + CA-attr) and CoRIM x5chain + ▼ + vulnerable MigTD resolves to high SVN → clears the baseline → accepted + + raw-chain RTMR1 : new leaf ⇒ different tdinfo_hash (measurement-visible) + signer anchor : same root+Subject ⇒ SAME tdinfo_hash (measurement-INvisible) ← new risk +``` + +The anchor trades away RTMR1's ability to distinguish a different signing certificate under the +same root + Subject. A **stolen intermediate CA** — which can issue an arbitrary Subject-matching +leaf — is the concrete exposure this proposal introduces, and it must be mitigated at the +PKI/authorization layer (below). + +## Not introduced here: stolen leaf key (reused certificate) + +A stolen **leaf private key** reusing its existing certificate changes no certificate bytes, so +it is invisible to RTMR1 with or without the anchor — a **pre-existing** risk the anchor neither +adds nor removes (unlike the stolen-intermediate limitation above, which is new). Like that case, +it is a signer-authorization question measurement cannot answer, mitigated by revocation (below). + +## Mitigations + +Neither attack is visible to measurement, so both are handled at the PKI/policy layer. Three +controls apply, each covering the two attacks differently: + +- **Strict key protection, especially of intermediate CA keys** — the primary control, since the + anchor cannot distinguish a rogue same-Subject leaf minted by a stolen intermediate. Keep + signing keys offline in an HSM, rarely used, ideally under m-of-n quorum. +- **Certificate revocation (CRL)** — revoke the leaked leaf or intermediate certificate and have + consumers reject it. It covers **both** attacks and is the only in-guest control that closes a + stolen intermediate (revoking the intermediate rejects its whole subtree). MigTD performs no + revocation check today; the *Revocation* section below sketches an in-guest design. +- **`notBefore` floor** — a lightweight complement: a policy date requiring each signer leaf's + `notBefore ≥ floor`. On a detected leak the authority raises the floor to the detection date, + rejecting the leaked certificate while re-issued certs pass. It is simpler than a CRL, needs no + trusted clock (it compares two static values), and cannot be bypassed by a reused leaf cert — but + it does **not** stop a stolen intermediate (which mints a fresh, current-dated leaf) and is + coarser, rejecting every certificate older than the floor. + +**Recommendation.** Prevent these compromises with strict key protection; optionally add a `notBefore` floor in policy if the leaf-key-leak case is the main concern. The long-term solution is the CRL as the in-guest control. A servTD signer CRL, similar to the Intel platform CRLs, is proposed below. + +# Revocation — the servTD signer CRL + +The attestation-service side is standard PKI (CRL/OCSP over the CoRIM `x5chain`) and is out of +scope. The in-guest side is the new work: ship a CRL for the servTD signer chain in the policy and +enforce it fail-closed, reusing the existing platform-CRL machinery (`src/crypto/src/crl.rs`, the +`pck_crl_num` / `root_ca_crl_num` floor pattern). + +| Concern | Mechanism | +|---------|-----------| +| **Delivery** | `servtdCollateral.servtdCrl` — an optional PEM CRL co-located with the signers it revokes. Optional for backward compatibility: a policy without it skips the check. | +| **Authentication** | The CRL signature is verified against the issuing CA in the RTMR1-anchored signer chain before its contents are trusted; only a CA (`cA=TRUE`) may issue it, so a peer cannot forge one without the shared root key. | +| **Enforcement** | Every certificate in the TCB-mapping and identity signer chains is checked against the CRL; a revoked serial fails closed. | +| **Anti-rollback** | A monotonic `servtd_crl_num` policy floor rejects a CRL older than required, mirroring `pck_crl_num` / `root_ca_crl_num`. | + +**Enforcement points.** The local policy's CRL is checked at boot (during policy verification) +against both signer chains — a MigTD whose own policy revokes its signer refuses to start — and, +during migration, against the *peer's* chains. The peer check uses the **local** CRL (the peer's +chain root is already bound to the local root by `validate_peer_cert_chain`), so a peer cannot +launder a revocation-free CRL of its own. Production policies should therefore always ship a +`servtdCrl`, empty if nothing is revoked. + +**No trusted clock.** Guest time is VMM-supplied, so the CRL `nextUpdate` window cannot be +enforced in-guest; freshness relies on the `servtd_crl_num` floor. Time-window checks are left to +the attestation service. + +**Interaction with measurement.** `servtdCrl` is inside the measured `policyData`, so updating it +churns `tdinfo_hash` — revoking a signer becomes a policy re-release + re-endorsement. That is +acceptable for a rare, deliberate event and matches the platform `root_ca_crl` / `pck_crl`. If +in-place updates without re-endorsement are ever needed, `servtdCrl` could be redacted from the +RTMR2 extend (like `servtdTcbMapping`), leaving `servtd_crl_num` as the sole anti-rollback control. + +## Residual risks & open questions + +- **Stolen key with a not-yet-revoked certificate** stays invisible until the authority detects + the compromise and publishes a revoking CRL — inherent to CRL-based revocation. +- **Service-side revocation** (CRL/OCSP over the CoRIM `x5chain`, plus `nextUpdate` time-window + checks) is standard PKI, tracked separately. +- **CoRIM delivery.** When the servTD collateral is a signed CoRIM (COSE `x5chain`, RFC 9360), + revocation should thread the CRL through the COSE flow or run CRL/OCSP on the `x5chain`. +- **`cRLSign` KeyUsage.** Optionally require the CRL issuer to assert the `cRLSign` KeyUsage bit + in addition to `cA=TRUE`, as defence-in-depth against a mis-issued CA certificate. +- **Multiple / per-issuer CRLs.** A single CRL authenticated against the signer chain's CA + suffices when the mapping and identity issuers share a root; distinct sub-CAs would need one + CRL per issuing CA or a small list. + # Notes -- **This proposal changes only RTMR1.** RTMR2 (the policy) is the companion +- **The anchor 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. +- **Orthogonal to the TCB-mapping proposal's RTMR2 changes.** That proposal's removal of the + outer policy signature (now part of the proposal itself, not a future item) and its remaining + *Future consideration* of dropping `servtdIdentity` both affect only **RTMR2** (the signed + `policyData`), not RTMR1; the anchor is 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, and **not** the intermediate CAs. A leaf key (or an intermediate CA) 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 does not pin intermediate identity) 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. + model and makes the root CA the unit of trust — the intended, explicit trade-off. Its + consequences for a forged TCB mapping, and the mitigations, are detailed in + *[Security considerations — signing-key compromise](#security-considerations--signing-key-compromise)* above. - **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. From 884f283653e6e97c4b20fc8ba608bfc5c3b17a46 Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Sun, 12 Jul 2026 22:06:44 +0000 Subject: [PATCH 7/8] doc: "tcbmapping signer" -> "policy signer" term change The TCB mapping signer is now renamed as policy signer in the proposal Signed-off-by: Haitao Huang Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/tcb_mapping_design_proposal.md | 37 +++++++++++++----------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/doc/tcb_mapping_design_proposal.md b/doc/tcb_mapping_design_proposal.md index 69b0b1718..4f730c619 100644 --- a/doc/tcb_mapping_design_proposal.md +++ b/doc/tcb_mapping_design_proposal.md @@ -155,20 +155,20 @@ Break policy content into independent measured components so RTMR2 no longer dep | 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` **and** `servtdCollateral.servtdTcbMappingIssuerChain` 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 `servtdIdentityIssuerChain`). The TCBMapping issuer chain is **not** measured here — it is anchored by RTMR1. See "RTMR2 single redacted extend" below. | +| **RTMR1** | Policy issuer cert chain | Policy issuer cert chain (**unchanged**) — the policy signer now also signs `servtdTcbMapping` | +| **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 `servtdIdentityIssuerChain`). `servtdTcbMapping` is signed by the policy signer, whose chain is measured into RTMR1. 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_ISSUER_CHAIN_FFS_GUID` | Policy issuer cert chain | Policy issuer cert chain (**unchanged**) | **RTMR1** | | `MIGTD_POLICY_FFS_GUID` | Signed policy with collaterals | Policy with collaterals (no outer signature), updated `svnMappings` semantics | **RTMR2** | With this split: -- RTMR2 = measurement of canonical `policyData` with `servtdCollateral.servtdTcbMapping` **and** `servtdCollateral.servtdTcbMappingIssuerChain` redacted — every other field is automatically bound by being inside the canonical object. Redacting `servtdTcbMapping` is the escape hatch that preserves circularity-freedom and lets its content be re-issued after the IGVM is shipped without touching `tdinfo_hash`. The issuer chain is redacted only to avoid measuring it twice — it is already measured into RTMR1 (see below). +- RTMR2 = measurement of canonical `policyData` with `servtdCollateral.servtdTcbMapping` redacted — every other field is automatically bound by being inside the canonical object. This is the escape hatch that preserves circularity-freedom and lets the mapping content be re-issued after the IGVM is shipped without touching `tdinfo_hash`. - 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. +- RTMR1 = measurement of the policy issuer chain, **unchanged** — the policy signer now also signs `servtdTcbMapping`, so no separate TCB-mapping signer chain is introduced. - The outer policy-blob signature is removed. **New design — full tdinfo hash in svnMappings but unmeasured, removing circular dependency:** @@ -182,15 +182,12 @@ With this split: │ │ └── servtdCollateral │ │ │ │ ├── servtdIdentity {tdIdentity, signature} [measured]│ │ │ │ ├── servtdIdentityIssuerChain [measured]│ │ -│ │ ├── servtdTcbMappingIssuerChain [NOT measured]│ │ │ │ └── servtdTcbMapping [NOT measured]│ │ │ │ └── svnMappings[]: {tdinfo_hash, isvsvn} │ │ │ └──────────────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────────────┘ - RTMR2 = SHA384(canonical( policyData - minus servtdTcbMapping - minus servtdTcbMappingIssuerChain )) + RTMR2 = SHA384(canonical( policyData minus servtdTcbMapping )) │ ▼ tdinfo_hash = SHA384(TDINFO) over MRTD, RTMR0, RTMR1, RTMR2 @@ -215,17 +212,15 @@ To minimize code branching, the proposed design **replaces** the current impleme ## RTMR2 single redacted extend RTMR2 is extended **once** with the canonical bytes of `policyData` with -`servtdCollateral.servtdTcbMapping` and -`servtdCollateral.servtdTcbMappingIssuerChain` removed. Every other +`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`) — is bound into RTMR2 by virtue of being inside the canonical object bytes. Redacting `servtdTcbMapping` is the escape hatch that makes its content updateable -after the IGVM is shipped without perturbing `tdinfo_hash`; -`servtdTcbMappingIssuerChain` is redacted only to avoid measuring it -twice, since it is already measured into RTMR1. +after the IGVM is shipped without perturbing `tdinfo_hash`. `servtdTcbMapping` +is signed by the policy signer, whose chain is measured into RTMR1. This single extend folds together two security properties: detecting drift between the authority-endorsed `policyData` bytes and the bytes loaded @@ -245,23 +240,23 @@ its playback / TCB-downgrade attacks (covered by including - 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` and its issuer chain are the redacted fields:** +**Why `servtdTcbMapping` is the redacted field:** - Measuring `servtdTcbMapping` would defeat the entire purpose of the proposal: it 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. -- `servtdTcbMappingIssuerChain` is redacted for a *different* reason: it is **already measured into RTMR1** (which anchors the TCB-mapping signer), so measuring it again in RTMR2 would be redundant. Redacting it from RTMR2 does **not** make the chain hash-neutral — swapping the chain still changes RTMR1 and therefore `tdinfo_hash`; making chain/leaf-key rotation hash-neutral is the separate RTMR1 signer-anchor change (see Future considerations). -- The redaction is what enables the authority to re-issue the `servtdTcbMapping` content (adding/removing `svnMappings[]` entries, bumping `nextUpdate`, etc.) without changing RTMR2. Operators just swap the signed TCB mapping artifact alongside the existing IGVM. (Rotating the TCB-mapping signing key without a new IGVM additionally requires the RTMR1 signer-anchor change — see Future considerations.) +- `servtdTcbMapping` is signed by the **policy signer**, whose issuer chain is measured into RTMR1 — the mapping needs no separate signer chain. Re-issuing the mapping *content* is hash-neutral, but rotating the **policy signing key** changes RTMR1 (and `tdinfo_hash`) and needs a new IGVM unless the RTMR1 signer-anchor change (see Future considerations) is adopted. +- The redaction is what enables the authority to re-issue the `servtdTcbMapping` content (adding/removing `svnMappings[]` entries, bumping `nextUpdate`, etc.) without changing RTMR2. 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. -- `servtdIdentityIssuerChain` is covered for free by the RTMR2 extend: an attacker who substituted it could weaponise it to validate an arbitrary identity, and this scheme rules that out by construction. `servtdTcbMappingIssuerChain` is instead measured by **RTMR1** (which anchors the TCB-mapping signer), so it is redacted from RTMR2 only to avoid measuring it twice — not to make it hash-neutral. +- `servtdIdentityIssuerChain` is covered for free by the RTMR2 extend: an attacker who substituted it could weaponise it to validate an arbitrary identity, and this scheme rules that out by construction. `servtdTcbMapping` is signed by the policy signer, whose chain is measured into RTMR1 rather than RTMR2. - 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` and its issuer chain redacted** *(chosen)* | One RTMR2 extend, one tag, one event-log entry. | Breaks the circular dependency by redacting the field that contains `tdinfo_hash`; the issuer chain is redacted too, only because it is already measured into RTMR1; binds every other field by construction. | +| **Single canonical extend over `policyData` with `servtdTcbMapping` redacted** *(chosen)* | One RTMR2 extend, one tag, one event-log entry. | Breaks the circular dependency by redacting 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. | @@ -270,9 +265,9 @@ its playback / TCB-downgrade attacks (covered by including 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 issuer collateral (`servtdIdentity` and `servtdTcbMapping`) 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 TCBMapping issuer 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`. +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 policy issuer 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 empty `servtdTcbMapping` and `servtdTcbMappingIssuerChain` sentinels (the redacted RTMR2 extend ignores these fields). Run `td-shim-enroll` to overwrite the CFV slots. Measure the re-enrolled binary to obtain the production `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. From c5c8c136c616f0392ccc671a7cd0897ef8bd1c24 Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Sun, 12 Jul 2026 23:22:11 +0000 Subject: [PATCH 8/8] doc: 'policy signer' terminology + revocation-section cleanup in RTMR1 anchor proposal - Use 'policy signer' consistently, matching the TCB-mapping proposal (the RTMR1-anchored signer signs servtdTcbMapping; no separate TCB-mapping signer). - Trim the mitigations recommendation and drop the 'cRLSign KeyUsage' and 'per-issuer CRLs' open-question bullets: marginal value under the shared-root trust model, and the existing CRL machinery checks cA=TRUE only. - Minor wording/punctuation fixes. Signed-off-by: Haitao Huang Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e3356e2-6b75-4348-bb8d-95c05491ef8b --- doc/rtmr1_signer_anchor_proposal.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/doc/rtmr1_signer_anchor_proposal.md b/doc/rtmr1_signer_anchor_proposal.md index ff773ca82..08ade8f24 100644 --- a/doc/rtmr1_signer_anchor_proposal.md +++ b/doc/rtmr1_signer_anchor_proposal.md @@ -14,7 +14,7 @@ RTMR1 Signer-Anchor Measurement & servTD Signer Revocation # 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`), +bytes of the policy 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 @@ -32,7 +32,8 @@ 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 +policy signer (the signer whose chain RTMR1 measures; it signs `servtdTcbMapping`). 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, @@ -142,7 +143,7 @@ the leaf Subject — and nothing else. | **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 +The CFV still ships the **full** chain (peer validation and policy-signer 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. @@ -224,7 +225,7 @@ leaf Subject are unchanged. | **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; -the anchor changes only RTMR1. RTMR2 redacts TCBMapping, so rotating the TCBMapping +the anchor changes only RTMR1. RTMR2 redacts `servtdTcbMapping`, so rotating the policy signing leaf — the trust authority RTMR1 anchors — leaves RTMR2 (and the hash) unchanged. ## Regional leaf certificates @@ -305,7 +306,7 @@ controls apply, each covering the two attacks differently: it does **not** stop a stolen intermediate (which mints a fresh, current-dated leaf) and is coarser, rejecting every certificate older than the floor. -**Recommendation.** Prevent these compromises with strict key protection; optionally add a `notBefore` floor in policy if the leaf-key-leak case is the main concern. The long-term solution is the CRL as the in-guest control. A servTD signer CRL, similar to the Intel platform CRLs, is proposed below. +**Recommendation.** Prevent these compromises with strict key protection. The long-term solution is the CRL as the in-guest control. A servTD signer CRL, similar to the Intel platform CRLs, is proposed below. # Revocation — the servTD signer CRL @@ -318,7 +319,7 @@ enforce it fail-closed, reusing the existing platform-CRL machinery (`src/crypto |---------|-----------| | **Delivery** | `servtdCollateral.servtdCrl` — an optional PEM CRL co-located with the signers it revokes. Optional for backward compatibility: a policy without it skips the check. | | **Authentication** | The CRL signature is verified against the issuing CA in the RTMR1-anchored signer chain before its contents are trusted; only a CA (`cA=TRUE`) may issue it, so a peer cannot forge one without the shared root key. | -| **Enforcement** | Every certificate in the TCB-mapping and identity signer chains is checked against the CRL; a revoked serial fails closed. | +| **Enforcement** | Every certificate in the policy-signer and identity-signer chains is checked against the CRL; a revoked serial fails closed. | | **Anti-rollback** | A monotonic `servtd_crl_num` policy floor rejects a CRL older than required, mirroring `pck_crl_num` / `root_ca_crl_num`. | **Enforcement points.** The local policy's CRL is checked at boot (during policy verification) @@ -334,7 +335,7 @@ the attestation service. **Interaction with measurement.** `servtdCrl` is inside the measured `policyData`, so updating it churns `tdinfo_hash` — revoking a signer becomes a policy re-release + re-endorsement. That is -acceptable for a rare, deliberate event and matches the platform `root_ca_crl` / `pck_crl`. If +acceptable for a rare, deliberate event and matches the platform `root_ca_crl` / `pck_crl` process. If in-place updates without re-endorsement are ever needed, `servtdCrl` could be redacted from the RTMR2 extend (like `servtdTcbMapping`), leaving `servtd_crl_num` as the sole anti-rollback control. @@ -346,11 +347,6 @@ RTMR2 extend (like `servtdTcbMapping`), leaving `servtd_crl_num` as the sole ant checks) is standard PKI, tracked separately. - **CoRIM delivery.** When the servTD collateral is a signed CoRIM (COSE `x5chain`, RFC 9360), revocation should thread the CRL through the COSE flow or run CRL/OCSP on the `x5chain`. -- **`cRLSign` KeyUsage.** Optionally require the CRL issuer to assert the `cRLSign` KeyUsage bit - in addition to `cA=TRUE`, as defence-in-depth against a mis-issued CA certificate. -- **Multiple / per-issuer CRLs.** A single CRL authenticated against the signer chain's CA - suffices when the mapping and identity issuers share a root; distinct sub-CAs would need one - CRL per issuing CA or a small list. # Notes