1525 Words
Guest article by maxice8

Recently I got myself into package sd-boot from the Systemd project into Alpine Linux. I previously packaged it as an April’s fools joke for Void Linux Here.

After receiving some negative feedback on including it, more specifically about the fact I had to include 23 patches (which could be trimmed but would be more work than just copying the work of the OpenEmbedded and NixOS Musl people) to make it compile.

A side-effect of packaging sd-boot is that I got into, and learnt a few things about booting directly from UEFI. Including:

  • Creating Unified Kernel Images as described here.
  • Creating all the components necessary for Secure Boot.
  • Signing the Unified Kernel Image to boot under Secure Boot.

Unified Kernel Images

Unified Kernel Images, according to the documentation is:

A unified kernel image is a single EFI PE executable combining an EFI stub loader, a kernel image, an initramfs image, and the kernel command line.

So basically a big file with all the components necessary for booting directly from UEFI instead of relying on a bootloader. Easier for signing.

Installing

The first step is installing the packages necessary to perform all the steps necessary to have a working Unified Kernel Image.

For this we are using Alpine Linux since it’s what I use (Change the commands to equivalents of your distribution).

We need to install the binutils package which provides the objcopy binary and gummiboot which provides the EFI stub loader in /usr/lib/gummiboot/linuxx64.efi.stub.

Both are in the main repository, so installing them is a matter of just calling apk with the proper commands:

# apk add binutils gummiboot

Unifying

With all the packages installed we can make the Unified Kernel Image.

CPU Microcode

One of the components of the Unified Kernel Image is the initramfs. On other bootloaders you can just specify more than initramfs, having the CPU microcode coming before the actual initramfs.

Not so much with the Unified Kernel Image. For that we will have to create a single initramfs that includes the CPU microcode.

$ cat /boot/intel-ucode.img /boot/initramfs-lts > /tmp/initramfs-lts

Simply enough, that will work. Remember to change the intel-ucode for whatever AMD uses or to ignore this section altogether if you don’t need it.

objcopying

Now that we have our initramfs with the microcode we can use objcopy to create the image. We will have 4 sections:

  • .osrel, where the contents of /etc/os-release are present (some distributions have it in /usr/lib/os-release, but not Alpine Linux)
  • .cmdline, where the options passed to the command line of the kernel are present, we will use /proc/cmdline to get from our currently running kernel, but you can write your own into a file and use it instead.
  • .linux, the kernel itself goes here.
  • .initrd, the initramfs itself goes here.

We will use the EFI stub loader from gummiboot which is present in /usr/lib/gummiboot/linuxx64.efi.stub and the Unified Kernel Image will be written to /boot as alpine.efi.

The objcopy invocation goes like so:

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/linux-lts" --change-section-vma .linux=0x40000 \
	--add-section .initrd="/tmp/unified-initramfs" --change-section-vma .initrd=0x3000000 \
	/usr/lib/gummiboot/linuxx64.efi.stub /boot/alpine.efi

And we have a functional Unified Kernel Image.

$ file /boot/alpine.efi
/boot/alpine.efi: PE32+ executable (EFI application) x86-64 (stripped to external PDB), for MS Windows

Secure Boot

One of the things I wanted to do when re-creating my boot scheme by using sd-boot was to have Secure Boot enabled. Just for challenging myself into new territory.

Arch Linux Wiki has a full page on it, and they point to an extensive article on Secure Boot. I won’t waste anytime trying to explain Secure Boot, the article referred from the Arch Linux Wiki is written by someone that is much more qualified to write about it than me. (That is your call to give it a read if you are interested, I’ll wait)

In my approach I made my own keys, which seemed easier than using Shim or PreLoader.

Installing

First we install the components necessary, we will need efitools. It is available only in edge because it was in the testing repository. I have moved it to main so it should make it to 3.12 but not 3.11 and below. So enable the testing repository and install it.

We will also use the sbsign binary from the sbsigntool package. It has the same situation as efitools, was in testing, moved to main, should be available in Alpine Linux 3.12 but not 3.11 and below.

In the meantime we will also use uuidgen from the util-linux package, so if you didn’t have that package installed already, do it.

# apk add efitools sbsigntool util-linux

You will also need the openssl command from the openssl package ( or libressl if your Alpine Linux version uses that). But since that is a system package that is very widely used, I will assume you already have it.

The following are literally copied from the Arch Linux Wiki.

$ uuidgen --random > GUID.txt
$ 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
$ sign-efi-sig-list -g "$(< GUID.txt)" -c PK.crt -k PK.key PK /dev/null rm_PK.auth
$ 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

$ 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

Enrolling

Now that the keys were made we need to add them to the Motherboard. As of writing this I’m using a Dell Inspiron 5566. The Dell firmware is really really nice and allows me to do all the required operations by just clicking around their interface.

That means you will have to figure your way in your motherboard’s firmware.

First copy the required files to the FAT boot partition that the motherboard can see.

# cp *.cer *.esl *.auth /boot

Then reboot into your motherboard’s firmware and enroll them. You can remove the files afterward.

# rm -f /boot/*.cer /boot/*.esl /boot/*.auth

Signing

Now that our keys are enrolled we can sign our Unified Kernel Image:

$ sbsign --key db.key --cert db.crt --output /boot/alpine.efi /boot/alpine.efi

Booting

The last step is creating an UEFI boot entry, stored normally in NVRAM as UEFI config variables with boot configuration along them.

Installing

To deal with UEFI config variables in Linux we need the mount command which is part of the busybox package, it will be replaced with the mount command from the util-linux package if the latter is installed, but both are OK.

To manipulate the UEFI config variables we will use the efibootmgr command from the efibootmgr package. It is in the community repository.

# apk add efibootmgr

Mounting

First we need to mount the UEFI config variables with the efivarfs filesystem. According to the kernel documentation it can be done like this:

# mount -t efivarfs none /sys/firmware/efi/efivars

Configuring

Now we create the boot entry with efibootmgr. I’ll show the final invocation of efibootmgr first and explain each switch/flag used separately below it.

# efibootmgr \
	--create \
	--disk /dev/sda \
	--part 1 \
	--label "Alpine Linux" \
	--loader "\alpine.efi"

So:

  • The --create call should be obvious, we want to create the boot entry, with it we also add it to the boot order, if you do not wish to add to it to the boot order then --create-only can be used.
  • --disk /dev/sda points to the disk where the EFI partition is, if your EFI partition is in another disk then change it appropriately.
  • --part 1 is the number of the partition, since my EFI partition is in /dev/sda1 then this is 1, but if your partition is in /dev/sda5 then it is 5.
  • --label "Alpine Linux" label used, this will appear in the Motherboard Firmware for editing and when you press F12 (or whatever F-Key) to pick a different boot option. If you use another distribution then change it to it.
  • --loader "\alpine.efi" the location of the Unified Kernel Image, in relation to the EFI partition, since we used /boot/alpine.efi then we set it to \alpine.efi (NOTE: the backslash is how it should be, this is Windows stuff).

After calling the invocation you can use efibootmgr to see if was added properly.

$ efibootmgr
BootCurrent: 0000
Timeout: 0 seconds
BootOrder: 0000
Boot0000* Alpine Linux

This is my system, I have executed the order 66 on all other boot options, your should still have the other options like “Windows Boot Manager”. The important thing here is that the “Alpine Linux” entry appears.

If you wish to have it default to booting Alpine Linux then have it be the first boot entry that appears in BootOrder.