# #Measured Debian Boot with TPM 2.0 and UEFI

I travel a lot. (Well, at least in the pre-COVID era, I did.) This means I drag my Thinkpad X1 Carbon gen 6 laptop to many places, and often leave it unattended in hotel rooms. While my data is all encrypted-at-rest, getting access to the data when the machine is first powered on necessarily involves running some code that is neither encrypted nor authenticated: namely, the bootloader and enough of the operating system kernel to decrypt and mount volumes.

Furthermore, this process requires me to type in a passphrase every time I boot the machine, which exposes the passphrase to anyone observing surreptitiously, and is in general an annoyance.

To increase the complexity of someone instrumenting my laptop without my knowledge and in doing so gaining access to secrets as I use the compromised machine unawares, I decided I needed to explore the use of trusted computing technologies to protect my installation from tampering.

## Measured boot

There are a number of technologies that can reduce (but of course not eliminate) the attack surface of a machine. For this purpose, I am limiting my evaluation of technologies to those that protect the boot sequence from most kinds of tampering. Other measures are required for runtime protection against zero-day remote exploits, physical attacks that employ USB DMA, tire irons, etc., as well as some advanced kinds of physical tampering.

My adversary is someone who intends to tamper with my machine without my knowledge, with the purposes of either making off with some information or instrumenting the machine such that information encrypted-at-rest is revealed to the attacker out-of-band. There's not a lot of value to me in tamper prevention: once someone's screwed with my machine, I am unlikely to trust it again regardless. My main goal is tamper detection. I thus settled on measured boot, which measures the sequence of executable code launched during the boot process in a way that is highly resistant to forgery: at the end of this process, this measurement can be used to verify that the machine has not been tampered with, as well as to unlock disk encryption keys that can be used to mount volumes without requiring the user enter a passphrase.

Juniper has a short page outlining the differences between measured boot and secure boot.

## Requirements

From various places (see references for a list of such resources) I managed to piece together a working measured boot that unseals a LUKS decryption passphrase for the root partition on a trusted Debian bullseye installation. This provides both tamper detection (machine won't boot automatically if the passphrase cannot be unsealed) and protection for data-at-rest (via the use of dm-crypt for the root partition).

I had a few requirements going into this project:

• Use my laptop's existing TPM, which is the PTT in the (dreaded) Intel Management Engine
• Measure everything from the BIOS through the kernel, initrd, and kernel command line
• Require an expected boot chain measurement before automatically decrypting and mounting any data partitions
• Fall back to passphrase entry on failure
• Use my laptop's native UEFI boot manager (e.g., not systemd-boot) with no intermediate bootloader (like Trusted GRUB)
• Avoid the need for any persistent PKI
• Keep the implementation simple and avoid the need for any packages not in the main Debian repository

## Solution overview

While it required a lot of experimentation, the final implementation is actually fairly simple. The basics are:

• Activate the TPM in the BIOS
• Figure out which PCRs to include in the sealing/unsealing process
• Add a keyscript to the initrd to try unsealing the passphrase for the root partition, otherwise falling back to password entry
• Modify crypttab to refer to this keyscript
• Include tpm2_unseal and all required libraries and kernel modules in the initrd
• Package the kernel, initrd, and kernel command line into a single EFI-bootable blob residing directly on the EFI partition
• Add a static EFI boot manager entry to chain to the blob
• Create a new binary LUKS passphrase, written to a directory on the root partition readable only by root
• Execute a script to seal the passphrase to the current set of PCR values

## Prerequisites

First, you'll need an encrypted root partition. This guide assumes you have an existing LUKS-managed encrypted root partition (with no separate /boot partition) and that you use grub2 configured with GRUB_ENABLE_CRYPTODISK=y that prompts for a passphrase when the bootloader first launches. You might also be using efistub with the kernel and initrd on the EFI partition and the kernel command line parameters in the EFI boot manager configuration, but you will still have an encrypted root. Your encrypted partition might conceal an ext4-formatted volume or an LVM physical volume, and it might be atop a software RAID array: none of these variants should impact the general approach, though some of the details may vary.

To proceed, you will need to install the following Debian packages and their dependencies:

• tpm2-tools >= 4.0
• initramfs-tools

I'm sure there's a way to do this with dracut in place of initramfs-tools, but you'll need to figure it out yourself.

## Preparation

### Activate the TPM

Enter your machine's BIOS settings and make sure a TPM supporting the 2.0 spec is active. If you have a discrete TPM, that is probably the best choice; otherwise, a firmware TPM such as Intel's PTT (part of the dreaded Management Engine suite) will do. Reboot into Linux and verify it is working by running tpm2_pcrread as root.

### Determine PCRs to include

There doesn't seem to be a whole lot of information about this available. The problem is likely that the PCRs are platform-specific, combined with the relatively sparse use of trusted computing overall among desktop Linux users. Example listings I've found are all similar to the following:

PCR Measured entity
0 BIOS
1 BIOS configuration
2 Option ROMs
3 Option ROM configuration
4 MBR (master boot record)
5 MBR configuration
6 State transitions and wake events
7 Platform manufacturer specific measurements
8–15 Static operating system
16 Debug
23 Application support

But this exact list appears in several places, and so might be cargo cult. I took an experimental approach: dumping the PCRs to a file with tpm2_pcrread, and then rebooting and otherwise screwing with the configuration several times (warm boot; hibernate and resume; boot from shutdown; boot into Windows and then warm reboot back into Linux; sleep and wake up; change display brightness) to find out which ones didn't change. For my X1C, that turned out to be all of the non-zero registers below 16 (0-7). Upon reading the TCG's definition of PCR 6, I removed that one from consideration as I explicitly want resume-from-hibernate to be treated the same as initial boot.

### Keyscript

Now that you have the set of target PCRs, you'll want to create a script that the boot sequence will use to unseal the passphrase that we'll later add to the TPM. I created /etc/measuredboot and placed a script there called unseal_key_or_prompt:

#! /bin/sh

exec 3>&1 1>&2

if [ "$CRYPTTAB_TRIED" = 0 ]; then if ! tpm2_unseal --tcti device:/dev/tpm0 -c 0x81000000 -p pcr:sha256:0,1,2,3,4,5,7 1>&3 3>&-; then echo 'TPM unseal of LUKS key failed' exit 1 fi else echo "Decryption try$CRYPTTAB_TRIED"
}

case $1 in prereqs) prereqs exit 0 ;; esac . /usr/share/initramfs-tools/hook-functions copy_exec /usr/bin/tpm2_unseal /usr/bin copy_exec /usr/lib/x86_64-linux-gnu/libtss2-tcti-device.so.0 /usr/lib/x86_64-linux-gnu force_load tpm_tis force_load tpm_crb The top part of this script (through the line that sources hook-functions) is boilerplate. The important lines for us are the last four: • The first copies tpm2_unseal and its link-time library dependencies to the initrd • The second copies the TCTI device interface library (which tpm2_unseal dlopens) to the initrd • The third and fourth copy two modules and their dependencies to the initrd and force them to be loaded before unsealing is attempted The list of modules came from examining the output of lsmod on my running system to include whichever tpm-related modules were not prerequisites for others. I didn't investigate any further than noting that modinfo has no interesting description for either. To put the final touches on the initrd, run the update command: update-initramfs -u which will regenerate the initrd for the currently running kernel. ### Create the unified kernel blob The UEFI boot manager is going to measure whatever it directly loads: for installations using grub, this means the grub EFI bootloader. For an efistub installation, this means the compressed kernel. Notably, neither includes the initrd or the kernel command line. This gap is wide enough to drive a tank through. Thankfully, there is an easy solution: building your own EFI application comprising all three entities plus a stub that knows how to tease it all apart when executed. Thankfully, systemd comes with such a stub, and notably it does not require you to install systemd-boot itself to your EFI partition. Simply verify that you have a file called /usr/lib/systemd/boot/efi/linuxx64.efi.stub (which on my system comes from the systemd package) and you should be good to go. I use the following script to generate my unified kernel blob into the existing debian subdirectory on my EFI partition: #! /bin/bash set -e echo "root=UUID=$(sudo blkid -s UUID -o value /dev/mylvm/root) rootfstype=ext4 add_efi_memmap \
ro cryptdevice=UUID=6a84f43d-03ed-41b7-9dee-d5d2cdc0b474:lvm" >/tmp/kernel-command-line.txt

objcopy \
/usr/lib/systemd/boot/efi/linuxx64.efi.stub /boot/efi/EFI/debian/linux.efi

You will need to modify several things in the line that generates the kernel command line:

• root= specifies the root volume: I pull it as a UUID from my root LVM logical volume /dev/mylvm/root.
• cryptdevice= specifies the partition containing the encrypted root. In my case, it is an LVM physical volume with the given UUID.

Other things to note:

• rootfstype=ext4 is probably just a hint. Cargo cult.
• Several pages I found indicate add_efi_memmap may no longer be necessary. Cargo cult.
• I don't think the osrel section in the blob is necessary, but I included it anyway. Cargo cult.
• You can add any other kernel command line arguments you might desire to this line.

### Add an EFI boot entry

The final step before rebooting will be to add a new boot entry to your EFI. You should need to do this only once, since this boot entry simply chains to the self-contained blob you just created.

efibootmgr --disk /dev/nvme0n1p1 -c -L "Debian (unified)" -l '\EFI\debian\linux.efi'

The parameter to --disk is the device for your EFI partition.

### Reboot!

Now you should reboot using this boot entry. This may require you to enter your machine's boot device selection menu, or you can use efibootmgr to pre-select the new entry for the next boot. If you're successful, you'll notice in the dmesg output (you do get dmesg output, right?) that the automatic unsealing of the LUKS passphrase will fail and that you'll then be prompted for the passphrase:

Decryption try 1
🔐 Please unlock disk mylvm:  (press TAB for no echo)

Enter the passphrase and complete the boot process. You are now ready to set up the TPM to automatically decrypt your drive on subsequent boots.

### Create a binary LUKS passphrase

You could use your typeable passphrase for this, but it's easy enough to create a new binary passphrase for your disk. My TPM is limited to 128 octets per sealed entity, so I created a 128-octet binary passphrase in a directory on my root partition readable only by root, and then added it to the LUKS header:

dd if=/dev/urandom of=/etc/keys/luks-tpm.key bs=128 count=1
cryptsetup luksAddKey /dev/nvme0n1p5 /etc/keys/luks-tpm.key

where /dev/nvme0n1p5 is the device node for my LUKS partition. In the second step, you'll be prompted to enter an existing passphrase.

### Seal the passphrase

The final step is to seal this new passphrase to the current set of PCR values for registers you identified early on as stable across boots. I have a script that does this, which I'll explain in as much detail as I understand it. If you are a trusted computing expert, I would love to hear if there is a better way to do this.

#! /bin/bash

set -e

mkdir -m 0700 /tmp/tpm2
cd /tmp/tpm2

tpm2_createpolicy --policy-pcr -l sha256:0,1,2,3,4,5,7 -L policy.digest
tpm2_createprimary -C e -g sha256 -G rsa -c primary.context
tpm2_create -g sha256 -u obj.pub -r obj.priv -C primary.context -L policy.digest \
tpm2_evictcontrol -C o -c 0x81000000 || true
tpm2_evictcontrol -C o -c load.context 0x81000000

cd -
rm -rf /tmp/tpm2

tpm2_getcap handles-persistent
tpm2_unseal -c 0x81000000 -p pcr:sha256:0,1,2,3,4,5,7 | hd
• tpm2_createpolicy creates a policy that authorizes some subsequent action on the basis of specified PCR values.
• tpm2_createprimary creates a primary resident RSA key pair object under the endorsement hierarchy and returns a context object that allows one to subsequently perform TPM operations with this resident key pair, the outputs of which can be authenticated as having been performed on this device. The endorsement hierarchy is used to make assertions that are constrained to a specific TPM instance via chaining to primary keys that are resident to (i.e., neither importable to nor exportable from) the TPM.
• tpm2_create creates a sealing object from the specified input (the LUKS passphrase) that is tied to the above primary object according to the above policy. The output is a pair of objects representing the sealed passphrase that then need to be loaded into the TPM.
• tpm2_load loads the objects created in the previous step into the TPM.
• The first tpm2_evictcontrol removes whatever persistent object is stored in the TPM's NVRAM at the given handle, if any.
• The second tpm2_evictcontrol moves the newly-loaded object into NVRAM at the given handle.

The remaining commands delete all the ephemeral state required to load the sealed passphrase into the TPM, and then demonstrate that the key can be unsealed. You may wish to compare the output of the final command to the hex dump of the input passphrase at /etc/keys/luks-tpm.key.

### All set

Reboot and make sure the root filesystem is mounted without requiring passphrase entry. If this fails, you'll need to track down the cause, which will likely wind up requiring tweaks to the set of modules or libraries that need to be installed into the initrd or to the set of PCR values required to match by the policy.

## Security considerations

• Without platform-specific documentation, there's really no way to know which are the right set of PCRs to include, nor even if the PCRs cover the entirety of the code and critical configuration required to prevent silent compromise.
• Understand your adversary and set appropriate expectations for any kind of tampering countermeasure. In other words, review this classic opinion piece.

## To do

• Debianize the implementation, which likely means parameterizing much of the implementation into configuration
• Write a script to predict the set of PCR values for a boot chain including a new unified kernel blob so I do not need to assume a secure boot state (beyond a chosen anchor set of PCR values for layers below the OS) in order to re-seal the passphrase
• Passphrase entry timeout and reboot to "safe" insecure OS
• Support multiple kernels at different persistent object handles