[HowTo] Using Secure Boot and TPM2 to unlock LUKS partition on boot

Difficulty: ★★★★★

Note

This tutorial is almost complete, it lacks only good formatting and one tiny addition which is not necessary but brings a bit of polish to the end result. Formatting, I’ll do it in a couple of days. Sorry for that. I just feel that if I postpone publishing this again then I forget about it for weeks or even months (again) - that has already happened a couple of times :grinning_face_with_smiling_eyes:
By its nature this post is a response to the question once asked by @linux-aarhus and several other people (@Arisa @muvvenby). Hope you folks will find it useful.
PS: all commands were checked by me before posting. If you find anything being weird / unclear, do not hesitate to post and correct me as my bash / shell in general skills leave much to be desired.

Preface: this tutorial is made with several assumptions in mind, which are listed below.

  • Your PC has TPM2 device, which is /dev/tpmrm0.
  • You have already installed your system on GPT-partitioned drive using UEFI mode.
  • You have full disk encryption (FDE) already in place, meaning that your root (with /boot directory - no separate unencrypted boot partition is necessary), home and swap partitions are LVM2 volumes located inside LUKSv2 container (/dev/sda2), and the only unencrypted partition is EFI partition (/dev/sda1).
  • Your EFI partition ($esp) size is 300 Mb or larger.
  • The name of encrypted partition is cryptlvm. Adjust respective commands according to the name of yours.
  • You have AUR enabled and yay installed, because some of tools are not available in official repo.

The explanation of how to do the above is out of scope of this tutorial, please refer to Arch Wiki’s “LVM on LUKS” page for details. However it’s totally possible to use whatever layout you want, e.g. BTRFS subvolumes inside LUKSv2 container or swapfile instead of partition, it’s just the specific scenario I’m about to describe.

Before you begin, install all required tools:
yay -S sbsigntools efitools tpm2-tools tpm2-totp-git plymouth-git sbupdate-git shim-signed

Secure Boot:

The first step is to enable Secure Boot support. Normally it should be disabled because if using its default settings it prevents Manjaro from booting, but as it is required for ensuring that only allowed code is executed, you need to enable it as follows:
mkdir -p -v /etc/efikeys
chmod -v 700 /etc/efikeys
cd /etc/efikeys

The following saves default Secure Boot keys just in case, but we’re not going to change / remove them anyway so you can skip this step:
efi-readvar -v PK -o default_PK.esl
efi-readvar -v KEK -o default_KEK.esl
efi-readvar -v db -o default_db.esl
efi-readvar -v dbx -o default_dbx.esl

Now it’s time to generate machine-specific key which will be used to authenticate your Manjaro kernels and bootloader(s):
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$(hostname) platform key/" -keyout db.key -out db.crt -days 3650 -nodes -sha256
openssl x509 -outform DER -in db.crt -out db.cer

Now you need to replace grub with systemd-boot to make whole setup easier and more straightforward. With extra effort you can keep Grub and use it as your bootloader but that again is out of scope of this how-to since it would overcomplicate things dramatically. Frankly speaking, systemd-boot isn’t necessary too, but it makes booting different kernels and efi tools easier than just UEFI boot menu.
First, remove grub:
pacman -R grub

The following will change the mountpoint of your EFI partition to sane and logical location, which is, most importantly, perfectly supported by systemd-boot.
umount /boot/efi
sed -i 's|/efi/boot|/efi|' /etc/fstab
mkdir -p /efi
mount -a

Now let’s copy our Secure Boot certificate on $ESP to let MokManager utility enroll it on the next boot:
cp db.cer /efi/MOK.cer

Mkinitcpio and cryptsetup preparation:

Time to prepare initrd part of your boot image. You need to decide which path you prefer: udev-based initrd or systemd-based one. If you have no idea, choose the first (Manjaro/Arch default). Hereinafter everything hallmarked with (a) should be considered referring to udev-based initrd setup, and everything with a foregoing (b) literal – referring to systemd-based setup.

