Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 35 additions & 27 deletions dstack-attest/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -1178,19 +1177,24 @@ impl<T> Attestation<T> {

/// 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<String> {
/// 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<String> {
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
Expand Down Expand Up @@ -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");
Expand All @@ -2128,23 +2132,27 @@ mod tests {
tdx_event(3, 8, b"runtime-payload"),
];

let lite_events: Vec<TdxEvent> = 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<TdxEvent> = 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<TdxEvent> = 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]
Expand Down
23 changes: 11 additions & 12 deletions dstack-attest/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,20 @@ pub(crate) fn strip_tdx_lite_event_log(event_log: Vec<TdxEvent>) -> Vec<TdxEvent
.collect()
}

pub(crate) fn is_tdx_lite_config(config: &str) -> bool {
serde_json::from_str::<dstack_types::VmConfig>(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<TdxEvent>,
config: &str,
_config: &str,
) -> Vec<TdxEvent> {
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)]
Expand Down
32 changes: 20 additions & 12 deletions dstack-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<TdxOsImageMeasurementDocument>,
/// GCP TDX no-image-download measurement material. Present for GCP
Expand Down
30 changes: 23 additions & 7 deletions vmm/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(())
}

Expand Down
Loading