implementing secure boot on arch linux
Since it’s not very well documented on the internet, I’ll share my experience on implementing Secure Boot on my machine.
If you want a deeper understanding of how it works I suggest Rodsbooks’s Secure Boot, from which I learned it myself. Obviously the ArchWiki is another great source.
Introduction#
I’ll start by explaining what Secure Boot is:
Secure boot is a security feature provided by UEFI that verifies the first boot
components (kernel, initramfs, bootloader) with a public key infrastructure.
The system uses a database of trusted keys (db) and forbidden keys (dbx) to
check if a component is trusted or not.
These keys, in turn, are signed by KEKs (Key Exchange Key) with which computers
are shipped with. Usually there are two keys, a Microsoft one and one from the
motherboard manufacturer.
Finally there is a single platform key (PK), which is a top-level key that
signs KEKs. This one is also provided by the motherboard manufacturer.
MOKs (Machine owner keys) are an alternative to db keys, but are not a standard
part of Secure Boot and are used by signed bootloaders like shim or PreLoader.
I will not use these because I’ll focus on the standard and minimalist way of
implementing Secure Boot, but signed bootloaders are the easiest way to do it.
The security of this process lies mostly on how the keys used to sign binaries
are stored, so if your priority is not complete control of you machine or you
don’t intend to remove the Microsoft keys from the firmware, I suggest you use
shim or PreLoader, which are shipped already signed by Microsoft.
If you decide to use your own keys to sign binaries, think about disabling
Microsoft
keys,
but be aware that this might brick your hardware on some machines,
including laptops, making it impossible to get into the UEFI/BIOS settings to
rectify the situation. This is due to the fact that some device firmware is
signed using Microsoft’s key.
Generating the keys#
Now, the fun part:
The first step is to create your own keys. To do this install sbsigntools
,
openssl
and efitools
. To speed up the process you can use Rodsbooks’s
script:
#!/bin/bash
# Copyright (c) 2015 by Roderick W. Smith
# Licensed under the terms of the GPL v3
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=`python3 -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
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
-k PK.key -c PK.crt KEK KEK.esl KEK.auth
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
-k KEK.key -c KEK.crt db DB.esl DB.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 "but some UEFIs require the *.auth files."
echo ""
Now you should have created all of the keys you need. Move them in a folder available at boot but give it proper permissions so it won’t be readable by everyone.
For example I moved the keys in /root/keys. This means that even if they are protected by the filesystem, they would be saved in plain text and whoever has root permission will be able to read the keys and sign malware, so that it will be executed at boot. For this reason it’s preferable to use signed bootloaders.
If you prefer signing yourself, then make sure the partition where the keys are stored is encrypted and the EFI binaries are stored in another partition, available at boot.
Whatever you choose, make sure to set a password to access the firmware, otherwise everything we do will be for nothing, since anyone with physical access to you computer would be able to add their own keys to the firmware or disable secure boot and boot their own image.
Signing EFI binaries#
Let’s start signing all the EFI binaries that would be checked during boot, for
example
/boot/EFI/<GRUBDIR>/grubx64.efi
:
sbsign --key /root/keys/DB.key --cert /root/keys/DB.crt --output grubx64.efi grubx64.efi
and the kernel at /boot/vmlinuz-linux
:
sbsign --key /root/keys/DB.key --cert /root/keys/DB.crt --output vmlinuz-linux vmlinuz-linux
If this commands produce errors like warning: data remaining[1231832 vs 1357089]: gaps between PE/COFF sections?
, these can safely be ignored.
This commands will overwrite your unsigned files, so make sure to change the output filename if you don’t want that happen.
To verify that the EFI binaries have been signed successfully you can use sbverify
like this:
sbverify --list /boot/vmlinuz-linux
If the output shows the name of the issuer you chose during the generation of the keys, then the executable has been correctly signed.
The problem with signing single files in arch is that it won’t protect the initramfs files from tampering, so we need to make a unified kernel image.
First we need initramfs to be an uncompressed cpio archive, so you can edit /etc/mkinitcpio.conf
so that COMPRESSION="cat"
is present at the end.
In this case it may be convenient to just disable the fallback initramfs preset, since you would have to create another unified kernel to protect that too.
Edit /etc/mkinitcpio.d/linux.preset
and remove 'fallback'
from PRESETS('default' 'fallback')
.
Now create a file with the boot command lines, you can do so with the following
command cat /proc/cmdline > cmdline.txt
, merge the microcode file (if you use
it) with the initramfs (cat "/boot/intel-ucode.img /boot/initramfs-linux.img" > /tmp/boot/initramfs.img
), move initramfs and the kernel in a temporary
folder (/tmp/boot
) and then use this command to create the unified image:
/usr/bin/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=/tmp/boot/vmlinuz-linux --change-section-vma .linux=0x40000 \
--add-section .initrd=/tmp/boot/initramfs.img --change-section-vma .initrd=0x3000000 \
/usr/lib/systemd/boot/efi/linuxx64.efi.stub /tmp/boot/unified-kernel.efi
Now you have the unified kernel image, you just need to sign it with the
command shown previously and make your bootloader boot your new image instead.
For GRUB2 you can append these lines to /etc/grub.d/40_custom
, inserting your own UUIDs:
menuentry "Arch Linux" --class arch --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-<root partition uuid>' {
insmod fat
insmod chain
search --no-floppy --set=root --fs-uuid <partition's uuid where unified-kernel.img is saved>
chainloader /unified-kernel.img
}
You can also use grub-customizer to do the same thing, the important thing is that the boot script contains chainloader /unified-kernel.img
.
Automating the signing process#
To make everything easier, we can use a pacman hook to make sure that the EFI
binaries get automatically signed during every update of the kernel or
initramfs files.
The script I use is the following, you just need to modify the variables to suit your case:
#!/bin/bash
FILE=$(echo $1 | sed 's/boot\///')
BOOTDIR=/boot
CERTDIR=/root/keys # the directory where the keys are stored
KERNEL=$1
INITRAMFS="/boot/intel-ucode.img /boot/initramfs-$(echo $FILE | sed 's/vmlinuz-//').img"
EFISTUB=/usr/lib/systemd/boot/efi/linuxx64.efi.stub
BUILDDIR=/tmp/_boot
OUTIMG=/boot/$(echo $FILE | sed 's/vmlinuz-//').img
CMDLINE=/etc/cmdline # this file is a copy of /proc/cmdline
mkdir -p $BUILDDIR
cat ${INITRAMFS} > ${BUILDDIR}/initramfs.img
/usr/bin/objcopy \
--add-section .osrel=/etc/os-release --change-section-vma .osrel=0x20000 \
--add-section .cmdline=${CMDLINE} --change-section-vma .cmdline=0x30000 \
--add-section .linux=${KERNEL} --change-section-vma .linux=0x40000 \
--add-section .initrd=${BUILDDIR}/initramfs.img --change-section-vma .initrd=0x3000000 \
${EFISTUB} ${BUILDDIR}/combined-boot.efi
/usr/bin/sbsign --key ${CERTDIR}/DB.key --cert ${CERTDIR}/DB.crt --output ${BUILDDIR}/combined-boot-signed.efi ${BUILDDIR}/combined-boot.efi
cp ${BUILDDIR}/combined-boot-signed.efi ${OUTIMG}
rm -r $BUILDDIR
Finally we create /etc/pacman.d/hooks/secure-boot.hook
with the following content:
[Trigger]
Operation = Install
Operation = Upgrade
Type = Path
Target = usr/lib/modules/*/vmlinuz
[Trigger]
Operation = Install
Operation = Upgrade
Operation = Remove
Type = Path
Target = usr/lib/initcpio/*
# remove or change the following if you don't use intel-ucode
[Trigger]
Operation = Install
Operation = Upgrade
Operation = Remove
Type = Package
Target = intel-ucode
[Trigger]
Operation = Upgrade
Type = Package
Target = systemd
[Action]
Description = Updating UEFI kernel images...
When = PostTransaction
Exec = /bin/sh -c '/root/secure-boot/autosign.sh "boot/vmlinuz-linux"'
NeedsTargets
This hook gets triggered every time a package like systemd or intel-ucode is upgraded or when initramfs files and kernel images are modified.
To test the hook you can run sudo pacman -S linux
: if in the post-transaction
hooks is present the “Updating UEFI kernel images…” message, then the hook is
working correctly.
Installing the keys in the firmware#
If everything worked, now you can install the keys in you firmware. Unfortunately if you want to use the firmware of your own computer, this passage is different for every UEFI, so I won’t be describing this in detail.
You will need to put your motherboard into secure boot setup mode. To do this on a Thinkpad x240, boot into setup (press F1 at the splash screen), toggle over to “Security” (right arrow key), toggle down to “Secure Boot” (down arrow key), select it (enter), go down to “Reset to Setup Mode”, and hit enter. Now, hit escape to go back, scroll over to “Reboot”, select “Exit Saving Changes”, and hit “Yes”.
You are now ready to use KeyTool to install the keys. Copy
/usr/share/efitools/efi/KeyTool.efi
to your ESP (ie, into /boot), and boot from
it.
Now you can boot into the KeyTool entry, and you’re ready to replace the keys.
On the KeyTool main menu, you have the option to save the existing keys, which I suggest you do.
After you do or don’t do that, select “Edit Keys” and hit enter, which will bring you to the edit keys page.
Next you may delete every key you want deleted, even Microsoft ones, but make sure you don’t have a dual boot setup or you hardware doesn’t need them to work.
Now you need to add your keys, in the order db, KEK, and then PK.
To add a db key, select the db entry, hit “Add New Key”, select the device with
your .cer, .esl, and .auth files, navigate to the files, and select the DB.esl
file. Repeat this for the KEK with KEK.esl
.
Finally, add your platform key. Select “The Platform Key (PK)”, select “Replace
Key(s)”, navigate to PK.auth
, and select it. You can now exit the KeyTool
menus.
Restart your device and check if everything is working correctly, you could try booting from a usb drive and see if the booting process gets interrupted (which is what should happen if the EFI executable in the usb drive is not signed).
Conclusion#
The manual implementation of Secure Boot is annoying and very error-prone, it
took me 2 days to make it work correctly with my limited knowledge on the
matter, but at least I discovered how a system can be secured from the very
beginning of the session.
My final advice is to encrypt your root partition with LUKS to keep your data
safe.
Enjoy your secured boot process! 🔒