]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
stub: introduce "boot secret" stored in an EFI variable inaccessible to the OS 41217/head
authorLennart Poettering <lennart@amutable.com>
Sat, 7 Mar 2026 22:44:37 +0000 (23:44 +0100)
committerLennart Poettering <lennart@amutable.com>
Wed, 25 Mar 2026 15:19:27 +0000 (16:19 +0100)
man/systemd-stub.xml
src/boot/boot-secret.c [new file with mode: 0644]
src/boot/boot-secret.h [new file with mode: 0644]
src/boot/meson.build
src/boot/stub.c
tmpfiles.d/20-systemd-stub.conf.in

index 251d79ea6e14e110165fa950575ab7033d89fd28..bf23c900d026c52e3a21f4385d0edbf9c7953500 100644 (file)
 
         <xi:include href="version-info.xml" xpointer="v257"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>LoaderBootSecret</varname></term>
+
+        <listitem><para>A non-volatile EFI variable only accessible from the pre-boot environment
+        (i.e. access from the OS is not permitted) that contains a per-system secret. It is set automatically
+        by <command>systemd-stub</command> if not present already. A secret derived from the value of this
+        EFI variable is passed to the OS in <filename>/.extra/boot-secret</filename>, see below.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
     </variablelist>
 
     <para>Note that some of the variables above may also be set by the boot loader. The stub will only set
 
         <xi:include href="version-info.xml" xpointer="v257"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><filename>/.extra/boot-secret</filename></term>
+        <listitem><para>A 32 byte per-system secret which is derived from a 32 byte secret stored in an EFI
+        variable (<varname>LoaderBootSecret</varname>, see above), which itself is only accessible to the
+        pre-boot environment. This may be used for various early-boot cryptographic purposes, and OS file
+        system access to it is restricted to root. The <varname>IMAGE_ID=</varname>/<varname>ID=</varname>
+        data from the <literal>.osrel</literal> is hashed into the secret, to ensure that different images
+        get a distinct secret passed. Moreover, a randomized 32 byte value stored in the ESP in the
+        <literal>/loader/boot-secret-mixin</literal> file is hashed in as well, ensuring that distinct disks will
+        result in different boot secrets.</para>
+
+        <para>Note: this boot secret is ultimately protected only by firmware-enforced access controls on the
+        EFI variable. This is generally a much weaker protection than TPM-based approaches have, and it is
+        hence strongly recommended to use the TPM on systems that possess one. The boot secret is primarily
+        intended to be a lower-security fallback for cases where a TPM is not available.</para>
+
+        <para>Applications should never protect resources directly with this secret, but derive their own
+        secret from it (by hashing it together with some application ID, in HMAC mode for example), in order
+        not to accidentally leak the primary boot secret.</para>
+
+        <para>Note that the boot secret is only available if the pre-boot environment had a suitable RNG
+        source at the current boot or an earlier one. This source can be an initialized on-disk
+        random seed or the EFI RNG support, or both.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
     </variablelist>
 
     <para>Note that all these files are located in the <literal>tmpfs</literal> file system the kernel sets
