diff mbox series

[bug#68524,v2,2/2] gnu: bootloaders: Add uefi-uki-bootloader.

Message ID 22f2967a552454baade056c60a37c02e36a048a5.1706435500.git.lilah@lunabee.space
State New
Headers show
Series Support root encryption and secure boot. | expand

Commit Message

Lilah Tascheter Jan. 28, 2024, 9:51 a.m. UTC
* doc/guix.texi (Bootloader Configuration)[bootloader,targets]: Document
  uefi-uki-bootloader and uefi-uki-signed-bootloader.
  (Keyboard Layout, Networking, and Partitioning)[Disk Partitioning]:
  Note use of uefi-uki-bootloader and uefi-uki-signed-bootloader.
* gnu/bootloader/uki.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add bootloader/uki.scm.

Change-Id: I2097da9f3dd35137b3419f6d0545de26d53cb6da
---
 doc/guix.texi          |  45 ++++++++++----
 gnu/bootloader/uki.scm | 129 +++++++++++++++++++++++++++++++++++++++++
 gnu/local.mk           |   1 +
 3 files changed, 163 insertions(+), 12 deletions(-)
 create mode 100644 gnu/bootloader/uki.scm

Comments

Hilton Chain Feb. 11, 2024, 6:39 p.m. UTC | #1
Hi Lilah,

On Sun, 28 Jan 2024 17:51:41 +0800,
Lilah Tascheter via Guix-patches wrote:
>
> * doc/guix.texi (Bootloader Configuration)[bootloader,targets]: Document
>   uefi-uki-bootloader and uefi-uki-signed-bootloader.
>   (Keyboard Layout, Networking, and Partitioning)[Disk Partitioning]:
>   Note use of uefi-uki-bootloader and uefi-uki-signed-bootloader.
> * gnu/bootloader/uki.scm: New file.
> * gnu/local.mk (GNU_SYSTEM_MODULES): Add bootloader/uki.scm.
>
> Change-Id: I2097da9f3dd35137b3419f6d0545de26d53cb6da
> ---
>  doc/guix.texi          |  45 ++++++++++----
>  gnu/bootloader/uki.scm | 129 +++++++++++++++++++++++++++++++++++++++++
>  gnu/local.mk           |   1 +
>  3 files changed, 163 insertions(+), 12 deletions(-)
>  create mode 100644 gnu/bootloader/uki.scm
>
> diff --git a/doc/guix.texi b/doc/guix.texi
> index c458befb76..30fd5d022b 100644
> --- a/doc/guix.texi
> +++ b/doc/guix.texi
> @@ -2723,12 +2723,13 @@ Keyboard Layout and Networking and Partitioning
>  for @command{cryptsetup luksFormat}.  You can check which key derivation
>  function is being used by a device by running @command{cryptsetup
>  luksDump @var{device}}, and looking for the PBKDF field of your
> -keyslots.
> +keyslots.  UEFI stub bootloaders, such as @code{uefi-uki-bootloader}, can
> +however use the default, more secure key derivation function.
>  @end quotation
>
> -Assuming you want to store the root partition on @file{/dev/sda2}, the
> -command sequence to format it as a LUKS2 partition would be along these
> -lines:
> +Assuming you want to store the root partition on @file{/dev/sda2} and use GRUB
> +as your bootloader, the command sequence to format it as a LUKS2 partition would
> +be along these lines:
>
>  @example
>  cryptsetup luksFormat --type luks2 --pbkdf pbkdf2 /dev/sda2
> @@ -41136,8 +41137,9 @@ Bootloader Configuration
>  The bootloader to use, as a @code{bootloader} object.  For now
>  @code{grub-bootloader}, @code{grub-efi-bootloader},
>  @code{grub-efi-removable-bootloader}, @code{grub-efi-netboot-bootloader},
> -@code{grub-efi-netboot-removable-bootloader}, @code{extlinux-bootloader}
> -and @code{u-boot-bootloader} are supported.
> +@code{grub-efi-netboot-removable-bootloader}, @code{extlinux-bootloader},
> +@code{u-boot-bootloader}, @code{uefi-uki-bootloader}, and
> +@code{uefi-uki-signed-bootloader} are supported.
>
>  @cindex ARM, bootloaders
>  @cindex AArch64, bootloaders
> @@ -41244,6 +41246,25 @@ Bootloader Configuration
>  unbootable.
>  @end quotation
>
> +@vindex uefi-uki-bootloader
> +@code{uefi-uki-bootloader} boots a linux kernel directly through UEFI, without
> +an intermediary like GRUB. The main practical advantage of this is allowing
> +root/store encryption without an extra GRUB password entry and slow decryption
> +step.
> +
> +@vindex uefi-uki-signed-bootloader
> +@code{uefi-uki-signed-bootloader} is like @code{uefi-uki-bootloader}, except
> +that it is a procedure that returns a bootloader compatible with UEFI secure
> +boot. You must provide it with two paths: an out-of-store secure boot db
> +certificate (in PEM format), and out-of-store db key, in that order.

Please add two spaces between sentences, e.g. "Sentence 1.  Sentence 2".

I think "out-of-store" is not necessary here. :)

