Bug 254637 - [PATCH] Read kern.geom.eli.passphrase from UEFI variable for unattended boot without passphrase on disk
Summary: [PATCH] Read kern.geom.eli.passphrase from UEFI variable for unattended boot ...
Status: New
Alias: None
Product: Base System
Classification: Unclassified
Component: kern (show other bugs)
Version: 12.2-RELEASE
Hardware: Any Any
: --- Affects Many People
Assignee: freebsd-geom (Nobody)
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2021-03-29 06:50 UTC by Kyle George
Modified: 2021-04-03 11:12 UTC (History)
0 users

See Also:


Attachments
Patch to loader.efi to read kern.geom.eli.passphrase from UEFI environment (1.58 KB, patch)
2021-03-29 06:50 UTC, Kyle George
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Kyle George 2021-03-29 06:50:34 UTC
Created attachment 223677 [details]
Patch to loader.efi to read kern.geom.eli.passphrase from UEFI environment

TL;DR this patch reads kern.geom.eli.passphrase from a namespaced UEFI "environment variable" (NVRAM firmware variable), setting it in the boot environment for unattended boot of encrypted boot disks, without storing anything on-disk in the clear.

GELI is used in FreeBSD to encrypt disks / partitions.  When doing encryption of the boot disk, the options to decrypt the boot disk at boot time are either enter a passphrase, or (if applicable) keep an unencrypted /boot partition on the disk with keys.  However, in addition to not being a great idea (consider the threat model of having to RMA a drive, for example), the latter option does not work with UEFI: the UEFI loader.efi [0] stage probes devices and when it probes a GELI partition, it prompts for a passphrase.  So storing on disk in /boot is not an option with UEFI (and not really great anyway), and, at least on ZFS root, /boot is part of the zpool and thus encrypted by GELI regardless.  While the /boot option does work with legacy BIOS in some configurations, this is going away on modern systems.

The FreeBSD boot system already looks for the variable kern.geom.eli.passphrase in the boot runtime environment and, when present, this passphrase is used before prompting the user when it encounters a GELI-encrypted partition.  So if there was some way to get kern.geom.eli.passphrase populated it would solve this problem ...

UEFI has the ability to store arbitrary, namespaced variables in system NVRAM.  These are stored separate from any boot media (like on the motherboard).  This is good for at least two reasons: 1) we can read them early in the boot process, separate from any disk block and 2) because they're separate from the disks, the keys are never written in the clear to disk.  FreeBSD already has a (mostly unused, as far as I can tell) UEFI variable namespace (cfee69ad-a0de-47a9-93a8-f63106f8ae99).  We can store kern.geom.eli.passphrase there and read it into the boot environment _before_ disks are probed, making the rest of the boot infrastructure try it before prompting.  The attached patch does just that.

kern.geom.eli.passphrase can be written and read by userspace tool efivar:

# write

% cat passphrase | head -1 | tr -d $'\n' | doas efivar --name cfee69ad-a0de-47a9-93a8-f63106f8ae99-kern.geom.eli.passphrase -w

# read

% doas efivar -n cfee69ad-a0de-47a9-93a8-f63106f8ae99-kern.geom.eli.passphrase
cfee69ad-a0de-47a9-93a8-f63106f8ae99-kern.geom.eli.passphrase
0000: 74 65 73 74 31 32 33 34

      ^^^^^^^^^^^^^^^^^^^^^^^ test1234

efivar requires root to read/write.

The patch, which is fairly simple (but took a while to figure out exactly where/how to patch), is inline and attached:

    diff --git a/stand/efi/loader/main.c b/stand/efi/loader/main.c
    index a5213a51d88b..ecce7a2e4bba 100644
    --- a/stand/efi/loader/main.c
    +++ b/stand/efi/loader/main.c
    @@ -869,6 +869,8 @@ main(int argc, CHAR16 *argv[])
            char boot_info[4096];
            char buf[32];
            bool uefi_boot_mgr;
    +       char geom_eli_passphrase[256];
    +       UINTN geom_eli_bufsz;

            archsw.arch_autoload = efi_autoload;
            archsw.arch_getdev = efi_getdev;
    @@ -902,6 +904,22 @@ main(int argc, CHAR16 *argv[])
             */
            bcache_init(32768, 512);

    +       /*
    +        * Read kern.geom.eli.passphrase from the EFI environment under the
    +        * FreeBSD EFI GUID namespace (efi_freebsd_getenv).  Read before scanning
    +        * block IO media so that it's available when probing.
    +        */
    +       geom_eli_bufsz = sizeof(geom_eli_passphrase);
    +       bzero(geom_eli_passphrase, geom_eli_bufsz);
    +       rv = efi_freebsd_getenv("kern.geom.eli.passphrase", geom_eli_passphrase,
    +           &geom_eli_bufsz);
    +       if (rv == EFI_SUCCESS) {
    +               printf("kern.geom.eli.phassphrase read from EFI env\n");
    +               env_setenv("kern.geom.eli.passphrase", EV_VOLATILE,
    +                   &geom_eli_passphrase, env_noset, env_nounset);
    +               bzero(geom_eli_passphrase, geom_eli_bufsz);
    +       }
    +
            /*
             * Scan the BLOCK IO MEDIA handles then
             * march through the device switch probing for things.

To build:

    % cd /usr/src
    % patch -p1 < /path/to/patch.patch
    % cd /usr/src/stand/efi/loader
    % make
    % make install # changes /boot/loader.efi

To install:

    % mount -t msdosfs /dev/gpt/efiboot0 /mnt/efi
    % cp /boot/loader.efi /mnt/efi/efi/boot/BOOTx64.efi

To test:

    On any UEFI + GELI boot system, set the passphrase like above, and reboot.  Your system should boot without prompting (assuming your passphrase is correct, of course).

I've tested this / I'm using this on my own machine: 12.2-RELEASE amd64, with UEFI + GELI + zfsboot.  Patch is made against 12.2 branch from the git mirror.

[0] loader.efi is the first and last stage loader now, according to http://freebsd.1045724.x6.nabble.com/UEFI-loader-efi-and-boot-config-td6308485.html.  boot1.efi, which used to be the first stage, is being phased out.