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
27 changes: 2 additions & 25 deletions crates/cli/src/measure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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)?);
Expand All @@ -86,15 +75,3 @@ fn emit<M: Measurement>(
fn load_uki(path: &Path) -> Result<Uki> {
Uki::parse(&std::fs::read(path)?)
}

fn parse_ram(s: &str) -> Result<u64, String> {
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::<u64>().map(|n| n * mult).map_err(|e| format!("invalid RAM size '{s}': {e}"))
}
8 changes: 7 additions & 1 deletion crates/cli/src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ pub(crate) struct Args {
#[arg(long)]
pccs_url: Option<String>,

/// Firmware blob (required for self-hosted Portable measurements)
#[arg(long)]
firmware: Option<PathBuf>,

/// Print actual/expected register values on mismatch
#[arg(short, long)]
debug: bool,
Expand All @@ -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 {
Expand All @@ -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(())
}
Expand Down
2 changes: 1 addition & 1 deletion crates/measure/src/dcap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
48 changes: 37 additions & 11 deletions crates/measure/src/dcap/self_hosted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -12,22 +21,39 @@ use crate::event::{
SEPARATOR,
};

/// Full self-hosted TDX measurement
pub fn measure(
_hashes: &DcapImageHashes,
_firmware: &[u8],
_ram_bytes: u64,
) -> Result<DcapRegisters> {
// 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<Register<Sha384>> {
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<Sha384> {
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
Expand Down
65 changes: 64 additions & 1 deletion crates/measure/src/dcap/td_hob.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<u8>, 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;
Expand Down
59 changes: 54 additions & 5 deletions crates/measure/src/dcap/tdvf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Vec<Section>> {
pub(super) fn tdx_metadata_sections(fw: &[u8]) -> Result<Vec<Section>> {
let offset = tdx_metadata_offset(fw)?;
if offset > fw.len() {
bail!("TDX metadata offset {} > firmware size {}", offset, fw.len());
Expand All @@ -70,7 +116,10 @@ fn tdx_metadata_sections(fw: &[u8]) -> Result<Vec<Section>> {
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..];
}
Expand Down
22 changes: 17 additions & 5 deletions crates/verify/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
pub mod azure;
pub mod dcap;
pub mod gcp;
pub mod self_hosted;

use std::time::{SystemTime, UNIX_EPOCH};

Expand All @@ -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
Expand All @@ -31,15 +33,25 @@ pub fn verify_at(
expected: &MeasurementOutput,
evidence: &AttestationEvidence,
pccs: &Pccs,
firmware: Option<&[u8]>,
time: u64,
debug: bool,
) -> Result<[u8; 64], VerifyError> {
match (expected, evidence.platform.attestation_type) {
(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) => {
Expand Down Expand Up @@ -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,
Expand Down
Loading