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.

71 changes: 71 additions & 0 deletions cc-eventlog/src/tdx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ use crate::{
tcg::TcgEventLog,
};

pub const TDX_ACPI_DATA_EVENT_TYPE: u32 = 10;
pub const TDX_ACPI_DATA_EVENT_PAYLOAD: &[u8] = b"ACPI DATA";
pub const TDX_ACPI_LOADER_EVENT: &str = "acpi-loader";
pub const TDX_ACPI_RSDP_EVENT: &str = "acpi-rsdp";
pub const TDX_ACPI_TABLES_EVENT: &str = "acpi-tables";
pub const TDX_ACPI_DATA_EVENT_NAMES: [&str; 3] = [
TDX_ACPI_LOADER_EVENT,
TDX_ACPI_RSDP_EVENT,
TDX_ACPI_TABLES_EVENT,
];

/// This is the TDX event log format that is used to store the event log in the TDX guest.
/// It is a simplified version of the TCG event log format, containing only a single digest
/// and the raw event data. The IMR index is zero-based, unlike the TCG event log format
Expand Down Expand Up @@ -97,9 +108,69 @@ impl From<RuntimeEvent> for TdxEvent {
}
}

pub 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
}

/// Give dstack's three Pre202505 OVMF ACPI DATA RTMR0 events stable semantic
/// names. The firmware event payload is the same "ACPI DATA" marker for all
/// three entries, so the guest labels them before exposing the event log.
pub fn label_tdx_acpi_data_events(event_logs: &mut [TdxEvent]) {
let mut acpi_idx = 0;
for event in event_logs
.iter_mut()
.filter(|event| is_tdx_acpi_data_event(event))
{
if let Some(name) = TDX_ACPI_DATA_EVENT_NAMES.get(acpi_idx) {
event.event = (*name).to_string();
}
acpi_idx += 1;
}
}

/// Read both boottime and runtime event logs.
pub fn read_event_log() -> Result<Vec<TdxEvent>> {
let mut event_logs = TcgEventLog::decode_from_ccel_file()?.to_cc_event_log()?;
label_tdx_acpi_data_events(&mut event_logs);
event_logs.extend(RuntimeEvent::read_all()?.into_iter().map(Into::into));
Ok(event_logs)
}

#[cfg(test)]
mod tests {
use super::*;

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

#[test]
fn labels_pre202505_acpi_data_events_in_order() {
let mut events = vec![
TdxEvent::new(0, 4, String::new(), vec![0]),
acpi_data_event(1),
acpi_data_event(2),
acpi_data_event(3),
TdxEvent::new(3, DSTACK_RUNTIME_EVENT_TYPE, "app-id".into(), vec![4]),
];

label_tdx_acpi_data_events(&mut events);

let names = events
.iter()
.filter(|event| is_tdx_acpi_data_event(event))
.map(|event| event.event.as_str())
.collect::<Vec<_>>();
assert_eq!(names, TDX_ACPI_DATA_EVENT_NAMES);
assert_eq!(events[0].event, "");
assert_eq!(events[4].event, "app-id");
}
}
25 changes: 25 additions & 0 deletions docs/security/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,31 @@ This is also reflected at the source: the event log shipped alongside an attesta

The reason boot-time event log entries (RTMR0-2) are dropped is that **nothing downstream consumes them**. Verification recomputes the OS-layer measurements directly from the signed `rt_mr0/1/2` values and compares them to independently reproduced expected measurements, so the corresponding boot event log would be redundant. Keeping it would only bloat the RA-TLS certificate and expose extra detail without adding any verification capability. RTMR3, by contrast, is runtime-extended (compose-hash, key-provider, instance-id, and application-emitted events), so its event log is the only one with a real consumer — the replay that proves what was extended into RTMR3.

### Why TDX lite mode does not validate ACPI table contents

TDX lite mode verifies the OS image without downloading the image and without
running QEMU to regenerate ACPI tables. It still uses the three RTMR0 `ACPI
DATA` digests from the attestation event log as measurement inputs. The guest
labels those three events as `acpi-loader`, `acpi-rsdp`, and `acpi-tables`
before exposing the event log, and the verifier checks that the recomputed RTMR
values match the hardware-signed quote. What it does not do is reconstruct and
byte-compare the full ACPI table contents.

This is safe for dstack's threat model because ACPI tables are treated as
untrusted host-provided platform description, not as trusted guest code. The
dangerous executable part of ACPI is AML (ACPI Machine Language): malicious AML
can try to use `SystemMemory` operation regions through the Linux ACPICA
interpreter to read or write guest physical memory. dstack kernels include the
BadAML sandbox patch (`0002-acpi-sandbox-block-aml-systemmemory-ram-access.patch`),
which hooks the ACPI `SystemMemory` region handler, walks the guest page tables,
and denies AML access to encrypted/private guest RAM. AML can only access
unencrypted/shared mappings.

Therefore, an infrastructure operator can still provide bad ACPI data and cause
misconfiguration or denial of service, but unvalidated ACPI/AML cannot tamper
with confidential private memory or extract secrets. That residual availability
risk is already outside dstack's confidentiality/integrity guarantees.

### TCB status is surfaced, not gated, during verification

dstack's `validate_tcb` does not reject a quote based on its TCB status string (`UpToDate`, `OutOfDate`, `ConfigurationNeeded`, `SWHardeningNeeded`, ...). It only enforces hard invariants: debug mode must be off, and the SEAM/service-TD measurements must be well-formed. The verified report carries the `status` field through to the caller.
Expand Down
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
Loading
Loading