diff --git a/crates/cli/src/measure.rs b/crates/cli/src/measure.rs index 7cc6ac6..7781a4b 100644 --- a/crates/cli/src/measure.rs +++ b/crates/cli/src/measure.rs @@ -34,12 +34,6 @@ pub(crate) enum Target { SelfHosted { /// Image file to measure uki: PathBuf, - /// Firmware file - #[arg(long)] - firmware: PathBuf, - /// RAM size (e.g. "2G" or "512M") - #[arg(long, default_value = "2G", value_parser = parse_ram)] - ram: u64, #[arg(long)] debug: bool, }, @@ -61,14 +55,9 @@ pub(crate) fn run(target: Target) -> Result<()> { let hashes = measure::dcap::measure(&load_uki(&uki)?); emit(measure::dcap::gcp::measure(&hashes), debug, MeasurementOutput::Dcap)? } - Target::SelfHosted { uki, firmware, ram, debug } => { + Target::SelfHosted { uki, debug } => { let hashes = measure::dcap::measure(&load_uki(&uki)?); - let fw = std::fs::read(&firmware)?; - emit( - measure::dcap::self_hosted::measure(&hashes, &fw, ram)?, - debug, - MeasurementOutput::Dcap, - )? + emit(measure::dcap::self_hosted::measure(&hashes), debug, MeasurementOutput::Dcap)? } }; println!("{}", to_string_pretty(&out)?); @@ -86,15 +75,3 @@ fn emit( fn load_uki(path: &Path) -> Result { Uki::parse(&std::fs::read(path)?) } - -fn parse_ram(s: &str) -> Result { - let s = s.trim(); - let (num, mult) = if let Some(n) = s.strip_suffix('G') { - (n, 1024 * 1024 * 1024) - } else if let Some(n) = s.strip_suffix('M') { - (n, 1024 * 1024) - } else { - (s, 1) - }; - num.parse::().map(|n| n * mult).map_err(|e| format!("invalid RAM size '{s}': {e}")) -} diff --git a/crates/cli/src/verify.rs b/crates/cli/src/verify.rs index 85bad83..0fbdbe7 100644 --- a/crates/cli/src/verify.rs +++ b/crates/cli/src/verify.rs @@ -22,6 +22,10 @@ pub(crate) struct Args { #[arg(long)] pccs_url: Option, + /// Firmware blob (required for self-hosted Portable measurements) + #[arg(long)] + firmware: Option, + /// Print actual/expected register values on mismatch #[arg(short, long)] debug: bool, @@ -30,6 +34,7 @@ pub(crate) struct Args { pub(crate) fn run(args: Args) -> Result<()> { let expected: MeasurementOutput = serde_json::from_slice(&std::fs::read(&args.measurement)?)?; let evidence: AttestationEvidence = serde_json::from_slice(&read_evidence(args.evidence)?)?; + let firmware = args.firmware.map(std::fs::read).transpose()?; let runtime = tokio::runtime::Runtime::new()?; let pccs = runtime.block_on(async { @@ -38,7 +43,8 @@ pub(crate) fn run(args: Args) -> Result<()> { anyhow::Ok(pccs) })?; - let report_data = verify::verify(&expected, &evidence, &pccs, args.debug)?; + let report_data = + verify::verify(&expected, &evidence, &pccs, firmware.as_deref(), args.debug)?; println!("{}", hex::encode(report_data)); Ok(()) } diff --git a/crates/measure/src/dcap/mod.rs b/crates/measure/src/dcap/mod.rs index d6d7353..4aa4a50 100644 --- a/crates/measure/src/dcap/mod.rs +++ b/crates/measure/src/dcap/mod.rs @@ -7,8 +7,8 @@ pub mod self_hosted; pub mod td_hob; mod gpt; -#[allow(dead_code)] mod tdvf; +pub use tdvf::mrtd_sha384; use serde::Serialize; use sha2::{Digest, Sha384}; diff --git a/crates/measure/src/dcap/self_hosted.rs b/crates/measure/src/dcap/self_hosted.rs index 2849e2b..cf2d133 100644 --- a/crates/measure/src/dcap/self_hosted.rs +++ b/crates/measure/src/dcap/self_hosted.rs @@ -2,8 +2,17 @@ use anyhow::Result; use sha2::Sha384; +use types::AcpiHashes; -use super::{DcapImageHashes, DcapRegisters}; +use super::{ + DcapImageHashes, + DcapRegisters, + build_rtmr2, + gcp::BOOT_0000_HASH, + secure_boot::{EFI_GLOBAL_VARIABLE_GUID, EFI_IMAGE_SECURITY_DATABASE_GUID, secure_boot_hash}, + td_hob, + tdvf::cfv_sha384, +}; use crate::event::{ CALLING_EFI_APP, EXIT_BOOT_SERVICES, @@ -12,22 +21,39 @@ use crate::event::{ SEPARATOR, }; -/// Full self-hosted TDX measurement -pub fn measure( - _hashes: &DcapImageHashes, - _firmware: &[u8], - _ram_bytes: u64, -) -> Result { - // TODO: compute for td-shim? - todo!() +/// Self-hosted RTMR1 and RTMR2 measurements +pub fn measure(hashes: &DcapImageHashes) -> DcapRegisters { + DcapRegisters { rtmr1: build_rtmr1(hashes), rtmr2: build_rtmr2(hashes) } } -/// Generated RTMR1 for self-hosted TDX image +/// RTMR0 rebuilt from firmware blob + platform metadata +pub fn build_rtmr0(fw: &[u8], ram_bytes: u64, acpi: &AcpiHashes) -> Result> { + let global = &EFI_GLOBAL_VARIABLE_GUID; + let db = &EFI_IMAGE_SECURITY_DATABASE_GUID; + let mut mr = Register::new(); + mr.extend_raw(td_hob::digest_from_firmware(fw, ram_bytes)?, "TD HOB"); + mr.extend_raw(cfv_sha384(fw)?, "CFV image"); + mr.extend_raw(secure_boot_hash(global, "SecureBoot", &[]), "SecureBoot"); + mr.extend_raw(secure_boot_hash(global, "PK", &[]), "PK"); + mr.extend_raw(secure_boot_hash(global, "KEK", &[]), "KEK"); + mr.extend_raw(secure_boot_hash(db, "db", &[]), "db"); + mr.extend_raw(secure_boot_hash(db, "dbx", &[]), "dbx"); + mr.extend(SEPARATOR, "separator"); + mr.extend_raw(acpi.loader, "ACPI loader"); + mr.extend_raw(acpi.rsdp, "ACPI RSDP"); + mr.extend_raw(acpi.tables, "ACPI tables"); + mr.extend(&[0x00, 0x00], "boot order"); + mr.extend_raw(BOOT_0000_HASH, "boot 0000"); + Ok(mr) +} + +/// RTMR1 for self-hosted TDX image pub fn build_rtmr1(hashes: &DcapImageHashes) -> Register { let mut mr = Register::new(); - mr.extend_raw(hashes.kernel_authenticode, "kernel authenticode"); + mr.extend_raw(hashes.uki_authenticode, "UKI authenticode"); mr.extend(CALLING_EFI_APP, "calling EFI app"); mr.extend(SEPARATOR, "separator"); + mr.extend_raw(hashes.kernel_authenticode, "kernel authenticode"); mr.extend(EXIT_BOOT_SERVICES, "exit boot services"); mr.extend(EXIT_BOOT_SERVICES_SUCCESS, "exit boot services success"); mr diff --git a/crates/measure/src/dcap/td_hob.rs b/crates/measure/src/dcap/td_hob.rs index 022f497..59c7724 100644 --- a/crates/measure/src/dcap/td_hob.rs +++ b/crates/measure/src/dcap/td_hob.rs @@ -1,13 +1,21 @@ -//! TD HOB digest for GCP c3-standard TDX VMs +//! TD HOB digest computation use anyhow::{Result, ensure}; use sha2::{Digest, Sha384}; +use super::tdvf::{SECTION_TYPE_TD_HOB, SECTION_TYPE_TEMP_MEM, tdx_metadata_sections}; + const TEMPLATE: &[u8; HASH_LEN] = include_bytes!("../../assets/td_hob_template.bin"); const HASH_LEN: usize = 0x248; const RESOURCE_LENGTH_OFFSET: usize = 0x240; const GIB: u64 = 1 << 30; +const LOW_MEM_TOP: u64 = 0x8000_0000; +const HIGH_MEM_START: u64 = 0x1_0000_0000; +/// RAM threshold where qemu splits memory +const HIGH_MEM_THRESHOLD: u64 = 0xB000_0000; + +/// TD HOB digest for GCP c3-standard TDX VMs pub fn digest(ram_bytes: u64) -> Result<[u8; 48]> { ensure!(ram_bytes > 3 * GIB, "RAM must be > 3 GiB, got {ram_bytes} B"); let above_4g = ram_bytes - 3 * GIB; @@ -16,6 +24,61 @@ pub fn digest(ram_bytes: u64) -> Result<[u8; 48]> { Ok(Sha384::digest(buf).into()) } +/// TD HOB digest reconstructed from TDVF metadata for self-hosted TDX +pub fn digest_from_firmware(fw: &[u8], ram_bytes: u64) -> Result<[u8; 48]> { + let mut accepted = Vec::new(); + let mut td_hob_base = 0x80_9000u64; + for s in tdx_metadata_sections(fw)? { + if matches!(s.kind, SECTION_TYPE_TD_HOB | SECTION_TYPE_TEMP_MEM) { + accepted.push((s.memory_address, s.memory_address + s.memory_data_size)); + } + if s.kind == SECTION_TYPE_TD_HOB { + td_hob_base = s.memory_address; + } + } + accepted.sort(); + + let mut hob = vec![0u8; 56]; + hob[0] = 0x01; // HobType = EFI_HOB_TYPE_HANDOFF + hob[2..4].copy_from_slice(&56u16.to_le_bytes()); // HobLength + hob[8..12].copy_from_slice(&9u32.to_le_bytes()); // Version + + let mut cursor = 0u64; + for (start, end) in accepted { + if cursor < start { + push_memory_range(&mut hob, false, cursor, start - cursor); + } + push_memory_range(&mut hob, true, start, end - start); + cursor = end; + } + ensure!(cursor <= ram_bytes, "accepted regions exceed RAM ({cursor:#x} > {ram_bytes:#x})"); + + let low_end = if ram_bytes >= HIGH_MEM_THRESHOLD { LOW_MEM_TOP } else { ram_bytes }; + if cursor < low_end { + push_memory_range(&mut hob, false, cursor, low_end - cursor); + } + if ram_bytes >= HIGH_MEM_THRESHOLD { + push_memory_range(&mut hob, false, HIGH_MEM_START, ram_bytes - LOW_MEM_TOP); + } + + let end_of_hob_list = td_hob_base + hob.len() as u64 + 8; + hob[48..56].copy_from_slice(&end_of_hob_list.to_le_bytes()); + Ok(Sha384::digest(&hob).into()) +} + +/// Append an EFI_HOB_RESOURCE_DESCRIPTOR for one physical memory range +fn push_memory_range(hob: &mut Vec, accepted: bool, start: u64, length: u64) { + hob.extend_from_slice(&[0x03, 0x00]); // HobType = EFI_HOB_TYPE_RESOURCE_DESCRIPTOR + hob.extend_from_slice(&48u16.to_le_bytes()); // HobLength + hob.extend_from_slice(&[0u8; 4]); // Reserved + hob.extend_from_slice(&[0u8; 16]); // Owner + hob.push(if accepted { 0x00 } else { 0x07 }); // ResourceType + hob.extend_from_slice(&[0u8; 3]); // padding + hob.extend_from_slice(&7u32.to_le_bytes()); // ResourceAttribute + hob.extend_from_slice(&start.to_le_bytes()); + hob.extend_from_slice(&length.to_le_bytes()); +} + #[cfg(test)] mod tests { use hex_literal::hex; diff --git a/crates/measure/src/dcap/tdvf.rs b/crates/measure/src/dcap/tdvf.rs index a335eb8..d41456f 100644 --- a/crates/measure/src/dcap/tdvf.rs +++ b/crates/measure/src/dcap/tdvf.rs @@ -16,6 +16,49 @@ const FW_GUID_ENTRY_SIZE: usize = 18; // u16 size + 16-byte GUID const TDX_METADATA_OFFSET_GUID: &str = "e47a6535-984a-4798-865e-4685a7bf8ec2"; const SECTION_TYPE_CFV: u32 = 1; +pub(super) const SECTION_TYPE_TD_HOB: u32 = 2; +pub(super) const SECTION_TYPE_TEMP_MEM: u32 = 3; + +const PAGE_SIZE: u64 = 0x1000; +const MR_EXTEND_GRANULARITY: usize = 0x100; +const ATTRIBUTE_MR_EXTEND: u32 = 0x01; +const ATTRIBUTE_PAGE_AUG: u32 = 0x02; + +/// MRTD value for a TD built from this firmware (single-pass page ordering, QEMU >= 9.0) +pub fn mrtd_sha384(fw: &[u8]) -> Result<[u8; 48]> { + let mut h = Sha384::new(); + for s in tdx_metadata_sections(fw)? { + let num_pages = s.memory_data_size / PAGE_SIZE; + for page in 0..num_pages { + let page_gpa = s.memory_address + page * PAGE_SIZE; + if s.attributes & ATTRIBUTE_PAGE_AUG == 0 { + extend_tdx_op(&mut h, b"MEM.PAGE.ADD", page_gpa); + } + if s.attributes & ATTRIBUTE_MR_EXTEND != 0 { + for chunk in 0..(PAGE_SIZE as usize / MR_EXTEND_GRANULARITY) { + let gpa = page_gpa + (chunk * MR_EXTEND_GRANULARITY) as u64; + extend_tdx_op(&mut h, b"MR.EXTEND", gpa); + let start = s.image_offset as usize + + (page * PAGE_SIZE) as usize + + chunk * MR_EXTEND_GRANULARITY; + let end = start + MR_EXTEND_GRANULARITY; + if end > fw.len() { + bail!("section data out of bounds: {start}..{end} of {}", fw.len()); + } + h.update(&fw[start..end]); + } + } + } + } + Ok(h.finalize().into()) +} + +fn extend_tdx_op(h: &mut Sha384, op: &[u8], gpa: u64) { + let mut buf = [0u8; 128]; + buf[..op.len()].copy_from_slice(op); + buf[16..24].copy_from_slice(&gpa.to_le_bytes()); + h.update(buf); +} /// SHA-384 of the Configuration Firmware Volume section of an OVMF blob /// This is the value measured into RTMR0 as EV_EFI_PLATFORM_FIRMWARE_BLOB2 @@ -39,13 +82,16 @@ fn configuration_firmware_volume(fw: &[u8]) -> Result<&[u8]> { } #[derive(Debug)] -struct Section { - image_offset: u32, - raw_data_size: u32, - kind: u32, +pub(super) struct Section { + pub(super) image_offset: u32, + pub(super) raw_data_size: u32, + pub(super) memory_address: u64, + pub(super) memory_data_size: u64, + pub(super) kind: u32, + pub(super) attributes: u32, } -fn tdx_metadata_sections(fw: &[u8]) -> Result> { +pub(super) fn tdx_metadata_sections(fw: &[u8]) -> Result> { let offset = tdx_metadata_offset(fw)?; if offset > fw.len() { bail!("TDX metadata offset {} > firmware size {}", offset, fw.len()); @@ -70,7 +116,10 @@ fn tdx_metadata_sections(fw: &[u8]) -> Result> { sections.push(Section { image_offset: u32::from_le_bytes(cursor[0..4].try_into().unwrap()), raw_data_size: u32::from_le_bytes(cursor[4..8].try_into().unwrap()), + memory_address: u64::from_le_bytes(cursor[8..16].try_into().unwrap()), + memory_data_size: u64::from_le_bytes(cursor[16..24].try_into().unwrap()), kind: u32::from_le_bytes(cursor[24..28].try_into().unwrap()), + attributes: u32::from_le_bytes(cursor[28..32].try_into().unwrap()), }); cursor = &cursor[32..]; } diff --git a/crates/verify/src/lib.rs b/crates/verify/src/lib.rs index 2cb32a0..cb98372 100644 --- a/crates/verify/src/lib.rs +++ b/crates/verify/src/lib.rs @@ -4,6 +4,7 @@ pub mod azure; pub mod dcap; pub mod gcp; +pub mod self_hosted; use std::time::{SystemTime, UNIX_EPOCH}; @@ -19,10 +20,11 @@ pub fn verify( expected: &MeasurementOutput, evidence: &AttestationEvidence, pccs: &Pccs, + firmware: Option<&[u8]>, debug: bool, ) -> Result<[u8; 64], VerifyError> { let time = SystemTime::now().duration_since(UNIX_EPOCH).expect("time before epoch").as_secs(); - verify_at(expected, evidence, pccs, time, debug) + verify_at(expected, evidence, pccs, firmware, time, debug) } /// Same as [`verify`] but takes an explicit time argument @@ -31,6 +33,7 @@ pub fn verify_at( expected: &MeasurementOutput, evidence: &AttestationEvidence, pccs: &Pccs, + firmware: Option<&[u8]>, time: u64, debug: bool, ) -> Result<[u8; 64], VerifyError> { @@ -38,8 +41,17 @@ pub fn verify_at( (MeasurementOutput::Portable(p), AttestationType::GcpTdx) => { gcp::verify_portable(&p.dcap, &evidence.platform, &evidence.quote, pccs, time, debug) } - (MeasurementOutput::Portable(_), AttestationType::SelfHostedTdx) => { - Err(VerifyError::SelfHostedRebuildNotImplemented) + (MeasurementOutput::Portable(p), AttestationType::SelfHostedTdx) => { + let firmware = firmware.ok_or(VerifyError::MissingFirmware)?; + self_hosted::verify_portable( + &p.dcap, + &evidence.platform, + firmware, + &evidence.quote, + pccs, + time, + debug, + ) } #[cfg(feature = "azure")] (MeasurementOutput::Portable(p), AttestationType::AzureTdx) => { @@ -159,8 +171,8 @@ pub enum VerifyError { RegisterMismatch(Vec<&'static str>), #[error("Platform metadata is missing ACPI hashes")] MissingAcpi, - #[error("Self-hosted register reconstruction is not yet implemented")] - SelfHostedRebuildNotImplemented, + #[error("Firmware blob required for self-hosted register verification")] + MissingFirmware, #[cfg(not(feature = "azure"))] #[error("Azure verification requested but `azure` feature is not enabled")] AzureFeatureDisabled, diff --git a/crates/verify/src/self_hosted.rs b/crates/verify/src/self_hosted.rs new file mode 100644 index 0000000..d1658eb --- /dev/null +++ b/crates/verify/src/self_hosted.rs @@ -0,0 +1,48 @@ +//! Self-hosted TDX register verification + +use measure::dcap::{build_rtmr2, mrtd_sha384, self_hosted}; +use pccs::Pccs; +use types::{DcapImageHashes, PlatformMetadata}; + +use crate::{VerifyError, dcap, report_mismatch}; + +pub fn verify_portable( + image_hashes: &DcapImageHashes, + platform: &PlatformMetadata, + firmware: &[u8], + quote: &[u8], + pccs: &Pccs, + time: u64, + debug: bool, +) -> Result<[u8; 64], VerifyError> { + let raw = dcap::validate_quote_at(quote, pccs, time)?; + let acpi = platform.acpi.as_ref().ok_or(VerifyError::MissingAcpi)?; + + let expected_mrtd = mrtd_sha384(firmware)?; + let expected_rtmr0 = self_hosted::build_rtmr0(firmware, platform.ram_bytes, acpi)?; + let expected_rtmr1 = self_hosted::build_rtmr1(image_hashes); + let expected_rtmr2 = build_rtmr2(image_hashes); + + let mut mismatches = Vec::new(); + if raw.mrtd != expected_mrtd { + report_mismatch(debug, "MRTD", &raw.mrtd, &expected_mrtd); + mismatches.push("MRTD"); + } + for (name, actual, expected) in [ + ("RTMR0", raw.rtmr0, &expected_rtmr0), + ("RTMR1", raw.rtmr1, &expected_rtmr1), + ("RTMR2", raw.rtmr2, &expected_rtmr2), + ] { + if actual != expected.value() { + report_mismatch(debug, name, &actual, &expected.value()); + if debug { + eprintln!(" events: {:#?}", expected.debug_json()); + } + mismatches.push(name); + } + } + if !mismatches.is_empty() { + return Err(VerifyError::RegisterMismatch(mismatches)); + } + Ok(raw.report_data) +}