> +
> +@quotation Warning
> +When using @code{uefi-uki-bootloader} or @code{uefi-uki-signed-bootloader},
> +@emph{do not} turn off your computer if a system reconfigure failed at
> +installing the bootloader.  Until bootloader installation succeeds, your system
> +is in an unbootable state.
> +@end quotation
> +
>  @item @code{targets}
>  This is a list of strings denoting the targets onto which to install the
>  bootloader.
> @@ -41252,12 +41273,12 @@ Bootloader Configuration
>  For @code{grub-bootloader}, for example, they should be device names
>  understood by the bootloader @command{installer} command, such as
>  @code{/dev/sda} or @code{(hd0)} (@pxref{Invoking grub-install,,, grub,
> -GNU GRUB Manual}).  For @code{grub-efi-bootloader} and
> -@code{grub-efi-removable-bootloader} they should be mount
> -points of the EFI file system, usually @file{/boot/efi}.  For
> -@code{grub-efi-netboot-bootloader}, @code{targets} should be the mount
> -points corresponding to TFTP root directories served by your TFTP
> -server.
> +GNU GRUB Manual}).  For @code{grub-efi-bootloader},
> +@code{grub-efi-removable-bootloader}, @code{uefi-uki-bootloader}, and
> +@code{uefi-uki-signed-bootloader}, they should be mount points of the EFI file
> +system, usually @file{/boot/efi}.  For @code{grub-efi-netboot-bootloader},
> +@code{targets} should be the mount points corresponding to TFTP root directories
> +served by your TFTP server.
>
>  @item @code{menu-entries} (default: @code{'()})
>  A possibly empty list of @code{menu-entry} objects (see below), denoting
> diff --git a/gnu/bootloader/uki.scm b/gnu/bootloader/uki.scm
> new file mode 100644
> index 0000000000..0ef62295d6
> --- /dev/null
> +++ b/gnu/bootloader/uki.scm
> @@ -0,0 +1,129 @@
> +;;; GNU Guix --- Functional package management for GNU
> +;;; Copyright © 2024 Lilah Tascheter <lilah@lunabee.space>
> +;;;
> +;;; This file is part of GNU Guix.
> +;;;
> +;;; GNU Guix is free software; you can redistribute it and/or modify it
> +;;; under the terms of the GNU General Public License as published by
> +;;; the Free Software Foundation; either version 3 of the License, or (at
> +;;; your option) any later version.
> +;;;
> +;;; GNU Guix is distributed in the hope that it will be useful, but
> +;;; WITHOUT ANY WARRANTY; without even the implied warranty of
> +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +;;; GNU General Public License for more details.
> +;;;
> +;;; You should have received a copy of the GNU General Public License
> +;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
> +
> +(define-module (gnu bootloader uki)
> +  #:use-module (gnu bootloader)
> +  #:use-module (gnu packages bootloaders)
> +  #:use-module (gnu packages efi)
> +  #:use-module (gnu packages linux)
> +  #:use-module (guix gexp)
> +  #:use-module (guix modules)
> +  #:use-module (srfi srfi-1)
> +  #:export (uefi-uki-bootloader uefi-uki-signed-bootloader))
> +
> +;; config generator makes script creating uki images
> +;; install runs script
> +;; install device is path to uefi dir
> +(define vendor "Guix")
> +(define script-loc "/boot/install-uki.scm")
> +
> +(define* (uefi-uki-configuration-file #:optional cert privkey)
> +  (lambda* (config entries #:key (old-entries '()) #:allow-other-keys)
> +
> +    (define (menu-entry->args e)

I'd prefer using ‘entry’ instead of ‘e’, same for some other nonobvious
abbreviations.

> +      (let* ((boot (bootloader-configuration-bootloader config))
> +             (stub (bootloader-package boot)))
> +        #~(list "--os-release" #$(menu-entry-label e)
> +            "--linux" #$(menu-entry-linux e) "--initrd" #$(menu-entry-initrd e)
> +            "--cmdline" (string-join (list #$@(menu-entry-linux-arguments e)))
> +            "--stub" #$(file-append stub "/libexec/" (systemd-stub-name))
> +            #$@(if cert #~("--secureboot-certificate" #$cert) #~())
> +            #$@(if privkey #~("--secureboot-private-key" #$privkey) #~()))))
> +
> +    (define (enum-filenames . args) ; same args as iota
> +      (map (lambda (n) (string-append (number->string n) ".efi"))
> +        (apply iota (map length args))))

"Same args as iota" doesn't explain the procudre, it actually accepts lists
instead of numbers, and only the first two lists are intended to be used, right?

I'd suggest just having two arguments with more descriptive names.

> +
> +    (program-file "install-uki"
> +      (with-imported-modules (source-module-closure '((guix build syscalls)
> +                                                      (guix build utils)))
> +        #~(let* ((target (cadr (command-line)))
> +                 (vendir (string-append target "/EFI/" #$vendor))
> +                 (schema (string-append vendir "/boot.mgr"))
> +                 (findmnt #$(file-append util-linux "/bin/findmnt"))
> +                 (efibootmgr #$(file-append efibootmgr "/sbin/efibootmgr")))
> +            (use-modules (guix build syscalls) (guix build utils)
> +                         (ice-9 popen) (ice-9 textual-ports))
> +
> +            (define (out name) (string-append vendir "/" name))
> +            (define disk
> +              (call-with-port
> +                (open-pipe* OPEN_READ findmnt "-fnro" "SOURCE" "-T" target)
> +                (lambda (port) (get-line port)))) ; only 1 line: the device

(guix build syscalls) has procedures can be utilized for this, no need to invoke
findmnt.

> +
> +            ;; delete all boot entries and files we control
> +            (when (file-exists? schema)
> +              (call-with-input-file schema
> +                (lambda (port)
> +                  (for-each (lambda (l)
> +                              (unless (string-null? l)
> +                                (system* efibootmgr "-B" "-L" l "-q")))

Dispite the "-q" option, error messages will still be visible to user, please
use ‘invoke/quiet’ + error handling instead.

And I'd prefer long options.

> +                    (string-split (get-string-all port) #\lf)))))

‘#\newline’ is preferred over ‘#\lf’ in Guix source.

> +            (when (directory-exists? vendir) (delete-file-recursively vendir))
> +            (mkdir-p vendir)
> +
> +            (define (install port boot? oos)
> +              (lambda (args label name)
> +                (let ((minbytes (* 2 (stat:size (stat #$script-loc)))))
> +                  (put-string port label)
> +                  (put-char port #\lf)
> +                  (force-output port) ; make sure space is alloc'd
> +                  (apply invoke #$(file-append ukify "/bin/ukify")

‘invoke/quiet’ can be used.

> +                    "build" "-o" (out name) args)
> +                  ;; make sure we have enough space for next install-uki.scm
> +                  (when (and oos (< (free-disk-space vendir) minbytes)) (oos))
> +                  (invoke efibootmgr (if boot? "-c" "-C") "-L" label "-d" disk
> +                    "-l" (string-append "\\EFI\\" #$vendor "\\" name) "-q"))))

You're calling the exception handler directly here, it might be better to move
the exception handling part into the procedure to avoid this.

> +
> +            (call-with-output-file schema
> +              (lambda (port) ; prioritize latest UKIs in limited ESP space
> +                (for-each (install port #t #f)
> +                  (list #$@(map-in-order menu-entry->args entries))
> +                  (list #$@(map-in-order menu-entry-label entries))
> +                  (list #$@(enum-filenames entries)))
> +                (for-each ; old-entries can fail (out of space) we don't care
> +                  (lambda (args label name)
> +                    (define (cleanup . _) ; do exit early if out of space tho
> +                      (when (file-exists? (out name)) (delete-file (out name)))
> +                      (exit))
> +                    (with-exception-handler cleanup
> +                      (lambda _ ((install port #f cleanup) args label name))))
> +                  (list #$@(map-in-order menu-entry->args old-entries))
> +                  (list #$@(map-in-order menu-entry-label old-entries))
> +                  (list #$@(enum-filenames old-entries entries))))))))))

These two ‘for-each’ can be merged into one.

> +
> +(define install-uefi-uki
> +  #~(lambda (bootloader target mount-point)
> +      (invoke (string-append mount-point #$script-loc)
> +              (string-append mount-point target))))
> +
> +(define* (make-uefi-uki-bootloader #:optional cert privkey)
> +  (bootloader
> +    (name 'uefi-uki)
> +    (package systemd-stub)
> +    (installer install-uefi-uki)
> +    (disk-image-installer #f)
> +    (configuration-file script-loc)
> +    (configuration-file-generator (uefi-uki-configuration-file cert privkey))))
> +
> +;; IMPORTANT NOTE: if bootloader install fails, do not turn off your computer! until
> +;; install succeeds, your system is unbootable.
> +(define uefi-uki-bootloader (make-uefi-uki-bootloader))
> +;; use ukify genkey to generate cert and privkey. DO NOT include in store.
> +(define (uefi-uki-signed-bootloader cert privkey)
> +  (make-uefi-uki-bootloader cert privkey))

They'll have the same bootloader name, making these two bootloaders
indistinguishable from boot-parameters.

> diff --git a/gnu/local.mk b/gnu/local.mk
> index ab63bd5881..0a15251ddf 100644
> --- a/gnu/local.mk
> +++ b/gnu/local.mk
> @@ -90,6 +90,7 @@ GNU_SYSTEM_MODULES =				\
>    %D%/bootloader/extlinux.scm                   \
>    %D%/bootloader/u-boot.scm                     \
>    %D%/bootloader/depthcharge.scm                \
> +  %D%/bootloader/uki.scm                        \
>    %D%/ci.scm					\
>    %D%/compression.scm				\
>    %D%/home.scm					\
> --
> 2.41.0

I tried to adjust uki.scm before commenting, so here's a paste of my adjusted
version, in case some of my comments are not expressed clearly:
https://paste.sr.ht/~hako/62bb15503290273e869520e12466718ebb82e000

Thanks
Lilah Tascheter Feb. 13, 2024, 2:11 a.m. UTC | #2
heyo!

thanks for the review :) I'll submit a revised patch, but had a question before
I get to work on it.

> I tried to adjust uki.scm before commenting, so here's a paste of my
> adjusted version, in case some of my comments are not expressed clearly:
> https://paste.sr.ht/~hako/62bb15503290273e869520e12466718ebb82e000

nighttime sky I didn't realize reinstall-bootloader existed. shit.

at this point, I don't think the install-uki.scm hack is a good idea. to get
this fully functioning, will probably have to do some more invasive edits to the
bootloader system, since the current one pretty much assumes an
extlinux/grubalike (which is what necessitated install-uki in the first place).
RFC on the following plan:

* add secure-boot-cert and secure-boot-key fields to bootloader-configuration.

* deprecate configuration-file and configuration-file-generator in the
  bootloader struct, and instead create an install-configuration-file field,
  similar to install-bootloader. default procedure will be to do the current
  install-boot-cfg (gnu build install) using the deprecated fields.

* rework uki.scm to, instead, run efibootmgr in install-configuration-file and
  install the uki.efi files in install-bootloader. remove the separation between
  uefi-uki-signed-bootloader and uefi-uki-bootloader, instead working off the
  new bootloader-configuration fields.

this plan should work with reinstall-bootloader, even though it uses the default
bootloader-configuration, since files are only signed during installation
proper.

opinions?

thanks,
lilah
Lilah Tascheter Feb. 13, 2024, 7:34 a.m. UTC | #3
> * add secure-boot-cert and secure-boot-key fields to bootloader-
> configuration.
>
> * deprecate configuration-file and configuration-file-generator in
>   the bootloader struct, and instead create an install-configuration-file
>   field, similar to install-bootloader. default procedure will be to do the
>   current install-boot-cfg (gnu build install) using the deprecated fields.
>
> * rework uki.scm to, instead, run efibootmgr in install-
>   configuration-file and install the uki.efi files in install-bootloader.
>   remove the separation between uefi-uki-signed-bootloader and
>   uefi-uki-bootloader, instead working off the new bootloader-configuration
>   fields.

amending: also edit the bootloader-installer and bootloader-disk-image-installer
procedures to provide the bootloader-configuration in some manner.

lilah
Hilton Chain Feb. 14, 2024, 6:02 p.m. UTC | #4
Hi Lilah,

On Tue, 13 Feb 2024 15:34:55 +0800,
Lilah Tascheter wrote:
>
> > * add secure-boot-cert and secure-boot-key fields to bootloader-
> > configuration.

How about using a pair instead of two fields?  And because the usage depends on
the bootlodaer, I'd like to use a generic name.

e.g. signing-keypair
 => '("/path/to/certificate" . "/path/to/private.key")

> > * deprecate configuration-file and configuration-file-generator in
> >   the bootloader struct, and instead create an install-configuration-file
> >   field, similar to install-bootloader. default procedure will be to do the
> >   current install-boot-cfg (gnu build install) using the deprecated fields.

I'd prefer ‘configuration-installer’, since the installation target may not be a
file. :)

I don't think the deprecation is necessary though, other bootloaders don't have
to duplicate this part of code, and in my opinion the following definition does
make sense.

--8<---------------cut here---------------start------------->8---
(define uefi-uki-bootloader
  (bootloader
   (name 'uefi-uki)
   (package systemd-stub)
   (installer install-uefi-uki)
   (configuration-installer install-uefi-uki-configuration)
   (configuration-file #f)
   (configuration-file-generator #f)))
--8<---------------cut here---------------end--------------->8---

> > * rework uki.scm to, instead, run efibootmgr in install-
> >   configuration-file and install the uki.efi files in install-bootloader.
> >   remove the separation between uefi-uki-signed-bootloader and
> >   uefi-uki-bootloader, instead working off the new bootloader-configuration
> >   fields.
>
> amending: also edit the bootloader-installer and bootloader-disk-image-installer
> procedures to provide the bootloader-configuration in some manner.

I agree that <bootloader> needs modifying, since unified kernel images currently
cannot be well described.  And to support proper generation switching, some
fields of <bootloader-configuration> need exposing.

As this now involves deeper change, I think it's better to post the plan on
guix-devel@gnu.org for wider visibility and potential discussions.

Thanks
diff mbox series

Patch

diff --git a/doc/guix.texi b/doc/guix.texi
index c458befb76..30fd5d022b 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -2723,12 +2723,13 @@  Keyboard Layout and Networking and Partitioning
 for @command{cryptsetup luksFormat}.  You can check which key derivation
 function is being used by a device by running @command{cryptsetup
 luksDump @var{device}}, and looking for the PBKDF field of your
-keyslots.
+keyslots.  UEFI stub bootloaders, such as @code{uefi-uki-bootloader}, can
+however use the default, more secure key derivation function.
 @end quotation
 
-Assuming you want to store the root partition on @file{/dev/sda2}, the
-command sequence to format it as a LUKS2 partition would be along these
-lines:
+Assuming you want to store the root partition on @file{/dev/sda2} and use GRUB
+as your bootloader, the command sequence to format it as a LUKS2 partition would
+be along these lines:
 
 @example
 cryptsetup luksFormat --type luks2 --pbkdf pbkdf2 /dev/sda2
@@ -41136,8 +41137,9 @@  Bootloader Configuration
 The bootloader to use, as a @code{bootloader} object.  For now
 @code{grub-bootloader}, @code{grub-efi-bootloader},
 @code{grub-efi-removable-bootloader}, @code{grub-efi-netboot-bootloader},
-@code{grub-efi-netboot-removable-bootloader}, @code{extlinux-bootloader}
-and @code{u-boot-bootloader} are supported.
+@code{grub-efi-netboot-removable-bootloader}, @code{extlinux-bootloader},
+@code{u-boot-bootloader}, @code{uefi-uki-bootloader}, and
+@code{uefi-uki-signed-bootloader} are supported.
 
 @cindex ARM, bootloaders
 @cindex AArch64, bootloaders
@@ -41244,6 +41246,25 @@  Bootloader Configuration
 unbootable.
 @end quotation
 
+@vindex uefi-uki-bootloader
+@code{uefi-uki-bootloader} boots a linux kernel directly through UEFI, without
+an intermediary like GRUB. The main practical advantage of this is allowing
+root/store encryption without an extra GRUB password entry and slow decryption
+step.
+
+@vindex uefi-uki-signed-bootloader
+@code{uefi-uki-signed-bootloader} is like @code{uefi-uki-bootloader}, except
+that it is a procedure that returns a bootloader compatible with UEFI secure
+boot. You must provide it with two paths: an out-of-store secure boot db
+certificate (in PEM format), and out-of-store db key, in that order.
+
+@quotation Warning
+When using @code{uefi-uki-bootloader} or @code{uefi-uki-signed-bootloader},
+@emph{do not} turn off your computer if a system reconfigure failed at
+installing the bootloader.  Until bootloader installation succeeds, your system
+is in an unbootable state.
+@end quotation
+
 @item @code{targets}
 This is a list of strings denoting the targets onto which to install the
 bootloader.
@@ -41252,12 +41273,12 @@  Bootloader Configuration
 For @code{grub-bootloader}, for example, they should be device names
 understood by the bootloader @command{installer} command, such as
 @code{/dev/sda} or @code{(hd0)} (@pxref{Invoking grub-install,,, grub,
-GNU GRUB Manual}).  For @code{grub-efi-bootloader} and
-@code{grub-efi-removable-bootloader} they should be mount
-points of the EFI file system, usually @file{/boot/efi}.  For
-@code{grub-efi-netboot-bootloader}, @code{targets} should be the mount
-points corresponding to TFTP root directories served by your TFTP
-server.
+GNU GRUB Manual}).  For @code{grub-efi-bootloader},
+@code{grub-efi-removable-bootloader}, @code{uefi-uki-bootloader}, and
+@code{uefi-uki-signed-bootloader}, they should be mount points of the EFI file
+system, usually @file{/boot/efi}.  For @code{grub-efi-netboot-bootloader},
+@code{targets} should be the mount points corresponding to TFTP root directories
+served by your TFTP server.
 
 @item @code{menu-entries} (default: @code{'()})
 A possibly empty list of @code{menu-entry} objects (see below), denoting
diff --git a/gnu/bootloader/uki.scm b/gnu/bootloader/uki.scm
new file mode 100644
index 0000000000..0ef62295d6
--- /dev/null
+++ b/gnu/bootloader/uki.scm
@@ -0,0 +1,129 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2024 Lilah Tascheter <lilah@lunabee.space>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu bootloader uki)
+  #:use-module (gnu bootloader)
+  #:use-module (gnu packages bootloaders)
+  #:use-module (gnu packages efi)
+  #:use-module (gnu packages linux)
+  #:use-module (guix gexp)
+  #:use-module (guix modules)
+  #:use-module (srfi srfi-1)
+  #:export (uefi-uki-bootloader uefi-uki-signed-bootloader))
+
+;; config generator makes script creating uki images
+;; install runs script
+;; install device is path to uefi dir
+(define vendor "Guix")
+(define script-loc "/boot/install-uki.scm")
+
+(define* (uefi-uki-configuration-file #:optional cert privkey)
+  (lambda* (config entries #:key (old-entries '()) #:allow-other-keys)
+
+    (define (menu-entry->args e)
+      (let* ((boot (bootloader-configuration-bootloader config))
+             (stub (bootloader-package boot)))
+        #~(list "--os-release" #$(menu-entry-label e)
+            "--linux" #$(menu-entry-linux e) "--initrd" #$(menu-entry-initrd e)
+            "--cmdline" (string-join (list #$@(menu-entry-linux-arguments e)))
+            "--stub" #$(file-append stub "/libexec/" (systemd-stub-name))
+            #$@(if cert #~("--secureboot-certificate" #$cert) #~())
+            #$@(if privkey #~("--secureboot-private-key" #$privkey) #~()))))
+
+    (define (enum-filenames . args) ; same args as iota
+      (map (lambda (n) (string-append (number->string n) ".efi"))
+        (apply iota (map length args))))
+
+    (program-file "install-uki"
+      (with-imported-modules (source-module-closure '((guix build syscalls)
+                                                      (guix build utils)))
+        #~(let* ((target (cadr (command-line)))
+                 (vendir (string-append target "/EFI/" #$vendor))
+                 (schema (string-append vendir "/boot.mgr"))
+                 (findmnt #$(file-append util-linux "/bin/findmnt"))
+                 (efibootmgr #$(file-append efibootmgr "/sbin/efibootmgr")))
+            (use-modules (guix build syscalls) (guix build utils)
+                         (ice-9 popen) (ice-9 textual-ports))
+
+            (define (out name) (string-append vendir "/" name))
+            (define disk
+              (call-with-port
+                (open-pipe* OPEN_READ findmnt "-fnro" "SOURCE" "-T" target)
+                (lambda (port) (get-line port)))) ; only 1 line: the device
+
+            ;; delete all boot entries and files we control
+            (when (file-exists? schema)
+              (call-with-input-file schema
+                (lambda (port)
+                  (for-each (lambda (l)
+                              (unless (string-null? l)
+                                (system* efibootmgr "-B" "-L" l "-q")))
+                    (string-split (get-string-all port) #\lf)))))
+            (when (directory-exists? vendir) (delete-file-recursively vendir))
+            (mkdir-p vendir)
+
+            (define (install port boot? oos)
+              (lambda (args label name)
+                (let ((minbytes (* 2 (stat:size (stat #$script-loc)))))
+                  (put-string port label)
+                  (put-char port #\lf)
+                  (force-output port) ; make sure space is alloc'd
+                  (apply invoke #$(file-append ukify "/bin/ukify")
+                    "build" "-o" (out name) args)
+                  ;; make sure we have enough space for next install-uki.scm
+                  (when (and oos (< (free-disk-space vendir) minbytes)) (oos))
+                  (invoke efibootmgr (if boot? "-c" "-C") "-L" label "-d" disk
+                    "-l" (string-append "\\EFI\\" #$vendor "\\" name) "-q"))))
+
+            (call-with-output-file schema
+              (lambda (port) ; prioritize latest UKIs in limited ESP space
+                (for-each (install port #t #f)
+                  (list #$@(map-in-order menu-entry->args entries))
+                  (list #$@(map-in-order menu-entry-label entries))
+                  (list #$@(enum-filenames entries)))
+                (for-each ; old-entries can fail (out of space) we don't care
+                  (lambda (args label name)
+                    (define (cleanup . _) ; do exit early if out of space tho
+                      (when (file-exists? (out name)) (delete-file (out name)))
+                      (exit))
+                    (with-exception-handler cleanup
+                      (lambda _ ((install port #f cleanup) args label name))))
+                  (list #$@(map-in-order menu-entry->args old-entries))
+                  (list #$@(map-in-order menu-entry-label old-entries))
+                  (list #$@(enum-filenames old-entries entries))))))))))
+
+(define install-uefi-uki
+  #~(lambda (bootloader target mount-point)
+      (invoke (string-append mount-point #$script-loc)
+              (string-append mount-point target))))
+
+(define* (make-uefi-uki-bootloader #:optional cert privkey)
+  (bootloader
+    (name 'uefi-uki)
+    (package systemd-stub)
+    (installer install-uefi-uki)
+    (disk-image-installer #f)
+    (configuration-file script-loc)
+    (configuration-file-generator (uefi-uki-configuration-file cert privkey))))
+
+;; IMPORTANT NOTE: if bootloader install fails, do not turn off your computer! until
+;; install succeeds, your system is unbootable.
+(define uefi-uki-bootloader (make-uefi-uki-bootloader))
+;; use ukify genkey to generate cert and privkey. DO NOT include in store.
+(define (uefi-uki-signed-bootloader cert privkey)
+  (make-uefi-uki-bootloader cert privkey))
diff --git a/gnu/local.mk b/gnu/local.mk
index ab63bd5881..0a15251ddf 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -90,6 +90,7 @@  GNU_SYSTEM_MODULES =				\
   %D%/bootloader/extlinux.scm                   \
   %D%/bootloader/u-boot.scm                     \
   %D%/bootloader/depthcharge.scm                \
+  %D%/bootloader/uki.scm                        \
   %D%/ci.scm					\
   %D%/compression.scm				\
   %D%/home.scm					\