GRUB2 encrypted boot unlock using a TPM

This post describes who to use Grub2 to unlock a LUKS encrypted /boot partition using a keyfile that sealed using a TPM. The original patches for this are from Hernan Gatta and are on the grub2-devel mailing list. These can be applied onto the current Grub2 master branch, although involves some manual merging. Then this patched Grub2 can be used to boot a Fedora 36 virtual machine with encrypted boot partition without manual interaction.

These instructions follow what Hernan Gatta described in the emails on the list, and apply them to a Fedora 36 system.

Install Fedora 36

Default Fedora 36 install. Automatic setup using virt-install:

cat << EOF > kickstart.cfg
  graphical
  keyboard --vckeymap=us --xlayouts='us'
  lang en_US.UTF-8
  url --url="https://download.fedoraproject.org/pub/fedora/linux/releases/36/Server/x86_64/os"
  %packages
  @^custom-environment
  @guest-agents
  @standard
  %end
  firstboot --enable
  ignoredisk --only-use=vda
  autopart --encrypted --passphrase=pw2233
  clearpart --none --initlabel
  timezone Europe/Berlin --utc
  rootpw rootpw22
  reboot
EOF

Then create the VM:

virt-install \
  --name "enc-boot" \
  --boot uefi \
  --memory 2048 \
  --vcpus 2 \
  --disk size=8 \
  --location "https://download.fedoraproject.org/pub/fedora/linux/releases/36/Server/x86_64/os" \
  --os-variant fedora36 \
  --initrd-inject "./kickstart.cfg" \
  --extra-args="inst.ks=file://kickstart.cfg"

Use automatic partitioning, enable encryption. This results in a partition layout like this:

[root@fedora ~]# lsblk -o NAME,FSTYPE,SIZE,TYPE,MOUNTPOINTS
NAME                                          FSTYPE       SIZE TYPE  MOUNTPOINTS
sr0                                                       1024M rom
zram0                                                      5.8G disk  [SWAP]
vda                                                         20G disk
├─vda1                                        vfat         600M part  /boot/efi
├─vda2                                        ext4           1G part  /boot
└─vda3                                        crypto_LUKS 18.4G part
  └─luks-ac79e1eb-25a9-4b36-ade8-da53150718f3 btrfs       18.4G crypt /home
                                                                      /

A standard ESP partition, unencrypted /boot, and / and /home on a LUKS encrypted btrfs.

Disable SecureBoot

Our new grub binary is not signed and can not be booted on a SecureBoot enabled system unless further steps are taken, which not in the scope of this article. To be able to boot the patched grub later, let's just disable secure boot.

mokutil --disable-verification

Choose a temporary password. You will be asked to enter it (or parts of it) once when rebooting. Then reboot and follow the instructions during boot to disable SecureBoot. Then check if SecureBoot is really disabled:

[root@fedora ~]# mok-util --sb-state
SecureBoot enabled
SecureBoot validation is disabled in shim

Disable Boot Loader Spec

Only needed if BLS patches not used.

Fedora uses the Boot Loader Spec. This places some parts of the Grub2 config in extra files on /boot. This is not yet supported by upstream Grub, so disable it for now:

sed -i s/GRUB_ENABLE_BLSCFG=true/GRUB_ENABLE_BLSCFG=false/ /etc/default/grub
grub2-mkconfig -o /boot/grub2/grub.cfg
rm -r /boot/loader

Build Grub with Patches

The following tools are needed to build Grub2 from source:

dnf install \
  make \
  binutils \
  bison \
  gcc \
  gettext-devel \
  flex \
  git \
  patch \
  automake

Then pull the Grub2 sources including the patches for unlocking /boot. This branch also includes the patch from the Fedora version of Grub2 that adds support for Boot Loader Specification (BLS), which is used in Fedora 36. These commands pull and build Grub2:

git clone https://github.com/osteffenrh/grub2
cd grub2
git checkout tpm-unlock-BLS
./bootstrap
mkdir build
./configure --with-platform=efi --prefix=$(realpath ./build)
make
make install

If you don't want BLS, use the tpm-unlock branch instead.

Now make a grub.cfg (to be baked into the grub image) with this content:

cd build
cat << EOF > grub.cfg
tpm2_key_protector_init -k (hd0,gpt1)/efi/fedora/sealed_key
cryptomount -k tpm2 (hd0,gpt2)
configfile (crypto0)/grub2/grub.cfg
EOF

./bin/grub-mkstandalone \
    --modules="part_gpt cryptodisk luks luks2 pbkdf2 ext2" \
    -o grub2-standalone.efi \
    -O x86_64-efi \
    "/boot/grub/grub.cfg=./grub.cfg"

There is our fresh grub binary:

# file grub2-standalone.efi
grub2-standalone.efi: PE32+ executable (EFI application) x86-64 (stripped to external PDB), for MS Windows

Replace fedora grub:

cd /boot/efi/EFI/fedora
mv grubx64.efi grubx64_fedora.efi
cp /root/grub2/build/grub2-standalone.efi grubx64.efi

Encrypt /boot

umount /boot/efi
cd boot
tar cf ../b.tar .
cd ..
umount /boot
wipefs --all /dev/vda2
cryptsetup  luksFormat --type luks1 --pbkdf pbkdf2 --force-password /dev/vda2

Choose some password. Later unlocking will be automatic, but we need this for manual interaction.

Add a keyfile for automatic unlocking:

mkdir -p /etc/keys/
dd if=/dev/urandom of=/etc/keys/boot.key bs=1 count=32
chmod 400 /etc/keys/boot.key
cryptsetup luksAddKey /dev/vda2 /etc/keys/boot.key --pbkdf=pbkdf2 --hash=sha512

Enable automatic unlocking during Linux boot process:

echo "boot_crypt /dev/vda2 /etc/keys/boot.key luks,discard" >> /etc/crypttab
systemctl daemon-reload

Unlock now and add a file system:

systemctl start systemd-cryptsetup@boot_crypt
mkfs.ext4 /dev/mapper/boot_crypt

Replace the existing fstab entry for /boot with

/dev/mapper/boot_crypt /boot ext4 defaults  1 2

Then:

systemctl daemon-reload
mount /boot
mount /boot/efi

Add the original content back in and regenerate the Grub2 config:

cd /boot
tar xf ../b.tar
rm ../b.tar
grub2-mkconfig -o /boot/grub2/grub.cfg

Now it looks like this:

[root@fedora boot]# lsblk -o NAME,FSTYPE,SIZE,TYPE,MOUNTPOINTS
NAME                                          FSTYPE       SIZE TYPE  MOUNTPOINTS
sr0                                                       1024M rom
zram0                                                      5.8G disk  [SWAP]
vda                                                         20G disk
├─vda1                                        vfat         600M part
├─vda2                                        crypto_LUKS    1G part
│ └─boot_crypt                                ext4        1008M crypt /boot
└─vda3                                        crypto_LUKS 18.4G part
  └─luks-ac79e1eb-25a9-4b36-ade8-da53150718f3 btrfs       18.4G crypt /home
                                                                      /
cd /root/grub2/build/bin
./grub-protect --action=add --protector=tpm2 \
               --tpm2-keyfile=/etc/keys/boot.key \
               --tpm2-outfile=/boot/efi/EFI/fedora/sealed_key

Note: key sizes seem to be limited to 128 bytes max. I got error 0x1d5 from TPM otherwise. See https://github.com/tpm2-software/tpm2-tools/issues/1621

[root@fedora bin]# stat /boot/efi/EFI/fedora/sealed_key
  File: /boot/efi/EFI/fedora/sealed_key
  Size: 240           Blocks: 8          IO Block: 4096   regular file
Device: 252,1 Inode: 7           Links: 1
Access: (0700/-rwx------)  Uid: (    0/    root)   Gid: (    0/    root)
Context: system_u:object_r:dosfs_t:s0
Access: 2022-06-02 02:00:00.000000000 +0200
Modify: 2022-06-02 10:53:42.000000000 +0200
Change: 2022-06-02 10:53:42.020000000 +0200
 Birth: -

Ready to go! We could reboot the system now. Grub will auto-unlock the /boot partition and load the grub.cfg from Fedora. The normal boot menu should appear. You still have to enter the password for vda3 (aka /) during Linux boot.

Unlock root partition

One way to automatically unlock /root once Linux is running (during initrd phase) is systemd-cryptenroll. Other options are clevis, or embedding a keyfile into initrd directly. Since /boot is encrypted, that is file.

Let's use cryptenroll:

dnf install tpm2-tools
systemd-cryptenroll --tpm2-device=auto /dev/vda3

Edit /etc/crypttab and add tpm2-device=auto to the options of the /dev/vda3 LUKS device. Fedora uses UUIDs to identify partitions, so this will look different, but it is probably the first line:

# cat /etc/crypttab
luks-ac79e1eb-25a9-4b36-ade8-da53150718f3 UUID=79a3878e-180e-4510-978d-4a74ebd633f2 none tpm2-device=auto,discard,luks
boot_crypt /dev/vda2 /etc/keys/boot.key luks,discard

Then run:

dracut --force
reboot

The system should reboot. Grub should automatically unlock /boot, load the grub.cfg from there, and continue booting into Fedora. Then systemd should unlock /root automatically and you should end up at the login prompt. Tadaaaa!

ToDo and further steps

  • Re-enable SecureBoot. For this we need to sign the grub2.efi binary and enroll matching keys in EFI.
  • Tie the unlocking to a sensible set of PCRs. Right now we did not specify any and just used the defaults.