Skip to main content

max / alloy

Give vmtest a machine with no keyboard, and stop it re-minting the TPM The USB gate's bench tests need hardware no machine in this tree can be. alloy-usb-gate suspends enforcement when the count of ID_INPUT_KEYBOARD devices reaches zero, and every machine here has an i8042 keyboard soldered to it that usbguard has no jurisdiction over, so the count never reaches zero on real hardware. q35 carries an i8042 property, so KEYBOARD=none is a machine with no keyboard at all and KEYBOARD=usb is one whose only keyboard is on an xHCI and can be pulled from the monitor with device_del kbd0. That is the second and third bench tests, neither of which was runnable before. Also stop the harness swapping the TPM out underneath an installed disk. swtpm_setup carries --overwrite and ran whenever the swtpm daemon was not up, rather than only on a first run, so a disk that unlocks from the TPM lost its sealed key and fell back to asking for a passphrase. It reports "Failed to unseal secret using TPM2: State not recoverable", which reads as a broken enrolment rather than as changed hardware. That matters more now: a machine with no keyboard cannot answer a passphrase prompt at all, so the TPM path is the only way an encrypted target boots in that mode.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01WFBzMprSmNCfvdj2cGZyka
Author: Max Johnson <me@maxj.phd> · 2026-09-08 19:33 UTC
Signed with PGP, not checked
Commit: 26f7d65897ef3d459535c807b5b07f30379f8dda
Parent: 05d3a28
2 files changed, +83 insertions, -3 deletions
@@ -47,6 +47,21 @@
47 47 TARGET_BUS=nvme ./run-vm.sh live # a target that reports `tran: nvme`
48 48 python3 install_preseeded.py # install from a medium with an answer sheet
49 49
50 + `KEYBOARD` decides what keyboards the machine has, and exists because the USB
51 + gate's bench tests need hardware no machine here can be. `usr/bin/alloy-usb-gate`
52 + suspends enforcement whenever the count of `ID_INPUT_KEYBOARD` devices reaches
53 + zero, and every physical machine in this tree has an i8042 keyboard soldered to
54 + it that USBGuard has no jurisdiction over, so the count never reaches zero on
55 + real hardware. `ps2` is q35's own i8042 controller and the default. `none` is
56 + `i8042=off`: no keyboard of any kind, which is the machine the gate is supposed
57 + to open for at boot. `usb` is `i8042=off` plus a USB keyboard on an xHCI
58 + controller, so the only keyboard is one USBGuard can take away; its `id=kbd0` is
59 + what `qmp.py cmd device_del '{"id": "kbd0"}'` unplugs, which is the third bench
60 + test and cannot be done from inside the guest.
61 +
62 + KEYBOARD=none ./run-vm.sh installed # zero keyboards
63 + KEYBOARD=usb ./run-vm.sh installed # one, and it is removable
64 +
50 65 `TARGET_BUS` decides how the target disk is attached, and it is not a
51 66 performance knob. A recipe whose disk rule is `single-internal-nvme` cannot be
52 67 exercised against a virtio disk, whose lsblk `tran` is null, and a rule that
@@ -114,6 +129,32 @@
114 129 then written once in its own syntax with nothing to quote through nushell. Use
115 130 `sh -c '...'` only when stdin is carrying something else.
116 131
132 + **`run-vm.sh` re-creates the software TPM whenever swtpm is not running, and
133 + that silently breaks an installed disk that unlocks with one.** The setup call
134 + carries `--overwrite`, and it fires whenever `state/tpm/swtpm-sock` is absent —
135 + which is every time the swtpm daemon has been stopped, not only on a first run.
136 + A new TPM has a new seed, so the sealed key in the LUKS header can no longer be
137 + loaded, and the guest asks for the passphrase instead. What it prints is
138 + `Failed to unseal secret using TPM2: State not recoverable` with
139 + `Esys_Load() ... ErrorCode (0x0000018b)` above it, in the journal of the boot
140 + that prompted. It reads like a broken enrolment rather than a harness that
141 + changed the hardware underneath it. Delete `state/tpm/` deliberately to simulate
142 + a cleared TPM; do not let it happen by accident.
143 +
144 + **Re-enrolling a TPM2 keyslot takes two commands, not one.**
145 + `systemd-cryptenroll --wipe-slot=tpm2 --tpm2-device=auto <dev>` answers
146 + `This PCR set is already enrolled, executing no operation` and changes nothing,
147 + because it compares against the existing enrolment before it wipes. Wipe and
148 + enrol separately, and read the output: the pair that works prints `Wiped slot N`
149 + and then `New TPM2 token enrolled as key slot N`.
150 +
151 + **A machine with `KEYBOARD=none` cannot answer a LUKS passphrase prompt.**
152 + There is no input device for `vm.py type` to reach, so an encrypted target is
153 + unreachable in that mode and the run stalls at the prompt with no way forward.
154 + Bench-testing the gate against an encrypted disk therefore depends on the TPM
155 + path working. `install_drive.py` leaves encryption off by default, and a target
156 + installed that way is the one to use for keyboard-shaped tests.
157 +
117 158 **The medium's ssh installer needs privilege it does not get on its own.**
118 159 `alloy install` does not escalate, and an ssh session lands unprivileged, so the
119 160 wizard draws and answers every step and then fails on the first command that
@@ -24,8 +24,19 @@
24 24 # would; delete that directory to simulate a cleared TPM.
25 25 mkdir -p "$SCRATCH/tpm"
26 26 if [ ! -S "$SCRATCH/tpm/swtpm-sock" ]; then
27 - swtpm_setup --tpm2 --tpmstate "$SCRATCH/tpm" --createek --create-ek-cert \
28 - --create-platform-cert --lock-nvram --overwrite >/dev/null 2>&1 || true
27 + # Only mint a TPM when there is not one already. The setup call carries
28 + # `--overwrite`, and this branch is taken every time the swtpm daemon has been
29 + # stopped rather than only on a first run, so running it unconditionally gives
30 + # the machine a new seed behind its own back. An installed disk that unlocks
31 + # from the TPM then cannot load its sealed key and falls back to asking for the
32 + # passphrase, reporting `Failed to unseal secret using TPM2: State not
33 + # recoverable` — which reads as a broken enrolment rather than as hardware the
34 + # harness swapped out. Deleting state/tpm/ is still how you clear a TPM on
35 + # purpose; this only stops it happening by accident.
36 + if [ ! -f "$SCRATCH/tpm/tpm2-00.permall" ]; then
37 + swtpm_setup --tpm2 --tpmstate "$SCRATCH/tpm" --createek --create-ek-cert \
38 + --create-platform-cert --lock-nvram --overwrite >/dev/null 2>&1 || true
39 + fi
29 40 swtpm socket --tpm2 --tpmstate dir="$SCRATCH/tpm" \
30 41 --ctrl type=unixio,path="$SCRATCH/tpm/swtpm-sock" \
31 42 --flags startup-clear --daemon
@@ -48,8 +59,36 @@
48 59 *) echo "unknown TARGET_BUS '$TARGET_BUS'; expected virtio or nvme" >&2; exit 1 ;;
49 60 esac
50 61
62 + # What keyboards the machine has, which is hardware the gate's bench tests need
63 + # and no machine here can be. `usr/bin/alloy-usb-gate` suspends enforcement
64 + # whenever the count of ID_INPUT_KEYBOARD devices reaches zero, so exercising it
65 + # means a machine that can have zero, and every physical machine here has an
66 + # i8042 keyboard soldered to it that usbguard has no jurisdiction over.
67 + #
68 + # ps2 q35's own i8042 controller. The default, and what every scenario
69 + # written before this one assumes.
70 + # none i8042=off. Zero keyboards of any kind, which is the machine the gate
71 + # is supposed to open for at boot.
72 + # usb i8042=off plus a USB keyboard on an xHCI controller, so the only
73 + # keyboard on the machine is one usbguard CAN take away. `id=kbd0` is
74 + # load-bearing: unplugging it mid-session is `device_del kbd0` over the
75 + # monitor, which is the third bench test and cannot be done from inside
76 + # the guest.
77 + #
78 + # q35 carries no USB controller unless one is asked for, so the `usb` case adds
79 + # an xHCI rather than relying on a default that is not there.
80 + KEYBOARD="${KEYBOARD:-ps2}"
81 + case "$KEYBOARD" in
82 + ps2) MACHINE="q35" ; KBD_ARGS=() ;;
83 + none) MACHINE="q35,i8042=off"; KBD_ARGS=() ;;
84 + usb) MACHINE="q35,i8042=off"
85 + KBD_ARGS=(-device qemu-xhci,id=xhci -device usb-kbd,bus=xhci.0,id=kbd0) ;;
86 + *) echo "unknown KEYBOARD '$KEYBOARD'; expected ps2, none or usb" >&2; exit 1 ;;
87 + esac
88 +
51 89 args=(
52 - -enable-kvm -machine q35 -cpu host -m 4608 -smp 4
90 + -enable-kvm -machine "$MACHINE" -cpu host -m 4608 -smp 4
91 + "${KBD_ARGS[@]}"
53 92 -drive if=pflash,format=raw,unit=0,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd
54 93 -drive "if=pflash,format=raw,unit=1,file=$VARS"
55 94 "${TARGET_ARGS[@]}"