tests: support TPM2 + SecureBoot tests

Test that our measurements exposes a TPM PCR index in the userspace
through efivarfs.
This commit is contained in:
Raito Bezarius 2023-04-30 02:45:56 +02:00
parent 606b9e8bab
commit f603e0c134
1 changed files with 141 additions and 39 deletions

View File

@ -3,10 +3,90 @@
}:
let
inherit (pkgs) lib;
inherit (pkgs) lib system;
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;
testScript = ''
${lib.optionalString useTPM2 tpm2Initialization}
${lib.optionalString readEfiVariables efiVariablesHelpers}
${testScript}
'';
mkSecureBootTest = { name, machine ? { }, useSecureBoot ? true, testScript }: pkgs.nixosTest {
inherit name testScript;
nodes.machine = { lib, ... }: {
imports = [
lanzabooteModule
@ -17,9 +97,23 @@ let
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;
};
@ -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
@ -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",
@ -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"):
@ -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)
'';
};
}