FAQ: What is a TPM and how can I use it on Linux?

Featured image

TLDR: The goal of this text is to make TPMs usable for tech nerds (not only TPM experts). There are a lot of important details, but documentation is scattered. I explain the basics of TPMs and how you can use them on Linux for full disk encryption (conceptionally and with Linux hands-on examples). The commands can also be found in this Github gist.

What is a TPM?

A TPM (Trusted Platform Module) is a dedicated security chip on your mainboard. Nowadays, only TPM version 2.0 is relevant. It’s very likely your device has one, as it’s a Windows (soft) requirement since 2016. For Windows 11, a TPM 2.0 is a hard requirement and often the only requirement why a system can not be upgraded. A TPM is a passive device. When software is not using it, it’s not doing anything. A TPM can be used as a random number generator or as a private key storage. For example, it can hold your private ssh keys or encrypt credentials of your systemd services.

TPM chip on mainboard

image source: 36C3: Hacking (with) a TPM (slides)

How can I use a TPM for FDE?

When speaking about TPMs and full disk encryption, a TPM can be used in two ways:

  • The TPM generates a high-entropy key used to encrypt the disk. The TPM is protected by a PIN, which can also be alphanumeric. During boot, you have to enter the correct PIN to decrypt the disk. The TPM implements a lockout mechanism, so brute forcing the PIN is not possible. In the end, using a TPM allows you to use a weaker password comparing it with a password directly used for decryption.
  • You may know it from Mac or Windows: You boot the device and it gets decrypted automatically. But only if the device was not modified. This is called Measured Boot: During boot, each boot component (like firmware and initrd) is hashed and verified by TPM’s PCR registers. Optionally, the user needs to enter a TPM PIN (so called pre-boot-authentication). How can data be safe when even the attacker is able to automatically decrypt the disk? The attacker can only boot the unaltered system and ends up seeing the user login screen and thus can’t access any data. If the attacker boots from an USB drive or backdoors the initrd, the PCR register values change and the TPM will not release the key (called unseal in the TPM world).

How do PCR registers work?

A TPM has multiple PCRs (Platform Configuration Register). Each PCR holds a sha256 hash value. During boot, each register is initialized with zeros. It’s not possible to reset PCR values back to zeros (volatile). You can think of an interface like this:

  • func tpm2_pcr_extend(pcr_index int, data bytes): You can send data to the TPM. This can be code or configuration. The data gets sha256-hashed and the output is saved into register pcr_index. Each PCR is addressed by its number/index. When you call tpm2_pcr_extend a second time, it prepends the current value of the PCR. So it’s only possible to extend data.
  • func tpm2_pcr_read(pcr_index int) bytes: Of course, you can also get the current value of a specific PCR register.

Linux hands-on #1: Check if TPM exists and explore PCRs

The commands and the output can also be found in this gist. You first have to install the tpm2-tools package. I’m currently using systemd version 257.5-3. You need to collapse, as it’s a lot of output.

Click to expand

Do I have a TPM?

kmille@linbox:~ journalctl --boot --dmesg --grep=tpm
Mar 05 12:14:27 linbox kernel: tpm_tis MSFT0101:00: 2.0 TPM (device-id 0x0, rev-id 78)

kmille@linbox:~ systemd-analyze has-tpm2
yes
+firmware
+driver
+system
+subsystem
+libraries
  +libtss2-esys.so.0
  +libtss2-rc.so.0
  +libtss2-mu.so.0

kmille@linbox:~ systemd-cryptenroll --tpm2-device=list
PATH        DEVICE      DRIVER 
/dev/tpmrm0 MSFT0101:00 tpm_tis

Show current PCR values

kmille@linbox:~ systemd-analyze pcrs
NR NAME                SHA256                                                          
 0 platform-code       580e88d67629044ad81f9cad63ce047917ce32e642ce75ab905705ed2f511a9f
 1 platform-config     b17bf98f95e6352fbd6ab4ee68112c685de93194178dc08c6424c747d76b0dbe
 2 external-code       ab4157ae0f9c56ffe355577b8db3eeefc66fd975418e10b456b42e052f4e81ea
 3 external-config     3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969
 4 boot-loader-code    3554fdc2b63bd8bee871a9e922e9f15abe5481b146561490b93a1740601fc241
 5 boot-loader-config  ef9e5396f8cdf684369567da4a0feae785c2a1e7fe9219805cbb3a2307545787
 6 host-platform       3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969
 7 secure-boot-policy  3b6994f4fc70b3f8715ade0cc477987d170d0d52ec19eca50dbfc33c3da70010
 8 -                   0000000000000000000000000000000000000000000000000000000000000000
 9 kernel-initrd       66a416ab8f64f7ea4b580af4b1a506f672e247adfcc6e6a6dc9fcf70018494a1
10 ima                 0000000000000000000000000000000000000000000000000000000000000000
11 kernel-boot         a5397d19b39229b5961029e1b42c908e7898444be24ed6353ec0d501b4a84e6b
12 kernel-config       0000000000000000000000000000000000000000000000000000000000000000
13 sysexts             0000000000000000000000000000000000000000000000000000000000000000
14 shim-policy         0000000000000000000000000000000000000000000000000000000000000000
15 system-identity     dcbf0accd4b9473d68fe4229757f99053ba495a52f7697d92d3b2b3acba1cdb5
16 debug               0000000000000000000000000000000000000000000000000000000000000000
17 -                   ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
18 -                   ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
19 -                   ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
20 -                   ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
21 -                   ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
22 -                   ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
23 application-support 0000000000000000000000000000000000000000000000000000000000000000

