‘secure’ automatic system decryption - tpm2 + LUKS

5 minute read

‘Secure’ automatic system decrpytion with tpm and LUKS

A few months ago, in August 2023, I bought a Thinkpad t14s Gen 4 AMD through a good deal, so it replaced my previous (consumer grade) HP envy, which now is my oldest son’s first computer!

For the new laptop, I basically chose the same setup as before. It’s running Debian Sid with sway, and a self-built kernel for fun. Oh, and when I received it, the graphics of the relatively new AMD chipset were only properly supported by the most recent kernel RC, so I had to build my own anyways.

This article is not about that, though: When I set up my machine, I decided to set up automatic decryption of the (LUKS-encrypted) system partition with the built-in tpm 2.0. In the following text I will refer to tpm 2.0 as tpm for simplicity and lazyness reasons.

The Problem

With linux systems, it’s common good practice (and the default for many distributions) to encrypt the system partition using LUKS. The simplest partitioning scheme for that is to have a (small) unencrypted partition for the UEFI, to be mounted at /boot/efi, a (bigger) unencrypted partition for the kernel image + initial ramdisk image, to be mounted at /boot, and the rest of the storage encrypted, to be mounted at /, sometimes with separate partitions, e.g. for /home.

The unencrypted partitions are unencrypted, because they contain data that has to be read before it possibly can be decrypted. How are you supposed to decrypt a filesystem if you have to decrypt the filesystem to get the configuration, tooling, drivers etc. required to perform the decryption before you can decrypt?

So, the partitions remain unencrypted, but secured against manipulation and tampering through Secure Boot.

This boils down to “There is a cryptographic chain, rooting in the UEFI + the trusted platform module (tpm), which will detect if somebody tampered with my system, e.g. by manipulation my UEFI configuration, manipulating or replacing my Kernel, and it will notify me and refuse booting. Everything loaded at boot has to be signed with a trusted key, manipulation will invalidate the signatures.”

This chain is rooted in the UEFI and the tpm, which are (in most cases, and so also in my case) black box systems with somewhat doubtable trustworthiness. Everybody has to make their own decision of whether you want to trust those systems, and to what degree.

Anyway, as I already relied on the UEFI and the tpm, I decided to also trust the tpm to decrypt my system partition if it decides that the system is in an unmanipulated state, so that it decrypts the system partition without me having to enter a passphrase. I don’t want to tell you that this is a good idea, but I decided that I can live with the added risk in exchange for the added convenience. If somebody targets me and my data, I would probably give in and tell them the passphrase at least after the second broken finger. And if they “own” secure boot etc., they could let me boot a tampered system that either gives them the wanted data or that logs the passphrase.

LUKS

The system partition is encrypted with LUKS, the Linux Unified Key Setup. What’s important here is that the encrypted partition is not encrypted with a set up key from a Keyslot directly. The keys in configured Keyslots decrypt the encryption key, which is then used to encrypt/decrypt the data. This is significant, because e.g. passphrases can be changed without having to reencrypt all data, and independent keys can be configured for encryption/decryption! And this is just what we want.

We want one key for decryption, our manual passphrase, to remain functional and independent from the tpm. Otherwise, if the tpm decides that the system is not trustworthy (or if I fuddled up my system), I would not have access to my data anymore myself.

What this looks like in my final setup:

❯ sudo cryptsetup luksDump /dev/nvme0n1p3
LUKS header information
Version:        2
Epoch:          9
Metadata area:  16384 [bytes]
Keyslots area:  16744448 [bytes]
UUID:           xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Label:          (no label)
Subsystem:      (no subsystem)
Flags:          (no flags)

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

Keyslots:
  0: luks2
        Key:        512 bits
        Priority:   normal
        Cipher:     aes-xts-plain64
        Cipher key: 512 bits
        PBKDF:      argon2id
        Time cost:  9
        Memory:     1048576
        Threads:    4
        Salt:       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        AF stripes: 4000
        AF hash:    sha256
        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:       sha256
        Iterations: 1000
        Salt:       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        AF stripes: 4000
        AF hash:    sha256
        Area offset:290816 [bytes]
        Area length:258048 [bytes]
        Digest ID:  0
Tokens:
  0: clevis
        Keyslot:    1
Digests:
  0: pbkdf2
        Hash:       sha256
        Iterations: 235317
        Salt:       xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        Digest:     xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

You can see that there are two configured keyslots, one for the passphrase and one for the tpm.

tpm 2

The tpm, amongst other things, is a key storage and retrieval system. It contains a secret, e.g. a key for a LUKS encrypted filesystem, and will only make it accessible if it decides that the system in its current state is trustworthy, measuring the trustworthiness through integrity checks. These are hash checks of relevant memory areas of the system, specified through Platform Configuration Registers (PCR).

For our purposes, we are interested in the integrity of the UEFI configuration (“nobody changed the boot order or extracted the disk and tpm and put it in a lab setup, accessing it externally”) and of the secure boot state (“Nobody switched off secure boot”).

These attributes map to PCR1 (UEFI configuration) and PCR7 (Secure Boot State).

So, we want to make sure that we configure tpm to measure against PCR1 and PCR7 for integrity before unlocking LUKS.

clevis

We want to configure our system in a way that makes sure that during boot the system tries to decrypt the LUKS filesystem using the tpm.

This works easiest using clevis, an automated decryption policy framework with tpm support.

In the beginning I set it up with systemd-cryptsetup, but that needs a lot of manual work and breaks every time one of the involved components (and there are a few) receives an update.

Assuming I have clevis installed and /dev/nvme0n1p3 is my encrypted partition, setting everything up is as easy as this, followed by providing the existing passphrase (which is not stored or used permanently, as discussed in the LUKS section):

$ sudo clevis luks bind -d /dev/nvme0n1p3 tpm2 '{"pcr_bank":"sha256","pcr_ids":"1,7"}'

initramfs

Depending on the configuration of your system, you might have to make sure that the tpm modules are part of your initramfs, e.g. by specifying them in /etc/initramfs-tools/modules

$ cat /etc/initramfs-tools/modules
tpm
tpm_tis_core
tpm_tis
tpm_crb

Notable mentions

There are many helpful resources about the matter. The most helpful and concise for me (besides the manpages) are

tl,dr:

If you trust your system firmware and tpm enough, you can set your system up to decrypt automatically without providing a passphrase using clevis.

Leave a comment