Hey folks!

I have implemented support for booting Guix System using systemd-boot.  I'd 
like to get it upstreamed, but I want to see if there is interest and whether 
my general approach is agreeable before I put in the work required to make a 
clean patch ready for review.  If this is the sort of thing you think belongs 
in Guix, please let me know.  I'd like to offer some thanks to Lilah Tascheter 
for code and advice that she provided to help get me started hacking on Guix.  
My systemd-boot implementation builds on top of her work.

Below I have included some background context and a justification for why I 
want to use systemd-boot with Guix.  After that you'll find a high-level 
description of my approach.  You can find my implementation in my channel at 
https://git.squircle.space/gooy-guix.git/ in (gnu bootloader systemd).  
However, please remember that I do not feel that the implementation in my 
channel is ready to be submitted in a PR.  I have a number of cleanup tasks I 
need to complete first.

* Why systemd-boot?

I have been working on extending Guix's family of bootloaders to accomplish two 
objectives.

First, I want to improve Guix's support for full disk encryption.  The limiting 
factor here is GRUB.  grub-bootloader keeps the kernel and initrd in /gnu/store 
and thus needs to mount the filesystem containing the store before it can boot. 
 GRUB only supports a subset of the features of the full LUKS implementation in 
Linux, and even for the configurations that GRUB does support I have found its 
implementations are painfully slow.  In my opinion, the best solution is to put 
the kernel and initrd on an unencrypted filesystem.  That ensures that when it 
comes time to decrypt the root filesystem we will have access to a 
full-featured implementation for LUKS.

Second, I want Guix to support secure boot using MOK (machine owner keys).  On 
systems where the device owner is able to register new keys with the UEFI, they 
are able to sign their bootloader and related files and have the UEFI validate 
the signature before booting.  This protects the user against attacks that 
attempt to manipulate the booatloader or kernel.  It isn't complete protection 
against an evil-main-style attack, but it definitely increases the 
sophistication required to pull off that attack.

I believe that the best way to accomplish both of these is to add systemd-boot 
as a bootloader option in Guix.

Lilah implemented a bootloader that accomplished both of my objectives in 
https://issues.guix.gnu.org/68524.  Her initial implementation worked by 
generating "UKI"s (Unified Kernel Images) and registering them as boot options 
in the UEFI.  A UKI is a binary file that contains a kernel, an initrd, and a 
small UEFI bootloader "stub".  The stub makes the binary into a bootloader of 
sorts that the UEFI can directly boot into.  Reviewers pointed out that Lilah's 
implementation did not support `guix system rollback`, and so Lilah created a 
followup patchset that refactored the Guix bootloader machinery to allow her 
UEFI UKI bootloader to correctly handle rollbacks.  Herman Rimm contributed to 
this, but it seems like that effort stalled out.  See these threads for more 
info.
- https://issues.guix.gnu.org/73202
- https://lists.gnu.org/r/guix-devel/2024-02/msg00228.html
- https://mail.gnu.org/archive/html/guix-patches/2024-08/msg00309.html

The reason Lilah's first implementation didn't support rollback is fairly 
simple.  Guix's bootloader handling has two phases.  First, Guix generates a 
bootloader-specific "configuration file" and copies it onto the device that 
will contain the bootloader.  The logic for generating the bootloader 
configuration file is defined by the configuration-file-generator slot on the 
bootloader record, but the generator function must return a derivation that 
outputs a single file.  Guix's bootloader infrastructure copies the 
configuration file into its appropriate destination.  This happens during `guix 
system reconfigure` and `guix system rollback`.  In the second phase, Guix 
calls the function in the installer slot of the bootloader record.  This 
function is expected to, well, install the bootloader onto the target device.  
Unlike the configuration file generator, the installer is allowed to write to 
the installation target freely.  However, the installer is *not* called during 
`guix system rollback`.  Lilah needed to generate UKIs and configure the UEFI 
during the installer phase since each UKI needs to be individually copied onto 
the target disk.  Since the installer isn't run during rollbacks, her 
bootloader couldn't reconfigure the UEFI and change the boot order.

To make a UKI-based boot work with Guix's current bootloader system, we need to 
make it so that changing the configuration file is sufficient to change the 
boot order.  This means we cannot rely on directly configuring the UEFI to 
alter boot order.  We need a bootloader that is responsible for picking the 
right UKI to boot after consulting a configuration file.

Traditionally, GRUB fills this role.  We could imagine generating UKIs and then 
using GRUB's chainloader feature to make boot entries for them.  I believe this 
was proposed by one of the reviewers on Lilah's original patch.  In theory, 
this would work!  However, I chose to use systemd-boot as my bootloader.  Since 
my objective is to enable secure-boot, I wanted a dead-simple bootloader that 
has minimal surface area for attack.  It should be impossible to trick the 
bootloader into booting something that isn't properly signed.  GRUB is an 
incredibly impressive piece of software, but I don't think anyone would 
describe it as simple.  It is extremely feature-rich, and unfortunately 
features mean complexity and attack surface area.  By contrast, systemd-boot is 
specifically designed to have minimal functionality, and supporting secure boot 
is a first class feature of the bootloader.  For my purposes, systemd-boot is 
the right bootloader to use.

If you have concerns about systemd and dependencies, worry not.  systemd-boot 
is a standalone part of systemd.  Using systemd-boot does not require any other 
systemd components to be installed into the OS.  We only need the EFI stub 
binary, bootctl (a tool for installing systemd-boot), and a Python script named 
ukify.

* How systemd-bootloader works

The basic strategy I used is as follows.  I defined a new bootloader instance

(define-public systemd-bootloader
  (bootloader
   (name 'systemd-bootloader)
   ...
   (installer install-systemd-boot)
   (configuration-file "/boot/efi/loader/loader.conf")
   (configuration-file-generator systemd-bootloader-conf-generator)))

systemd-bootloader-conf-generator creates a loader.conf for systemd-boot which 
names the UKI that should be booted by default.  I also embed some metadata in 
loader.conf that describes how to generate the UKIs described by the system's 
bootloader-configuration-menu-entries.  install-systemd-boot installs 
systemd-boot and then reads the metadata embedded in loader.conf and produces 
the needed UKIs in /boot/efi/EFI/Linux.  If you're familiar with Lilah's 
install-uki.scm approach, this should seem familiar.  Unlike Lilah's approach, 
the configuration file contains data about how to generate the UKIs rather than 
executable code which will produce them.

Here is an example of what the loader.conf contains.  First, it sets options 
that systemd-boot checks for.  Namely, default and timeout.  The default option 
names a UKI file that will be generated during bootloader installation.  The 
UKI file names include a hash of the components they are made from (label, 
kernel, initrd, kernel arguments).  For brevity I have truncated hashes.

timeout 5
default guix-abcdef...efi

After those fields, there are a series of lines that contain the metadata 
required to generate the UKIs from the system's 
bootloader-configuration-menu-entries.  systemd-boot treats lines starting with 
# as comments, so we can easily embed our metadata without conflicting with 
systemd-boot's interpretation of the file.  Here's an example of what one of 
those lines might look like

# GUIX-ENTRY: (ukify "menu-entry-label goes here" 
"/gnu/store/abcdef...-kernel/bzImage" "/gnu/store/abcdef...-initrd" 
"kernel.arguments=go_here")

When we run install-systemd-boot, it scans through loader.conf looking for 
lines that start with "# GUIX-ENTRY: ".  We then read in the sexp that follows 
on the line.  The output filename is computed via hashing the 4 components, and 
if the UKI doesn't exist, it is generated using the data contained in the sexp. 
 One of the (ukify ...) sexps will correspond to the .efi binary named in the 
`default` option earlier in the file.  Any guix-*.efi files in /EFI/Linux that 
don't have corresponding entries in loader.conf are deleted.

When a system rollback occurs, guix only regenerates loader.conf.  This is fine 
since the UKI for the old system version will still be in /EFI/Linux.  All we 
need to do is change which file is named in the `default` setting.

* Conclusion

Again, if you think this is the sort of thing that should be upstreamed into 
Guix, please let me know.  I'm happy to put in the effort to adapt my work into 
a form suitable for merging into Guix, but I don't want to spend my time on 
that unless y'all are actually interested in merging something like this.

Thanks!
-Ada

Attachment: signature.asc
Description: OpenPGP digital signature

Reply via email to