From 8890bf7f09f6efa38f69734a11e77caf171d55df Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Sun, 30 Apr 2023 16:52:28 +0200 Subject: [PATCH 01/19] feat: add cpio packing for companion files --- rust/stub/Cargo.toml | 28 ++ rust/stub/src/main.rs | 354 +++++++++++++++++++++++ rust/stub/src/measure.rs | 60 ++++ rust/uefi/Cargo.lock | 15 + rust/uefi/linux-bootloader/src/cpio.rs | 311 ++++++++++++++++++++ rust/uefi/linux-bootloader/src/initrd.rs | 42 +++ 6 files changed, 810 insertions(+) create mode 100644 rust/stub/Cargo.toml create mode 100644 rust/stub/src/main.rs create mode 100644 rust/stub/src/measure.rs create mode 100644 rust/uefi/linux-bootloader/src/cpio.rs create mode 100644 rust/uefi/linux-bootloader/src/initrd.rs diff --git a/rust/stub/Cargo.toml b/rust/stub/Cargo.toml new file mode 100644 index 00000000..9ed6c72b --- /dev/null +++ b/rust/stub/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "lanzaboote_stub" +version = "0.1.0" +edition = "2021" +publish = false +# For UEFI target +rust-version = "1.68" + +[dependencies] +uefi = { version = "0.20.0", default-features = false, features = [ "alloc", "global_allocator" ] } +uefi-services = { version = "0.17.0", default-features = false, features = [ "panic_handler", "logger" ] } +goblin = { version = "0.6.1", default-features = false, features = [ "pe64", "alloc" ]} +bitflags = "2.2.1" + +# Even in debug builds, we don't enable the debug logs, because they generate a lot of spam from goblin. +log = { version = "0.4.17", default-features = false, features = [ "max_level_info", "release_max_level_warn" ]} + +# 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" +# std::io for alloc/no_std +# FIXME: I don't want this extra dependency actually. +acid_io = { git = "https://github.com/dataphract/acid_io", default-features = false, features = [ "alloc" ] } + +[profile.release] +opt-level = "s" +lto = true diff --git a/rust/stub/src/main.rs b/rust/stub/src/main.rs new file mode 100644 index 00000000..97c36b59 --- /dev/null +++ b/rust/stub/src/main.rs @@ -0,0 +1,354 @@ +#![no_main] +#![no_std] +#![deny(unsafe_op_in_unsafe_fn)] + +extern crate alloc; + +mod linux_loader; +mod pe_loader; +mod pe_section; +mod uefi_helpers; +mod part_discovery; +mod measure; +mod unified_sections; +mod tpm; +mod cpio; +mod initrd; + +use alloc::vec::Vec; +use log::{info, warn, debug}; +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::{ + loaded_image::LoadedImage, + media::file::{File, FileAttribute, FileMode}, + }, + CStr16, CString16, Result, +}; +use uefi_helpers::SystemdLoaderFeatures; + +use crate::{ + linux_loader::InitrdLoader, + uefi_helpers::{booted_image_file, read_all, export_efi_variables, get_loader_features}, + initrd::CompanionInitrd +}; + +type Hash = sha2::digest::Output; + +/// Print the startup logo on boot. +fn print_logo() { + info!( + " + _ _ _ + | | | | | | + | | __ _ _ __ ______ _| |__ ___ ___ | |_ ___ + | |/ _` | '_ \\|_ / _` | '_ \\ / _ \\ / _ \\| __/ _ \\ + | | (_| | | | |/ / (_| | |_) | (_) | (_) | || __/ + |_|\\__,_|_| |_/___\\__,_|_.__/ \\___/ \\___/ \\__\\___| + +" + ); +} + +/// The configuration that is embedded at build time. +/// +/// After lanzaboote is built, lzbt needs to embed configuration +/// into the binary. This struct represents that information. +struct EmbeddedConfiguration { + /// The filename of the kernel to be booted. This filename is + /// relative to the root of the volume that contains the + /// lanzaboote binary. + kernel_filename: CString16, + + /// The cryptographic hash of the kernel. + kernel_hash: Hash, + + /// The filename of the initrd to be passed to the kernel. See + /// `kernel_filename` for how to interpret these filenames. + initrd_filename: CString16, + + /// The cryptographic hash of the initrd. This hash is computed + /// over the whole PE binary, not only the embedded initrd. + initrd_hash: Hash, + + /// The kernel command-line. + cmdline: CString16, +} + +/// Extract a string, stored as UTF-8, from a PE section. +fn extract_string(pe_data: &[u8], section: &str) -> Result { + let string = pe_section_as_string(pe_data, section).ok_or(Status::INVALID_PARAMETER)?; + + Ok(CString16::try_from(string.as_str()).map_err(|_| Status::INVALID_PARAMETER)?) +} + +/// Extract a Blake3 hash from a PE section. +fn extract_hash(pe_data: &[u8], section: &str) -> Result { + let array: [u8; 32] = pe_section(pe_data, section) + .ok_or(Status::INVALID_PARAMETER)? + .try_into() + .map_err(|_| Status::INVALID_PARAMETER)?; + + Ok(array.into()) +} + +impl EmbeddedConfiguration { + fn new(file_data: &[u8]) -> Result { + Ok(Self { + kernel_filename: extract_string(file_data, ".kernelp")?, + kernel_hash: extract_hash(file_data, ".kernelh")?, + + initrd_filename: extract_string(file_data, ".initrdp")?, + initrd_hash: extract_hash(file_data, ".initrdh")?, + + cmdline: extract_string(file_data, ".cmdline")?, + }) + } +} + +/// Boot the Linux kernel without checking the PE signature. +/// +/// We assume that the caller has made sure that the image is safe to +/// be loaded using other means. +fn boot_linux_unchecked( + handle: Handle, + system_table: SystemTable, + kernel_data: Vec, + kernel_cmdline: &CStr16, + initrd_data: Vec, +) -> uefi::Result<()> { + let kernel = + Image::load(system_table.boot_services(), &kernel_data).expect("Failed to load the kernel"); + + let mut initrd_loader = InitrdLoader::new(system_table.boot_services(), handle, initrd_data)?; + + let status = unsafe { kernel.start(handle, &system_table, kernel_cmdline) }; + + initrd_loader.uninstall(system_table.boot_services())?; + status.into() +} + +/// Boot the Linux kernel via the UEFI PE loader. +/// +/// This should only succeed when UEFI Secure Boot is off (or +/// broken...), because the Lanzaboote tool does not sign the kernel. +/// +/// In essence, we can use this routine to detect whether Secure Boot +/// is actually enabled. +fn boot_linux_uefi( + handle: Handle, + system_table: SystemTable, + kernel_data: Vec, + kernel_cmdline: &CStr16, + initrd_data: Vec, +) -> uefi::Result<()> { + let kernel_handle = system_table.boot_services().load_image( + handle, + uefi::table::boot::LoadImageSource::FromBuffer { + buffer: &kernel_data, + file_path: None, + }, + )?; + + let mut kernel_image = system_table + .boot_services() + .open_protocol_exclusive::(kernel_handle)?; + + unsafe { + kernel_image.set_load_options( + kernel_cmdline.as_ptr() as *const u8, + // This unwrap is "safe" in the sense that any + // command-line that doesn't fit 4G is surely broken. + u32::try_from(kernel_cmdline.num_bytes()).unwrap(), + ); + } + + let mut initrd_loader = InitrdLoader::new(system_table.boot_services(), handle, initrd_data)?; + + let status = system_table + .boot_services() + .start_image(kernel_handle) + .status(); + + initrd_loader.uninstall(system_table.boot_services())?; + status.into() +} + +#[entry] +fn main(handle: Handle, mut system_table: SystemTable) -> Status { + uefi_services::init(&mut system_table).unwrap(); + + print_logo(); + + // 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. + let config: EmbeddedConfiguration = unsafe { + EmbeddedConfiguration::new( + booted_image_file(system_table.boot_services()) + .unwrap() + .as_slice(), + ) + .expect("Failed to extract configuration from binary. Did you run lzbt?") + }; + + let kernel_data; + let initrd_data; + + { + let mut file_system = system_table + .boot_services() + .get_image_file_system(handle) + .expect("Failed to get file system handle"); + let mut root = file_system + .open_volume() + .expect("Failed to find ESP root directory"); + + let mut kernel_file = root + .open( + &config.kernel_filename, + FileMode::Read, + FileAttribute::empty(), + ) + .expect("Failed to open kernel file for reading") + .into_regular_file() + .expect("Kernel is not a regular file"); + + kernel_data = read_all(&mut kernel_file).expect("Failed to read kernel file into memory"); + + let mut initrd_file = root + .open( + &config.initrd_filename, + FileMode::Read, + FileAttribute::empty(), + ) + .expect("Failed to open initrd for reading") + .into_regular_file() + .expect("Initrd is not a regular file"); + + initrd_data = read_all(&mut initrd_file).expect("Failed to read kernel file into memory"); + } + + let is_kernel_hash_correct = Sha256::digest(&kernel_data) == config.kernel_hash; + let is_initrd_hash_correct = Sha256::digest(&initrd_data) == config.initrd_hash; + + if !is_kernel_hash_correct { + warn!("Hash mismatch for kernel!"); + } + + if !is_initrd_hash_correct { + warn!("Hash mismatch for initrd!"); + } + + if tpm_available(system_table.boot_services()) { + debug!("TPM available, will proceed to measurements."); + } + + if let Ok(features) = get_loader_features(system_table.runtime_services()) { + if features.contains(SystemdLoaderFeatures::RandomSeed) { + // FIXME: process random seed then on the disk. + debug!("Random seed is available, but lanzaboote does not support it yet."); + } + } + + unsafe { + // Iterate over unified sections and measure them + let _ = measure_image(&system_table, booted_image_file( + system_table.boot_services() + ).unwrap()).expect("Failed to measure the image"); + } + + export_efi_variables(&system_table) + .expect("Failed to export stub EFI variables"); + + let mut initrds = Vec::with_capacity(6); + if let Ok(mut simple_filesystem) = system_table.boot_services().get_image_file_system(handle) { + if let Ok(Some(credentials_initrd)) = cpio::pack_cpio(system_table.boot_services(), + &mut simple_filesystem, + None, + cstr16!(".cred"), + ".extra/credentials", + 0o500, + 0o400, + measure::TPM_PCR_INDEX_KERNEL_PARAMETERS, + "Credentials initrd") { + initrds.push(CompanionInitrd::Credentials(credentials_initrd)); + } + + if let Ok(Some(global_credentials_initrd)) = cpio::pack_cpio(system_table.boot_services(), + &mut simple_filesystem, + Some(cstr16!("\\loader\\credentials")), + cstr16!(".cred"), + ".extra/global_credentials", + 0o500, + 0o400, + measure::TPM_PCR_INDEX_KERNEL_PARAMETERS, + "Global credentials initrd") { + initrds.push(CompanionInitrd::GlobalCredentials(global_credentials_initrd)); + } + + if let Ok(Some(sysext_initrd)) = cpio::pack_cpio(system_table.boot_services(), + &mut simple_filesystem, + None, + cstr16!(".raw"), + ".extra/sysext", + 0o500, + 0o400, + measure::TPM_PCR_INDEX_KERNEL_PARAMETERS, + "System extension initrd") { + initrds.push(CompanionInitrd::SystemExtension(sysext_initrd)); + } + } + + // Let's export any StubPcr EFI variable we might need. + let _ = initrd::export_pcr_efi_variables(&system_table.runtime_services(), initrds); + + if is_kernel_hash_correct && is_initrd_hash_correct { + boot_linux_unchecked( + handle, + system_table, + kernel_data, + &config.cmdline, + initrd_data, + ) + .status() + } else { + // There is no good way to detect whether Secure Boot is + // enabled. This is unfortunate, because we want to give the + // user a way to recover from hash mismatches when Secure Boot + // is off. + // + // So in case we get a hash mismatch, we will try to load the + // Linux image using LoadImage. What happens then depends on + // whether Secure Boot is enabled: + // + // **With Secure Boot**, the firmware will reject loading the + // image with status::SECURITY_VIOLATION. + // + // **Without Secure Boot**, the firmware will just load the + // Linux kernel. + // + // This is the behavior we want. A slight turd is that we + // increase the attack surface here by exposing the unverfied + // Linux image to the UEFI firmware. But in case the PE loader + // of the firmware is broken, we have little hope of security + // anyway. + + warn!("Trying to continue as non-Secure Boot. This will fail when Secure Boot is enabled."); + + boot_linux_uefi( + handle, + system_table, + kernel_data, + &config.cmdline, + initrd_data, + ) + .status() + } +} diff --git a/rust/stub/src/measure.rs b/rust/stub/src/measure.rs new file mode 100644 index 00000000..ce0402c6 --- /dev/null +++ b/rust/stub/src/measure.rs @@ -0,0 +1,60 @@ +use uefi::{table::{runtime::VariableAttributes, Boot, SystemTable}, cstr16, proto::tcg::PcrIndex}; + +use crate::{uefi_helpers::{PeInMemory, SD_LOADER}, pe_section::pe_section_data, unified_sections::UnifiedSection, tpm::tpm_log_event_ascii}; + +/// This is the TPM PCR where lanzastub extends its payload into, before using them. +/// All unified sections: kernel ELF image, embedded initrd, etc. +/// Contrary to PCR4, it contains precomputable data because PCR4 contains +/// the whole PE measured, this PCR is made of "static data". +pub const TPM_PCR_INDEX_KERNEL_IMAGE: PcrIndex = PcrIndex(11); +/// This is the TPM PCR where lanzastub extends the kernel command line and any passed credentials +/// into. +pub const TPM_PCR_INDEX_KERNEL_PARAMETERS: PcrIndex = PcrIndex(12); +/// This is the TPM PCR where lanzastub extends the initrd system extension images into +/// which we pass to the booted kernel. +pub const TPM_PCR_INDEX_INITRD_SYSEXTS: PcrIndex = PcrIndex(13); +/// This is the TPM PCR where we measure the root fs volume key (and maybe /var/'s) if it is split +/// off. +/// Unused at the moment in lanzastub. +pub const TPM_PCR_INDEX_VOLUME_KEY: PcrIndex = PcrIndex(15); + +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(); + + 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) { + 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"), + &SD_LOADER, + VariableAttributes::from_bits_truncate(0x0), + &TPM_PCR_INDEX_KERNEL_IMAGE.0.to_le_bytes() + )?; + } + + Ok(measurements) +} diff --git a/rust/uefi/Cargo.lock b/rust/uefi/Cargo.lock index 94d63e80..ccb5bd58 100644 --- a/rust/uefi/Cargo.lock +++ b/rust/uefi/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "acid_io" +version = "0.1.0" +source = "git+https://github.com/dataphract/acid_io#2d549317fe9253df8b510ba6bbdcfe623a837286" +dependencies = [ + "libc", + "memchr", +] + [[package]] name = "bit_field" version = "0.10.2" @@ -112,6 +121,12 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "plain" version = "0.2.3" diff --git a/rust/uefi/linux-bootloader/src/cpio.rs b/rust/uefi/linux-bootloader/src/cpio.rs new file mode 100644 index 00000000..1fd31231 --- /dev/null +++ b/rust/uefi/linux-bootloader/src/cpio.rs @@ -0,0 +1,311 @@ +use uefi::{CStr16, cstr16, proto::{tcg::PcrIndex, media::fs::SimpleFileSystem}, CString16, prelude::BootServices, table::boot::ScopedProtocol}; +use alloc::{vec, vec::Vec, string::String, format}; +use acid_io::{{Cursor, Write}, Result}; + +use crate::tpm::tpm_log_event_ascii; + +const MAGIC_NUMBER: &[u8; 6] = b"070701"; +const TRAILER_NAME: &str= "TRAILER!!!"; +const CPIO_HEX: &[u8; 16] = b"0123456789abcdef"; + +struct Entry { + name: String, + ino: u32, + mode: u32, + uid: u32, + gid: u32, + nlink: u32, + mtime: u32, + file_size: u32, + dev_major: u32, + dev_minor: u32, + rdev_major: u32, + rdev_minor: u32, +} + +const STATIC_HEADER_LEN: usize = core::mem::size_of::() + - core::mem::size_of::() // remove `name` size, which cannot be derived statically + // unstable for const fn yet: https://github.com/rust-lang/rust/issues/46571 + // + core::mem::size_of_val(MAGIC_NUMBER) + + core::mem::size_of::<&[u8; 6]>() // = 6 + + core::mem::size_of::() // filename size + + 1 // NULL-terminator for filename (\0) + + core::mem::size_of::(); // CRC + +/// Compute the necessary padding based on the provided length +/// It returns None if no padding is necessary. +fn compute_pad4(len: usize) -> Option> { + let overhang = len % 4; + if overhang != 0 { + let repeat = 4 - overhang; + Some(vec![0u8; repeat]) + } else { + None + } +} + +/// Align on N-byte boundary a value. +fn align(value: usize) -> usize { + // Assert if A is a power of 2. + // assert!(A & (A - 1) == 0); + + if value > usize::MAX - (A - 1) { + usize::MAX + } else { + (value + A - 1) & !(A - 1) + } +} + +trait WriteBytesExt : Write { + fn write_cpio_word(&mut self, word: u32) -> Result<()> { + // A CPIO word is the hex(word) written as chars. + // We do it manually because format! will allocate. + self.write_all( + &word.to_le_bytes() + .into_iter() + .enumerate() + // u8 -> usize is always safe. + .map(|(i, c)| CPIO_HEX[((c >> (4 * i)) & 0xF) as usize]) + .rev() + .collect::>() + ) + } + + fn write_cpio_header(&mut self, entry: Entry) -> Result { + let mut header_size = STATIC_HEADER_LEN; + self.write_all(MAGIC_NUMBER)?; + self.write_cpio_word(entry.ino)?; + self.write_cpio_word(entry.mode)?; + self.write_cpio_word(entry.uid)?; + self.write_cpio_word(entry.gid)?; + self.write_cpio_word(entry.nlink)?; + self.write_cpio_word(entry.mtime)?; + self.write_cpio_word(entry.file_size)?; + self.write_cpio_word(entry.dev_major)?; + self.write_cpio_word(entry.dev_minor)?; + self.write_cpio_word(entry.rdev_major)?; + self.write_cpio_word(entry.rdev_minor)?; + self.write_cpio_word((entry.name.len() + 1).try_into().expect("Filename cannot be longer than a 32-bits size"))?; + self.write_cpio_word(0u32)?; // CRC + self.write_all(entry.name.as_bytes())?; + header_size += entry.name.len(); + self.write(&[0u8])?; // Write \0 for the string. + // Pad to a multiple of 4 bytes + if let Some(pad) = compute_pad4(STATIC_HEADER_LEN + entry.name.len()) { + self.write_all(&pad)?; + header_size += pad.len(); + } + Ok(header_size) + } + + fn write_cpio_contents(&mut self, header_size: usize, contents: &[u8]) -> Result { + let mut total_size = header_size + contents.len(); + self.write_all(contents)?; + if let Some(pad) = compute_pad4(total_size) { + self.write_all(&pad)?; + total_size += pad.len(); + } + Ok(total_size) + } + + fn write_cpio_entry(&mut self, header: Entry, contents: &[u8]) -> Result { + let header_size = self.write_cpio_header(header)?; + + self.write_cpio_contents(header_size, contents) + } +} + +impl WriteBytesExt for W {} + +// A Cpio archive with convenience methods +// to pack stuff into it. +pub struct Cpio { + buffer: Vec, + inode_counter: u32 +} + +impl Cpio { + fn pack_one(&mut self, fname: &CStr16, contents: &[u8], target_dir_prefix: &str, access_mode: u32) -> uefi::Result + { + // cpio cannot deal with > 32 bits file sizes + // SAFETY: u32::MAX as usize can wrap if usize < u32. + // hopefully, I will never encounter a usize = u16 in the wild. + if contents.len() > (u32::MAX as usize) { + return Err(uefi::Status::LOAD_ERROR.into()); + } + + // cpio cannot deal with > 2^32 - 1 inodes neither + if self.inode_counter == u32::MAX { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + let mut current_len = STATIC_HEADER_LEN + 1; // 1 for the `/` separator + + if current_len > usize::MAX - target_dir_prefix.len() { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + current_len += target_dir_prefix.len(); + + if current_len > usize::MAX - fname.num_bytes() { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + current_len += fname.num_bytes(); + + // SAFETY: u32::MAX as usize can wrap if usize < u32. + if target_dir_prefix.len() + fname.num_bytes() >= (u32::MAX as usize) { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + // Perform 4-byte alignment of current_len + current_len = align::<4>(current_len); + if current_len == usize::MAX { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + // Perform 4-byte alignment of contents.len() + let aligned_contents_len = align::<4>(contents.len()); + if aligned_contents_len == usize::MAX { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + if current_len > usize::MAX - aligned_contents_len { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + current_len += aligned_contents_len; + + if self.buffer.len() > usize::MAX - current_len { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + // Perform re-allocation now. + let mut elt_buffer: Vec = Vec::with_capacity(current_len); + let mut cur = Cursor::new(&mut elt_buffer); + + self.inode_counter += 1; + // TODO: perform the concat properly + // transform fname to string + cur.write_cpio_entry(Entry { + name: format!("{}/{}", target_dir_prefix, fname), + ino: self.inode_counter, + mode: access_mode | 0100000, // S_IFREG + uid: 0, + gid: 0, + nlink: 1, + mtime: 0, + // This was checked previously. + file_size: contents.len().try_into().unwrap(), + dev_major: 0, + dev_minor: 0, + rdev_major: 0, + rdev_minor: 0 + }, contents).map_err(|_err| uefi::Status::BAD_BUFFER_SIZE)?; + + // Concat the element buffer. + self.buffer.append(&mut elt_buffer); + + Ok(()) + } + fn pack_dir(&mut self, path: &str, access_mode: u32) -> uefi::Result { + // cpio cannot deal with > 2^32 - 1 inodes neither + if self.inode_counter == u32::MAX { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + let mut current_len = STATIC_HEADER_LEN; + if current_len > usize::MAX - path.len() { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + current_len += path.len(); + + // Align the whole header + current_len = align::<4>(current_len); + if self.buffer.len() == usize::MAX || self.buffer.len() > usize::MAX - current_len { + return Err(uefi::Status::OUT_OF_RESOURCES.into()); + } + + let mut elt_buffer: Vec = Vec::with_capacity(current_len); + let mut cur = Cursor::new(&mut elt_buffer); + + self.inode_counter += 1; + cur.write_cpio_header(Entry { + name: path.into(), + ino: self.inode_counter, + mode: access_mode | 0100000, // S_IFREG + uid: 0, + gid: 0, + nlink: 1, + mtime: 0, + file_size: 0, + dev_major: 0, + dev_minor: 0, + rdev_major: 0, + rdev_minor: 0 + }).map_err(|_err| uefi::Status::BAD_BUFFER_SIZE)?; + + // Concat the element buffer. + self.buffer.append(&mut elt_buffer); + + Ok(()) + } + + fn pack_prefix(&mut self, path: &str, dir_mode: u32) -> uefi::Result { + // Iterate over all parts of `path` + // pack_dir it + Ok(()) + } + + fn pack_trailer(&mut self) -> uefi::Result { + self.pack_one(cstr16!("."), TRAILER_NAME.as_bytes(), "", 0) + } +} + + +pub fn pack_cpio( + boot_services: &BootServices, + fs: &mut ScopedProtocol, + dropin_dir: Option<&CStr16>, + match_suffix: &CStr16, + target_dir_prefix: &str, + dir_mode: u32, + access_mode: u32, + tpm_pcr: PcrIndex, + tpm_description: &str) -> uefi::Result> { + match fs.open_volume() { + Ok(root_dir) => { + Ok(None) + }, + // Err(uefi::Status::UNSUPPORTED) => Ok(None), + // Log the error. + Err(err) => Err(err) + } +} + +pub fn pack_cpio_literal( + boot_services: &BootServices, + data: &Vec, + target_dir_prefix: &str, + target_filename: &CStr16, + dir_mode: u32, + access_mode: u32, + tpm_pcr: PcrIndex, + tpm_description: &str) -> uefi::Result { + let mut cpio = Cpio { + buffer: Vec::new(), + inode_counter: 0 + }; + + cpio.pack_prefix(target_dir_prefix, dir_mode)?; + cpio.pack_one( + target_filename, + data, + target_dir_prefix, + access_mode)?; + cpio.pack_trailer()?; + tpm_log_event_ascii(boot_services, tpm_pcr, data, tpm_description)?; + + Ok(cpio) +} diff --git a/rust/uefi/linux-bootloader/src/initrd.rs b/rust/uefi/linux-bootloader/src/initrd.rs new file mode 100644 index 00000000..7bf9e1ef --- /dev/null +++ b/rust/uefi/linux-bootloader/src/initrd.rs @@ -0,0 +1,42 @@ +use crate::{cpio::Cpio, uefi_helpers::SD_LOADER, measure::TPM_PCR_INDEX_KERNEL_PARAMETERS}; +use alloc::vec::Vec; +use uefi::{prelude::RuntimeServices, table::runtime::VariableAttributes, cstr16}; + +pub enum CompanionInitrd { + Credentials(Cpio), + GlobalCredentials(Cpio), + SystemExtension(Cpio), + PcrSignature(Cpio), + PcrPublicKey(Cpio) +} + +pub fn export_pcr_efi_variables(runtime_services: &RuntimeServices, + initrds: Vec) -> uefi::Result { + // Do we have kernel parameters that were measured + if initrds.iter().any(|e| match e { + CompanionInitrd::Credentials(_) => true, + CompanionInitrd::GlobalCredentials(_) => true, + _ => false + }) { + runtime_services.set_variable( + cstr16!("StubPcrKernelParameters"), + &SD_LOADER, + VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS, + &TPM_PCR_INDEX_KERNEL_PARAMETERS.0.to_le_bytes() + )?; + } + // Do we have system extensions that were measured + if initrds.iter().any(|e| match e { + CompanionInitrd::SystemExtension(_) => true, + _ => false + }) { + runtime_services.set_variable( + cstr16!("StubPcrInitRDSysExts"), + &SD_LOADER, + VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS, + &TPM_PCR_INDEX_KERNEL_PARAMETERS.0.to_le_bytes() + )?; + } + + Ok(()) +} From 2a4f584c08a01b1f033fb9510803b5cbc6f41053 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Thu, 9 Nov 2023 19:27:52 +0100 Subject: [PATCH 02/19] linux-bootloader: rework the cpio code It is now enclosed in its own subdirectory, but full of `expect`. --- rust/uefi/linux-bootloader/Cargo.toml | 1 + rust/uefi/linux-bootloader/src/cpio/mod.rs | 2 + rust/uefi/linux-bootloader/src/cpio/packer.rs | 44 ++++++ .../src/{cpio.rs => cpio/writer.rs} | 140 ++++++++++-------- 4 files changed, 124 insertions(+), 63 deletions(-) create mode 100644 rust/uefi/linux-bootloader/src/cpio/mod.rs create mode 100644 rust/uefi/linux-bootloader/src/cpio/packer.rs rename rust/uefi/linux-bootloader/src/{cpio.rs => cpio/writer.rs} (77%) diff --git a/rust/uefi/linux-bootloader/Cargo.toml b/rust/uefi/linux-bootloader/Cargo.toml index 3a03d54a..090198b5 100644 --- a/rust/uefi/linux-bootloader/Cargo.toml +++ b/rust/uefi/linux-bootloader/Cargo.toml @@ -19,6 +19,7 @@ bitflags = "2.4.1" # Even in debug builds, we don't enable the debug logs, because they generate a lot of spam from goblin. log = { version = "0.4.20", default-features = false, features = [ "max_level_info", "release_max_level_warn" ]} +embedded-io = { version = "0.6.1", default-features = false, features = [ "alloc" ] } [badges] maintenance = { status = "actively-developed" } diff --git a/rust/uefi/linux-bootloader/src/cpio/mod.rs b/rust/uefi/linux-bootloader/src/cpio/mod.rs new file mode 100644 index 00000000..80ab20ed --- /dev/null +++ b/rust/uefi/linux-bootloader/src/cpio/mod.rs @@ -0,0 +1,2 @@ +pub mod writer; +pub mod packer; diff --git a/rust/uefi/linux-bootloader/src/cpio/packer.rs b/rust/uefi/linux-bootloader/src/cpio/packer.rs new file mode 100644 index 00000000..ae5bf94c --- /dev/null +++ b/rust/uefi/linux-bootloader/src/cpio/packer.rs @@ -0,0 +1,44 @@ +use alloc::vec::Vec; +use uefi::{CStr16, CString16}; + +use super::writer::Cpio; + +pub fn pack_cpio( + fs: &mut uefi::fs::FileSystem, + mut files: Vec, + target_dir_prefix: &str, + dir_mode: u32, + access_mode: u32) -> uefi::fs::FileSystemResult { + // Ensure uniform and stability to make TPM measurements independent of the read order. + files.sort(); + + let mut cpio = Cpio::new(); + cpio.pack_prefix(target_dir_prefix, dir_mode).expect("Failed to pack the prefix."); + for filename in files { + let contents = fs.read(filename.as_ref())?; + cpio.pack_one(filename.as_ref(), &contents, target_dir_prefix, access_mode).expect("Failed to pack an element."); + } + + cpio.pack_trailer().expect("Failed to pack the trailer."); + + Ok(cpio) +} + +pub fn pack_cpio_literal( + data: &Vec, + target_dir_prefix: &str, + target_filename: &CStr16, + dir_mode: u32, + access_mode: u32) -> uefi::Result { + let mut cpio = Cpio::new(); + + cpio.pack_prefix(target_dir_prefix, dir_mode)?; + cpio.pack_one( + target_filename, + data, + target_dir_prefix, + access_mode)?; + cpio.pack_trailer()?; + + Ok(cpio) +} diff --git a/rust/uefi/linux-bootloader/src/cpio.rs b/rust/uefi/linux-bootloader/src/cpio/writer.rs similarity index 77% rename from rust/uefi/linux-bootloader/src/cpio.rs rename to rust/uefi/linux-bootloader/src/cpio/writer.rs index 1fd31231..cadd3bec 100644 --- a/rust/uefi/linux-bootloader/src/cpio.rs +++ b/rust/uefi/linux-bootloader/src/cpio/writer.rs @@ -1,8 +1,6 @@ -use uefi::{CStr16, cstr16, proto::{tcg::PcrIndex, media::fs::SimpleFileSystem}, CString16, prelude::BootServices, table::boot::ScopedProtocol}; +use uefi::{CStr16, cstr16}; use alloc::{vec, vec::Vec, string::String, format}; -use acid_io::{{Cursor, Write}, Result}; - -use crate::tpm::tpm_log_event_ascii; +use embedded_io::{Write, ErrorType, ErrorKind, Error}; const MAGIC_NUMBER: &[u8; 6] = b"070701"; const TRAILER_NAME: &str= "TRAILER!!!"; @@ -57,7 +55,7 @@ fn align(value: usize) -> usize { } trait WriteBytesExt : Write { - fn write_cpio_word(&mut self, word: u32) -> Result<()> { + fn write_cpio_word(&mut self, word: u32) -> Result<(), Self::Error> { // A CPIO word is the hex(word) written as chars. // We do it manually because format! will allocate. self.write_all( @@ -71,7 +69,7 @@ trait WriteBytesExt : Write { ) } - fn write_cpio_header(&mut self, entry: Entry) -> Result { + fn write_cpio_header(&mut self, entry: Entry) -> Result { let mut header_size = STATIC_HEADER_LEN; self.write_all(MAGIC_NUMBER)?; self.write_cpio_word(entry.ino)?; @@ -98,7 +96,7 @@ trait WriteBytesExt : Write { Ok(header_size) } - fn write_cpio_contents(&mut self, header_size: usize, contents: &[u8]) -> Result { + fn write_cpio_contents(&mut self, header_size: usize, contents: &[u8]) -> Result { let mut total_size = header_size + contents.len(); self.write_all(contents)?; if let Some(pad) = compute_pad4(total_size) { @@ -108,7 +106,7 @@ trait WriteBytesExt : Write { Ok(total_size) } - fn write_cpio_entry(&mut self, header: Entry, contents: &[u8]) -> Result { + fn write_cpio_entry(&mut self, header: Entry, contents: &[u8]) -> Result { let header_size = self.write_cpio_header(header)?; self.write_cpio_contents(header_size, contents) @@ -117,15 +115,71 @@ trait WriteBytesExt : Write { impl WriteBytesExt for W {} -// A Cpio archive with convenience methods -// to pack stuff into it. +struct MemoryCursor<'a> { + buffer: &'a mut Vec +} + +impl<'a> MemoryCursor<'a> { + fn new(buffer: &'a mut Vec) -> Self { + Self { + buffer + } + } +} + +#[derive(Debug)] +struct UefiError(uefi::Error); + +impl Error for UefiError { + fn kind(&self) -> ErrorKind { + match self.0.status() { + uefi::Status::UNSUPPORTED => ErrorKind::Unsupported, + uefi::Status::IP_ADDRESS_CONFLICT => ErrorKind::AddrInUse, + uefi::Status::INVALID_PARAMETER => ErrorKind::InvalidInput, + uefi::Status::TIMEOUT => ErrorKind::TimedOut, + uefi::Status::NOT_READY => ErrorKind::Interrupted, + uefi::Status::OUT_OF_RESOURCES => ErrorKind::OutOfMemory, + _ => ErrorKind::Other + } + } +} + +impl ErrorType for MemoryCursor<'_> { + type Error = UefiError; +} + +impl Write for MemoryCursor<'_> { + fn write(&mut self, buf: &[u8]) -> Result { + self.buffer.extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +/// A CPIO archive with convenience methods +/// to pack a file hierarchy inside. pub struct Cpio { buffer: Vec, inode_counter: u32 } +impl From for Vec { + fn from(value: Cpio) -> Self { + value.buffer + } +} + impl Cpio { - fn pack_one(&mut self, fname: &CStr16, contents: &[u8], target_dir_prefix: &str, access_mode: u32) -> uefi::Result + pub fn new() -> Self { + Self { + buffer: Vec::new(), + inode_counter: 0 + } + } + + pub fn pack_one(&mut self, fname: &CStr16, contents: &[u8], target_dir_prefix: &str, access_mode: u32) -> uefi::Result { // cpio cannot deal with > 32 bits file sizes // SAFETY: u32::MAX as usize can wrap if usize < u32. @@ -182,7 +236,7 @@ impl Cpio { // Perform re-allocation now. let mut elt_buffer: Vec = Vec::with_capacity(current_len); - let mut cur = Cursor::new(&mut elt_buffer); + let mut cur = MemoryCursor::new(&mut elt_buffer); self.inode_counter += 1; // TODO: perform the concat properly @@ -208,7 +262,7 @@ impl Cpio { Ok(()) } - fn pack_dir(&mut self, path: &str, access_mode: u32) -> uefi::Result { + pub fn pack_dir(&mut self, path: &str, access_mode: u32) -> uefi::Result { // cpio cannot deal with > 2^32 - 1 inodes neither if self.inode_counter == u32::MAX { return Err(uefi::Status::OUT_OF_RESOURCES.into()); @@ -228,7 +282,7 @@ impl Cpio { } let mut elt_buffer: Vec = Vec::with_capacity(current_len); - let mut cur = Cursor::new(&mut elt_buffer); + let mut cur = MemoryCursor::new(&mut elt_buffer); self.inode_counter += 1; cur.write_cpio_header(Entry { @@ -252,60 +306,20 @@ impl Cpio { Ok(()) } - fn pack_prefix(&mut self, path: &str, dir_mode: u32) -> uefi::Result { - // Iterate over all parts of `path` - // pack_dir it + pub fn pack_prefix(&mut self, path: &str, dir_mode: u32) -> uefi::Result { + // TODO: bring Unix paths inside UEFI + // and just reuse &Path there and iterate over ancestors().rev()? + let mut ancestor = String::new(); + for component in path.split('/') { + ancestor = ancestor + "/" + component; + self.pack_dir(&ancestor, 0o555)?; + } Ok(()) } - fn pack_trailer(&mut self) -> uefi::Result { + pub fn pack_trailer(&mut self) -> uefi::Result { self.pack_one(cstr16!("."), TRAILER_NAME.as_bytes(), "", 0) } } -pub fn pack_cpio( - boot_services: &BootServices, - fs: &mut ScopedProtocol, - dropin_dir: Option<&CStr16>, - match_suffix: &CStr16, - target_dir_prefix: &str, - dir_mode: u32, - access_mode: u32, - tpm_pcr: PcrIndex, - tpm_description: &str) -> uefi::Result> { - match fs.open_volume() { - Ok(root_dir) => { - Ok(None) - }, - // Err(uefi::Status::UNSUPPORTED) => Ok(None), - // Log the error. - Err(err) => Err(err) - } -} - -pub fn pack_cpio_literal( - boot_services: &BootServices, - data: &Vec, - target_dir_prefix: &str, - target_filename: &CStr16, - dir_mode: u32, - access_mode: u32, - tpm_pcr: PcrIndex, - tpm_description: &str) -> uefi::Result { - let mut cpio = Cpio { - buffer: Vec::new(), - inode_counter: 0 - }; - - cpio.pack_prefix(target_dir_prefix, dir_mode)?; - cpio.pack_one( - target_filename, - data, - target_dir_prefix, - access_mode)?; - cpio.pack_trailer()?; - tpm_log_event_ascii(boot_services, tpm_pcr, data, tpm_description)?; - - Ok(cpio) -} From a366f739abd0433642b4a82149a7dc11d12d8194 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Thu, 9 Nov 2023 19:28:44 +0100 Subject: [PATCH 03/19] stub/common: add credential/sysext discovery It is now possible to discover credentials and systemd extensions and pack them as CPIOs. From 4a52120ccce9ee30cb9f7475bcd5b829aab89ac6 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Thu, 9 Nov 2023 19:30:05 +0100 Subject: [PATCH 04/19] stub(*): support dynamic initrds With this feature, it is now possible to load dynamic initrds (possibly read from filesystem or generated on the fly) and extend existing initrds. This feature will be useful to implement addons in the future. --- rust/uefi/stub/src/fat.rs | 6 +++++- rust/uefi/stub/src/main.rs | 8 ++++++-- rust/uefi/stub/src/thin.rs | 7 ++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/rust/uefi/stub/src/fat.rs b/rust/uefi/stub/src/fat.rs index cce30d34..be6cc0a0 100644 --- a/rust/uefi/stub/src/fat.rs +++ b/rust/uefi/stub/src/fat.rs @@ -40,7 +40,11 @@ impl EmbeddedConfiguration { } } -pub fn boot_linux(handle: Handle, mut system_table: SystemTable) -> Status { +pub fn boot_linux( + handle: Handle, + mut system_table: SystemTable, + dynamic_initrds: Vec>, +) -> Status { uefi_services::init(&mut system_table).unwrap(); // SAFETY: We get a slice that represents our currently running diff --git a/rust/uefi/stub/src/main.rs b/rust/uefi/stub/src/main.rs index bc5c6dee..f4dc1bd3 100644 --- a/rust/uefi/stub/src/main.rs +++ b/rust/uefi/stub/src/main.rs @@ -15,6 +15,7 @@ mod thin; #[cfg(all(feature = "fat", feature = "thin"))] compile_error!("A thin and fat stub cannot be produced at the same time, disable either `thin` or `fat` feature"); +use alloc::vec::Vec; use linux_bootloader::efivars::{export_efi_variables, get_loader_features, EfiLoaderFeatures}; use linux_bootloader::measure::measure_image; use linux_bootloader::tpm::tpm_available; @@ -69,15 +70,18 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { export_efi_variables(STUB_NAME, &system_table).expect("Failed to export stub EFI variables"); let status; + // A list of dynamically assembled initrds, e.g. credential initrds or system extension + // initrds. + let mut dynamic_initrds: Vec> = Vec::new(); #[cfg(feature = "fat")] { - status = fat::boot_linux(handle, system_table) + status = fat::boot_linux(handle, system_table, dynamic_initrds) } #[cfg(feature = "thin")] { - status = thin::boot_linux(handle, system_table).status() + status = thin::boot_linux(handle, system_table, dynamic_initrds).status() } status diff --git a/rust/uefi/stub/src/thin.rs b/rust/uefi/stub/src/thin.rs index bf91c5e2..5183b82d 100644 --- a/rust/uefi/stub/src/thin.rs +++ b/rust/uefi/stub/src/thin.rs @@ -1,3 +1,4 @@ +use alloc::vec::Vec; use log::{error, warn}; use sha2::{Digest, Sha256}; use uefi::{fs::FileSystem, prelude::*, CString16, Result}; @@ -75,7 +76,11 @@ fn check_hash(data: &[u8], expected_hash: Hash, name: &str, secure_boot: bool) - Ok(()) } -pub fn boot_linux(handle: Handle, mut system_table: SystemTable) -> uefi::Result<()> { +pub fn boot_linux( + handle: Handle, + mut system_table: SystemTable, + dynamic_initrds: Vec>, +) -> uefi::Result<()> { uefi_services::init(&mut system_table).unwrap(); // SAFETY: We get a slice that represents our currently running From 634f012efb6cd61c31d38a7f4f6ca062dc8f0592 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Thu, 9 Nov 2023 19:30:24 +0100 Subject: [PATCH 05/19] stub: discover credentials and system extensions and load them --- rust/uefi/stub/src/main.rs | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/rust/uefi/stub/src/main.rs b/rust/uefi/stub/src/main.rs index f4dc1bd3..79f349ac 100644 --- a/rust/uefi/stub/src/main.rs +++ b/rust/uefi/stub/src/main.rs @@ -16,6 +16,9 @@ mod thin; compile_error!("A thin and fat stub cannot be produced at the same time, disable either `thin` or `fat` feature"); use alloc::vec::Vec; +use linux_bootloader::companions::{ + discover_credentials, discover_system_extensions, get_default_dropin_directory, +}; use linux_bootloader::efivars::{export_efi_variables, get_loader_features, EfiLoaderFeatures}; use linux_bootloader::measure::measure_image; use linux_bootloader::tpm::tpm_available; @@ -74,6 +77,55 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { // initrds. let mut dynamic_initrds: Vec> = Vec::new(); + { + // This is a block for doing filesystem operations once and for all, related to companion + // files, nothing can open the LoadedImage protocol here. + // Everything must use `filesystem`. + let mut companions = Vec::new(); + let mut filesystem = uefi::fs::FileSystem::new( + system_table + .boot_services() + .get_image_file_system(system_table.boot_services().image_handle()) + .expect("Failed to open the simple filesystem for this image; netboot?"), + ); + let default_dropin_directory; + + if let Some(loaded_image_path) = pe_in_memory.file_path() { + default_dropin_directory = get_default_dropin_directory(system_table.boot_services(), loaded_image_path, &mut filesystem) + .expect("Failed to obtain the default drop-in directory"); + } else { + default_dropin_directory = None; + } + + // TODO: how to do the proper .as_ref()? Should I take AsRef in the call definition… ? + companions.append( + &mut discover_credentials( + &mut filesystem, + default_dropin_directory.as_ref().map(|x| x.as_ref()), + ) + .expect("Failed to discover system credentials"), + ); + if let Some(default_dropin_dir) = default_dropin_directory { + companions.append( + &mut discover_system_extensions(&mut filesystem, &default_dropin_dir) + .expect("Failed to discover system extensions"), + ); + } + + if is_tpm_available { + // TODO: in the future, devise a threat model where this can fail, see above + // measurements to understand the context. + let _ = measure_companion_initrds(&system_table, &companions); + } + + dynamic_initrds.append( + &mut companions + .into_iter() + .map(|initrd| initrd.into_cpio().into_inner()) + .collect(), + ); + } + #[cfg(feature = "fat")] { status = fat::boot_linux(handle, system_table, dynamic_initrds) From 9d16eea62dc5aa235ee7291dafae8971a17ee8fc Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Thu, 9 Nov 2023 19:30:41 +0100 Subject: [PATCH 06/19] stub(cargo): use personal fork of uefi-rs for development --- rust/uefi/Cargo.lock | 42 +++++++++++++++--------------------------- rust/uefi/Cargo.toml | 6 ++++++ 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/rust/uefi/Cargo.lock b/rust/uefi/Cargo.lock index ccb5bd58..4a23ea3a 100644 --- a/rust/uefi/Cargo.lock +++ b/rust/uefi/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "acid_io" -version = "0.1.0" -source = "git+https://github.com/dataphract/acid_io#2d549317fe9253df8b510ba6bbdcfe623a837286" -dependencies = [ - "libc", - "memchr", -] - [[package]] name = "bit_field" version = "0.10.2" @@ -67,6 +58,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "generic-array" version = "0.14.7" @@ -110,6 +107,7 @@ name = "linux-bootloader" version = "0.3.0" dependencies = [ "bitflags", + "embedded-io", "goblin", "log", "uefi", @@ -121,12 +119,6 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - [[package]] name = "plain" version = "0.2.3" @@ -241,9 +233,8 @@ dependencies = [ [[package]] name = "uefi" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ead9f748a4646479b850add36b527113a80e80a7e0f44d7b0334291850dcc5" +version = "0.25.0" +source = "git+https://github.com/RaitoBezarius/uefi-rs?branch=fs-improvements#2b1da832c04ba7d209d386b096de5061eb4bf7b8" dependencies = [ "bitflags", "log", @@ -256,9 +247,8 @@ dependencies = [ [[package]] name = "uefi-macros" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a7b1c2c808c3db854a54d5215e3f7e7aaf5dcfbce095598cba6af29895695d" +version = "0.12.0" +source = "git+https://github.com/RaitoBezarius/uefi-rs?branch=fs-improvements#2b1da832c04ba7d209d386b096de5061eb4bf7b8" dependencies = [ "proc-macro2", "quote", @@ -267,9 +257,8 @@ dependencies = [ [[package]] name = "uefi-raw" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864ac69eadd877bfb34e7814be1928122ed0057d9f975169a56ee496aa7bdfd7" +version = "0.4.0" +source = "git+https://github.com/RaitoBezarius/uefi-rs?branch=fs-improvements#2b1da832c04ba7d209d386b096de5061eb4bf7b8" dependencies = [ "bitflags", "ptr_meta", @@ -278,9 +267,8 @@ dependencies = [ [[package]] name = "uefi-services" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79fcb420624743c895bad0f9480fbc2f64e7c8d8611fb1ada6bdd799942feb4" +version = "0.22.0" +source = "git+https://github.com/RaitoBezarius/uefi-rs?branch=fs-improvements#2b1da832c04ba7d209d386b096de5061eb4bf7b8" dependencies = [ "cfg-if", "log", diff --git a/rust/uefi/Cargo.toml b/rust/uefi/Cargo.toml index 821f948e..1f01fa73 100644 --- a/rust/uefi/Cargo.toml +++ b/rust/uefi/Cargo.toml @@ -9,6 +9,12 @@ default-members = [ "stub" ] +[patch.crates-io] +uefi = { git = "https://github.com/RaitoBezarius/uefi-rs", branch = "fs-improvements" } +uefi-raw = { git = "https://github.com/RaitoBezarius/uefi-rs", branch = "fs-improvements" } +uefi-macros = { git = "https://github.com/RaitoBezarius/uefi-rs", branch = "fs-improvements" } +uefi-services = { git = "https://github.com/RaitoBezarius/uefi-rs", branch = "fs-improvements" } + [workspace.package] version = "0.3.0" From 4b38f19384eebad13a73a721a106f2d0724b79f8 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 15 Nov 2023 03:00:34 +0100 Subject: [PATCH 07/19] feat(cpio): move cpio archive assembling in its own library called `pio` This is better to perform integration testing and separate the cost of ownership / maintenance. --- rust/uefi/Cargo.lock | 50 +++ rust/uefi/Cargo.toml | 1 + rust/uefi/linux-bootloader/Cargo.toml | 1 + rust/uefi/linux-bootloader/src/cpio/mod.rs | 2 - rust/uefi/linux-bootloader/src/cpio/writer.rs | 325 ---------------- .../src/load_file_protocol.rs | 162 ++++++++ rust/uefi/linux-bootloader/src/pxe.rs | 233 ++++++++++++ rust/uefi/linux-bootloader/tests/cpio.rs | 4 + rust/uefi/pio/Cargo.toml | 13 + rust/uefi/pio/src/cursor.rs | 37 ++ rust/uefi/pio/src/errors.rs | 19 + rust/uefi/pio/src/lib.rs | 7 + .../src/cpio => pio/src}/packer.rs | 0 rust/uefi/pio/src/writer.rs | 346 ++++++++++++++++++ rust/uefi/pio/tests/read_write.rs | 93 +++++ 15 files changed, 966 insertions(+), 327 deletions(-) delete mode 100644 rust/uefi/linux-bootloader/src/cpio/mod.rs delete mode 100644 rust/uefi/linux-bootloader/src/cpio/writer.rs create mode 100644 rust/uefi/linux-bootloader/src/load_file_protocol.rs create mode 100644 rust/uefi/linux-bootloader/src/pxe.rs create mode 100644 rust/uefi/linux-bootloader/tests/cpio.rs create mode 100644 rust/uefi/pio/Cargo.toml create mode 100644 rust/uefi/pio/src/cursor.rs create mode 100644 rust/uefi/pio/src/errors.rs create mode 100644 rust/uefi/pio/src/lib.rs rename rust/uefi/{linux-bootloader/src/cpio => pio/src}/packer.rs (100%) create mode 100644 rust/uefi/pio/src/writer.rs create mode 100644 rust/uefi/pio/tests/read_write.rs diff --git a/rust/uefi/Cargo.lock b/rust/uefi/Cargo.lock index 4a23ea3a..a318e347 100644 --- a/rust/uefi/Cargo.lock +++ b/rust/uefi/Cargo.lock @@ -29,6 +29,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cpio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e77cfc4543efb4837662cb7cd53464ae66f0fd5c708d71e0f338b1c11d62d3" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -58,6 +64,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "embedded-io" version = "0.6.1" @@ -85,6 +97,12 @@ dependencies = [ "scroll", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "lanzaboote_stub" version = "0.3.0" @@ -110,6 +128,7 @@ dependencies = [ "embedded-io", "goblin", "log", + "pio", "uefi", ] @@ -119,6 +138,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "pio" +version = "0.1.0" +dependencies = [ + "cpio", + "embedded-io", + "snafu", +] + [[package]] name = "plain" version = "0.2.3" @@ -194,6 +222,28 @@ dependencies = [ "digest", ] +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/rust/uefi/Cargo.toml b/rust/uefi/Cargo.toml index 1f01fa73..7e65ff30 100644 --- a/rust/uefi/Cargo.toml +++ b/rust/uefi/Cargo.toml @@ -2,6 +2,7 @@ members = [ "stub", + "pio", "linux-bootloader", ] diff --git a/rust/uefi/linux-bootloader/Cargo.toml b/rust/uefi/linux-bootloader/Cargo.toml index 090198b5..6b83b71e 100644 --- a/rust/uefi/linux-bootloader/Cargo.toml +++ b/rust/uefi/linux-bootloader/Cargo.toml @@ -16,6 +16,7 @@ uefi = { version = "0.26.0", default-features = false, features = [ "alloc", "gl # Update blocked by #237 goblin = { version = "=0.6.1", default-features = false, features = [ "pe64", "alloc" ]} bitflags = "2.4.1" +pio = { path = "../pio" } # Even in debug builds, we don't enable the debug logs, because they generate a lot of spam from goblin. log = { version = "0.4.20", default-features = false, features = [ "max_level_info", "release_max_level_warn" ]} diff --git a/rust/uefi/linux-bootloader/src/cpio/mod.rs b/rust/uefi/linux-bootloader/src/cpio/mod.rs deleted file mode 100644 index 80ab20ed..00000000 --- a/rust/uefi/linux-bootloader/src/cpio/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod writer; -pub mod packer; diff --git a/rust/uefi/linux-bootloader/src/cpio/writer.rs b/rust/uefi/linux-bootloader/src/cpio/writer.rs deleted file mode 100644 index cadd3bec..00000000 --- a/rust/uefi/linux-bootloader/src/cpio/writer.rs +++ /dev/null @@ -1,325 +0,0 @@ -use uefi::{CStr16, cstr16}; -use alloc::{vec, vec::Vec, string::String, format}; -use embedded_io::{Write, ErrorType, ErrorKind, Error}; - -const MAGIC_NUMBER: &[u8; 6] = b"070701"; -const TRAILER_NAME: &str= "TRAILER!!!"; -const CPIO_HEX: &[u8; 16] = b"0123456789abcdef"; - -struct Entry { - name: String, - ino: u32, - mode: u32, - uid: u32, - gid: u32, - nlink: u32, - mtime: u32, - file_size: u32, - dev_major: u32, - dev_minor: u32, - rdev_major: u32, - rdev_minor: u32, -} - -const STATIC_HEADER_LEN: usize = core::mem::size_of::() - - core::mem::size_of::() // remove `name` size, which cannot be derived statically - // unstable for const fn yet: https://github.com/rust-lang/rust/issues/46571 - // + core::mem::size_of_val(MAGIC_NUMBER) - + core::mem::size_of::<&[u8; 6]>() // = 6 - + core::mem::size_of::() // filename size - + 1 // NULL-terminator for filename (\0) - + core::mem::size_of::(); // CRC - -/// Compute the necessary padding based on the provided length -/// It returns None if no padding is necessary. -fn compute_pad4(len: usize) -> Option> { - let overhang = len % 4; - if overhang != 0 { - let repeat = 4 - overhang; - Some(vec![0u8; repeat]) - } else { - None - } -} - -/// Align on N-byte boundary a value. -fn align(value: usize) -> usize { - // Assert if A is a power of 2. - // assert!(A & (A - 1) == 0); - - if value > usize::MAX - (A - 1) { - usize::MAX - } else { - (value + A - 1) & !(A - 1) - } -} - -trait WriteBytesExt : Write { - fn write_cpio_word(&mut self, word: u32) -> Result<(), Self::Error> { - // A CPIO word is the hex(word) written as chars. - // We do it manually because format! will allocate. - self.write_all( - &word.to_le_bytes() - .into_iter() - .enumerate() - // u8 -> usize is always safe. - .map(|(i, c)| CPIO_HEX[((c >> (4 * i)) & 0xF) as usize]) - .rev() - .collect::>() - ) - } - - fn write_cpio_header(&mut self, entry: Entry) -> Result { - let mut header_size = STATIC_HEADER_LEN; - self.write_all(MAGIC_NUMBER)?; - self.write_cpio_word(entry.ino)?; - self.write_cpio_word(entry.mode)?; - self.write_cpio_word(entry.uid)?; - self.write_cpio_word(entry.gid)?; - self.write_cpio_word(entry.nlink)?; - self.write_cpio_word(entry.mtime)?; - self.write_cpio_word(entry.file_size)?; - self.write_cpio_word(entry.dev_major)?; - self.write_cpio_word(entry.dev_minor)?; - self.write_cpio_word(entry.rdev_major)?; - self.write_cpio_word(entry.rdev_minor)?; - self.write_cpio_word((entry.name.len() + 1).try_into().expect("Filename cannot be longer than a 32-bits size"))?; - self.write_cpio_word(0u32)?; // CRC - self.write_all(entry.name.as_bytes())?; - header_size += entry.name.len(); - self.write(&[0u8])?; // Write \0 for the string. - // Pad to a multiple of 4 bytes - if let Some(pad) = compute_pad4(STATIC_HEADER_LEN + entry.name.len()) { - self.write_all(&pad)?; - header_size += pad.len(); - } - Ok(header_size) - } - - fn write_cpio_contents(&mut self, header_size: usize, contents: &[u8]) -> Result { - let mut total_size = header_size + contents.len(); - self.write_all(contents)?; - if let Some(pad) = compute_pad4(total_size) { - self.write_all(&pad)?; - total_size += pad.len(); - } - Ok(total_size) - } - - fn write_cpio_entry(&mut self, header: Entry, contents: &[u8]) -> Result { - let header_size = self.write_cpio_header(header)?; - - self.write_cpio_contents(header_size, contents) - } -} - -impl WriteBytesExt for W {} - -struct MemoryCursor<'a> { - buffer: &'a mut Vec -} - -impl<'a> MemoryCursor<'a> { - fn new(buffer: &'a mut Vec) -> Self { - Self { - buffer - } - } -} - -#[derive(Debug)] -struct UefiError(uefi::Error); - -impl Error for UefiError { - fn kind(&self) -> ErrorKind { - match self.0.status() { - uefi::Status::UNSUPPORTED => ErrorKind::Unsupported, - uefi::Status::IP_ADDRESS_CONFLICT => ErrorKind::AddrInUse, - uefi::Status::INVALID_PARAMETER => ErrorKind::InvalidInput, - uefi::Status::TIMEOUT => ErrorKind::TimedOut, - uefi::Status::NOT_READY => ErrorKind::Interrupted, - uefi::Status::OUT_OF_RESOURCES => ErrorKind::OutOfMemory, - _ => ErrorKind::Other - } - } -} - -impl ErrorType for MemoryCursor<'_> { - type Error = UefiError; -} - -impl Write for MemoryCursor<'_> { - fn write(&mut self, buf: &[u8]) -> Result { - self.buffer.extend_from_slice(buf); - Ok(buf.len()) - } - fn flush(&mut self) -> Result<(), Self::Error> { - Ok(()) - } -} - -/// A CPIO archive with convenience methods -/// to pack a file hierarchy inside. -pub struct Cpio { - buffer: Vec, - inode_counter: u32 -} - -impl From for Vec { - fn from(value: Cpio) -> Self { - value.buffer - } -} - -impl Cpio { - pub fn new() -> Self { - Self { - buffer: Vec::new(), - inode_counter: 0 - } - } - - pub fn pack_one(&mut self, fname: &CStr16, contents: &[u8], target_dir_prefix: &str, access_mode: u32) -> uefi::Result - { - // cpio cannot deal with > 32 bits file sizes - // SAFETY: u32::MAX as usize can wrap if usize < u32. - // hopefully, I will never encounter a usize = u16 in the wild. - if contents.len() > (u32::MAX as usize) { - return Err(uefi::Status::LOAD_ERROR.into()); - } - - // cpio cannot deal with > 2^32 - 1 inodes neither - if self.inode_counter == u32::MAX { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - let mut current_len = STATIC_HEADER_LEN + 1; // 1 for the `/` separator - - if current_len > usize::MAX - target_dir_prefix.len() { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - current_len += target_dir_prefix.len(); - - if current_len > usize::MAX - fname.num_bytes() { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - current_len += fname.num_bytes(); - - // SAFETY: u32::MAX as usize can wrap if usize < u32. - if target_dir_prefix.len() + fname.num_bytes() >= (u32::MAX as usize) { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - // Perform 4-byte alignment of current_len - current_len = align::<4>(current_len); - if current_len == usize::MAX { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - // Perform 4-byte alignment of contents.len() - let aligned_contents_len = align::<4>(contents.len()); - if aligned_contents_len == usize::MAX { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - if current_len > usize::MAX - aligned_contents_len { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - current_len += aligned_contents_len; - - if self.buffer.len() > usize::MAX - current_len { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - // Perform re-allocation now. - let mut elt_buffer: Vec = Vec::with_capacity(current_len); - let mut cur = MemoryCursor::new(&mut elt_buffer); - - self.inode_counter += 1; - // TODO: perform the concat properly - // transform fname to string - cur.write_cpio_entry(Entry { - name: format!("{}/{}", target_dir_prefix, fname), - ino: self.inode_counter, - mode: access_mode | 0100000, // S_IFREG - uid: 0, - gid: 0, - nlink: 1, - mtime: 0, - // This was checked previously. - file_size: contents.len().try_into().unwrap(), - dev_major: 0, - dev_minor: 0, - rdev_major: 0, - rdev_minor: 0 - }, contents).map_err(|_err| uefi::Status::BAD_BUFFER_SIZE)?; - - // Concat the element buffer. - self.buffer.append(&mut elt_buffer); - - Ok(()) - } - pub fn pack_dir(&mut self, path: &str, access_mode: u32) -> uefi::Result { - // cpio cannot deal with > 2^32 - 1 inodes neither - if self.inode_counter == u32::MAX { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - let mut current_len = STATIC_HEADER_LEN; - if current_len > usize::MAX - path.len() { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - current_len += path.len(); - - // Align the whole header - current_len = align::<4>(current_len); - if self.buffer.len() == usize::MAX || self.buffer.len() > usize::MAX - current_len { - return Err(uefi::Status::OUT_OF_RESOURCES.into()); - } - - let mut elt_buffer: Vec = Vec::with_capacity(current_len); - let mut cur = MemoryCursor::new(&mut elt_buffer); - - self.inode_counter += 1; - cur.write_cpio_header(Entry { - name: path.into(), - ino: self.inode_counter, - mode: access_mode | 0100000, // S_IFREG - uid: 0, - gid: 0, - nlink: 1, - mtime: 0, - file_size: 0, - dev_major: 0, - dev_minor: 0, - rdev_major: 0, - rdev_minor: 0 - }).map_err(|_err| uefi::Status::BAD_BUFFER_SIZE)?; - - // Concat the element buffer. - self.buffer.append(&mut elt_buffer); - - Ok(()) - } - - pub fn pack_prefix(&mut self, path: &str, dir_mode: u32) -> uefi::Result { - // TODO: bring Unix paths inside UEFI - // and just reuse &Path there and iterate over ancestors().rev()? - let mut ancestor = String::new(); - for component in path.split('/') { - ancestor = ancestor + "/" + component; - self.pack_dir(&ancestor, 0o555)?; - } - Ok(()) - } - - pub fn pack_trailer(&mut self) -> uefi::Result { - self.pack_one(cstr16!("."), TRAILER_NAME.as_bytes(), "", 0) - } -} - - diff --git a/rust/uefi/linux-bootloader/src/load_file_protocol.rs b/rust/uefi/linux-bootloader/src/load_file_protocol.rs new file mode 100644 index 00000000..900fca88 --- /dev/null +++ b/rust/uefi/linux-bootloader/src/load_file_protocol.rs @@ -0,0 +1,162 @@ +//! Load file support protocols. + +use alloc::vec::Vec; + +use log::warn; +use uefi::proto::device_path::{FfiDevicePath, DevicePath}; +use uefi::proto::unsafe_protocol; +use uefi::{Result, Status}; +use core::ffi::c_void; +use core::ptr; + +/// The UEFI LoadFile protocol. +/// +/// This protocol has a single method to load a file according to some +/// device path. +/// +/// This interface is (much more) implemented by many devices, e.g. network and filesystems. +#[derive(Debug)] +#[repr(C)] +#[unsafe_protocol("56ec3091-954c-11d2-8e3f-00a0c969723b")] +pub struct LoadFileProtocol { + load_file: unsafe extern "efiapi" fn( + this: &mut LoadFileProtocol, + file_path: *const FfiDevicePath, + boot_policy: bool, + buffer_size: *mut usize, + buffer: *mut c_void, + ) -> Status, +} + +impl LoadFileProtocol { + /// Load file addressed by provided device path + pub fn load_file(&mut self, + file_path: &DevicePath, + boot_policy: bool, + buffer_size: &mut usize, + buffer: *mut c_void + ) -> Status { + unsafe { + (self.load_file)(self, + file_path.as_ffi_ptr(), + boot_policy, + buffer_size as *mut usize, + buffer + ) + } + } + + /// Load file addressed by the provided device path. + pub fn load_file_in_heap(&mut self, + file_path: &DevicePath, + boot_policy: bool, + ) -> Result> { + let mut buffer_size: usize = 0; + let mut status: Status; + unsafe { + status = (self.load_file)(self, + file_path.as_ffi_ptr(), + boot_policy, + ptr::addr_of_mut!(buffer_size), + ptr::null_mut() + ); + } + + warn!("size obtained: {buffer_size}"); + + if status.is_error() { + return Err(status.into()); + } + + let mut buffer: Vec = Vec::with_capacity(buffer_size); + unsafe { + status = (self.load_file)(self, + file_path.as_ffi_ptr(), + boot_policy, + ptr::addr_of_mut!(buffer_size), + buffer.as_mut_ptr() as *mut c_void + ); + } + + if status.is_error() { + return Err(status.into()); + } + + Ok(buffer) + } +} + +/// The UEFI LoadFile2 protocol. +/// +/// This protocol has a single method to load a file according to some +/// device path. +/// +/// This interface is implemented by many devices, e.g. network and filesystems. +#[derive(Debug)] +#[repr(C)] +#[unsafe_protocol("4006c0c1-fcb3-403e-996d-4a6c8724e06d")] +pub struct LoadFile2Protocol { + load_file: unsafe extern "efiapi" fn( + this: &mut LoadFile2Protocol, + file_path: *const FfiDevicePath, + boot_policy: bool, + buffer_size: *mut usize, + buffer: *mut c_void, + ) -> Status, +} + +impl LoadFile2Protocol { + /// Load file addressed by provided device path + pub fn load_file(&mut self, + file_path: &DevicePath, + boot_policy: bool, + buffer_size: &mut usize, + buffer: *mut c_void + ) -> Status { + unsafe { + (self.load_file)(self, + file_path.as_ffi_ptr(), + boot_policy, + buffer_size as *mut usize, + buffer + ) + } + } + + /// Load file addressed by the provided device path. + pub fn load_file_in_heap(&mut self, + file_path: &DevicePath, + boot_policy: bool, + ) -> Result> { + let mut buffer_size: usize = 0; + let mut status: Status; + unsafe { + status = (self.load_file)(self, + file_path.as_ffi_ptr(), + boot_policy, + ptr::addr_of_mut!(buffer_size), + ptr::null_mut() + ); + } + + if status.is_error() { + return Err(status.into()); + } + + let mut buffer: Vec = Vec::with_capacity(buffer_size); + unsafe { + status = (self.load_file)(self, + file_path.as_ffi_ptr(), + boot_policy, + ptr::addr_of_mut!(buffer_size), + buffer.as_mut_ptr() as *mut c_void + ); + } + + if status.is_error() { + return Err(status.into()); + } + + Ok(buffer) + } +} diff --git a/rust/uefi/linux-bootloader/src/pxe.rs b/rust/uefi/linux-bootloader/src/pxe.rs new file mode 100644 index 00000000..fbd899a3 --- /dev/null +++ b/rust/uefi/linux-bootloader/src/pxe.rs @@ -0,0 +1,233 @@ +use core::ffi::c_void; + +use uefi::{proto::unsafe_protocol, Status}; + +/// PXE support + +const PXEBASE_CODE_PROTOCOL_REVISION: u64 = 0x00010000; +const PXEBASE_CODE_MAX_ARP_ENTRIES: usize = 8; +const PXEBASE_CODE_MAX_ROUTE_ENTRIES: usize = 8; +const PXEBASE_CODE_MAX_IPCNT: usize = 8; + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +struct IPv4Address { + addr: [u8; 4] +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +struct IPv6Address { + addr: [u8; 16] +} + +#[repr(C)] +union IPAddress { + addr: [u8; 4], + v4: IPv4Address, + v6: IPv6Address +} + +#[derive(Debug)] +#[repr(C)] +struct MACAddress { + addr: [u8; 32] +} + +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct PXEBaseCodeDHCPv6Packet { + message_type: u32, + transaction_id: u32, + dhcp_options: [u8; 1024] +} + +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct PXEBaseCodeDHCPv4Packet { + bootp_opcode: u8, + bootp_hw_type: u8, + bootp_hw_addr_len: u8, + bootp_gate_hops: u8, + bootp_ident: u32, + bootp_seconds: u16, + bootp_flags: u16, + bootp_client_ip_addr: [u8; 4], + bootp_yi_addr: [u8; 4], + bootp_si_addr: [u8; 4], + bootp_gi_addr: [u8; 4], + bootp_hw_addr: [u8; 16], + bootp_srv_name: [u8; 64], + bootp_bootfile: [u8; 128], + dhcp_magik: u32, + dhcp_options: [u8; 56] +} + +#[repr(C)] +union PXEBaseCodePacket { + raw: [u8; 1472], + dhcpv4: PXEBaseCodeDHCPv4Packet, + dhcpv6: PXEBaseCodeDHCPv6Packet +} + +#[derive(Debug)] +#[repr(C)] +struct PXEBaseCodeTFTPError { + error_code: u8, + error_string: [u8; 127] +} + +#[derive(Copy, Clone, Debug)] +#[repr(C)] +struct PXEBaseICMPEcho { + identifier: u16, + sequence: u16 +} + +#[repr(C)] +union PXEBaseCodeICMPErrorMetadata { + _reserved: u32, + mtu: u32, + pointer: u32, + echo: PXEBaseICMPEcho +} + +#[repr(C)] +struct PXEBaseCodeICMPError { + r#type: u8, + code: u8, + checksum: u16, + metadata: PXEBaseCodeICMPErrorMetadata, + data: [u8; 494] +} + +#[repr(C)] +struct PXEBaseCodeIPFilter { + filters: u8, + ip_count: u8, + _reserved: u16, + ip_list: [IPAddress; PXEBASE_CODE_MAX_IPCNT] +} + +#[repr(C)] +struct PXEBaseCodeARPEntry { + ip_addr: IPAddress, + mac_addr: MACAddress +} + +#[repr(C)] +struct PXEBaseCodeRouteEntry { + ip_addr: IPAddress, + subnet_mask: IPAddress, + gateway_addr: IPAddress +} + +#[repr(C)] +struct PXEBaseCodeMode { + started: bool, + ipv6_available: bool, + ipv6_supported: bool, + using_ipv6: bool, + bis_supported: bool, + bis_detected: bool, + auto_arp: bool, + send_guid: bool, + dhcp_discover_valid: bool, + dhcp_ack_received: bool, + proxy_offer_received: bool, + pxe_discover_valid: bool, + pxe_reply_valid: bool, + pxe_bis_reply_received: bool, + icmp_error_received: bool, + tftp_error_received: bool, + make_callbacks: bool, + ttl: u8, + tos: u8, + // EFI_IP_ADDRESS + station_ip: IPAddress, + subnet_mask: IPAddress, + // EFI_PXE_BASE_CODE_PACKET + dhcp_discover: PXEBaseCodePacket, + dhcp_ack: PXEBaseCodePacket, + proxy_offer: PXEBaseCodePacket, + pxe_discover: PXEBaseCodePacket, + pxe_reply: PXEBaseCodePacket, + pxe_bis_reply: PXEBaseCodePacket, + // IP_FILTER + ip_filter: PXEBaseCodeIPFilter, + arp_cache_entries: u32, + // ARP_ENTRY array + arp_cache: [PXEBaseCodeARPEntry; PXEBASE_CODE_MAX_ARP_ENTRIES], + route_table_entries: u32, + route_table: [PXEBaseCodeRouteEntry; PXEBASE_CODE_MAX_ROUTE_ENTRIES], + // _ERROR + icmp_error: PXEBaseCodeICMPError, + tftp_error: PXEBaseCodeTFTPError, +} + +struct PXEBaseCodeServerList { + r#type: u16, + accept_any_response: bool, + _reserved: u8, + ip_address: IPAddress +} + +struct PXEBaseCodeDiscoverInfo { + use_multicast: bool, + use_broadcast: bool, + use_unicast: bool, + must_use_list: bool, + server_multicast_ip: IPAddress, + ip_count: u16, + // It is a dynamically sized array based on `ip_count`… + server_list: *mut PXEBaseCodeServerList, +} + +enum PXEBaseCodeTFTPOpcode { + First, + GetFileSize, + ReadFile, + WriteFile, + ReadDirectory, + MulticastGetFileSize, + MulticastReadFile, + MulticastReadDirectory, + Last +} + +struct PXEBaseCodeMTFTPInfo { + multicast_ip: IPAddress, + client_port: u16, + server_port: u16, + listen_timeout: u16, + transmit_timeout: u16 +} + +#[derive(Debug)] +#[repr(C)] +#[unsafe_protocol("03c4e603-ac28-11d3-9a2d-0090273fc14d")] +pub struct PXEBaseCodeProtocol { + revision: u64, + start: unsafe extern "efiapi" fn(this: &mut PXEBaseCodeProtocol, use_ipv6: bool) -> Status, + stop: unsafe extern "efiapi" fn(this: &mut PXEBaseCodeProtocol) -> Status, + perform_dhcp: unsafe extern "efiapi" fn(this: &mut PXEBaseCodeProtocol, sort_offers: bool) -> Status, + discover: unsafe extern "efiapi" fn(this: &mut PXEBaseCodeProtocol, r#type: u16, layer: *mut u16, use_boot_integrity_services: bool, info: *const PXEBaseCodeDiscoverInfo), + perform_mtftp: unsafe extern "efiapi" fn(this: &mut PXEBaseCodeProtocol, + operation: PXEBaseCodeProtocol, + buffer: *mut c_void, + overwrite_file: bool, + buffer_size: *mut usize, + block_size: *const usize, + server_ip: *const IPAddress, + filename: *const u8, + info: *const PXEBaseCodeMTFTPInfo, + dont_use_buffer: bool), + udp_write: unsafe extern "efiapi" fn(), + udp_read: unsafe extern "efiapi" fn(), + set_ip_filter: unsafe extern "efiapi" fn(), + perform_arp: unsafe extern "efiapi" fn(), + set_parameters: unsafe extern "efiapi" fn(), + set_station_ip: unsafe extern "efiapi" fn(), + set_packets: unsafe extern "efiapi" fn(), + mode: *const PXEBaseCodeMode +} diff --git a/rust/uefi/linux-bootloader/tests/cpio.rs b/rust/uefi/linux-bootloader/tests/cpio.rs new file mode 100644 index 00000000..d12ad9da --- /dev/null +++ b/rust/uefi/linux-bootloader/tests/cpio.rs @@ -0,0 +1,4 @@ +#[test] +fn test_writing_files() { + +} diff --git a/rust/uefi/pio/Cargo.toml b/rust/uefi/pio/Cargo.toml new file mode 100644 index 00000000..56428de4 --- /dev/null +++ b/rust/uefi/pio/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pio" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +embedded-io = { version = "0.6.1", features = [ "alloc" ] } +snafu = { version = "0.7.5", default-features = false } + +[dev-dependencies] +cpio = "0.2.2" diff --git a/rust/uefi/pio/src/cursor.rs b/rust/uefi/pio/src/cursor.rs new file mode 100644 index 00000000..aaccaf0a --- /dev/null +++ b/rust/uefi/pio/src/cursor.rs @@ -0,0 +1,37 @@ +use core::convert::Infallible; + +use alloc::vec::Vec; +use embedded_io::{ErrorType, Write}; + +pub struct Cursor { + buffer: Vec, +} + +impl Cursor { + pub fn new(buffer: Vec) -> Self { + Self { buffer } + } + + pub fn into_inner(self) -> Vec { + self.buffer + } + + pub fn get_mut(&mut self) -> &mut Vec { + &mut self.buffer + } +} + +impl ErrorType for Cursor { + type Error = Infallible; +} + +impl Write for Cursor { + fn write(&mut self, buf: &[u8]) -> Result { + self.buffer.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} diff --git a/rust/uefi/pio/src/errors.rs b/rust/uefi/pio/src/errors.rs new file mode 100644 index 00000000..030bb045 --- /dev/null +++ b/rust/uefi/pio/src/errors.rs @@ -0,0 +1,19 @@ +use snafu::prelude::Snafu; + +#[derive(Debug, Snafu)] +pub enum CPIOError { + #[snafu(display("File size does not fit in 32 bits ({got})"))] + TooLargeFileSize { got: usize }, + #[snafu(display("This CPIO archive is exceeding the maximum amount of inodes (2^32 - 1)"))] + MaximumInodesReached, + #[snafu(display( + "This CPIO archive is too large to fit inside of a 64 bits integer in terms of buffer size" + ))] + MaximumArchiveReached, + #[snafu(display( + "Provided buffer size is too small, expected: {expected} bytes, got: {got} bytes" + ))] + InsufficientBufferSize { expected: usize, got: usize }, + #[snafu(display("An IO error was encountered: {src:?}"))] + IOError { src: IOError }, +} diff --git a/rust/uefi/pio/src/lib.rs b/rust/uefi/pio/src/lib.rs new file mode 100644 index 00000000..c60c7b86 --- /dev/null +++ b/rust/uefi/pio/src/lib.rs @@ -0,0 +1,7 @@ +#![no_std] +extern crate alloc; + +pub mod cursor; +pub mod errors; +pub mod writer; +// pub mod packer; diff --git a/rust/uefi/linux-bootloader/src/cpio/packer.rs b/rust/uefi/pio/src/packer.rs similarity index 100% rename from rust/uefi/linux-bootloader/src/cpio/packer.rs rename to rust/uefi/pio/src/packer.rs diff --git a/rust/uefi/pio/src/writer.rs b/rust/uefi/pio/src/writer.rs new file mode 100644 index 00000000..4125e91b --- /dev/null +++ b/rust/uefi/pio/src/writer.rs @@ -0,0 +1,346 @@ +use core::marker::PhantomData; + +use alloc::{ + format, + string::{String, ToString}, + vec, + vec::Vec, +}; +use embedded_io::Write; + +use crate::{cursor::Cursor, errors::CPIOError}; + +const MAGIC_NUMBER: &[u8; 6] = b"070701"; +const TRAILER_NAME: &str = "TRAILER!!!"; + +pub type Result = core::result::Result>; + +struct Entry { + name: String, + ino: u32, + mode: u32, + uid: u32, + gid: u32, + nlink: u32, + mtime: u32, + file_size: u32, + dev_major: u32, + dev_minor: u32, + rdev_major: u32, + rdev_minor: u32, +} + +const STATIC_HEADER_LEN: usize = 6 // c_magic[6] + + (8 * 13); // c_ino, c_mode, c_uid, c_gid, c_nlink, c_mtime, c_filesize, c_devmajor, + // c_devminor, c_rdevmajor, c_rdevminor, c_namesize, c_check, all of them being &[u8; 8]. + +/// Compute the necessary padding based on the provided length +/// It returns None if no padding is necessary. +fn compute_pad4(len: usize) -> Option> { + let overhang = len % 4; + if overhang != 0 { + let repeat = 4 - overhang; + Some(vec![0u8; repeat]) + } else { + None + } +} + +/// Align on N-byte boundary a value. +fn align(value: usize) -> usize { + // Assert if A is a power of 2. + // assert!(A & (A - 1) == 0); + + if value > usize::MAX - (A - 1) { + usize::MAX + } else { + (value + A - 1) & !(A - 1) + } +} + +trait WriteBytesExt: Write { + fn write_cpio_word(&mut self, word: u32) -> core::result::Result<(), Self::Error> { + // A CPIO word is the hex(word) written as chars. + self.write_all(format!("{:08x}", word).as_bytes()) + } + + fn write_cpio_header(&mut self, entry: Entry) -> core::result::Result { + let mut header_size = STATIC_HEADER_LEN; + self.write_all(MAGIC_NUMBER)?; + self.write_cpio_word(entry.ino)?; + self.write_cpio_word(entry.mode)?; + self.write_cpio_word(entry.uid)?; + self.write_cpio_word(entry.gid)?; + self.write_cpio_word(entry.nlink)?; + self.write_cpio_word(entry.mtime)?; + self.write_cpio_word(entry.file_size)?; + self.write_cpio_word(entry.dev_major)?; + self.write_cpio_word(entry.dev_minor)?; + self.write_cpio_word(entry.rdev_major)?; + self.write_cpio_word(entry.rdev_minor)?; + self.write_cpio_word( + (entry.name.len() + 1) + .try_into() + .expect("Filename cannot be longer than a 32-bits size"), + )?; + self.write_cpio_word(0u32)?; // CRC + self.write_all(entry.name.as_bytes())?; + header_size += entry.name.len(); + self.write(&[0u8])?; // Write \0 for the string. + header_size += 1; + // Pad to a multiple of 4 bytes + if let Some(pad) = compute_pad4(header_size) { + self.write_all(&pad)?; + header_size += pad.len(); + } + assert!( + header_size % 4 == 0, + "CPIO header is not aligned on a 4-bytes boundary!" + ); + Ok(header_size) + } + + fn write_cpio_contents( + &mut self, + header_size: usize, + contents: &[u8], + ) -> core::result::Result { + let mut total_size = header_size + contents.len(); + self.write_all(contents)?; + if let Some(pad) = compute_pad4(contents.len()) { + self.write_all(&pad)?; + total_size += pad.len(); + } + assert!( + total_size % 4 == 0, + "CPIO file data is not aligned on a 4-bytes boundary!" + ); + Ok(total_size) + } + + fn write_cpio_entry( + &mut self, + header: Entry, + contents: &[u8], + ) -> core::result::Result { + let header_size = self.write_cpio_header(header)?; + + self.write_cpio_contents(header_size, contents) + } +} + +impl WriteBytesExt for W {} + +/// A CPIO archive with convenience methods +/// to pack a file hierarchy inside. +pub struct Cpio { + buffer: Vec, + inode_counter: u32, + _error: PhantomData, +} + +impl From> for Vec { + fn from(value: Cpio) -> Self { + value.into_inner() + } +} + +impl AsRef<[u8]> for Cpio { + fn as_ref(&self) -> &[u8] { + self.buffer.as_ref() + } +} + +impl Default for Cpio { + fn default() -> Self { + Self::new() + } +} + +impl Cpio { + pub fn new() -> Self { + Self { + buffer: Vec::new(), + inode_counter: 0, + _error: PhantomData, + } + } + + pub fn into_inner(self) -> Vec { + self.buffer + } + + /// Pack inside the archive a file named `fname` containing `contents` under + /// `target_dir_prefix` hierarchy of files with access mode specified by `access_mode`. + /// It may return IO errors or error specific to the CPIO archives. + pub fn pack_one( + &mut self, + fname: &str, + contents: &[u8], + target_dir_prefix: &str, + access_mode: u32, + ) -> Result { + // cpio cannot deal with > 32 bits file sizes + // SAFETY: u32::MAX as usize can wrap if usize < u32. + // hopefully, I will never encounter a usize = u16 in the wild. + if contents.len() > (u32::MAX as usize) { + return Err(CPIOError::TooLargeFileSize { + got: contents.len(), + }); + } + + // cpio cannot deal with > 2^32 - 1 inodes neither + if self.inode_counter == u32::MAX { + return Err(CPIOError::MaximumInodesReached); + } + + let mut current_len = STATIC_HEADER_LEN + 1; // 1 for the `/` separator + + if current_len > usize::MAX - target_dir_prefix.len() { + return Err(CPIOError::MaximumArchiveReached); + } + + current_len += target_dir_prefix.len(); + + if current_len > usize::MAX - fname.len() { + return Err(CPIOError::MaximumArchiveReached); + } + + current_len += fname.len(); + + // SAFETY: u32::MAX as usize can wrap if usize < u32. + if target_dir_prefix.len() + fname.len() >= (u32::MAX as usize) { + return Err(CPIOError::MaximumArchiveReached); + } + + // Perform 4-byte alignment of current_len + current_len = align::<4>(current_len); + if current_len == usize::MAX { + return Err(CPIOError::MaximumArchiveReached); + } + + // Perform 4-byte alignment of contents.len() + let aligned_contents_len = align::<4>(contents.len()); + if aligned_contents_len == usize::MAX { + return Err(CPIOError::MaximumArchiveReached); + } + + if current_len > usize::MAX - aligned_contents_len { + return Err(CPIOError::MaximumArchiveReached); + } + + current_len += aligned_contents_len; + + if self.buffer.len() > usize::MAX - current_len { + return Err(CPIOError::MaximumArchiveReached); + } + + // Perform re-allocation now. + let mut cur = Cursor::new(Vec::with_capacity(current_len)); + + self.inode_counter += 1; + // TODO: perform the concat properly + // transform fname to string + let written = cur + .write_cpio_entry( + Entry { + name: if !target_dir_prefix.is_empty() { + format!("{}/{}", target_dir_prefix, fname) + } else { + fname.to_string() + }, + ino: self.inode_counter, + mode: access_mode | 0o100000, // S_IFREG + uid: 0, + gid: 0, + nlink: 1, + mtime: 0, + // This was checked previously. + file_size: contents.len().try_into().unwrap(), + dev_major: 0, + dev_minor: 0, + rdev_major: 0, + rdev_minor: 0, + }, + contents, + ) + .unwrap(); // This is infaillible as long as allocation is not failible. + + // Concat the element buffer. + self.buffer.append(cur.get_mut()); + + Ok(written) + } + pub fn pack_dir(&mut self, path: &str, access_mode: u32) -> Result<(), IOError> { + // cpio cannot deal with > 2^32 - 1 inodes neither + if self.inode_counter == u32::MAX { + return Err(CPIOError::MaximumInodesReached); + } + + let mut current_len = STATIC_HEADER_LEN; + if current_len > usize::MAX - path.len() { + return Err(CPIOError::MaximumArchiveReached); + } + + current_len += path.len(); + + // Align the whole header + current_len = align::<4>(current_len); + if self.buffer.len() == usize::MAX || self.buffer.len() > usize::MAX - current_len { + return Err(CPIOError::MaximumArchiveReached); + } + + let mut cur = Cursor::new(Vec::with_capacity(current_len)); + + self.inode_counter += 1; + cur.write_cpio_header(Entry { + name: path.into(), + ino: self.inode_counter, + mode: access_mode | 0o040000, // S_IFDIR + uid: 0, + gid: 0, + nlink: 1, + mtime: 0, + file_size: 0, + dev_major: 0, + dev_minor: 0, + rdev_major: 0, + rdev_minor: 0, + }) + .unwrap(); // This is infaillible as long as allocation is not failible. + + // Concat the element buffer. + self.buffer.append(cur.get_mut()); + + Ok(()) + } + + pub fn pack_prefix(&mut self, path: &str, dir_mode: u32) -> Result<(), IOError> { + // TODO: bring Unix paths inside this crate? + // and just reuse &Path there and iterate over ancestors().rev()? + let mut ancestor = String::new(); + + // This will serialize all directory inodes of all prefix paths + // until the final directory which will be serialized with the proper `dir_mode` + let components = path.split('/'); + let parts = components.clone().count(); + if parts == 0 { + // packing the prefix of an empty path is trivial. + return Ok(()); + } + + let last = components.clone().last().unwrap(); + let prefixes = components.take(parts - 1); + + for component in prefixes { + ancestor = ancestor + "/" + component; + self.pack_dir(&ancestor, 0o555)?; + } + + self.pack_dir(&(ancestor + "/" + last), dir_mode) + } + + pub fn pack_trailer(&mut self) -> Result { + self.pack_one(TRAILER_NAME, b"", "", 0) + } +} diff --git a/rust/uefi/pio/tests/read_write.rs b/rust/uefi/pio/tests/read_write.rs new file mode 100644 index 00000000..3c881fc8 --- /dev/null +++ b/rust/uefi/pio/tests/read_write.rs @@ -0,0 +1,93 @@ +use std::{ + convert::Infallible, + io::{stdout, Cursor, Write}, +}; + +use cpio::NewcReader; +use pio::writer::Cpio; + +/* + * This test is not used in practice, + * because this is a interactive debugging test. + * Use it as a model to investigate issues. + * + * #[test] +fn visual_diagnose() { + let mut cpio = Cpio::::new(); + let contents = vec![0xAA; 10]; + let one_size = cpio.pack_one("test.txt", &contents, "", 0o000) + .expect("Failed to pack a file at the root directory"); + let trailer_size = cpio.pack_trailer() + .expect("Failed to pack the trailer of the CPIO archive"); + + let data = cpio.into_inner(); + stdout().write_all(data.as_slice().escape_ascii().collect::>().as_ref()) + .expect("Failed to write the CPIO textual representation"); + print!("\n"); + + let reader = NewcReader::new(Cursor::new(data)).expect("Failed to read the first entry"); + let entry = reader.entry(); + println!("entry: {}", entry.name()); + assert_eq!(entry.name(), "/test.txt"); + let reader = NewcReader::new(reader.finish().expect("To finish reading")).expect("Failed to read the trailer"); + let entry = reader.entry(); + println!("entry: {}", entry.name()); +} +*/ + +#[test] +fn alignment() { + let mut cpio = Cpio::::new(); + let contents = vec![0xAA; 10]; + let one_size = cpio + .pack_one("test.txt", &contents, "", 0o000) + .expect("Failed to pack a file at the root directory"); + let trailer_size = cpio + .pack_trailer() + .expect("Failed to pack the trailer of the CPIO archive"); + + assert!( + cpio.into_inner().len() % 4 == 0, + "CPIO is not aligned on a 4 bytes boundary!" + ); +} + +#[test] +fn write_read_prefix() { + let mut cpio = Cpio::::new(); + let contents = vec![0xAA; 10]; + cpio.pack_prefix("a/b/c/d/e/f", 0o600) + .expect("Failed to pack prefixes of a directory, including itself"); + + let data = cpio.into_inner(); + stdout() + .write_all(data.as_slice().escape_ascii().collect::>().as_ref()) + .expect("Failed to write the CPIO textual representation"); + print!("\n"); + + let reader = NewcReader::new(Cursor::new(data)).expect("Failed to read the first entry"); + let entry = reader.entry(); + println!("entry: {}", entry.name()); + assert_eq!(entry.name(), "/a"); + let reader = NewcReader::new(reader.finish().expect("To finish reading")) + .expect("Failed to read the trailer"); + let entry = reader.entry(); + println!("entry: {}", "/a/b"); +} + +#[test] +fn write_read_basic() { + let mut cpio = Cpio::::new(); + let contents = vec![0xAA; 10]; + let one_size = cpio + .pack_one("test.txt", &contents, "", 0o000) + .expect("Failed to pack a file at the root directory"); + let trailer_size = cpio + .pack_trailer() + .expect("Failed to pack the trailer of the CPIO archive"); + + assert!( + cpio.into_inner().len() % 4 == 0, + "CPIO is not aligned on a 4 bytes boundary!" + ); +} From 6668ee18a1181805c8736b5178f5876e42002bca Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 15 Nov 2023 05:25:10 +0100 Subject: [PATCH 08/19] stub: measure companion initrds --- rust/uefi/stub/src/main.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rust/uefi/stub/src/main.rs b/rust/uefi/stub/src/main.rs index 79f349ac..62a14a3a 100644 --- a/rust/uefi/stub/src/main.rs +++ b/rust/uefi/stub/src/main.rs @@ -20,7 +20,7 @@ use linux_bootloader::companions::{ discover_credentials, discover_system_extensions, get_default_dropin_directory, }; use linux_bootloader::efivars::{export_efi_variables, get_loader_features, EfiLoaderFeatures}; -use linux_bootloader::measure::measure_image; +use linux_bootloader::measure::{measure_companion_initrds, measure_image}; use linux_bootloader::tpm::tpm_available; use linux_bootloader::uefi_helpers::booted_image_file; use log::info; @@ -50,7 +50,10 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { print_logo(); - if tpm_available(system_table.boot_services()) { + let is_tpm_available = tpm_available(system_table.boot_services()); + let pe_in_memory = booted_image_file(system_table.boot_services()).expect("Failed to extract the in-memory information about our own image"); + + if is_tpm_available { info!("TPM available, will proceed to measurements."); // Iterate over unified sections and measure them // For now, ignore failures during measurements. @@ -58,10 +61,8 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { // and ensure this hard-fail correctly. let _ = measure_image( &system_table, - booted_image_file(system_table.boot_services()).unwrap(), + &pe_in_memory ); - // TODO: Measure kernel parameters - // TODO: Measure sysexts } if let Ok(features) = get_loader_features(system_table.runtime_services()) { From 31c3422b7fa79fd8c1ffc649c887a7b212321ee5 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 15 Nov 2023 05:24:59 +0100 Subject: [PATCH 09/19] stub: merge dynamically initrds For dynamic usecases, e.g. credentials or system extension images, we have a need for dynamic merging of initrds. --- rust/uefi/stub/src/fat.rs | 15 +++++++++++++-- rust/uefi/stub/src/thin.rs | 10 +++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/rust/uefi/stub/src/fat.rs b/rust/uefi/stub/src/fat.rs index be6cc0a0..48402445 100644 --- a/rust/uefi/stub/src/fat.rs +++ b/rust/uefi/stub/src/fat.rs @@ -51,7 +51,7 @@ pub fn boot_linux( // 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. - let config = unsafe { + let mut config = unsafe { EmbeddedConfiguration::new( booted_image_file(system_table.boot_services()) .unwrap() @@ -67,5 +67,16 @@ pub fn boot_linux( secure_boot_enabled, ); - boot_linux_unchecked(handle, system_table, config.kernel, &cmdline, config.initrd).status() + let mut final_initrd = Vec::new(); + final_initrd.append(&mut config.initrd); + + // Correctness: dynamic initrds are supposed to be validated by caller, + // i.e. they are system extension images or credentials + // that are supposedly measured in TPM2. + // Therefore, it is normal to not verify their hashes against a configuration. + for mut extra_initrd in dynamic_initrds { + final_initrd.append(&mut extra_initrd); + } + + boot_linux_unchecked(handle, system_table, config.kernel, &cmdline, final_initrd).status() } diff --git a/rust/uefi/stub/src/thin.rs b/rust/uefi/stub/src/thin.rs index 5183b82d..3817dade 100644 --- a/rust/uefi/stub/src/thin.rs +++ b/rust/uefi/stub/src/thin.rs @@ -99,7 +99,7 @@ pub fn boot_linux( let secure_boot_enabled = get_secure_boot_status(system_table.runtime_services()); let kernel_data; - let initrd_data; + let mut initrd_data; { let file_system = system_table @@ -135,5 +135,13 @@ pub fn boot_linux( secure_boot_enabled, )?; + // Correctness: dynamic initrds are supposed to be validated by caller, + // i.e. they are system extension images or credentials + // that are supposedly measured in TPM2. + // Therefore, it is normal to not verify their hashes against a configuration. + for mut extra_initrd in dynamic_initrds { + initrd_data.append(&mut extra_initrd); + } + boot_linux_unchecked(handle, system_table, kernel_data, &cmdline, initrd_data) } From 3bbcd7d4b3b723e24718696155968fa81f7501a2 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 15 Nov 2023 05:25:33 +0100 Subject: [PATCH 10/19] linux-bootloader: shuffle things around --- rust/uefi/linux-bootloader/src/companions.rs | 159 +++++++++++++++++++ rust/uefi/linux-bootloader/src/cpio.rs | 71 +++++++++ rust/uefi/linux-bootloader/src/initrd.rs | 42 ----- rust/uefi/linux-bootloader/src/lib.rs | 2 + rust/uefi/linux-bootloader/src/measure.rs | 91 ++++++++++- rust/uefi/linux-bootloader/tests/cpio.rs | 4 - 6 files changed, 320 insertions(+), 49 deletions(-) create mode 100644 rust/uefi/linux-bootloader/src/companions.rs create mode 100644 rust/uefi/linux-bootloader/src/cpio.rs delete mode 100644 rust/uefi/linux-bootloader/src/initrd.rs delete mode 100644 rust/uefi/linux-bootloader/tests/cpio.rs diff --git a/rust/uefi/linux-bootloader/src/companions.rs b/rust/uefi/linux-bootloader/src/companions.rs new file mode 100644 index 00000000..3b36bf06 --- /dev/null +++ b/rust/uefi/linux-bootloader/src/companions.rs @@ -0,0 +1,159 @@ +use crate::cpio::{pack_cpio, Cpio}; +use alloc::{string::ToString, vec::Vec}; +use uefi::{ + cstr16, + fs::{Path, PathBuf}, + prelude::BootServices, + proto::device_path::{ + text::{AllowShortcuts, DisplayOnly}, + DevicePath, + }, + CString16, +}; + +/// Locate files with ASCII filenames and matching the suffix passed as a parameter. +/// Returns a list of their paths. +pub fn find_files( + fs: &mut uefi::fs::FileSystem, + search_path: &Path, + suffix: &str, +) -> uefi::Result> { + let mut results = Vec::new(); + + for maybe_entry in fs.read_dir(search_path).unwrap() { + let entry = maybe_entry?; + if entry.is_regular_file() { + let fname = entry.file_name(); + if fname.is_ascii() && fname.to_string().ends_with(suffix) { + let mut full_path = CString16::from(search_path.to_cstr16()); + full_path.push_str(cstr16!("\\")); + full_path.push_str(fname); + results.push(full_path.into()); + } + } + } + + Ok(results) +} + +/// Returns the "default" drop-in directory if it exists. +/// This will be in general $loaded_image_path.extra/ +pub fn get_default_dropin_directory( + boot_services: &BootServices, + loaded_image_file_path: &DevicePath, + fs: &mut uefi::fs::FileSystem, +) -> uefi::Result> { + // We could use LoadedImageDevicePath to get the full device path + // and perform replacement of the last node before END_ENTIRE + // by another node containing the filename + .extra + // But this is as much tedious as performing a conversion to string + // then opening the root directory and finding the new directory. + let mut target_directory = loaded_image_file_path.to_string(boot_services, DisplayOnly(false), AllowShortcuts(false)) + // This is the Result-level error + .expect("Failed to obtain the string representation of the loaded image file path") + // This is the Option-level error (not enough memory) + .expect("Failed to obtain the string representation of the loaded image file path; not enough memory?"); + target_directory.push_str(cstr16!(".extra")); + + return Ok(fs + .metadata(target_directory.as_ref()) + .ok() + .and_then(|metadata| { + if metadata.is_directory() { + Some(PathBuf::from(target_directory)) + } else { + None + } + })); +} + +/// Potential companion initrd assembled on the fly +/// during discovery workflows, e.g. finding files in drop-in directories. +pub enum CompanionInitrd { + Credentials(Cpio), + GlobalCredentials(Cpio), + SystemExtension(Cpio), + PcrSignature(Cpio), + PcrPublicKey(Cpio), +} + +impl CompanionInitrd { + pub fn into_cpio(self) -> Cpio { + match self { + Self::Credentials(c) + | Self::GlobalCredentials(c) + | Self::SystemExtension(c) + | Self::PcrSignature(c) + | Self::PcrPublicKey(c) => c, + } + } +} + +/// Discover any credentials, i.e. files ending by .cred +/// Credentials comes into two variants: +/// - global credentials ($ESP/loader/credentials/*.cred), global to the ESP +/// - local credentials ($path_to_image.extra/*.cred), specific to this image +/// +/// Those will be unmeasured, you are responsible for measuring them or not. +/// But CPIOs are guaranteed to be stable and independent of file discovery order. +pub fn discover_credentials( + fs: &mut uefi::fs::FileSystem, + default_dropin_dir: Option<&Path>, +) -> uefi::Result> { + let mut companions = Vec::new(); + + let default_global_dropin_dir = cstr16!("\\loader\\credentials"); + if fs.try_exists(default_global_dropin_dir).unwrap() { + let metadata = fs.metadata(default_global_dropin_dir).expect("Failed to obtain metadata on `\\loader\\credentials` path (which is supposed to exist)"); + if metadata.is_directory() { + let global_credentials: Vec = + find_files(fs, default_global_dropin_dir.as_ref(), ".cred")?; + + if !global_credentials.is_empty() { + companions.push(CompanionInitrd::GlobalCredentials( + pack_cpio( + fs, + global_credentials, + ".extra/global_credentials", + 0o500, + 0o400, + ) + .expect("Failed to pack CPIO"), + )); + } + } + } + + if let Some(default_dropin_dir) = default_dropin_dir { + let local_credentials: Vec = find_files(fs, default_dropin_dir, ".cred")?; + + if !local_credentials.is_empty() { + companions.push(CompanionInitrd::Credentials( + pack_cpio(fs, local_credentials, ".extra/credentials", 0o500, 0o400) + .expect("Failed to pack CPIO"), + )); + } + } + + Ok(companions) +} +/// Discover any system image extension, i.e. files ending by .raw +/// They must be present inside $path_to_image.extra/*.raw, specific to this image. +/// +/// Those will be unmeasured, you are responsible for measuring them or not. +/// But CPIOs are guaranteed to be stable and independent of file discovery order. +pub fn discover_system_extensions( + fs: &mut uefi::fs::FileSystem, + default_dropin_dir: &Path, +) -> uefi::Result> { + let mut companions = Vec::new(); + let sysexts = find_files(fs, default_dropin_dir, ".raw")?; + + if !sysexts.is_empty() { + companions.push(CompanionInitrd::SystemExtension( + pack_cpio(fs, sysexts, ".extra/sysext", 0o555, 0o444).expect("Failed to pack CPIO"), + )); + } + + Ok(companions) +} diff --git a/rust/uefi/linux-bootloader/src/cpio.rs b/rust/uefi/linux-bootloader/src/cpio.rs new file mode 100644 index 00000000..731c1c5e --- /dev/null +++ b/rust/uefi/linux-bootloader/src/cpio.rs @@ -0,0 +1,71 @@ +use core::convert::Infallible; + +use alloc::{string::String, vec::Vec}; +use pio::errors::CPIOError; +use uefi::fs::{Path, PathBuf}; + +pub type Cpio = pio::writer::Cpio; +pub type Result = core::result::Result>; + +/// Given a file contents and a filename, this will create an ad-hoc CPIO archive +/// containing this single item inside. +/// It is largely similar to `pack_cpio` except that it operates on a single file that is already +/// in memory. +pub fn pack_cpio_literal( + contents: &[u8], + target_filename: &Path, + target_dir_prefix: &str, + dir_mode: u32, + access_mode: u32, +) -> Result { + let mut cpio = Cpio::new(); + + let utf8_filename = String::from(target_filename.to_cstr16()); + + cpio.pack_prefix(target_dir_prefix, dir_mode)?; + cpio.pack_one(&utf8_filename, contents, target_dir_prefix, access_mode)?; + cpio.pack_trailer()?; + + Ok(cpio) +} + +/// Given a list of filenames and a filesystem high-level interface, +/// this will pack all those files in-memory in a CPIO archive (newc format) +/// which will decompress to the provided `target_dir_prefix`. +/// +/// In the CPIO archives, only the basename is retained as a filename. +/// +/// For consistency of TPM2 measurements, the `files` list will be sorted in this function. +/// +/// Target directory prefix will be created with `dir_mode` access privileges, +/// files will be created with `access_mode`. +/// +/// All prefixes of the target directory prefix excluding itself will be created with 555 +/// permission bits. +pub fn pack_cpio( + fs: &mut uefi::fs::FileSystem, + mut files: Vec, + target_dir_prefix: &str, + dir_mode: u32, + access_mode: u32, +) -> Result { + let mut cpio = Cpio::new(); + + // Ensure consistency of the CPIO archive layout for future potential measurements via TPM2. + files.sort(); + + cpio.pack_prefix(target_dir_prefix, dir_mode)?; + for file in files { + let utf8_filename = String::from( + &file + .components() + .last() + .expect("Expected the filename to possess a file name!"), + ); + let contents = fs.read(file).expect("failed to read"); + cpio.pack_one(&utf8_filename, &contents, target_dir_prefix, access_mode)?; + } + cpio.pack_trailer()?; + + Ok(cpio) +} diff --git a/rust/uefi/linux-bootloader/src/initrd.rs b/rust/uefi/linux-bootloader/src/initrd.rs deleted file mode 100644 index 7bf9e1ef..00000000 --- a/rust/uefi/linux-bootloader/src/initrd.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::{cpio::Cpio, uefi_helpers::SD_LOADER, measure::TPM_PCR_INDEX_KERNEL_PARAMETERS}; -use alloc::vec::Vec; -use uefi::{prelude::RuntimeServices, table::runtime::VariableAttributes, cstr16}; - -pub enum CompanionInitrd { - Credentials(Cpio), - GlobalCredentials(Cpio), - SystemExtension(Cpio), - PcrSignature(Cpio), - PcrPublicKey(Cpio) -} - -pub fn export_pcr_efi_variables(runtime_services: &RuntimeServices, - initrds: Vec) -> uefi::Result { - // Do we have kernel parameters that were measured - if initrds.iter().any(|e| match e { - CompanionInitrd::Credentials(_) => true, - CompanionInitrd::GlobalCredentials(_) => true, - _ => false - }) { - runtime_services.set_variable( - cstr16!("StubPcrKernelParameters"), - &SD_LOADER, - VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS, - &TPM_PCR_INDEX_KERNEL_PARAMETERS.0.to_le_bytes() - )?; - } - // Do we have system extensions that were measured - if initrds.iter().any(|e| match e { - CompanionInitrd::SystemExtension(_) => true, - _ => false - }) { - runtime_services.set_variable( - cstr16!("StubPcrInitRDSysExts"), - &SD_LOADER, - VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS, - &TPM_PCR_INDEX_KERNEL_PARAMETERS.0.to_le_bytes() - )?; - } - - Ok(()) -} diff --git a/rust/uefi/linux-bootloader/src/lib.rs b/rust/uefi/linux-bootloader/src/lib.rs index a4e1f28b..e4e93e41 100644 --- a/rust/uefi/linux-bootloader/src/lib.rs +++ b/rust/uefi/linux-bootloader/src/lib.rs @@ -2,6 +2,8 @@ extern crate alloc; +pub mod companions; +pub mod cpio; pub mod efivars; pub mod linux_loader; pub mod measure; diff --git a/rust/uefi/linux-bootloader/src/measure.rs b/rust/uefi/linux-bootloader/src/measure.rs index 15ccc682..940757e3 100644 --- a/rust/uefi/linux-bootloader/src/measure.rs +++ b/rust/uefi/linux-bootloader/src/measure.rs @@ -6,13 +6,20 @@ use uefi::{ }; use crate::{ - efivars::BOOT_LOADER_VENDOR_UUID, pe_section::pe_section_data, tpm::tpm_log_event_ascii, - uefi_helpers::PeInMemory, unified_sections::UnifiedSection, + companions::CompanionInitrd, efivars::BOOT_LOADER_VENDOR_UUID, pe_section::pe_section_data, + tpm::tpm_log_event_ascii, uefi_helpers::PeInMemory, unified_sections::UnifiedSection, }; +/// This is where any stub payloads are extended, e.g. kernel ELF image, embedded initrd +/// and so on. +/// Compared to PCR4, this contains only the unified sections rather than the whole PE image as-is. const TPM_PCR_INDEX_KERNEL_IMAGE: PcrIndex = PcrIndex(11); +/// This is where lanzastub extends the kernel command line and any passed credentials into +const TPM_PCR_INDEX_KERNEL_CONFIG: PcrIndex = PcrIndex(12); +/// This is where we extend the initrd sysext images into which we pass to the booted kernel +const TPM_PCR_INDEX_SYSEXTS: PcrIndex = PcrIndex(13); -pub fn measure_image(system_table: &SystemTable, image: PeInMemory) -> uefi::Result { +pub fn measure_image(system_table: &SystemTable, image: &PeInMemory) -> uefi::Result { let runtime_services = system_table.runtime_services(); let boot_services = system_table.boot_services(); @@ -60,3 +67,81 @@ pub fn measure_image(system_table: &SystemTable, image: PeInMemory) -> uef Ok(measurements) } + +/// Performs all the expected measurements for any list of +/// companion initrds of any form. +/// +/// Relies on the passed order of `companions` for measurements in the same PCR. +/// You are responsible for honoring a stable order. +pub fn measure_companion_initrds( + system_table: &SystemTable, + companions: &[CompanionInitrd], +) -> uefi::Result { + let runtime_services = system_table.runtime_services(); + let boot_services = system_table.boot_services(); + + let mut measurements = 0; + let mut credentials_measured = 0; + let mut sysext_measured = false; + + for initrd in companions { + match initrd { + CompanionInitrd::PcrSignature(_) | CompanionInitrd::PcrPublicKey(_) => { + continue; + } + CompanionInitrd::Credentials(cpio) => { + if tpm_log_event_ascii( + boot_services, + TPM_PCR_INDEX_KERNEL_CONFIG, + cpio.as_ref(), + "Credentials initrd", + )? { + measurements += 1; + credentials_measured += 1; + } + } + CompanionInitrd::GlobalCredentials(cpio) => { + if tpm_log_event_ascii( + boot_services, + TPM_PCR_INDEX_KERNEL_CONFIG, + cpio.as_ref(), + "Global credentials initrd", + )? { + measurements += 1; + credentials_measured += 1; + } + } + CompanionInitrd::SystemExtension(cpio) => { + if tpm_log_event_ascii( + boot_services, + TPM_PCR_INDEX_SYSEXTS, + cpio.as_ref(), + "System extension initrd", + )? { + measurements += 1; + sysext_measured = true; + } + } + } + } + + if credentials_measured > 0 { + runtime_services.set_variable( + cstr16!("StubPcrKernelParameters"), + &BOOT_LOADER_VENDOR_UUID, + VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS, + &TPM_PCR_INDEX_KERNEL_CONFIG.0.to_le_bytes(), + )?; + } + + if sysext_measured { + runtime_services.set_variable( + cstr16!("StubPcrInitRDSysExts"), + &BOOT_LOADER_VENDOR_UUID, + VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS, + &TPM_PCR_INDEX_SYSEXTS.0.to_le_bytes(), + )?; + } + + Ok(measurements) +} diff --git a/rust/uefi/linux-bootloader/tests/cpio.rs b/rust/uefi/linux-bootloader/tests/cpio.rs deleted file mode 100644 index d12ad9da..00000000 --- a/rust/uefi/linux-bootloader/tests/cpio.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[test] -fn test_writing_files() { - -} From 06f9a35c2f14f7b0058d0f64dd3efe53aabb25d4 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 15 Nov 2023 07:37:13 +0100 Subject: [PATCH 11/19] tool(systemd): support global credentials installation Now, lzbt can install global credentials directories passed inside the ESP and manage the lifecycle of it in a basic fashion. --- rust/tool/Cargo.lock | 8 ++--- rust/tool/systemd/Cargo.toml | 2 +- rust/tool/systemd/src/cli.rs | 10 +++++++ rust/tool/systemd/src/esp.rs | 8 +++-- rust/tool/systemd/src/install.rs | 51 +++++++++++++++++++++++++++++++- 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/rust/tool/Cargo.lock b/rust/tool/Cargo.lock index 24b1ce29..1399d9ca 100644 --- a/rust/tool/Cargo.lock +++ b/rust/tool/Cargo.lock @@ -143,9 +143,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" dependencies = [ "memchr", "regex-automata", @@ -639,9 +639,9 @@ checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" dependencies = [ "bitflags 2.4.1", "errno", diff --git a/rust/tool/systemd/Cargo.toml b/rust/tool/systemd/Cargo.toml index 225dfdde..b394ceaf 100644 --- a/rust/tool/systemd/Cargo.toml +++ b/rust/tool/systemd/Cargo.toml @@ -17,6 +17,7 @@ serde_json = "1.0.108" sha2 = "0.10.8" tempfile = "3.8.1" nix = { version = "0.27.1", default-features = false, features = [ "fs" ] } +walkdir = "2.4.0" [dev-dependencies] assert_cmd = "2.0.12" @@ -24,4 +25,3 @@ expect-test = "1.4.1" filetime = "0.2.23" rand = "0.8.5" goblin = "0.7.1" -walkdir = "2.4.0" diff --git a/rust/tool/systemd/src/cli.rs b/rust/tool/systemd/src/cli.rs index 90d91c03..5642b3ef 100644 --- a/rust/tool/systemd/src/cli.rs +++ b/rust/tool/systemd/src/cli.rs @@ -55,6 +55,14 @@ struct InstallCommand { #[arg(long, default_value_t = 1)] configuration_limit: usize, + /// Directory containing the global credentials to install in the ESP + #[arg(long)] + global_credentials_directory: Option, + + /// Directory containing the local credentials to install in the ESP for latest generation + #[arg(long)] + local_credentials_directory: Option, + /// EFI system partition mountpoint (e.g. efiSysMountPoint) esp: PathBuf, @@ -102,6 +110,8 @@ fn install(args: InstallCommand) -> Result<()> { args.configuration_limit, args.esp, args.generations, + args.global_credentials_directory, + args.local_credentials_directory, ) .install() } diff --git a/rust/tool/systemd/src/esp.rs b/rust/tool/systemd/src/esp.rs index 7227367f..2d10752f 100644 --- a/rust/tool/systemd/src/esp.rs +++ b/rust/tool/systemd/src/esp.rs @@ -17,9 +17,10 @@ pub struct SystemdEspPaths { pub systemd_boot: PathBuf, pub loader: PathBuf, pub systemd_boot_loader_config: PathBuf, + pub global_credentials: PathBuf, } -impl EspPaths<10> for SystemdEspPaths { +impl EspPaths<11> for SystemdEspPaths { fn new(esp: impl AsRef, architecture: Architecture) -> Self { let esp = esp.as_ref(); let efi = esp.join("EFI"); @@ -29,6 +30,7 @@ impl EspPaths<10> for SystemdEspPaths { let efi_efi_fallback_dir = efi.join("BOOT"); let loader = esp.join("loader"); let systemd_boot_loader_config = loader.join("loader.conf"); + let global_credentials = loader.join("credentials"); Self { esp: esp.to_path_buf(), @@ -41,6 +43,7 @@ impl EspPaths<10> for SystemdEspPaths { systemd_boot: efi_systemd.join(architecture.systemd_filename()), loader, systemd_boot_loader_config, + global_credentials, } } @@ -52,7 +55,7 @@ impl EspPaths<10> for SystemdEspPaths { &self.linux } - fn iter(&self) -> std::array::IntoIter<&PathBuf, 10> { + fn iter(&self) -> std::array::IntoIter<&PathBuf, 11> { [ &self.esp, &self.efi, @@ -64,6 +67,7 @@ impl EspPaths<10> for SystemdEspPaths { &self.systemd_boot, &self.loader, &self.systemd_boot_loader_config, + &self.global_credentials, ] .into_iter() } diff --git a/rust/tool/systemd/src/install.rs b/rust/tool/systemd/src/install.rs index a40dca44..39335d90 100644 --- a/rust/tool/systemd/src/install.rs +++ b/rust/tool/systemd/src/install.rs @@ -7,11 +7,12 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::string::ToString; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use base32ct::{Base32Unpadded, Encoding}; use nix::unistd::syncfs; use sha2::{Digest, Sha256}; use tempfile::TempDir; +use walkdir::WalkDir; use crate::architecture::SystemdArchitectureExt; use crate::esp::SystemdEspPaths; @@ -36,6 +37,8 @@ pub struct Installer { esp_paths: SystemdEspPaths, generation_links: Vec, arch: Architecture, + global_credentials_dir: Option, + _local_credentials_dir: Option, } impl Installer { @@ -49,6 +52,8 @@ impl Installer { configuration_limit: usize, esp: PathBuf, generation_links: Vec, + global_credentials_dir: Option, + local_credentials_dir: Option, ) -> Self { let mut gc_roots = Roots::new(); let esp_paths = SystemdEspPaths::new(esp, arch); @@ -65,6 +70,8 @@ impl Installer { esp_paths, generation_links, arch, + global_credentials_dir, + _local_credentials_dir: local_credentials_dir, } } @@ -96,6 +103,9 @@ impl Installer { self.install_generations_from_links(&links)?; self.install_systemd_boot()?; + if let Some(credentials_dir) = &self.global_credentials_dir { + self.install_global_credentials(credentials_dir)?; + } if self.broken_gens.is_empty() { log::info!("Collecting garbage..."); @@ -351,6 +361,45 @@ impl Installer { Ok(()) } + + /// Install global credentials to ESP. + /// + /// Global credentials are only updated when they differ from the existing ones. + /// Global credentials are never removed manually, but we can warn user of leftover + /// global credentials which are not tracked anymore inside of the NixOS system closure. + fn install_global_credentials(&self, credentials_dir: &Path) -> Result<()> { + for entry in WalkDir::new(credentials_dir) { + let entry = entry?; + // Skip non-files. + if !entry.file_type().is_file() { + continue; + } + + let ext = entry.path().extension(); + if ext.is_none() || ext.unwrap() != "cred" { + bail!("`{}` is specified as a global credential but does not end with `.cred` as per specification, please correct this issue by moving, renaming or deleting this file.", entry.path().display()); + } + + // By virtue of previous check, `file_name` must exist. + let target_path = self + .esp_paths + .global_credentials + .join(entry.path().file_name().unwrap()); + + if !target_path.try_exists()? || file_hash(entry.path())? != file_hash(&target_path)? { + log::info!("Updating global credential {target_path:?}..."); + install(entry.path(), &target_path).with_context(|| { + format!( + "Failed to install global credential {:?} to {:?}", + entry.path(), + &target_path + ) + })?; + } + } + + Ok(()) + } } /// Translate an EFI path to an absolute path on the mounted ESP. From ebf70fe9518cb2c7cf452c1c87e7efb555e3dc67 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 15 Nov 2023 07:39:12 +0100 Subject: [PATCH 12/19] linux-bootloader: take note of the `image_device_path` in `PeInMemory` As soon as we open in exclusive our own image, we will not be able easily to re-open it because of the locking mechanism. Therefore, we should use the opportunity to get information we may be interested in and avoid us re-opening the loaded image protocol. This is useful to know from where you were loaded for example, e.g. filesystem path. --- .../uefi/linux-bootloader/src/uefi_helpers.rs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/rust/uefi/linux-bootloader/src/uefi_helpers.rs b/rust/uefi/linux-bootloader/src/uefi_helpers.rs index 2178e70e..02481c8f 100644 --- a/rust/uefi/linux-bootloader/src/uefi_helpers.rs +++ b/rust/uefi/linux-bootloader/src/uefi_helpers.rs @@ -1,9 +1,17 @@ use core::ffi::c_void; -use uefi::{prelude::BootServices, proto::loaded_image::LoadedImage, Result}; +use uefi::{ + prelude::BootServices, + proto::{ + device_path::{DevicePath, FfiDevicePath}, + loaded_image::LoadedImage, + }, + Result, +}; #[derive(Debug, Clone, Copy)] pub struct PeInMemory { + image_device_path: Option<*const FfiDevicePath>, image_base: *const c_void, image_size: usize, } @@ -22,6 +30,23 @@ impl PeInMemory { pub unsafe fn as_slice(&self) -> &'static [u8] { unsafe { core::slice::from_raw_parts(self.image_base as *const u8, self.image_size) } } + + /// Return optionally a reference to the device path + /// relative to this image's simple file system. + pub fn file_path(&self) -> Option<&DevicePath> { + // SAFETY: + // + // The returned reference to the device path will be alive as long + // as `self` is alive as it relies on the thin internal pointer to remain around, + // which is guaranteed as long as the structure is not dropped. + // + // This means that the safety guarantees of [`uefi::device_path::DevicePath::from_ffi_ptr`] + // are guaranteed. + unsafe { + self.image_device_path + .map(|ptr| DevicePath::from_ffi_ptr(ptr)) + } + } } /// Open the currently executing image as a file. @@ -31,6 +56,7 @@ pub fn booted_image_file(boot_services: &BootServices) -> Result { let (image_base, image_size) = loaded_image.info(); Ok(PeInMemory { + image_device_path: loaded_image.file_path().map(|dp| dp.as_ffi_ptr()), image_base, image_size: usize::try_from(image_size).map_err(|_| uefi::Status::INVALID_PARAMETER)?, }) From afd200221de6f89dba79b2e604dfc88d4fb6e22c Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 15 Nov 2023 07:40:00 +0100 Subject: [PATCH 13/19] modules/lanzaboote: support global credentials and local credentials Now the lanzaboote module can understand passed list of credentials, either global or local and pass them to `lzbt` CLI. --- nix/modules/lanzaboote.nix | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/nix/modules/lanzaboote.nix b/nix/modules/lanzaboote.nix index d1dd8876..4e19bcdd 100644 --- a/nix/modules/lanzaboote.nix +++ b/nix/modules/lanzaboote.nix @@ -12,6 +12,25 @@ let lib.generators.mkKeyValueDefault { } " " k v; }; + assembleCredentialDirectoryFromDrv = drv: '' + for credential in ${drv}/*; do + echo "Processing $credential" + if [[ "''${credential##*.}" != "cred" ]]; then + echo "Found a non-credential: $credential, please remove it or move it." + exit 1 + fi + cp $credential $out/ + done + ''; + + assembleCredentialDirectory = drvs: pkgs.runCommand "assemble-credentials" { } '' + mkdir -p $out/ + ${concatStringsSep "\n" (map assembleCredentialDirectoryFromDrv drvs)} + ''; + + globalCredentialsDirectory = assembleCredentialDirectory cfg.globalCredentials; + localCredentialsDirectory = assembleCredentialDirectory cfg.localCredentials; + loaderConfigFile = loaderSettingsFormat.generate "loader.conf" cfg.settings; configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit; @@ -36,6 +55,30 @@ in ''; }; + globalCredentials = mkOption { + type = types.listOf types.package; + description = lib.mdDoc '' + A list of derivations containing multiple .cred files inside of it. + If anything else than a .cred is found, in the top-level, this will fail + at assembly time. + + This will be installed in $ESP/loader/credentials. + In case of data conflict, the installer will fail and ask for manual removal. + ''; + }; + + localCredentials = mkOption { + type = types.listOf types.package; + description = lib.mdDoc '' + A list of derivations containing multiple .cred files inside of it. + If anything else than a .cred is found, in the top-level, this will fail + at assembly time. + + This will be installed in this generation's drop-in directory specifically. + In case of data conflict, the installer will fail and ask for manual removal. + ''; + }; + pkiBundle = mkOption { type = types.nullOr types.path; description = "PKI bundle containing db, PK, KEK"; @@ -125,6 +168,8 @@ in --public-key ${cfg.publicKeyFile} \ --private-key ${cfg.privateKeyFile} \ --configuration-limit ${toString configurationLimit} \ + --global-credentials-directory ${globalCredentialsDirectory} \ + --local-credentials-directory ${localCredentialsDirectory} \ ${config.boot.loader.efi.efiSysMountPoint} \ /nix/var/nix/profiles/system-*-link ''; From 423ed7500c81f6e52289f9b96f2c286ce11c3205 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 15 Nov 2023 07:40:43 +0100 Subject: [PATCH 14/19] nix/tests/lanzaboote: add `credentials-basic` test Create a simple test to evaluate credentials mechanism with lanzaboote that does not even make use of TPM2 and just copy garbage around. --- nix/tests/lanzaboote.nix | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/nix/tests/lanzaboote.nix b/nix/tests/lanzaboote.nix index f6e0ceba..d59f63e7 100644 --- a/nix/tests/lanzaboote.nix +++ b/nix/tests/lanzaboote.nix @@ -6,7 +6,8 @@ let inherit (pkgs) lib system; defaultTimeout = 5 * 60; # = 5 minutes - mkSecureBootTest = { name, machine ? { }, useSecureBoot ? true, useTPM2 ? false, readEfiVariables ? false, testScript }: + # `handInstrumentationOverInInitrd`: stop the test script at initrd time in stage 1 so you can assert stage 1 behavior via https://github.com/NixOS/nixpkgs/pull/256226 backdoor. + mkSecureBootTest = { name, machine ? { }, useSecureBoot ? true, useTPM2 ? false, readEfiVariables ? false, handInstrumentationInInitrd ? false, globalCredentials ? [ ], localCredentials ? [ ], testScript }: let tpmSocketPath = "/tmp/swtpm-sock"; tpmDeviceModels = { @@ -94,6 +95,9 @@ let machine ]; + testing.initrdBackdoor = lib.mkIf handInstrumentationInInitrd true; + boot.initrd.systemd.enable = lib.mkIf handInstrumentationInInitrd true; + virtualisation = { useBootLoader = true; useEFIBoot = true; @@ -146,6 +150,7 @@ let enable = true; enrollKeys = lib.mkDefault true; pkiBundle = ./fixtures/uefi-keys; + inherit globalCredentials localCredentials; }; }; }; @@ -450,4 +455,17 @@ in ''; }; + credentials-basic = mkSecureBootTest { + name = "lanzaboote-credentials-basic"; + readEfiVariables = true; + handInstrumentationInInitrd = true; + globalCredentials = [ + (pkgs.writeTextDir "super-secret.cred" "MASTER_KEY=lanzarote") + ]; + testScript = '' + machine.start() + contents = machine.succeed("cat /.extra/global_credentials/super-secret.cred") + assert "MASTER_KEY=lanzarote" == contents, f"Unexpected credential contents, got: {contents}" + ''; + }; } From 113bd2449fe80974bca826df69533abd67e36fde Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 15 Nov 2023 08:26:47 +0100 Subject: [PATCH 15/19] linux-bootloader: communicate that we theoretically support credentials and sysexts now systemd just has to pick them up! --- rust/uefi/linux-bootloader/src/efivars.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/uefi/linux-bootloader/src/efivars.rs b/rust/uefi/linux-bootloader/src/efivars.rs index 23c0f36d..3b0cb7da 100644 --- a/rust/uefi/linux-bootloader/src/efivars.rs +++ b/rust/uefi/linux-bootloader/src/efivars.rs @@ -146,7 +146,9 @@ pub fn export_efi_variables(stub_info_name: &str, system_table: &SystemTable(boot_services.image_handle())?; From 63d41c9f2a1a3cf7cd70b4ca065d44a7a57ac4de Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Wed, 15 Nov 2023 08:28:59 +0100 Subject: [PATCH 16/19] stub(thin): align initrds! And offer a way to debug CPIO representations just with your eyes, Here's a useful snippet to transform that representation into a proper binary CPIO using Python: >>> raw = "" >>> open("/tmp/cpio", "wb").write(bytes(raw.replace("\\x00", "\x00"), "ascii")) Then, you can poke the cpio using `cpio -t < /tmp/cpio` or extract it and `cpio` should complain accordingly. --- rust/uefi/stub/src/thin.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/rust/uefi/stub/src/thin.rs b/rust/uefi/stub/src/thin.rs index 3817dade..b5e57b1a 100644 --- a/rust/uefi/stub/src/thin.rs +++ b/rust/uefi/stub/src/thin.rs @@ -1,3 +1,4 @@ +use alloc::vec; use alloc::vec::Vec; use log::{error, warn}; use sha2::{Digest, Sha256}; @@ -139,8 +140,31 @@ pub fn boot_linux( // i.e. they are system extension images or credentials // that are supposedly measured in TPM2. // Therefore, it is normal to not verify their hashes against a configuration. + + /// Compute the necessary padding based on the provided length + /// It returns None if no padding is necessary. + fn compute_pad4(len: usize) -> Option> { + let overhang = len % 4; + if overhang != 0 { + let repeat = 4 - overhang; + Some(vec![0u8; repeat]) + } else { + None + } + } + if let Some(mut padding) = compute_pad4(initrd_data.len()) { + initrd_data.append(&mut padding); + } + for mut extra_initrd in dynamic_initrds { + // Uncomment for maximal debugging pleasure. + // let debug_representation = extra_initrd.as_slice().escape_ascii().collect::>(); + // log::warn!("{:?}", String::from_utf8_lossy(&debug_representation)); initrd_data.append(&mut extra_initrd); + // Extra initrds ideally should be aligned, but just in case, let's verify this. + if let Some(mut padding) = compute_pad4(initrd_data.len()) { + initrd_data.append(&mut padding); + } } boot_linux_unchecked(handle, system_table, kernel_data, &cmdline, initrd_data) From 3289b607cc7b24d792c38186ac999018303ed5a0 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Tue, 2 Jan 2024 01:08:35 +0100 Subject: [PATCH 17/19] stub: pin to uefi 0.25.0 fork Until I rebase my changes and push it upstream properly. --- rust/uefi/Cargo.lock | 24 ++++++++++++------------ rust/uefi/linux-bootloader/Cargo.toml | 2 +- rust/uefi/stub/Cargo.toml | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/rust/uefi/Cargo.lock b/rust/uefi/Cargo.lock index a318e347..106e5fd2 100644 --- a/rust/uefi/Cargo.lock +++ b/rust/uefi/Cargo.lock @@ -116,9 +116,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "linux-bootloader" @@ -155,9 +155,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1" dependencies = [ "unicode-ident", ] @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "22a37c9326af5ed140c86a46655b5278de879853be5573c01df185b6f49a580a" dependencies = [ "proc-macro2", ] @@ -208,7 +208,7 @@ checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.45", ] [[package]] @@ -257,9 +257,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "0eae3c679c56dc214320b67a1bc04ef3dfbd6411f6443974b5e4893231298e66" dependencies = [ "proc-macro2", "quote", @@ -302,7 +302,7 @@ source = "git+https://github.com/RaitoBezarius/uefi-rs?branch=fs-improvements#2b dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.45", ] [[package]] @@ -327,9 +327,9 @@ dependencies = [ [[package]] name = "uguid" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef516f0806c5f61da6aa95125d0eb2d91cc95b2df426c06bde8be657282aee5" +checksum = "ab14ea9660d240e7865ce9d54ecdbd1cd9fa5802ae6f4512f093c7907e921533" [[package]] name = "unicode-ident" diff --git a/rust/uefi/linux-bootloader/Cargo.toml b/rust/uefi/linux-bootloader/Cargo.toml index 6b83b71e..3904c4cb 100644 --- a/rust/uefi/linux-bootloader/Cargo.toml +++ b/rust/uefi/linux-bootloader/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/nix-community/lanzaboote/" rust-version = "1.68" [dependencies] -uefi = { version = "0.26.0", default-features = false, features = [ "alloc", "global_allocator" ] } +uefi = { version = "0.25.0", default-features = false, features = [ "alloc", "global_allocator" ] } # Update blocked by #237 goblin = { version = "=0.6.1", default-features = false, features = [ "pe64", "alloc" ]} bitflags = "2.4.1" diff --git a/rust/uefi/stub/Cargo.toml b/rust/uefi/stub/Cargo.toml index b135fea2..a11410f1 100644 --- a/rust/uefi/stub/Cargo.toml +++ b/rust/uefi/stub/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" publish = false [dependencies] -uefi = { version = "0.26.0", default-features = false, features = [ "alloc", "global_allocator" ] } -uefi-services = { version = "0.23.0", default-features = false, features = [ "panic_handler", "logger" ] } +uefi = { version = "0.25.0", default-features = false, features = [ "alloc", "global_allocator" ] } +uefi-services = { version = "0.22.0", default-features = false, features = [ "panic_handler", "logger" ] } # Even in debug builds, we don't enable the debug logs, because they generate a lot of spam from goblin. log = { version = "0.4.20", default-features = false, features = [ "max_level_info", "release_max_level_warn" ]} # Use software implementation because the UEFI target seems to need it. From 58722f4ecfa3c986f30d7fa9d793863425f8392f Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Tue, 2 Jan 2024 01:08:55 +0100 Subject: [PATCH 18/19] flake: bump to 24.05+ Now, systemd is fixed, we can use credentials. --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 722f3186..efbb371d 100644 --- a/flake.lock +++ b/flake.lock @@ -97,11 +97,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1699354722, - "narHash": "sha256-abmqUReg4PsyQSwv4d0zjcWpMHrd3IFJiTb2tZpfF04=", + "lastModified": 1704060006, + "narHash": "sha256-DJqxVZf35Gaw07Vt0qT1cS0pqfzZiuB+WfLTJNJdbgY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cfbb29d76949ae53c457f152c52c173ea4bdd862", + "rev": "e5af05cbf33cf3d4148a4a1ce6cab1c5fb8fec09", "type": "github" }, "original": { From cf5887c55a13c31391dc5d9b8cb8646d4861d3b3 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Tue, 2 Jan 2024 01:56:44 +0100 Subject: [PATCH 19/19] stub: `cargo fmt` it Those are leftovers that didn't fuse correctly with my past commits. --- rust/uefi/stub/src/main.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/rust/uefi/stub/src/main.rs b/rust/uefi/stub/src/main.rs index 62a14a3a..45148824 100644 --- a/rust/uefi/stub/src/main.rs +++ b/rust/uefi/stub/src/main.rs @@ -51,7 +51,8 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { print_logo(); let is_tpm_available = tpm_available(system_table.boot_services()); - let pe_in_memory = booted_image_file(system_table.boot_services()).expect("Failed to extract the in-memory information about our own image"); + let pe_in_memory = booted_image_file(system_table.boot_services()) + .expect("Failed to extract the in-memory information about our own image"); if is_tpm_available { info!("TPM available, will proceed to measurements."); @@ -59,10 +60,7 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { // 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, - &pe_in_memory - ); + let _ = measure_image(&system_table, &pe_in_memory); } if let Ok(features) = get_loader_features(system_table.runtime_services()) { @@ -92,8 +90,12 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { let default_dropin_directory; if let Some(loaded_image_path) = pe_in_memory.file_path() { - default_dropin_directory = get_default_dropin_directory(system_table.boot_services(), loaded_image_path, &mut filesystem) - .expect("Failed to obtain the default drop-in directory"); + default_dropin_directory = get_default_dropin_directory( + system_table.boot_services(), + loaded_image_path, + &mut filesystem, + ) + .expect("Failed to obtain the default drop-in directory"); } else { default_dropin_directory = None; }