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
|
signature on the Linux kernel and embedding a cryptographic hash of
|
||||||
the initrd into the signed UKI.
|
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`.
|
The stub lives in `rust/stub`.
|
||||||
|
|
||||||
### Fwupd
|
### Fwupd
|
||||||
|
|
49
flake.nix
49
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 = [
|
systems = [
|
||||||
"x86_64-linux"
|
"x86_64-linux"
|
||||||
|
|
||||||
|
@ -81,7 +92,7 @@
|
||||||
craneLib = crane.lib.x86_64-linux.overrideToolchain uefi-rust-stable;
|
craneLib = crane.lib.x86_64-linux.overrideToolchain uefi-rust-stable;
|
||||||
|
|
||||||
# Build attributes for a Rust application.
|
# Build attributes for a Rust application.
|
||||||
buildRustApp =
|
buildRustApp = lib.makeOverridable (
|
||||||
{ src
|
{ src
|
||||||
, target ? null
|
, target ? null
|
||||||
, doCheck ? true
|
, doCheck ? true
|
||||||
|
@ -126,7 +137,8 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
rustfmt = craneLib.cargoFmt (commonArgs // { inherit cargoArtifacts; });
|
rustfmt = craneLib.cargoFmt (commonArgs // { inherit cargoArtifacts; });
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
stubCrane = buildRustApp {
|
stubCrane = buildRustApp {
|
||||||
src = craneLib.cleanCargoSource ./rust/stub;
|
src = craneLib.cleanCargoSource ./rust/stub;
|
||||||
|
@ -134,7 +146,14 @@
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fatStubCrane = stubCrane.override {
|
||||||
|
extraArgs = {
|
||||||
|
cargoExtraArgs = "--no-default-features --features fat";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
stub = stubCrane.package;
|
stub = stubCrane.package;
|
||||||
|
fatStub = fatStubCrane.package;
|
||||||
|
|
||||||
toolCrane = buildRustApp {
|
toolCrane = buildRustApp {
|
||||||
src = ./rust/tool;
|
src = ./rust/tool;
|
||||||
|
@ -164,7 +183,7 @@
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages = {
|
packages = {
|
||||||
inherit stub;
|
inherit stub fatStub;
|
||||||
tool = wrappedTool;
|
tool = wrappedTool;
|
||||||
lzbt = wrappedTool;
|
lzbt = wrappedTool;
|
||||||
};
|
};
|
||||||
|
@ -173,14 +192,26 @@
|
||||||
inherit (config.packages) tool;
|
inherit (config.packages) tool;
|
||||||
};
|
};
|
||||||
|
|
||||||
checks = {
|
checks =
|
||||||
|
let
|
||||||
|
nixosLib = import (pkgs.path + "/nixos/lib") { };
|
||||||
|
runTest = module: nixosLib.runTest {
|
||||||
|
imports = [ module ];
|
||||||
|
hostPkgs = pkgs;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
toolClippy = toolCrane.clippy;
|
toolClippy = toolCrane.clippy;
|
||||||
stubClippy = stubCrane.clippy;
|
stubClippy = stubCrane.clippy;
|
||||||
|
fatStubClippy = fatStubCrane.clippy;
|
||||||
toolFmt = toolCrane.rustfmt;
|
toolFmt = toolCrane.rustfmt;
|
||||||
stubFmt = stubCrane.rustfmt;
|
stubFmt = stubCrane.rustfmt;
|
||||||
} // (import ./nix/tests/lanzaboote.nix {
|
} // (import ./nix/tests/lanzaboote.nix {
|
||||||
inherit pkgs;
|
inherit pkgs;
|
||||||
lanzabooteModule = self.nixosModules.lanzaboote;
|
lanzabooteModule = self.nixosModules.lanzaboote;
|
||||||
|
}) // (import ./nix/tests/stub.nix {
|
||||||
|
inherit pkgs runTest;
|
||||||
|
ukiModule = self.nixosModules.uki;
|
||||||
});
|
});
|
||||||
|
|
||||||
pre-commit = {
|
pre-commit = {
|
||||||
|
@ -193,8 +224,16 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
shellHook = ''
|
shellHook =
|
||||||
|
let
|
||||||
|
systemdUkify = pkgs.systemdMinimal.override {
|
||||||
|
withEfi = true;
|
||||||
|
withUkify = true;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
''
|
||||||
${config.pre-commit.installationScript}
|
${config.pre-commit.installationScript}
|
||||||
|
export PATH=$PATH:${systemdUkify}/lib/systemd
|
||||||
'';
|
'';
|
||||||
|
|
||||||
packages =
|
packages =
|
||||||
|
|
|
@ -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" ]}
|
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.
|
# 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 for TPM TCG interface version 1.
|
||||||
sha1_smol = "1.0.0"
|
sha1_smol = "1.0.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = [ "thin" ]
|
||||||
|
thin = ["dep:sha2"]
|
||||||
|
fat = []
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
lto = true
|
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;
|
extern crate alloc;
|
||||||
|
|
||||||
|
mod common;
|
||||||
mod efivars;
|
mod efivars;
|
||||||
mod linux_loader;
|
mod linux_loader;
|
||||||
mod measure;
|
mod measure;
|
||||||
|
@ -13,19 +14,19 @@ mod tpm;
|
||||||
mod uefi_helpers;
|
mod uefi_helpers;
|
||||||
mod unified_sections;
|
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 efivars::{export_efi_variables, get_loader_features, EfiLoaderFeatures};
|
||||||
use log::{info, warn};
|
use log::info;
|
||||||
use measure::measure_image;
|
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 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};
|
use crate::uefi_helpers::booted_image_file;
|
||||||
|
|
||||||
type Hash = sha2::digest::Output<Sha256>;
|
|
||||||
|
|
||||||
/// Print the startup logo on boot.
|
/// Print the startup logo on boot.
|
||||||
fn print_logo() {
|
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]
|
#[entry]
|
||||||
fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
|
fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
|
||||||
uefi_services::init(&mut system_table).unwrap();
|
uefi_services::init(&mut system_table).unwrap();
|
||||||
|
|
||||||
print_logo();
|
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()) {
|
if tpm_available(system_table.boot_services()) {
|
||||||
info!("TPM available, will proceed to measurements.");
|
info!("TPM available, will proceed to measurements.");
|
||||||
unsafe {
|
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");
|
export_efi_variables(&system_table).expect("Failed to export stub EFI variables");
|
||||||
|
|
||||||
if is_kernel_hash_correct && is_initrd_hash_correct {
|
let status;
|
||||||
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.");
|
#[cfg(feature = "fat")]
|
||||||
|
{
|
||||||
|
status = fat::boot_linux(handle, system_table)
|
||||||
|
}
|
||||||
|
|
||||||
boot_linux_uefi(
|
#[cfg(feature = "thin")]
|
||||||
handle,
|
{
|
||||||
system_table,
|
status = thin::boot_linux(handle, system_table)
|
||||||
kernel_data,
|
|
||||||
&config.cmdline,
|
|
||||||
initrd_data,
|
|
||||||
)
|
|
||||||
.status()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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