tpm

Raspberry Pi 4, LetsTrust TPM and Yocto

As briefly mentioned in the measured boot blog post, I had some issues with a TPM in the emulated environment. In the end, I bought a physical TPM chip for Raspberry Pi and verified that the measured boot worked as intended on the actual hardware, confirming that the issues I encountered were likely due to my somewhat esoteric virtual setup. Getting this LetsTrust TPM module working was fairly simple but there were a few things I learned along the way that may be worth sharing.

LetsTrust TPM Module

First of all, let’s clarify what is this LetsTrust TPM module. It’s a TPM module that connects to the Raspberry Pi’s GPIO pins, and communicates with the Raspberry Pi via SPI (Serial Peripheral Interface). The chip in the module is Infineon’s SLB9672. There are a few other TPM modules for Raspberry Pi, but this LetsTrust module was cheap and readily available, so I decided to get it. I still don’t quite understand the full capabilities of TPM devices, but it seems to work quite well for the little use I have so I think it’s worth the money. This was my comprehensive hands-on review of it.

There’s a surprisingly high amount of resources for integrating the LetsTrust TPM module into the Yocto build: two. First of all, there’s this guide which goes deep into details and is fairly thorough. Secondly, there’s this meta-slb9670-rpi layer that does most of the things required for integrating the module into the Yocto build. SLB9670 in the name of the meta-layer is the earlier version of the Infineon chip that was used in the older versions of the LetsTrust module.

The guide is a bit outdated and not all of the steps in it seem to be mandatory anymore, but here’s an outline of what needs to be done to get the LetsTrust working with Raspberry Pi:

  • Create a device tree overlay that enables the SPI bus and defines the TPM module
  • Configure TPM to be enabled in the kernel and U-boot
  • Enable SPI TPM drivers in the kernel
  • Add TPM2 software stack to the image
  • (Outdated) Patch U-boot to communicate to the module via GPIOs using bit-banging
  • (Optional) Add a service to enable TPM in Linux

The meta-slb9670-rpi layer does these things, and a bit more to define a FIT image and the boot script.

Updating meta-slb9670-rpi

However, the layer has not been updated since Dunfell version of Yocto. The upgrade from Dunfell to Scarthgap is fortunately fairly straightforward as it’s mostly syntax fixes. The meta-layer had a few other issues though. Booting the FIT image didn’t work because the boot script loaded the image too close to the kernel load address, overwriting the image when kernel was loaded. I made the executive decision to load the FIT image to the initrd load address because there’s plenty of space there and the boot is not using initrd anyway. A few other things in the meta-layer required clean-up as well, like removing some unused files and a service that loaded tpm_tis_spi module that was actually being built as a kernel built-in feature.

You can find the updated branch from my fork of meta-slb9670-rpi. Adding that layer with the dependencies should be enough to do the trick. The trick, in this case, is adding the LetsTrust module to Yocto builds. Simple as that. So, what’s the point of this blog post? First, to showcase the updated meta-layer, and second, to introduce the measured boot functionality I added. That was a slightly more complicated affair. But not too much, no need to be scared. However, before proceeding I recommend taking a look at the meta-layer and it’s contents, because it will be referenced later on.

Adding Measured Boot

First, I recommend reading my blog post about enabling measured boot to Yocto, because we will follow the steps listed there.

Second, I want to say that I’m not a big fan of SystemD, and after writing this text I have once again a bit more repressed anger towards it. Because, for some reason, reading the measured boot event log just does not work on SystemD. tpm2_eventlog command either says that the log cannot be read, or straight up segfaults. Reading PCR registers works just fine though. I still haven’t found the exact root cause for this, but I hope I’ll find it soon because I need some of the SystemD features for the upcoming texts. It may be related to the device management. If I figure out how to fix this I’ll add a link here, but for the time being these instructions are only for SysVinit-style systems. My SEO optimizer complains that this chapter is too long, but in my opinion, SystemD rants can never be too long.

I try really hard not to get on the SystemD hate train, but SystemD itself makes it so difficult at times.

Back to the actual business. The first step of adding the measured boot is enabling the feature in the U-boot configuration. However, we want to enable measured boot without the devicetree measurement, so the additional configuration looks like this:

CONFIG_MEASURED_BOOT=y
# CONFIG_MEASURE_DEVICETREE is not set

It seems that the PCRs are not constant if the devicetree is measured. I’m not 100% sure why, but my theory is that the proprietary Raspberry Pi bootloader that runs before U-Boot edits the devicetree on-the-fly, and therefore the devicetree measured by U-boot is never constant. I suppose RasPi bootloader does this to support the different RAM variants without requiring a separate devicetree for each. The main memory size is defined as zero in the devicetree source and there is a “Will be filled by the bootloader” comment next to the zero value, so at least something happens there.

Next, the memory region for the measurement log needs to be defined. In the measured boot blog post two methods for this were presented: either defining a reserved-memory section or defining sml-base and sml-size. I mentioned in the blog post that I couldn’t get sml-base working with QEMU, but with Raspberry Pi it was the opposite. Defining a reserved-memory block didn’t work, but the linux,sml-base and linux,sml-size did. I have a feeling that this is related to the fact that the memory node in devicetree is dynamically defined by the bootloader, but I cannot prove it.

So, to define the event log location we just add the two required parameters under the TPM devicetree node in the letstrust-tpm-overlay like this:

slb9670: slb9670@0 {
	compatible = "infineon,slb9670", "tis,tpm2-spi", "tcg,tpm_tis-spi";
	reg = <0>;
	gpio-reset = <&gpio 24 1>;
	#address-cells = <1>;
	#size-cells = <0>;
	status = "okay";

	/* for kernel driver */
	spi-max-frequency = <1000000>;
	linux,sml-base = <0x00 0xf6ffa000>;
	linux,sml-size = <0x6000>;
};

Note how I fixed the bad bad thing I did the last time. The event log should now be aligned to the end of the free RAM (non-reserved areas checked from /proc/iomem). This is only the case for my 4GB RAM RPI4 though, with the 2GB and 8GB versions the memory region is either in a wrong or non-existent place. I’m starting to understand the reasoning behind the dynamic memory definition done by the bootloader.

Et voilà! Just with these two changes tpm2_eventlog and tpm2_pcrread can now be used to read the boot measurements after booting the image. I created scarthgap-measured-boot-raspberrypi4-4gb branch (strong contender for the longest branch name I’ve ever created) for this feature because for now the measured boot works properly only on the 4GB variant using SysVinit, so it shouldn’t be merged into the main scarthgap branch. Maybe in the future there will be rainbows, sunshine and working SystemD-based systems.

Yocto Emulation: Setting Up QEMU with TPM

As promised, it’s time for the QEMU follow-up. Last time we got Yocto’s runqemu command to launch u-boot, boot up a kernel, and mount a virtual drive with multiple partitions. Quite a lot of stuff. This time we are “just” going to add a TPM device to the virtual machine. As before, you can find the example meta-layer from Github. It contains the example snippets presented in this blog text, and should be ready to use.

Why is this virtualized TPM worth the effort? Well, if you have ever been in a painful situation where you’re working with TPMs and you’re writing some scripts or programs using them, you know that the development is not as straightforward as one would hope. The flows tend to be confusing, frustrating, and difficult. Using a virtual environment that’s easy to reset and that’s quite close to the actual hardware is a nice aid for developing and testing these types of applications.

In a nutshell, the idea is to run swtpm TPM emulator on the host machine, and then launch QEMU Arm device emulator that talks with the swtpm process. QEMU has an option for a TPM device that can be passed through to the guest device, so the process is fairly easy. With these systems in place, we can have a virtual TPM chip inside the virtual machine. *insert yo dawg meme here*

TPM Emulation With swtpm

Because I’m terrible at explaining things understandably, I’m going to ask my co-author ChatGPT to summarise in one paragraph what a TPM is:

Trusted Platform Module (TPM) is a hardware-based security feature integrated into computer systems to provide a secure foundation for various cryptographic functions and protect sensitive data. TPM securely stores cryptographic keys, certificates, and passwords, ensuring they remain inaccessible to unauthorized entities. It enables secure boot processes, integrity measurement, and secure storage of credentials, enhancing the overall security of computing devices by thwarting attacks such as tampering, unauthorized access, and data breaches.

I’m not sure if this is easier to understand than my ramblings, but I guess it makes the point clear. It’s a hardware chip that can be used to store and generate secrets. One extra thing worth knowing is that there are two notable versions of the TPM specification: 1.2 and 2.0. When I’m talking about TPM in this blog text, I mean TPM 2.0.

Since we’re using emulated hardware, we don’t have the “hardware” part in the system. Well, QEMU has a passthrough option for hardware TPMs, but for development purposes it’s easier to have an emulated TPM, something that swtpm can be used for. Installing swtpm is straightforward, as it can be found in most of the package repositories. For example, on Ubuntu, you can just run:

sudo apt install swtpm

Building swtpm is also an option. It has quite a few dependencies though, so you may want to consider just fetching the packages. Sometimes taking the easy route is allowed.

Whichever option you choose, once you’re ready you can run the following commands to set up the swtpm and launch the swtpm process:

