diff --git a/man/systemd-stub.xml b/man/systemd-stub.xml
index 251d79ea6e..bf23c900d0 100644
--- a/man/systemd-stub.xml
+++ b/man/systemd-stub.xml
@@ -655,6 +655,17 @@
+
+
+ LoaderBootSecret
+
+ 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 systemd-stub if not present already. A secret derived from the value of this
+ EFI variable is passed to the OS in /.extra/boot-secret, see below.
+
+
+
Note that some of the variables above may also be set by the boot loader. The stub will only set
@@ -762,6 +773,33 @@
+
+
+ /.extra/boot-secret
+ A 32 byte per-system secret which is derived from a 32 byte secret stored in an EFI
+ variable (LoaderBootSecret, 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 IMAGE_ID=/ID=
+ data from the .osrel 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
+ /loader/boot-secret-mixin file is hashed in as well, ensuring that distinct disks will
+ result in different boot secrets.
+
+ 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.
+
+ 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.
+
+ 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.
+
+
+
Note that all these files are located in the tmpfs file system the kernel sets
diff --git a/src/boot/boot-secret.c b/src/boot/boot-secret.c
new file mode 100644
index 0000000000..54078b51c2
--- /dev/null
+++ b/src/boot/boot-secret.c
@@ -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
index 0000000000..d3e9f53d9c
--- /dev/null
+++ b/src/boot/boot-secret.h
@@ -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]);
diff --git a/src/boot/meson.build b/src/boot/meson.build
index c51510e96f..058d9276bd 100644
--- a/src/boot/meson.build
+++ b/src/boot/meson.build
@@ -337,6 +337,7 @@ systemd_boot_sources = files(
)
stub_sources = files(
+ 'boot-secret.c',
'cpio.c',
'linux.c',
'splash.c',
diff --git a/src/boot/stub.c b/src/boot/stub.c
index 7ef5a43a04..66b20805d5 100644
--- a/src/boot/stub.c
+++ b/src/boot/stub.c
@@ -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, ¶meters_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,
diff --git a/tmpfiles.d/20-systemd-stub.conf.in b/tmpfiles.d/20-systemd-stub.conf.in
index 512f39a3e9..916c9e503b 100644
--- a/tmpfiles.d/20-systemd-stub.conf.in
+++ b/tmpfiles.d/20-systemd-stub.conf.in
@@ -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