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

feat: minimal poc for TPM measurements à la sd-stub #167

Merged
merged 2 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 140 additions & 38 deletions nix/tests/lanzaboote.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,127 @@
}:

let
inherit (pkgs) lib;
inherit (pkgs) lib system;

mkSecureBootTest = { name, machine ? { }, useSecureBoot ? true, testScript }: pkgs.nixosTest {
inherit name testScript;
nodes.machine = { lib, ... }: {
imports = [
lanzabooteModule
machine
];
mkSecureBootTest = { name, machine ? { }, useSecureBoot ? true, useTPM2 ? false, readEfiVariables ? false, testScript }:
let
tpmSocketPath = "/tmp/swtpm-sock";
tpmDeviceModels = {
x86_64-linux = "tpm-tis";
aarch64-linux = "tpm-tis-device";
};
# Should go to nixpkgs.
efiVariablesHelpers = ''
import struct

SD_LOADER_GUID = "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
def read_raw_variable(var: str) -> bytes:
attr_var = machine.succeed(f"cat /sys/firmware/efi/efivars/{var}-{SD_LOADER_GUID}").encode('raw_unicode_escape')
_ = attr_var[:4] # First 4 bytes are attributes according to https://www.kernel.org/doc/html/latest/filesystems/efivarfs.html
value = attr_var[4:]
return value
def read_string_variable(var: str, encoding='utf-16-le') -> str:
return read_raw_variable(var).decode(encoding).rstrip('\x00')
# By default, it will read a 4 byte value, read `struct` docs to change the format.
def assert_variable_uint(var: str, expected: int, format: str = 'I'):
with subtest(f"Is `{var}` set to {expected} (uint)"):
value, = struct.unpack(f'<{format}', read_raw_variable(var))
assert value == expected, f"Unexpected variable value in `{var}`, expected: `{expected}`, actual: `{value}`"
def assert_variable_string(var: str, expected: str, encoding='utf-16-le'):
with subtest(f"Is `{var}` correctly set"):
value = read_string_variable(var, encoding)
assert value == expected, f"Unexpected variable value in `{var}`, expected: `{expected.encode(encoding)!r}`, actual: `{value.encode(encoding)!r}`"
def assert_variable_string_contains(var: str, expected_substring: str):
with subtest(f"Do `{var}` contain expected substrings"):
value = read_string_variable(var).strip()
assert expected_substring in value, f"Did not find expected substring in `{var}`, expected substring: `{expected_substring}`, actual value: `{value}`"
'';
tpm2Initialization = ''
import subprocess
from tempfile import TemporaryDirectory

# From systemd-initrd-luks-tpm2.nix
class Tpm:
def __init__(self):
self.state_dir = TemporaryDirectory()
self.start()

def start(self):
self.proc = subprocess.Popen(["${pkgs.swtpm}/bin/swtpm",
"socket",
"--tpmstate", f"dir={self.state_dir.name}",
"--ctrl", "type=unixio,path=${tpmSocketPath}",
"--tpm2",
])

# Check whether starting swtpm failed
try:
exit_code = self.proc.wait(timeout=0.2)
if exit_code is not None and exit_code != 0:
raise Exception("failed to start swtpm")
except subprocess.TimeoutExpired:
pass

"""Check whether the swtpm process exited due to an error"""
def check(self):
exit_code = self.proc.poll()
if exit_code is not None and exit_code != 0:
raise Exception("swtpm process died")

tpm = Tpm()

@polling_condition
def swtpm_running():
tpm.check()
'';
in
pkgs.nixosTest {
inherit name;

virtualisation = {
useBootLoader = true;
useEFIBoot = true;
testScript = ''
${lib.optionalString useTPM2 tpm2Initialization}
${lib.optionalString readEfiVariables efiVariablesHelpers}
${testScript}
'';

inherit useSecureBoot;
};

boot.loader.efi = {
canTouchEfiVariables = true;
};
boot.lanzaboote = {
enable = true;
enrollKeys = lib.mkDefault true;
pkiBundle = ./fixtures/uefi-keys;
nodes.machine = { lib, ... }: {
imports = [
lanzabooteModule
machine
];

virtualisation = {
useBootLoader = true;
useEFIBoot = true;

efi.OVMF = pkgs.OVMF.override {
secureBoot = useSecureBoot;
tpmSupport = useTPM2; # This is needed otherwise OVMF won't initialize the TPM2 protocol.
};


qemu.options = lib.mkIf useTPM2 [
"-chardev socket,id=chrtpm,path=${tpmSocketPath}"
"-tpmdev emulator,id=tpm_dev_0,chardev=chrtpm"
"-device ${tpmDeviceModels.${system}},tpmdev=tpm_dev_0"
];

inherit useSecureBoot;
};

boot.initrd.availableKernelModules = lib.mkIf useTPM2 [ "tpm_tis" ];

boot.loader.efi = {
canTouchEfiVariables = true;
};
boot.lanzaboote = {
enable = true;
enrollKeys = lib.mkDefault true;
pkiBundle = ./fixtures/uefi-keys;
};
};
};
};

# Execute a boot test that has an intentionally broken secure boot
# chain. This test is expected to fail with Secure Boot and should
Expand Down Expand Up @@ -271,9 +365,8 @@ in
export-efi-variables = mkSecureBootTest {
name = "lanzaboote-exports-efi-variables";
machine.environment.systemPackages = [ pkgs.efibootmgr ];
readEfiVariables = true;
testScript = ''
import struct

# We will choose to boot directly on the stub.
# To perform this trick, we will boot first with systemd-boot.
# Then, we will add a new boot entry in EFI with higher priority
Expand Down Expand Up @@ -301,7 +394,6 @@ in
"test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f && false || true"
)

SD_LOADER_GUID = "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
expected_variables = ["LoaderDevicePartUUID",
"LoaderImageIdentifier",
"LoaderFirmwareInfo",
Expand All @@ -310,20 +402,6 @@ in
"StubFeatures"
]

def read_raw_variable(var: str) -> bytes:
attr_var = machine.succeed(f"cat /sys/firmware/efi/efivars/{var}-{SD_LOADER_GUID}").encode('raw_unicode_escape')
return attr_var[4:] # First 4 bytes are attributes according to https://www.kernel.org/doc/html/latest/filesystems/efivarfs.html
def read_string_variable(var: str, encoding='utf-16-le') -> str:
return read_raw_variable(var).decode(encoding).rstrip('\x00')
def assert_variable_string(var: str, expected: str, encoding='utf-16-le'):
with subtest(f"Is `{var}` correctly set"):
value = read_string_variable(var, encoding)
assert value == expected, f"Unexpected variable value in `{var}`, expected: `{expected.encode(encoding)!r}`, actual: `{value.encode(encoding)!r}`"
def assert_variable_string_contains(var: str, expected_substring: str):
with subtest(f"Do `{var}` contain expected substrings"):
value = read_string_variable(var).strip()
assert expected_substring in value, f"Did not find expected substring in `{var}`, expected substring: `{expected_substring}`, actual value: `{value}`"

# Debug all systemd loader specification GUID EFI variables loaded by the current environment.
print(machine.succeed(f"ls /sys/firmware/efi/efivars/*-{SD_LOADER_GUID}"))
with subtest("Check if supported variables are exported"):
Expand All @@ -344,4 +422,28 @@ in
assert struct.unpack('<Q', read_raw_variable("StubFeatures")) != 0
'';
};

tpm2-export-efi-variables = mkSecureBootTest {
name = "lanzaboote-tpm2-exports-efi-variables";
useTPM2 = true;
readEfiVariables = true;
testScript = ''
machine.start()

# TODO: the other variables are not yet supported.
expected_variables = [
"StubPcrKernelImage"
]

# Debug all systemd loader specification GUID EFI variables loaded by the current environment.
print(machine.succeed(f"ls /sys/firmware/efi/efivars/*-{SD_LOADER_GUID}"))
with subtest("Check if supported variables are exported"):
for expected_var in expected_variables:
machine.succeed(f"test -e /sys/firmware/efi/efivars/{expected_var}-{SD_LOADER_GUID}")

# "Static" parts of the UKI is measured in PCR11
assert_variable_uint("StubPcrKernelImage", 11)
'';
};

}
7 changes: 7 additions & 0 deletions rust/stub/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions rust/stub/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 23 additions & 2 deletions rust/stub/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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
}
}
RaitoBezarius marked this conversation as resolved.
Show resolved Hide resolved

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");
Expand Down
65 changes: 65 additions & 0 deletions rust/stub/src/measure.rs
Original file line number Diff line number Diff line change
@@ -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() };
RaitoBezarius marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Loading