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.

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

use crate::amd_sev_snp::VerifiedAmdSnpReport;
use crate::v1::{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 +597,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 +991,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 @@ -1665,6 +1672,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 +1728,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
132 changes: 121 additions & 11 deletions dstack-attest/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,56 @@ use tpm_types::TpmQuote;

pub const ATTESTATION_VERSION: u64 = 1;

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()
}

pub(crate) fn strip_tdx_measurement_event_log(event_log: Vec<TdxEvent>) -> Vec<TdxEvent> {
let rtmr0_count = event_log.iter().filter(|event| event.imr == 0).count();
let acpi_indexes = if rtmr0_count >= 17 {
[10usize, 11, 12]
} else {
[8usize, 9, 10]
};
let mut rtmr0_index = 0usize;

event_log
.into_iter()
.filter_map(|event| {
if event.imr == 0 {
let keep = acpi_indexes.contains(&rtmr0_index);
rtmr0_index += 1;
keep.then(|| event.stripped())
} else if event.imr == 3 {
Some(event.stripped())
} else {
None
}
})
.collect()
}

pub(crate) fn is_tdx_measurement_config(config: &str) -> bool {
serde_json::from_str::<dstack_types::VmConfig>(config)
.map(|config| config.tdx_attestation_variant.is_measurement())
.unwrap_or(false)
}

pub(crate) fn strip_tdx_event_log_for_config(
event_log: Vec<TdxEvent>,
config: &str,
) -> Vec<TdxEvent> {
if is_tdx_measurement_config(config) {
strip_tdx_measurement_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 +142,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 +288,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 +461,69 @@ 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 runtime_event() -> TdxEvent {
RuntimeEvent {
event: "app-id".into(),
payload: vec![0x42],
}
.into()
}

#[test]
fn measurement_stripping_keeps_only_pre202505_acpi_digests_and_runtime_payloads() {
let mut event_log = (0..13).map(boot_event).collect::<Vec<_>>();
event_log.push(runtime_event());

let stripped = strip_tdx_measurement_event_log(event_log);

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

#[test]
fn measurement_stripping_keeps_only_stable202505_acpi_digests_and_runtime_payloads() {
let mut event_log = (0..17).map(boot_event).collect::<Vec<_>>();
event_log.push(runtime_event());

let stripped = strip_tdx_measurement_event_log(event_log);

assert_eq!(stripped.len(), 4);
assert_eq!(
stripped[0..3]
.iter()
.map(|event| event.digest.clone())
.collect::<Vec<_>>(),
vec![vec![10u8; 48], vec![11u8; 48], vec![12u8; 48]]
);
assert!(stripped[0..3]
.iter()
.all(|event| event.imr == 0 && event.event_payload.is_empty()));
assert_eq!(stripped[3].imr, 3);
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
85 changes: 80 additions & 5 deletions dstack-mr/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ use anyhow::{bail, Context, Result};
use object::pe;
use sha2::{Digest, Sha384};

/// QEMU's TDX setup-header patch places the initrd at a memory-dependent
/// address below this guest-memory size. At and above this threshold the
/// patched kernel Authenticode hash is stable for a given kernel/initrd pair.
pub const TDX_KERNEL_HASH_STABLE_MIN_MEMORY: u64 = 0xB0000000;
/// QEMU's low-memory initrd placement also resolves to the same below-4G
/// placement at exactly 2 GiB, so it shares the high-memory patched kernel hash.
pub const TDX_KERNEL_HASH_COMPAT_2G_MEMORY: u64 = 0x80000000;

pub fn tdx_kernel_hash_uses_precomputed_high_mem(memory_size: u64) -> bool {
memory_size == TDX_KERNEL_HASH_COMPAT_2G_MEMORY
|| memory_size >= TDX_KERNEL_HASH_STABLE_MIN_MEMORY
}

/// Calculates the Authenticode hash of a PE/COFF file
fn authenticode_sha384_hash(data: &[u8]) -> Result<Vec<u8>> {
let lfanew_offset = 0x3c;
Expand Down Expand Up @@ -177,8 +190,8 @@ fn patch_kernel(
0x37ffffff
};

let lowmem = if mem_size < 0xb0000000 {
0xb0000000
let lowmem = if mem_size < TDX_KERNEL_HASH_STABLE_MIN_MEMORY {
TDX_KERNEL_HASH_STABLE_MIN_MEMORY
} else {
0x80000000
};
Expand Down Expand Up @@ -211,16 +224,28 @@ fn patch_kernel(
Ok(kd)
}

/// Compute the first RTMR[1] event digest: the Authenticode SHA-384 hash of the
/// kernel after QEMU applies its setup-header patches.
pub(crate) fn patched_kernel_authenticode_sha384(
kernel_data: &[u8],
initrd_size: u32,
mem_size: u64,
acpi_data_size: u32,
) -> Result<Vec<u8>> {
let kd = patch_kernel(kernel_data, initrd_size, mem_size, acpi_data_size)
.context("Failed to patch kernel")?;
authenticode_sha384_hash(&kd).context("Failed to compute kernel hash")
}

/// Measures a QEMU-patched TDX kernel image.
pub(crate) fn rtmr1_log(
kernel_data: &[u8],
initrd_size: u32,
mem_size: u64,
acpi_data_size: u32,
) -> Result<Vec<Vec<u8>>> {
let kd = patch_kernel(kernel_data, initrd_size, mem_size, acpi_data_size)
.context("Failed to patch kernel")?;
let kernel_hash = authenticode_sha384_hash(&kd).context("Failed to compute kernel hash")?;
let kernel_hash =
patched_kernel_authenticode_sha384(kernel_data, initrd_size, mem_size, acpi_data_size)?;
Ok(vec![
kernel_hash,
measure_sha384(b"Calling EFI Application from Boot Option"),
Expand All @@ -236,3 +261,53 @@ pub(crate) fn measure_cmdline(cmdline: &str) -> Vec<u8> {
utf16_cmdline.extend([0, 0]);
measure_sha384(&utf16_cmdline)
}

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

fn initrd_addr(kernel: &[u8]) -> u32 {
u32::from_le_bytes(kernel[0x218..0x21c].try_into().unwrap())
}

#[test]
fn tdx_kernel_patch_uses_precomputed_digest_at_2g_and_high_memory() {
let mut kernel = vec![0u8; 0x1000];
// Linux boot protocol >= 2.12 with XLF_CAN_BE_LOADED_ABOVE_4G makes
// QEMU derive the initrd address from available low memory.
kernel[0x206..0x208].copy_from_slice(&0x020cu16.to_le_bytes());
kernel[0x236..0x238].copy_from_slice(&0x0040u16.to_le_bytes());

let below_2g = patch_kernel(&kernel, 0x100000, 0x80000000 - 0x1000, 0x28000).unwrap();
let at_2g = patch_kernel(&kernel, 0x100000, 0x80000000, 0x28000).unwrap();
let between_2g_and_high_mem = patch_kernel(
&kernel,
0x100000,
TDX_KERNEL_HASH_STABLE_MIN_MEMORY - 0x1000,
0x28000,
)
.unwrap();
let at_threshold = patch_kernel(
&kernel,
0x100000,
TDX_KERNEL_HASH_STABLE_MIN_MEMORY,
0x28000,
)
.unwrap();
let above_threshold = patch_kernel(
&kernel,
0x100000,
TDX_KERNEL_HASH_STABLE_MIN_MEMORY + 0x4000_0000,
0x28000,
)
.unwrap();

assert_ne!(initrd_addr(&below_2g), initrd_addr(&at_2g));
assert_ne!(
initrd_addr(&between_2g_and_high_mem),
initrd_addr(&at_threshold)
);
assert_eq!(initrd_addr(&at_2g), initrd_addr(&at_threshold));
assert_eq!(initrd_addr(&at_threshold), initrd_addr(&above_threshold));
}
}
2 changes: 2 additions & 0 deletions dstack-mr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ pub type RtmrLogs = [RtmrLog; 3];
mod acpi;
mod kernel;
mod machine;
pub mod measurement;
mod num;
pub mod sev;
mod tdvf;
pub mod tdx;
mod uefi_var;
mod util;

Expand Down
Loading
Loading