Skip to content

Instantly share code, notes, and snippets.

@JoelJaeschke
Last active March 29, 2022 17:29
Show Gist options
  • Save JoelJaeschke/6be253279e85e8f285cf3c7178f57d2f to your computer and use it in GitHub Desktop.
Save JoelJaeschke/6be253279e85e8f285cf3c7178f57d2f to your computer and use it in GitHub Desktop.
SecureBoot with EFI Stub for Debian/Fedora

Convert Debian to use EFI Stub

This is a manual to convert a Debian installation to use EFI Stub for booting instead of relying on a conventional bootloader like GRUB. When used in conjunction with a unified kernel image, it is easy to sign kernel images and encrypt everything except the EFI boot partition while Secure Boot takes care of checking the kernel signature.

Necessary dependencies

Installation is easier on a minimal system as less things can break. Note that this manual is intended for a fresh installation and not for converting your system, although that should be possible the same way. To make things easy, install your system as if you were going to use it normally. This manual will assume the following partitioning scheme:

1) EFI partition
2) /boot partition
3) / partition (possibly encrypted, although that is not mandatory)

Before starting, make sure the following packages are installed (required):

binutils
efibootmgr
efitools
dosfstools
parted
gdisk

You should now have a system that boots using conventional GRUB (and possibly shim's).

Creating a bootable image using systemd's stub

This manual will first run through creating a plain unified kernel image containing the initramfs as well as the kernel and the kernel commandline parameters. At the bottom, there is a script that does all these steps automatically, but I would recommend to run through all steps by hand once to see how things are working.

Create unified kernel image

First, a unified kernel image is necessary. To create it, you need the kernel code running on boot. For Debian/Fedora, this is located at /boot/vmlinuz-$(uname -r). Next, the initial ramdisk is located at /boot/initrd.img-$(uname -r). Additionally, command options for startup are necessary. Your system will have all necessary parameters set by default during installation, as some things like which microcode to load can depend on your platform and system. The easiest route is to just adapt them from cat /proc/cmdline. Note here that the initrd= parameter can be left out as that will now be taken care of by systemd-boot.

initrd=\\EFI\\Debian\\initrd.img \ <-- Can be left out
rd.luks.name=58d7fdfa-c5fb-411f-8e2d-dfccd954773f=nvme0n1p3_crypt \
rd.luks.options=allow-discards \
root=UUID=3928b0de-0d86-4afa-b2bd-d1cbc649bfd2 \
rw quiet

Next, the EFI image itself needs to be created by running the following line. It is possible to substitue .cmdline={..} for the path to a file that holds your command line options.

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

Note the last line: /boot/efi/EFI/Debian/unified_debian.efi may change depending on your distribution. Check the correct path on your distribution of choice.

Lastly, a new boot entry for the image is necessary in order for the UEFI to know what to boot. The entry can be created by running the following command:

$ efibootmgr \
   --create --disk /dev/nvme0n1 --part 1 \
   --label "Unified Kernel Image" \
   --loader "\EFI\Debian\unified_kernel.efi"

The last line is local to the /boot/efi directory and here, a backslash is necessary. Adapt the filenames from the previous command.

Note that the label is not important and you can choose anything you want. Try and not use special characters as that can lead to some nasty problems on some misbehaved UEFI implementations.

Lastly, check if the bootorder is correct. After running efibootmgr -v, your entry should be at the top of the boot order. If not, you need to change it so the newly created entry has the highest precedence. To create a new order, run efibootmgr -o XXXX,YYYY,ZZZZ where XXXX is the id of your unified kernel image and YYYY,ZZZZ,... and so forth are the remaining entries. On my laptop for example (Lenovo L480), there is a setting in the BIOS called Unlock boot order that needs to be disabled for efibootmgr to be able to overwrite entries.

On some UEFI implementations, the above might not work correctly. In that case, try to set the boot order manually via the UEFI interface of your motherboard. And make sure you are running the most up-to-date version of UEFI firmware. If the above does not work, this is very likely to be a bug in your UEFI and you should probably make your motherboard manufacturer aware.

If you now reboot your system and run efibootmgr, the output should look something like this:

BootCurrent: 0000
Timeout: 1 seconds
BootOrder: 0000,0001
Boot0000* Unified Kernel (Debian)
Boot0001* Unified Kernel (Recovery)

The BootCurrent entry reports the image that has been booted and should be the Unified Kernel Image you just created. If this differs and is your GRUB boot, check if BootOrder is correct and refer to the above steps.

Add personal keys for Secure Boot

IMPORTANT: Before doing any of this, disable Secure Boot. There may be some hidden conditions that may cause your firmware to brick.

First, it is helpful to have all the old keys backed up to somewhere safe. Most motherboards do allow for the original keys to be restored, but better to be safe than sorry. To read keys, the efi-readvar utility from the efitools package will be used. To read all the keys, run the following commands:

$ efi-readvar -v PK -o old_PK.esl
$ efi-readvar -v KEK -o old_KEK.esl
$ efi-readvar -v db -o old_db.esl
$ efi-readvar -v dbx -o old_dbx.esl

This will store the Platform Key (PK), the Key Exchange Key (KEK) and the database keys in the current directory.

Next, a GUID for the user will be created. This is soley to make identification later on more easy and not necessary.

$ uuidgen --random > GUID.txt

Now, your new personal keys need to be created. To do so, you can either manually run the following pieces of code or alternatively download and run the following script: https://www.rodsbooks.com/efi-bootloaders/mkkeys.sh (Choose something for the common name)

Platform key:

$ openssl req -newkey rsa:4096 -nodes -keyout PK.key \
   -new -x509 -sha256 -days 3650 -subj "/CN=my Platform Key/" \
   -out PK.crt
$ openssl x509 -outform DER -in PK.crt -out PK.cer
$ cert-to-efi-sig-list -g "$(< GUID.txt)" PK.crt PK.esl
$ sign-efi-sig-list -g "$(< GUID.txt)" -k PK.key \
   -c PK.crt PK PK.esl PK.auth

Key Exchange Key:

$ openssl req -newkey rsa:4096 -nodes -keyout KEK.key \
   -new -x509 -sha256 -days 3650 -subj "/CN=my Key Exchange Key/" \
   -out KEK.crt
$ openssl x509 -outform DER -in KEK.crt -out KEK.cer
$ cert-to-efi-sig-list -g "$(< GUID.txt)" KEK.crt KEK.esl
$ sign-efi-sig-list -g "$(< GUID.txt)" -k PK.key \
   -c PK.crt KEK KEK.esl KEK.auth

Signature Database key:

$ openssl req -newkey rsa:4096 -nodes -keyout db.key \
   -new -x509 -sha256 -days 3650 -subj "/CN=my Signature Database key/" \
   -out db.crt
$ openssl x509 -outform DER -in db.crt -out db.cer
$ cert-to-efi-sig-list -g "$(< GUID.txt)" db.crt db.esl
$ sign-efi-sig-list -g "$(< GUID.txt)" -k KEK.key -c KEK.crt \
   db db.esl db.auth

To enroll the newly created keys, the process varies wildly depending on manufacturer and firmware version. If your UEFI can enroll keys from a FAT32 partition like the ESP, it is enough to copy all necessary files to the partition and enroll them manually from within your UEFI:

$ cp *.{cer,esl,auth} /boot/efi/EFI/Debian/keys

It may be advisable to enroll keys in reverse order of their precedence. Keys for secure boot follow a hierarchical order where the PK is more important than the KEK which in turn is more important than the Signature Database. To enroll your keys, start of by clearing everything and then enroll the keys in reverse order: DB -> KEK -> PK. I may have soft-bricked my motherboard the first time I did this in another order and the only remedy was to reflash the mainboard. 1

If that does not work for you, I would recommand to have a look at other possibilities listed in [6].

Signing the Unified Kernel Image

To sign the kernel with the keys created earlier, run the following command:

$ sbsign --key db.key --cert db.crt --output \
    /boot/efi/EFI/debian/unified_kernel.efi \
    /boot/efi/EFI/Debian/unified_kernel.efi

After the kernel is signed, everything is ready and you should be able to reboot, enable secure boot and run your system.

Automatic image rebuilding

Running objcopy and sbsign on every kernel update or change is not only tedious but also error-prone. To automate the process, you can create a script that runs on every update or removal of the kernel as well as the initramfs.

#!/bin/sh
printf "I: Updating unified kernel image...\n"
  
# This is a copy of the old image to be able to revert back after a broken update
cp /boot/efi/EFI/Debian/unified_kernel.efi /boot/efi/EFI/Debian/unified_kernel_old.efi

new_kernel_version=$(ls -lrt --time-style=full-iso /lib/modules | cut -d" " -f9 | tail -n1)
printf "I: New kernel versio is $new_kernel_version\n"
  
# Build a new kernel image
objcopy \
   --add-section .osrel="/etc/os-release" --change-section-vma .osrel=0x20000 \
   --add-section .cmdline="/proc/cmdline" --change-section-vma .cmdline=0x30000 \
   --add-section .linux="/boot/vmlinuz-$new_kernel_version" --change-section-vma .linux=0x40000 \
   --add-section .initrd="/boot/initrd.img-$new_kernel_version" --change-section-vma .initrd=0x3000000 \
   /usr/lib/systemd/boot/efi/linuxx64.efi.stub /boot/efi/EFI/Debian/unified_debian.efi
   
# Sign the kernel with our keys
sbsign --key db.key --cert db.crt --output \
   /boot/efi/EFI/debian/unified_kernel.efi \
   /boot/efi/EFI/Debian/unified_kernel.efi

Note that both the .linux and the .initrd section may be different between distributions in that debian names the ramdisk initrd.img-$(uname -r) while fedora for example names it ìnitramfs-$(uname -r).img. Adjust accordingly.

This script needs to be located at /etc/kernel/{postinst.d,postrm.d}/zz-update-uki and /etc/initramfs/post-update.d/zz-update-uki. Do this by either copying or symlinking the files there. It is important that the file starts with zz as the scripts in each folder are run in alpha-numerical order. By starting with zz*, we can assure that it will always run at the end.

It may be advisable to create another slot for the backup copy, should something ever break during an update. Follow the steps from before to do so (regarding the entry using efibootmgr).

You are now done and should be able to just reboot and run. The next steps are just cleanup.

Cleaning up the system

During install, the system was setup to use GRUB and all the tools that come with this. Additionally, the /boot partition can now be moved into the encrypted volumes as it is no longer necessary to boot since all necessary components are included inside of the unified kernel image. The following steps will remove all unnecessary components.

Move /boot to / partition

The Debian installer apparently forces you to create a /boot partition. Since it does not necessarily need to be a separate partition anymore, lets move it to the root file system.

First, unmount the EFI partition:

umount /boot/efi

Next, copy the /boot folder. Note the -a here which preserves all privileges. If this flag is not present, it may screw up permissions and make Linux unhappy:

cp -a /boot ~/boot_cpy

Then, unmont /boot:

umount /boot

Now, we can remove the old /boot directory:

rm -rf /boot

Lastly, recreate /boot again to create it on the encrypted partition:

mv ~/boot_cpy /boot

To finish this step, update /etc/fstab and remove separate entry for /boot as this is now present inside the partition itself.

Extend EFI partition and remove boot partition

Since the /boot partition is now gone, there is some unused space on the drive. While it would be perfectly fine to leave that empty, it can also be re-purposed as extra space for the EFI partition. First, let's backup the EFI partition:

cp -a /boot/efi/ ~/efi_back    

Next, we unmount the EFI partition:

umount /boot/efi  

Now, we repartition the disk:

gdisk /dev/nvme0n1
  p (list partitions)
  d -> 2 	(delete /boot partition)
  d -> 1	(delete /boot/efi partition)
  n (create new efi partition)
    -> [Enter] (partition number, default should be 1)
    -> [Enter] (first sector to use)
    -> [Enter] (last sector to use)
    -> EF00 (EFI partition number)
    -> w (write to disk)

To make Linux rediscover partitions, run the follwing:

partprobe /dev/nvme0n1

Next, format the new partition to be used as a EFI partition:

mkfs.fat -F32 /dev/nvme0n1p1

Lastly, we remount the partition and then copy all files from our backup into it:

mount /dev/nvme0n1p1 /boot/efi
cp -a ~/efi_back/EFI /boot/efi/

To finish things up, grab the new EFI partition UUID by typing blkid | grep EFI and then update /etc/fstab with the new UUID, rebuild the kernel image by rerunning the shell scripte created earlier and recreate the efibootmgr entry. Afterwards, you can safely reboot.

Uninstall grub (Debian)

As GRUB is still present on your system, it can now be removed as a last step. To do so, lets first remove all GRUB packages:

apt purge grub-efi-amd64 grub-efi-amd64-bin grub-efi-amd64-signed grub2-common

The rest of the dependencies can then be removed by running autoremove:

apt autoremove

Lastly, all remnants from the EFI partition can be removed:

rm fbx64.efi grub.cfg grubx64.efi BOOTX64.CSV mmx64.efi

Uninstall grub (Fedora)

As GRUB is still present on your system, it can now be removed as a last step. To do so, lets first remove all GRUB packages. As these are protected, we first need to remove all entries relating to grub and shims in /etc/dnf/protected.d/. These have names like /etc/protected.d/grub2-pc.x86_64.conf. Once they are removed, we can uninstall all modules by running

dnf remove grub2*

Lastly, all remnants from the EFI partition can be removed:

rm grub.cfg

Sources

[1] https://wiki.debian.org/EFIStub

[2] https://visualplanet.org/blog/?p=460

[3] https://superuser.com/questions/1230741/how-to-resize-the-efi-system-partition

[4] https://www.cogitri.dev/posts/04-secure-boot-with-unified-kernel-image/

[5] https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot

[6] https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot#Enrolling_keys_in_firmware

[x] https://www.reddit.com/r/linuxquestions/comments/9u2pjf/nvidia_and_secure_boot_how_can_i_make_these_both/

Notes

Footnotes

  1. This may have also been caused by a faulty UEFI implementation. I did not care enough to look into this more deeply and since the hardware was fairly new at the time, support forums did not have any information that described similar things yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment