1import ./make-test-python.nix ({ lib, pkgs, system, ... }:
2
3let
4 tpmSocketPath = "/tmp/swtpm-sock";
5 tpmDeviceModels = {
6 x86_64-linux = "tpm-tis";
7 aarch64-linux = "tpm-tis-device";
8 };
9in
10
11{
12 name = "systemd-credentials-tpm2";
13
14 meta = {
15 maintainers = with pkgs.lib.maintainers; [ tmarkus ];
16 };
17
18 nodes.machine = { pkgs, ... }: {
19 virtualisation = {
20 qemu.options = [
21 "-chardev socket,id=chrtpm,path=${tpmSocketPath}"
22 "-tpmdev emulator,id=tpm_dev_0,chardev=chrtpm"
23 "-device ${tpmDeviceModels.${system}},tpmdev=tpm_dev_0"
24 ];
25 };
26
27 boot.initrd.availableKernelModules = [ "tpm_tis" ];
28
29 environment.systemPackages = with pkgs; [ diffutils ];
30 };
31
32 testScript = ''
33 import subprocess
34 from tempfile import TemporaryDirectory
35
36 # From systemd-initrd-luks-tpm2.nix
37 class Tpm:
38 def __init__(self):
39 self.state_dir = TemporaryDirectory()
40 self.start()
41
42 def start(self):
43 self.proc = subprocess.Popen(["${pkgs.swtpm}/bin/swtpm",
44 "socket",
45 "--tpmstate", f"dir={self.state_dir.name}",
46 "--ctrl", "type=unixio,path=${tpmSocketPath}",
47 "--tpm2",
48 ])
49
50 # Check whether starting swtpm failed
51 try:
52 exit_code = self.proc.wait(timeout=0.2)
53 if exit_code is not None and exit_code != 0:
54 raise Exception("failed to start swtpm")
55 except subprocess.TimeoutExpired:
56 pass
57
58 """Check whether the swtpm process exited due to an error"""
59 def check(self):
60 exit_code = self.proc.poll()
61 if exit_code is not None and exit_code != 0:
62 raise Exception("swtpm process died")
63
64 CRED_NAME = "testkey"
65 CRED_RAW_FILE = f"/root/{CRED_NAME}"
66 CRED_FILE = f"/root/{CRED_NAME}.cred"
67
68 def systemd_run(machine, cmd):
69 machine.log(f"Executing command (via systemd-run): \"{cmd}\"")
70
71 (status, out) = machine.execute( " ".join([
72 "systemd-run",
73 "--service-type=exec",
74 "--quiet",
75 "--wait",
76 "-E PATH=\"$PATH\"",
77 "-p StandardOutput=journal",
78 "-p StandardError=journal",
79 f"-p LoadCredentialEncrypted={CRED_NAME}:{CRED_FILE}",
80 f"$SHELL -c '{cmd}'"
81 ]) )
82
83 if status != 0:
84 raise Exception(f"systemd_run failed (status {status})")
85
86 machine.log("systemd-run finished successfully")
87
88 tpm = Tpm()
89
90 @polling_condition
91 def swtpm_running():
92 tpm.check()
93
94 machine.wait_for_unit("multi-user.target")
95
96 with subtest("Check whether TPM device exists"):
97 machine.succeed("test -e /dev/tpm0")
98 machine.succeed("test -e /dev/tpmrm0")
99
100 with subtest("Check whether systemd-creds detects TPM2 correctly"):
101 cmd = "systemd-creds has-tpm2"
102 machine.log(f"Running \"{cmd}\"")
103 (status, _) = machine.execute(cmd)
104
105 # Check exit code equals 0 or 1 (1 means firmware support is missing, which is OK here)
106 if status != 0 and status != 1:
107 raise Exception("systemd-creds failed to detect TPM2")
108
109 with subtest("Encrypt credential using systemd-creds"):
110 machine.succeed(f"dd if=/dev/urandom of={CRED_RAW_FILE} bs=1k count=16")
111 machine.succeed(f"systemd-creds --with-key=host+tpm2 encrypt --name=testkey {CRED_RAW_FILE} {CRED_FILE}")
112
113 with subtest("Write provided credential and check for equality"):
114 CRED_OUT_FILE = f"/root/{CRED_NAME}.out"
115 systemd_run(machine, f"systemd-creds cat testkey > {CRED_OUT_FILE}")
116 machine.succeed(f"cmp --silent -- {CRED_RAW_FILE} {CRED_OUT_FILE}")
117
118 with subtest("Check whether systemd service can see credential in systemd-creds list"):
119 systemd_run(machine, f"systemd-creds list | grep {CRED_NAME}")
120
121 with subtest("Check whether systemd service can access credential in $CREDENTIALS_DIRECTORY"):
122 systemd_run(machine, f"cmp --silent -- $CREDENTIALS_DIRECTORY/{CRED_NAME} {CRED_RAW_FILE}")
123 '';
124})