-
-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Further improvements to TPM measurements #361
base: master
Are you sure you want to change the base?
Conversation
9927006
to
8e46bea
Compare
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. |
8e46bea
to
2c4c991
Compare
// | ||
// As per reference: | ||
// "Measured hash covers the PE section name in ASCII (including a trailing NUL byte!)." | ||
let section_name_cs_utf8 = CString::new(section_name).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this unwrap safe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this might actually be an oversight on my part, thanks for catching it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cleaned up the relevant flow by moving some section name logic into UnifiedSection
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. |
2c4c991
to
a47b99d
Compare
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. |
5470fd3
to
fffc7d5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, I would only ask for a NixOS test to ensure that we don't regress this behavior, but I don't know what would be a good testing strategy here, so it's nit.
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. |
systemd-stub measures both the section name and the section contents. Furthermore it measures sections in a predefined order.
fffc7d5
to
0949c32
Compare
Does this mean that we need to put the hash in the main sections instead of the path? i.e. swap |
let Some(data) = pe_section_data(pe_binary, section) else { | ||
continue; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a case where we should continue? Or should we just fail?
let Some(data) = pe_section_data(pe_binary, section) else { | |
continue; | |
}; | |
let data = pe_section_data(pe_binary, section).with_context(|| "Failed to extract section: {section_name}")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If some section is not available for measurement, we should probably continue until we actually execute things and this is where things can ultimately crash.
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 <[email protected]>
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 <[email protected]>
---
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? |
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 |
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. |
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.