Maria Nicolae's Website

RSS Feed (What is RSS?)


Back to Blog.

Arch Linux Full-Disk Encryption with TPM2 + LUKS Passphrase

Getting the benefits of TPM2 without fully trusting it.

Maria Nicolae,

In a full-disk encryption setup, Secure Boot and TPM2 unlocking can help mitigate certain types of attacks, like evil maid attacks and rootkits. In principle, this is not mutually exclusive with password-based authentication either, since TPM provides a mechanism to prompt the user for a PIN when unlocking the disk. That is to say, TPM unlocking is not necessarily unattended unlocking.

Such a setup, however, still rests all security on the TPM, which is undesirable if the user does not entirely trust TPM. Why might this be the case? First of all, it is known that some implementations of TPM unlocking are vulnerable to key sniffing attacks. Besides that, there is the fact that TPM's root of trust, the Storage Root Key, cannot be replaced by the user with a key that they generated themselves, meaning that TPM is treacherous and not fully under the user's control.

To address this on my own computers, I created a non-standard setup in LUKS in which unlocking the disk involves both the TPM and an LUKS passphrase prompt. That way, a compromise of the TPM would not compromise the entire full-disk encryption; there would still be the LUKS passphrase to crack, as in a "vanilla" LUKS setup without Secure Boot and TPM. Here, I take you through what I did to set this up while (re-)installing Arch Linux.

Designing the Setup

In a typical LUKS full-disk encryption setup on a UEFI system, the layout of the disk partitions is something like this:

When unlocking an LUKS volume using a passphrase, a key is generated by running the passphrase through some key-derivation function. This key is not what is used to encrypt and decrypt the volume, however. Instead, it is merely used to access the volume key, an encrypted copy of which is stored in the LUKS volume's header. Similarly, in a TPM unlocking setup, the volume key is encrypted with a key stored in the TPM. Thus, to achieve the non-standard setup that I want, I needed to have the volume key be doubly encrypted, first with a passphrase, and then with the TPM's key.

LUKS does not provide any mechanism to encrypt the volume key in multiple layers like this. It does allow for multiple "keyslots", differently-encrypted copies of the volume key stored side-by-side, but this creates an OR relationship between the various factors of authentication, in which any one is sufficient on its own, rather than the AND relationship that I was after.

One way to achieve this, the first that I thought of, would be to nest two LUKS volumes:

Here, the outer volume would unlock via TPM, while the inner layer unlocks via a passphrase. This approach, however, would mean that any disk I/O with the root volume would have to go through two passes of LUKS encryption, which is bad for performance. Ideally, only the volume key would be doubly encrypted, not the contents of the volume itself.

Fortunately, LUKS allows us to "detach" a volume's header, and have separate files for the header and the bare encrypted volume itself. What I then came up with was to have the root volume be bare, with its header being stored in a separate LUKS volume:

An LUKS header is 16 MiB in size, so in theory I only needed this first LUKS volume to be 32 MiB in size, but I allocated some extra space to be on the safe side.

Finally, I wanted to have swap space, and to be able to hibernate/suspend-to-disk, so I replace the root volume here with an LVM volume containing the root and swap volumes within it:

Implementing the Setup

Implementing this setup involved three main steps. First, while installing Arch Linux, I created and formatted the disk partitions. Next, at the end of the installation process, before exiting the installation medium, I set up the two-layer unlocking process, to be done on each boot of the installed system. Finally, after I finished installing Arch Linux and booted this new installation, I set up Secure Boot and TPM2 unlocking.

(Full disclosure: I did this months ago, and did not journal the process live when I did it. Thus, to refresh my memory, I did this setup again in a virtual machine as I was writing this blog post.)

Formatting the Disk

After booting into the Arch Linux installation medium, the disk I wanted to boot from was mounted as /dev/sda. First of all, I filled the disk with secure random bytes:

cat /dev/random > /dev/sda

