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 { lanzabooteCrane = buildRustApp {
src = ./rust/lanzaboote; src = ./rust/lanzaboote;
target = "x86_64-unknown-uefi"; target = "x86_64-unknown-uefi";
doCheck = false; doCheck = false;
}; };
initrd-stub = initrdStubCrane.package;
lanzaboote = lanzabooteCrane.package; lanzaboote = lanzabooteCrane.package;
lanzatoolCrane = buildRustApp { lanzatoolCrane = buildRustApp {
@ -87,8 +78,7 @@
makeWrapper ${lanzatool-unwrapped}/bin/lanzatool $out/bin/lanzatool \ makeWrapper ${lanzatool-unwrapped}/bin/lanzatool $out/bin/lanzatool \
--set PATH ${lib.makeBinPath [ pkgs.binutils-unwrapped pkgs.sbsigntool ]} \ --set PATH ${lib.makeBinPath [ pkgs.binutils-unwrapped pkgs.sbsigntool ]} \
--set RUST_BACKTRACE full \ --set RUST_BACKTRACE full \
--set LANZABOOTE_STUB ${lanzaboote}/bin/lanzaboote.efi \ --set LANZABOOTE_STUB ${lanzaboote}/bin/lanzaboote.efi
--set LANZABOOTE_INITRD_STUB ${initrd-stub}/bin/initrd-stub.efi \
''; '';
in { in {
overlays.default = final: prev: { overlays.default = final: prev: {
@ -98,7 +88,7 @@
nixosModules.lanzaboote = import ./nix/lanzaboote.nix; nixosModules.lanzaboote = import ./nix/lanzaboote.nix;
packages.x86_64-linux = { packages.x86_64-linux = {
inherit initrd-stub lanzaboote lanzatool; inherit lanzaboote lanzatool;
default = 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; inherit name;
testScript = '' testScript = ''
import json import json
@ -166,10 +159,14 @@
src_path = ${path.src} src_path = ${path.src}
dst_path = ${path.dst} dst_path = ${path.dst}
machine.succeed(f"cp -rf {src_path} {dst_path}") machine.succeed(f"cp -rf {src_path} {dst_path}")
'' + lib.optionalString appendCrap ''
machine.succeed(f"echo Foo >> {dst_path}")
'' +
''
machine.succeed("sync") machine.succeed("sync")
machine.crash() machine.crash()
machine.start() machine.start()
machine.wait_for_console_text("panicked") machine.wait_for_console_text("Hash mismatch")
''; '';
}; };
in in
@ -221,13 +218,21 @@
assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") 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 { is-initrd-secured = mkUnsignedTest {
name = "unsigned-initrd-do-not-boot-under-secureboot"; name = "unsigned-initrd-do-not-boot-under-secureboot";
path = { path = {
src = "bootspec.get('initrd')"; src = "bootspec.get('initrd')";
dst = "convert_to_esp(bootspec.get('initrd'))"; dst = "convert_to_esp(bootspec.get('initrd'))";
}; };
appendCrap = true;
}; };
is-kernel-secured = mkUnsignedTest { is-kernel-secured = mkUnsignedTest {
name = "unsigned-kernel-do-not-boot-under-secureboot"; name = "unsigned-kernel-do-not-boot-under-secureboot";
path = { 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. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.1" version = "0.10.1"
@ -14,12 +26,37 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "constant_time_eq"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279"
[[package]] [[package]]
name = "ed25519-compact" name = "ed25519-compact"
version = "2.0.2" version = "2.0.2"
@ -41,6 +78,7 @@ dependencies = [
name = "lanzaboote" name = "lanzaboote"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"blake3",
"ed25519-compact", "ed25519-compact",
"goblin", "goblin",
"log", "log",

View File

@ -11,6 +11,9 @@ log = "0.4.17"
ed25519-compact = { version = "2.0.2", default-features = false, features = [] } ed25519-compact = { version = "2.0.2", default-features = false, features = [] }
goblin = { version = "0.6.0", default-features = false, features = [ "pe64", "alloc" ]} 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] [profile.release]
opt-level = "s" opt-level = "s"
lto = true lto = true

View File

@ -5,22 +5,18 @@
//! because we read the initrd multiple times. The code needs to be //! because we read the initrd multiple times. The code needs to be
//! restructured to solve this. //! 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::{ use uefi::{
prelude::BootServices, prelude::BootServices,
proto::{ proto::{
device_path::{DevicePath, FfiDevicePath}, device_path::{DevicePath, FfiDevicePath},
media::file::RegularFile,
Protocol, Protocol,
}, },
table::boot::LoadImageSource,
unsafe_guid, Handle, Identify, Result, ResultExt, Status, unsafe_guid, Handle, Identify, Result, ResultExt, Status,
}; };
use crate::uefi_helpers::read_all;
/// The Linux kernel's initrd loading device path. /// The Linux kernel's initrd loading device path.
/// ///
/// The Linux kernel points us to /// The Linux kernel points us to
@ -65,23 +61,10 @@ struct LoadFile2Protocol {
) -> Status, ) -> Status,
// This is not part of the official protocol struct. // This is not part of the official protocol struct.
file: RegularFile, initrd_data: Vec<u8>,
range: Range<usize>,
} }
impl LoadFile2Protocol { 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( fn load_file(
&mut self, &mut self,
_file_path: *const FfiDevicePath, _file_path: *const FfiDevicePath,
@ -89,24 +72,21 @@ impl LoadFile2Protocol {
buffer_size: *mut usize, buffer_size: *mut usize,
buffer: *mut c_void, buffer: *mut c_void,
) -> Result<()> { ) -> Result<()> {
if buffer.is_null() || unsafe { *buffer_size } < self.initrd_size() { if buffer.is_null() || unsafe { *buffer_size } < self.initrd_data.len() {
unsafe { unsafe {
*buffer_size = self.initrd_size(); *buffer_size = self.initrd_data.len();
} }
return Err(Status::BUFFER_TOO_SMALL.into()); return Err(Status::BUFFER_TOO_SMALL.into());
}; };
self.file
.set_position(self.initrd_start().try_into().unwrap())?;
unsafe { unsafe {
*buffer_size = self.initrd_size(); *buffer_size = self.initrd_data.len();
} }
let output_slice: &mut [u8] = let output_slice: &mut [u8] =
unsafe { &mut *slice_from_raw_parts_mut(buffer as *mut u8, *buffer_size) }; 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())?; output_slice.copy_from_slice(&self.initrd_data);
assert_eq!(read_bytes, unsafe { *buffer_size });
Ok(()) Ok(())
} }
@ -134,73 +114,15 @@ pub struct InitrdLoader {
registered: bool, 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 { impl InitrdLoader {
/// Create a new [`InitrdLoader`]. /// Create a new [`InitrdLoader`].
/// ///
/// `handle` is the handle where the protocols are registered /// `handle` is the handle where the protocols are registered
/// on. `file` is the file that is served to Linux. /// on. `file` is the file that is served to Linux.
pub fn new( pub fn new(boot_services: &BootServices, handle: Handle, initrd_data: Vec<u8>) -> Result<Self> {
boot_services: &BootServices,
handle: Handle,
mut file: RegularFile,
) -> Result<Self> {
initrd_verify(boot_services, &mut file)?;
let range = initrd_location(&mut file)?;
let mut proto = Box::pin(LoadFile2Protocol { let mut proto = Box::pin(LoadFile2Protocol {
load_file: raw_load_file, load_file: raw_load_file,
file, initrd_data,
range,
}); });
// Linux finds the right handle by looking for something that // Linux finds the right handle by looking for something that

View File

@ -9,7 +9,8 @@ mod linux_loader;
mod pe_section; mod pe_section;
mod uefi_helpers; mod uefi_helpers;
use pe_section::pe_section_as_string; use blake3::Hash;
use pe_section::{pe_section, pe_section_as_string};
use uefi::{ use uefi::{
prelude::*, prelude::*,
proto::{ proto::{
@ -52,9 +53,33 @@ struct EmbeddedConfiguration {
/// lanzaboote binary. /// lanzaboote binary.
kernel_filename: CString16, kernel_filename: CString16,
/// The cryptographic hash of the kernel.
kernel_hash: Hash,
/// The filename of the initrd to be passed to the kernel. See /// The filename of the initrd to be passed to the kernel. See
/// `kernel_filename` for how to interpret these filenames. /// `kernel_filename` for how to interpret these filenames.
initrd_filename: CString16, 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 { impl EmbeddedConfiguration {
@ -62,14 +87,12 @@ impl EmbeddedConfiguration {
file.set_position(0)?; file.set_position(0)?;
let file_data = read_all(file)?; 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 { Ok(Self {
kernel_filename: CString16::try_from(kernel_filename.as_str()).unwrap(), kernel_filename: extract_filename(&file_data, ".kernelp")?,
initrd_filename: CString16::try_from(initrd_filename.as_str()).unwrap(), 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()) EmbeddedConfiguration::new(&mut booted_image_file(system_table.boot_services()).unwrap())
.expect("Failed to extract configuration from binary. Did you run lanzatool?"); .expect("Failed to extract configuration from binary. Did you run lanzatool?");
let mut kernel_file; let kernel_data;
let initrd = { let initrd_data;
{
let mut file_system = system_table let mut file_system = system_table
.boot_services() .boot_services()
.get_image_file_system(handle) .get_image_file_system(handle)
@ -94,7 +119,7 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
.open_volume() .open_volume()
.expect("Failed to find ESP root directory"); .expect("Failed to find ESP root directory");
kernel_file = root let mut kernel_file = root
.open( .open(
&config.kernel_filename, &config.kernel_filename,
FileMode::Read, FileMode::Read,
@ -104,23 +129,41 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
.into_regular_file() .into_regular_file()
.expect("Kernel is not a regular file"); .expect("Kernel is not a regular file");
root.open( kernel_data = read_all(&mut kernel_file).expect("Failed to read kernel file into memory");
let mut initrd_file = root
.open(
&config.initrd_filename, &config.initrd_filename,
FileMode::Read, FileMode::Read,
FileAttribute::empty(), FileAttribute::empty(),
) )
.expect("Failed to open initrd for reading") .expect("Failed to open initrd for reading")
.into_regular_file() .into_regular_file()
.expect("Initrd is not a 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 = let kernel_cmdline =
booted_image_cmdline(system_table.boot_services()).expect("Failed to fetch command line"); booted_image_cmdline(system_table.boot_services()).expect("Failed to fetch command line");
let kernel_handle = { let kernel_handle = {
let kernel_data =
read_all(&mut kernel_file).expect("Failed to read kernel file into memory");
system_table system_table
.boot_services() .boot_services()
.load_image( .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"); .expect("Failed to load the initrd. It may not be there or it is not signed");
let status = system_table let status = system_table
.boot_services() .boot_services()

View File

@ -8,6 +8,18 @@ version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" 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]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -31,6 +43,35 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -74,6 +115,33 @@ dependencies = [
"os_str_bytes", "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]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.8.0" version = "1.8.0"
@ -83,6 +151,16 @@ dependencies = [
"instant", "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]] [[package]]
name = "goblin" name = "goblin"
version = "0.6.0" version = "0.6.0"
@ -129,6 +207,7 @@ name = "lanzatool"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"blake3",
"clap", "clap",
"goblin", "goblin",
"nix", "nix",
@ -305,6 +384,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.103" version = "1.0.103"
@ -339,6 +424,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.5" 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 = { version = "1.0.147", features = ["derive"] }
serde_json = "1.0.89" serde_json = "1.0.89"
tempfile = "3.3.0" tempfile = "3.3.0"
blake3 = "1.3.3"

View File

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

View File

@ -14,7 +14,6 @@ use crate::signature::KeyPair;
pub struct Installer { pub struct Installer {
lanzaboote_stub: PathBuf, lanzaboote_stub: PathBuf,
initrd_stub: PathBuf,
key_pair: KeyPair, key_pair: KeyPair,
_pki_bundle: Option<PathBuf>, _pki_bundle: Option<PathBuf>,
_auto_enroll: bool, _auto_enroll: bool,
@ -25,7 +24,6 @@ pub struct Installer {
impl Installer { impl Installer {
pub fn new( pub fn new(
lanzaboote_stub: PathBuf, lanzaboote_stub: PathBuf,
initrd_stub: PathBuf,
key_pair: KeyPair, key_pair: KeyPair,
_pki_bundle: Option<PathBuf>, _pki_bundle: Option<PathBuf>,
_auto_enroll: bool, _auto_enroll: bool,
@ -34,7 +32,6 @@ impl Installer {
) -> Self { ) -> Self {
Self { Self {
lanzaboote_stub, lanzaboote_stub,
initrd_stub,
key_pair, key_pair,
_pki_bundle, _pki_bundle,
_auto_enroll, _auto_enroll,
@ -68,14 +65,10 @@ impl Installer {
} }
fn install_generation(&self, generation: &Generation) -> Result<()> { fn install_generation(&self, generation: &Generation) -> Result<()> {
println!("Reading bootspec...");
let bootspec = &generation.bootspec; let bootspec = &generation.bootspec;
let esp_paths = EspPaths::new(&self.esp, generation)?; let esp_paths = EspPaths::new(&self.esp, generation)?;
println!("Assembling lanzaboote image...");
let kernel_cmdline = let kernel_cmdline =
assemble_kernel_cmdline(&bootspec.init, bootspec.kernel_params.clone()); assemble_kernel_cmdline(&bootspec.init, bootspec.kernel_params.clone());
@ -87,6 +80,31 @@ impl Installer {
// TODO(Raito): prove to niksnur this is actually acceptable. // TODO(Raito): prove to niksnur this is actually acceptable.
let secure_temp_dir = tempdir()?; 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( let lanzaboote_image = pe::lanzaboote_image(
&secure_temp_dir, &secure_temp_dir,
&self.lanzaboote_stub, &self.lanzaboote_stub,
@ -98,40 +116,17 @@ impl Installer {
) )
.context("Failed to assemble stub")?; .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"); // Sync files to persistent storage. This may improve the
copy(&bootspec.initrd, &initrd_location)?; // chance of a consistent boot directory in case the system
if let Some(initrd_secrets_script) = &bootspec.initrd_secrets { // crashes.
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();
}
println!( println!(
"Successfully installed lanzaboote to '{}'", "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( pub fn append_initrd_secrets(
append_initrd_secrets_path: &Path, append_initrd_secrets_path: &Path,
initrd_path: &PathBuf, initrd_path: &PathBuf,

View File

@ -12,6 +12,11 @@ use crate::utils;
use tempfile::TempDir; 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( pub fn lanzaboote_image(
target_dir: &TempDir, target_dir: &TempDir,
lanzaboote_stub: &Path, lanzaboote_stub: &Path,
@ -21,49 +26,67 @@ pub fn lanzaboote_image(
initrd_path: &Path, initrd_path: &Path,
esp: &Path, esp: &Path,
) -> Result<PathBuf> { ) -> Result<PathBuf> {
// objcopy copies files into the PE binary. That's why we have to write the contents // objcopy can only copy files into the PE binary. That's why we
// of some bootspec properties to disk // have to write the contents of some bootspec properties to disk.
let (kernel_cmdline_file, _) = let kernel_cmdline_file = write_to_tmp(target_dir, "kernel-cmdline", kernel_cmdline.join(" "))?;
write_to_tmp(target_dir, "kernel-cmdline", kernel_cmdline.join(" "))?;
let (kernel_path_file, _) = write_to_tmp( let kernel_path_file = write_to_tmp(
target_dir, target_dir,
"kernel-esp-path", "kernel-esp-path",
esp_relative_path_string(esp, kernel_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, target_dir,
"initrd-esp-path", "initrd-esp-path",
esp_relative_path_string(esp, initrd_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 os_release_offs = stub_offset(lanzaboote_stub)?;
let kernel_cmdline_offs = os_release_offs + file_size(os_release)?; let kernel_cmdline_offs = os_release_offs + file_size(os_release)?;
let initrd_path_offs = kernel_cmdline_offs + file_size(&kernel_cmdline_file)?; 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 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![ let sections = vec![
s(".osrel", os_release, os_release_offs), s(".osrel", os_release, os_release_offs),
s(".cmdline", kernel_cmdline_file, kernel_cmdline_offs), s(".cmdline", kernel_cmdline_file, kernel_cmdline_offs),
s(".initrdp", initrd_path_file, initrd_path_offs), s(".initrdp", initrd_path_file, initrd_path_offs),
s(".kernelp", kernel_path_file, kernel_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) 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> { /// Compute the blake3 hash of a file.
let initrd_offs = stub_offset(initrd_stub)?; fn file_hash(file: &Path) -> Result<blake3::Hash> {
let sections = vec![s(".initrd", initrd, initrd_offs)]; Ok(blake3::hash(&fs::read(file)?))
wrap_in_pe(target_dir, "wrapped-initrd.exe", initrd_stub, sections)
} }
/// 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( fn wrap_in_pe(
target_dir: &TempDir, target_dir: &TempDir,
filename: &str, output_filename: &str,
stub: &Path, stub: &Path,
sections: Vec<Section>, sections: Vec<Section>,
) -> Result<PathBuf> { ) -> Result<PathBuf> {
let image_path = target_dir.path().join(filename); let image_path = target_dir.path().join(output_filename);
let _ = fs::OpenOptions::new() let _ = fs::OpenOptions::new()
.create(true) .create(true)
.write(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( fn write_to_tmp(
secure_temp: &TempDir, secure_temp: &TempDir,
filename: &str, filename: &str,
contents: impl AsRef<[u8]>, contents: impl AsRef<[u8]>,
) -> Result<(PathBuf, fs::File)> { ) -> Result<PathBuf> {
let path = secure_temp.path().join(filename);
let mut tmpfile = fs::OpenOptions::new() let mut tmpfile = fs::OpenOptions::new()
.create(true) .create(true)
.write(true) .write(true)
.mode(0o600) .mode(0o600)
.open(secure_temp.path().join(filename)) .open(&path)
.context("Failed to create tempfile")?; .context("Failed to create tempfile")?;
tmpfile tmpfile
.write_all(contents.as_ref()) .write_all(contents.as_ref())
.context("Failed to write to tempfile")?; .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 { fn esp_relative_path_string(esp: &Path, path: &Path) -> String {