Replace HOOKS array in your /etc/mkinitcpio.conf and save old ones commented in the end of the file just in case:
OLD_HOOKS=$(cat /etc/mkinitcpio.conf | grep "^HOOKS=(")
a)
sed -i 's|'"${OLD_HOOKS}"'|HOOKS=(base udev keyboard consolefont autodetect plymouth modconf block plymouth-tpm2-totp tpm2 plymouth-encrypt lvm2 resume filesystems)|' /etc/mkinitcpio.conf && echo '#previous_'$OLD_HOOKS |tee -a /etc/mkinitcpio.conf
OR
b)
sed -i 's|'"${OLD_HOOKS}"'|HOOKS=(base systemd keyboard sd-vconsole autodetect sd-plymouth modconf block sd-plymouth-tpm2-totp sd-encrypt lvm2 filesystems)|' /etc/mkinitcpio.conf && echo '#previous_'$OLD_HOOKS |tee -a /etc/mkinitcpio.conf
Do the following to add entry about your LUKS partition to crypttab (discard is optional):
echo cryptlvm $(blkid -s UUID -o value /dev/sda2) - tpm2-device=/dev/tpmrm0,tpm2-pcrs=0+7+14,nofail,discard,x-initrd.attach | tee -a /etc/crypttab.initramfs

Also for better (de)compression speed/efficiency ratio consider setting COMPRESSION="zstd" in your /etc/mkinitcpio.conf if not set already. Note though that this works only starting from linux 5.10.

At this stage you might want to check your cryptsetup status:
cryptsetup luksDump /dev/sda2
Normally you should have 1 or 2 keyslots already occupied, the first one is your passphrase, the other is probably /crypto_keyfile.bin, and no tokens in place (yet). I suggest nuking your slot 1 if it’s occupied by crypto_keyfile at the moment (since it’s not safe to use crypto_keyfile in configuration we’re building):
cryptsetup luksKillSlot /dev/sda2 1
Double-check, of course, I doubt you want to destroy a slot with actual passphrase.

Configuring sbupdate and kernel command line

Sbupdate is a tool that generates signed and thus trusted kernel images used to boot your system. Issue
cat /proc/cmdline > /etc/cmdline
GRUB_LEFTOVER=$(cat /etc/cmdline | grep "BOOT_IMAGE/" | cut -d' ' -f1)
a)
sed -i 's|'"{GRUB_LEFTOVER}"'|splash tpmkey=/dev/sda1:/keyfile:0x81000001 tpmpcr=sha1:0,7,14|' /etc/cmdline
OR
b)
sed -i 's|'"{GRUB_LEFTOVER}"'|splash|' /etc/cmdline
CMDLINE=$(cat /etc/cmdline)

echo >>/etc/sbupdate.conf "
KEY_DIR="/etc/efikeys"
ESP_DIR="/efi"
OUT_DIR="EFI/Linux"
SPLASH="/sys/firmware/acpi/bgrt/image"
BACKUP=0
EXTRA_SIGN=('/efi/EFI/systemd/grubx64.efi')
CMDLINE_DEFAULT='$CMDLINE'"

Setting up systemd-boot, cleaning old configuration:

The following will install systemd-boot and create a new boot entry for shim efi utility which will be used as a “bridge” between UEFI and Linux bootloader:
OLD_MANJARO="$(efibootmgr | grep "manjaro" | cut -d' ' -f1 | cut -c 5-8)"
efibootgmr -b "${OLD_MANJARO}" -B
bootctl install && systemctl enable systemd-boot-update.service
cp /usr/share/shim-signed/*64* /efi/EFI/systemd/
cp /efi/EFI/systemd/systemd-bootx64.efi /efi/EFI/systemd/grubx64.efi
efibootgmr -c -d /dev/sda -p 1 -L "Manjaro" -l /EFI/systemd/shimx64.efi -v
mkinitcpio -P && sbupdate
BOOTORDER="$(efibootmgr | grep "^BootOrder: " | cut -d' ' -f2)"
efibootmgr --bootorder "${BOOTORDER}"
Open /efi/loader/loader.conf and add there default @saved. This will make systemd-boot select your previously booted kernel by default, which is handy isn’t it? Another useful change is setting timeout to 1 or 2 - most likely that’s long enough period to see bootloader’s menu and stop countdown if needed.
By entering the last command above you’ll have a new boot order where “Manjaro” (SB signed, actually shimx64.efi file) will be the first option, and “Linux Boot Manager” (no signature, actually systemd-bootx64.efi file) will be the 2nd one, auto-updateable efi binary which you might want from time to time to sign with sbsigntools or just manually copy it with resulting name of “grubx64.efi” and run sbupdate afterwards.

Okay, you are close to the final step now, but before that reboot, enter BIOS settings, set Secure Boot ON there, exit saving setting, and the next thing you see will be MokManager’s blue screen (of death), where you should enroll your Secure Boot MOK certificate located on your $ESP.
When you finish enrolling, your PC will reboot and the next thing you will see will be systemd-boot menu. Choose any boot entry and enter your unlock passphrase.

Configuring TPM2 module and tools:

a)
Let’s install luks-tpm2 tool and respective hook for mkinitcpio:
yay -S luks-tpm2 mkinitcpio-tpm2-encrypt
Then move luks-tpm2 alpm hook in order to avoid its triggering on kernel / bootloader update. Your TPM2 setup will rely on BIOS firmware, Secure Boot status and your MOK certificates check instead.
mv /usr/share/libalpm/hooks/luks-​tpm2.​hook /usr/share/libalpm/hooks/luks-​tpm2.​hook.​bak
Time to make your TPM module aware of your persistence!
tpm2_createprimary -c primary.ctx
tpm2_evictcontrol -c primary.ctx
Now look at the output of the second command and write down the index of the persistent handle, it will be used later. I just assume it’s 0x81000001.
Edit /etc/default/luks-tpm2:

echo >>/etc/default/luks-tpm2 "
SEALED_KEY_PUBLIC="/efi/keyfile.pub"
SEALED_KEY_PRIVATE="/efi/keyfile.priv"
TPM2TOOLS_TCTI="device:/dev/tpmrm0"
ROOT_DEVICE="/dev/sda2"
PARENT_HANDLE="0x81000001"
PCRS="sha1:0,7,14"
UNSEAL_PCRS="sha1:0,7,14"

And add a TPM-sealed key to LUKS header:
luks-tpm2 -p /efi/keyfile -H 0x81000001 /dev/sda2 init
mkinitcpio -P && sbupdate
OR
b)
This is much less complicated, but only works properly since systemd v.250:
systemd-cryptenroll /dev/sda2 --tpm2-device=/dev/tpmrm0 --tpm2-pcrs=0+7+14

Reboot and enjoy. That’s it.

TOTP boot indication:

tpm2-totp and its hooks which were added on mkinitcpio step are responsible for showing a 6-digit code during boot time if all checks are successful. It doesn’t unlock your partition, but just indicates that PCRs are the same, and I suggest using the following way:
tpm2-totp -p 0,5,7,14 -b SHA1 -P - init, this will hang waiting for your input. Enter some password, press Ctrl+D twice. Install Google Authenticator on your phone, scan the QR code on your screen, done. Enter tpm2-totp show. The digits on your phone and in the terminal should be the same. Now every time you boot your PC you will see identical digits on both devices. And if you don’t see any digits, that means that there was a change to GPT / Partition table, because you initialized tpm2-totp with not only 0 (UEFI/BIOS code), 7 (Secure boot state) and 14 (MOK) PCRs as we did for unlocking LUKS partition, but with PCR 5 as well, which changes when partition table is changed. Immediately comes to mind that it can happen when booting with some USB stick plugged in. At least that’s how I use it, you are welcome to man systemd-cryptenroll and read about PCRs if you want to dive deeper into details.

Final provisions:

  • From now on you have auto-unlocking of your encrypted drive using TPM module based on verifying of UEFI configuration + Secure Boot state + MOK list during the boot sequence. Kernel and cmdline checks are not used because Manjaro has oob support for variety of kernels, some may have unique cmdlines, so you probably do not want to enter your password every time you decide to boot another kernel. Relying on the above and having all executables signed should be enough.
  • Note though: we left systemd-bootx64.efi on $esp unsigned. When you decide to update it, do not copy it to grubx64.efi if unsure whether it was maliciously tampered with, use the following 2 commands:
    bootctl update
    sudo sbsign --key /etc/efikeys/db.key --cert /etc/efikeys/db.crt --output /efi/EFI/systemd/grubx64.efi /efi/EFI/systemd/systemd-bootx64.efi.
    Then use bootctl command to view current bootloader info, for example, note how grubx64.efi hasn’t been updated yet on my system while systemd-boot-update.service has already done it for systemd-bootx64.efi:

Available Boot Loaders on ESP:
ESP: /efi (/dev/disk/by-partuuid/8a18a9ff-7d73-4fcf-ad75-32a832ab609e)
File: └─/EFI/systemd/fbx64.efi
File: └─/EFI/systemd/grubx64.efi (systemd-boot 250.3-2-manjaro)
File: └─/EFI/systemd/mmx64.efi
File: └─/EFI/systemd/shimx64.efi
File: └─/EFI/systemd/systemd-bootx64.efi (systemd-boot 250.3-3-manjaro)
File: └─/EFI/BOOT/BOOTX64.EFI

  • The rationale behind using sha1 is that it is more common but you are free to use sha256 or something else if your BIOS supports that.

  • Now every time you have some changes to PCRs on which your check is based, the auto-unlock will fail and you will have to enter your passphrase. This could be caused by not only malicious agent (Smith), but also your own actions like disabling Secure Boot, enrolling some other MOK certificate or deleting the current one, updating/downgrading BIOS. If you know it is you who triggered that, then provided that it was MOK or BIOS change (not disabling of Secure Boot), on completion of boot procedure you should do the following:
    a) luks-tpm2 reset
    b) systemd-cryptenroll /dev/sda2 --wipe-slot=tpm2
    systemd-cryptenroll /dev/sda2 --tpm2-device=/dev/tpmrm0 --tpm2-pcrs=0+7+14
    And on the next boot you should not see any password prompt (due to the auto-unlocking functionality working again).

  • Updating tpm2-totp functionality is simple:
    tpm2-totp -p 0,5,7,14 -b SHA1 -P - reseal, then enter your totp password, press Ctrl+D twice and it’s done.

Sources:
man sed
man bootctl
man systemd-cryptenroll
Arch Wiki for systemd-boot
Arch Wiki for TPM
Arch Wiki for Secure Boot
Luks-tpm2 GitHub page

8 Likes

This is a nice piece of work.

Great job, @openminded!

1 Like

I am very grateful you took the time to do this writeup. I cannot even remember asking - so :100: :+1: for throwing in your spare time.

Without this writeup I would have taken considerably longer to get there so thank you.

I am in the process of implementing this on one of my laptops. I have the feeling it will come in handy in the future.

I am - as usual - taking notes Manjaro Secure Boot | root.nix.dk.

@openminded
I note that tpm2-totp and plymouth are in the repos. Would you mind to note why you build them from git?

4 Likes

Hi Frede, glad you liked it.

Regarding your question: both are terribly outdated, just look at their repo versions and when those were built. tpm2-totp is needed for something I haven’t added to this howto yet, you can skip it for sure; plymouth used to become outdated quite often as its development pace is hardly based on release model, I mean it’s that kind of software that gets broken (well, severity is low given the nature of this stuff) from time to time with kernel updates, and we know well that Manjaro is fast to update its kernels, and fixes (for Plymouth) usually come quickly as git commits but release is often somewhere on the horizon, so git version might be a better choice. What makes it worse is that Plymouth is historically of a very low priority for Manjaro maintainers so even if new release was published on its source page, it can take quite a lot of time to make it into repos. It’s okay, but the concept of seamless and silent boot doesn’t work without Plymouth, and when it’s broken, no cookies silent or seamless boot for you.

2 Likes

Another reason why I mention these packages and suggest installing them even though I’ve yet to describe anything about them is the fact that I wrote this to be as much “copypasteable” as possible. When I suggest modifying mkinitcpio.conf I offer the exact list of hooks to be placed as that’s the only way (I know of) to form an easy to understand command line to do so if I want this to be a base for future steps requiring those hooks to be in place already.

About mounting $esp at /efi.

Since /esp is not a default folder a command to create the folder should be added

mkdir -p /efi
1 Like

Nice catch man, thank you! Indeed I overlooked that. Edited now, right before mount -a.

While we are at it - I was wondering why you have used | instead of / in the sed commands?

Most likely I have no idea of what sed can do but I would use

sed -i 's/'"${OLD_HOOKS}"'/HOOKS=(base systemd keyboard sd-vconsole autodetect sd-plymouth modconf block sd-plymouth-tpm2-totp sd-encrypt lvm2 filesystems)/g' /etc/mkinitcpio.conf 

instead of

sed -i 's|'"${OLD_HOOKS}"'|HOOKS=(base systemd keyboard sd-vconsole autodetect sd-plymouth modconf block sd-plymouth-tpm2-totp sd-encrypt lvm2 filesystems)|' /etc/mkinitcpio.conf && echo '#previous_'$OLD_HOOKS |tee -a /etc/mkinitcpio.conf

Well as I discovered once that sed does not dictate which delimiter should be used I decided to pick | in order to avoid clashing with paths delimiters which you might see in editing “cmdline” part below. Thing is I wrote this chunk by chunk and sometimes some part from one step happened to move to another stage (since many of them are interconnected as you might have noticed), so I decided to use one delimiter for all sed commands here just to stop puzzling myself and make this write-up more consistent.
In fact, this is something very unusual to me, I mean sed. Actually I used to just double click on a file and edit it in Kate and then save it with the help of PolKit rights elevation :laughing: Yes I am heavily flawed by GUI tools :grinning:

1 Like

I have reached the checking the cryptsetup :slight_smile: which makes it page 5 in my notebook

2 Likes

I have just looked through your notepad and I must say I envy (a bit) your dedication and persistence on describing actions you’re taking. I wish I had those.
On cryptsetup step the most tricky thing is to find which slot is occupied by /crypto_keyfile.bin and remove it. I don’t have it for a very long time so I just don’t remember which one is that. But anyway it seems to me that the best approach is to set keyslot 0 to your passphrase even if unsure and kill other slots before proceeding. Ofc if there’s only one keyslot active and you’re sure you entered your actual passphrase on boot already or remember you had it set before then there’s no need to “kill” others as they do not exist (yet).

And make sure your LUKS is v2! Otherwise systemd-cryptenroll won’t work!

1 Like

Regarding cmdline: at that point I was trying to make use of existing cmdline with slight modification that omits grub-specific part and adds some other parts (as for systemd initrd, there’s just splash to make Plymouth show a some boot animation). But if the system being modified has just been installed and you are still in live system, then instead /proc/cmdline you need to find your installed system cmdline - in grub’s cfg.
Man, I know this is complicated. Here is just an example of resulting cmdline for systemd initrd:

quiet splash audit=0 apparmor=1 security=apparmor rd.systemd.show_status=auto rd.udev.log_level=3 random.trust_cpu=on nmi_watchdog=0 snd_hda_codec_hdmi.enable_silent_stream=0 root=/dev/%lvmname%/root resume=/dev/%lvmname%/swap

And here’s for udev-based:

tpmkey=/dev/sda1:/keyfile:0x81000001 tpmpcr=sha1:0,7,14 cryptdevice=UUID=%LUKS_UUID%:cryptlvm:allow-discards ro quiet splash audit=0 apparmor=1 security=apparmor systemd.show_status=auto udev.log_level=3 random.trust_cpu=on nmi_watchdog=0 snd_hda_codec_hdmi.enable_silent_stream=0 root=/dev/%lvmname%/root resume=/dev/%lvmname%/swap

I hope the concept is clear.

1 Like

I ran into a wall - first I did a CLI install but that plymouth password redirect was hanging waiting for the unlocking of the luks volume.

Then I did a base Lxqt install - and ran into the same problem - then I realized - I need the plymouth-sddm variant enabled.

New round of notes to stablilze plymouth before continuing the actual secure boot.

I doubt that plymouth can cause this, I for one use a common sddm.service and it works fine with plymouth.
How does it look? You enter the password and hit Enter, and nothing happens? On Plymouth screen you can press Esc and see a standard boot text wall.
The sequence should be as follows: first you edit /etc/crypttab.initramfs, then do mkinitcpio -P and then run sbupdate (given that /etc/sbupdate config has been edited). After reboot, systemd-cryptenroll command. Before rebooting, it could be a good idea to check if crypttab is present in initrd image with lsinitcpio /boot/initramfs-5.10-x86_64.img |grep crypttab.

I didn’t write the message down - I was fairly convinced it has something to with my knowledge.

It was something like passing … to totp … plymouth … - I really don’t remember the exact wording - perhaps I lucky again :slight_smile:

But it is a challenge that suits my brain - although I is running hot - then I let it rest for a couple of hours :zzz:

Ouch maybe totp thing is the culprit. I am not sure though. But now I regret for having posted without totp-related section - I thought having that hook added but not yet configured is fine. Try deleting it from /etc/mkinitcpio.conf before issuing mkinitcpio -P command or just don’t add it in the first place.

EDIT: I wiped my totp config and rebuilt initrd and resulting kernel image, rebooted, and no hiccup on boot observed. So there’s probably something else. I’m scratching my head…
Here’s Arch Wiki relevant page (btw).

Good documentation comes from trial and error :slight_smile:

2 Likes

So I added everything I wanted to share and did some formatting, hope this writeup has become a bit easier to read and understand.

1 Like

Sorrey for the silence and missing progress -I have been busy - and this learning project of mine has no high priority at the moment - so don’t touch my car - I’ll be back

2 Likes

Long time no see. Anyone managed to set this up?
My setup has been working fine since its deployment in January and it has survived a couple of systemd upgrades. To me it’s been so hassle-free now that I totally forgot all commands listed in the how-to above lol!