From dcad61c74d65a46913cac7ac4983a8eb35854dbb Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 7 Mar 2026 23:44:37 +0100 Subject: [PATCH] stub: introduce "boot secret" stored in an EFI variable inaccessible to the OS --- man/systemd-stub.xml | 38 +++ src/boot/boot-secret.c | 374 +++++++++++++++++++++++++++++ src/boot/boot-secret.h | 13 + src/boot/meson.build | 1 + src/boot/stub.c | 30 +++ tmpfiles.d/20-systemd-stub.conf.in | 1 + 6 files changed, 457 insertions(+) create mode 100644 src/boot/boot-secret.c create mode 100644 src/boot/boot-secret.h diff --git a/man/systemd-stub.xml b/man/systemd-stub.xml index 251d79ea6e1..bf23c900d02 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 00000000000..54078b51c23 --- /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 00000000000..d3e9f53d9ce --- /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 c51510e96f4..058d9276bd1 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 7ef5a43a04c..66b20805d53 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 512f39a3e9f..916c9e503be 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 -- 2.47.3