Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

hesiod
Copy link
Contributor

@hesiod hesiod commented Jun 12, 2024

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.

@hesiod hesiod force-pushed the measurement-improvements branch 2 times, most recently from 9927006 to 8e46bea Compare June 12, 2024 13:17
@hesiod
Copy link
Contributor Author

hesiod commented Jun 12, 2024

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.

@hesiod hesiod marked this pull request as ready for review June 12, 2024 13:18
//
// 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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this unwrap safe?

Copy link
Contributor Author

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!

Copy link
Contributor Author

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

@blitz
Copy link
Member

blitz commented Jun 19, 2024

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.

@hesiod
Copy link
Contributor Author

hesiod commented Jun 19, 2024

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.

@hesiod hesiod force-pushed the measurement-improvements branch 2 times, most recently from 5470fd3 to fffc7d5 Compare June 19, 2024 11:59
Copy link
Member

@RaitoBezarius RaitoBezarius left a 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.

@blitz
Copy link
Member

blitz commented Jun 20, 2024

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?

@RaitoBezarius
Copy link
Member

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.
@ElvishJerricco
Copy link

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?

Comment on lines +102 to +104
let Some(data) = pe_section_data(pe_binary, section) else {
continue;
};

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?

Suggested change
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}")?;

Copy link
Member

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.

@RaitoBezarius
Copy link
Member

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.

@RaitoBezarius
Copy link
Member

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,
+            &section_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.

@ElvishJerricco
Copy link

@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
Copy link
Member

@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.

@ElvishJerricco
Copy link

@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
Copy link
Member

@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.

@ElvishJerricco
Copy link

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.

@RaitoBezarius
Copy link
Member

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:

  • measurements
  • tools that manipulates unified sections UKIs

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants