Full Disk Encryption on Arch Linux backed by TPM 2.0

Right now I have a new laptop running Arch Linux (more on that in a later post) and being security minded, I’d like my hard-drive to be encrypted. However, having to enter the encryption key every boot is a pain too.

From BitLocker and Android, I’ve came to accept that for moderate security, it is OK not to require a password on boot as long as the system being booted into is secure. That is, you cannot access any file or perform any operation until you login with the correct password.

However, just putting the encryption key on disk is not OK. Then anyone can just come, read the encryption key and use that to decrypt the storage, ruining the whole point of encryption. This is where Trusted Platform Module (TPM) comes in. I can store the encryption key inside the TPM and the TPM would only reveal it if the binary being booted can be trusted. Thus if someone boots from a live CD, they will not be able to read the encryption key and will not be able to decrypt the storage.

The TPM audits the system state by the use of Platform Configuration Registers (PCRs). When you query the TPM for the encryption key, it will check whether the PCRs matches the stored PCR or not. The key point is that they are values which cannot be set and can only be appended. Each process in the boot (e.g. BIOS/UEFI) would append relevant configurations to the PCRs. Thus even if you know the desired PCR states, you cannot set them!

Note: If you are on tpm-tools 4.0, then the commands are different. See this comment for the latest command lines.

PCR values on my system:

$ sudo tpm2_pcrlist 
sha1 :
0 : 1fd39d490ad7291d3f2dac54026a4cc99ca9786f
1 : e8bce68064885bdef8c4d73fa3306c332d70bf1a
2 : b2a83b0ebf2f8374299a5b2bdfc31ea955ad7236
3 : b2a83b0ebf2f8374299a5b2bdfc31ea955ad7236
4 : 9ff77ea5c2c7d0cbb4268697928d21547a41e340
5 : d2787021116ce3480792b9633dd155af7ec44047
6 : b2a83b0ebf2f8374299a5b2bdfc31ea955ad7236
7 : 511a3614be4ba059fafab45e33f29306dce2d95f
8 : 0000000000000000000000000000000000000000
9 : 0000000000000000000000000000000000000000
10 : 0000000000000000000000000000000000000000
11 : 0000000000000000000000000000000000000000
12 : 0000000000000000000000000000000000000000
13 : 0000000000000000000000000000000000000000
14 : 0000000000000000000000000000000000000000
15 : 0000000000000000000000000000000000000000
16 : 0000000000000000000000000000000000000000
17 : ffffffffffffffffffffffffffffffffffffffff
18 : ffffffffffffffffffffffffffffffffffffffff
19 : ffffffffffffffffffffffffffffffffffffffff
20 : ffffffffffffffffffffffffffffffffffffffff
21 : ffffffffffffffffffffffffffffffffffffffff
22 : ffffffffffffffffffffffffffffffffffffffff
23 : 0000000000000000000000000000000000000000

Here are the important PCRs (source):

  • PCR0: Core System Firmware Executable Code (changes when a BIOS update is performed)
  • PCR1: Core System Firmware Data (changes when you change your BIOS settings)
  • PCR4: Boot Manager (changes when you change the boot manager executable)
  • PCR7: Secure Boot Status

Let’s move on to the setup. Here’s my partition setup:

/dev/sdb5, mounted on / => Ext4 encrypted using LUKS
/dev/sdb2, mounted on /efi => EFI System Partition (ESP), unencrypted

Since anything inside root (/) is encrypted, we can trust that it is not tampered and thus does not need to be verified. The ESP, however, cannot be trusted as anyone can use a live CD and change the values. When the UEFI loads the boot manager, it will append the hash of the boot manager into PCR4. However, if the boot manageris not TPM-aware, then the kernel, initramfs or the kernel command lines will not be verified at all, making it easy for attackers to substitute another kernel and gain access to the system.

Luckily, systemd-boot provides a solution to this problem. There is a feature which allows you to combine the bootloader, kernel, initramfs and the kernel command lines into one big file which will then be loaded as the “boot manager” and thus if anything changes, then PCR4 will change!

Here’s the command to build the image and add it to your EFI Boot Menu.

