Merge pull request #182 from nix-community/fat-uki
stub: add fat variant
This commit is contained in:
commit
da24357977
|
@ -87,6 +87,13 @@ on the ESP. The chain of trust is maintained by validating the
|
|||
signature on the Linux kernel and embedding a cryptographic hash of
|
||||
the initrd into the signed UKI.
|
||||
|
||||
The stub is available in a "thin" and a "fat" variant. The "thin" variant is
|
||||
the one described above and is tailor made for NixOS. The "fat" variant aims to
|
||||
work exactly like the `systemd-stub`---in fact, it's supposed to eventually
|
||||
replace it. The "thin" variant is the default, you can build it from the stub
|
||||
directory with `cargo build`. The "fat" variant needs to be enabled at build
|
||||
time with `cargo build --no-default-features --features fat`.
|
||||
|
||||
The stub lives in `rust/stub`.
|
||||
|
||||
### Fwupd
|
||||
|
|
69
flake.nix
69
flake.nix
|
@ -59,6 +59,17 @@
|
|||
}
|
||||
);
|
||||
|
||||
flake.nixosModules.uki = moduleWithSystem (
|
||||
perSystem@{ config }:
|
||||
{ lib, ... }: {
|
||||
imports = [
|
||||
./nix/modules/uki.nix
|
||||
];
|
||||
|
||||
boot.loader.uki.stub = lib.mkDefault "${perSystem.config.packages.fatStub}/bin/lanzaboote_stub.efi";
|
||||
}
|
||||
);
|
||||
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
|
||||
|
@ -81,7 +92,7 @@
|
|||
craneLib = crane.lib.x86_64-linux.overrideToolchain uefi-rust-stable;
|
||||
|
||||
# Build attributes for a Rust application.
|
||||
buildRustApp =
|
||||
buildRustApp = lib.makeOverridable (
|
||||
{ src
|
||||
, target ? null
|
||||
, doCheck ? true
|
||||
|
@ -126,7 +137,8 @@
|
|||
});
|
||||
|
||||
rustfmt = craneLib.cargoFmt (commonArgs // { inherit cargoArtifacts; });
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
stubCrane = buildRustApp {
|
||||
src = craneLib.cleanCargoSource ./rust/stub;
|
||||
|
@ -134,7 +146,14 @@
|
|||
doCheck = false;
|
||||
};
|
||||
|
||||
fatStubCrane = stubCrane.override {
|
||||
extraArgs = {
|
||||
cargoExtraArgs = "--no-default-features --features fat";
|
||||
};
|
||||
};
|
||||
|
||||
stub = stubCrane.package;
|
||||
fatStub = fatStubCrane.package;
|
||||
|
||||
toolCrane = buildRustApp {
|
||||
src = ./rust/tool;
|
||||
|
@ -164,7 +183,7 @@
|
|||
in
|
||||
{
|
||||
packages = {
|
||||
inherit stub;
|
||||
inherit stub fatStub;
|
||||
tool = wrappedTool;
|
||||
lzbt = wrappedTool;
|
||||
};
|
||||
|
@ -173,15 +192,27 @@
|
|||
inherit (config.packages) tool;
|
||||
};
|
||||
|
||||
checks = {
|
||||
toolClippy = toolCrane.clippy;
|
||||
stubClippy = stubCrane.clippy;
|
||||
toolFmt = toolCrane.rustfmt;
|
||||
stubFmt = stubCrane.rustfmt;
|
||||
} // (import ./nix/tests/lanzaboote.nix {
|
||||
inherit pkgs;
|
||||
lanzabooteModule = self.nixosModules.lanzaboote;
|
||||
});
|
||||
checks =
|
||||
let
|
||||
nixosLib = import (pkgs.path + "/nixos/lib") { };
|
||||
runTest = module: nixosLib.runTest {
|
||||
imports = [ module ];
|
||||
hostPkgs = pkgs;
|
||||
};
|
||||
in
|
||||
{
|
||||
toolClippy = toolCrane.clippy;
|
||||
stubClippy = stubCrane.clippy;
|
||||
fatStubClippy = fatStubCrane.clippy;
|
||||
toolFmt = toolCrane.rustfmt;
|
||||
stubFmt = stubCrane.rustfmt;
|
||||
} // (import ./nix/tests/lanzaboote.nix {
|
||||
inherit pkgs;
|
||||
lanzabooteModule = self.nixosModules.lanzaboote;
|
||||
}) // (import ./nix/tests/stub.nix {
|
||||
inherit pkgs runTest;
|
||||
ukiModule = self.nixosModules.uki;
|
||||
});
|
||||
|
||||
pre-commit = {
|
||||
check.enable = true;
|
||||
|
@ -193,9 +224,17 @@
|
|||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
shellHook = ''
|
||||
${config.pre-commit.installationScript}
|
||||
'';
|
||||
shellHook =
|
||||
let
|
||||
systemdUkify = pkgs.systemdMinimal.override {
|
||||
withEfi = true;
|
||||
withUkify = true;
|
||||
};
|
||||
in
|
||||
''
|
||||
${config.pre-commit.installationScript}
|
||||
export PATH=$PATH:${systemdUkify}/lib/systemd
|
||||
'';
|
||||
|
||||
packages =
|
||||
let
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
# This module introduces a simple boot loader installer that installs a UKI,
|
||||
# leveraging bootspec. It is only designed to be useful in tests where
|
||||
# rebuilding is unlikely/hard.
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.boot.loader.uki;
|
||||
in
|
||||
{
|
||||
options.boot.loader.uki = {
|
||||
enable = lib.mkEnableOption "UKI";
|
||||
|
||||
stub = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = "Path to the UKI stub to use.";
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
systemdUkify = pkgs.systemdMinimal.override {
|
||||
withEfi = true;
|
||||
withUkify = true;
|
||||
};
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
boot.bootspec.enable = true;
|
||||
boot.loader.external = {
|
||||
enable = true;
|
||||
installHook =
|
||||
let
|
||||
bootspecNamespace = ''"org.nixos.bootspec.v1"'';
|
||||
installer = pkgs.writeShellApplication {
|
||||
name = "install-uki";
|
||||
runtimeInputs = with pkgs; [ jq systemd binutils ];
|
||||
text = ''
|
||||
boot_json=/nix/var/nix/profiles/system-1-link/boot.json
|
||||
kernel=$(jq -r '.${bootspecNamespace}.kernel' "$boot_json")
|
||||
initrd=$(jq -r '.${bootspecNamespace}.initrd' "$boot_json")
|
||||
init=$(jq -r '.${bootspecNamespace}.init' "$boot_json")
|
||||
|
||||
${systemdUkify}/lib/systemd/ukify \
|
||||
"$kernel" \
|
||||
"$initrd" \
|
||||
--stub=${cfg.stub} \
|
||||
--cmdline="init=$init ${builtins.toString config.boot.kernelParams}" \
|
||||
--os-release="@${config.system.build.etc}/etc/os-release" \
|
||||
--output=uki.efi
|
||||
|
||||
esp=${config.boot.loader.efi.efiSysMountPoint}
|
||||
|
||||
bootctl install --esp-path="$esp"
|
||||
install uki.efi "$esp"/EFI/Linux/
|
||||
'';
|
||||
};
|
||||
in
|
||||
"${installer}/bin/install-uki";
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
{ pkgs, runTest, ukiModule }:
|
||||
|
||||
let
|
||||
common = _: {
|
||||
imports = [ ukiModule ];
|
||||
|
||||
virtualisation = {
|
||||
useBootLoader = true;
|
||||
useEFIBoot = true;
|
||||
};
|
||||
|
||||
boot.loader.uki.enable = true;
|
||||
boot.loader.efi = {
|
||||
canTouchEfiVariables = true;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
# This test serves as a baseline to make sure that the custom boot installer
|
||||
# script defined in the ukiModule works with the upstream systemd-stub. When
|
||||
# this test fails something is very wrong.
|
||||
systemd-stub = runTest {
|
||||
name = "systemd-stub";
|
||||
nodes.machine = _: {
|
||||
imports = [ common ];
|
||||
boot.loader.uki.stub = "${pkgs.systemd}/lib/systemd/boot/efi/linuxx64.efi.stub";
|
||||
};
|
||||
testScript = ''
|
||||
machine.start()
|
||||
print(machine.succeed("bootctl status"))
|
||||
'';
|
||||
};
|
||||
|
||||
fatStub = runTest {
|
||||
name = "fat-stub";
|
||||
nodes.machine = _: {
|
||||
imports = [ common ];
|
||||
};
|
||||
testScript = ''
|
||||
machine.start()
|
||||
print(machine.succeed("bootctl status"))
|
||||
'';
|
||||
};
|
||||
}
|
|
@ -16,10 +16,15 @@ bitflags = "2.3.1"
|
|||
log = { version = "0.4.18", default-features = false, features = [ "max_level_info", "release_max_level_warn" ]}
|
||||
|
||||
# Use software implementation because the UEFI target seems to need it.
|
||||
sha2 = { version = "0.10.6", default-features = false, features = ["force-soft"] }
|
||||
sha2 = { version = "0.10.6", default-features = false, features = ["force-soft"], optional = true }
|
||||
# SHA1 for TPM TCG interface version 1.
|
||||
sha1_smol = "1.0.0"
|
||||
|
||||
[features]
|
||||
default = [ "thin" ]
|
||||
thin = ["dep:sha2"]
|
||||
fat = []
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
use alloc::vec::Vec;
|
||||
use uefi::{prelude::*, CStr16, CString16, Result};
|
||||
|
||||
use crate::linux_loader::InitrdLoader;
|
||||
use crate::pe_loader::Image;
|
||||
use crate::pe_section::pe_section_as_string;
|
||||
|
||||
/// Extract a string, stored as UTF-8, from a PE section.
|
||||
pub fn extract_string(pe_data: &[u8], section: &str) -> Result<CString16> {
|
||||
let string = pe_section_as_string(pe_data, section).ok_or(Status::INVALID_PARAMETER)?;
|
||||
|
||||
Ok(CString16::try_from(string.as_str()).map_err(|_| Status::INVALID_PARAMETER)?)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub 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.to_result()
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
use alloc::vec::Vec;
|
||||
use uefi::{prelude::*, CString16, Result};
|
||||
|
||||
use crate::common::{boot_linux_unchecked, extract_string};
|
||||
use crate::pe_section::pe_section;
|
||||
use crate::uefi_helpers::booted_image_file;
|
||||
|
||||
/// Extract bytes from a PE section.
|
||||
pub fn extract_bytes(pe_data: &[u8], section: &str) -> Result<Vec<u8>> {
|
||||
let bytes: Vec<u8> = pe_section(pe_data, section)
|
||||
.ok_or(Status::INVALID_PARAMETER)?
|
||||
.try_into()
|
||||
.map_err(|_| Status::INVALID_PARAMETER)?;
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// The configuration that is embedded at build time.
|
||||
///
|
||||
/// After this stub is built, configuration need to be embedded into the binary by adding PE
|
||||
/// sections. This struct represents that information.
|
||||
struct EmbeddedConfiguration {
|
||||
/// The kernel command-line.
|
||||
cmdline: CString16,
|
||||
|
||||
/// The kernel as raw bytes.
|
||||
kernel: Vec<u8>,
|
||||
|
||||
/// The initrd as raw bytes.
|
||||
initrd: Vec<u8>,
|
||||
}
|
||||
|
||||
impl EmbeddedConfiguration {
|
||||
fn new(file_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {
|
||||
kernel: extract_bytes(file_data, ".linux")?,
|
||||
initrd: extract_bytes(file_data, ".initrd")?,
|
||||
cmdline: extract_string(file_data, ".cmdline")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn boot_linux(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
|
||||
uefi_services::init(&mut system_table).unwrap();
|
||||
|
||||
// SAFETY: We get a slice that represents our currently running
|
||||
// image and then parse the PE data structures from it. This is
|
||||
// safe, because we don't touch any data in the data sections that
|
||||
// might conceivably change while we look at the slice.
|
||||
let config = unsafe {
|
||||
EmbeddedConfiguration::new(
|
||||
booted_image_file(system_table.boot_services())
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
)
|
||||
.expect("Failed to extract configuration from binary.")
|
||||
};
|
||||
|
||||
boot_linux_unchecked(
|
||||
handle,
|
||||
system_table,
|
||||
config.kernel,
|
||||
&config.cmdline,
|
||||
config.initrd,
|
||||
)
|
||||
.status()
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
extern crate alloc;
|
||||
|
||||
mod common;
|
||||
mod efivars;
|
||||
mod linux_loader;
|
||||
mod measure;
|
||||
|
@ -13,19 +14,19 @@ mod tpm;
|
|||
mod uefi_helpers;
|
||||
mod unified_sections;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
#[cfg(feature = "fat")]
|
||||
mod fat;
|
||||
|
||||
#[cfg(feature = "thin")]
|
||||
mod thin;
|
||||
|
||||
use efivars::{export_efi_variables, get_loader_features, EfiLoaderFeatures};
|
||||
use log::{info, warn};
|
||||
use log::info;
|
||||
use measure::measure_image;
|
||||
use pe_loader::Image;
|
||||
use pe_section::{pe_section, pe_section_as_string};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tpm::tpm_available;
|
||||
use uefi::{prelude::*, proto::loaded_image::LoadedImage, CStr16, CString16, Result};
|
||||
use uefi::prelude::*;
|
||||
|
||||
use crate::{linux_loader::InitrdLoader, uefi_helpers::booted_image_file};
|
||||
|
||||
type Hash = sha2::digest::Output<Sha256>;
|
||||
use crate::uefi_helpers::booted_image_file;
|
||||
|
||||
/// Print the startup logo on boot.
|
||||
fn print_logo() {
|
||||
|
@ -42,177 +43,12 @@ fn print_logo() {
|
|||
);
|
||||
}
|
||||
|
||||
/// The configuration that is embedded at build time.
|
||||
///
|
||||
/// After lanzaboote is built, lzbt needs to embed configuration
|
||||
/// into the binary. This struct represents that information.
|
||||
struct EmbeddedConfiguration {
|
||||
/// The filename of the kernel to be booted. This filename is
|
||||
/// relative to the root of the volume that contains the
|
||||
/// lanzaboote binary.
|
||||
kernel_filename: CString16,
|
||||
|
||||
/// The cryptographic hash of the kernel.
|
||||
kernel_hash: Hash,
|
||||
|
||||
/// The filename of the initrd to be passed to the kernel. See
|
||||
/// `kernel_filename` for how to interpret these filenames.
|
||||
initrd_filename: CString16,
|
||||
|
||||
/// The cryptographic hash of the initrd. This hash is computed
|
||||
/// over the whole PE binary, not only the embedded initrd.
|
||||
initrd_hash: Hash,
|
||||
|
||||
/// The kernel command-line.
|
||||
cmdline: CString16,
|
||||
}
|
||||
|
||||
/// Extract a string, stored as UTF-8, from a PE section.
|
||||
fn extract_string(pe_data: &[u8], section: &str) -> Result<CString16> {
|
||||
let string = pe_section_as_string(pe_data, section).ok_or(Status::INVALID_PARAMETER)?;
|
||||
|
||||
Ok(CString16::try_from(string.as_str()).map_err(|_| Status::INVALID_PARAMETER)?)
|
||||
}
|
||||
|
||||
/// Extract a Blake3 hash from a PE section.
|
||||
fn extract_hash(pe_data: &[u8], section: &str) -> Result<Hash> {
|
||||
let array: [u8; 32] = pe_section(pe_data, section)
|
||||
.ok_or(Status::INVALID_PARAMETER)?
|
||||
.try_into()
|
||||
.map_err(|_| Status::INVALID_PARAMETER)?;
|
||||
|
||||
Ok(array.into())
|
||||
}
|
||||
|
||||
impl EmbeddedConfiguration {
|
||||
fn new(file_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {
|
||||
kernel_filename: extract_string(file_data, ".kernelp")?,
|
||||
kernel_hash: extract_hash(file_data, ".kernelh")?,
|
||||
|
||||
initrd_filename: extract_string(file_data, ".initrdp")?,
|
||||
initrd_hash: extract_hash(file_data, ".initrdh")?,
|
||||
|
||||
cmdline: extract_string(file_data, ".cmdline")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.to_result()
|
||||
}
|
||||
|
||||
/// 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.to_result()
|
||||
}
|
||||
|
||||
#[entry]
|
||||
fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
|
||||
uefi_services::init(&mut system_table).unwrap();
|
||||
|
||||
print_logo();
|
||||
|
||||
// SAFETY: We get a slice that represents our currently running
|
||||
// image and then parse the PE data structures from it. This is
|
||||
// safe, because we don't touch any data in the data sections that
|
||||
// might conceivably change while we look at the slice.
|
||||
let config: EmbeddedConfiguration = unsafe {
|
||||
EmbeddedConfiguration::new(
|
||||
booted_image_file(system_table.boot_services())
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
)
|
||||
.expect("Failed to extract configuration from binary. Did you run lzbt?")
|
||||
};
|
||||
|
||||
let kernel_data;
|
||||
let initrd_data;
|
||||
|
||||
{
|
||||
let mut file_system = system_table
|
||||
.boot_services()
|
||||
.get_image_file_system(handle)
|
||||
.expect("Failed to get file system handle");
|
||||
|
||||
kernel_data = file_system
|
||||
.read(&*config.kernel_filename)
|
||||
.expect("Failed to read kernel file into memory");
|
||||
initrd_data = file_system
|
||||
.read(&*config.initrd_filename)
|
||||
.expect("Failed to read initrd file into memory");
|
||||
}
|
||||
|
||||
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 {
|
||||
warn!("Hash mismatch for kernel!");
|
||||
}
|
||||
|
||||
if !is_initrd_hash_correct {
|
||||
warn!("Hash mismatch for initrd!");
|
||||
}
|
||||
|
||||
if tpm_available(system_table.boot_services()) {
|
||||
info!("TPM available, will proceed to measurements.");
|
||||
unsafe {
|
||||
|
@ -237,46 +73,17 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
|
|||
}
|
||||
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,
|
||||
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 status;
|
||||
|
||||
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()
|
||||
#[cfg(feature = "fat")]
|
||||
{
|
||||
status = fat::boot_linux(handle, system_table)
|
||||
}
|
||||
|
||||
#[cfg(feature = "thin")]
|
||||
{
|
||||
status = thin::boot_linux(handle, system_table)
|
||||
}
|
||||
|
||||
status
|
||||
}
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
use alloc::vec::Vec;
|
||||
use log::warn;
|
||||
use sha2::{Digest, Sha256};
|
||||
use uefi::{prelude::*, proto::loaded_image::LoadedImage, CStr16, CString16, Result};
|
||||
|
||||
use crate::common::{boot_linux_unchecked, extract_string};
|
||||
use crate::pe_section::pe_section;
|
||||
use crate::{linux_loader::InitrdLoader, uefi_helpers::booted_image_file};
|
||||
|
||||
type Hash = sha2::digest::Output<Sha256>;
|
||||
|
||||
/// The configuration that is embedded at build time.
|
||||
///
|
||||
/// After this stub is built, lzbt needs to embed configuration into the binary by adding PE
|
||||
/// sections. This struct represents that information.
|
||||
struct EmbeddedConfiguration {
|
||||
/// The filename of the kernel to be booted. This filename is
|
||||
/// relative to the root of the volume that contains the
|
||||
/// lanzaboote binary.
|
||||
kernel_filename: CString16,
|
||||
|
||||
/// The cryptographic hash of the kernel.
|
||||
kernel_hash: Hash,
|
||||
|
||||
/// The filename of the initrd to be passed to the kernel. See
|
||||
/// `kernel_filename` for how to interpret these filenames.
|
||||
initrd_filename: CString16,
|
||||
|
||||
/// The cryptographic hash of the initrd. This hash is computed
|
||||
/// over the whole PE binary, not only the embedded initrd.
|
||||
initrd_hash: Hash,
|
||||
|
||||
/// The kernel command-line.
|
||||
cmdline: CString16,
|
||||
}
|
||||
|
||||
/// Extract a SHA256 hash from a PE section.
|
||||
fn extract_hash(pe_data: &[u8], section: &str) -> Result<Hash> {
|
||||
let array: [u8; 32] = pe_section(pe_data, section)
|
||||
.ok_or(Status::INVALID_PARAMETER)?
|
||||
.try_into()
|
||||
.map_err(|_| Status::INVALID_PARAMETER)?;
|
||||
|
||||
Ok(array.into())
|
||||
}
|
||||
|
||||
impl EmbeddedConfiguration {
|
||||
fn new(file_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {
|
||||
kernel_filename: extract_string(file_data, ".kernelp")?,
|
||||
kernel_hash: extract_hash(file_data, ".kernelh")?,
|
||||
|
||||
initrd_filename: extract_string(file_data, ".initrdp")?,
|
||||
initrd_hash: extract_hash(file_data, ".initrdh")?,
|
||||
|
||||
cmdline: extract_string(file_data, ".cmdline")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.to_result()
|
||||
}
|
||||
|
||||
pub fn boot_linux(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
|
||||
uefi_services::init(&mut system_table).unwrap();
|
||||
|
||||
// SAFETY: We get a slice that represents our currently running
|
||||
// image and then parse the PE data structures from it. This is
|
||||
// safe, because we don't touch any data in the data sections that
|
||||
// might conceivably change while we look at the slice.
|
||||
let config = unsafe {
|
||||
EmbeddedConfiguration::new(
|
||||
booted_image_file(system_table.boot_services())
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
)
|
||||
.expect("Failed to extract configuration from binary. Did you run lzbt?")
|
||||
};
|
||||
|
||||
let kernel_data;
|
||||
let initrd_data;
|
||||
|
||||
{
|
||||
let mut file_system = system_table
|
||||
.boot_services()
|
||||
.get_image_file_system(handle)
|
||||
.expect("Failed to get file system handle");
|
||||
|
||||
kernel_data = file_system
|
||||
.read(&*config.kernel_filename)
|
||||
.expect("Failed to read kernel file into memory");
|
||||
initrd_data = file_system
|
||||
.read(&*config.initrd_filename)
|
||||
.expect("Failed to read initrd file into memory");
|
||||
}
|
||||
|
||||
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 {
|
||||
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(
|
||||
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.
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue