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": { 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 ''; 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}" + ''; + }; } 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/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. diff --git a/rust/uefi/Cargo.lock b/rust/uefi/Cargo.lock index 94d63e80..106e5fd2 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,18 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "generic-array" version = "0.14.7" @@ -79,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" @@ -92,17 +116,19 @@ 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" version = "0.3.0" dependencies = [ "bitflags", + "embedded-io", "goblin", "log", + "pio", "uefi", ] @@ -112,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" @@ -120,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", ] @@ -149,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", ] @@ -173,7 +208,7 @@ checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.45", ] [[package]] @@ -187,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" @@ -200,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", @@ -226,9 +283,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", @@ -241,20 +297,18 @@ 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", - "syn 2.0.39", + "syn 2.0.45", ] [[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", @@ -263,9 +317,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", @@ -274,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/Cargo.toml b/rust/uefi/Cargo.toml index 821f948e..7e65ff30 100644 --- a/rust/uefi/Cargo.toml +++ b/rust/uefi/Cargo.toml @@ -2,6 +2,7 @@ members = [ "stub", + "pio", "linux-bootloader", ] @@ -9,6 +10,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" diff --git a/rust/uefi/linux-bootloader/Cargo.toml b/rust/uefi/linux-bootloader/Cargo.toml index 3a03d54a..3904c4cb 100644 --- a/rust/uefi/linux-bootloader/Cargo.toml +++ b/rust/uefi/linux-bootloader/Cargo.toml @@ -12,13 +12,15 @@ 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" +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" ]} +embedded-io = { version = "0.6.1", default-features = false, features = [ "alloc" ] } [badges] maintenance = { status = "actively-developed" } 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/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())?; 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/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/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/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/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)?, }) 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/pio/src/packer.rs b/rust/uefi/pio/src/packer.rs new file mode 100644 index 00000000..ae5bf94c --- /dev/null +++ b/rust/uefi/pio/src/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/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!" + ); +} 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. diff --git a/rust/uefi/stub/src/fat.rs b/rust/uefi/stub/src/fat.rs index cce30d34..48402445 100644 --- a/rust/uefi/stub/src/fat.rs +++ b/rust/uefi/stub/src/fat.rs @@ -40,14 +40,18 @@ 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 // 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() @@ -63,5 +67,16 @@ pub fn boot_linux(handle: Handle, mut system_table: SystemTable) -> Status 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/main.rs b/rust/uefi/stub/src/main.rs index bc5c6dee..45148824 100644 --- a/rust/uefi/stub/src/main.rs +++ b/rust/uefi/stub/src/main.rs @@ -15,8 +15,12 @@ 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::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; @@ -46,18 +50,17 @@ 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. // TODO: in the future, devise a threat model where this can fail // and ensure this hard-fail correctly. - let _ = measure_image( - &system_table, - booted_image_file(system_table.boot_services()).unwrap(), - ); - // TODO: Measure kernel parameters - // TODO: Measure sysexts + let _ = measure_image(&system_table, &pe_in_memory); } if let Ok(features) = get_loader_features(system_table.runtime_services()) { @@ -69,15 +72,71 @@ 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(); + + { + // 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) + 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..b5e57b1a 100644 --- a/rust/uefi/stub/src/thin.rs +++ b/rust/uefi/stub/src/thin.rs @@ -1,3 +1,5 @@ +use alloc::vec; +use alloc::vec::Vec; use log::{error, warn}; use sha2::{Digest, Sha256}; use uefi::{fs::FileSystem, prelude::*, CString16, Result}; @@ -75,7 +77,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 @@ -94,7 +100,7 @@ pub fn boot_linux(handle: Handle, mut system_table: SystemTable) -> uefi:: 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 @@ -130,5 +136,36 @@ pub fn boot_linux(handle: Handle, mut system_table: SystemTable) -> uefi:: 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. + + /// 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) }