Merge pull request #21 from nix-community/boot-file-integrity

Verify Kernel/Initrd Integrity using Blake3
This commit is contained in:
Julian Stecklina 2022-12-09 23:54:14 +00:00 committed by GitHub
commit 06da27529f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 338 additions and 320 deletions

View File

@ -54,21 +54,12 @@
});
};
# This is basically an empty EFI application that we use as a
# carrier for the initrd.
initrdStubCrane = buildRustApp {
src = ./rust/initrd-stub;
target = "x86_64-unknown-uefi";
doCheck = false;
};
lanzabooteCrane = buildRustApp {
src = ./rust/lanzaboote;
target = "x86_64-unknown-uefi";
doCheck = false;
};
initrd-stub = initrdStubCrane.package;
lanzaboote = lanzabooteCrane.package;
lanzatoolCrane = buildRustApp {
@ -87,8 +78,7 @@
makeWrapper ${lanzatool-unwrapped}/bin/lanzatool $out/bin/lanzatool \
--set PATH ${lib.makeBinPath [ pkgs.binutils-unwrapped pkgs.sbsigntool ]} \
--set RUST_BACKTRACE full \
--set LANZABOOTE_STUB ${lanzaboote}/bin/lanzaboote.efi \
--set LANZABOOTE_INITRD_STUB ${initrd-stub}/bin/initrd-stub.efi \
--set LANZABOOTE_STUB ${lanzaboote}/bin/lanzaboote.efi
'';
in {
overlays.default = final: prev: {
@ -98,7 +88,7 @@
nixosModules.lanzaboote = import ./nix/lanzaboote.nix;
packages.x86_64-linux = {
inherit initrd-stub lanzaboote lanzatool;
inherit lanzaboote lanzatool;
default = lanzatool;
};
@ -149,7 +139,10 @@
};
};
};
mkUnsignedTest = { name, path }: mkSecureBootTest {
# Execute a boot test that is intended to fail.
#
mkUnsignedTest = { name, path, appendCrap ? false }: mkSecureBootTest {
inherit name;
testScript = ''
import json
@ -166,10 +159,14 @@
src_path = ${path.src}
dst_path = ${path.dst}
machine.succeed(f"cp -rf {src_path} {dst_path}")
'' + lib.optionalString appendCrap ''
machine.succeed(f"echo Foo >> {dst_path}")
'' +
''
machine.succeed("sync")
machine.crash()
machine.start()
machine.wait_for_console_text("panicked")
machine.wait_for_console_text("Hash mismatch")
'';
};
in
@ -221,13 +218,21 @@
assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status")
'';
};
# 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.
is-initrd-secured = mkUnsignedTest {
name = "unsigned-initrd-do-not-boot-under-secureboot";
path = {
src = "bootspec.get('initrd')";
dst = "convert_to_esp(bootspec.get('initrd'))";
};
appendCrap = true;
};
is-kernel-secured = mkUnsignedTest {
name = "unsigned-kernel-do-not-boot-under-secureboot";
path = {

View File

@ -1,2 +0,0 @@
[build]
target = "x86_64-unknown-uefi"

View File

@ -1,104 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bit_field"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "initrd-stub"
version = "0.1.0"
dependencies = [
"uefi",
]
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "proc-macro2"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "ucs2"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad643914094137d475641b6bab89462505316ec2ce70907ad20102d28a79ab8"
dependencies = [
"bit_field",
]
[[package]]
name = "uefi"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07b87700863d65dd4841556be3374d8d4f9f8dbb577ad93a39859e70b3b91f35"
dependencies = [
"bitflags",
"log",
"ucs2",
"uefi-macros",
]
[[package]]
name = "uefi-macros"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "275f054a1d9fd7e43a2ce91cc24298a87b281117dea8afc120ae95faa0e96b94"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"

View File

@ -1,12 +0,0 @@
[package]
name = "initrd-stub"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
uefi = { version = "0.18.0", default-features = false, features = [ ] }
[profile.release]
opt-level = "s"
lto = true

View File

@ -1,19 +0,0 @@
#![no_main]
#![no_std]
#![feature(abi_efiapi)]
use core::panic::PanicInfo;
use uefi::{
prelude::{entry, Boot, SystemTable},
Handle, Status,
};
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[entry]
fn main(_handle: Handle, mut _system_table: SystemTable<Boot>) -> Status {
Status::UNSUPPORTED
}

View File

@ -2,6 +2,18 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]]
name = "arrayvec"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]]
name = "bit_field"
version = "0.10.1"
@ -14,12 +26,37 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blake3"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
]
[[package]]
name = "cc"
version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "constant_time_eq"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279"
[[package]]
name = "ed25519-compact"
version = "2.0.2"
@ -41,6 +78,7 @@ dependencies = [
name = "lanzaboote"
version = "0.1.0"
dependencies = [
"blake3",
"ed25519-compact",
"goblin",
"log",

View File

@ -11,6 +11,9 @@ log = "0.4.17"
ed25519-compact = { version = "2.0.2", default-features = false, features = [] }
goblin = { version = "0.6.0", default-features = false, features = [ "pe64", "alloc" ]}
# We don't want the assembly implementations for now.
blake3 = { version = "1.3.3", default-features = false, features = [ "pure" ]}
[profile.release]
opt-level = "s"
lto = true

View File

@ -5,22 +5,18 @@
//! because we read the initrd multiple times. The code needs to be
//! restructured to solve this.
use core::{ffi::c_void, ops::Range, pin::Pin, ptr::slice_from_raw_parts_mut};
use core::{ffi::c_void, pin::Pin, ptr::slice_from_raw_parts_mut};
use alloc::boxed::Box;
use alloc::{boxed::Box, vec::Vec};
use uefi::{
prelude::BootServices,
proto::{
device_path::{DevicePath, FfiDevicePath},
media::file::RegularFile,
Protocol,
},
table::boot::LoadImageSource,
unsafe_guid, Handle, Identify, Result, ResultExt, Status,
};
use crate::uefi_helpers::read_all;
/// The Linux kernel's initrd loading device path.
///
/// The Linux kernel points us to
@ -65,23 +61,10 @@ struct LoadFile2Protocol {
) -> Status,
// This is not part of the official protocol struct.
file: RegularFile,
range: Range<usize>,
initrd_data: Vec<u8>,
}
impl LoadFile2Protocol {
fn initrd_start(&self) -> usize {
self.range.start
}
fn initrd_size(&self) -> usize {
if self.range.is_empty() {
0
} else {
self.range.end - self.range.start
}
}
fn load_file(
&mut self,
_file_path: *const FfiDevicePath,
@ -89,24 +72,21 @@ impl LoadFile2Protocol {
buffer_size: *mut usize,
buffer: *mut c_void,
) -> Result<()> {
if buffer.is_null() || unsafe { *buffer_size } < self.initrd_size() {
if buffer.is_null() || unsafe { *buffer_size } < self.initrd_data.len() {
unsafe {
*buffer_size = self.initrd_size();
*buffer_size = self.initrd_data.len();
}
return Err(Status::BUFFER_TOO_SMALL.into());
};
self.file
.set_position(self.initrd_start().try_into().unwrap())?;
unsafe {
*buffer_size = self.initrd_size();
*buffer_size = self.initrd_data.len();
}
let output_slice: &mut [u8] =
unsafe { &mut *slice_from_raw_parts_mut(buffer as *mut u8, *buffer_size) };
let read_bytes = self.file.read(output_slice).map_err(|e| e.status())?;
assert_eq!(read_bytes, unsafe { *buffer_size });
output_slice.copy_from_slice(&self.initrd_data);
Ok(())
}
@ -134,73 +114,15 @@ pub struct InitrdLoader {
registered: bool,
}
/// Returns the data range of the initrd in the PE binary.
///
/// The initrd has to be embedded in the file as a .initrd PE section.
fn initrd_location(initrd_efi: &mut RegularFile) -> Result<Range<usize>> {
initrd_efi.set_position(0)?;
let file_data = read_all(initrd_efi)?;
let pe_binary = goblin::pe::PE::parse(&file_data).map_err(|_| Status::INVALID_PARAMETER)?;
pe_binary
.sections
.iter()
.find(|s| s.name().unwrap() == ".initrd")
.map(|s| {
let section_start: usize = s.pointer_to_raw_data.try_into().unwrap();
let section_size: usize = s.size_of_raw_data.try_into().unwrap();
Range {
start: section_start,
end: section_start + section_size,
}
})
.ok_or_else(|| Status::END_OF_FILE.into())
}
/// Check the signature of the initrd.
///
/// For this to work, the initrd needs to be a PE binary. We misuse
/// [`BootServices::load_image`] for this.
fn initrd_verify(boot_services: &BootServices, initrd_efi: &mut RegularFile) -> Result<()> {
initrd_efi.set_position(0)?;
let file_data = read_all(initrd_efi)?;
let initrd_handle = boot_services.load_image(
boot_services.image_handle(),
LoadImageSource::FromBuffer {
buffer: &file_data,
file_path: None,
},
)?;
// If we get here, the security policy allowed loading the
// image. This means that it was signed with an acceptable key in
// the Secure Boot scenario.
boot_services.unload_image(initrd_handle)?;
Ok(())
}
impl InitrdLoader {
/// Create a new [`InitrdLoader`].
///
/// `handle` is the handle where the protocols are registered
/// on. `file` is the file that is served to Linux.
pub fn new(
boot_services: &BootServices,
handle: Handle,
mut file: RegularFile,
) -> Result<Self> {
initrd_verify(boot_services, &mut file)?;
let range = initrd_location(&mut file)?;
pub fn new(boot_services: &BootServices, handle: Handle, initrd_data: Vec<u8>) -> Result<Self> {
let mut proto = Box::pin(LoadFile2Protocol {
load_file: raw_load_file,
file,
range,
initrd_data,
});
// Linux finds the right handle by looking for something that

View File

@ -9,7 +9,8 @@ mod linux_loader;
mod pe_section;
mod uefi_helpers;
use pe_section::pe_section_as_string;
use blake3::Hash;
use pe_section::{pe_section, pe_section_as_string};
use uefi::{
prelude::*,
proto::{
@ -52,9 +53,33 @@ struct EmbeddedConfiguration {
/// 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,
}
/// Extract a filename from a PE section. The filename is stored as UTF-8.
fn extract_filename(file_data: &[u8], section: &str) -> Result<CString16> {
let filename = pe_section_as_string(file_data, section).ok_or(Status::INVALID_PARAMETER)?;
Ok(CString16::try_from(filename.as_str()).map_err(|_| Status::INVALID_PARAMETER)?)
}
/// Extract a Blake3 hash from a PE section.
fn extract_hash(file_data: &[u8], section: &str) -> Result<Hash> {
let array: [u8; 32] = pe_section(file_data, section)
.ok_or(Status::INVALID_PARAMETER)?
.try_into()
.map_err(|_| Status::INVALID_PARAMETER)?;
Ok(array.into())
}
impl EmbeddedConfiguration {
@ -62,14 +87,12 @@ impl EmbeddedConfiguration {
file.set_position(0)?;
let file_data = read_all(file)?;
let kernel_filename =
pe_section_as_string(&file_data, ".kernelp").ok_or(Status::INVALID_PARAMETER)?;
let initrd_filename =
pe_section_as_string(&file_data, ".initrdp").ok_or(Status::INVALID_PARAMETER)?;
Ok(Self {
kernel_filename: CString16::try_from(kernel_filename.as_str()).unwrap(),
initrd_filename: CString16::try_from(initrd_filename.as_str()).unwrap(),
kernel_filename: extract_filename(&file_data, ".kernelp")?,
kernel_hash: extract_hash(&file_data, ".kernelh")?,
initrd_filename: extract_filename(&file_data, ".initrdp")?,
initrd_hash: extract_hash(&file_data, ".initrdh")?,
})
}
}
@ -84,8 +107,10 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
EmbeddedConfiguration::new(&mut booted_image_file(system_table.boot_services()).unwrap())
.expect("Failed to extract configuration from binary. Did you run lanzatool?");
let mut kernel_file;
let initrd = {
let kernel_data;
let initrd_data;
{
let mut file_system = system_table
.boot_services()
.get_image_file_system(handle)
@ -94,7 +119,7 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
.open_volume()
.expect("Failed to find ESP root directory");
kernel_file = root
let mut kernel_file = root
.open(
&config.kernel_filename,
FileMode::Read,
@ -104,23 +129,41 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
.into_regular_file()
.expect("Kernel is not a regular file");
root.open(
&config.initrd_filename,
FileMode::Read,
FileAttribute::empty(),
)
.expect("Failed to open initrd for reading")
.into_regular_file()
.expect("Initrd is not a regular file")
};
kernel_data = read_all(&mut kernel_file).expect("Failed to read kernel file into memory");
let mut initrd_file = root
.open(
&config.initrd_filename,
FileMode::Read,
FileAttribute::empty(),
)
.expect("Failed to open initrd for reading")
.into_regular_file()
.expect("Initrd is not a regular file");
initrd_data = read_all(&mut initrd_file).expect("Failed to read kernel file into memory");
}
if blake3::hash(&kernel_data) != config.kernel_hash {
system_table
.stdout()
.output_string(cstr16!("Hash mismatch for kernel. Refusing to load!\r\n"))
.unwrap();
return Status::SECURITY_VIOLATION;
}
if blake3::hash(&initrd_data) != config.initrd_hash {
system_table
.stdout()
.output_string(cstr16!("Hash mismatch for initrd. Refusing to load!\r\n"))
.unwrap();
return Status::SECURITY_VIOLATION;
}
let kernel_cmdline =
booted_image_cmdline(system_table.boot_services()).expect("Failed to fetch command line");
let kernel_handle = {
let kernel_data =
read_all(&mut kernel_file).expect("Failed to read kernel file into memory");
system_table
.boot_services()
.load_image(
@ -145,7 +188,7 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
);
}
let mut initrd_loader = InitrdLoader::new(system_table.boot_services(), handle, initrd)
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");
let status = system_table
.boot_services()

View File

@ -8,6 +8,18 @@ version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]]
name = "arrayvec"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]]
name = "atty"
version = "0.2.14"
@ -31,6 +43,35 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blake3"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
dependencies = [
"generic-array",
]
[[package]]
name = "cc"
version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -74,6 +115,33 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "constant_time_eq"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "fastrand"
version = "1.8.0"
@ -83,6 +151,16 @@ dependencies = [
"instant",
]
[[package]]
name = "generic-array"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "goblin"
version = "0.6.0"
@ -129,6 +207,7 @@ name = "lanzatool"
version = "0.1.0"
dependencies = [
"anyhow",
"blake3",
"clap",
"goblin",
"nix",
@ -305,6 +384,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.103"
@ -339,6 +424,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-ident"
version = "1.0.5"

View File

@ -13,3 +13,4 @@ nix = { version = "0.25.0", default-features = false, features = [ "fs" ] }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = "1.0.89"
tempfile = "3.3.0"
blake3 = "1.3.3"

View File

@ -59,14 +59,11 @@ impl Commands {
fn install(args: InstallCommand) -> Result<()> {
let lanzaboote_stub =
std::env::var("LANZABOOTE_STUB").context("Failed to read LANZABOOTE_STUB env variable")?;
let initrd_stub = std::env::var("LANZABOOTE_INITRD_STUB")
.context("Failed to read LANZABOOTE_INITRD_STUB env variable")?;
let key_pair = KeyPair::new(&args.public_key, &args.private_key);
install::Installer::new(
PathBuf::from(lanzaboote_stub),
PathBuf::from(initrd_stub),
key_pair,
args.pki_bundle,
args.auto_enroll,

View File

@ -14,7 +14,6 @@ use crate::signature::KeyPair;
pub struct Installer {
lanzaboote_stub: PathBuf,
initrd_stub: PathBuf,
key_pair: KeyPair,
_pki_bundle: Option<PathBuf>,
_auto_enroll: bool,
@ -25,7 +24,6 @@ pub struct Installer {
impl Installer {
pub fn new(
lanzaboote_stub: PathBuf,
initrd_stub: PathBuf,
key_pair: KeyPair,
_pki_bundle: Option<PathBuf>,
_auto_enroll: bool,
@ -34,7 +32,6 @@ impl Installer {
) -> Self {
Self {
lanzaboote_stub,
initrd_stub,
key_pair,
_pki_bundle,
_auto_enroll,
@ -68,14 +65,10 @@ impl Installer {
}
fn install_generation(&self, generation: &Generation) -> Result<()> {
println!("Reading bootspec...");
let bootspec = &generation.bootspec;
let esp_paths = EspPaths::new(&self.esp, generation)?;
println!("Assembling lanzaboote image...");
let kernel_cmdline =
assemble_kernel_cmdline(&bootspec.init, bootspec.kernel_params.clone());
@ -87,6 +80,31 @@ impl Installer {
// TODO(Raito): prove to niksnur this is actually acceptable.
let secure_temp_dir = tempdir()?;
println!("Appending secrets to initrd...");
let initrd_location = secure_temp_dir.path().join("initrd");
copy(&bootspec.initrd, &initrd_location)?;
if let Some(initrd_secrets_script) = &bootspec.initrd_secrets {
append_initrd_secrets(initrd_secrets_script, &initrd_location)?;
}
let systemd_boot = bootspec
.toplevel
.join("systemd/lib/systemd/boot/efi/systemd-bootx64.efi");
[
(&systemd_boot, &esp_paths.efi_fallback),
(&systemd_boot, &esp_paths.systemd_boot),
(&bootspec.kernel, &esp_paths.kernel),
]
.into_iter()
.try_for_each(|(from, to)| install_signed(&self.key_pair, from, to))?;
// The initrd doesn't need to be signed. Lanzaboote has its
// hash embedded and will refuse loading it when the hash
// mismatches.
install(&initrd_location, &esp_paths.initrd).context("Failed to install initrd to ESP")?;
let lanzaboote_image = pe::lanzaboote_image(
&secure_temp_dir,
&self.lanzaboote_stub,
@ -98,40 +116,17 @@ impl Installer {
)
.context("Failed to assemble stub")?;
println!("Wrapping initrd into a PE binary...");
install_signed(
&self.key_pair,
&lanzaboote_image,
&esp_paths.lanzaboote_image,
)
.context("Failed to install lanzaboote")?;
let initrd_location = secure_temp_dir.path().join("initrd");
copy(&bootspec.initrd, &initrd_location)?;
if let Some(initrd_secrets_script) = &bootspec.initrd_secrets {
append_initrd_secrets(initrd_secrets_script, &initrd_location)?;
}
let wrapped_initrd = pe::wrap_initrd(&secure_temp_dir, &self.initrd_stub, &initrd_location)
.context("Failed to assemble stub")?;
println!("Sign and copy files to EFI system partition...");
let systemd_boot = bootspec
.toplevel
.join("systemd/lib/systemd/boot/efi/systemd-bootx64.efi");
let files_to_copy_and_sign = [
(&systemd_boot, &esp_paths.efi_fallback),
(&systemd_boot, &esp_paths.systemd_boot),
(&lanzaboote_image, &esp_paths.lanzaboote_image),
(&bootspec.kernel, &esp_paths.kernel),
(&wrapped_initrd, &esp_paths.initrd),
];
for (from, to) in files_to_copy_and_sign {
println!("Signing {}...", to.display());
ensure_parent_dir(to);
self.key_pair.sign_and_copy(from, to).with_context(|| {
format!("Failed to copy and sign file from {:?} to {:?}", from, to)
})?;
// Call sync to improve the likelihood that file is actually written to disk
sync();
}
// Sync files to persistent storage. This may improve the
// chance of a consistent boot directory in case the system
// crashes.
sync();
println!(
"Successfully installed lanzaboote to '{}'",
@ -142,6 +137,38 @@ impl Installer {
}
}
/// Install a PE file. The PE gets signed in the process.
///
/// The file is only signed and copied if it doesn't exist at the destination
fn install_signed(key_pair: &KeyPair, from: &Path, to: &Path) -> Result<()> {
if to.exists() {
println!("{} already exists, skipping...", to.display());
} else {
println!("Signing and installing {}...", to.display());
ensure_parent_dir(to);
key_pair
.sign_and_copy(from, to)
.with_context(|| format!("Failed to copy and sign file from {:?} to {:?}", from, to))?;
}
Ok(())
}
/// Install an arbitrary file
///
/// The file is only copied if it doesn't exist at the destination
fn install(from: &Path, to: &Path) -> Result<()> {
if to.exists() {
println!("{} already exists, skipping...", to.display());
} else {
println!("Installing {}...", to.display());
ensure_parent_dir(to);
copy(from, to)?;
}
Ok(())
}
pub fn append_initrd_secrets(
append_initrd_secrets_path: &Path,
initrd_path: &PathBuf,

View File

@ -12,6 +12,11 @@ use crate::utils;
use tempfile::TempDir;
/// Attach all information that lanzaboote needs into the PE binary.
///
/// When this function is called the referenced files already need to
/// be present in the ESP. This is required, because we need to read
/// them to compute hashes.
pub fn lanzaboote_image(
target_dir: &TempDir,
lanzaboote_stub: &Path,
@ -21,49 +26,67 @@ pub fn lanzaboote_image(
initrd_path: &Path,
esp: &Path,
) -> Result<PathBuf> {
// objcopy copies files into the PE binary. That's why we have to write the contents
// of some bootspec properties to disk
let (kernel_cmdline_file, _) =
write_to_tmp(target_dir, "kernel-cmdline", kernel_cmdline.join(" "))?;
let (kernel_path_file, _) = write_to_tmp(
// objcopy can only copy files into the PE binary. That's why we
// have to write the contents of some bootspec properties to disk.
let kernel_cmdline_file = write_to_tmp(target_dir, "kernel-cmdline", kernel_cmdline.join(" "))?;
let kernel_path_file = write_to_tmp(
target_dir,
"kernel-esp-path",
esp_relative_path_string(esp, kernel_path),
)?;
let (initrd_path_file, _) = write_to_tmp(
let kernel_hash_file = write_to_tmp(
target_dir,
"kernel-hash",
file_hash(kernel_path)?.as_bytes(),
)?;
let initrd_path_file = write_to_tmp(
target_dir,
"initrd-esp-path",
esp_relative_path_string(esp, initrd_path),
)?;
let initrd_hash_file = write_to_tmp(
target_dir,
"initrd-hash",
file_hash(initrd_path)?.as_bytes(),
)?;
let os_release_offs = stub_offset(lanzaboote_stub)?;
let kernel_cmdline_offs = os_release_offs + file_size(os_release)?;
let initrd_path_offs = kernel_cmdline_offs + file_size(&kernel_cmdline_file)?;
let kernel_path_offs = initrd_path_offs + file_size(&initrd_path_file)?;
let initrd_hash_offs = kernel_path_offs + file_size(&kernel_path_file)?;
let kernel_hash_offs = initrd_hash_offs + file_size(&initrd_hash_file)?;
let sections = vec![
s(".osrel", os_release, os_release_offs),
s(".cmdline", kernel_cmdline_file, kernel_cmdline_offs),
s(".initrdp", initrd_path_file, initrd_path_offs),
s(".kernelp", kernel_path_file, kernel_path_offs),
s(".initrdh", initrd_hash_file, initrd_hash_offs),
s(".kernelh", kernel_hash_file, kernel_hash_offs),
];
wrap_in_pe(target_dir, "lanzaboote-stub.efi", lanzaboote_stub, sections)
}
pub fn wrap_initrd(target_dir: &TempDir, initrd_stub: &Path, initrd: &Path) -> Result<PathBuf> {
let initrd_offs = stub_offset(initrd_stub)?;
let sections = vec![s(".initrd", initrd, initrd_offs)];
wrap_in_pe(target_dir, "wrapped-initrd.exe", initrd_stub, sections)
/// Compute the blake3 hash of a file.
fn file_hash(file: &Path) -> Result<blake3::Hash> {
Ok(blake3::hash(&fs::read(file)?))
}
/// Take a PE binary stub and attach sections to it.
///
/// The result is then written to a new file. Returns the filename of
/// the generated file.
fn wrap_in_pe(
target_dir: &TempDir,
filename: &str,
output_filename: &str,
stub: &Path,
sections: Vec<Section>,
) -> Result<PathBuf> {
let image_path = target_dir.path().join(filename);
let image_path = target_dir.path().join(output_filename);
let _ = fs::OpenOptions::new()
.create(true)
.write(true)
@ -117,21 +140,26 @@ fn s(name: &'static str, file_path: impl AsRef<Path>, offset: u64) -> Section {
}
}
/// Write a `u8` slice to a temporary file.
fn write_to_tmp(
secure_temp: &TempDir,
filename: &str,
contents: impl AsRef<[u8]>,
) -> Result<(PathBuf, fs::File)> {
) -> Result<PathBuf> {
let path = secure_temp.path().join(filename);
let mut tmpfile = fs::OpenOptions::new()
.create(true)
.write(true)
.mode(0o600)
.open(secure_temp.path().join(filename))
.open(&path)
.context("Failed to create tempfile")?;
tmpfile
.write_all(contents.as_ref())
.context("Failed to write to tempfile")?;
Ok((secure_temp.path().join(filename), tmpfile))
Ok(path)
}
fn esp_relative_path_string(esp: &Path, path: &Path) -> String {