mkdir /tmp/mytpm1
swtpm_setup --tpmstate /tmp/mytpm1 \
  --create-ek-cert \
  --create-platform-cert \
  --create-spk \
  --tpm2 \
  --overwrite
swtpm socket --tpmstate dir=/tmp/mytpm1 \
  --ctrl type=unixio,path=/tmp/mytpm1/swtpm-sock \
  --tpm2 \
  --log level=20

Once the process launches, it opens a Unix domain socket that listens to the incoming connections. It’s worth knowing that the process gets launched as a foreground job, and once a connected process exits swtpm exits as well. Next, we’re going to make QEMU talk with the swtpm daemon.

QEMU TPM

Fortunately, making QEMU communicate with TPM isn’t anything groundbreaking. There’s a whole page of documentation dedicated to this topic, so we’re just going to follow it. For Arm devices, we want to pass the following additional parameters to QEMU:

-chardev socket,id=chrtpm,path=/tmp/mytpm1/swtpm-sock \
-tpmdev emulator,id=tpm0,chardev=chrtpm \
-device tpm-spapr,tpmdev=tpm0 \

These parameters should result in the QEMU connecting to the swtpm, and using the emulated software TPM as a TPM in the emulated machine. Simple as.

One thing worth noting though. Since we’re adding a new device to the virtual machine, the device tree changes as well. Therefore, we need to dump the device tree again. This was discussed more in-depth in the first part of this emulation exercise, so I recommend reading that. In summary, you can dump the device tree with the following runqemu command:

BIOS=tmp/deploy/images/qemuarm-uboot/u-boot.bin \
runqemu \
core-image-base nographic wic.qcow2 \
qemuparams="-chardev socket,id=chrtpm,path=/tmp/mytpm1/swtpm-sock \
-tpmdev emulator,id=tpm0,chardev=chrtpm \
-device tpm-tis-device,tpmdev=tpm0 \
-machine dumpdtb=qemu.dtb"

Then, you need to move the dumped binary to a location where it can get installed to the boot partition as a part of the Yocto build. This was also discussed in the first blog text.

TPM2.0 Software Stack

Configuring Yocto

Now that we have the virtualized hardware in order, it’s time to get the software part sorted out. Yocto has a meta-layer that contains security features and programs. That layer is aptly named meta-security. To add the TPM-related stuff into the firmware image, add sub-layer meta-tpm to bblayers.conf. meta-tpm has dependencies to meta-openembedded sub-layers meta-oe and meta-python, so add those as well.

Once the layers are added, we still need to configure the build a bit. The following should be added to your distro.conf, or if you don’t have one, local.conf should suffice:

DISTRO_FEATURES:append = " tpm"

Configuring Linux Kernel

Next, to get the TPM device working together with Linux, we need to configure the kernel. First of all, the TPM feature needs to be enabled, and then the driver for our emulated chip needs to be added. If you were curious enough to decompile the QEMU device tree binary, you maybe noticed that the emulated TPM device is compatible with tcg,tpm-tis-mmio. Therefore, we don’t need a specific driver, the generic tpm-tis driver should do. The following two config lines both enable TPM and add the driver:

CONFIG_TCG_TPM=y
CONFIG_TCG_TIS=y

If you’re wondering what TCG means, it stands for Trusted Computing Group, the organization that has developed the TPM standard. TIS on the other hand stands for TPM Interface Specification. There are a lot of TLAs here that begin with the letter T, and we haven’t even seen all of them yet.

Well, here’s the yo dawg meme.

Configuring U-Boot

Configuring TPM support for U-Boot is quite simple. Actually, the U-Boot I built worked straight away with the defconfig. However, if you have issues with TPM in U-Boot, you should ensure that you have the following configuration items enabled:

# Enable TPM 2.0
CONFIG_TPM=y
CONFIG_TPM_V2=y
# Add MMIO interface for device
CONFIG_TPM2_MMIO=y
# Add TPM command
CONFIG_CMD_TPM=y
# This should be enabled automatically if
# CMD_TPM and TPM_V2 are enabled
CONFIG_CMD_TPM_V2=y

Installing tpm2-tools

In theory, we now should have completed the original goal of booting a Yocto image on an emulator that has a virtual TPM. However, there’s still nothing that uses the TPM. To add plenty of packages, tpm2-tools among them, we can add the following to the image configuration:

IMAGE_INSTALL:append = " \
    packagegroup-security-tpm2 \
    libtss2-tcti-device \
"

packagegroup-security-tpm2 contains the following packages:

tpm2-tools
trousers
tpm2-tss
libtss2
tpm2-abrmd
tpm2-pkcs11

For our testing purposes, we are mostly interested in tpm2-tools and tpm2-tss, and libtss2 that tpm2-tools requires. TSS here stands for TPM2 Software Stack. trousers is an older implementation of the stack, tpm2-abrmd (=access broker & resource manager daemon) didn’t work for me (and AFAIK using a kernel-managed device is preferred anyway), and PKCS#11 isn’t required for our simple example. libtss2-tcti-device is required to enable a TCTI (TPM Command Transmission Interface) for communication with Linux kernel TPM device files. These are the last acronyms, so now you can let out a sigh of relief.

Running QEMU

Now you can rebuild the image to compile a suitable kernel and user-space tools. Once the build finishes, you can use the following command to launch QEMU (ensure that swtpm is running):

BIOS=tmp/deploy/images/qemuarm-uboot/u-boot.bin \
runqemu \
core-image-base nographic wic.qcow2 \
qemuparams="-chardev socket,id=chrtpm,path=/tmp/mytpm1/swtpm-sock \
-tpmdev emulator,id=tpm0,chardev=chrtpm \
-device tpm-tis-device,tpmdev=tpm0"

Then, stop the booting process to drop into the U-Boot terminal. We wrote the boot script in the previous blog, but now we can add tpm2 commands to initialize and self-test the TPM. The first three commands of this complete boot script set-up and self-test the TPM:

# Initalize TPM
tpm2 init
tpm2 startup TPM2_SU_CLEAR
tpm2 self_test full
# Set boot arguments for the kernel
setenv bootargs root=/dev/vda2 console=ttyAMA0
# Load kernel image
setenv loadaddr 0x40200000
fatload virtio 0:1 ${loadaddr} zImage
# Load device tree binary
setenv loadaddr_dtb 0x49000000
fatload virtio 0:1 ${loadaddr_dtb} qemu.dtb
# Boot the kernel
bootz ${loadaddr} - ${loadaddr_dtb}

Now, once the machine boots up, you should see /dev/tpm0 and /dev/tpmrm0 devices present in the system. tpm0 is a direct access device, and tpmrm0 is a device using the kernel’s resource manager. The latter of these is the alternative to tpm2-abrmd, and we’re going to be using it for a demo.

TPM Demo

Before we proceed, I warn you that my knowledge of actual TPM usage is a bit shallow. So, the example presented here may not necessarily follow the best practices, but it should perform a simple task that should prove that the QEMU TPM works. We are going to create a key, store it in the TPM, sign a file and verify the signature. When you’ve got the device booted with the swtpm running in the background, you can start trying out these commands:

# Set environment variable for selecting TPM device
# instead of the abrmd.
export TPM2TOOLS_TCTI="device:/dev/tpmrm0"
# Create contexts
tpm2_createprimary -C e -c primary.ctx
tpm2_create -G rsa -u rsa.pub -r rsa.priv -C primary.ctx
# Load and store contexts
tpm2_load -C primary.ctx -u rsa.pub -r rsa.priv -c rsa.ctx
tpm2_evictcontrol -C o -c primary.ctx 0x81010002
tpm2_evictcontrol -C o -c rsa.ctx 0x81010003
# Remove generated files and create message
rm rsa.pub rsa.priv rsa.ctx primary.ctx
echo "my message" > message.dat
# Sign and verify signature with TPM handles
tpm2_sign -c 0x81010003 -g sha256 -o sig.rssa message.dat
tpm2_verifysignature -c 0x81010003 -g sha256 -s sig.rssa -m message.dat

If life goes your way, all the commands should succeed without issues and you can create and verify the signature using the handles in the TPM. Usually, things aren’t that simple. If you see errors related to abrmd, you may need to define the TCTI as the tpmrm0 device. The TPM2TOOLS_TCTI environment variable should do that. However, if that doesn’t work you can try adding -T "device:/dev/tpmrm0" to the tpm2_* commands, so for example the first command looks like this:

tpm2_createprimary -C e -c primary.ctx -T "device:/dev/tpmrm0"

When running the tpm2_* commands, you should see swtpm printing out plenty of information. This information includes requests and responses received and sent by the daemon. To make some sense of these hexadecimal dumps, you can use tpmstream tool.

That should wrap up my texts about QEMU, Yocto and TPM. Hopefully, these will help you set up a QEMU device that has a TPM in it. I also hope that in the long run this setup helps you to develop and debug secure Linux systems that utilize TPM properly. Perhaps I’ll write more about TPMs in the future, it was quite difficult to find understandable sources and examples utilizing its features. But maybe first I’d need to understand the TPMs a bit better myself.