How are PCRs extended?

  1. Get current value of PCR 7
  2. Get sha256 of /etc/passwd (just an example)
  3. Extend PCR 7 with hash of /etc/passwd (you can only extend hash values. Probably because memory is limited in the TPM)
  4. Get new value of PCR 7
kmille@linbox:~ sudo tpm2_pcrread sha256:7 
  sha256:
    7 : 0x3B6994F4FC70B3F8715ADE0CC477987D170D0D52EC19ECA50DBFC33C3DA70010

kmille@linbox:~ cat /etc/passwd | sha256sum       
0e33a0c414b1d752930473d5eccf46ddf5bd2333328ed5562ec337b63c08465a  

kmille@linbox:~ sudo tpm2_pcrextend 7:sha256=0e33a0c414b1d752930473d5eccf46ddf5bd2333328ed5562ec337b63c08465a

kmille@linbox:~ sudo tpm2_pcrread sha256:7
  sha256:
    7 : 0xCEDF7419118AB3B7305A077E41BC9AA29E70C37FE2CB9712B17043213E1FFA83

Let’s reproduce the hash using python

#!/usr/bin/env python
import hashlib
from binascii import unhexlify

current_hash = unhexlify("3B6994F4FC70B3F8715ADE0CC477987D170D0D52EC19ECA50DBFC33C3DA70010")
new_data = unhexlify("0e33a0c414b1d752930473d5eccf46ddf5bd2333328ed5562ec337b63c08465a")

data = current_hash + new_data
sha2 = hashlib.sha256(data)
print(sha2.hexdigest())
kmille@linbox:~# ./hashit.py
cedf7419118ab3b7305a077e41bc9aa29e70c37fe2cb9712b17043213e1ffa83

As you can see, the current hash is concatenated with the new data (also a hash), before sha256-hashing it again.

How to reset a PCR?

The TPM does not allow resetting PCRs. Though it’s possible to reset PCR 16, which is a debug PCR.

kmille@linbox:~# sudo tpm2_pcrreset 7
WARNING:esys:src/tss2-esys/api/Esys_PCR_Reset.c:287:Esys_PCR_Reset_Finish() Received TPM Error 
ERROR:esys:src/tss2-esys/api/Esys_PCR_Reset.c:98:Esys_PCR_Reset() Esys Finish ErrorCode (0x00000907) 
ERROR: Esys_PCR_Reset(0x907) - tpm:warn(2.0): bad locality
ERROR: Could not reset PCR index: 7
ERROR: Unable to run tpm2_pcrreset

kmille@linbox:~# sudo tpm2_pcrreset 16
kmille@linbox:~# sudo tpm2_pcrread sha256:16
  sha256:
    16: 0x0000000000000000000000000000000000000000000000000000000000000000

How are components measured?

My laptop has 24 PCR registers (0-23), most of them have a specific purpose (specification, section “TPM2 PCRs and policies” of systemd-cryptenroll man page). During boot, each boot component measures the next component by hashing its data/configuration into a dedicated PCR. So for example, the firmware executable code is hashed in PCR 0. The firmware configuration (UEFI settings) is hashed in PCR 1. The firmware hashes the Secure Boot state and the trusted CA certificates in PCR 7. The Linux kernel measures all initrds it receives to PCR 9. The boot loader (GRUB or systemd-boot) measures the kernel command line in PCR 12. If a UKI (Unified Kernel Image) is used, systemd-stub measures the UKI in PCR 11. systemd-pcrphase measures boot phase strings like “enter-initrd” in PCR 11, allowing the disk unlock to only work inside the initrd. For more information, check out systemd’s documentation TPM2 PCR Measurements Made by systemd.

When gets the key unsealed?

I said the TPM unseals the disk encryption key when the system was not modified. What does “not modified” exactly means? When you enroll the TPM for disk encryption, you need to specify which PCRs should be bound to the TPM. For example, when you enroll the TPM with PCR 7, it takes the current value of PCR 7 and binds it to the TPM. When you want to unseal the key, the current value of PCR 7 needs to have the same value as it had before when it was enrolled.

Linux hands-on #2: How to use a TPM for FDE

Click to expand

First, let’s create a disk

kmille@linbox:~ fallocate -l 100m disk.raw
kmille@linbox:~ 

Encrypt the disk

Use an empty password here, we will remove it later.

kmille@linbox:~ sudo cryptsetup luksFormat disk.raw 

WARNING!
========
This will overwrite data on disk.raw irrevocably.

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for disk.raw: 
Verify passphrase:


kmille@linbox:~ sudo systemd-cryptenroll disk.raw                                                         
SLOT TYPE    
   0 password

Enroll TPM

  • --tpm2-device=auto automatically uses the right TPM device (most of the time there is just one)
  • --tpm2-with-pin requires a pin to unlock the device
  • --tpm2-pcrs=1+2+3+4 sets the PCR registers
  • --wipe-slot=empty automatically removes key slot 0 (the one with the empty password)
kmille@linbox:~ sudo systemd-cryptenroll --tpm2-device=auto --tpm2-with-pin=yes --tpm2-pcrs=1+2+3+4 --wipe-slot=empty disk.raw
🔐 Please enter current passphrase for disk /home/kmille/disk.raw:                         
🔐 Please enter TPM2 PIN: ••••                    
🔐 Please enter TPM2 PIN (repeat): ••••                    
New TPM2 token enrolled as key slot 1.
Wiped slot 0.

kmille@linbox:~ sudo systemd-cryptenroll disk.raw                                                                             
SLOT TYPE
   1 tpm2

Add recovery key

kmille@linbox:~ sudo systemd-cryptenroll --unlock-tpm2-device=auto --recovery-key disk.raw 
Automatically discovered security TPM2 token unlocks volume.
🔐 Please enter TPM2 PIN: ••••                    
A secret recovery key has been generated for this volume:

    🔐 rhdhclnt-chjudccv-hhtunvnj-cceutteg-cnvdlnlg-njdbrnkv-rlugknnj-nfldvrie

Please save this secret recovery key at a secure location. It may be used to
regain access to the volume if the other configured access credentials have
been lost or forgotten. The recovery key may be entered in place of a password
whenever authentication is requested.

Optionally scan the recovery key for safekeeping:

█████████████████████████████████████████
█████████████████████████████████████████
████ ▄▄▄▄▄ █▀█ █▄ ▀▀ ▀▄▄▄▄█▄██ ▄▄▄▄▄ ████
████ █   █ █▀▀▀█ ▀▀██▀▀▀▄▄ ▀▀█ █   █ ████
████ █▄▄▄█ █▀ █▀▀ ▀▀ ▄▄▄▄▄█▄ █ █▄▄▄█ ████
████▄▄▄▄▄▄▄█▄▀ ▀▄█▄█ █▄▀ ▀ ▀ █▄▄▄▄▄▄▄████
████  ▄ ▄█▄ ▄▄▀▄▀ ▄▄▀ ▀ ▀▀▄ ▄▄▀▄█▄▀▄▀████
████▄▀ ▄ █▄  █▄█▀▄█  ▄▀█▀  ▀▄▀▀▀▄ ▀▄█████
██████ ▀▀▄▄▀ ▄▄█▄ █▄  ▀ ▀▀  ▄ ▀▄▄▀██ ████
████  ▀█ ▀▄ ██▀ ▄█  ▀▄ █▀  ▀▄▄▀▄█▀ ▄█████
████▄ █▀ ▀▄▀█▀█▄  ▄█▀▄▀▀▀▀ ▀██▀▄▄▀█▄▀████
████▀▄  █▀▄ ██▀██▄▄▄██▀▀▀ ▄▀▄ ▄██▀▀▄█████
████▀▄█ █▀▄ ▀█ ▀▄▄▄▄ ▄▀▀ ▀  ▄▄▀█▄ ▀█ ████
████ █  ██▄▄▄▀▄█▀▄▄  ▄ ▀▀ ▄███▄  █ ▄▀████
████▄██▄█▄▄█▀█▀ ▄ ▄██▄█ ▀▀▄▀ ▄▄▄ ▀█▀▀████
████ ▄▄▄▄▄ █▄██▀ █▄▄▀▄ ▀▄    █▄█    ▀████
████ █   █ █ ▄▀▀█▄▄▄  ▀▀▀▀▄█  ▄  █▀▄█████
████ █▄▄▄█ █ ▄▄▀▄█▄▄▄▄▀█ ▀  ▄█ ▀█ ▀▄█████
████▄▄▄▄▄▄▄█▄▄███▄▄██▄█▄███▄▄██▄▄██▄█████
█████████████████████████████████████████
█████████████████████████████████████████

New recovery key enrolled as key slot 0.

kmille@linbox:~ sudo systemd-cryptenroll disk.raw 
SLOT TYPE    
   0 recovery
   1 tpm2

Open the device with TPM, create a file system, mount it

kmille@linbox:~ sudo systemd-cryptsetup attach tpm2-test disk.raw none tpm2-device=auto
🔐 Please enter LUKS2 token PIN: ••••               

kmille@linbox:~ sudo mkfs.ext4 /dev/mapper/tpm2-test 
mke2fs 1.47.2 (1-Jan-2025)
Creating filesystem with 21504 4k blocks and 21504 inodes

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done


sudo mount /dev/mapper/tpm2-test /mnt 
kmille@linbox:~

kmille@linbox:~ sudo systemd-cryptsetup detach tpm2-test
kmille@linbox:~ 

Open the device with recovery key

kmille@linbox:~ sudo cryptsetup open disk.raw tpm2-test
Enter passphrase for disk.raw: 
kmille@linbox:~ sudo cryptsetup close tpm2-test 
kmille@linbox:~ 

See dm-crypt/System configuration#crypttab and dm-crypt/System configuration#Trusted Platform Module and FIDO2 keys in order to unlock the volume at boot time.

Unsealing does not work when PCRs mismatch

Extend and thus change PCR 4. The TPM does not unseal the key anymore.

kmille@linbox:~# sudo tpm2_pcrextend 4:sha256=0e33a0c414b1d752930473d5eccf46ddf5bd2333328ed5562ec337b63c08465a

kmille@linbox:~# sudo systemd-cryptsetup attach tpm2-test disk.raw none tpm2-device=auto
🔐 Please enter LUKS2 token PIN: ••••                    
Failed to unseal secret using TPM2: Operation not permitted
Set cipher aes, mode xts-plain64, key size 512 bits for device disk.raw.

To re-enroll the TPM, use systemd-cryptenroll with --wipe-slot=tpm2 to remove the existing TPM key slot.

Show the luks header

kmille@linbox:~ sudo cryptsetup luksDump disk.raw
LUKS header information
Version:        2         
Epoch:          8        
Metadata area:  16384 [bytes]                                       
Keyslots area:  16744448 [bytes]                                    
UUID:           f8cc1e8b-66fc-427e-9da8-04042325af72                
Label:          (no label)                                          
Subsystem:      (no subsystem)
Flags:          (no flags)

Data segments:
  0: crypt
        offset: 16777216 [bytes]
        length: (whole device)
        cipher: aes-xts-plain64
        sector: 4096 [bytes]

Keyslots:
  0: luks2
        Key:        512 bits
        Priority:   normal
        Cipher:     aes-xts-plain64
        Cipher key: 512 bits
        PBKDF:      pbkdf2
        Hash:       sha512
        Iterations: 1000
        Salt:       ac aa 7d 68 c4 cf cb 2d 70 da 3b de 9d 13 7c 32 
                    86 d4 ef 51 16 3e 8e ca 41 34 7e 2e 83 6c e9 9a 
        AF stripes: 4000
        AF hash:    sha512
        Area offset:32768 [bytes]
        Area length:258048 [bytes]
        Digest ID:  0
  1: luks2
        Key:        512 bits
        Priority:   normal
        Cipher:     aes-xts-plain64
        Cipher key: 512 bits
        PBKDF:      pbkdf2
        Hash:       sha512
        Iterations: 1000
        Salt:       88 a0 d0 47 52 03 47 0a a7 39 7a e8 90 b6 25 b5 
                    96 3b b1 de f0 11 1e 35 93 21 8c 37 f1 2d 96 a7 
        AF stripes: 4000
        AF hash:    sha512
        Area offset:290816 [bytes]
        Area length:258048 [bytes]
        Digest ID:  0
Tokens:
  0: systemd-tpm2
        tpm2-hash-pcrs:   1+2+3+4
        tpm2-pcr-bank:    sha256
        tpm2-pubkey:
                    (null)
        tpm2-pubkey-pcrs: 
        tpm2-primary-alg: ecc
        tpm2-pin:         true
        tpm2-pcrlock:     false
        tpm2-salt:        true
        tpm2-srk:         true
        tpm2-pcrlock-nv:  false
        tpm2-policy-hash:
                    aa 37 1c 54 18 7c 54 61 3e 1f f3 41 1f 20 a7 e7
                    3d db 17 ec a5 ed ca 0c 00 91 af 6d 98 a5 f3 7d
        tpm2-blob:        00 9e 00 20 b2 72 5d c0 e1 48 3b 8f e2 0f 13 9b
                    53 05 f5 79 bf 8d 55 c8 63 9a 35 2f 27 c9 ed 68
                    93 8f 5a ce 00 10 b3 86 85 12 3b cd a7 ef 97 5e
                    e9 9a 44 b0 17 96 2a d6 49 dd 5a 9d 6d 6c 90 fa
                    24 2f 40 18 62 a4 89 aa ec be a6 ec 52 c2 24 d9
                    8b 22 7b 6d 96 d0 e4 91 52 77 34 9e 74 a9 71 cb
                    73 db 8e 7e 7a a0 44 60 a6 94 1b a4 5c 72 17 f5
                    85 d5 1c 23 7f ed 07 01 04 39 37 43 c5 53 b5 c6
                    d4 eb 6c 3e 00 57 d0 37 52 e2 ff a7 90 2f 69 f4
                    ff 82 9e 3f d1 16 61 85 26 34 37 b1 c6 f6 a0 dd
                    00 4e 00 08 00 0b 00 00 00 12 00 20 aa 37 1c 54
                    18 7c 54 61 3e 1f f3 41 1f 20 a7 e7 3d db 17 ec
                    a5 ed ca 0c 00 91 af 6d 98 a5 f3 7d 00 10 00 20
                    22 ce 78 91 3a 57 45 2f 11 79 72 24 32 47 20 54
                    2e 4f 00 b6 ba 44 fe e6 d1 cf 07 8c e1 49 2d 78
        Keyslot:    1
  1: systemd-recovery
        Keyslot:    0
Digests:
  0: pbkdf2
        Hash:       sha256
        Iterations: 89775
        Salt:       44 c8 01 e3 76 ef e1 9e f7 ce df a9 eb 66 a6 7c 
                    65 58 83 1e 1f d4 94 56 3b d4 b2 62 3b e9 5c 13 
        Digest:     07 9d e3 44 0e 94 8b 8a 39 70 48 5f 9e 3b 00 f0 
       

Which PCRs should I use?

At some point during boot, the TPM gets asked to unseal the key. It compares the current PCR values with the ones when the key was enrolled. The more PCRs you bind to the encryption key the better, as more components are measured and the attack surface shrinks. But what happens if you update your firmware or change your firmware settings? Then the PCRs don’t match anymore and the TPM will not unseal the key. In this case you need a recovery key to unlock the disk. After entering the recovery key and booting the system, you can re-enroll the TPM with the current/updated PCR values. Then after a reboot, TPM unsealing works again.

How to handle regular kernel updates?

PCR 7 holds Secure Boot’s state and certificates. The PCR 7 hash value should never change. When you bind the key to PCR 0 (firmware) or PCR 1 (firmware configuration), you have to manually re-enroll rarely. So that’s not a big deal in the long run. But what about your kernel? Your kernel (and thus initrd) is updated relatively often. In theory, you can handle this in two ways:

  • Enter the recovery key during boot and re-enroll TPM
  • Write a script that pre-calculates the right PCR value and re-enroll the TPM before rebooting. When enrolling, you can tell the TPM to use a static (precalculated) value for a specific PCR instead of using the current PCR value (example for PCR4, systemd MR: systemd-measure).

There is a more practical solution to this problem. It’s called signed PCR policy.

What is a signed PCR policy?

