stub: make handling of insecure boot more explicit

When Secure Boot is not available (unsupported or disabled), Lanzaboote
will attempt to boot kernels and initrds even when they fail the hash
verification. Previously, this would happen by falling back to use
LoadImage on the kernel, which fails if Secure Boot is available, as the
kernel is not signed.
The SecureBoot variable offers a more explicit way of checking whether
Secure Boot is available. If the firmware supports Secure Boot, it
initializes this variable to 1 if it is enabled, and to 0 if it is
disabled. Applications are not supposed to modify this variable, and in
particular, since only trusted applications are loaded when Secure Boot
is active, we can assume it is never changed to 0 or deleted if Secure
Boot is active.
Hence, we can be sure of Secure Boot being inactive if this variable is
absent or set to 0, and thus treat all hash verification errors as
non-fatal and proceed to boot arbitrary kernels and initrds (a warning
is still logged in this case). In all other cases, we treat all hash
verification failures as fatal security violations, as it must be done
in the case where Secure Boot is active (it is expected that this does
not lead to any false positives in practice, unless there are bigger
problems anyway).
This commit is contained in:
Alois Wohlschlager 2023-10-01 19:21:11 +02:00
parent 62ffd894f0
commit db39223a7c
No known key found for this signature in database
GPG Key ID: E0F59EA5E5216914
3 changed files with 73 additions and 102 deletions

View File

@ -144,7 +144,7 @@ let
machine.crash() machine.crash()
machine.start() machine.start()
'' + (if useSecureBoot then '' '' + (if useSecureBoot then ''
machine.wait_for_console_text("Hash mismatch") machine.wait_for_console_text("hash does not match")
'' else '' '' else ''
# Just check that the system came up. # Just check that the system came up.
print(machine.succeed("bootctl", timeout=120)) print(machine.succeed("bootctl", timeout=120))

View File

@ -77,7 +77,7 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
#[cfg(feature = "thin")] #[cfg(feature = "thin")]
{ {
status = thin::boot_linux(handle, system_table) status = thin::boot_linux(handle, system_table).status()
} }
status status

View File

@ -1,12 +1,10 @@
use alloc::vec::Vec; use log::{error, warn};
use log::warn;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use uefi::fs::FileSystem; use uefi::{fs::FileSystem, guid, prelude::*, table::runtime::VariableVendor, CString16, Result};
use uefi::{prelude::*, proto::loaded_image::LoadedImage, CStr16, CString16, Result};
use crate::common::{boot_linux_unchecked, extract_string}; use crate::common::{boot_linux_unchecked, extract_string};
use linux_bootloader::pe_section::pe_section; use linux_bootloader::pe_section::pe_section;
use linux_bootloader::{linux_loader::InitrdLoader, uefi_helpers::booted_image_file}; use linux_bootloader::uefi_helpers::booted_image_file;
type Hash = sha2::digest::Output<Sha256>; type Hash = sha2::digest::Output<Sha256>;
@ -59,53 +57,25 @@ impl EmbeddedConfiguration {
} }
} }
/// Boot the Linux kernel via the UEFI PE loader. /// Verify some data against its expected hash.
/// ///
/// This should only succeed when UEFI Secure Boot is off (or /// In case of a mismatch:
/// broken...), because the Lanzaboote tool does not sign the kernel. /// * If Secure Boot is active, an error message is logged, and the SECURITY_VIOLATION error is returned to stop the boot.
/// /// * If Secure Boot is not active, only a warning is logged, and the boot process is allowed to continue.
/// In essence, we can use this routine to detect whether Secure Boot fn check_hash(data: &[u8], expected_hash: Hash, name: &str, secure_boot: bool) -> uefi::Result<()> {
/// is actually enabled. let hash_correct = Sha256::digest(data) == expected_hash;
fn boot_linux_uefi( if !hash_correct {
handle: Handle, if secure_boot {
system_table: SystemTable<Boot>, error!("{name} hash does not match!");
kernel_data: Vec<u8>, return Err(Status::SECURITY_VIOLATION.into());
kernel_cmdline: &CStr16, } else {
initrd_data: Vec<u8>, warn!("{name} hash does not match! Continuing anyway.");
) -> uefi::Result<()> { }
let kernel_handle = system_table.boot_services().load_image( }
handle, Ok(())
uefi::table::boot::LoadImageSource::FromBuffer {
buffer: &kernel_data,
file_path: None,
},
)?;
let mut kernel_image = system_table
.boot_services()
.open_protocol_exclusive::<LoadedImage>(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)?; pub fn boot_linux(handle: Handle, mut system_table: SystemTable<Boot>) -> uefi::Result<()> {
let status = system_table
.boot_services()
.start_image(kernel_handle)
.status();
initrd_loader.uninstall(system_table.boot_services())?;
status.to_result()
}
pub fn boot_linux(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
uefi_services::init(&mut system_table).unwrap(); uefi_services::init(&mut system_table).unwrap();
// SAFETY: We get a slice that represents our currently running // SAFETY: We get a slice that represents our currently running
@ -121,6 +91,40 @@ pub fn boot_linux(handle: Handle, mut system_table: SystemTable<Boot>) -> Status
.expect("Failed to extract configuration from binary. Did you run lzbt?") .expect("Failed to extract configuration from binary. Did you run lzbt?")
}; };
// The firmware initialized SecureBoot to 1 if performing signature checks, and 0 if it doesn't.
// Applications are not supposed to modify this variable (in particular, don't change the value from 1 to 0).
let secure_boot_enabled = system_table
.runtime_services()
.get_variable(
cstr16!("SecureBoot"),
&VariableVendor(guid!("8be4df61-93ca-11d2-aa0d-00e098032b8c")),
&mut [1],
)
.and_then(|(value, _)| match value {
[0] => Ok(false),
[1] => Ok(true),
[v] => {
warn!(
"Unexpected value of SecureBoot variable: {v}. Performing verification anyway."
);
Ok(true)
}
_ => Err(Status::BAD_BUFFER_SIZE.into()),
})
.unwrap_or_else(|err| {
if err.status() == Status::NOT_FOUND {
warn!("SecureBoot variable not found. Assuming Secure Boot is not supported.");
false
} else {
warn!("Failed to read SecureBoot variable: {err}. Performing verification anyway.");
true
}
});
if !secure_boot_enabled {
warn!("Secure Boot is not active!");
}
let kernel_data; let kernel_data;
let initrd_data; let initrd_data;
@ -139,18 +143,19 @@ pub fn boot_linux(handle: Handle, mut system_table: SystemTable<Boot>) -> Status
.expect("Failed to read initrd file into memory"); .expect("Failed to read initrd file into memory");
} }
let is_kernel_hash_correct = Sha256::digest(&kernel_data) == config.kernel_hash; check_hash(
let is_initrd_hash_correct = Sha256::digest(&initrd_data) == config.initrd_hash; &kernel_data,
config.kernel_hash,
"Kernel",
secure_boot_enabled,
)?;
check_hash(
&initrd_data,
config.initrd_hash,
"Initrd",
secure_boot_enabled,
)?;
if !is_kernel_hash_correct {
warn!("Hash mismatch for kernel!");
}
if !is_initrd_hash_correct {
warn!("Hash mismatch for initrd!");
}
if is_kernel_hash_correct && is_initrd_hash_correct {
boot_linux_unchecked( boot_linux_unchecked(
handle, handle,
system_table, system_table,
@ -158,38 +163,4 @@ pub fn boot_linux(handle: Handle, mut system_table: SystemTable<Boot>) -> Status
&config.cmdline, &config.cmdline,
initrd_data, 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()
}
} }