diff --git a/rust/uefi/linux-bootloader/src/measure.rs b/rust/uefi/linux-bootloader/src/measure.rs index 5e37a7d3..912639de 100644 --- a/rust/uefi/linux-bootloader/src/measure.rs +++ b/rust/uefi/linux-bootloader/src/measure.rs @@ -1,9 +1,10 @@ -use alloc::{string::ToString, vec::Vec}; +use alloc::{collections::BTreeMap, ffi::CString, string::ToString}; use log::info; use uefi::{ cstr16, proto::tcg::PcrIndex, table::{runtime::VariableAttributes, Boot, SystemTable}, + CString16, }; use crate::{ @@ -26,35 +27,83 @@ pub fn measure_image(system_table: &SystemTable, image: PeInMemory) -> uef let pe = goblin::pe::PE::parse(pe_binary).map_err(|_err| uefi::Status::LOAD_ERROR)?; let mut measurements = 0; - for section in pe.sections { - let section_name = section.name().map_err(|_err| uefi::Status::UNSUPPORTED)?; - if let Ok(unified_section) = UnifiedSection::try_from(section_name) { - // UNSTABLE: && in the previous if is an unstable feature - // https://github.com/rust-lang/rust/issues/53667 - if unified_section.should_be_measured() { - // Here, perform the TPM log event in ASCII. - if let Some(data) = pe_section_data(pe_binary, §ion) { - info!("Measuring section `{}`...", section_name); - if tpm_log_event_ascii( - boot_services, - TPM_PCR_INDEX_KERNEL_IMAGE, - data, - section_name, - )? { - measurements += 1; - } - } - } + + // Match behaviour of systemd-stub (see src/boot/efi/stub.c in systemd) + // The encoding as well as the ordering of measurements is critical. + // + // References: + // + // "TPM2 PCR Measurements Made by systemd", https://systemd.io/TPM2_PCR_MEASUREMENTS/ + // Section: PCR Measurements Made by systemd-stub (UEFI) + // - PCR 11, EV_IPL, “PE Section Name” + // - PCR 11, EV_IPL, “PE Section Data” + // + // Unified Kernel Image (UKI) specification, UAPI Group, + // https://uapi-group.org/specifications/specs/unified_kernel_image/#uki-tpm-pcr-measurements + // + // Citing from "UKI TPM PCR Measurements": + // On systems with a Trusted Platform Module (TPM) the UEFI boot stub shall measure the sections listed above, + // starting from the .linux section, in the order as listed (which should be considered the canonical order). + // The .pcrsig section is not measured. + // + // For each section two measurements shall be made into PCR 11 with the event code EV_IPL: + // - The section name in ASCII (including one trailing NUL byte) + // - The (binary) section contents + // + // The above should be repeated for every section defined above, so that the measurements are interleaved: + // section name followed by section data, followed by the next section name and its section data, and so on. + + // NOTE: The order of measurements is important, so the use of BTreeMap is intentional here. + let ordered_sections: BTreeMap<_, _> = pe + .sections + .iter() + .filter_map(|section| { + let section_name = section.name().ok()?; + let unified_section = UnifiedSection::try_from(section_name).ok()?; + unified_section.should_be_measured().then_some((unified_section, section)) + }) + .collect(); + + for (unified_section, section) in ordered_sections { + let section_name = unified_section.name(); + + info!("Measuring section `{}`...", section_name); + + // First measure the section name itself + // This needs to be an UTF-8 encoded string with a trailing null byte + // + // As per reference: + // "Measured hash covers the PE section name in ASCII (including a trailing NUL byte!)." + let section_name_cstr_utf8 = unified_section.name_cstr(); + + if tpm_log_event_ascii( + boot_services, + TPM_PCR_INDEX_KERNEL_IMAGE, + section_name_cstr_utf8.as_bytes_with_nul(), + section_name, + )? { + measurements += 1; + } + + // Then measure the section contents. + let Some(data) = pe_section_data(pe_binary, section) else { + continue; + }; + + if tpm_log_event_ascii( + boot_services, + TPM_PCR_INDEX_KERNEL_IMAGE, + data, + section_name, + )? { + measurements += 1; } } if measurements > 0 { - let pcr_index_encoded = TPM_PCR_INDEX_KERNEL_IMAGE - .0 - .to_string() - .encode_utf16() - .flat_map(|c| c.to_le_bytes()) - .collect::>(); + let pcr_index_encoded = + CString16::try_from(TPM_PCR_INDEX_KERNEL_IMAGE.0.to_string().as_str()) + .map_err(|_err| uefi::Status::UNSUPPORTED)?; // If we did some measurements, expose a variable encoding the PCR where // we have done the measurements. @@ -62,7 +111,7 @@ pub fn measure_image(system_table: &SystemTable, image: PeInMemory) -> uef cstr16!("StubPcrKernelImage"), &BOOT_LOADER_VENDOR_UUID, VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS, - &pcr_index_encoded, + pcr_index_encoded.as_bytes(), )?; } diff --git a/rust/uefi/linux-bootloader/src/unified_sections.rs b/rust/uefi/linux-bootloader/src/unified_sections.rs index a8f83576..32b81b54 100644 --- a/rust/uefi/linux-bootloader/src/unified_sections.rs +++ b/rust/uefi/linux-bootloader/src/unified_sections.rs @@ -1,8 +1,11 @@ +use alloc::ffi::CString; + /// List of PE sections that have a special meaning with respect to /// UKI specification. /// This is the canonical order in which they are measured into TPM /// PCR 11. /// !!! DO NOT REORDER !!! +#[derive(PartialEq, Eq, PartialOrd, Ord)] #[repr(u8)] pub enum UnifiedSection { Linux = 0, @@ -37,4 +40,26 @@ impl UnifiedSection { pub fn should_be_measured(&self) -> bool { !matches!(self, UnifiedSection::PcrSig) } + + /// The canonical section name. + pub fn name(&self) -> &'static str { + match self { + UnifiedSection::Linux => ".linux", + UnifiedSection::OsRel => ".osrel", + UnifiedSection::CmdLine => ".cmdline", + UnifiedSection::Initrd => ".initrd", + UnifiedSection::Splash => ".splash", + UnifiedSection::Dtb => ".dtb", + UnifiedSection::PcrSig => ".pcrsig", + UnifiedSection::PcrPkey => ".pcrpkey", + } + } + + /// The section name as a `CString`. + pub fn name_cstr(&self) -> CString { + // This should never panic: + // CString::new() only returns an error on strings containing a null byte, + // and we only call it on strings we specified above + CString::new(self.name()).expect("section name should not contain a null byte") + } }