So there are two technical ways to use a PCR: The one I described earlier by comparing PCR values (current and the previous one when enrolling). The other mechanism works like this:

  1. You once generate an asymmetric key pair (public/private key)
  2. When enrolling the TPM, you bind the public key to the TPM
  3. The UKI is signed with the key pair bound to the TPM. When you rebuild the UKI after a kernel update, you just have to sign it

Linux hands-on #3: How to use a signed PCR policy

Click to expand

An UKI contains kernel, initrd and kernel parameters. I recommend using UKIs, which are measured in PCR 11. To use them, first install the systemd-ukify package. The ukify helper gets used automatically when mkinitcpio rebuilds the initrd/UKI after a kernel update.

First, create a config file and generate a key pair

kmille@linbox: cat /etc/kernel/uki.conf 
[UKI]
OSRelease=@/etc/os-release
PCRBanks=sha256

[PCRSignature:initrd]
Phases=enter-initrd
PCRPrivateKey=/etc/kernel/pcr-initrd.key.pem
PCRPublicKey=/etc/kernel/pcr-initrd.pub.pem
kmille@linbox: ukify genkey -c /etc/kernel/uki.conf
...

kmille@linbox: ls /etc/kernel/pcr-initrd.*
-rw------- 1 root root 1.7K Apr  9 14:42 /etc/kernel/pcr-initrd.key.pem
-rw-r--r-- 1 root root  451 Apr  9 14:42 /etc/kernel/pcr-initrd.pub.pem

How to rebuild initrd and add a signed PCR policy with mkinitcpio?

mkinitcpio is automatically executed by a pacman hook. The signing works automatically. Take a look at the parameters of systemd-measure.

kmille@linbox: sudo mkinitcpio -p linux
==> Building image from preset: /etc/mkinitcpio.d/linux.preset: 'default'
==> Using configuration file: '/etc/mkinitcpio.conf'
...
==> Creating unified kernel image: '/efi/EFI/Linux/arch-linux.efi'
  -> Using ukify to build UKI
  -> Using cmdline file: '/etc/kernel/cmdline'
Using config file: /etc/kernel/uki.conf
Splash image /usr/share/systemd/bootctl/splash-arch.bmp is 566×167 pixels
+ /usr/lib/systemd/systemd-measure sign --osrel=/tmp/mkinitcpio.Z5giRN --cmdline=/tmp/mkinitcpio.ktFNom --uname=/tmp/tmp.unamekpdotn3e --splash=/usr/share/systemd/bootctl/splash-arch.bmp --pcrpkey=/etc/kernel/pcr-initrd.pub.pem --linux=/boot/vmlinuz-linux --initrd=/tmp/mkinitcpio.iu1UeJ --sbat=/tmp/tmp.sbatdqoct7oa --bank=sha256 --private-key=/etc/kernel/pcr-initrd.key.pem --public-key=/etc/kernel/pcr-initrd.pub.pem --phase=enter-initrd
Wrote unsigned /efi/EFI/Linux/arch-linux.efi
==> Unified kernel image generation successful
...

What’s inside the UKI?

kmille@linbox:~ sudo ukify inspect /efi/EFI/Linux/arch-linux.efi
.sbat:
  size: 326 bytes
  sha256: 09089f4e6c44f00f363ccce9f4bd8af566482be1eb6686fd8ad6bee5465abea8
  text:
    sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
    systemd-stub,1,The systemd Developers,systemd,257,https://systemd.io/
    systemd-stub.arch,1,Arch Linux,systemd,257.5,https://archlinux.org/packages/core/x86_64/systemd/
    uki,1,UKI,uki,1,https://uapi-group.org/specifications/specs/unified_kernel_image/
    
.osrel:
  size: 408 bytes
  sha256: 77395add081afa6cd4abacbc77944dda4bb1ebedbae041c8f3e2283a95f3ccc3
  text:
    VERSION_ID=6.14.3-arch1-1
    NAME="Arch Linux"
    PRETTY_NAME="Arch Linux"
    ID=arch
    BUILD_ID=rolling
    ANSI_COLOR="38;2;23;147;209"
    HOME_URL="https://archlinux.org/"
    DOCUMENTATION_URL="https://wiki.archlinux.org/"
    SUPPORT_URL="https://bbs.archlinux.org/"
    BUG_REPORT_URL="https://gitlab.archlinux.org/groups/archlinux/-/issues"
    PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/"
    LOGO=archlinux-logo
.cmdline:
  size: 5 bytes
  sha256: 1bd3612e2cf8c65ab2eb1f3d2eff2c1940279ed95786ac2a33385c3ae48b26d4
  text:
    rw 
    
.uname:
  size: 14 bytes
  sha256: 40dccbf1a571538db1b62afc3113acfc91863f45eb911a3fc16894eeffc08281
  text:
    6.14.3-arch1-1
.splash:
  size: 378226 bytes
  sha256: d0ec3070db1f29799adbc8be8a26341290d2df4bf35939a5adce8e7947bad3bf
.pcrpkey:
  size: 451 bytes
  sha256: 1458de633c10d4d231197303d2c124b0769c0ff5a4ab9a923bd82f48ca16d64c
  text:
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoQl3cmNMx+b9mjIf0HVu
    MofwJOaWcLbghwaBipghOprLOUb74n4jaWrV+YPCTHX1pXYFt2vCDQwE0j87AFnL
    hSwcSOEcGHGTnqn+8hETMVZDlvonrTDIQxCPqeIggHBiAbe6OJg1g14dm9U4jiLB
    Nu3I2HXrhWpCUv1o8t7vkawPI++2wLIM0KyGgDU0s+eaoNtIYBKuXmLP5KAtvvMl
    yvWSTzmOGs1ez6kN5Y9t3VRtajj1N7R+Vcdyd87k3E8MmIFwr3dc93pQ7YE/D07s
    Q6/TIl3VRZZmkf0uVwPIWq4coc6P4QGFYyXAHoyGcDvvZAAuG5jLFu0WX3o1Eiry
    +wIDAQAB
    -----END PUBLIC KEY-----
.linux:
  size: 15368704 bytes
  sha256: 45def5a3453c6682acf39799a377e1ad6e27cbed39263d6dd40bd990cccf5ea9
.initrd:
  size: 25548170 bytes
  sha256: 8c5efeda193932e132a9c27c17d48625b09b791342f34c7a2051859c162d8403
.pcrsig:
  size: 534 bytes
  sha256: 40c789f54d835962f426977c6cd5eb119251baad28601a3afbed0c3865de6de3
  text:
    {"sha256": [{"pcrs": [11], "pkfp": "c470426df161e6da0383a4268a487a909572990d69aac94882734dcde0bdd22b", "pol": "e248a22a4cb5031d99d57e3f3970eb7b2246cfe1b7fc379b843272686e8022b5", "sig": "ALdEzYi7xKrf+j3mXgCL1Sij4yXBRJvtEJFp3tnnfMx13aATgjWBYdO8166UE19vUfOeffiEdYwg9OgqMQZdHhYJXNCt6ADauejYjeYYNv2lTeeJBN2Qt6sz7wcLvEuLGQmldMgHILNRtriutggKJs43Ux971y5CMP7ONZPqWrpAHGoA4lHiG3FWGfK4r76y6tzR2bL71hDRxZZweZ72jEiLu9SM/IB/8wBzVVDeUsRCRi7DxEZRgSq2lG7e/pZInFD5HcWhBIdJ5gg9IKvBNGveBR/tK/d+iu7ZGV/kcSdZrdz358XjZFcKVGilU4ya4qB6UFQN6zps+i5RpaZBbg=="}]}
kmille@linbox: sudo lsinitcpio /efi/EFI/Linux/arch-linux.efi                                                          
bin
early_cpio
lib
lib64
sbin
usr/
usr/lib/
usr/lib/firmware/
usr/lib/firmware/amd/
usr/lib/firmware/amd/amd_sev_fam17h_model0xh.sbin.zst
...

How to enroll a TPM with a signed PCR policy?

kmille@linbox: sudo systemd-cryptenroll --tpm2-device=auto --tpm2-with-pin=yes --wipe-slot=empty --tpm2-pcrs=0+1+2+7 --tpm2-public-key=/etc/kernel/pcr-initrd.pub.pem --tpm2-public-key-pcrs=11 disk.raw

🔐 Please enter current passphrase for disk /home/kmille/disk.raw:                         
🔐 Please enter TPM2 PIN: •••                     
🔐 Please enter TPM2 PIN (repeat): •••                     
New TPM2 token enrolled as key slot 1.
Wiped slot 0.

kmille@linbox: sudo systemd-cryptenroll disk.raw                                                                                                                                                        
SLOT TYPE
   1 tpm2

Show luks header

kmille@linbox: sudo cryptsetup luksDump disk.raw
LUKS header information
Version:       	2
Epoch:         	6
Metadata area: 	16384 [bytes]
Keyslots area: 	16744448 [bytes]
UUID:          	d65341e2-92bb-450f-bb85-ed5880cc930e
Label:         	(no label)
Subsystem:     	(no subsystem)
Flags:       	(no flags)

Data segments:
  0: crypt
	offset: 16777216 [bytes]
	length: (whole device)
	cipher: aes-xts-plain64
	sector: 4096 [bytes]

Keyslots:
  1: luks2
	Key:        512 bits
	Priority:   normal
	Cipher:     aes-xts-plain64
	Cipher key: 512 bits
	PBKDF:      pbkdf2
	Hash:       sha512
	Iterations: 1000
	Salt:       73 13 d4 d9 c9 81 8a c6 35 5a 73 b0 b0 f8 4e 77 
	            ed f5 ca bb c5 9b 7d 11 e2 c6 ef 90 88 f6 4f 1c 
	AF stripes: 4000
	AF hash:    sha512
	Area offset:290816 [bytes]
	Area length:258048 [bytes]
	Digest ID:  0
Tokens:
  0: systemd-tpm2
	tpm2-hash-pcrs:   0+1+2+7
	tpm2-pcr-bank:    sha256
	tpm2-pubkey:
	            2d 2d 2d 2d 2d 42 45 47 49 4e 20 50 55 42 4c 49
	            43 20 4b 45 59 2d 2d 2d 2d 2d 0a 4d 49 49 42 49
	            6a 41 4e 42 67 6b 71 68 6b 69 47 39 77 30 42 41
	            51 45 46 41 41 4f 43 41 51 38 41 4d 49 49 42 43
	            67 4b 43 41 51 45 41 6f 51 6c 33 63 6d 4e 4d 78
	            2b 62 39 6d 6a 49 66 30 48 56 75 0a 4d 6f 66 77
	            4a 4f 61 57 63 4c 62 67 68 77 61 42 69 70 67 68
	            4f 70 72 4c 4f 55 62 37 34 6e 34 6a 61 57 72 56
	            2b 59 50 43 54 48 58 31 70 58 59 46 74 32 76 43
	            44 51 77 45 30 6a 38 37 41 46 6e 4c 0a 68 53 77
	            63 53 4f 45 63 47 48 47 54 6e 71 6e 2b 38 68 45
	            54 4d 56 5a 44 6c 76 6f 6e 72 54 44 49 51 78 43
	            50 71 65 49 67 67 48 42 69 41 62 65 36 4f 4a 67
	            31 67 31 34 64 6d 39 55 34 6a 69 4c 42 0a 4e 75
	            33 49 32 48 58 72 68 57 70 43 55 76 31 6f 38 74
	            37 76 6b 61 77 50 49 2b 2b 32 77 4c 49 4d 30 4b
	            79 47 67 44 55 30 73 2b 65 61 6f 4e 74 49 59 42
	            4b 75 58 6d 4c 50 35 4b 41 74 76 76 4d 6c 0a 79
	            76 57 53 54 7a 6d 4f 47 73 31 65 7a 36 6b 4e 35
	            59 39 74 33 56 52 74 61 6a 6a 31 4e 37 52 2b 56
	            63 64 79 64 38 37 6b 33 45 38 4d 6d 49 46 77 72
	            33 64 63 39 33 70 51 37 59 45 2f 44 30 37 73 0a
	            51 36 2f 54 49 6c 33 56 52 5a 5a 6d 6b 66 30 75
	            56 77 50 49 57 71 34 63 6f 63 36 50 34 51 47 46
	            59 79 58 41 48 6f 79 47 63 44 76 76 5a 41 41 75
	            47 35 6a 4c 46 75 30 57 58 33 6f 31 45 69 72 79
	            0a 2b 77 49 44 41 51 41 42 0a 2d 2d 2d 2d 2d 45
	            4e 44 20 50 55 42 4c 49 43 20 4b 45 59 2d 2d 2d
	            2d 2d 0a
	tpm2-pubkey-pcrs: 11
	tpm2-primary-alg: ecc
	tpm2-pin:         true
	tpm2-pcrlock:     false
	tpm2-salt:        true
	tpm2-srk:         true
	tpm2-pcrlock-nv:  false
	tpm2-policy-hash:
	            b7 72 4c fa 18 d1 8e a8 d8 d9 77 0f b8 24 da 92
	            46 e8 07 b3 aa 4c e7 96 1f 9c aa 61 a7 4c 1c ba
	tpm2-blob:        00 9e 00 20 80 8c c0 51 57 57 04 f2 74 2b a9 90
	            a8 a7 6e f4 7f 72 e0 a7 cc 8f d6 80 e3 0e 36 35
	            ef 33 b5 1f 00 10 69 3c 6e c0 4d df f9 e0 22 da
	            fe c6 20 cc 89 37 45 49 e3 81 65 71 f7 0c 8d e2
	            f1 56 16 2a 70 89 08 3e c6 e1 62 4f 81 66 88 e8
	            93 a4 d9 d9 0a 90 eb fc 0a 28 e4 4e da fe 21 f7
	            6e aa 68 8d a4 70 cd 15 70 e3 84 67 6c a7 40 48
	            02 a7 76 40 1e 8b cd 50 4f 9a e6 61 30 ff 2d 8d
	            ce c1 60 2f a1 e3 db a4 26 d4 e5 65 bc 58 ca da
	            30 b0 1d 10 2b 83 d9 1d 65 c7 a3 3c 26 53 28 3c
	            00 4e 00 08 00 0b 00 00 00 12 00 20 b7 72 4c fa
	            18 d1 8e a8 d8 d9 77 0f b8 24 da 92 46 e8 07 b3
	            aa 4c e7 96 1f 9c aa 61 a7 4c 1c ba 00 10 00 20
	            3c 14 e8 d6 66 1b df d0 4b cf db c9 8b a5 63 de
	            56 24 30 bf cb ea 32 6e 07 ed 37 c5 cd a8 9f ba
	Keyslot:    1
Digests:
  0: pbkdf2
	Hash:       sha256
	Iterations: 98550
	Salt:       d1 ca d1 a0 b0 8c 48 8f 49 3e 06 5a c6 86 6d d6 
	            f3 99 25 c9 53 34 2f dd 35 d3 7e 78 39 1b 75 4d 
	Digest:     b3 4e 54 67 a7 72 bc f4 72 f2 0d 17 9c 86 93 59 
	            76 9f 35 69 81 f3 f2 92 4c 7e ce f4 59 ae 06 f9 

What can go wrong when using a TPM for FDE?

How to use the TPM really depends on the system requirements and the used software. A small issue in the design can easily break the whole security of the system.

It’s hard to find good documentation about how to properly setup Linux with a TPM. Most articles are incomplete or don’t explain why certain things are necessary. Right now, the most comprehensive guides to understand Linux and TPMs are the man pages of systemd-cryptenroll and ukify, together with some blog posts written by Lennart Poettering (1, 2, 3). As there is no single easy-to-use/understand text, people are discussing about which registers to use (my systemd issue to discuss about this, people discussing which PCRs to use, more). So, what can go wrong?

  • On a system not using UKIs, if you don’t measure the initrd (PCR 9), the attacker can just backdoor it (blog post, talk).
  • If kernel parameters are not measured and can be edited by an attacker, they can have a root shell after the disk is decrypted (init=/bin/bash).
  • If you only bind to PCR 7 and your Secure Boot configuration trusts Microsoft’s Third Party keys: An attacker can boot a regular Ubuntu from USB, read PCR 7 and create a fake disk with a malicious init. This attack is described here (and some discussion). I still didn’t fully understand it. But I just want to explain shim at this point: “Shim is the pre-bootloader that runs on UEFI systems, meant to be a bit of code signed by Microsoft, that embeds our own certificate (which signs our grub binaries), so that it can load the “real” bootloader: GRUB.” (Ubuntu Wiki). (But then you can also just boot Ubuntu from USB and decrypt the disk from there (when only using PCR 7)!?)
  • There was also a bug: When hammering the ’enter’ key, suddenly a recovery shell appeared, right after the disk was unlocked (blog post).

Let’s look at the Arch Wiki and their TPM setup instructions (2025/05). They only bind PCR 7 (Secure Boot). This is not great but also not broken, but only because:

  • They use it with UKIs, so the integrity of the kernel and initrd is verified by Secure Boot.
  • They use systemd-boot, which does not allow you to change kernel parameters if Secure Boot is enabled.
  • They use Secure Boot with custom keys (but also enroll Microsoft’s keys?). The docs are not super clear here, as they just link to the generic Secure Boot Wiki page.

Can I decrypt the disk from a different computer?

If you use a TPM, decryption only works with the TPM. When your mainboard or TPM is broken, you can not decrypt the disk anymore. That’s why a recovery key is highly recommended. A recovery key is basically an auto-generated high-entropy password. Full disk encryption software like LUKS or BitLocker support multiple key slots. Each key slot can be used separately to unlock the disk. The TPM uses only one of these key slots.

Are there any attacks or vulnerabilities targeting TPMs?

In the past, there have been some TPM vulnerabilities like TPM-Fail and faulTPM. If you think about hardware attacks, there are two relevant ones and both are pretty reliable to perform: Cold boot and TPM sniffing attacks.

When the device gets decrypted automatically, the attacker has an infinite number of attempts to perform cold boot attacks. That’s why pre-boot authentication is really important, especially if your hardware does not support encrypted memory. When pre-boot-authentication is enabled or a TPM is not used, an attacker only has a single attempt to succeed with this attack.

Another problem are sniffing attacks. During decryption, the CPU asks the TPM for the full disk encryption key. The TPM checks the integrity of the system and sends the key back to the CPU. This traffic can be sniffed by an attacker. Again: Without pre-boot-authentication, the attacker has an infinite number of tries for this attack. There are even workshops to learn TPM sniffing attacks (1 , 2). More implementation details on can be found in this Github repo.

TPM sniffing attacks are well known for a very long time. In 2024, stacksmashing published a video showing their implementation of the attack. They used a Raspberry Pi Pico and debug pins of the laptop’s mainboard, so no soldering was required. Watch and enjoy:

TPM traffic can also be encrypted. Systemd implemented this so called “parameter encryption” in Pull Request #22630 (announcement by Lennart Poettering). There is also a talk about systemd and the TPM implementation on YouTube. Sniffing attacks can also be mitigated by using encrypted memory or firmware TPMs (fTPM) like Intel PTT (Intel Platform Trust Technology) or “AMD Firmware TPM”. Then the TPM lays inside the CPU.

How good is Windows’ BitLocker TPM implementation?

Currently (2025/05), Window’s BitLocker encryption is totally broken. Windows uses a TPM that automatically decrypts the disk during boot, which makes cold booters really happy. Windows does not implement TPM parameter encryption, opening the door for TPM sniffing attacks. Let me quote from a pentesting blog (they use it for red teaming):

Indeed, the way we perform the attack nowadays allows us to break the BitLocker protection in only a few minutes on the three major enterprise-grade laptop manufacturers (i.e. Lenovo, HP, and Dell).

You should expect cops (or some of their avaricious service providers) to be able to decrypt Windows devices. To enable pre-boot-authentication, you need to use the command line or change values in the registry. So it’s unnecessary hard to change this for regular users.

Last but not least, there is the bitpixie vulnerability (CVE-2023-21563), a TPM related issue. There is a nice talk explaining the problem and why it is still not fixed (speaker’s blog post). Proof of concepts can be found online on Github (1, 2). On top of this, the BitLocker recovery key is often stored online in the user’s Microsoft account (how to remove it).

Do you trust a TPM?

In the end it’s a blackbox holding your full disk encryption key. Do you trust self-encrypting disks? Probably not. So why do you trust a TPM? It could have a backdoor. It’s also a nice target for intelligence services for planting a backdoor. On the other side: Even when they have a backdoor, they probably won’t use it against you (assuming you are not a terrorist or a spy). In the end, you have to decide, it’s your choice.

How do you do FDE?

I use Arch Linux with UEFI, Secure Boot and linux-hardened kernel, together with systemd-boot, a TPM, encrypted memory and UKIs. Currenlty, this is my favorite Arch Linux install guide. My changes:

  • The guide only uses PCR 7. That’s the TPM enrollment command I use:

systemd-cryptenroll –tpm2-device=auto –wipe-slot=tpm2 –tpm2-with-pin=yes –tpm2-pcrs=0+1+2+7 –tpm2-public-key=/etc/kernel/pcr-initrd.pub.pem –tpm2-public-key-pcrs=11 /dev/nvme0n1p2

  • As I explained in this blog post, I have two partitions: One for / and one for /home (and one for /efi). The reason for this is that I use my own “hibernate” implementation, because hibernation is disabled in linux-hardened. I only use the TPM (tamper detection with Measured Boot) for / partition (so no full trust). For /home, I use my Yubikey (check my YubiKey cheatsheet, it’s also using systemd-cryptenroll) or a long diceware password.

Thanks for reading, feel free to send feedback or correct wrong things! As a follow-up, I recommend reading this (not yet merged!) post about Laptop Hardware Security.

Buy me a coffee