Skip to content
Open
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 95 additions & 17 deletions dstack-attest/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport;
pub use tpm_types::TpmQuote;

use crate::amd_sev_snp::VerifiedAmdSnpReport;
use crate::v1::{
is_tdx_acpi_data_event, is_tdx_lite_config, strip_tdx_event_log_for_config,
strip_tdx_runtime_event_log,
};
pub use crate::v1::{Attestation as AttestationV1, PlatformEvidence, StackEvidence};

pub const SNP_REPORT_DATA_RANGE: std::ops::Range<usize> = 0x50..0x90;
Expand Down Expand Up @@ -596,17 +600,24 @@ impl VersionedAttestation {
}
}

/// Strip data for certificate embedding (e.g. keep RTMR3 event logs only).
/// Strip data for certificate embedding.
pub fn into_stripped(self) -> Self {
match self {
Self::V0 { mut attestation } => {
if let Some(tdx_quote) = attestation.tdx_quote_mut() {
tdx_quote.event_log = tdx_quote
.event_log
.iter()
.filter(|e| e.imr == 3)
.map(|e| e.stripped())
.collect();
match &mut attestation.quote {
AttestationQuote::DstackTdx(tdx_quote) => {
tdx_quote.event_log = strip_tdx_event_log_for_config(
std::mem::take(&mut tdx_quote.event_log),
&attestation.config,
);
}
AttestationQuote::DstackGcpTdx(quote) => {
quote.tdx_quote.event_log = strip_tdx_runtime_event_log(std::mem::take(
&mut quote.tdx_quote.event_log,
));
}
AttestationQuote::DstackAmdSevSnp(_)
| AttestationQuote::DstackNitroEnclave(_) => {}
}
Self::V0 { attestation }
}
Expand Down Expand Up @@ -983,17 +994,16 @@ pub enum AttestationQuote {
DstackTdx(TdxQuote),
DstackGcpTdx(DstackGcpTdxQuote),
DstackNitroEnclave(DstackNitroQuote),
/// Keep this last to preserve SCALE discriminants for existing variants.
DstackAmdSevSnp(SnpQuote),
}

impl AttestationQuote {
pub fn mode(&self) -> AttestationMode {
match self {
AttestationQuote::DstackTdx { .. } => AttestationMode::DstackTdx,
AttestationQuote::DstackAmdSevSnp { .. } => AttestationMode::DstackAmdSevSnp,
AttestationQuote::DstackGcpTdx { .. } => AttestationMode::DstackGcpTdx,
AttestationQuote::DstackNitroEnclave { .. } => AttestationMode::DstackNitroEnclave,
AttestationQuote::DstackTdx(_) => AttestationMode::DstackTdx,
AttestationQuote::DstackAmdSevSnp(_) => AttestationMode::DstackAmdSevSnp,
AttestationQuote::DstackGcpTdx(_) => AttestationMode::DstackGcpTdx,
AttestationQuote::DstackNitroEnclave(_) => AttestationMode::DstackNitroEnclave,
}
}
}
Expand Down Expand Up @@ -1122,8 +1132,29 @@ impl<T> Attestation<T> {
/// Get TDX event log string with RTMR[0-2] payloads stripped to reduce size.
/// Only digests are kept for boot-time events; runtime events (RTMR3) retain full payload.
pub fn get_tdx_event_log_string(&self) -> Option<String> {
self.get_tdx_event_log_string_for_config("")
}

/// 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> {
self.tdx_quote().map(|q| {
let stripped: Vec<_> = q.event_log.iter().map(|e| e.stripped()).collect();
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) {
stripped.event_payload = e.event_payload.clone();
}
stripped
})
.collect();
serde_json::to_string(&stripped).unwrap_or_default()
})
}
Expand Down Expand Up @@ -1665,6 +1696,14 @@ impl Attestation {
.map_err(|_| anyhow!("Quote lock poisoned"))?;

let mode = AttestationMode::detect()?;
let config = match mode {
AttestationMode::DstackAmdSevSnp
| AttestationMode::DstackTdx
| AttestationMode::DstackGcpTdx => {
read_vm_config().context("Failed to read vm config")?
}
AttestationMode::DstackNitroEnclave => String::new(),
};
let runtime_events = match mode {
AttestationMode::DstackTdx | AttestationMode::DstackGcpTdx => {
RuntimeEvent::read_all().context("Failed to read runtime events")?
Expand Down Expand Up @@ -1713,9 +1752,7 @@ impl Attestation {
let config = match &quote {
AttestationQuote::DstackAmdSevSnp(_)
| AttestationQuote::DstackTdx(_)
| AttestationQuote::DstackGcpTdx(_) => {
read_vm_config().context("Failed to read vm config")?
}
| AttestationQuote::DstackGcpTdx(_) => config,
AttestationQuote::DstackNitroEnclave(quote) => {
let os_image_hash = quote
.decode_image_hash()
Expand Down Expand Up @@ -2002,6 +2039,47 @@ mod tests {
}
}

fn tdx_event(imr: u32, event_type: u32, event_payload: &[u8]) -> TdxEvent {
TdxEvent {
imr,
event_type,
digest: vec![event_type as u8; 48],
event: String::new(),
event_payload: event_payload.to_vec(),
}
}

#[test]
fn tdx_event_log_string_for_lite_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");
};
tdx_quote.event_log = vec![
tdx_event(0, 10, b"ACPI DATA"),
tdx_event(0, 4, b"boot-payload"),
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());
}

#[test]
fn test_to_report_data_with_hash() {
let content_type = QuoteContentType::AppData;
Expand Down
128 changes: 117 additions & 11 deletions dstack-attest/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,61 @@ use tpm_types::TpmQuote;

pub const ATTESTATION_VERSION: u64 = 1;

const TDX_ACPI_DATA_EVENT_TYPE: u32 = 10;
const TDX_ACPI_DATA_EVENT_PAYLOAD: &[u8] = b"ACPI DATA";

pub(crate) fn is_tdx_acpi_data_event(event: &TdxEvent) -> bool {
event.imr == 0
&& event.event_type == TDX_ACPI_DATA_EVENT_TYPE
&& event.event_payload == TDX_ACPI_DATA_EVENT_PAYLOAD
}

pub(crate) fn strip_tdx_runtime_event_log(event_log: Vec<TdxEvent>) -> Vec<TdxEvent> {
event_log
.into_iter()
.filter(|event| event.imr == 3)
.map(|event| event.stripped())
.collect()
}

fn strip_tdx_lite_acpi_data_event(event: TdxEvent) -> TdxEvent {
let mut event = event.stripped();
event.event_payload = TDX_ACPI_DATA_EVENT_PAYLOAD.to_vec();
event
}

pub(crate) fn strip_tdx_lite_event_log(event_log: Vec<TdxEvent>) -> Vec<TdxEvent> {
event_log
.into_iter()
.filter_map(|event| {
if is_tdx_acpi_data_event(&event) {
Some(strip_tdx_lite_acpi_data_event(event))
} else if event.imr == 3 {
Some(event.stripped())
} else {
None
}
})
.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)
}

pub(crate) fn strip_tdx_event_log_for_config(
event_log: Vec<TdxEvent>,
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)
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data")]
pub enum PlatformEvidence {
Expand Down Expand Up @@ -92,26 +147,22 @@ impl PlatformEvidence {
}

pub fn into_stripped(self) -> Self {
self.into_stripped_for_config("")
}

pub fn into_stripped_for_config(self, config: &str) -> Self {
match self {
Self::Tdx { quote, event_log } => Self::Tdx {
quote,
event_log: event_log
.into_iter()
.filter(|event| event.imr == 3)
.map(|event| event.stripped())
.collect(),
event_log: strip_tdx_event_log_for_config(event_log, config),
},
Self::GcpTdx {
quote,
event_log,
tpm_quote,
} => Self::GcpTdx {
quote,
event_log: event_log
.into_iter()
.filter(|event| event.imr == 3)
.map(|event| event.stripped())
.collect(),
event_log: strip_tdx_runtime_event_log(event_log),
tpm_quote,
},
other => other,
Expand Down Expand Up @@ -242,9 +293,10 @@ impl Attestation {
}

pub fn into_stripped(self) -> Self {
let config = self.stack.config().to_string();
Self {
version: self.version,
platform: self.platform.into_stripped(),
platform: self.platform.into_stripped_for_config(&config),
stack: self.stack,
}
}
Expand Down Expand Up @@ -414,6 +466,60 @@ mod tests {
);
}

fn boot_event(idx: usize) -> TdxEvent {
TdxEvent {
imr: 0,
event_type: idx as u32,
digest: vec![idx as u8; 48],
event: String::new(),
event_payload: vec![0xff; idx + 1],
}
}

fn acpi_data_event(idx: usize) -> TdxEvent {
TdxEvent {
imr: 0,
event_type: TDX_ACPI_DATA_EVENT_TYPE,
digest: vec![idx as u8; 48],
event: String::new(),
event_payload: TDX_ACPI_DATA_EVENT_PAYLOAD.to_vec(),
}
}

fn runtime_event() -> TdxEvent {
RuntimeEvent {
event: "app-id".into(),
payload: vec![0x42],
}
.into()
}

#[test]
fn lite_stripping_keeps_only_acpi_data_digests_and_runtime_payloads() {
let mut event_log = (0..20).map(boot_event).collect::<Vec<_>>();
event_log[3] = acpi_data_event(3);
event_log[8] = acpi_data_event(8);
event_log[15] = acpi_data_event(15);
event_log.push(runtime_event());

let stripped = strip_tdx_lite_event_log(event_log);

assert_eq!(stripped.len(), 4);
assert_eq!(
stripped[0..3]
.iter()
.map(|event| event.digest.clone())
.collect::<Vec<_>>(),
vec![vec![3u8; 48], vec![8u8; 48], vec![15u8; 48]]
);
assert!(stripped[0..3]
.iter()
.all(|event| event.imr == 0 && event.event_payload == TDX_ACPI_DATA_EVENT_PAYLOAD));
assert_eq!(stripped[3].imr, 3);
assert_eq!(stripped[3].event, "app-id");
assert_eq!(stripped[3].event_payload, vec![0x42]);
}

#[test]
fn sev_snp_with_report_data_patches_report_and_stack() {
let mut report = vec![0x11; 1184];
Expand Down
4 changes: 2 additions & 2 deletions dstack-attest/tests/sev_snp_verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ fn verify_sev_snp_attestation_bin() {
// image build's digest.sev.txt.
assert_eq!(
hex::encode(&binding.os_image_hash),
"32b4767373ad7fa0f9c418925006194d5c3f5619529f309fe81156789fecd8bc",
"b6e8403b8f6167bcef4e39aa1039d8728fe624532ca6cedf2625a87fac2e5fda",
"derived os_image_hash"
);
// The HOST_DATA-bound app identity is recovered from the mr_config document.
Expand All @@ -111,7 +111,7 @@ fn verify_sev_snp_attestation_bin() {
// Forged / tampered quote coverage (all offline, using the real fixture).
// ---------------------------------------------------------------------------

const OS_IMAGE_HASH: &str = "32b4767373ad7fa0f9c418925006194d5c3f5619529f309fe81156789fecd8bc";
const OS_IMAGE_HASH: &str = "b6e8403b8f6167bcef4e39aa1039d8728fe624532ca6cedf2625a87fac2e5fda";

fn decoded_attestation() -> dstack_attest::attestation::Attestation {
let versioned =
Expand Down
5 changes: 2 additions & 3 deletions dstack-mr/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,8 @@ struct MachineConfig {
#[arg(long)]
qemu_version: Option<String>,

/// dstack OS version (MAJOR.MINOR.PATCH), used to pick the OVMF measurement layout.
/// 0.5.10 <= ver < 0.6.0 and ver >= 0.6.1 use the edk2-stable202505 layout; everything
/// else uses the legacy layout. If omitted, falls back to `image_info.version`.
/// dstack OS version (MAJOR.MINOR.PATCH), validated before using the supported OVMF
/// measurement layout. If omitted, falls back to `image_info.version`.
#[arg(long)]
dstack_os_version: Option<String>,

Expand Down
Loading
Loading