Open hesiod opened 5 months ago
I originally wanted to add some more improvements to this PR, but I think they'll take some more time and the fix in this PR is kind of self contained, so I'm going to mark it as ready.
From the Rust side, this looks good to me. I've commented some nits, feel free to ignore those. This also does what the comments claim.
@RaitoBezarius If you have a second, can you double-check that this makes sense from a high-level perspective? If so, I'd merge it.
I just realized there might be a small unhandled issue: Some sections are allowed to appear multiple times, but with this PR only one section would be measured. This is not terribly important for NixOS atm, but perhaps there's a simple solution.
I just realized there might be a small unhandled issue: Some sections are allowed to appear multiple times, but with this PR only one section would be measured. This is not terribly important for NixOS atm, but perhaps there's a simple solution.
Great point!
This sounds like a security issue, because the same measurements could lead to different running code. Is the simple solution to bail out when there are duplicated sections? Or is there another quick solution?
I reread that PR and this is what we need to support pcrlock.
so unless I'm mistaken we actually measure the kernel/initrd file paths and not their contents
Does this mean that we need to put the hash in the main sections instead of the path? i.e. swap .linux
and .linuxh
?
so unless I'm mistaken we actually measure the kernel/initrd file paths and not their contents
Does this mean that we need to put the hash in the main sections instead of the path? i.e. swap
.linux
and.linuxh
?
Right. Or, we could just open and measure the contents for real (?), I think we can just be isomorphic to what systemd-stub does for real without too much cost.
From b9fffb780b45e584c534242f1730d2eca457438a Mon Sep 17 00:00:00 2001
From: Raito Bezarius <masterancpp@gmail.com>
Date: Mon, 14 Oct 2024 19:15:04 +0200
Subject: [PATCH] fix(stub): read the actual section data for measurements
This enable us to match systemd-stub measurement behavior instead of
measuring filenames.
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
---
rust/uefi/linux-bootloader/src/measure.rs | 30 ++++++++++++++++++--
rust/uefi/linux-bootloader/src/pe_section.rs | 7 +++++
rust/uefi/stub/src/common.rs | 9 ------
rust/uefi/stub/src/main.rs | 2 +-
rust/uefi/stub/src/thin.rs | 4 +--
5 files changed, 37 insertions(+), 15 deletions(-)
diff --git a/rust/uefi/linux-bootloader/src/measure.rs b/rust/uefi/linux-bootloader/src/measure.rs
index d12c1c0..b8a4b87 100644
--- a/rust/uefi/linux-bootloader/src/measure.rs
+++ b/rust/uefi/linux-bootloader/src/measure.rs
@@ -10,7 +10,7 @@ use uefi::{
use crate::{
companions::{CompanionInitrd, CompanionInitrdType},
efivars::BOOT_LOADER_VENDOR_UUID,
- pe_section::pe_section_data,
+ pe_section::{extract_string, pe_section_data},
tpm::tpm_log_event_ascii,
uefi_helpers::PeInMemory,
unified_sections::UnifiedSection,
@@ -25,7 +25,11 @@ const TPM_PCR_INDEX_KERNEL_CONFIG: PcrIndex = PcrIndex(12);
/// This is where we extend the initrd sysext images into which we pass to the booted kernel
const TPM_PCR_INDEX_SYSEXTS: PcrIndex = PcrIndex(13);
-pub fn measure_image(system_table: &SystemTable<Boot>, image: &PeInMemory) -> uefi::Result<u32> {
+pub fn measure_image(
+ handle: uefi::Handle,
+ system_table: &SystemTable<Boot>,
+ image: &PeInMemory,
+) -> uefi::Result<u32> {
let runtime_services = system_table.runtime_services();
let boot_services = system_table.boot_services();
@@ -103,10 +107,30 @@ pub fn measure_image(system_table: &SystemTable<Boot>, image: &PeInMemory) -> ue
continue;
};
+ let section_data = match unified_section {
+ // The PE data in this case is the filename.
+ UnifiedSection::Linux | UnifiedSection::Initrd => {
+ let filename = extract_string(pe_binary, section_name)?;
+
+ let file_system = system_table.boot_services().get_image_file_system(handle)?;
+
+ let mut file_system = uefi::fs::FileSystem::new(file_system);
+ file_system
+ .read(&*filename)
+ .map_err(|fs_error| match fs_error {
+ uefi::fs::Error::Io(_) => uefi::Status::LOAD_ERROR,
+ uefi::fs::Error::Path(_) => uefi::Status::INVALID_PARAMETER,
+ uefi::fs::Error::Utf8Encoding(_) => uefi::Status::INVALID_PARAMETER,
+ })?
+ }
+ // .cmdline is already baked as it should be.
+ _ => data.to_vec(),
+ };
+
if tpm_log_event_ascii(
boot_services,
TPM_PCR_INDEX_KERNEL_IMAGE,
- data,
+ §ion_data,
section_name,
)? {
measurements += 1;
diff --git a/rust/uefi/linux-bootloader/src/pe_section.rs b/rust/uefi/linux-bootloader/src/pe_section.rs
index be23142..5caa934 100644
--- a/rust/uefi/linux-bootloader/src/pe_section.rs
+++ b/rust/uefi/linux-bootloader/src/pe_section.rs
@@ -34,3 +34,10 @@ pub fn pe_section<'a>(pe_data: &'a [u8], section_name: &str) -> Option<&'a [u8]>
pub fn pe_section_as_string<'a>(pe_data: &'a [u8], section_name: &str) -> Option<String> {
pe_section(pe_data, section_name).map(|data| core::str::from_utf8(data).unwrap().to_owned())
}
+
+/// Extract a string, stored as UTF-8, from a PE section.
+pub fn extract_string(pe_data: &[u8], section: &str) -> uefi::Result<uefi::CString16> {
+ let string = pe_section_as_string(pe_data, section).ok_or(uefi::Status::INVALID_PARAMETER)?;
+
+ Ok(uefi::CString16::try_from(string.as_str()).map_err(|_| uefi::Status::INVALID_PARAMETER)?)
+}
diff --git a/rust/uefi/stub/src/common.rs b/rust/uefi/stub/src/common.rs
index e8fe20c..6e83339 100644
--- a/rust/uefi/stub/src/common.rs
+++ b/rust/uefi/stub/src/common.rs
@@ -2,19 +2,10 @@ use alloc::vec::Vec;
use log::warn;
use uefi::{
guid, prelude::*, proto::loaded_image::LoadedImage, table::runtime::VariableVendor, CStr16,
- CString16, Result,
};
use linux_bootloader::linux_loader::InitrdLoader;
use linux_bootloader::pe_loader::Image;
-use linux_bootloader::pe_section::pe_section_as_string;
-
-/// Extract a string, stored as UTF-8, from a PE section.
-pub fn extract_string(pe_data: &[u8], section: &str) -> Result<CString16> {
- let string = pe_section_as_string(pe_data, section).ok_or(Status::INVALID_PARAMETER)?;
-
- Ok(CString16::try_from(string.as_str()).map_err(|_| Status::INVALID_PARAMETER)?)
-}
/// Obtain the kernel command line that should be used for booting.
///
diff --git a/rust/uefi/stub/src/main.rs b/rust/uefi/stub/src/main.rs
index 1de559b..126f7a7 100644
--- a/rust/uefi/stub/src/main.rs
+++ b/rust/uefi/stub/src/main.rs
@@ -60,7 +60,7 @@ fn main(handle: Handle, system_table: SystemTable<Boot>) -> Status {
// For now, ignore failures during measurements.
// TODO: in the future, devise a threat model where this can fail
// and ensure this hard-fail correctly.
- let _ = measure_image(&system_table, &pe_in_memory);
+ let _ = measure_image(handle, &system_table, &pe_in_memory);
}
if let Ok(features) = get_loader_features(system_table.runtime_services()) {
diff --git a/rust/uefi/stub/src/thin.rs b/rust/uefi/stub/src/thin.rs
index d202aa7..5c8aafe 100644
--- a/rust/uefi/stub/src/thin.rs
+++ b/rust/uefi/stub/src/thin.rs
@@ -4,8 +4,8 @@ use log::{error, warn};
use sha2::{Digest, Sha256};
use uefi::{fs::FileSystem, prelude::*, CString16, Result};
-use crate::common::{boot_linux_unchecked, extract_string, get_cmdline, get_secure_boot_status};
-use linux_bootloader::pe_section::pe_section;
+use crate::common::{boot_linux_unchecked, get_cmdline, get_secure_boot_status};
+use linux_bootloader::pe_section::{extract_string, pe_section};
use linux_bootloader::uefi_helpers::booted_image_file;
type Hash = sha2::digest::Output<Sha256>;
--
2.46.0
Should do the job, I guess? @hesiod if you want I can push on your PR and complete it, just hit me up, otherwise I will open a new one.
@RaitoBezarius I think it might be a slight problem if lanzaboote measures something different than the section contents. There are other tools that will be making predictions based on the section contents, are there not? Won't pcrlock do that, for instance?
@RaitoBezarius I think it might be a slight problem if lanzaboote measures something different than the section contents. There are other tools that will be making predictions based on the section contents, are there not? Won't pcrlock do that, for instance?
Well, true but also pcrlock will be deceptive if we measure only the filename tbh and not the hash, we can fix that by providing our .pcrlock files to rectify pcrlock predictions.
@RaitoBezarius Sure but I figured we could swap the roles of the path and hash sections. i.e. Put the hash in .linux
and the path in a different section. Then the section would adequately represent the contents we intend to load.
@RaitoBezarius Sure but I figured we could swap the roles of the path and hash sections. i.e. Put the hash in
.linux
and the path in a different section. Then the section would adequately represent the contents we intend to load.
Yes but then any tool that expects a Linux kernel will find a hash of a Linux kernel, etc.
I would need more time to ponder the ecosystem consequences of this and we should probably loop in the systemd folks.
Yes but then any tool that expects a Linux kernel will find a hash of a Linux kernel, etc.
Well sure but it's already the case that it won't find a Linux kernel. At least when it's the hash, it's a representative measurement for the TPM2.
Yes but then any tool that expects a Linux kernel will find a hash of a Linux kernel, etc.
Well sure but it's already the case that it won't find a Linux kernel. At least when it's the hash, it's a representative measurement for the TPM2.
There's two dimensions:
lanzaboote has been a long-time offender of the UKI specification, whether we will continue to offend that specification mostly depends on whether we have time to remove the violations.
Measuring kernel hash or kernel filename doesn't change the fact that we are violating this specification and we should probably not let people do something useful with our sections for prediction and advise them to use our own tools to perform the right predictions or read documents so they understand what they are doing, IMHO.
Going from there, it's my belief that, at least, we should get the measurements on-par with what a normal UKI should give out on PCR11. In a hopeful future, we will remove the violations and these measurements will stay the same even if we change the underlying formats (e.g. UKI with no initrd, no command line and addons for the rest).
I have no strong opinion on that but this is how I would implement this personally, trying to remove more and more about our specifics even if we don't do it in one sweep.
I've been working towards getting systemd-pcrlock to work. This PR includes some further fixes, but some work remains.
There is an important caveat: While PCR 11 measurements line up with
systemd-pcrlock
's expectations, they don't really make sense at the moment since they measure the section contents of the PE image, so unless I'm mistaken we actually measure the kernel/initrd file paths and not their contents. This shouldn't be too complicated to implement, but probably needs some tinkering with the current architecture.I also noticed some race conditions in the NixOS test I introduced, but @nikstur was faster.