Merge pull request #21 from nix-community/boot-file-integrity
Verify Kernel/Initrd Integrity using Blake3
This commit is contained in:
commit
06da27529f
33
flake.nix
33
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 = {
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[build]
|
||||
target = "x86_64-unknown-uefi"
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue