Merge pull request #166 from nix-community/sd-stub-efi-variables

feat: minimal poc for exporting UEFI variables à la sd-boot
This commit is contained in:
nikstur 2023-05-05 21:32:50 +02:00 committed by GitHub
commit ae49611bd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 324 additions and 7 deletions

View File

@ -267,4 +267,81 @@ in
f"Expected: {expected_loader_config} is not included in actual config: '{actual_loader_config}'" 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('<Q', read_raw_variable("StubFeatures")) != 0
'';
};
} }

17
rust/stub/Cargo.lock generated
View File

@ -14,6 +14,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -31,9 +37,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -83,6 +89,7 @@ dependencies = [
name = "lanzaboote_stub" name = "lanzaboote_stub"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bitflags 2.2.1",
"goblin", "goblin",
"log", "log",
"sha2", "sha2",
@ -92,9 +99,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.141" version = "0.2.142"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]] [[package]]
name = "log" name = "log"
@ -212,7 +219,7 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab39d5e7740f21ed4c46d6659f31038bbe3fe7a8be1f702d8a984348837c43b1" checksum = "ab39d5e7740f21ed4c46d6659f31038bbe3fe7a8be1f702d8a984348837c43b1"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"log", "log",
"ptr_meta", "ptr_meta",
"ucs2", "ucs2",

View File

@ -4,12 +4,13 @@ version = "0.1.0"
edition = "2021" edition = "2021"
publish = false publish = false
# For UEFI target # For UEFI target
rust_version = "1.68" rust-version = "1.68"
[dependencies] [dependencies]
uefi = { version = "0.20.0", default-features = false, features = [ "alloc", "global_allocator" ] } 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" ] } uefi-services = { version = "0.17.0", default-features = false, features = [ "panic_handler", "logger" ] }
goblin = { version = "0.6.1", default-features = false, features = [ "pe64", "alloc" ]} 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. # 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" ]} log = { version = "0.4.17", default-features = false, features = [ "max_level_info", "release_max_level_warn" ]}

221
rust/stub/src/efivars.rs Normal file
View File

@ -0,0 +1,221 @@
use alloc::{vec::Vec, vec, string::ToString, format};
use uefi::{Result, Handle, prelude::{BootServices, RuntimeServices}, proto::{device_path::{DevicePath, media::{HardDrive, PartitionSignature}, DeviceType, DeviceSubType, text::DevicePathToText}, loaded_image::LoadedImage}, Guid, guid, table::{runtime::{VariableVendor, VariableAttributes}, SystemTable, Boot}, cstr16, CStr16};
use bitflags::bitflags;
fn disk_get_part_uuid(boot_services: &BootServices, disk_handle: Handle) -> Result<Guid> {
let dp = boot_services.open_protocol_exclusive::<DevicePath>(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<EfiLoaderFeatures> {
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<F>(runtime_services: &RuntimeServices,
name: &CStr16,
vendor: &VariableVendor,
attributes: VariableAttributes,
get_fallback_value: F) -> uefi::Result
where F: FnOnce() -> uefi::Result<Vec<u8>>
{
// 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<Boot>) -> 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::<LoadedImage>(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::<Vec<u8>>()
)
).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::<DevicePathToText>(
boot_services.get_handle_for_protocol::<DevicePathToText>()?
)?;
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::<Vec<u8>>()
)
).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::<Vec<u8>>()
)
).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::<Vec<u8>>()
).ok();
// StubFeatures
runtime_services.set_variable(
cstr16!("StubFeatures"),
&BOOT_LOADER_VENDOR_UUID,
default_attributes,
&stub_features.bits().to_le_bytes()
).ok();
Ok(())
}

View File

@ -8,9 +8,10 @@ mod linux_loader;
mod pe_loader; mod pe_loader;
mod pe_section; mod pe_section;
mod uefi_helpers; mod uefi_helpers;
mod efivars;
use alloc::vec::Vec; use alloc::vec::Vec;
use log::{info, warn}; use log::{info, warn, debug};
use pe_loader::Image; use pe_loader::Image;
use pe_section::{pe_section, pe_section_as_string}; use pe_section::{pe_section, pe_section_as_string};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@ -22,6 +23,7 @@ use uefi::{
}, },
CStr16, CString16, Result, CStr16, CString16, Result,
}; };
use efivars::{EfiLoaderFeatures, export_efi_variables, get_loader_features};
use crate::{ use crate::{
linux_loader::InitrdLoader, linux_loader::InitrdLoader,
@ -236,6 +238,15 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
warn!("Hash mismatch for initrd!"); 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 { if is_kernel_hash_correct && is_initrd_hash_correct {
boot_linux_unchecked( boot_linux_unchecked(
handle, handle,