From e7b3a7bb24685fc209f552b9a7c23641f6f20d04 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 6 Mar 2026 21:17:53 +0100 Subject: [PATCH 1/5] memory-util: move memeqbyte() & friends to src/fundamental/ --- src/basic/memory-util.c | 21 -------------------- src/basic/memory-util.h | 6 ------ src/fundamental/memory-util-fundamental.c | 24 +++++++++++++++++++++++ src/fundamental/memory-util-fundamental.h | 7 +++++-- src/fundamental/meson.build | 1 + 5 files changed, 30 insertions(+), 29 deletions(-) create mode 100644 src/fundamental/memory-util-fundamental.c diff --git a/src/basic/memory-util.c b/src/basic/memory-util.c index b39ec725a9..e091cfb8f1 100644 --- a/src/basic/memory-util.c +++ b/src/basic/memory-util.c @@ -20,27 +20,6 @@ size_t page_size(void) { return pgsz; } -bool memeqbyte(uint8_t byte, const void *data, size_t length) { - /* Does the buffer consist entirely of the same specific byte value? - * Copied from https://github.com/systemd/casync/, copied in turn from - * https://github.com/rustyrussell/ccan/blob/master/ccan/mem/mem.c#L92, - * which is licensed CC-0. - */ - - const uint8_t *p = data; - - /* Check first 16 bytes manually */ - for (size_t i = 0; i < 16; i++, length--) { - if (length == 0) - return true; - if (p[i] != byte) - return false; - } - - /* Now we know first 16 bytes match, memcmp() with self. */ - return memcmp(data, p + 16, length) == 0; -} - void* memdup_reverse(const void *mem, size_t size) { assert(mem); assert(size != 0); diff --git a/src/basic/memory-util.h b/src/basic/memory-util.h index 1a609dea98..f16118fbb0 100644 --- a/src/basic/memory-util.h +++ b/src/basic/memory-util.h @@ -57,12 +57,6 @@ static inline int memcmp_nn(const void *s1, size_t n1, const void *s2, size_t n2 #define zero(x) (memzero(&(x), sizeof(x))) -bool memeqbyte(uint8_t byte, const void *data, size_t length) _nonnull_if_nonzero_(2, 3); - -#define memeqzero(data, length) memeqbyte(0x00, data, length) - -#define eqzero(x) memeqzero(x, sizeof(x)) - static inline void* mempset(void *s, int c, size_t n) { memset(s, c, n); return (uint8_t*) s + n; diff --git a/src/fundamental/memory-util-fundamental.c b/src/fundamental/memory-util-fundamental.c new file mode 100644 index 0000000000..02b55251fd --- /dev/null +++ b/src/fundamental/memory-util-fundamental.c @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "memory-util-fundamental.h" + +bool memeqbyte(uint8_t byte, const void *data, size_t length) { + /* Does the buffer consist entirely of the same specific byte value? + * Copied from https://github.com/systemd/casync/, copied in turn from + * https://github.com/rustyrussell/ccan/blob/master/ccan/mem/mem.c#L92, + * which is licensed CC-0. + */ + + const uint8_t *p = data; + + /* Check first 16 bytes manually */ + for (size_t i = 0; i < 16; i++, length--) { + if (length == 0) + return true; + if (p[i] != byte) + return false; + } + + /* Now we know first 16 bytes match, memcmp() with self. */ + return memcmp(data, p + 16, length) == 0; +} diff --git a/src/fundamental/memory-util-fundamental.h b/src/fundamental/memory-util-fundamental.h index c2a99a2039..7c88264053 100644 --- a/src/fundamental/memory-util-fundamental.h +++ b/src/fundamental/memory-util-fundamental.h @@ -9,8 +9,7 @@ # include #endif -#include "assert-fundamental.h" -#include "cleanup-fundamental.h" +#include "assert-fundamental.h" /* IWYU pragma: keep */ #include "macro-fundamental.h" #define memzero(x, l) \ @@ -148,3 +147,7 @@ static inline uint64_t ALIGN_OFFSET_U64(uint64_t l, uint64_t ali) { assert(((uintptr_t) _p) % alignof(t) == 0); \ (t *) _p; \ }) + +bool memeqbyte(uint8_t byte, const void *data, size_t length) _nonnull_if_nonzero_(2, 3); +#define memeqzero(data, length) memeqbyte(0x00, data, length) +#define eqzero(x) memeqzero(x, sizeof(x)) diff --git a/src/fundamental/meson.build b/src/fundamental/meson.build index 6bc26caad2..14d956ac07 100644 --- a/src/fundamental/meson.build +++ b/src/fundamental/meson.build @@ -8,6 +8,7 @@ fundamental_sources = files( 'edid-fundamental.c', 'efivars-fundamental.c', 'iovec-util-fundamental.h', + 'memory-util-fundamental.c', 'sha1-fundamental.c', 'sha256-fundamental.c', 'string-util-fundamental.c', From c678b0b06063c1aae3e3c5a089982f83f6e216a0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 6 Mar 2026 17:53:09 +0100 Subject: [PATCH 2/5] efi: add efivar_get_raw_full() flavour that returns the variable attributes too --- src/boot/efi-efivars.c | 13 +++++++++++-- src/boot/efi-efivars.h | 5 ++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/boot/efi-efivars.c b/src/boot/efi-efivars.c index 0358a071e0..5581d98ec3 100644 --- a/src/boot/efi-efivars.c +++ b/src/boot/efi-efivars.c @@ -177,7 +177,13 @@ EFI_STATUS efivar_get_uint64_le(const EFI_GUID *vendor, const char16_t *name, ui return EFI_SUCCESS; } -EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, void **ret_data, size_t *ret_size) { +EFI_STATUS efivar_get_raw_full( + const EFI_GUID *vendor, + const char16_t *name, + uint32_t *ret_attributes, + void **ret_data, + size_t *ret_size) { + EFI_STATUS err; assert(vendor); @@ -188,11 +194,14 @@ EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, void **r if (err != EFI_BUFFER_TOO_SMALL) return err; + uint32_t attributes = 0; _cleanup_free_ void *buf = xmalloc(size); - err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, NULL, &size, buf); + err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, ret_attributes ? &attributes : NULL, &size, buf); if (err != EFI_SUCCESS) return err; + if (ret_attributes) + *ret_attributes = attributes; if (ret_data) *ret_data = TAKE_PTR(buf); if (ret_size) diff --git a/src/boot/efi-efivars.h b/src/boot/efi-efivars.h index 1e74d6483c..6d88f56e71 100644 --- a/src/boot/efi-efivars.h +++ b/src/boot/efi-efivars.h @@ -22,7 +22,10 @@ void efivar_set_time_usec(const EFI_GUID *vendor, const char16_t *name, uint64_t EFI_STATUS efivar_unset(const EFI_GUID *vendor, const char16_t *name, uint32_t flags); EFI_STATUS efivar_get_str16(const EFI_GUID *vendor, const char16_t *name, char16_t **ret); -EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, void **ret_data, size_t *ret_size); +EFI_STATUS efivar_get_raw_full(const EFI_GUID *vendor, const char16_t *name, uint32_t *ret_attributes, void **ret_data, size_t *ret_size); +static inline EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, void **ret_data, size_t *ret_size) { + return efivar_get_raw_full(vendor, name, NULL, ret_data, ret_size); +} EFI_STATUS efivar_get_uint64_str16(const EFI_GUID *vendor, const char16_t *name, uint64_t *ret); EFI_STATUS efivar_get_uint32_le(const EFI_GUID *vendor, const char16_t *name, uint32_t *ret); EFI_STATUS efivar_get_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t *ret); From 9b15ddd9cf4ca973e3b5453e7583595550d3ec42 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 7 Mar 2026 22:11:31 +0100 Subject: [PATCH 3/5] random-seed: move seed efi table definitions to header --- src/boot/random-seed.c | 8 -------- src/boot/random-seed.h | 8 ++++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/boot/random-seed.c b/src/boot/random-seed.c index 30e74af214..8ef7ee7e52 100644 --- a/src/boot/random-seed.c +++ b/src/boot/random-seed.c @@ -12,14 +12,6 @@ #define RANDOM_MAX_SIZE_MIN (32U) #define RANDOM_MAX_SIZE_MAX (32U*1024U) -struct linux_efi_random_seed { - uint32_t size; - uint8_t seed[]; -}; - -#define LINUX_EFI_RANDOM_SEED_TABLE_GUID \ - { 0x1ce1e5bc, 0x7ceb, 0x42f2, { 0x81, 0xe5, 0x8a, 0xad, 0xf1, 0x80, 0xf5, 0x7b } } - /* SHA256 gives us 256/8=32 bytes */ #define HASH_VALUE_SIZE 32 diff --git a/src/boot/random-seed.h b/src/boot/random-seed.h index 67f005dff5..4a9f01bf45 100644 --- a/src/boot/random-seed.h +++ b/src/boot/random-seed.h @@ -3,4 +3,12 @@ #include "efi.h" +struct linux_efi_random_seed { + uint32_t size; + uint8_t seed[]; +}; + +#define LINUX_EFI_RANDOM_SEED_TABLE_GUID \ + { 0x1ce1e5bc, 0x7ceb, 0x42f2, { 0x81, 0xe5, 0x8a, 0xad, 0xf1, 0x80, 0xf5, 0x7b } } + EFI_STATUS process_random_seed(EFI_FILE *root_dir); From 6dc6b48ec98ad6eccd5af00b4c96bb70cff9286c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 7 Mar 2026 22:11:44 +0100 Subject: [PATCH 4/5] random-seed: when we have a reasonable RNG then create random seed file if missing Previously we'd never write the ESP random seed file (or initialize the random seed EFI table) if it didn't already exist. Let's adjust this a bit, and also create it fresh if we have a "good" random source, i.e. if the EFI table already existed or if the RNG protocol is implemented by EFI. This is useful as it increases the chance the random seed table is valid, and we can use it as source for randomness in later stages. --- src/boot/random-seed.c | 70 +++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/src/boot/random-seed.c b/src/boot/random-seed.c index 8ef7ee7e52..a31215c3b0 100644 --- a/src/boot/random-seed.c +++ b/src/boot/random-seed.c @@ -185,46 +185,66 @@ EFI_STATUS process_random_seed(EFI_FILE *root_dir) { explicit_bzero_safe(system_token, size); } + bool created = false; err = root_dir->Open( root_dir, &handle, (char16_t *) u"\\loader\\random-seed", EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE, 0); + if (err == EFI_NOT_FOUND && seeded_by_efi) { + /* If the file does not exist, but we are reasonably well seeded, create the seed file */ + created = true; + err = root_dir->Open( + root_dir, + &handle, + (char16_t *) u"\\loader\\random-seed", + EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE, + 0); + } if (err != EFI_SUCCESS) { if (!IN_SET(err, EFI_NOT_FOUND, EFI_WRITE_PROTECTED)) log_error_status(err, "Failed to open random seed file: %m"); return err; } - err = get_file_info(handle, &info, NULL); - if (err != EFI_SUCCESS) - return log_error_status(err, "Failed to get file info for random seed: %m"); + if (!created) { + err = get_file_info(handle, &info, /* ret_size= */ NULL); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to get file info for random seed: %m"); - size = info->FileSize; - if (size < RANDOM_MAX_SIZE_MIN) - return log_error_status(EFI_INVALID_PARAMETER, "Random seed file is too short."); - - if (size > RANDOM_MAX_SIZE_MAX) - return log_error_status(EFI_INVALID_PARAMETER, "Random seed file is too large."); - - seed = xmalloc(size); - rsize = size; - err = handle->Read(handle, &rsize, seed); - if (err != EFI_SUCCESS) - return log_error_status(err, "Failed to read random seed file: %m"); - if (rsize != size) { - explicit_bzero_safe(seed, rsize); - return log_error_status(EFI_PROTOCOL_ERROR, "Short read on random seed file."); + /* Treat a short file just like a freshly created one for robustness reasons: consider a case + * where in a previous run a file was just created and the system was then powered off. In + * such a case the file will already exist, but be too short. */ + created = info->FileSize < RANDOM_MAX_SIZE_MIN; } - sha256_process_bytes(&size, sizeof(size), &hash); - sha256_process_bytes(seed, size, &hash); - explicit_bzero_safe(seed, size); + if (created) { + size = 0; + sha256_process_bytes(&size, sizeof(size), &hash); + } else { + size = info->FileSize; + if (size > RANDOM_MAX_SIZE_MAX) + return log_error_status(EFI_INVALID_PARAMETER, "Random seed file is too large."); - err = handle->SetPosition(handle, 0); - if (err != EFI_SUCCESS) - return log_error_status(err, "Failed to seek to beginning of random seed file: %m"); + seed = xmalloc(size); + rsize = size; + err = handle->Read(handle, &rsize, seed); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to read random seed file: %m"); + if (rsize != size) { + explicit_bzero_safe(seed, rsize); + return log_error_status(EFI_PROTOCOL_ERROR, "Short read on random seed file."); + } + + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(seed, size, &hash); + explicit_bzero_safe(seed, size); + + err = handle->SetPosition(handle, 0); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to seek to beginning of random seed file: %m"); + } /* Let's also include the UEFI monotonic counter (which is supposedly increasing on every single * boot) in the hash, so that even if the changes to the ESP for some reason should not be @@ -253,7 +273,7 @@ EFI_STATUS process_random_seed(EFI_FILE *root_dir) { size = sizeof(random_bytes); /* If the file size is too large, zero out the remaining bytes on disk. */ - if (size < info->FileSize) { + if (!created && size < info->FileSize) { err = handle->SetPosition(handle, size); if (err != EFI_SUCCESS) return log_error_status(err, "Failed to seek to offset of random seed file: %m"); From dcad61c74d65a46913cac7ac4983a8eb35854dbb Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 7 Mar 2026 23:44:37 +0100 Subject: [PATCH 5/5] 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 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