# Assume that the correct kernel command line
# is stored in /boot/kernel-command-line.txt
objcopy \
--add-section .osrel="/usr/lib/os-release" --change-section-vma .osrel=0x20000 \
--add-section .cmdline="/boot/kernel-command-line.txt" --change-section-vma .cmdline=0x30000 \
--add-section .linux="/boot/vmlinuz-linux" --change-section-vma .linux=0x40000 \
--add-section .initrd="/boot/initramfs-linux.img" --change-section-vma .initrd=0x3000000 \
"/usr/lib/systemd/boot/efi/linuxx64.efi.stub" "/efi/EFI/Linux/Linux.efi"
# Add to boot manager
efibootmgr --create --disk /dev/sdb2 --part 2 --label "Arch Linux" --loader "\EFI\Linux\Linux.efi" --verbose

After that (assuming you already have an existing dm-crypt LUKS setup), we can generate a new key and add it to the LUKS partition.

dd if=/dev/random of=/root/secret.bin bs=32 count=1
cryptsetup luksAddKey /dev/sdb5 /root/secret.bin

Note that I’m leaving the key on-disk at /root/secret.bin. This should be OK as the permission is limited and it’s in an encrypted location. The reason is that every time we change the kernel, we need to re-add the key to the TPM.

Now let’s add the key to the TPM. Note that you may need to reset your TPM from the BIOS setup page for these commands to succeed. Also, I’m using PCR 0, 2, 4 and 7, but feel free to change based on your paranoia (but you will need to edit the list in the mkinitcpio hook as well).

tpm2_createpolicy -P -L sha1:0,2,4,7 -f policy.digest
tpm2_createprimary -H e -g sha1 -G rsa -C primary.context
tpm2_create -g sha256 -G keyedhash -u obj.pub -r obj.priv -c primary.context -L policy.digest -A "noda|adminwithpolicy|fixedparent|fixedtpm" -I secret.bin
tpm2_load -c primary.context -u obj.pub -r obj.priv -C load.context
tpm2_evictcontrol -c load.context -A o -S 0x81000000
rm load.context obj.priv obj.pub policy.digest primary.context

Now you can check that the key is successfully added:

$ sudo tpm2_listpersistent
persistent-handle[0]:0x81000000 key-alg:keyedhash hash-alg:sha256 object-attr:fixedtpm|fixedparent|adminwithpolicy|noda

We can also try to query the encryption key from the TPM. You should get the same value as secret.bin.

sudo tpm2_unseal -H 0x81000000 -L sha1:0,2,4,7

Now we need to setup initramfs to load the key from TPM on boot instead of prompting. Clone https://github.com/pawitp/arch-luks-tpm and copy the files to /etc/initcpio/hooks/encrypt-tpm and /etc/initcpio/install/encrypt-tpm respectively. Finally, edit /etc/mkinitcpio.conf to add “encrypt-tpm” in front of the “encrypt” hook.

After that, regenerate initramfs and the EFI image and reboot your system. You will find that… it doesn’t work! You’re still prompted for your encryption password. That is because you’ve just replaced your EFI image and thus your PCR4 changed. (If it worked, then you’re in serious trouble as the TPM or the UEFI BIOS is not doing what it should.)

Thus, every time you change your kernel, you must remove the old key from the TPM and re-add it again.

# Clear the old key
tpm2_evictcontrol -H 0x81000000 -A o
# Same commands as above
tpm2_createpolicy -P -L sha1:0,2,4,7 -f policy.digest
tpm2_createprimary -H e -g sha1 -G rsa -C primary.context
tpm2_create -g sha256 -G keyedhash -u obj.pub -r obj.priv -c primary.context -L policy.digest -A "noda|adminwithpolicy|fixedparent|fixedtpm" -I secret.bin
tpm2_load -c primary.context -u obj.pub -r obj.priv -C load.context
tpm2_evictcontrol -c load.context -A o -S 0x81000000
rm load.context obj.priv obj.pub policy.digest primary.context

Reboot again and viola! No password asked.

Note that if you’re suddenly prompted for the password without changing anything, then you have a good reason for doubting the integrity of your system. Did someone perhaps change your initramfs to steal your encryption password?

Finally, some notes on Secure Boot. Some might wonder why I did not enable Secure Boot to ensure that only trusted executable are booted. The problem is I think Secure Boot is very broken on x86-based systems due to its open nature. Anyone can boot a Windows Installation Media, press Shift + F10 and gain access to an Administrator command prompt. Even the secure boot solutions for Linux (PreLoader and shim) allow anyone to mark any binary as trusted right there on the boot screen!

Update: I have now found the right way to setup Secure Boot.

References:

Software Engineer