From 9dd9116b1ec88f834483de6b95f499dee25f0fea Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Sat, 29 Apr 2023 03:51:39 +0200 Subject: [PATCH] stub: export boot loader interface efivars --- nix/tests/lanzaboote.nix | 77 ++++++++++++++ rust/stub/Cargo.lock | 17 ++- rust/stub/Cargo.toml | 1 + rust/stub/src/efivars.rs | 221 +++++++++++++++++++++++++++++++++++++++ rust/stub/src/main.rs | 13 ++- 5 files changed, 323 insertions(+), 6 deletions(-) create mode 100644 rust/stub/src/efivars.rs diff --git a/nix/tests/lanzaboote.nix b/nix/tests/lanzaboote.nix index aa34087..a0f9f4a 100644 --- a/nix/tests/lanzaboote.nix +++ b/nix/tests/lanzaboote.nix @@ -267,4 +267,81 @@ in f"Expected: {expected_loader_config} is not included in actual config: '{actual_loader_config}'" ''; }; + + export-efi-variables = mkSecureBootTest { + name = "lanzaboote-exports-efi-variables"; + machine.environment.systemPackages = [ pkgs.efibootmgr ]; + testScript = '' + import struct + + # We will choose to boot directly on the stub. + # To perform this trick, we will boot first with systemd-boot. + # Then, we will add a new boot entry in EFI with higher priority + # pointing to our stub. + # Finally, we will reboot. + # We will also assert that systemd-boot is not running + # by checking for the sd-boot's specific EFI variables. + machine.start() + + # By construction, nixos-generation-1.efi is the stub we are interested in. + # TODO: this should work -- machine.succeed("efibootmgr -d /dev/vda -c -l \\EFI\\Linux\\nixos-generation-1.efi") -- efivars are not persisted + # across reboots atm? + # cheat code no 1 + machine.succeed("cp /boot/EFI/Linux/nixos-generation-1.efi /boot/EFI/BOOT/BOOTX64.EFI") + machine.succeed("cp /boot/EFI/Linux/nixos-generation-1.efi /boot/EFI/systemd/systemd-bootx64.efi") + + # Let's reboot. + machine.succeed("sync") + machine.crash() + machine.start() + + # This is the sd-boot EFI variable indicator, we should not have it at this point. + print(machine.execute("bootctl")[1]) # Check if there's incorrect value in the output. + machine.succeed( + "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f && false || true" + ) + + SD_LOADER_GUID = "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" + expected_variables = ["LoaderDevicePartUUID", + "LoaderImageIdentifier", + "LoaderFirmwareInfo", + "LoaderFirmwareType", + "StubInfo", + "StubFeatures" + ] + + def read_raw_variable(var: str) -> bytes: + attr_var = machine.succeed(f"cat /sys/firmware/efi/efivars/{var}-{SD_LOADER_GUID}").encode('raw_unicode_escape') + return attr_var[4:] # First 4 bytes are attributes according to https://www.kernel.org/doc/html/latest/filesystems/efivarfs.html + def read_string_variable(var: str, encoding='utf-16-le') -> str: + return read_raw_variable(var).decode(encoding).rstrip('\x00') + def assert_variable_string(var: str, expected: str, encoding='utf-16-le'): + with subtest(f"Is `{var}` correctly set"): + value = read_string_variable(var, encoding) + assert value == expected, f"Unexpected variable value in `{var}`, expected: `{expected.encode(encoding)!r}`, actual: `{value.encode(encoding)!r}`" + def assert_variable_string_contains(var: str, expected_substring: str): + with subtest(f"Do `{var}` contain expected substrings"): + value = read_string_variable(var).strip() + assert expected_substring in value, f"Did not find expected substring in `{var}`, expected substring: `{expected_substring}`, actual value: `{value}`" + + # Debug all systemd loader specification GUID EFI variables loaded by the current environment. + print(machine.succeed(f"ls /sys/firmware/efi/efivars/*-{SD_LOADER_GUID}")) + with subtest("Check if supported variables are exported"): + for expected_var in expected_variables: + machine.succeed(f"test -e /sys/firmware/efi/efivars/{expected_var}-{SD_LOADER_GUID}") + + with subtest("Is `StubInfo` correctly set"): + assert "lanzastub" in read_string_variable("StubInfo"), "Unexpected stub information, provenance is not lanzaboote project!" + + assert_variable_string("LoaderImageIdentifier", "\\EFI\\BOOT\\BOOTX64.EFI") + # TODO: exploit QEMU test infrastructure to pass the good value all the time. + assert_variable_string("LoaderDevicePartUUID", "1c06f03b-704e-4657-b9cd-681a087a2fdc") + # OVMF tests are using EDK II tree. + assert_variable_string_contains("LoaderFirmwareInfo", "EDK II") + assert_variable_string_contains("LoaderFirmwareType", "UEFI") + + with subtest("Is `StubFeatures` non-zero"): + assert struct.unpack(' Result { + let dp = boot_services.open_protocol_exclusive::(disk_handle)?; + + for node in dp.node_iter() { + if node.device_type() != DeviceType::MEDIA || node.sub_type() != DeviceSubType::MEDIA_HARD_DRIVE { + continue; + } + + if let Ok(hd_path) = <&HardDrive>::try_from(node) { + if let PartitionSignature::Guid(guid) = hd_path.partition_signature() { + return Ok(guid); + } + } + } + + Err(uefi::Status::UNSUPPORTED.into()) +} + + +/// systemd loader's GUID +/// != systemd's GUID +/// https://github.com/systemd/systemd/blob/main/src/boot/efi/util.h#L114-L121 +/// https://systemd.io/BOOT_LOADER_INTERFACE/ +pub const BOOT_LOADER_VENDOR_UUID: VariableVendor = VariableVendor(guid!("4a67b082-0a4c-41cf-b6c7-440b29bb8c4f")); + +/// Lanzaboote stub name +pub static STUB_INFO_STRING: &str = concat!("lanzastub ", env!("CARGO_PKG_VERSION")); + +bitflags! { + + #[repr(transparent)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] + /// Feature flags as described in https://systemd.io/BOOT_LOADER_INTERFACE/ + pub struct EfiLoaderFeatures: u64 { + const ConfigTimeout = 1 << 0; + const ConfigTimeoutOneShot = 1 << 1; + const EntryDefault = 1 << 2; + const EntryOneshot = 1 << 3; + const BootCounting = 1 << 4; + const XBOOTLDR = 1 << 5; + const RandomSeed = 1 << 6; + const LoadDriver = 1 << 7; + const SortKey = 1 << 8; + const SavedEntry = 1 << 9; + const DeviceTree = 1 << 10; + } +} + +/// Get the currently supported EFI features from the loader if they do exist +/// https://systemd.io/BOOT_LOADER_INTERFACE/ +pub fn get_loader_features(runtime_services: &RuntimeServices) -> Result { + if let Ok(size) = runtime_services.get_variable_size(cstr16!("LoaderFeatures"), &BOOT_LOADER_VENDOR_UUID) { + let mut buffer = vec![0; size].into_boxed_slice(); + runtime_services.get_variable( + cstr16!("LoaderFeatures"), + &BOOT_LOADER_VENDOR_UUID, + &mut buffer)?; + + return EfiLoaderFeatures::from_bits( + u64::from_le_bytes( + (*buffer).try_into() + .map_err(|_err| uefi::Status::BAD_BUFFER_SIZE)? + )) + .ok_or_else(|| uefi::Status::INCOMPATIBLE_VERSION.into()); + } + + Ok(Default::default()) +} + +bitflags! { + #[repr(transparent)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + /// Feature flags as described in https://www.freedesktop.org/software/systemd/man/systemd-stub.html + pub struct EfiStubFeatures: u64 { + /// Is `LoaderDevicePartUUID` loaded in UEFI variables? + const ReportBootPartition = 1 << 0; + /// Are credentials picked up from the boot partition? + const PickUpCredentials = 1 << 1; + /// Are system extensions picked up from the boot partition? + const PickUpSysExts = 1 << 2; + /// Are we able to measure kernel image, parameters and sysexts? + const ThreePcrs = 1 << 3; + /// Can we pass a random seed to the kernel? + const RandomSeed = 1 << 4; + } +} + +// This won't work on a big endian system. +// But okay, we do not really care, do we? +#[cfg(target_endian = "little")] +pub fn from_u16(from: &[u16]) -> &[u8] { + unsafe { + core::slice::from_raw_parts(from.as_ptr() as *mut u8, from.len().checked_mul(2).unwrap()) + } +} + +// Remove me when https://github.com/rust-osdev/uefi-rs/pull/788 lands +pub fn cstr16_to_bytes(s: &CStr16) -> &[u8] { + from_u16(s.to_u16_slice_with_nul()) +} + +/// Ensures that an UEFI variable is set or set it with a fallback value +/// computed in a lazy way. +pub fn ensure_efi_variable(runtime_services: &RuntimeServices, + name: &CStr16, + vendor: &VariableVendor, + attributes: VariableAttributes, + get_fallback_value: F) -> uefi::Result + where F: FnOnce() -> uefi::Result> +{ + // If we get a variable size, a variable already exist. + if runtime_services.get_variable_size(name, vendor).is_err() { + runtime_services.set_variable( + name, + vendor, + attributes, + &get_fallback_value()? + )?; + } + + uefi::Status::SUCCESS.into() +} + +/// Exports systemd-stub style EFI variables +pub fn export_efi_variables(system_table: &SystemTable) -> Result<()> { + let boot_services = system_table.boot_services(); + let runtime_services = system_table.runtime_services(); + + let stub_features: EfiStubFeatures = + EfiStubFeatures::ReportBootPartition; + + let loaded_image = + boot_services.open_protocol_exclusive::(boot_services.image_handle())?; + + let default_attributes = VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS; + + #[allow(unused_must_use)] + // LoaderDevicePartUUID + ensure_efi_variable(runtime_services, + cstr16!("LoaderDevicePartUUID"), + &BOOT_LOADER_VENDOR_UUID, + default_attributes, + || disk_get_part_uuid(boot_services, loaded_image.device()).map(|guid| guid.to_string() + .encode_utf16() + .flat_map(|c| c.to_le_bytes()) + .collect::>() + ) + ).ok(); + // LoaderImageIdentifier + ensure_efi_variable(runtime_services, + cstr16!("LoaderImageIdentifier"), + &BOOT_LOADER_VENDOR_UUID, + default_attributes, + || { + if let Some(dp) = loaded_image.file_path() { + let dp_protocol = boot_services.open_protocol_exclusive::( + boot_services.get_handle_for_protocol::()? + )?; + dp_protocol.convert_device_path_to_text( + boot_services, + dp, + uefi::proto::device_path::text::DisplayOnly(false), + uefi::proto::device_path::text::AllowShortcuts(false) + ).map(|ps| cstr16_to_bytes(&ps).to_vec()) + } else { + // If we cannot retrieve the filepath of the loaded image + // Then, we cannot set `LoaderImageIdentifier`. + Err(uefi::Status::UNSUPPORTED.into()) + } + }).ok(); + // LoaderFirmwareInfo + ensure_efi_variable(runtime_services, + cstr16!("LoaderFirmwareInfo"), + &BOOT_LOADER_VENDOR_UUID, + default_attributes, + || Ok( + format!("{} {}.{:02}", system_table.firmware_vendor(), system_table.firmware_revision() >> 16, system_table.firmware_revision() & 0xFFFFF) + .encode_utf16() + .flat_map(|c| c.to_le_bytes()) + .collect::>() + ) + ).ok(); + // LoaderFirmwareType + ensure_efi_variable(runtime_services, + cstr16!("LoaderFirmwareType"), + &BOOT_LOADER_VENDOR_UUID, + default_attributes, + || Ok( + format!("UEFI {:02}", system_table.uefi_revision()) + .encode_utf16() + .flat_map(|c| c.to_le_bytes()) + .collect::>() + ) + ).ok(); + // StubInfo + // FIXME: ideally, no one should be able to overwrite `StubInfo`, but that would require + // constructing an EFI authenticated variable payload. This seems overcomplicated for now. + runtime_services.set_variable( + cstr16!("StubInfo"), + &BOOT_LOADER_VENDOR_UUID, + default_attributes, + &STUB_INFO_STRING.encode_utf16().flat_map(|c| c.to_le_bytes()).collect::>() + ).ok(); + + // StubFeatures + runtime_services.set_variable( + cstr16!("StubFeatures"), + &BOOT_LOADER_VENDOR_UUID, + default_attributes, + &stub_features.bits().to_le_bytes() + ).ok(); + + Ok(()) +} + + diff --git a/rust/stub/src/main.rs b/rust/stub/src/main.rs index 856acc2..8588417 100644 --- a/rust/stub/src/main.rs +++ b/rust/stub/src/main.rs @@ -8,9 +8,10 @@ mod linux_loader; mod pe_loader; mod pe_section; mod uefi_helpers; +mod efivars; use alloc::vec::Vec; -use log::{info, warn}; +use log::{info, warn, debug}; use pe_loader::Image; use pe_section::{pe_section, pe_section_as_string}; use sha2::{Digest, Sha256}; @@ -22,6 +23,7 @@ use uefi::{ }, CStr16, CString16, Result, }; +use efivars::{EfiLoaderFeatures, export_efi_variables, get_loader_features}; use crate::{ linux_loader::InitrdLoader, @@ -236,6 +238,15 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { warn!("Hash mismatch for initrd!"); } + if let Ok(features) = get_loader_features(system_table.runtime_services()) { + if !features.contains(EfiLoaderFeatures::RandomSeed) { + // FIXME: process random seed then on the disk. + debug!("Random seed is available, but lanzaboote does not support it yet."); + } + } + export_efi_variables(&system_table) + .expect("Failed to export stub EFI variables"); + if is_kernel_hash_correct && is_initrd_hash_correct { boot_linux_unchecked( handle,