diff --git a/src/boot/boot-secret.c b/src/boot/boot-secret.c
new file mode 100644 (file)
index 0000000..54078b5
--- /dev/null
@@ -0,0 +1,374 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "boot-secret.h"
+#include "efi-efivars.h"
+#include "efi-log.h"
+#include "random-seed.h"
+#include "sha256-fundamental.h"
+#include "util.h"
+
+#define BOOT_SECRET_MIXIN_PATH u"\\loader\\boot-secret-mixin"
+
+/* This maintains a per-system secret that is stored in an EFI variable that is only accessible during EFI
+ * boot, and becomes inaccessible afterwards, once ExitBootServices() is called. The variable is
+ * automatically initialized if missing. A secret derived by hashing from this EFI variable secret is then
+ * passed to the OS, in an initrd file inaccessible to unprivileged userspace. To make things a bit more
+ * robust while hashing two more pieces of information are mixed in: a random "mixin" that is stored in the
+ * ESP and is supposed to ensure that the passed boot secrets are distinct for each disk used on the system;
+ * moreover an OS identifier derived from the UKI's .osrel field (ideally IMAGE_ID=, but if not defined ID=
+ * will do, with a final fallback to "linux"). Note that these two additions are not supposed to enhance the
+ * cryptographic quality of the secret, they are just supposed to make things more robust on systems with
+ * multiple disks and OSes.
+ *
+ * The boot secret passed to the OS can be used to protect resources during OS runtime, from earliest boot
+ * phases on, as a fallback for the usual TPM based protections.
+ *
+ * Note that this secret comes with much weaker protection than TPM backed secrets: there's no physical
+ * isolation, there are no cryptographic access policies, there's just the hope the firmware reasonably
+ * correctly implements boot-time-only EFI variable mechanism. (But then again, this is what mok/shim's
+ * security also relies on, and hence this all is not too bad?) */
+
+static EFI_STATUS random_seed_find_table(struct linux_efi_random_seed **ret) {
+        assert(ret);
+
+        /* We use the Linux random seed EFI table as our source of randomness, since there's reason to
+         * believe it is as good as it possibly would get. Note that we ourselves might be the ones
+         * initializing it, based on EFI RNG APIs, the monotonic boot counter, a random seed file on disk and
+         * the clock. */
+
+        struct linux_efi_random_seed *seed_table =
+                find_configuration_table(MAKE_GUID_PTR(LINUX_EFI_RANDOM_SEED_TABLE));
+        if (!seed_table)
+                return log_debug_status(EFI_NOT_FOUND, "No random seed available, not creating a boot secret.");
+        if (seed_table->size < BOOT_SECRET_SIZE)
+                return log_debug_status(EFI_NOT_FOUND, "Random seed is available, but too short.");
+
+        *ret = seed_table;
+        return EFI_SUCCESS;
+}
+
+static void random_seed_evolve(struct linux_efi_random_seed *seed_table) {
+        static const char label[] = "systemd-stub random seed evolve label v1";
+
+        assert(seed_table);
+
+        /* Whenever we derived something from the Linux random seed EFI table we evolve the secret in it, so
+         * that the seed is never reused. */
+
+        struct sha256_ctx hash;
+        CLEANUP_ERASE(hash);
+        sha256_init_ctx(&hash);
+        sha256_process_bytes(label, sizeof(label) - 1, &hash);
+        sha256_process_bytes(&seed_table->size, sizeof(seed_table->size), &hash);
+        sha256_process_bytes(seed_table->seed, seed_table->size, &hash);
+        assert(seed_table->size >= SHA256_DIGEST_SIZE);
+        sha256_finish_ctx(&hash, seed_table->seed);
+}
+
+static void random_seed_make_secret(
+                struct linux_efi_random_seed *seed_table,
+                uint8_t ret_secret[static BOOT_SECRET_SIZE]) {
+
+        static const char label[] = "systemd-stub random seed make secret label v1";
+
+        assert(seed_table);
+        assert(ret_secret);
+
+        /* Derive a new secret from the Linux random seed EFI table data */
+
+        struct sha256_ctx hash;
+        CLEANUP_ERASE(hash);
+        sha256_init_ctx(&hash);
+        sha256_process_bytes(label, sizeof(label) - 1, &hash);
+        sha256_process_bytes(&seed_table->size, sizeof(seed_table->size), &hash);
+        sha256_process_bytes(seed_table->seed, seed_table->size, &hash);
+        sha256_finish_ctx(&hash, ret_secret);
+
+        random_seed_evolve(seed_table); /* ← ensure the same seed is not reused */
+}
+
+static EFI_STATUS read_efivar_secret(uint8_t ret_secret[static BOOT_SECRET_SIZE]) {
+        EFI_STATUS err;
+
+        assert(ret_secret);
+
+        /* Reads the boot secret from the EFI variable, ensuring it's properly protected from the OS, as per
+         * the attribute flags */
+
+        _cleanup_free_ void* data = NULL;
+        uint32_t attributes;
+        size_t size = 0;
+        err = efivar_get_raw_full(MAKE_GUID_PTR(LOADER), u"LoaderBootSecret", &attributes, &data, &size);
+        if (err != EFI_SUCCESS)
+                return log_debug_status(err, "Failed to read LoaderBootSecret EFI variable: %m");
+
+        if (size != BOOT_SECRET_SIZE) {
+                err = log_debug_status(EFI_PROTOCOL_ERROR, "Unexpected size of BootSecret EFI variable, ignoring.");
+                goto finish;
+        }
+
+        if ((attributes & (EFI_VARIABLE_NON_VOLATILE|EFI_VARIABLE_BOOTSERVICE_ACCESS|EFI_VARIABLE_RUNTIME_ACCESS)) !=
+            (EFI_VARIABLE_NON_VOLATILE|EFI_VARIABLE_BOOTSERVICE_ACCESS)) {
+                err = log_debug_status(EFI_PROTOCOL_ERROR, "Unexpected attributes of BootSecret EFI variable, ignoring.");
+                goto finish;
+        }
+
+        memcpy(ret_secret, data, size);
+        err = EFI_SUCCESS;
+finish:
+        explicit_bzero_safe(data, size);
+        return err;
+}
+
+static EFI_STATUS setup_efivar_secret(
+                struct linux_efi_random_seed *seed_table,
+                uint8_t ret_secret[static BOOT_SECRET_SIZE]) {
+
+        EFI_STATUS err;
+
+        assert(seed_table);
+        assert(ret_secret);
+
+        /* Generates a new EFI variable secret, and stores it in an EFI variable. */
+
+        uint8_t secret[BOOT_SECRET_SIZE];
+        CLEANUP_ERASE(secret);
+        random_seed_make_secret(seed_table, secret);
+
+        /* Set the variable with the EFI_VARIABLE_RUNTIME_ACCESS flag off (!), so that it's invisible after
+         * ExitBootServices()! */
+        err = RT->SetVariable(
+                        (char16_t*) u"LoaderBootSecret",
+                        MAKE_GUID_PTR(LOADER),
+                        EFI_VARIABLE_NON_VOLATILE|EFI_VARIABLE_BOOTSERVICE_ACCESS, /* ← No EFI_VARIABLE_RUNTIME_ACCESS here */
+                        sizeof(secret),
+                        secret);
+        if (err != EFI_SUCCESS)
+                return log_debug_status(err, "Failed to set boot secret EFI variable: %m");
+
+        memcpy(ret_secret, secret, sizeof(secret));
+        return EFI_SUCCESS;
+}
+
+static EFI_STATUS acquire_efivar_secret(
+                struct linux_efi_random_seed *seed_table,
+                uint8_t ret_secret[static BOOT_SECRET_SIZE]) {
+
+        EFI_STATUS err;
+
+        assert(seed_table);
+        assert(ret_secret);
+
+        /* Try to read the boot secret EFI variable, but if it doesn't exist create a new one */
+
+        err = read_efivar_secret(ret_secret);
+        if (err != EFI_NOT_FOUND)
+                return err;
+
+        return setup_efivar_secret(seed_table, ret_secret);
+}
+
+static EFI_STATUS setup_secret_mixin(
+                EFI_FILE *handle,
+                struct linux_efi_random_seed *seed_table,
+                uint8_t ret_mixin[static BOOT_SECRET_SIZE]) {
+
+        EFI_STATUS err;
+
+        assert(handle);
+        assert(seed_table);
+        assert(ret_mixin);
+
+        /* This writes a new 'mixin' to the ESP, in case the ESP so far had none */
+
+        uint8_t mixin[BOOT_SECRET_SIZE];
+        random_seed_make_secret(seed_table, mixin);
+
+        size_t wsize = sizeof(mixin);
+        err = handle->Write(handle, &wsize, mixin);
+        if (err != EFI_SUCCESS)
+                return log_debug_status(err, "Failed to write secret mixin file: %m");
+        if (wsize != sizeof(mixin))
+                return log_debug_status(EFI_LOAD_ERROR, "Short write while writing secret mixin file: %m");
+
+        err = handle->Flush(handle);
+        if (err != EFI_SUCCESS)
+                return log_debug_status(err, "Failed to flush secret mixin file: %m");
+
+        memcpy(ret_mixin, mixin, sizeof(mixin));
+        return EFI_SUCCESS;
+}
+
+static EFI_STATUS acquire_secret_mixin(
+                EFI_FILE *root_dir,
+                struct linux_efi_random_seed *seed_table,
+                uint8_t ret_mixin[static BOOT_SECRET_SIZE]) {
+
+        EFI_STATUS err;
+
+        assert(seed_table);
+        assert(ret_mixin);
+
+        if (!root_dir)
+                return EFI_NOT_FOUND;
+
+        /* Acquires the mixin for the boot secret stored in the ESP. If it already exists we'll read it. If
+         * it doesn't we'll initialize it */
+
+        bool writable;
+        _cleanup_file_close_ EFI_FILE *handle = NULL;
+        err = root_dir->Open(
+                        root_dir,
+                        &handle,
+                        (char16_t *) BOOT_SECRET_MIXIN_PATH,
+                        EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE,
+                        /* Attributes= */ 0);
+        if (err == EFI_WRITE_PROTECTED) {
+                err = root_dir->Open(
+                                root_dir,
+                                &handle,
+                                (char16_t *) BOOT_SECRET_MIXIN_PATH,
+                                EFI_FILE_MODE_READ,
+                                /* Attributes= */ 0);
+                if (err != EFI_SUCCESS)
+                        return log_debug_status(err, "Failed to read the boot secret mixin file '%ls': %m", BOOT_SECRET_MIXIN_PATH);
+
+                writable = false;
+        } else if (err != EFI_SUCCESS)
+                return log_debug_status(err, "Failed to access the boot secret mixin file '%ls': %m", BOOT_SECRET_MIXIN_PATH);
+        else
+                writable = true;
+
+        _cleanup_free_ EFI_FILE_INFO *info = NULL;
+        err = get_file_info(handle, &info, /* ret_size= */ NULL);
+        if (err != EFI_SUCCESS)
+                return log_debug_status(err, "Failed to get boot secret mixin file '%ls' info: %m", BOOT_SECRET_MIXIN_PATH);
+        if (info->FileSize == 0 && writable) /* New file? Fill it. */
+                return setup_secret_mixin(handle, seed_table, ret_mixin);
+
+        /* If the mixin file is too small we won't overwrite it (in order to not destroy some potentially
+         * load bearing key), but we won't use it either. */
+        if (info->FileSize < BOOT_SECRET_SIZE)
+                return log_debug_status(EFI_PROTOCOL_ERROR, "Boot secret mixin file '%ls' is too short %" PRIu64 " < %u", BOOT_SECRET_MIXIN_PATH, info->FileSize, BOOT_SECRET_SIZE);
+
+        uint8_t mixin[BOOT_SECRET_SIZE];
+        size_t rsize = sizeof(mixin);
+        err = handle->Read(handle, &rsize, mixin);
+        if (err != EFI_SUCCESS)
+                return log_debug_status(err, "Failed to read boot secret mixin file '%ls': %m", BOOT_SECRET_MIXIN_PATH);
+        if (rsize != BOOT_SECRET_SIZE)
+                return log_debug_status(EFI_PROTOCOL_ERROR, "Unexpected size from Read(): %zu != %zu", rsize, sizeof(mixin));
+
+        memcpy(ret_mixin, mixin, BOOT_SECRET_SIZE);
+        return EFI_SUCCESS;
+}
+
+static char* pick_id(const char *_osrel, size_t osrel_size) {
+        assert(_osrel || osrel_size == 0);
+
+        /* Make a NUL terminated copy we can chop into pieces */
+        _cleanup_free_ char *osrel = NULL;
+        osrel = xmalloc(osrel_size + 1);
+        if (osrel_size > 0)
+                memcpy(osrel, _osrel, osrel_size);
+        osrel[osrel_size] = 0;
+
+        /* Find an OS ID. Preferably the IMAGE_ID. */
+        _cleanup_free_ char *os_id = NULL;
+        char *line, *key, *value;
+        size_t pos = 0;
+        while ((line = line_get_key_value(osrel, "=", &pos, &key, &value))) {
+                if (streq8(key, "IMAGE_ID"))
+                        return xstrdup8(value);
+
+                if (streq8(key, "ID")) {
+                        free(os_id);
+                        os_id = xstrdup8(value);
+                }
+        }
+
+        /* If the IMAGE_ID= wasn't set, use the OS ID=. If that one isn't set either fall back to "linux". */
+        return TAKE_PTR(os_id) ?: xstrdup8("linux");
+}
+
+static void derive_secret(
+                uint8_t efivar_secret[static BOOT_SECRET_SIZE],
+                uint8_t secret_mixin[static BOOT_SECRET_SIZE],
+                const char *id,
+                uint8_t ret[static BOOT_SECRET_SIZE]) {
+
+        static const char hash_label[] = "systemd-stub derive secret label v1";
+
+        assert(efivar_secret);
+        assert(secret_mixin);
+        assert(id);
+        assert(ret);
+
+        /* Now combine the EFI variable secret, the mixin from the ESP and the OS id to generate the secret
+         * to pass to the OS */
+
+        struct sha256_ctx hash;
+        CLEANUP_ERASE(hash);
+        sha256_init_ctx(&hash);
+        sha256_process_bytes(hash_label, sizeof(hash_label) - 1, &hash);
+        sha256_process_bytes(efivar_secret, BOOT_SECRET_SIZE, &hash);
+        sha256_process_bytes(secret_mixin, BOOT_SECRET_SIZE, &hash);
+
+        /* Include an OS id in the hash, so that every OS gets a different derived secret */
+        size_t size = strlen8(id);
+        sha256_process_bytes(&size, sizeof(size), &hash);
+        sha256_process_bytes(id, size, &hash);
+
+        assert_cc(SHA256_DIGEST_SIZE == BOOT_SECRET_SIZE);
+        sha256_finish_ctx(&hash, ret);
+}
+
+EFI_STATUS prepare_boot_secret(
+                EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
+                const PeSectionVector *osrel_section,
+                uint8_t ret[static BOOT_SECRET_SIZE]) {
+
+        EFI_STATUS err;
+
+        assert(loaded_image);
+        assert(ret);
+
+        /* Prepares the boot secret to pass to the OS */
+
+        if (!loaded_image->DeviceHandle)
+                return EFI_SUCCESS;
+
+        _cleanup_file_close_ EFI_FILE *root = NULL;
+        err = open_volume(loaded_image->DeviceHandle, &root);
+        if (err != EFI_SUCCESS)
+                return err;
+
+        /* We need the Linux random seed EFI table, so that we can initialize the EFI variable secret and
+         * generate the secret mixin. */
+        struct linux_efi_random_seed *seed_table = NULL;
+        err = random_seed_find_table(&seed_table);
+        if (err != EFI_SUCCESS)
+                return err;
+
+        uint8_t efivar_secret[BOOT_SECRET_SIZE];
+        CLEANUP_ERASE(efivar_secret);
+        err = acquire_efivar_secret(seed_table, efivar_secret);
+        if (err != EFI_SUCCESS)
+                return err;
+
+        uint8_t secret_mixin[BOOT_SECRET_SIZE];
+        err = acquire_secret_mixin(root, seed_table, secret_mixin);
+        if (err != EFI_SUCCESS)
+                return err;
+
+        const char *osrel = NULL;
+        size_t osrel_size = 0;
+        if (PE_SECTION_VECTOR_IS_SET(osrel_section)) {
+                osrel = (const char*) loaded_image->ImageBase + osrel_section->memory_offset;
+                osrel_size = osrel_section->memory_size;
+        }
+        _cleanup_free_ char *id = pick_id(osrel, osrel_size);
+
+        derive_secret(efivar_secret, secret_mixin, id, ret);
+        return EFI_SUCCESS;
+}
diff --git a/src/boot/boot-secret.h b/src/boot/boot-secret.h
new file mode 100644 (file)
index 0000000..d3e9f53
--- /dev/null
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "efi.h"
+#include "pe.h"
+#include "proto/loaded-image.h"
+
+#define BOOT_SECRET_SIZE 32U
+
+EFI_STATUS prepare_boot_secret(
+                EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
+                const PeSectionVector *osrel_section,
+                uint8_t ret[static BOOT_SECRET_SIZE]);
index c51510e96f4a19d78a73a568afc776f4ba574245..058d9276bd1fea7456914b0954283f44ff94f3fe 100644 (file)
@@ -337,6 +337,7 @@ systemd_boot_sources = files(
 )
 
 stub_sources = files(
+        'boot-secret.c',
         'cpio.c',
         'linux.c',
         'splash.c',
index 7ef5a43a04ca55fe7d63fb294566073794c5681f..66b20805d5389b46114c525046377c1d3c5cdf61 100644 (file)
@@ -1,5 +1,6 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include "boot-secret.h"
 #include "cpio.h"
 #include "device-path-util.h"
 #include "devicetree.h"
@@ -45,6 +46,7 @@ enum {
         INITRD_PCRPKEY,
         INITRD_OSREL,
         INITRD_PROFILE,
+        INITRD_BOOT_SECRET,
         _INITRD_MAX,
 };
 
@@ -978,6 +980,29 @@ static void generate_embedded_initrds(
         }
 }
 
