diff --git a/flake.nix b/flake.nix index 62a40e1..04ea3e5 100644 --- a/flake.nix +++ b/flake.nix @@ -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 = { diff --git a/rust/initrd-stub/.cargo/config b/rust/initrd-stub/.cargo/config deleted file mode 100644 index 85d49dc..0000000 --- a/rust/initrd-stub/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "x86_64-unknown-uefi" diff --git a/rust/initrd-stub/Cargo.lock b/rust/initrd-stub/Cargo.lock deleted file mode 100644 index 6c72db2..0000000 --- a/rust/initrd-stub/Cargo.lock +++ /dev/null @@ -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" diff --git a/rust/initrd-stub/Cargo.toml b/rust/initrd-stub/Cargo.toml deleted file mode 100644 index a324f85..0000000 --- a/rust/initrd-stub/Cargo.toml +++ /dev/null @@ -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 diff --git a/rust/initrd-stub/src/main.rs b/rust/initrd-stub/src/main.rs deleted file mode 100644 index 27f639d..0000000 --- a/rust/initrd-stub/src/main.rs +++ /dev/null @@ -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) -> Status { - Status::UNSUPPORTED -} diff --git a/rust/lanzaboote/Cargo.lock b/rust/lanzaboote/Cargo.lock index 5cd2e80..5d8413c 100644 --- a/rust/lanzaboote/Cargo.lock +++ b/rust/lanzaboote/Cargo.lock @@ -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", diff --git a/rust/lanzaboote/Cargo.toml b/rust/lanzaboote/Cargo.toml index 0acb79c..06f4aaf 100644 --- a/rust/lanzaboote/Cargo.toml +++ b/rust/lanzaboote/Cargo.toml @@ -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 diff --git a/rust/lanzaboote/src/linux_loader.rs b/rust/lanzaboote/src/linux_loader.rs index 46857ad..71286ac 100644 --- a/rust/lanzaboote/src/linux_loader.rs +++ b/rust/lanzaboote/src/linux_loader.rs @@ -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, + initrd_data: Vec, } 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> { - 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 { - initrd_verify(boot_services, &mut file)?; - - let range = initrd_location(&mut file)?; + pub fn new(boot_services: &BootServices, handle: Handle, initrd_data: Vec) -> Result { 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 diff --git a/rust/lanzaboote/src/main.rs b/rust/lanzaboote/src/main.rs index f172c49..15a1a06 100644 --- a/rust/lanzaboote/src/main.rs +++ b/rust/lanzaboote/src/main.rs @@ -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 { + 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 { + 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) -> 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) -> 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) -> 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) -> 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() diff --git a/rust/lanzatool/Cargo.lock b/rust/lanzatool/Cargo.lock index 1c9e8b0..4ef3c12 100644 --- a/rust/lanzatool/Cargo.lock +++ b/rust/lanzatool/Cargo.lock @@ -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" diff --git a/rust/lanzatool/Cargo.toml b/rust/lanzatool/Cargo.toml index 6ec5d7f..5ca7a67 100644 --- a/rust/lanzatool/Cargo.toml +++ b/rust/lanzatool/Cargo.toml @@ -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" diff --git a/rust/lanzatool/src/cli.rs b/rust/lanzatool/src/cli.rs index 5d8946e..ec51cf4 100644 --- a/rust/lanzatool/src/cli.rs +++ b/rust/lanzatool/src/cli.rs @@ -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, diff --git a/rust/lanzatool/src/install.rs b/rust/lanzatool/src/install.rs index 64bba50..91971ca 100644 --- a/rust/lanzatool/src/install.rs +++ b/rust/lanzatool/src/install.rs @@ -14,7 +14,6 @@ use crate::signature::KeyPair; pub struct Installer { lanzaboote_stub: PathBuf, - initrd_stub: PathBuf, key_pair: KeyPair, _pki_bundle: Option, _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, _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, diff --git a/rust/lanzatool/src/pe.rs b/rust/lanzatool/src/pe.rs index fa74696..a91851c 100644 --- a/rust/lanzatool/src/pe.rs +++ b/rust/lanzatool/src/pe.rs @@ -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 { - // 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 { - 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 { + 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
, ) -> Result { - 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, 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 { + 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 {