I did this to minimise the amount of information that the encrypted state of the disk would give away to an attacker, since encrypted data looks like random data. If the disk had instead been filled with zeroes when I started, for example, it would be obvious which blocks had been written to. This could give away how full the disk was, and possibly even some information about file sizes and directory structure.

After this, to begin formatting the disks, I ran

fdisk /dev/sda

to enter an interactive disk partitioning dialog. From here, I created a GPT (rather than MBR) partition table and three partitions, of sizes 1 GiB, 64 MiB, and the rest of the disk respectively. For the first partition only, I changed its partition type (command t in fdisk) to EFI System, from the default of Linux filesystem. I then saved this partition table and exited the fdisk dialog using the w command.

Next, I formatted the partitions, including setting up LUKS and LVM. First of all, I formatted the EFI system partition as FAT32:

mkfs.fat -F 32 /dev/sda1

I then formatted /dev/sda2 as an LUKS volume, which is to be the "outer" layer of this two-layer encryption setup:

cryptsetup luksFormat /dev/sda2

Here, I was prompted to set a passphrase for the volume. Since this volume is to be unlocked by TPM in the finished setup, I set a temporary weak passphrase, confident that no attacker would gain access in the brief timespan between now and when I finished this process. In the finished setup, I did also include a second keyslot in this volume, unlocked by a strong "recovery" passphrase, for me to use in the event that the Secure Boot state broke, so that such an event would not permanently lock me out. If I had been more paranoid about security, I could have set this recovery passphrase from the very beginning, rather than a temporary weak passphrase; you can choose either option if you're setting this up yourself.

Anyways, I then unlocked this volume, using the passphrase I just set:

cryptsetup open /dev/sda2 outer

Finally, I formatted /dev/sda3 as the "inner" LUKS volume, with a detached header stored encrypted inside the outer volume:

cryptsetup luksFormat /dev/sda --header /dev/mapper/outer

This volume will be unlocked with a passphrase in the finished setup, so when this command prompted me to set a passphrase, I used the one that I wanted to have to enter on boot. I chose this passphrase to be six random words from the Electronic Frontier Foundation's list of 7776 words, giving me log2(77766)77 bits of entropy. While I simply used a random number generator on a computer to select the words, one could use six-sided dice for this, since the length of the list is a power of six (65=7776) by design.

The last step in formatting the disk was to set up the LVM volumes in the inner LUKS volume. First of all, I unlocked this inner volume:

cryptsetup open /dev/sda3 --header /dev/mapper/outer cryptlvm

Next, I created the LVM volume

pvcreate /dev/mapper/cryptlvm
vgcreate RootGroup /dev/mapper/cryptlvm

and the root and swap subvolumes within:

lvcreate -L swap-size -n swap RootGroup
lvcreate -l 100%FREE -n root RootGroup

Right after this, I shrunk the root volume by 256 MiB, to allow e2scrub to be used on the ext4 filesystem that this volume will be formatted to:

lvreduce -L -256M RootGroup/root

Finally, I formatted the volumes:

mkfs.ext4 /dev/RootGroup/root
mkswap /dev/RootGroup/swap

After formatting the volumes, I mounted them

mount /dev/RootGroup/root /mnt
mount --mkdir /dev/sda1 /mnt/boot/efi
swapon /dev/RootGroup/swap

and proceeded with the normal Arch Linux installation process.

Setting up Unlock on Boot

The Linux boot process happens inside an inital ramdisk. On Arch Linux, this initial ramdisk is generated by the mkinitcpio script, which runs after boot-relevant system packages, such as the Linux kernel itself, are installed or upgraded. Thus, setting up unlock on boot was a matter of configuring this script.

After finishing the normal installation process, mkinitcpio had already been run once automatically, with a default configuration, so I started by cleaning up the resulting initial ramdisks:

rm /boot/initramfs*.img

(Note that I am now chrooted into the newly installed system.) To begin configuring mkinitcpio, I edited the existing /etc/mkinitcpio.d/linux.preset file to

ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-linux"

PRESETS=('default')

default_uki="/boot/efi/arch-linux.efi"

This configuration tells mkinitcpio to generate a Unified Kernel Image (UKI), arch-linux.efi, in the EFI system partition. This is a single EFI executable that contains the initial ramdisk and all of its resources. Thus, only this one file will need to be signed once Secure Boot is set up. Next, inside /etc/mkinitcpio.conf, I set the variable HOOKS to

HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt lvm2 filesystems fsck)

The systemd hook specifies that a systemd-based initial ramdisk is to be used, which is the only one that supports TPM unlock on boot. At boot time, the sd-encrypt hook performs unlocking, and then the lvm2 hook opens the root and swap LVM subvolumes.

Next, I set the kernel command line by creating /etc/cmdline.d/cmd.conf with contents

root=/dev/RootGroup/root rw

specifying the root volume to be mounted at /. Note that there are no command-line arguments here for decryption, or for restoring from hibernation. The systemd-based initial ramdisk handles the latter automatically, and for the former, I instead specify the (two-step) unlocking process by creating the file /etc/crypttab.initramfs with contents

outer        UUID=uuid-of-/dev/sda2 none luks,tries=2147483647
cryptlvm PARTUUID=uuid-of-/dev/sda3 none luks,tries=2147483647,header=/dev/mapper/outer

This file is a table listing, in order, which unlocks of encrypted volumes are to take place during the boot process. The first column is the name that the unlocked volume will take. The second column is the block device to be unlocked; to obtain their UUIDs, I used the blkid command. The third column is a path to the passphrase file to be used for the unlock, if specified; I left this blank, of course. The fourth and last column is options for the unlock process; I set luks so that LUKS would be used, set tries to the maximum value (2311) for "unlimited" passphrase retries, and set header to specify the detached header for /dev/sda3.

At this point, the mkinitcpio configuration was complete. However, I wasn't quite ready to generate the UKI just yet. There is a configuration file /etc/vconsole.conf for the virtual console that is not present by default, but that mkinitcpio expects to exist when generating a systemd-based initial ramdisk, even if it's empty. Thus, I created this as an empty file:

touch /etc/vconsole.conf

At last, I was ready to generate the UKI:

mkinitcpio -P

Finally, I used efibootmgr to create a UEFI boot entry for this UKI:

efibootmgr --create --disk /dev/sda --part 1 --label "Arch Linux" --loader '\arch-linux.efi' --unicode

I then exited the chroot environment, shut down the computer, and booted into the newly installed system.

Secure Boot

Setting up Secure Boot consisted of three main steps. First, I generated some keys, then I set up UKI signing in mkinitcpio, and finally I enrolled the keys into the UEFI firmware for Secure Boot.

To start, I created a dedicated directory for key generation and navigated to it:

mkdir /etc/secureboot
cd /etc/secureboot

Next, I created a directory structure within this to mirror the EFI variable structure:

mkdir -p keys/PK keys/KEK keys/db keys/dbx

After key generation, this will be synchronised with the Secure Boot state to enroll the keys into the firmware. To clarify what's going on here, there are four stores of cryptographic tokens (keys or hashes) in the Secure Boot state:

My setup consisted of the simplest possible version of this, with only one key in the KEK and db each, and an empty/unused dbx. Next, I generated a UUID to be the "identity" for these keys of mine:

uuidgen --random > GUID

To generate the keys themselves, I wrote a Bash script genkeys with contents

#!/bin/bash

DAYS=365
KEYS=(PK KEK db)
SIGNERS=(PK PK KEK)
GUID=$(cat GUID)

for ((i=0; i<${#KEYS[@]}; i++)); do
    KEY="${KEYS[$i]}"
    SIGNER="${SIGNERS[$i]}"

    openssl req -newkey rsa:4096 -nodes -keyout "$KEY.key" -new -x509 -sha256 \
        -days "$DAYS" -subj "/CN=Maria $KEY Key/" -out "$KEY.crt"
    openssl x509 -outform DER -in "$KEY.crt" -out "$KEY.cer"
    cert-to-efi-sig-list -g "$GUID" "$KEY.crt" "$KEY.esl"
    sign-efi-sig-list -g "GUID" -k "$SIGNER.key" -c "$SIGNER.crt" \
        "$KEY" "$KEY.esl" "keys/$KEY/$KEY.auth"
done

Here, the DAYS variable specifies how long the keys last before expiring; I chose one year. This script uses the cert-to-efi-sig-list and sign-efi-sig-list commands from efitools.

To set up UKI signing, I created a post hook in mkinitcpio. To do this, I created the file /etc/initcpio/post/uki-sbsign with contents

#!/usr/bin/env bash

uki="$3"
[[ -n "$uki" ]] || exit 0

keypairs=(/etc/secureboot/db.key /etc/secureboot/db.crt)
for (( i=0; i<${#keypairs[@]}; i+=2 )); do
    key="${keypairs[$i]}" cert="${keypairs[(( i + 1 ))]}"
    if ! sbverify --cert "$cert" "$uki" &>/dev/null; then
        sbsign --key "$key" --cert "$cert" --output "$uki" "$uki"
    fi
done

copied from the Arch Linux wiki. This uses the sbverify and sbsign commands from sbsigntools. I then made this executable

chmod +x /etc/initcpio/post/uki-sbsign

and regenerated the UKI:

mkinitcpio -P

Finally, I enrolled these keys into Secure Boot; I've found that the exact procedure for this varies from machine to machine. In the aforementioned sbsigntools package, the command

sbkeysync --keystore /etc/secureboot/keys --verbose

enrolls all but the Platform Key. On some machines, getting this to work requires entering Secure Boot "setup" mode by enabling Secure Boot and deleting the existing PK via the UEFI boot menu. Additionally, the command

sbkeysync --keystore /etc/secureboot/keys --verbose --pk

is meant to enroll the Platform Key, but on some machines, this didn't work, and I instead needed to copy the PK certificate into the EFI system partition

cp /etc/secureboot/PK.cer /boot/efi/

so that I could enroll it via the boot menu. After doing this, I deleted this certificate copy:

rm /boot/efi/PK.cer

After all this, I rebooted, and ran bootctl to ensure that Secure Boot was enabled and enforcing (i.e. in "user" mode rather than "setup" mode).

TPM Unlocking

Finally, with Secure Boot up and running, I was ready to set up TPM unlocking for the "outer" layer of encryption on /dev/sda2. After ensuring in the boot menu that a suitable TPM2 device was enabled, I ran

systemd-cryptenroll /dev/sda2 --wipe-slot=empty --tpm2-device=auto --tpm2-pcrs=7

to add TPM unlocking as an additional keyslot. Here, the --wipe-slot=empty option ensures that no existing keyslots are wiped, and the --tpm2-pcrs=7 option makes TPM unlock if and only if the Secure Boot state matches this current state. Next, to make sure that the boot process attempts TPM unlocking, I added an option to the corresponding entry in /etc/crypttab.initramfs, changing it to

outer UUID=uuid-of-/dev/sda2 none luks,tries=2147483647,tpm2-device=auto

and regenerated the UKI for the third and final time:

mkinitcpio -P

Finally, I added a "recovery" passphrase as another keyslot

cryptsetup luksAddKey /dev/sda2

and removed the weak temporary passphrase that I had originally set for this outer volume:

cryptsetup luksRemoveKey /dev/sda2

The recovery passphrase will be used if TPM unlocking does not occur, for example if the Secure Boot state has changed. Since I will only rarely need to use this passphrase, I did not memorise it, merely recording it in my password manager. I set this recovery passphrase to be 25 random digits and lowercase letters (giving log2(3625)129 bits of entropy), with groups of 5 characters separated by hyphens for easy reading (for example, 01234-56789-abcde-fghij-klmno).

At this point, the setup was finished. I rebooted my computer one last time, confirming that I was only prompted for one passphrase, for the inner layer of encryption.