+static void generate_boot_secret_initrd(
+                const uint8_t boot_secret[static BOOT_SECRET_SIZE],
+                struct iovec initrds[static _INITRD_MAX]) {
+
+        assert(initrds);
+
+        /* All zero means: no boot secret acquired */
+        if (memeqzero(boot_secret, BOOT_SECRET_SIZE))
+                return;
+
+        (void) pack_cpio_literal(
+                        boot_secret,
+                        BOOT_SECRET_SIZE,
+                        ".extra",
+                        u"boot-secret",
+                        /* dir_mode= */ 0555,
+                        /* access_mode= */ 0400,
+                        /* tpm_pcr= */ UINT32_MAX,
+                        /* tpm_description= */ NULL,
+                        initrds + INITRD_BOOT_SECRET,
+                        /* ret_measured= */ NULL);
+}
+
 static void lookup_embedded_initrds(
                 EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
                 const PeSectionVector sections[static _UNIFIED_SECTION_MAX],
@@ -1256,6 +1281,10 @@ static EFI_STATUS run(EFI_HANDLE image) {
 
         refresh_random_seed(loaded_image);
 
+        uint8_t boot_secret[BOOT_SECRET_SIZE] = {}; /* all zeroes means: not acquired */
+        CLEANUP_ERASE(boot_secret);
+        (void) prepare_boot_secret(loaded_image, sections + UNIFIED_SECTION_OSREL, boot_secret);
+
         uname = pe_section_to_str8(loaded_image, sections + UNIFIED_SECTION_UNAME);
 
         /* Let's now check if we actually want to use the command line, measure it if it was passed in. */
@@ -1285,6 +1314,7 @@ static EFI_STATUS run(EFI_HANDLE image) {
         /* Generate & find all initrds */
         generate_sidecar_initrds(loaded_image, initrds, &parameters_measured, &sysext_measured, &confext_measured);
         generate_embedded_initrds(loaded_image, sections, initrds);
+        generate_boot_secret_initrd(boot_secret, initrds);
         lookup_embedded_initrds(loaded_image, sections, initrds);
 
         /* Add initrds in the right order. Generally, later initrds can overwrite files in earlier ones,
index 512f39a3e9f6174cea1bfd7d167ab92c468e63d0..916c9e503be3ba40e2f2dc2d3c932fe3ca9e54e2 100644 (file)
@@ -12,6 +12,7 @@
 
 C /run/systemd/stub/profile 0444 root root - /.extra/profile
 C /run/systemd/stub/os-release 0444 root root - /.extra/os-release
+C /run/systemd/stub/boot-secret 0400 root root - /.extra/boot-secret
 
 {% if ENABLE_TPM %}
 C /run/systemd/tpm2-pcr-signature.json 0444 root root - /.extra/tpm2-pcr-signature.json