stub(tpm): Measure "UKI" (i.e. all unified sections in our stub)

This commit is contained in:
Raito Bezarius 2023-04-29 22:09:08 +02:00
parent 354ec6f451
commit 606b9e8bab
7 changed files with 258 additions and 11 deletions

7
rust/stub/Cargo.lock generated
View File

@ -92,6 +92,7 @@ dependencies = [
"bitflags 2.2.1",
"goblin",
"log",
"sha1_smol",
"sha2",
"uefi",
"uefi-services",
@ -176,6 +177,12 @@ dependencies = [
"syn",
]
[[package]]
name = "sha1_smol"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "sha2"
version = "0.10.6"

View File

@ -17,6 +17,8 @@ log = { version = "0.4.17", default-features = false, features = [ "max_level_in
# Use software implementation because the UEFI target seems to need it.
sha2 = { version = "0.10.6", default-features = false, features = ["force-soft"] }
# SHA1 for TPM TCG interface version 1.
sha1_smol = "1.0.0"
[profile.release]
opt-level = "s"

View File

@ -6,16 +6,21 @@ extern crate alloc;
mod efivars;
mod linux_loader;
mod measure;
mod pe_loader;
mod pe_section;
mod tpm;
mod uefi_helpers;
mod unified_sections;
use alloc::vec::Vec;
use efivars::{export_efi_variables, get_loader_features, EfiLoaderFeatures};
use log::{debug, info, warn};
use log::{info, warn};
use measure::measure_image;
use pe_loader::Image;
use pe_section::{pe_section, pe_section_as_string};
use sha2::{Digest, Sha256};
use tpm::tpm_available;
use uefi::{
prelude::*,
proto::{
@ -238,10 +243,26 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
warn!("Hash mismatch for initrd!");
}
if tpm_available(system_table.boot_services()) {
info!("TPM available, will proceed to measurements.");
unsafe {
// Iterate over unified sections and measure them
// 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,
booted_image_file(system_table.boot_services()).unwrap(),
);
// TODO: Measure kernel parameters
// TODO: Measure sysexts
}
}
if let Ok(features) = get_loader_features(system_table.runtime_services()) {
if !features.contains(EfiLoaderFeatures::RandomSeed) {
// FIXME: process random seed then on the disk.
debug!("Random seed is available, but lanzaboote does not support it yet.");
info!("Random seed is available, but lanzaboote does not support it yet.");
}
}
export_efi_variables(&system_table).expect("Failed to export stub EFI variables");

65
rust/stub/src/measure.rs Normal file
View File

@ -0,0 +1,65 @@
use log::info;
use uefi::{
cstr16,
proto::tcg::PcrIndex,
table::{runtime::VariableAttributes, Boot, SystemTable},
};
use crate::{
efivars::BOOT_LOADER_VENDOR_UUID, pe_section::pe_section_data, tpm::tpm_log_event_ascii,
uefi_helpers::PeInMemory, unified_sections::UnifiedSection,
};
const TPM_PCR_INDEX_KERNEL_IMAGE: PcrIndex = PcrIndex(11);
pub unsafe fn measure_image(
system_table: &SystemTable<Boot>,
image: PeInMemory,
) -> uefi::Result<u32> {
let runtime_services = system_table.runtime_services();
let boot_services = system_table.boot_services();
// SAFETY: We get a slice that represents our currently running
// image and then parse the PE data structures from it. This is
// safe, because we don't touch any data in the data sections that
// might conceivably change while we look at the slice.
// (data sections := all unified sections that can be measured.)
let pe_binary = unsafe { image.as_slice() };
let pe = goblin::pe::PE::parse(pe_binary).map_err(|_err| uefi::Status::LOAD_ERROR)?;
let mut measurements = 0;
for section in pe.sections {
let section_name = section.name().map_err(|_err| uefi::Status::UNSUPPORTED)?;
if let Ok(unified_section) = UnifiedSection::try_from(section_name) {
// UNSTABLE: && in the previous if is an unstable feature
// https://github.com/rust-lang/rust/issues/53667
if unified_section.should_be_measured() {
// Here, perform the TPM log event in ASCII.
if let Some(data) = pe_section_data(pe_binary, &section) {
info!("Measuring section `{}`...", section_name);
if tpm_log_event_ascii(
boot_services,
TPM_PCR_INDEX_KERNEL_IMAGE,
data,
section_name,
)? {
measurements += 1;
}
}
}
}
}
if measurements > 0 {
// If we did some measurements, expose a variable encoding the PCR where
// we have done the measurements.
runtime_services.set_variable(
cstr16!("StubPcrKernelImage"),
&BOOT_LOADER_VENDOR_UUID,
VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS,
&TPM_PCR_INDEX_KERNEL_IMAGE.0.to_le_bytes(),
)?;
}
Ok(measurements)
}

View File

@ -5,8 +5,21 @@
#![allow(clippy::bind_instead_of_map)]
use alloc::{borrow::ToOwned, string::String};
use goblin::pe::section_table::SectionTable;
/// Extracts the data of a section of a loaded PE file.
/// Extracts the data of a section in a loaded PE file
/// based on the section table.
pub fn pe_section_data<'a>(pe_data: &'a [u8], section: &SectionTable) -> Option<&'a [u8]> {
let section_start: usize = section.virtual_address.try_into().ok()?;
assert!(section.virtual_size <= section.size_of_raw_data);
let section_end: usize = section_start + usize::try_from(section.virtual_size).ok()?;
Some(&pe_data[section_start..section_end])
}
/// Extracts the data of a section of a loaded PE file
/// based on the section name.
pub fn pe_section<'a>(pe_data: &'a [u8], section_name: &str) -> Option<&'a [u8]> {
let pe_binary = goblin::pe::PE::parse(pe_data).ok()?;
@ -14,14 +27,7 @@ pub fn pe_section<'a>(pe_data: &'a [u8], section_name: &str) -> Option<&'a [u8]>
.sections
.iter()
.find(|s| s.name().map(|n| n == section_name).unwrap_or(false))
.and_then(|s| {
let section_start: usize = s.virtual_address.try_into().ok()?;
assert!(s.virtual_size <= s.size_of_raw_data);
let section_end: usize = section_start + usize::try_from(s.virtual_size).ok()?;
Some(&pe_data[section_start..section_end])
})
.and_then(|s| pe_section_data(pe_data, s))
}
/// Extracts the data of a section of a loaded PE image and returns it as a string.

106
rust/stub/src/tpm.rs Normal file
View File

@ -0,0 +1,106 @@
use alloc::vec;
use core::mem::{self, MaybeUninit};
use log::warn;
use uefi::{
prelude::BootServices,
proto::tcg::{
v1::{self, Sha1Digest},
v2, EventType, PcrIndex,
},
table::boot::ScopedProtocol,
};
fn open_capable_tpm2(boot_services: &BootServices) -> uefi::Result<ScopedProtocol<v2::Tcg>> {
let tpm_handle = boot_services.get_handle_for_protocol::<v2::Tcg>()?;
let mut tpm_protocol = boot_services.open_protocol_exclusive::<v2::Tcg>(tpm_handle)?;
let capabilities = tpm_protocol.get_capability()?;
/*
* Here's systemd-stub perform a cast to EFI_TCG_BOOT_SERVICE_CAPABILITY
* indicating there could be some quirks to workaround.
* It should probably go to uefi-rs?
if capabilities.structure_version.major == 1 && capabilities.structure_version.minor == 0 {
}*/
if !capabilities.tpm_present() {
warn!("Capability `TPM present` is not there for the existing TPM TCGv2 protocol");
return Err(uefi::Status::UNSUPPORTED.into());
}
Ok(tpm_protocol)
}
fn open_capable_tpm1(boot_services: &BootServices) -> uefi::Result<ScopedProtocol<v1::Tcg>> {
let tpm_handle = boot_services.get_handle_for_protocol::<v1::Tcg>()?;
let mut tpm_protocol = boot_services.open_protocol_exclusive::<v1::Tcg>(tpm_handle)?;
let status_check = tpm_protocol.status_check()?;
if status_check.protocol_capability.tpm_deactivated()
|| !status_check.protocol_capability.tpm_present()
{
warn!("Capability `TPM present` is not there or `TPM deactivated` is there for the existing TPM TCGv1 protocol");
return Err(uefi::Status::UNSUPPORTED.into());
}
Ok(tpm_protocol)
}
pub fn tpm_available(boot_services: &BootServices) -> bool {
open_capable_tpm2(boot_services).is_ok() || open_capable_tpm1(boot_services).is_ok()
}
/// Log an event in the TPM with `buffer` as data.
/// Returns a boolean whether the measurement has been done or not in case of success.
pub fn tpm_log_event_ascii(
boot_services: &BootServices,
pcr_index: PcrIndex,
buffer: &[u8],
description: &str,
) -> uefi::Result<bool> {
if pcr_index.0 == u32::MAX {
return Ok(false);
}
if let Ok(mut tpm2) = open_capable_tpm2(boot_services) {
let required_size = mem::size_of::<u32>()
// EventHeader is private…
+ mem::size_of::<u32>() + mem::size_of::<u16>() + mem::size_of::<PcrIndex>() + mem::size_of::<EventType>()
+ description.len();
let mut event_buffer = vec![MaybeUninit::<u8>::uninit(); required_size];
let event = v2::PcrEventInputs::new_in_buffer(
event_buffer.as_mut_slice(),
pcr_index,
EventType::IPL,
description.as_bytes(),
)?;
// FIXME: what do we want as flags here?
tpm2.hash_log_extend_event(Default::default(), buffer, event)?;
} else if let Ok(mut tpm1) = open_capable_tpm1(boot_services) {
let required_size = mem::size_of::<PcrIndex>()
+ mem::size_of::<EventType>()
+ mem::size_of::<Sha1Digest>()
+ mem::size_of::<u32>()
+ description.len();
let mut event_buffer = vec![MaybeUninit::<u8>::uninit(); required_size];
// Compute sha1 of the event data
let mut m = sha1_smol::Sha1::new();
m.update(description.as_bytes());
let event = v1::PcrEvent::new_in_buffer(
event_buffer.as_mut_slice(),
pcr_index,
EventType::IPL,
m.digest().bytes(),
description.as_bytes(),
)?;
tpm1.hash_log_extend_event(event, Some(buffer))?;
}
Ok(true)
}

View File

@ -0,0 +1,40 @@
/// List of PE sections that have a special meaning with respect to
/// UKI specification.
/// This is the canonical order in which they are measured into TPM
/// PCR 11.
/// !!! DO NOT REORDER !!!
#[repr(u8)]
pub enum UnifiedSection {
Linux = 0,
OsRel = 1,
CmdLine = 2,
Initrd = 3,
Splash = 4,
Dtb = 5,
PcrSig = 6,
PcrPkey = 7,
}
impl TryFrom<&str> for UnifiedSection {
type Error = uefi::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
".linux" => Self::Linux,
".osrel" => Self::OsRel,
".cmdline" => Self::CmdLine,
".initrd" => Self::Initrd,
".splash" => Self::Splash,
".dtb" => Self::Dtb,
".pcrsig" => Self::PcrSig,
".pcrpkey" => Self::PcrPkey,
_ => return Err(uefi::Status::INVALID_PARAMETER.into()),
})
}
}
impl UnifiedSection {
/// Whether this section should be measured into TPM.
pub fn should_be_measured(&self) -> bool {
!matches!(self, UnifiedSection::PcrSig)
}
}