stub: introduce "boot secret" stored in an EFI variable inaccessible to the OS

This commit is contained in:
Lennart Poettering
2026-03-07 23:44:37 +01:00
parent 6dc6b48ec9
commit dcad61c74d
6 changed files with 457 additions and 0 deletions

View File

@@ -655,6 +655,17 @@
<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
@@ -762,6 +773,33 @@
<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

374
src/boot/boot-secret.c Normal file
View File

@@ -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;
}

13
src/boot/boot-secret.h Normal file
View File

@@ -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]);

View File

@@ -337,6 +337,7 @@ systemd_boot_sources = files(
)
stub_sources = files(
'boot-secret.c',
'cpio.c',
'linux.c',
'splash.c',

View 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,

View 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