Date

Introduction

Recently, I upgraded my NAS machine and decided I wanted to set up full disk encryption with the disk encryption key sealed inside a TPM. This setup is very similar to Microsoft's BitLocker disk encryption. Just to make it more difficult for myself, I decided to use a TPM2 device rather than an old TPM1.2 device. There are existing projects that implement this functionality for Linux using TPM1.2, but I did not find one for TPM2.

TPM? TPM2?

A TPM, or Trusted Platform Module, is a small and supposedly tamper-resistant chip that can be added to a computer in order to safely store some secret information. Among other features, a TPM can be programmed such that it only allows access to its secret information if the computer has booted up in the correct state (e.g. has not been booted from an external USB thumb drive). If a disk encryption key is stored in a TPM, then it can be configured to automatically unlock the root disk during a normal boot but not unlock it when something in the boot configuration changes.

There are two major versions of TPM devices, 1.2 and 2.0. A good description of the changes in 2.0 can be found here.

Hardware setup

I am using an ASRock E3V5 WS motherboard with ASRock's TPM-S 2.0 device which is based on a Nuvoton NPCT650. The TPM module just needs to be plugged into the matching connector near the bottom of the motherboard.

In the UEFI configuration for the motherboard, I ensured that the TPM module was detected, that the software/Intel ME emulated TPM was disabled, and that the TPM was set up to use SHA-2 hashes (which actually causes both SHA-1 and SHA-2 hashes to be available).

Software setup

On the software side, I am using IBM's TPM 2.0 TSS along with some custom shell scripts. Unfortunately, despite claims on James Bottomley's linked blog, Ubuntu 16.04 LTS did not seem to have a package for the IBM tools. I built the tools from source after applying this patch that disables the building of a shared object for the common code (preferring to statically link it instead). After compiling, I followed James Bottomley's instructions for creating a 81000001 key handle.

At this point, I created the /opt/tpmdisk directory and placed the .sh files from my repository into it. Finally, I copy the zzz-tpmdisk file into /usr/share/initramfs-tools/hooks/.

Disk setup

At this point, I create a 32-byte file of random data and store it at /keys/rootkey.bin. Ensure that this file is readable only to root. The purpose of this file is to allow the shell scripts to add/remove LUKS key slots without needing the "recovery" password. Because this file is being stored on the disk that is to be encrypted, it should not introduce an extra security vulnerability. I use the cryptsetup utility to set key slot 7 to the randomly generated file and key slot 6 to a "recovery" password. /opt/tpmdisk/new-disk-key.sh can now be run (as root) to create a third disk encryption key that will be sealed to the TPM. The sealed key will be stored in /opt/tpmdisk/sealedkey_{priv,pub}.bin

Finally, ,keyscript=/opt/tpmdisk/unseal-disk-key.sh is appended after the luks option in /etc/crypttab and the initramfs is regenerated.

Code walkthrough

new-disk-key.sh

This script is relatively straightforward. It creates a temporary directory and then creates inside it a temporary file with 32 random bytes. This will be the new sealed-to-the-TPM encryption key, but it is currently in the clear. The script removes the existing data in the appropriate LUKS key slot and assigns the newly-generated key to that key slot. The script also invokes seal-to-pcrs.sh to perform the actual TPM sealing operation. The script finally cleans up and removes the cleartext copy of the new key.

seal-to-pcrs.sh

This script performs the bulk of the work to seal secret data to a TPM. The data is sealed such that "PCR values" in the TPM must match certain values in order for the data to be unsealed. "PCR values" are special append-only registers that log various steps of the boot process. The PCRs that are used are specified (redundantly) by the PCRS and PCR_BITS variables in the script. The PCR_BITS variable is a hex value that contains 1 bits in the bit position corresponding to values in the PCRS variable.

The PCR values chosen here are modified from the BitLocker recommendations. However, note that all PCRs 8 and above are created by the operating system rather than by the BIOS/UEFI, and Linux does not seem to use any of them. Also, for a TPM2 system, PCR 7 contains the UEFI Secure Boot configuration, so that is included in my list of PCRs.

This script first gathers the current value of all the PCRs into a temp file (one line for each value). These values are then combined into a textual policy file. Note that TPM2 policies are rather complicated and can contain an arbitrary amount of AND and OR terms. Here we are just creating a single AND term with all of the PCR values. The textual policy is then converted into a binary policy. The data is finally sealed to the TPM using that policy file.

unseal-disk-key.sh

This simple script tries to use unseal-from-pcrs.sh to unseal the disk encryption key. If this fails for any reason (such as the kernel having been modified/upgraded), it falls back to the normal askpass utility that belongs to cryptsetup (where the "recovery" password can be entered). This is necessary because otherwise the Ubuntu initramfs will loop forever (or a very long time) repeatedly trying and failing to unlock the disk.

unseal-from-pcrs.sh

This script performs the bulk of the work to unseal data from a TPM. First, it loads the sealed data from files on disk into the temporary storage of the TPM. It then starts a session where policy verification can be performed. It then instructs the TPM to try to check the policy using (hopefully) the same list of PCRs that was used to create it. It then calls the unseal command and, if the PCR values match, the TPM will unseal the data. The script finally cleans up.