From 606b9e8bab0c21c25e9e84056887bb291ab4e70a Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Sat, 29 Apr 2023 22:09:08 +0200 Subject: [PATCH] stub(tpm): Measure "UKI" (i.e. all unified sections in our stub) --- rust/stub/Cargo.lock | 7 ++ rust/stub/Cargo.toml | 2 + rust/stub/src/main.rs | 25 ++++++- rust/stub/src/measure.rs | 65 ++++++++++++++++++ rust/stub/src/pe_section.rs | 24 ++++--- rust/stub/src/tpm.rs | 106 ++++++++++++++++++++++++++++++ rust/stub/src/unified_sections.rs | 40 +++++++++++ 7 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 rust/stub/src/measure.rs create mode 100644 rust/stub/src/tpm.rs create mode 100644 rust/stub/src/unified_sections.rs diff --git a/rust/stub/Cargo.lock b/rust/stub/Cargo.lock index 6c21c4b..c29adee 100644 --- a/rust/stub/Cargo.lock +++ b/rust/stub/Cargo.lock @@ -92,6 +92,7 @@ dependencies = [ "bitflags 2.2.1", "goblin", "log", + "sha1_smol", "sha2", "uefi", "uefi-services", @@ -176,6 +177,12 @@ dependencies = [ "syn", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.6" diff --git a/rust/stub/Cargo.toml b/rust/stub/Cargo.toml index bceed92..d26badb 100644 --- a/rust/stub/Cargo.toml +++ b/rust/stub/Cargo.toml @@ -17,6 +17,8 @@ log = { version = "0.4.17", default-features = false, features = [ "max_level_in # Use software implementation because the UEFI target seems to need it. sha2 = { version = "0.10.6", default-features = false, features = ["force-soft"] } +# SHA1 for TPM TCG interface version 1. +sha1_smol = "1.0.0" [profile.release] opt-level = "s" diff --git a/rust/stub/src/main.rs b/rust/stub/src/main.rs index 25c5275..a36f441 100644 --- a/rust/stub/src/main.rs +++ b/rust/stub/src/main.rs @@ -6,16 +6,21 @@ extern crate alloc; mod efivars; mod linux_loader; +mod measure; mod pe_loader; mod pe_section; +mod tpm; mod uefi_helpers; +mod unified_sections; use alloc::vec::Vec; use efivars::{export_efi_variables, get_loader_features, EfiLoaderFeatures}; -use log::{debug, info, warn}; +use log::{info, warn}; +use measure::measure_image; use pe_loader::Image; use pe_section::{pe_section, pe_section_as_string}; use sha2::{Digest, Sha256}; +use tpm::tpm_available; use uefi::{ prelude::*, proto::{ @@ -238,10 +243,26 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { warn!("Hash mismatch for initrd!"); } + if tpm_available(system_table.boot_services()) { + info!("TPM available, will proceed to measurements."); + unsafe { + // Iterate over unified sections and measure them + // For now, ignore failures during measurements. + // TODO: in the future, devise a threat model where this can fail + // and ensure this hard-fail correctly. + let _ = measure_image( + &system_table, + booted_image_file(system_table.boot_services()).unwrap(), + ); + // TODO: Measure kernel parameters + // TODO: Measure sysexts + } + } + if let Ok(features) = get_loader_features(system_table.runtime_services()) { if !features.contains(EfiLoaderFeatures::RandomSeed) { // FIXME: process random seed then on the disk. - debug!("Random seed is available, but lanzaboote does not support it yet."); + info!("Random seed is available, but lanzaboote does not support it yet."); } } export_efi_variables(&system_table).expect("Failed to export stub EFI variables"); diff --git a/rust/stub/src/measure.rs b/rust/stub/src/measure.rs new file mode 100644 index 0000000..4db1e44 --- /dev/null +++ b/rust/stub/src/measure.rs @@ -0,0 +1,65 @@ +use log::info; +use uefi::{ + cstr16, + proto::tcg::PcrIndex, + table::{runtime::VariableAttributes, Boot, SystemTable}, +}; + +use crate::{ + efivars::BOOT_LOADER_VENDOR_UUID, pe_section::pe_section_data, tpm::tpm_log_event_ascii, + uefi_helpers::PeInMemory, unified_sections::UnifiedSection, +}; + +const TPM_PCR_INDEX_KERNEL_IMAGE: PcrIndex = PcrIndex(11); + +pub unsafe fn measure_image( + system_table: &SystemTable, + image: PeInMemory, +) -> uefi::Result { + let runtime_services = system_table.runtime_services(); + let boot_services = system_table.boot_services(); + + // SAFETY: We get a slice that represents our currently running + // image and then parse the PE data structures from it. This is + // safe, because we don't touch any data in the data sections that + // might conceivably change while we look at the slice. + // (data sections := all unified sections that can be measured.) + let pe_binary = unsafe { image.as_slice() }; + 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; + } + } + } + } + } + + if measurements > 0 { + // If we did some measurements, expose a variable encoding the PCR where + // we have done the measurements. + runtime_services.set_variable( + cstr16!("StubPcrKernelImage"), + &BOOT_LOADER_VENDOR_UUID, + VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS, + &TPM_PCR_INDEX_KERNEL_IMAGE.0.to_le_bytes(), + )?; + } + + Ok(measurements) +} diff --git a/rust/stub/src/pe_section.rs b/rust/stub/src/pe_section.rs index ba9541b..be23142 100644 --- a/rust/stub/src/pe_section.rs +++ b/rust/stub/src/pe_section.rs @@ -5,8 +5,21 @@ #![allow(clippy::bind_instead_of_map)] use alloc::{borrow::ToOwned, string::String}; +use goblin::pe::section_table::SectionTable; -/// Extracts the data of a section of a loaded PE file. +/// Extracts the data of a section in a loaded PE file +/// based on the section table. +pub fn pe_section_data<'a>(pe_data: &'a [u8], section: &SectionTable) -> Option<&'a [u8]> { + let section_start: usize = section.virtual_address.try_into().ok()?; + + assert!(section.virtual_size <= section.size_of_raw_data); + let section_end: usize = section_start + usize::try_from(section.virtual_size).ok()?; + + Some(&pe_data[section_start..section_end]) +} + +/// Extracts the data of a section of a loaded PE file +/// based on the section name. pub fn pe_section<'a>(pe_data: &'a [u8], section_name: &str) -> Option<&'a [u8]> { let pe_binary = goblin::pe::PE::parse(pe_data).ok()?; @@ -14,14 +27,7 @@ pub fn pe_section<'a>(pe_data: &'a [u8], section_name: &str) -> Option<&'a [u8]> .sections .iter() .find(|s| s.name().map(|n| n == section_name).unwrap_or(false)) - .and_then(|s| { - let section_start: usize = s.virtual_address.try_into().ok()?; - - assert!(s.virtual_size <= s.size_of_raw_data); - let section_end: usize = section_start + usize::try_from(s.virtual_size).ok()?; - - Some(&pe_data[section_start..section_end]) - }) + .and_then(|s| pe_section_data(pe_data, s)) } /// Extracts the data of a section of a loaded PE image and returns it as a string. diff --git a/rust/stub/src/tpm.rs b/rust/stub/src/tpm.rs new file mode 100644 index 0000000..a905516 --- /dev/null +++ b/rust/stub/src/tpm.rs @@ -0,0 +1,106 @@ +use alloc::vec; +use core::mem::{self, MaybeUninit}; +use log::warn; +use uefi::{ + prelude::BootServices, + proto::tcg::{ + v1::{self, Sha1Digest}, + v2, EventType, PcrIndex, + }, + table::boot::ScopedProtocol, +}; + +fn open_capable_tpm2(boot_services: &BootServices) -> uefi::Result> { + let tpm_handle = boot_services.get_handle_for_protocol::()?; + let mut tpm_protocol = boot_services.open_protocol_exclusive::(tpm_handle)?; + + let capabilities = tpm_protocol.get_capability()?; + + /* + * Here's systemd-stub perform a cast to EFI_TCG_BOOT_SERVICE_CAPABILITY + * indicating there could be some quirks to workaround. + * It should probably go to uefi-rs? + if capabilities.structure_version.major == 1 && capabilities.structure_version.minor == 0 { + + }*/ + + if !capabilities.tpm_present() { + warn!("Capability `TPM present` is not there for the existing TPM TCGv2 protocol"); + return Err(uefi::Status::UNSUPPORTED.into()); + } + + Ok(tpm_protocol) +} + +fn open_capable_tpm1(boot_services: &BootServices) -> uefi::Result> { + let tpm_handle = boot_services.get_handle_for_protocol::()?; + let mut tpm_protocol = boot_services.open_protocol_exclusive::(tpm_handle)?; + + let status_check = tpm_protocol.status_check()?; + + if status_check.protocol_capability.tpm_deactivated() + || !status_check.protocol_capability.tpm_present() + { + warn!("Capability `TPM present` is not there or `TPM deactivated` is there for the existing TPM TCGv1 protocol"); + return Err(uefi::Status::UNSUPPORTED.into()); + } + + Ok(tpm_protocol) +} + +pub fn tpm_available(boot_services: &BootServices) -> bool { + open_capable_tpm2(boot_services).is_ok() || open_capable_tpm1(boot_services).is_ok() +} + +/// Log an event in the TPM with `buffer` as data. +/// Returns a boolean whether the measurement has been done or not in case of success. +pub fn tpm_log_event_ascii( + boot_services: &BootServices, + pcr_index: PcrIndex, + buffer: &[u8], + description: &str, +) -> uefi::Result { + if pcr_index.0 == u32::MAX { + return Ok(false); + } + if let Ok(mut tpm2) = open_capable_tpm2(boot_services) { + let required_size = mem::size_of::() + // EventHeader is privateā€¦ + + mem::size_of::() + mem::size_of::() + mem::size_of::() + mem::size_of::() + + description.len(); + + let mut event_buffer = vec![MaybeUninit::::uninit(); required_size]; + let event = v2::PcrEventInputs::new_in_buffer( + event_buffer.as_mut_slice(), + pcr_index, + EventType::IPL, + description.as_bytes(), + )?; + // FIXME: what do we want as flags here? + tpm2.hash_log_extend_event(Default::default(), buffer, event)?; + } else if let Ok(mut tpm1) = open_capable_tpm1(boot_services) { + let required_size = mem::size_of::() + + mem::size_of::() + + mem::size_of::() + + mem::size_of::() + + description.len(); + + let mut event_buffer = vec![MaybeUninit::::uninit(); required_size]; + + // Compute sha1 of the event data + let mut m = sha1_smol::Sha1::new(); + m.update(description.as_bytes()); + + let event = v1::PcrEvent::new_in_buffer( + event_buffer.as_mut_slice(), + pcr_index, + EventType::IPL, + m.digest().bytes(), + description.as_bytes(), + )?; + + tpm1.hash_log_extend_event(event, Some(buffer))?; + } + + Ok(true) +} diff --git a/rust/stub/src/unified_sections.rs b/rust/stub/src/unified_sections.rs new file mode 100644 index 0000000..a8f8357 --- /dev/null +++ b/rust/stub/src/unified_sections.rs @@ -0,0 +1,40 @@ +/// 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 !!! +#[repr(u8)] +pub enum UnifiedSection { + Linux = 0, + OsRel = 1, + CmdLine = 2, + Initrd = 3, + Splash = 4, + Dtb = 5, + PcrSig = 6, + PcrPkey = 7, +} + +impl TryFrom<&str> for UnifiedSection { + type Error = uefi::Error; + fn try_from(value: &str) -> Result { + Ok(match value { + ".linux" => Self::Linux, + ".osrel" => Self::OsRel, + ".cmdline" => Self::CmdLine, + ".initrd" => Self::Initrd, + ".splash" => Self::Splash, + ".dtb" => Self::Dtb, + ".pcrsig" => Self::PcrSig, + ".pcrpkey" => Self::PcrPkey, + _ => return Err(uefi::Status::INVALID_PARAMETER.into()), + }) + } +} + +impl UnifiedSection { + /// Whether this section should be measured into TPM. + pub fn should_be_measured(&self) -> bool { + !matches!(self, UnifiedSection::PcrSig) + } +}