4

The ultimate guide to Full Disk Encryption with TPM and Secure Boot

 2 years ago
source link: https://blastrock.github.io/fde-tpm-sb.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Securing your laptop

Now that you have everything needed, here is my plan.

What we want to do is to store the key to decrypt the partition in the TPM. And we want that TPM to only give back that key if a trusted software is running, guaranteed by Secure Boot.

Since your kernels and GRUB are stored on an unencrypted partition, you can't trust them. So we'll need to sign our kernel and initrd and have the TPM give back the key only if a kernel with the right signature is booted. This ensures that we have a chain of trust from the EFI firmware (which we will check too).

We are going to do this on Debian unstable, but it should work with other Debian-based distros like Ubuntu. If you are on ArchLinux, it looks like there is almost nothing to do as everything is handled by systemd-cryptenroll. While systemd-cryptenroll probably works on Debian, it does not work with an encrypted root partition.

Set up Secure Boot with your own keys

You most likely already have Secure Boot enabled and working. You can easily check for that:

$ mokutil --sb-state
SecureBoot enabled

If you don't, go to your UEFI setup and enable it.

Even now that you have Secure Boot enabled, your kernel is signed by Debian which boots a shim itself signed by Microsoft. This means that anybody can run untrusted code on your computer, like Windows or a live Linux distro. This is not a problem, what we want is to be able to differentiate the software you trust from the software you don't. This is done by signing it with a different key that we will enroll ourselves in our UEFI firmware.

There is a very complete guide about Secure Boot, containing instructions to generate your own keys. If you want to follow what I did, I generated them in /root/secureboot/keys. We'll only need the DB key for now, but you can read the rest of the guide if you are curious about the other keys.

So, copy DB.cer, DB.esl and DB.auth to your EFI partitions, somewhere like /boot/efi/certs/, reboot into UEFI setup and enroll the new key (you'll probably only need one of those files).

Sign your kernel with your new key

We will skip GRUB in the boot process. GRUB is signed by Debian and the shim that boots it is signed by Microsoft. We don't want to re-sign grub and then add our signature keys to it. This is probably feasable, but the chain of trust is simpler if we just skip GRUB and have our UEFI firmware directly load our Linux kernel.

We could just sign the official Debian kernel, beacuse it's compiled with an embedded boot loader called EFI stub. The issue with that is that the initrd image will not be signed. If you remember what we said above, that image is located in the /boot partition which is not encrypted, so an attacker can insert untrusted code in there without breaking Secure Boot.

What we want to do is to bundle our kernel, our initrd and the kernel command line in one single bootloader. As a bonus, it would be nice to do that automatically every time a new kernel is installed.

Create a file named /etc/kernel/postinst.d/zz-update-efistub (the zz part is important so that this is the last script run).

#!/bin/sh

objcopy \
    --add-section .osrel="/usr/lib/os-release" --change-section-vma .osrel=0x20000 \
    --add-section .cmdline="/cmdline" --change-section-vma .cmdline=0x30000 \
    --add-section .linux="/vmlinuz" --change-section-vma .linux=0x40000 \
    --add-section .initrd="/initrd.img" --change-section-vma .initrd=0x3000000 \
    "/usr/lib/systemd/boot/efi/linuxx64.efi.stub" "/boot/efi/EFI/debian-direct/Linux.efi"

sbsign \
    --key /root/secureboot/keys/DB.key \
    --cert /root/secureboot/keys/DB.crt \
    --output /boot/efi/EFI/debian-direct/Linux.efi \
    /boot/efi/EFI/debian-direct/Linux.efi

This script makes use of the EFI stub provided by systemd-boot. Replace the paths to your Securet Boot keys in the script. The output file will go into /boot/efi/EFI/debian-direct, I called it like that because it skips GRUB and goes directly to linux, but you are free to rename it however you want.

The kernel command line must be storred in /cmdline for the script to pick it up.

root=/dev/mapper/machine--vg-root rootfstype=ext4 add_efi_memmap panic=0 ro quiet

Replace the path to your root file system and the partition type. If you encounter problems, you might want to remove the quiet part to have a more verbose output.

That panic=0 part is weird, yet important, we will explain it later.

We also need to run this script whenever the initramfs is updated, set the execution permission and run it.

# mkdir -p /etc/initramfs/post-update.d
# ln -s ../../kernel/postinst.d/zz-update-efistub /etc/initramfs/post-update.d/zz-update-efistub
# chmod +x /etc/kernel/postinst.d/zz-update-efistub
# mkdir /boot/efi/EFI/debian-direct
# /etc/kernel/postinst.d/zz-update-efistub

Now you can add an entry in your EFI setup with that new bootloader:

# efibootmgr --disk /dev/nvme0n1 --create --label "debian-direct" --loader '\EFI\debian-direct\Linux.efi'

Try to reboot and run this bundle. If this is not your default entry, you might want to reorder the entries with efibootmgr or in the UEFI setup. If don't want it to be your default entry, you should have a way to pop the boot selection menu during boot. On my laptop, I need to spam the F12 key because the UEFI boots really fast.

If you managed to boot, congratulations! Only two more days are needed to complete the setup.

Unlock the LUKS partition with the TPM

I said that we will use the TPM to store the LUKS key and give it back only when the chain of trust is unbroken. The TPM can store a key encrypted with hash values coming from what are called PCRs. You can find a complete list of PCRs here. In this guide we will use just the following ones, but you are free to do as you like:

  • PCR0: Core System Firmware executable code

  • PCR2: extended or pluggable executable code

  • PCR7: Secure Boot State

The first two contains a hash of the UEFI firmware and the loaded modules. PCR7 contains a hash of a state that depends on which key was used to verify the signature of the EFI bootloader. When booting another bootloader (like a live distro or Windows), PCR7 will hold a different value which will not be able to unlock the LUKS key stored in the TPM.

It looks like some UEFI firmwares do not change PCR7 depending on which signature was found in the bootloader. This defeats the purpose of this guide, and if that's your case... well, you're back to using a passphrase. If you want to check it out before continuing this guide, you should try to run tpm2_pcrread and look at the PCR7 value (SHA1 or SHA256, it doesn't matter here). Run that command after booting through the debian-direct bundle we just created, and after booting through the usual GRUB boot. It should contain two different values.

Ok, still with us? Make sure that for the following part you didn't boot through GRUB and you are running the new debian-direct bundle. We will store the LUKS key with the current state of the TPM's PCRs.

First, we need to create a new key for the LUKS partition:

# dd if=/dev/random bs=64 count=1 | xxd -p -c999 | tr -d '\n' > /root/luks_key
1+0 records in
1+0 records out
64 bytes copied, 5.6501e-05 s, 1.1 MB/s
# cryptsetup luksAddKey /dev/nvme0n1p3 /root/luks_key --pbkdf-force-iterations=4 --pbkdf-parallel=1  --pbkdf-memory=32
Enter any existing passphrase: <enter your existing passphrase here>

For people wondering why we use a hexadecimal key instead of just a binary one, it's because the tool we'll use does not support binary keys.

For people wondering why we are specifying these PBKDF parameters, it's because PBKDFs are used to derive long keys from short, low entropy passphrases through a costly computation. In our case, the key already has 512 bits of entropy which is what is needed by the AES algorithm used. The simple thing to do would be to completely disable the PBKDF, but cryptsetup does not allow that.

Let's register that new key into the TPM:

# tpm2-initramfs-tool seal --data $(cat /root/luks_key) --pcrs 0,2,7

You can tweak the PCRs to use here.

Now that the key is registered, we need to use it to unlock the partition during boot. The only place this can be done is in the initrd image, where the usual passphrase-based unlock occurs.

The basic tpm2-initramfs-tool will only try to unlock with the TPM. Some times this will fail. Depending on what PCRs you have used, the TPM may be in a different state than now. When this happens, you want to be able to input your usual passphrase, boot your Linux, and fix the situation by resealing the key with the above command.

To still be able to fallback to the passphrase method, let's add a new script in /etc/initramfs-tools/tpm2-cryptsetup:

#!/bin/sh

[ "$CRYPTTAB_TRIED" -lt "1" ] && exec tpm2-initramfs-tool unseal --pcrs 0,2,7

stty -echo
echo -n "Please enter the passphrase for $CRYPTTAB_NAME ($CRYPTTAB_SOURCE): " >&2
read pass
echo >&2
stty echo
echo -n $pass

stty -echo allows the TTY to stop showing on the screen what we type, like when we type a password in sudo. stty echo reenables it, but for some reason that command fails, I couldn't fix it. It's fine though, as that TTY will be reset when the kernel boots.

Make it executable, even if we will never run it on our distro, otherwise update-initramfs will complain.

# chmod +x /etc/initramfs-tools/tpm2-cryptsetup

Add a hook to copy all this stuff into the initrd image by creating a file named /etc/initramfs-tools/hooks/tpm2-initramfs-tool:

. /usr/share/initramfs-tools/hook-functions

copy_exec /usr/lib/x86_64-linux-gnu/libtss2-tcti-device.so.0
copy_exec /usr/bin/tpm2-initramfs-tool

copy_exec /etc/initramfs-tools/tpm2-cryptsetup

Update your /etc/crypttab file:

nvme0n1p3_crypt UUID=375027ee-9720-42ce-bd57-aec0e76f8b4a unseal luks,discard,keyscript=/etc/initramfs-tools/tpm2-cryptsetup

You just need to edit the line you already have in there. Replace none by unseal and add the keyscript option.

Recreate the initrd image:

# update-initramfs -u -k all
update-initramfs: Generating /boot/initrd.img-5.16.0-6-amd64
warning: data remaining[91961712 vs 91972527]: gaps between PE/COFF sections?
warning: data remaining[91961712 vs 91972528]: gaps between PE/COFF sections?
Signing Unsigned original image

You can now reboot and your encrypted partition will be unlocked straight away without passphrase! If you try and reboot through GRUB however, the TPM will fail and our script above will ask for the passphrase to unlock the partition.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK