tests: support TPM2 + SecureBoot tests
Test that our measurements exposes a TPM PCR index in the userspace through efivarfs.
This commit is contained in:
parent
606b9e8bab
commit
f603e0c134
|
@ -3,10 +3,90 @@
|
||||||
}:
|
}:
|
||||||
|
|
||||||
let
|
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, ... }: {
|
nodes.machine = { lib, ... }: {
|
||||||
imports = [
|
imports = [
|
||||||
lanzabooteModule
|
lanzabooteModule
|
||||||
|
@ -17,9 +97,23 @@ let
|
||||||
useBootLoader = true;
|
useBootLoader = true;
|
||||||
useEFIBoot = 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;
|
inherit useSecureBoot;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = lib.mkIf useTPM2 [ "tpm_tis" ];
|
||||||
|
|
||||||
boot.loader.efi = {
|
boot.loader.efi = {
|
||||||
canTouchEfiVariables = true;
|
canTouchEfiVariables = true;
|
||||||
};
|
};
|
||||||
|
@ -271,9 +365,8 @@ in
|
||||||
export-efi-variables = mkSecureBootTest {
|
export-efi-variables = mkSecureBootTest {
|
||||||
name = "lanzaboote-exports-efi-variables";
|
name = "lanzaboote-exports-efi-variables";
|
||||||
machine.environment.systemPackages = [ pkgs.efibootmgr ];
|
machine.environment.systemPackages = [ pkgs.efibootmgr ];
|
||||||
|
readEfiVariables = true;
|
||||||
testScript = ''
|
testScript = ''
|
||||||
import struct
|
|
||||||
|
|
||||||
# We will choose to boot directly on the stub.
|
# We will choose to boot directly on the stub.
|
||||||
# To perform this trick, we will boot first with systemd-boot.
|
# To perform this trick, we will boot first with systemd-boot.
|
||||||
# Then, we will add a new boot entry in EFI with higher priority
|
# 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"
|
"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",
|
expected_variables = ["LoaderDevicePartUUID",
|
||||||
"LoaderImageIdentifier",
|
"LoaderImageIdentifier",
|
||||||
"LoaderFirmwareInfo",
|
"LoaderFirmwareInfo",
|
||||||
|
@ -310,20 +402,6 @@ in
|
||||||
"StubFeatures"
|
"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.
|
# 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}"))
|
print(machine.succeed(f"ls /sys/firmware/efi/efivars/*-{SD_LOADER_GUID}"))
|
||||||
with subtest("Check if supported variables are exported"):
|
with subtest("Check if supported variables are exported"):
|
||||||
|
@ -344,4 +422,28 @@ in
|
||||||
assert struct.unpack('<Q', read_raw_variable("StubFeatures")) != 0
|
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)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue