Secure boot on Arch Linux

Again an history of something that stopped working at 10pm on a Friday.

Table of Contents

Until recently I had set up my secure boot on my computer with an Arch linux system following this blog post from Matthew Bentley. However, one Friday night after 10pm, I did an update, including the linux kernel, the script automatically signed the new image, and after a reboot it stopped working. I am not sure what changed and I didn’t investigated very long but it seems that the creation of the unified image was not working, as even the unsigned unified version was not booting with secure boot disabled.

It was time for a new solution and it is described below.

I strongly recommend reading the original blog post which was really helpful to me and the Arch documentation page on secure boot.

What is secure boot ?

Secure boot is an option that prevents booting images that have not been signed with the required keys. In practice the BIOS is provided with some public certificates and the boot images are signed with the secret key which can prevent booting unauthorized software.

In this tutorial we are interested in enabling (or repairing) the secure boot on a linux computer.

Note that I will do oversimplification (mainly because I am lazy) and this tutorial will be specific to my case, and I cannot ensure that it will work for you. In particular, I am running Arch linux on a Thinkpad T470s, and my EFI System Partition (ESP) is mounted in /boot. My bootloader is systemd-boot.

Creating keys

This is mainly taken from Matthew Bentley’s original blog post as this is how I created the required files a while ago and this is included for reference. Some of the original blog post also seems to come from here.

The following script can be used to generate the keys and the certificates:

#!/bin/bash

echo -n "Enter a Common Name to embed in the keys: "
read NAME

openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME PK/" -keyout PK.key \
        -out PK.crt -days 3650 -nodes -sha256
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME KEK/" -keyout KEK.key \
        -out KEK.crt -days 3650 -nodes -sha256
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME DB/" -keyout DB.key \
        -out DB.crt -days 3650 -nodes -sha256
openssl x509 -in PK.crt -out PK.cer -outform DER
openssl x509 -in KEK.crt -out KEK.cer -outform DER
openssl x509 -in DB.crt -out DB.cer -outform DER

GUID=`python2 -c 'import uuid; print str(uuid.uuid1())'`
echo $GUID > myGUID.txt

cert-to-efi-sig-list -g $GUID PK.crt PK.esl
cert-to-efi-sig-list -g $GUID KEK.crt KEK.esl
cert-to-efi-sig-list -g $GUID DB.crt DB.esl
rm -f noPK.esl
touch noPK.esl

sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
                  -k PK.key -c PK.crt PK PK.esl PK.auth
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
                  -k PK.key -c PK.crt PK noPK.esl noPK.auth

chmod 0600 *.key

echo ""
echo ""
echo "For use with KeyTool, copy the *.auth and *.esl files to a FAT USB"
echo "flash drive or to your EFI System Partition (ESP)."
echo "For use with most UEFIs' built-in key managers, copy the *.cer files."
echo ""

It is recommend to do this on a safe location on your PC (/root/keys) for instance. sbsigntools and efitools are required.

After running the script, there are several keys and certificates:

$ ls /root/keys
DB.cer  DB.crt  DB.esl  DB.key  KEK.cer  KEK.crt  KEK.esl  KEK.key  mkkeys.sh  myGUID.txt  noPK.auth  noPK.esl  PK.auth  PK.cer  PK.crt  PK.esl  PK.key

Then the keys need to be installed. To do this, copy all the .cer, esl and .auth to /boot (or another FAT32 medium that will be accessible at boot time). Then reboot your PC, go to the BIOS, and reset the secure boot in setup mode.

Once on your system again, copy /usr/share/efitools/efi/KeyTool.efi to /boot and add a temporary boot entry in /boot/loader/entries/keytool.conf:

title  KeyTool
efi    /KeyTool.efi

Now boot in the KeyTool and replace the keys. Select “Edit keys” and delete the db and KEK keys. After this, add the db key: db > Add new key > DB.esl, the KEK key: KEK > Add new key > KEK.esl and the PK: The Platform Key (PK) > Replace Key(s) > PK.auth.

Again I recommend reading Matthew’s blog post, this section is better explain (also I took some shortcuts).

Creating a Unified Kernel Image (UKI)

One of the main difference when using the secure boot is that we need to create a unified kernel image and sign this image. This image contains the kernel, initrd and eventually other information such as the start command line or the name of the os release.

After a bit of search, including on the Arch documentation page on secure boot, I found the ukify utility script that was doing exactly what I wanted as a replacement of the objcopy utility to create the image. On Arch linux, the ukify utility can be installed by installing the systemd-ukify package.

sudo pacman -Syu systemd-ukify

After reading the documentation of ukify, the equivalent of the objcopy line propose in Matthew Bentley’s tutorial

objcopy \
    --add-section .osrel=/etc/os-release --change-section-vma .osrel=0x20000 \
    --add-section .cmdline="cmdline.txt" --change-section-vma .cmdline=0x30000 \
    --add-section .linux="/boot/vmlinuz-linux" --change-section-vma .linux=0x40000 \
    --add-section .initrd="/boot/initramfs-linux.img" --change-section-vma .initrd=0x3000000 \
    /usr/lib/systemd/boot/efi/linuxx64.efi.stub kernel.efi

can now be simply written as

/usr/lib/systemd/ukify build \
    --linux=/boot/vmlinuz-linux \
    --initrd=/boot/initramfs-linux.img \
    --cmdline=@/etc/cmdline \
    --os-release=@/etc/os-release \
    --output=kernel.efi

Note that for the cmdline and the os-release arguments, the @ character is very important since it discriminates between given directly the value or the file from where the value should be read.

One slight difference, if you are using another initrd (such as intel microcode), you should specify it by using several time the --initrd option:

/usr/lib/systemd/ukify build \
    --linux=/boot/vmlinuz-linux \
    --initrd=/boot/intel-ucode.img \
    --initrd=/boot/initramfs-linux.img \
    --cmdline=@/etc/cmdline \
    --os-release=@/etc/os-release \
    --output=kernel.efi

This image could now be signed with the same command line as in Matthew Bentley’s blog post using the sbsign utility.

sbsign --key /root/keys/DB.key --cert /root/keys/DB.crt --output kernel.efi kernel.efi

but it’s also possible to directly tell ukify to sign the image.

Creating a signed UKI

Again reading the documentation of ukify, it’s possible to create the command line that will in the same time crate the unified image and sign it:

/usr/lib/systemd/ukify build \
    --linux=/boot/vmlinuz-linux \
    --initrd=/boot/intel-ucode.img \
    --initrd=/boot/initramfs-linux.img \
    --cmdline=@/etc/cmdline \
    --os-release=@/etc/os-release \
    --secureboot-private-key=/root/keys/DB.key \
    --secureboot-certificate=/root/keys/DB.crt \
    --output=kernel.efi

Note that by default, sbsign is used as the signer no we don’t need to set the –signtool option.

Automation

Now let’s automate it ! The goal here is to configure a pacman hook so that every time the linux kernel is updated, a new signed image is automatically created. It would have been possible to configure ukify using the configuration file but for simplicity I will just put the whole command line in a script, in /root/secure-boot/make-signed-image.sh

#!/bin/bash

BOOTDIR=/boot
CERTDIR=/root/keys
KERNEL=/boot/vmlinuz-linux
INITRDLINUX=/boot/initramfs-linux.img
INITRDUCODE=/boot/intel-ucode.img
OUTIMG=/boot/kernel.efi
CMDLINE=/etc/cmdline
OSRELEASE=/etc/os-release

/usr/lib/systemd/ukify build \
    --linux=${KERNEL} \
    --initrd=${INITRDUCODE} \
    --initrd=${INITRDLINUX} \
    --cmdline=@${CMDLINE} \
    --os-release=@${OSRELEASE} \
    --secureboot-private-key=${CERTDIR}/DB.key \
    --secureboot-certificate=${CERTDIR}/DB.crt \
    --output=${OUTIMG}

Finally, set the pacman hook in /etc/pacman.d/hooks/secure-boot.hook:

[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = linux

[Action]
When = PostTransaction
Exec = /bin/sh -c 'while read -r f; do /root/secure-boot/make-signed-image.sh; done'
NeedsTargets

Now, each time the linux kernel is updated, the script make-signed-image.sh will be run after the update to create the new signed unified image.

For reference, here is the configuration for the entry in systemd-boot (/boot/loader/entries/arch.conf)

/boot/loader/entries/arch.conf
title	Arch Linux
efi	/kernel.efi
Yoann Piétri
Yoann Piétri
PhD student in Quantum Cryptography

My research interests include quantum physics, computer science and cryptography.