A TPM (Trusted Platform Module) is a security chip that generates and uses keys without exposing them, records what a machine booted, and can prove both to someone else.
Macs don’t come with one, so experimenting with a TPM on a Mac means running
Linux in a VM and giving that VM a TPM of its own. This post does that with
Multipass and swtpm, a software TPM that the guest’s kernel treats exactly
like a hardware chip. The result is a VM with /dev/tpm0 and /dev/tpmrm0
that come back after a reboot, with the TPM’s contents intact.
a VM
Multipass from Canonical runs Ubuntu VMs on macOS with a single command. Run this on the Mac:
multipass launch 24.04 --name tpmlab --cpus 4 --memory 4G --disk 20G
Then open a shell in it. Everything from here on runs inside the VM unless it says otherwise.
multipass shell tpmlab
a TPM, nearly
swtpm is the TPM emulator, and
tpm2-tools is the standard set of command-line tools for talking to a TPM.
swtpm-tools adds swtpm_setup, which this post doesn’t use but which you’ll
need later for things like an EK certificate, the certificate a TPM maker
issues to show the chip is genuine.
sudo apt-get update && sudo apt-get install -y swtpm swtpm-tools tpm2-tools
swtpm can run without any help from the kernel, but programs then reach it
over a socket instead of a device file. For it to appear as /dev/tpm0, the
kernel needs the tpm_vtpm_proxy module, and the VM doesn’t have it:
$ modinfo tpm_vtpm_proxy
modinfo: ERROR: Module tpm_vtpm_proxy not found.
multipass launch 24.04 boots the Ubuntu Server cloud image, which uses the
smaller linux-virtual kernel package. That package leaves out
linux-modules-extra, and that’s where tpm_vtpm_proxy lives.
Install the extra modules for the kernel that’s running now, together with
linux-image-extra-virtual, which makes sure every future kernel gets its
extra modules as well:
sudo apt-get install -y linux-modules-extra-$(uname -r) linux-image-extra-virtual
Without that second package the next kernel upgrade breaks the setup, because
the VM boots a kernel that has no tpm_vtpm_proxy and the TPM doesn’t come
back.
Loading the module creates /dev/vtpmx, the device swtpm uses to ask the
kernel for a new TPM:
sudo modprobe tpm_vtpm_proxy
starting swtpm
swtpm keeps everything the TPM remembers, such as its keys and its NV memory (a small amount of storage inside the TPM), in a state directory. It doesn’t create that directory itself, so the first attempt fails:
$ sudo swtpm chardev --vtpm-proxy --tpm2 --tpmstate dir=/var/lib/swtpm/tpm0
New TPM device: /dev/tpm0 (major/minor = 10/224)
swtpm: SWTPM_NVRAM_Lock_Dir: Could not open lockfile: No such file or directory
swtpm: Error: Could not initialize libtpms.
Create the directory. Keep it under /var/lib/swtpm: Ubuntu’s AppArmor
profile for swtpm only allows that path, and anywhere else swtpm fails with
Could not open lockfile: Permission denied.
sudo mkdir -p /var/lib/swtpm/tpm0
Then start swtpm again. It runs in the foreground:
sudo swtpm chardev --vtpm-proxy --tpm2 --tpmstate dir=/var/lib/swtpm/tpm0
checking the devices
From the Mac, open a second shell with multipass shell tpmlab and list the
devices:
ls -l /dev/tpm*
crw-rw---- 1 tss root 10, 224 Oct 4 17:53 /dev/tpm0
crw-rw---- 1 tss tss 252, 65536 Oct 4 17:53 /dev/tpmrm0
Asking the TPM for random bytes is the simplest test. Hex output means a command went from userspace through the kernel to swtpm and back:
sudo tpm2_getrandom --hex 16 -T device:/dev/tpmrm0
845efd7f9e0a21df66e0e9f171747add
The two devices have different owners. /dev/tpmrm0 belongs to tss:tss, so
root and members of the tss group, the group Ubuntu uses for TPM access, can
use it. /dev/tpm0 belongs to tss:root, which leaves only root and the tss
user, and group members get Permission denied. Joining the group, and opening
a new shell afterwards, is enough to use /dev/tpmrm0 without sudo:
sudo usermod -aG tss ubuntu
Pressing Ctrl-C in the first shell stops swtpm, and both devices disappear with it.
how the proxy works
The kernel module,
tpm_vtpm_proxy,
was written so that containers could each have their own TPM, but it works
the same for anything on the other end. When swtpm starts with
--vtpm-proxy, it opens /dev/vtpmx and asks the kernel for a new TPM
device. The kernel creates /dev/tpm0 and hands swtpm a file descriptor,
an open handle, connected to it.
Every command a program writes to /dev/tpm0 arrives on that file
descriptor, swtpm executes it, and the response goes back the same way. The
kernel registers the result as an ordinary TPM, so tpm2-tools can’t tell the
difference, and neither can kernel code that works with TPM devices.
/dev/tpm0 and /dev/tpmrm0 are two ways into that same TPM. /dev/tpm0 is
the raw device, and only one process can have it open at a time. This is a
second process trying while another one holds it:
$ sudo tpm2_getrandom --hex 8 -T device:/dev/tpm0
ERROR:tcti:src/tss2-tcti/tcti-device.c:451:Tss2_Tcti_Device_Init() Failed to open specified TCTI device file /dev/tpm0: Device or resource busy
Anything loaded into the TPM through /dev/tpm0, such as keys or sessions,
stays loaded for whoever opens it next, and cleaning up is left to you.
/dev/tpmrm0 goes through the kernel’s resource manager
(tpm2-space.c).
Every open of the device gets its own space, and the kernel moves that space’s
keys and sessions in and out of the TPM around each command, so many processes
can share the TPM without seeing each other’s objects. Unless you have a
specific reason to want the raw device, use /dev/tpmrm0.
after a reboot
At this point the TPM disappears as soon as the terminal running swtpm closes. For it to come back on every boot, the module has to load at startup and swtpm has to run as a service.
systemd loads every module listed in modules-load.d at boot:
echo tpm_vtpm_proxy | sudo tee /etc/modules-load.d/tpm_vtpm_proxy.conf
swtpm then runs as a systemd service. The unit file is also available here.
sudo tee /etc/systemd/system/swtpm.service >/dev/null <<'EOF'
[Unit]
Description=Software TPM 2.0 exposed as /dev/tpm0 via tpm_vtpm_proxy
After=systemd-modules-load.service
Wants=systemd-modules-load.service
[Service]
Type=simple
StateDirectory=swtpm/tpm0
StateDirectoryMode=0750
ExecStart=/usr/bin/swtpm chardev --vtpm-proxy --tpm2 --tpmstate dir=/var/lib/swtpm/tpm0
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
After= and Wants= make sure the module is loaded, and /dev/vtpmx
exists, before swtpm starts. StateDirectory= has systemd create
/var/lib/swtpm/tpm0, so on a new machine the mkdir from earlier isn’t
needed. swtpm stays in the foreground, which is what Type=simple expects.
If swtpm is still running in the other shell, stop it first, because two copies can’t use the same state directory. Then enable and start the service:
sudo systemctl daemon-reload && sudo systemctl enable --now swtpm.service
A reboot test should also show that the TPM kept its contents, not only that the device came back. Writing a short string into the TPM’s NV memory gives you something to look for afterwards:
echo -n "hello from before the reboot" > /tmp/m
sudo tpm2_nvdefine 0x1500016 -C o -s 32 -a "ownerread|ownerwrite|authread|authwrite" -T device:/dev/tpmrm0
sudo tpm2_nvwrite 0x1500016 -C o -i /tmp/m -T device:/dev/tpmrm0
Restart the VM from the Mac:
multipass restart tpmlab
Back in the VM, read the string again:
sudo tpm2_nvread 0x1500016 -C o -s 28 -T device:/dev/tpmrm0
hello from before the reboot
working from the Mac
I wrote tpmlsm this way: the code
lives on the Mac, where I edit it, and the VM runs it against the TPM.
multipass mount shares a directory from the Mac into the VM. Run it on the
Mac, since there’s no multipass command inside the VM. The first mount takes a
while because Multipass installs sshfs, the tool it uses to share the directory,
in the VM:
mkdir -p ~/code/tpm && multipass mount ~/code/tpm tpmlab:/home/ubuntu/tpm
Working notes. If something here is wrong, tell me.