From 48fb739552d9aaf37fc9fe71a1249fa69d5d2799 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 11 Jul 2026 21:16:08 -0700 Subject: [PATCH] tdx: always attach measurement and ACPI digests for verifier choice Carry tdx_measurement whenever the image provides it and always keep RTMR0 ACPI digest events in the exposed event log, so a verifier can independently pick lite or legacy for any TDX boot. tdx_attestation_variant still records only the VMM/KMS-resolved scheme for key release. --- dstack-attest/src/attestation.rs | 62 ++++++++++++++++++-------------- dstack-attest/src/v1.rs | 23 ++++++------ dstack-types/src/lib.rs | 32 ++++++++++------- vmm/src/app.rs | 30 ++++++++++++---- 4 files changed, 89 insertions(+), 58 deletions(-) diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index bc058f0bd..be1da10a3 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -33,8 +33,7 @@ pub use tpm_types::TpmQuote; use crate::amd_sev_snp::{AmdKdsClient, VerifiedAmdSnpReport}; use crate::v1::{ - is_tdx_acpi_data_event, is_tdx_lite_config, strip_tdx_event_log_for_config, - strip_tdx_runtime_event_log, + is_tdx_acpi_data_event, strip_tdx_event_log_for_config, strip_tdx_runtime_event_log, }; pub use crate::v1::{Attestation as AttestationV1, PlatformEvidence, StackEvidence}; @@ -1178,19 +1177,24 @@ impl Attestation { /// Get TDX event log string for a vm_config. /// - /// In lite mode, keep the `ACPI DATA` marker payloads in RTMR0 so callers - /// that still consume the top-level `event_log` can semantically identify - /// the ACPI table digest events without consulting the versioned - /// attestation field. - pub fn get_tdx_event_log_string_for_config(&self, config: &str) -> Option { + /// Always keeps the `ACPI DATA` marker payloads on the three RTMR0 ACPI + /// digest events, regardless of the vm_config's `tdx_attestation_variant`, + /// so callers that consume the top-level `event_log` can semantically + /// identify the ACPI table digest events without consulting the + /// versioned attestation field, and a verifier can choose lite + /// verification for any TDX boot rather than only ones resolved to lite + /// at launch. + /// + /// `config` is accepted for API stability but no longer changes the + /// result. + pub fn get_tdx_event_log_string_for_config(&self, _config: &str) -> Option { self.tdx_quote().map(|q| { - let keep_lite_acpi_payload = is_tdx_lite_config(config); let stripped: Vec<_> = q .event_log .iter() .map(|e| { let mut stripped = e.stripped(); - if keep_lite_acpi_payload && is_tdx_acpi_data_event(e) { + if is_tdx_acpi_data_event(e) { stripped.event_payload = e.event_payload.clone(); } stripped @@ -2117,7 +2121,7 @@ mod tests { } #[test] - fn tdx_event_log_string_for_lite_keeps_acpi_data_payloads() { + fn tdx_event_log_string_always_keeps_acpi_data_payloads() { let mut attestation = dummy_tdx_attestation([0u8; 64]); let AttestationQuote::DstackTdx(tdx_quote) = &mut attestation.quote else { panic!("expected TDX attestation"); @@ -2128,23 +2132,27 @@ mod tests { tdx_event(3, 8, b"runtime-payload"), ]; - let lite_events: Vec = serde_json::from_str( - &attestation - .get_tdx_event_log_string_for_config(r#"{"tdx_attestation_variant":"lite"}"#) - .expect("TDX event log"), - ) - .expect("decode lite event log"); - assert_eq!(lite_events[0].event_payload, b"ACPI DATA"); - assert!(lite_events[1].event_payload.is_empty()); - assert!(lite_events[2].event_payload.is_empty()); - - let legacy_events: Vec = serde_json::from_str( - &attestation - .get_tdx_event_log_string() - .expect("TDX event log"), - ) - .expect("decode legacy event log"); - assert!(legacy_events[0].event_payload.is_empty()); + // The ACPI DATA marker payload is retained regardless of the + // vm_config's tdx_attestation_variant (including no vm_config at + // all), so a verifier can choose lite verification for any TDX boot. + for config in [ + r#"{"tdx_attestation_variant":"lite"}"#, + r#"{"tdx_attestation_variant":"legacy"}"#, + "", + ] { + let events: Vec = serde_json::from_str( + &attestation + .get_tdx_event_log_string_for_config(config) + .expect("TDX event log"), + ) + .unwrap_or_else(|e| panic!("decode event log for config {config:?}: {e}")); + assert_eq!( + events[0].event_payload, b"ACPI DATA", + "config {config:?} must keep the ACPI DATA marker payload" + ); + assert!(events[1].event_payload.is_empty()); + assert!(events[2].event_payload.is_empty()); + } } #[test] diff --git a/dstack-attest/src/v1.rs b/dstack-attest/src/v1.rs index 3fa28952a..15ccb4231 100644 --- a/dstack-attest/src/v1.rs +++ b/dstack-attest/src/v1.rs @@ -46,21 +46,20 @@ pub(crate) fn strip_tdx_lite_event_log(event_log: Vec) -> Vec bool { - serde_json::from_str::(config) - .map(|config| config.tdx_attestation_variant.is_lite()) - .unwrap_or(false) -} - +/// Always keep the RTMR0 ACPI DATA digest events (in addition to RTMR3 +/// runtime events), regardless of the boot's `tdx_attestation_variant`. This +/// makes `strip_tdx_lite_event_log`'s output a strict superset of +/// `strip_tdx_runtime_event_log`'s, so a verifier can independently choose +/// lite or legacy verification for any TDX boot instead of being limited to +/// whatever the VMM resolved at launch. See +/// `dstack_types::TdxAttestationVariant` for the full rationale. +/// +/// `config` is accepted for API stability but no longer changes the result. pub(crate) fn strip_tdx_event_log_for_config( event_log: Vec, - config: &str, + _config: &str, ) -> Vec { - if is_tdx_lite_config(config) { - strip_tdx_lite_event_log(event_log) - } else { - strip_tdx_runtime_event_log(event_log) - } + strip_tdx_lite_event_log(event_log) } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/dstack-types/src/lib.rs b/dstack-types/src/lib.rs index c0adb6afd..21ea64eb0 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -35,18 +35,23 @@ impl OvmfVariant { } } -/// Selects how a TDX attestation should bind the OS image. +/// Records which TDX attestation/hash scheme the VMM resolved for this boot +/// (used by KMS's own key-release check and by the guest's fail-closed +/// `requirements.tdx_measure_acpi_tables` gate). It does not restrict what a +/// verifier can do: the guest's event log always retains the RTMR0 ACPI +/// digest events and `vm_config.tdx_measurement` is attached whenever the +/// image provides it, regardless of this flag, so any verifier can +/// independently pick `Legacy` or `Lite` verification for the same +/// attestation by supplying its own vm_config. /// -/// `Legacy` preserves the existing verifier behavior: `vm_config.os_image_hash` -/// is the image digest (`digest.txt`, i.e. `sha256(sha256sum.txt)`) and the -/// verifier recomputes the full TDX launch measurement using the legacy -/// image/QEMU-derived path. +/// `Legacy` recomputes the full TDX launch measurement using the +/// image/QEMU-derived path (`vm_config.os_image_hash` is `digest.txt`, i.e. +/// `sha256(sha256sum.txt)`). /// -/// `Lite` opts into the no-QEMU verifier path: `vm_config.os_image_hash` -/// remains the unified image digest (`sha256(sha256sum.txt)`), -/// `vm_config.tdx_measurement` carries `sha256sum.txt` plus the TDX measurement -/// CBOR file, and KMS/verifier select the new logic from this vm_config flag -/// while the attestation quote remains the existing `DstackTdx`. +/// `Lite` recomputes measurements from `vm_config.tdx_measurement` +/// (`sha256sum.txt` plus the TDX measurement CBOR file) and the event log's +/// ACPI digests, without downloading the image or running QEMU. The +/// attestation quote remains the existing `DstackTdx` in both cases. #[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Default)] #[serde(rename_all = "snake_case")] pub enum TdxAttestationVariant { @@ -602,8 +607,11 @@ pub struct VmConfig { /// omitted from legacy configs to keep old behavior and wire shape stable. #[serde(default, skip_serializing_if = "TdxAttestationVariant::is_legacy")] pub tdx_attestation_variant: TdxAttestationVariant, - /// TDX-only no-image-download measurement material. Present only when - /// `tdx_attestation_variant = "lite"` and omitted for legacy TDX. + /// TDX-only no-image-download measurement material. Attached whenever + /// the OS image provides it, regardless of `tdx_attestation_variant`, so + /// a verifier can choose lite verification even for a boot that resolved + /// to `Legacy`. Omitted only when the image predates this measurement + /// material. #[serde(default, skip_serializing_if = "Option::is_none")] pub tdx_measurement: Option, /// GCP TDX no-image-download measurement material. Present for GCP diff --git a/vmm/src/app.rs b/vmm/src/app.rs index 1d7c358cf..0ce99b437 100644 --- a/vmm/src/app.rs +++ b/vmm/src/app.rs @@ -1376,11 +1376,22 @@ fn make_vm_config( .as_ref() .and_then(|d| hex::decode(d).ok()) .unwrap_or_default(); - let tdx_measurement = if tdx_attestation_variant.is_lite() { - Some(image.tdx_measurement.clone().context( - "tdx lite attestation requested but image is missing \ - measurement.tdx.cbor/sha256sum.txt measurement material", - )?) + // Attach the lite measurement material whenever the image provides it, + // regardless of the resolved attestation variant: the guest's exposed + // event log always retains the RTMR0 ACPI digest events (see + // cc_eventlog::tdx::label_tdx_acpi_data_events), so a verifier can freely + // choose lite verification for a legacy-resolved boot too. + // `tdx_attestation_variant` keeps its original meaning of "the scheme the + // VMM/KMS resolved for this boot" and is unaffected by this. + let tdx_measurement = if is_tdx { + if tdx_attestation_variant.is_lite() { + Some(image.tdx_measurement.clone().context( + "tdx lite attestation requested but image is missing \ + measurement.tdx.cbor/sha256sum.txt measurement material", + )?) + } else { + image.tdx_measurement.clone() + } } else { None }; @@ -1627,7 +1638,10 @@ mod tests { let vm_config = make_vm_config(&config, &manifest, &image, &hex_of(0x22, 32), None, None)?; assert!(vm_config.get("tdx_attestation_variant").is_none()); - assert!(vm_config.get("tdx_measurement").is_none()); + // tdx_measurement is attached whenever the image supports it, even + // when the resolved variant is legacy, so a verifier can still + // choose lite verification for this boot. + assert!(vm_config.get("tdx_measurement").is_some()); assert_eq!( vm_config["os_image_hash"] .as_str() @@ -1693,7 +1707,9 @@ mod tests { )?; assert!(vm_config.get("tdx_attestation_variant").is_none()); - assert!(vm_config.get("tdx_measurement").is_none()); + // Still attached even though the requirement forced this boot to + // legacy: a verifier can independently choose lite for it. + assert!(vm_config.get("tdx_measurement").is_some()); Ok(()) }