Merge pull request #91 from nix-community/58-escape-hatch
Ignore Integrity Checks without Secure Boot
This commit is contained in:
commit
37ccc5d578
|
@ -6,7 +6,7 @@
|
|||
let
|
||||
inherit (pkgs) lib;
|
||||
|
||||
mkSecureBootTest = { name, machine ? { }, testScript }: testPkgs.nixosTest {
|
||||
mkSecureBootTest = { name, machine ? { }, useSecureBoot ? true, testScript }: testPkgs.nixosTest {
|
||||
inherit name testScript;
|
||||
nodes.machine = { lib, ... }: {
|
||||
imports = [
|
||||
|
@ -17,7 +17,8 @@ let
|
|||
virtualisation = {
|
||||
useBootLoader = true;
|
||||
useEFIBoot = true;
|
||||
useSecureBoot = true;
|
||||
|
||||
inherit useSecureBoot;
|
||||
};
|
||||
|
||||
boot.loader.efi = {
|
||||
|
@ -31,14 +32,18 @@ let
|
|||
};
|
||||
};
|
||||
|
||||
# Execute a SB test that is expected to fail because of a hash mismatch.
|
||||
# Execute a boot test that has an intentionally broken secure boot
|
||||
# chain. This test is expected to fail with Secure Boot and should
|
||||
# succeed without.
|
||||
#
|
||||
# Takes a set `path` consisting of a `src` and a `dst` attribute. The file at
|
||||
# `src` is copied to `dst` inside th VM. Optionally append some random data
|
||||
# ("crap") to the end of the file at `dst`. This is useful to easily change
|
||||
# the hash of a file and produce a hash mismatch when booting the stub.
|
||||
mkHashMismatchTest = { name, path, appendCrap ? false }: mkSecureBootTest {
|
||||
mkHashMismatchTest = { name, path, appendCrap ? false, useSecureBoot ? true }: mkSecureBootTest {
|
||||
inherit name;
|
||||
inherit useSecureBoot;
|
||||
|
||||
testScript = ''
|
||||
import json
|
||||
import os.path
|
||||
|
@ -62,9 +67,40 @@ let
|
|||
machine.succeed("sync")
|
||||
machine.crash()
|
||||
machine.start()
|
||||
'' + (if useSecureBoot then ''
|
||||
machine.wait_for_console_text("Hash mismatch")
|
||||
'';
|
||||
'' else ''
|
||||
# Just check that the system came up.
|
||||
print(machine.succeed("bootctl", timeout=120))
|
||||
'');
|
||||
};
|
||||
|
||||
# The initrd is not directly signed. Its hash is embedded into
|
||||
# lanzaboote. To make integrity verification fail, we actually have
|
||||
# to modify the initrd. Appending crap to the end is a harmless way
|
||||
# that would make the kernel still accept it.
|
||||
mkModifiedInitrdTest = { name, useSecureBoot }: mkHashMismatchTest {
|
||||
inherit name useSecureBoot;
|
||||
|
||||
path = {
|
||||
src = "bootspec.get('initrd')";
|
||||
dst = "convert_to_esp(bootspec.get('initrd'))";
|
||||
};
|
||||
|
||||
appendCrap = true;
|
||||
};
|
||||
|
||||
mkModifiedKernelTest = { name, useSecureBoot }: mkHashMismatchTest {
|
||||
inherit name useSecureBoot;
|
||||
|
||||
path = {
|
||||
src = "bootspec.get('kernel')";
|
||||
dst = "convert_to_esp(bootspec.get('kernel'))";
|
||||
};
|
||||
|
||||
appendCrap = true;
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
# TODO: user mode: OK
|
||||
|
@ -109,31 +145,28 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
# The initrd is not directly signed. Its hash is embedded
|
||||
# into the UKI. To make integrity verification fail, we
|
||||
# actually have to modify the initrd. Appending crap to the
|
||||
# end is a harmless way that would make the kernel still
|
||||
# accept it.
|
||||
secured-initrd = mkHashMismatchTest {
|
||||
name = "lanzaboote-secured-initrd";
|
||||
path = {
|
||||
src = "bootspec.get('initrd')";
|
||||
dst = "convert_to_esp(bootspec.get('initrd'))";
|
||||
};
|
||||
appendCrap = true;
|
||||
modified-initrd-doesnt-boot-with-secure-boot = mkModifiedInitrdTest {
|
||||
name = "modified-initrd-doesnt-boot-with-secure-boot";
|
||||
useSecureBoot = true;
|
||||
};
|
||||
|
||||
secured-kernel = mkHashMismatchTest {
|
||||
name = "lanzaboote-secured-kernel";
|
||||
path = {
|
||||
src = "bootspec.get('kernel')";
|
||||
dst = "convert_to_esp(bootspec.get('kernel'))";
|
||||
};
|
||||
appendCrap = true;
|
||||
modified-initrd-boots-without-secure-boot = mkModifiedInitrdTest {
|
||||
name = "modified-initrd-boots-without-secure-boot";
|
||||
useSecureBoot = false;
|
||||
};
|
||||
|
||||
specialisation = mkSecureBootTest {
|
||||
name = "lanzaboote-specialisation";
|
||||
modified-kernel-doesnt-boot-with-secure-boot = mkModifiedKernelTest {
|
||||
name = "modified-kernel-doesnt-boot-with-secure-boot";
|
||||
useSecureBoot = true;
|
||||
};
|
||||
|
||||
modified-kernel-boots-without-secure-boot = mkModifiedKernelTest {
|
||||
name = "modified-kernel-boots-without-secure-boot";
|
||||
useSecureBoot = false;
|
||||
};
|
||||
|
||||
specialisation-works = mkSecureBootTest {
|
||||
name = "specialisation-still-boot-under-secureboot";
|
||||
machine = { pkgs, ... }: {
|
||||
specialisation.variant.configuration = {
|
||||
environment.systemPackages = [
|
||||
|
|
|
@ -11,6 +11,7 @@ mod pe_loader;
|
|||
mod pe_section;
|
||||
mod uefi_helpers;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use pe_loader::Image;
|
||||
use pe_section::{pe_section, pe_section_as_string};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
@ -18,9 +19,10 @@ use uefi::{
|
|||
prelude::*,
|
||||
proto::{
|
||||
console::text::Output,
|
||||
loaded_image::LoadedImage,
|
||||
media::file::{File, FileAttribute, FileMode, RegularFile},
|
||||
},
|
||||
CString16, Result,
|
||||
CStr16, CString16, Result,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
@ -106,6 +108,74 @@ impl EmbeddedConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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<Boot>,
|
||||
kernel_data: Vec<u8>,
|
||||
kernel_cmdline: &CStr16,
|
||||
initrd_data: Vec<u8>,
|
||||
) -> 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<Boot>,
|
||||
kernel_data: Vec<u8>,
|
||||
kernel_cmdline: &CStr16,
|
||||
initrd_data: Vec<u8>,
|
||||
) -> 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::<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)?;
|
||||
|
||||
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<Boot>) -> Status {
|
||||
uefi_services::init(&mut system_table).unwrap();
|
||||
|
@ -153,32 +223,66 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
|
|||
initrd_data = read_all(&mut initrd_file).expect("Failed to read kernel file into memory");
|
||||
}
|
||||
|
||||
if Sha256::digest(&kernel_data) != config.kernel_hash {
|
||||
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 {
|
||||
system_table
|
||||
.stdout()
|
||||
.output_string(cstr16!("Hash mismatch for kernel. Refusing to load!\r\n"))
|
||||
.output_string(cstr16!("Hash mismatch for kernel!\r\n"))
|
||||
.unwrap();
|
||||
return Status::SECURITY_VIOLATION;
|
||||
}
|
||||
|
||||
if Sha256::digest(&initrd_data) != config.initrd_hash {
|
||||
if !is_initrd_hash_correct {
|
||||
system_table
|
||||
.stdout()
|
||||
.output_string(cstr16!("Hash mismatch for initrd. Refusing to load!\r\n"))
|
||||
.output_string(cstr16!("Hash mismatch for initrd!\r\n"))
|
||||
.unwrap();
|
||||
return Status::SECURITY_VIOLATION;
|
||||
}
|
||||
|
||||
let kernel =
|
||||
Image::load(system_table.boot_services(), &kernel_data).expect("Failed to load the kernel");
|
||||
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.
|
||||
|
||||
let mut initrd_loader = InitrdLoader::new(system_table.boot_services(), handle, initrd_data)
|
||||
.expect("Failed to load the initrd. It may not be there or it is not signed");
|
||||
system_table
|
||||
.stdout()
|
||||
.output_string(cstr16!("WARNING: Trying to continue as non-Secure Boot. This will fail when Secure Boot is enabled.\r\n"))
|
||||
.unwrap();
|
||||
|
||||
let status = unsafe { kernel.start(handle, &system_table, &config.cmdline) };
|
||||
|
||||
initrd_loader
|
||||
.uninstall(system_table.boot_services())
|
||||
.expect("Failed to uninstall the initrd protocols");
|
||||
status
|
||||
boot_linux_uefi(
|
||||
handle,
|
||||
system_table,
|
||||
kernel_data,
|
||||
&config.cmdline,
|
||||
initrd_data,
|
||||
)
|
||||
.status()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue