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:
commit
ae49611bd6
|
@ -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('<Q', read_raw_variable("StubFeatures")) != 0
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,6 +14,12 @@ version = "1.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
|
@ -31,9 +37,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181"
|
||||
checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -83,6 +89,7 @@ dependencies = [
|
|||
name = "lanzaboote_stub"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags 2.2.1",
|
||||
"goblin",
|
||||
"log",
|
||||
"sha2",
|
||||
|
@ -92,9 +99,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.141"
|
||||
version = "0.2.142"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
|
||||
checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
|
@ -212,7 +219,7 @@ version = "0.20.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab39d5e7740f21ed4c46d6659f31038bbe3fe7a8be1f702d8a984348837c43b1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"log",
|
||||
"ptr_meta",
|
||||
"ucs2",
|
||||
|
|
|
@ -4,12 +4,13 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
publish = false
|
||||
# For UEFI target
|
||||
rust_version = "1.68"
|
||||
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" ]}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
@ -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<Boot>) -> 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,
|
||||
|
|
Loading…
Reference in New Issue