diff options
-rw-r--r-- | .github/codeql-queries/UninitializedVariableWithCleanup.ql | 2 | ||||
-rw-r--r-- | docs/BOOT_LOADER_INTERFACE.md | 9 | ||||
-rw-r--r-- | docs/RANDOM_SEEDS.md | 51 | ||||
-rw-r--r-- | man/systemd-boot.xml | 22 | ||||
-rw-r--r-- | src/basic/random-util.h | 1 | ||||
-rw-r--r-- | src/boot/bootctl.c | 30 | ||||
-rw-r--r-- | src/boot/efi/efi-string.h | 7 | ||||
-rw-r--r-- | src/boot/efi/random-seed.c | 299 | ||||
-rw-r--r-- | src/boot/efi/util.h | 25 | ||||
-rw-r--r-- | src/core/efi-random.c | 82 | ||||
-rw-r--r-- | src/core/efi-random.h | 2 | ||||
-rw-r--r-- | src/core/main.c | 4 | ||||
-rw-r--r-- | src/random-seed/random-seed.c | 122 | ||||
-rw-r--r-- | units/systemd-boot-system-token.service | 3 |
14 files changed, 355 insertions, 304 deletions
diff --git a/.github/codeql-queries/UninitializedVariableWithCleanup.ql b/.github/codeql-queries/UninitializedVariableWithCleanup.ql index e514111f28..dadc6cb1b5 100644 --- a/.github/codeql-queries/UninitializedVariableWithCleanup.ql +++ b/.github/codeql-queries/UninitializedVariableWithCleanup.ql @@ -20,7 +20,7 @@ import semmle.code.cpp.controlflow.StackVariableReachability * since they don't do anything illegal even when the variable is uninitialized */ predicate cleanupFunctionDenyList(string fun) { - fun = "erase_char" + fun = "erase_char" or fun = "erase_obj" } /** diff --git a/docs/BOOT_LOADER_INTERFACE.md b/docs/BOOT_LOADER_INTERFACE.md index fc9336085b..5be4d1ad17 100644 --- a/docs/BOOT_LOADER_INTERFACE.md +++ b/docs/BOOT_LOADER_INTERFACE.md @@ -80,12 +80,6 @@ variables. All EFI variables use the vendor UUID * `1 << 5` → The boot loader supports looking for boot menu entries in the Extended Boot Loader Partition. * `1 << 6` → The boot loader supports passing a random seed to the OS. -* The EFI variable `LoaderRandomSeed` contains a binary random seed if set. It - is set by the boot loader to pass an entropy seed read from the ESP to the OS. - The system manager then credits this seed to the kernel's entropy pool. It is - the responsibility of the boot loader to ensure the quality and integrity of - the random seed. - * The EFI variable `LoaderSystemToken` contains binary random data, persistently set by the OS installer. Boot loaders that support passing random seeds to the OS should use this data and combine it with the random @@ -107,8 +101,7 @@ that directory is empty, and only if no other file systems are mounted there. The `systemctl reboot --boot-loader-entry=…` and `systemctl reboot --boot-loader-menu=…` commands rely on the `LoaderFeatures` , `LoaderConfigTimeoutOneShot`, `LoaderEntries`, `LoaderEntryOneShot` -variables. `LoaderRandomSeed` is read by PID during early boot and credited to -the kernel's random pool. +variables. ## Boot Loader Entry Identifiers diff --git a/docs/RANDOM_SEEDS.md b/docs/RANDOM_SEEDS.md index 3dc27f5552..b7240f0d89 100644 --- a/docs/RANDOM_SEEDS.md +++ b/docs/RANDOM_SEEDS.md @@ -197,28 +197,39 @@ boot, in order to ensure the entropy pool is filled up quickly. generate sufficient data), to generate a new random seed file to store in the ESP as well as a random seed to pass to the OS kernel. The new random seed file for the ESP is then written to the ESP, ensuring this is completed - before the OS is invoked. Very early during initialization PID 1 will read - the random seed provided in the EFI variable and credit it fully to the - kernel's entropy pool. - - This mechanism is able to safely provide an initialized entropy pool already - in the `initrd` and guarantees that different seeds are passed from the boot - loader to the OS on every boot (in a way that does not allow regeneration of - an old seed file from a new seed file). Moreover, when an OS image is - replicated between multiple images and the random seed is not reset, this - will still result in different random seeds being passed to the OS, as the - per-machine 'system token' is specific to the physical host, and not - included in OS disk images. If the 'system token' is properly initialized - and kept sufficiently secret it should not be possible to regenerate the - entropy pool of different machines, even if this seed is the only source of - entropy. + before the OS is invoked. + + The kernel then reads the random seed that the boot loader passes to it, via + the EFI configuration table entry, `LINUX_EFI_RANDOM_SEED_TABLE_GUID` + (1ce1e5bc-7ceb-42f2-81e5-8aadf180f57b), which is allocated with pool memory + of type `EfiACPIReclaimMemory`. Its contents have the form: + ``` + struct linux_efi_random_seed { + u32 size; // of the 'seed' array in bytes + u8 seed[]; + }; + ``` + The size field is generally set to 32 bytes, and the seed field includes a + hashed representation of any prior seed in `LINUX_EFI_RANDOM_SEED_TABLE_GUID` + together with the new seed. + + This mechanism is able to safely provide an initialized entropy pool before + userspace even starts and guarantees that different seeds are passed from + the boot loader to the OS on every boot (in a way that does not allow + regeneration of an old seed file from a new seed file). Moreover, when an OS + image is replicated between multiple images and the random seed is not + reset, this will still result in different random seeds being passed to the + OS, as the per-machine 'system token' is specific to the physical host, and + not included in OS disk images. If the 'system token' is properly + initialized and kept sufficiently secret it should not be possible to + regenerate the entropy pool of different machines, even if this seed is the + only source of entropy. Note that the writes to the ESP needed to maintain the random seed should be - minimal. The size of the random seed file is directly derived from the Linux - kernel's entropy pool size, which defaults to 512 bytes. This means updating - the random seed in the ESP should be doable safely with a single sector - write (since hard-disk sectors typically happen to be 512 bytes long, too), - which should be safe even with FAT file system drivers built into + minimal. Because the size of the random seed file is generally set to 32 bytes, + updating the random seed in the ESP should be doable safely with a single + sector write (since hard-disk sectors typically happen to be 512 bytes long, + too), which should be safe even with FAT file system drivers built into low-quality EFI firmwares. As a special restriction: in virtualized environments PID 1 will refrain diff --git a/man/systemd-boot.xml b/man/systemd-boot.xml index 57b66803fa..f96c4c6512 100644 --- a/man/systemd-boot.xml +++ b/man/systemd-boot.xml @@ -436,28 +436,6 @@ </varlistentry> <varlistentry> - <term><varname>LoaderRandomSeed</varname></term> - - <listitem><para>A binary random seed <command>systemd-boot</command> may optionally pass to the - OS. This is a volatile EFI variable that is hashed at boot from the combination of a random seed - stored in the ESP (in <filename>/loader/random-seed</filename>) and a "system token" persistently - stored in the EFI variable <varname>LoaderSystemToken</varname> (see below). During early OS boot the - system manager reads this variable and passes it to the OS kernel's random pool, crediting the full - entropy it contains. This is an efficient way to ensure the system starts up with a fully initialized - kernel random pool — as early as the initrd phase. <command>systemd-boot</command> reads - the random seed from the ESP, combines it with the "system token", and both derives a new random seed - to update in-place the seed stored in the ESP, and the random seed to pass to the OS from it via - SHA256 hashing in counter mode. This ensures that different physical systems that boot the same - "golden" OS image — i.e. containing the same random seed file in the ESP — will still pass a - different random seed to the OS. It is made sure the random seed stored in the ESP is fully - overwritten before the OS is booted, to ensure different random seed data is used between subsequent - boots.</para> - - <para>See <ulink url="https://systemd.io/RANDOM_SEEDS">Random Seeds</ulink> for - further information.</para></listitem> - </varlistentry> - - <varlistentry> <term><varname>LoaderSystemToken</varname></term> <listitem><para>A binary random data field, that is used for generating the random seed to pass to diff --git a/src/basic/random-util.h b/src/basic/random-util.h index 2d99807272..b1a4d10971 100644 --- a/src/basic/random-util.h +++ b/src/basic/random-util.h @@ -23,6 +23,7 @@ static inline uint32_t random_u32(void) { /* Some limits on the pool sizes when we deal with the kernel random pool */ #define RANDOM_POOL_SIZE_MIN 32U #define RANDOM_POOL_SIZE_MAX (10U*1024U*1024U) +#define RANDOM_EFI_SEED_SIZE 32U size_t random_pool_size(void); diff --git a/src/boot/bootctl.c b/src/boot/bootctl.c index 430887fe67..e04424b379 100644 --- a/src/boot/bootctl.c +++ b/src/boot/bootctl.c @@ -1886,8 +1886,6 @@ static int verb_status(int argc, char *argv[], void *userdata) { printf("\n"); printf("%sRandom Seed:%s\n", ansi_underline(), ansi_normal()); - have = access(EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderRandomSeed)), F_OK) >= 0; - printf(" Passed to OS: %s\n", yes_no(have)); have = access(EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderSystemToken)), F_OK) >= 0; printf(" System Token: %s\n", have ? "set" : "not set"); @@ -1977,10 +1975,10 @@ static int verb_list(int argc, char *argv[], void *userdata) { static int install_random_seed(const char *esp) { _cleanup_(unlink_and_freep) char *tmp = NULL; - _cleanup_free_ void *buffer = NULL; + unsigned char buffer[RANDOM_EFI_SEED_SIZE]; _cleanup_free_ char *path = NULL; _cleanup_close_ int fd = -1; - size_t sz, token_size; + size_t token_size; ssize_t n; int r; @@ -1990,13 +1988,7 @@ static int install_random_seed(const char *esp) { if (!path) return log_oom(); - sz = random_pool_size(); - - buffer = malloc(sz); - if (!buffer) - return log_oom(); - - r = crypto_random_bytes(buffer, sz); + r = crypto_random_bytes(buffer, sizeof(buffer)); if (r < 0) return log_error_errno(r, "Failed to acquire random seed: %m"); @@ -2017,10 +2009,10 @@ static int install_random_seed(const char *esp) { return log_error_errno(fd, "Failed to open random seed file for writing: %m"); } - n = write(fd, buffer, sz); + n = write(fd, buffer, sizeof(buffer)); if (n < 0) return log_error_errno(errno, "Failed to write random seed file: %m"); - if ((size_t) n != sz) + if ((size_t) n != sizeof(buffer)) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while writing random seed file."); if (rename(tmp, path) < 0) @@ -2028,7 +2020,7 @@ static int install_random_seed(const char *esp) { tmp = mfree(tmp); - log_info("Random seed file %s successfully written (%zu bytes).", path, sz); + log_info("Random seed file %s successfully written (%zu bytes).", path, sizeof(buffer)); if (!arg_touch_variables) return 0; @@ -2080,16 +2072,16 @@ static int install_random_seed(const char *esp) { if (r != -ENOENT) return log_error_errno(r, "Failed to test system token validity: %m"); } else { - if (token_size >= sz) { + if (token_size >= sizeof(buffer)) { /* Let's avoid writes if we can, and initialize this only once. */ log_debug("System token already written, not updating."); return 0; } - log_debug("Existing system token size (%zu) does not match our expectations (%zu), replacing.", token_size, sz); + log_debug("Existing system token size (%zu) does not match our expectations (%zu), replacing.", token_size, sizeof(buffer)); } - r = crypto_random_bytes(buffer, sz); + r = crypto_random_bytes(buffer, sizeof(buffer)); if (r < 0) return log_error_errno(r, "Failed to acquire random seed: %m"); @@ -2097,7 +2089,7 @@ static int install_random_seed(const char *esp) { * and possibly get identification information or too much insight into the kernel's entropy pool * state. */ RUN_WITH_UMASK(0077) { - r = efi_set_variable(EFI_LOADER_VARIABLE(LoaderSystemToken), buffer, sz); + r = efi_set_variable(EFI_LOADER_VARIABLE(LoaderSystemToken), buffer, sizeof(buffer)); if (r < 0) { if (!arg_graceful) return log_error_errno(r, "Failed to write 'LoaderSystemToken' EFI variable: %m"); @@ -2107,7 +2099,7 @@ static int install_random_seed(const char *esp) { else log_warning_errno(r, "Unable to write 'LoaderSystemToken' EFI variable, ignoring: %m"); } else - log_info("Successfully initialized system token in EFI variable with %zu bytes.", sz); + log_info("Successfully initialized system token in EFI variable with %zu bytes.", sizeof(buffer)); } return 0; diff --git a/src/boot/efi/efi-string.h b/src/boot/efi/efi-string.h index 1ebd5fd6b7..d4d76a7c18 100644 --- a/src/boot/efi/efi-string.h +++ b/src/boot/efi/efi-string.h @@ -119,6 +119,13 @@ static inline void *mempcpy(void * restrict dest, const void * restrict src, siz memcpy(dest, src, n); return (uint8_t *) dest + n; } + +static inline void explicit_bzero_safe(void *bytes, size_t len) { + if (!bytes || len == 0) + return; + memset(bytes, 0, len); + __asm__ __volatile__("": :"r"(bytes) :"memory"); +} #else /* For unit testing. */ int efi_memcmp(const void *p1, const void *p2, size_t n); diff --git a/src/boot/efi/random-seed.c b/src/boot/efi/random-seed.c index aea4f7e532..04bfd526f8 100644 --- a/src/boot/efi/random-seed.c +++ b/src/boot/efi/random-seed.c @@ -14,11 +14,24 @@ #define EFI_RNG_GUID &(const EFI_GUID) EFI_RNG_PROTOCOL_GUID +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 -static EFI_STATUS acquire_rng(UINTN size, void **ret) { - _cleanup_free_ void *data = NULL; +/* Linux's RNG is 256 bits, so let's provide this much */ +#define DESIRED_SEED_SIZE 32 + +/* Some basic domain separation in case somebody uses this data elsewhere */ +#define HASH_LABEL "systemd-boot random seed label v1" + +static EFI_STATUS acquire_rng(void *ret, UINTN size) { EFI_RNG_PROTOCOL *rng; EFI_STATUS err; @@ -32,126 +45,9 @@ static EFI_STATUS acquire_rng(UINTN size, void **ret) { if (!rng) return EFI_UNSUPPORTED; - data = xmalloc(size); - - err = rng->GetRNG(rng, NULL, size, data); + err = rng->GetRNG(rng, NULL, size, ret); if (err != EFI_SUCCESS) return log_error_status_stall(err, L"Failed to acquire RNG data: %r", err); - - *ret = TAKE_PTR(data); - return EFI_SUCCESS; -} - -static void hash_once( - const void *old_seed, - const void *rng, - UINTN size, - const void *system_token, - UINTN system_token_size, - uint64_t uefi_monotonic_counter, - UINTN counter, - uint8_t ret[static HASH_VALUE_SIZE]) { - - /* This hashes together: - * - * 1. The contents of the old seed file - * 2. Some random data acquired from the UEFI RNG (optional) - * 3. Some 'system token' the installer installed as EFI variable (optional) - * 4. The UEFI "monotonic counter" that increases with each boot - * 5. A supplied counter value - * - * And writes the result to the specified buffer. - */ - - struct sha256_ctx hash; - - assert(old_seed); - assert(system_token_size == 0 || system_token); - - sha256_init_ctx(&hash); - sha256_process_bytes(old_seed, size, &hash); - if (rng) - sha256_process_bytes(rng, size, &hash); - if (system_token_size > 0) - sha256_process_bytes(system_token, system_token_size, &hash); - sha256_process_bytes(&uefi_monotonic_counter, sizeof(uefi_monotonic_counter), &hash); - sha256_process_bytes(&counter, sizeof(counter), &hash); - sha256_finish_ctx(&hash, ret); -} - -static EFI_STATUS hash_many( - const void *old_seed, - const void *rng, - UINTN size, - const void *system_token, - UINTN system_token_size, - uint64_t uefi_monotonic_counter, - UINTN counter_start, - UINTN n, - void **ret) { - - _cleanup_free_ void *output = NULL; - - assert(old_seed); - assert(system_token_size == 0 || system_token); - assert(ret); - - /* Hashes the specified parameters in counter mode, generating n hash values, with the counter in the - * range counter_start…counter_start+n-1. */ - - output = xmalloc_multiply(HASH_VALUE_SIZE, n); - - for (UINTN i = 0; i < n; i++) - hash_once(old_seed, rng, size, - system_token, system_token_size, - uefi_monotonic_counter, - counter_start + i, - (uint8_t*) output + (i * HASH_VALUE_SIZE)); - - *ret = TAKE_PTR(output); - return EFI_SUCCESS; -} - -static EFI_STATUS mangle_random_seed( - const void *old_seed, - const void *rng, - UINTN size, - const void *system_token, - UINTN system_token_size, - uint64_t uefi_monotonic_counter, - void **ret_new_seed, - void **ret_for_kernel) { - - _cleanup_free_ void *new_seed = NULL, *for_kernel = NULL; - EFI_STATUS err; - UINTN n; - - assert(old_seed); - assert(system_token_size == 0 || system_token); - assert(ret_new_seed); - assert(ret_for_kernel); - - /* This takes the old seed file contents, an (optional) random number acquired from the UEFI RNG, an - * (optional) system 'token' installed once by the OS installer in an EFI variable, and hashes them - * together in counter mode, generating a new seed (to replace the file on disk) and the seed for the - * kernel. To keep things simple, the new seed and kernel data have the same size as the old seed and - * RNG data. */ - - n = (size + HASH_VALUE_SIZE - 1) / HASH_VALUE_SIZE; - - /* Begin hashing in counter mode at counter 0 for the new seed for the disk */ - err = hash_many(old_seed, rng, size, system_token, system_token_size, uefi_monotonic_counter, 0, n, &new_seed); - if (err != EFI_SUCCESS) - return err; - - /* Continue counting at 'n' for the seed for the kernel */ - err = hash_many(old_seed, rng, size, system_token, system_token_size, uefi_monotonic_counter, n, n, &for_kernel); - if (err != EFI_SUCCESS) - return err; - - *ret_new_seed = TAKE_PTR(new_seed); - *ret_for_kernel = TAKE_PTR(for_kernel); - return EFI_SUCCESS; } @@ -163,6 +59,7 @@ static EFI_STATUS acquire_system_token(void **ret, UINTN *ret_size) { assert(ret); assert(ret_size); + *ret_size = 0; err = efivar_get_raw(LOADER_GUID, L"LoaderSystemToken", &data, &size); if (err != EFI_SUCCESS) { if (err != EFI_NOT_FOUND) @@ -221,31 +118,83 @@ static void validate_sha256(void) { } EFI_STATUS process_random_seed(EFI_FILE *root_dir, RandomSeedMode mode) { - _cleanup_free_ void *seed = NULL, *new_seed = NULL, *rng = NULL, *for_kernel = NULL, *system_token = NULL; + _cleanup_erase_ uint8_t random_bytes[DESIRED_SEED_SIZE], hash_key[HASH_VALUE_SIZE]; + _cleanup_free_ struct linux_efi_random_seed *new_seed_table = NULL; + struct linux_efi_random_seed *previous_seed_table = NULL; + _cleanup_free_ void *seed = NULL, *system_token = NULL; _cleanup_(file_closep) EFI_FILE *handle = NULL; - UINTN size, rsize, wsize, system_token_size = 0; _cleanup_free_ EFI_FILE_INFO *info = NULL; + _cleanup_erase_ struct sha256_ctx hash; uint64_t uefi_monotonic_counter = 0; + size_t size, rsize, wsize; + bool seeded_by_efi = false; EFI_STATUS err; + EFI_TIME now; assert(root_dir); + assert_cc(DESIRED_SEED_SIZE == HASH_VALUE_SIZE); validate_sha256(); if (mode == RANDOM_SEED_OFF) return EFI_NOT_FOUND; - /* Let's better be safe than sorry, and for now disable this logic in SecureBoot mode, so that we - * don't credit a random seed that is not authenticated. */ - if (secure_boot_enabled()) - return EFI_NOT_FOUND; + /* hash = LABEL || sizeof(input1) || input1 || ... || sizeof(inputN) || inputN */ + sha256_init_ctx(&hash); + + /* Some basic domain separation in case somebody uses this data elsewhere */ + sha256_process_bytes(HASH_LABEL, sizeof(HASH_LABEL) - 1, &hash); + + for (size_t i = 0; i < ST->NumberOfTableEntries; ++i) + if (memcmp(&(const EFI_GUID)LINUX_EFI_RANDOM_SEED_TABLE_GUID, + &ST->ConfigurationTable[i].VendorGuid, sizeof(EFI_GUID)) == 0) { + previous_seed_table = ST->ConfigurationTable[i].VendorTable; + break; + } + if (!previous_seed_table) { + size = 0; + sha256_process_bytes(&size, sizeof(size), &hash); + } else { + size = previous_seed_table->size; + seeded_by_efi = size >= DESIRED_SEED_SIZE; + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(previous_seed_table->seed, size, &hash); + + /* Zero and free the previous seed table only at the end after we've managed to install a new + * one, so that in case this function fails or aborts, Linux still receives whatever the + * previous bootloader chain set. So, the next line of this block is not an explicit_bzero() + * call. */ + } + + /* Request some random data from the UEFI RNG. We don't need this to work safely, but it's a good + * idea to use it because it helps us for cases where users mistakenly include a random seed in + * golden master images that are replicated many times. */ + err = acquire_rng(random_bytes, sizeof(random_bytes)); + if (err != EFI_SUCCESS) { + size = 0; + /* If we can't get any randomness from EFI itself, then we'll only be relying on what's in + * ESP. But ESP is mutable, so if secure boot is enabled, we probably shouldn't trust that + * alone, in which case we bail out early. */ + if (!seeded_by_efi && secure_boot_enabled()) + return EFI_NOT_FOUND; + } else { + seeded_by_efi = true; + size = sizeof(random_bytes); + } + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(random_bytes, size, &hash); /* Get some system specific seed that the installer might have placed in an EFI variable. We include * it in our hash. This is protection against golden master image sloppiness, and it remains on the * system, even when disk images are duplicated or swapped out. */ - err = acquire_system_token(&system_token, &system_token_size); - if (mode != RANDOM_SEED_ALWAYS && err != EFI_SUCCESS) + err = acquire_system_token(&system_token, &size); + if (mode != RANDOM_SEED_ALWAYS && (err != EFI_SUCCESS || size < DESIRED_SEED_SIZE) && !seeded_by_efi) return err; + sha256_process_bytes(&size, sizeof(size), &hash); + if (system_token) { + sha256_process_bytes(system_token, size, &hash); + explicit_bzero_safe(system_token, size); + } err = root_dir->Open( root_dir, @@ -261,7 +210,7 @@ EFI_STATUS process_random_seed(EFI_FILE *root_dir, RandomSeedMode mode) { err = get_file_info_harder(handle, &info, NULL); if (err != EFI_SUCCESS) - return log_error_status_stall(err, L"Failed to get file info for random seed: %r"); + return log_error_status_stall(err, L"Failed to get file info for random seed: %r", err); size = info->FileSize; if (size < RANDOM_MAX_SIZE_MIN) @@ -271,51 +220,105 @@ EFI_STATUS process_random_seed(EFI_FILE *root_dir, RandomSeedMode mode) { return log_error_status_stall(EFI_INVALID_PARAMETER, L"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_stall(err, L"Failed to read random seed file: %r", err); - if (rsize != size) + if (rsize != size) { + explicit_bzero_safe(seed, rsize); return log_error_status_stall(EFI_PROTOCOL_ERROR, L"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_stall(err, L"Failed to seek to beginning of random seed file: %r", err); - /* Request some random data from the UEFI RNG. We don't need this to work safely, but it's a good - * idea to use it because it helps us for cases where users mistakenly include a random seed in - * golden master images that are replicated many times. */ - (void) acquire_rng(size, &rng); /* It's fine if this fails */ - /* 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 * persistent, the random seed we generate will still be different on every single boot. */ err = BS->GetNextMonotonicCount(&uefi_monotonic_counter); - if (err != EFI_SUCCESS) + if (err != EFI_SUCCESS && !seeded_by_efi) return log_error_status_stall(err, L"Failed to acquire UEFI monotonic counter: %r", err); - - /* Calculate new random seed for the disk and what to pass to the kernel */ - err = mangle_random_seed(seed, rng, size, system_token, system_token_size, uefi_monotonic_counter, &new_seed, &for_kernel); - if (err != EFI_SUCCESS) - return err; - + size = sizeof(uefi_monotonic_counter); + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(&uefi_monotonic_counter, size, &hash); + err = RT->GetTime(&now, NULL); + size = err == EFI_SUCCESS ? sizeof(now) : 0; /* Known to be flaky, so don't bark on error. */ + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(&now, size, &hash); + + /* hash_key = HASH(hash) */ + sha256_finish_ctx(&hash, hash_key); + + /* hash = hash_key || 0 */ + sha256_init_ctx(&hash); + sha256_process_bytes(hash_key, sizeof(hash_key), &hash); + sha256_process_bytes(&(const uint8_t){ 0 }, sizeof(uint8_t), &hash); + /* random_bytes = HASH(hash) */ + sha256_finish_ctx(&hash, random_bytes); + + size = sizeof(random_bytes); + /* If the file size is too large, zero out the remaining bytes on disk, and then truncate. */ + if (size < info->FileSize) { + err = handle->SetPosition(handle, size); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to seek to offset of random seed file: %r", err); + wsize = info->FileSize - size; + err = handle->Write(handle, &wsize, seed /* All zeros now */); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to write random seed file: %r", err); + if (wsize != info->FileSize - size) + return log_error_status_stall(EFI_PROTOCOL_ERROR, L"Short write on random seed file."); + err = handle->Flush(handle); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to flush random seed file: %r", err); + err = handle->SetPosition(handle, 0); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to seek to beginning of random seed file: %r", err); + info->FileSize = size; + err = handle->SetInfo(handle, &GenericFileInfo, info->Size, info); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to truncate random seed file: %r", err); + } /* Update the random seed on disk before we use it */ wsize = size; - err = handle->Write(handle, &wsize, new_seed); + err = handle->Write(handle, &wsize, random_bytes); if (err != EFI_SUCCESS) return log_error_status_stall(err, L"Failed to write random seed file: %r", err); if (wsize != size) return log_error_status_stall(EFI_PROTOCOL_ERROR, L"Short write on random seed file."); - err = handle->Flush(handle); if (err != EFI_SUCCESS) return log_error_status_stall(err, L"Failed to flush random seed file: %r", err); - /* We are good to go */ - err = efivar_set_raw(LOADER_GUID, L"LoaderRandomSeed", for_kernel, size, 0); + err = BS->AllocatePool(EfiACPIReclaimMemory, sizeof(*new_seed_table) + DESIRED_SEED_SIZE, + (void **) &new_seed_table); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to allocate EFI table for random seed: %r", err); + new_seed_table->size = DESIRED_SEED_SIZE; + + /* hash = hash_key || 1 */ + sha256_init_ctx(&hash); + sha256_process_bytes(hash_key, sizeof(hash_key), &hash); + sha256_process_bytes(&(const uint8_t){ 1 }, sizeof(uint8_t), &hash); + /* new_seed_table->seed = HASH(hash) */ + sha256_finish_ctx(&hash, new_seed_table->seed); + + err = BS->InstallConfigurationTable(&(EFI_GUID)LINUX_EFI_RANDOM_SEED_TABLE_GUID, new_seed_table); if (err != EFI_SUCCESS) - return log_error_status_stall(err, L"Failed to write random seed to EFI variable: %r", err); + return log_error_status_stall(err, L"Failed to install EFI table for random seed: %r", err); + TAKE_PTR(new_seed_table); + + if (previous_seed_table) { + /* Now that we've succeeded in installing the new table, we can safely nuke the old one. */ + explicit_bzero_safe(previous_seed_table->seed, previous_seed_table->size); + explicit_bzero_safe(previous_seed_table, sizeof(*previous_seed_table)); + free(previous_seed_table); + } return EFI_SUCCESS; } diff --git a/src/boot/efi/util.h b/src/boot/efi/util.h index b33c50f9fc..15dd87f774 100644 --- a/src/boot/efi/util.h +++ b/src/boot/efi/util.h @@ -10,6 +10,21 @@ #define UINTN_MAX (~(UINTN)0) #define INTN_MAX ((INTN)(UINTN_MAX>>1)) +#ifdef __OPTIMIZE__ +#ifndef __has_attribute +#define __has_attribute(x) 0 +#endif +#if __has_attribute(__error__) +__attribute__((noreturn)) extern void __assert_cl_failure__(void) __attribute__((__error__("compile-time assertion failed"))); +#else +__attribute__((noreturn)) extern void __assert_cl_failure__(void); +#endif +/* assert_cl generates a later-stage compile-time assertion when constant folding occurs. */ +#define assert_cl(condition) if (!(condition)) __assert_cl_failure__() +#else +#define assert_cl(condition) assert(condition) +#endif + /* gnu-efi format specifiers for integers are fixed to either 64bit with 'l' and 32bit without a size prefix. * We rely on %u/%d/%x to format regular ints, so ensure the size is what we expect. At the same time, we also * need specifiers for (U)INTN which are native (pointer) sized. */ @@ -43,6 +58,16 @@ static inline void freep(void *p) { #define _cleanup_free_ _cleanup_(freep) +static __always_inline void erase_obj(void *p) { + size_t l; + assert_cl(p != NULL); + l = __builtin_object_size(p, 0); + assert_cl(l != (size_t) -1); + explicit_bzero_safe(p, l); +} + +#define _cleanup_erase_ _cleanup_(erase_obj) + _malloc_ _alloc_(1) _returns_nonnull_ _warn_unused_result_ static inline void *xmalloc(size_t size) { void *p; diff --git a/src/core/efi-random.c b/src/core/efi-random.c index 4086b12739..61516775fc 100644 --- a/src/core/efi-random.c +++ b/src/core/efi-random.c @@ -12,79 +12,23 @@ #include "random-util.h" #include "strv.h" -/* If a random seed was passed by the boot loader in the LoaderRandomSeed EFI variable, let's credit it to - * the kernel's random pool, but only once per boot. If this is run very early during initialization we can - * instantly boot up with a filled random pool. - * - * This makes no judgement on the entropy passed, it's the job of the boot loader to only pass us a seed that - * is suitably validated. */ - -static void lock_down_efi_variables(void) { +void lock_down_efi_variables(void) { + _cleanup_close_ int fd = -1; int r; + fd = open(EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderSystemToken)), O_RDONLY|O_CLOEXEC); + if (fd < 0) { + if (errno != ENOENT) + log_warning_errno(errno, "Unable to open LoaderSystemToken EFI variable, ignoring: %m"); + return; + } + /* Paranoia: let's restrict access modes of these a bit, so that unprivileged users can't use them to * identify the system or gain too much insight into what we might have credited to the entropy * pool. */ - FOREACH_STRING(path, - EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderRandomSeed)), - EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderSystemToken))) { - - r = chattr_path(path, 0, FS_IMMUTABLE_FL, NULL); - if (r == -ENOENT) - continue; - if (r < 0) - log_warning_errno(r, "Failed to drop FS_IMMUTABLE_FL from %s, ignoring: %m", path); - - if (chmod(path, 0600) < 0) - log_warning_errno(errno, "Failed to reduce access mode of %s, ignoring: %m", path); - } -} - -int efi_take_random_seed(void) { - _cleanup_free_ void *value = NULL; - size_t size; - int r; - - /* Paranoia comes first. */ - lock_down_efi_variables(); - - if (access("/run/systemd/efi-random-seed-taken", F_OK) < 0) { - if (errno != ENOENT) { - log_warning_errno(errno, "Failed to determine whether we already used the random seed token, not using it."); - return 0; - } - - /* ENOENT means we haven't used it yet. */ - } else { - log_debug("EFI random seed already used, not using again."); - return 0; - } - - r = efi_get_variable(EFI_LOADER_VARIABLE(LoaderRandomSeed), NULL, &value, &size); - if (r == -EOPNOTSUPP) { - log_debug_errno(r, "System lacks EFI support, not initializing random seed from EFI variable."); - return 0; - } - if (r == -ENOENT) { - log_debug_errno(r, "Boot loader did not pass LoaderRandomSeed EFI variable, not crediting any entropy."); - return 0; - } + r = chattr_fd(fd, 0, FS_IMMUTABLE_FL, NULL); if (r < 0) - return log_warning_errno(r, "Failed to read LoaderRandomSeed EFI variable, ignoring: %m"); - - if (size == 0) - return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Random seed passed from boot loader has zero size? Ignoring."); - - /* Before we use the seed, let's mark it as used, so that we never credit it twice. Also, it's a nice - * way to let users known that we successfully acquired entropy from the boot loader. */ - r = touch("/run/systemd/efi-random-seed-taken"); - if (r < 0) - return log_warning_errno(r, "Unable to mark EFI random seed as used, not using it: %m"); - - r = random_write_entropy(-1, value, size, true); - if (r < 0) - return log_warning_errno(errno, "Failed to credit entropy, ignoring: %m"); - - log_info("Successfully credited entropy passed from boot loader."); - return 1; + log_warning_errno(r, "Failed to drop FS_IMMUTABLE_FL from LoaderSystemToken EFI variable, ignoring: %m"); + if (fchmod(fd, 0600) < 0) + log_warning_errno(errno, "Failed to reduce access mode of LoaderSystemToken EFI variable, ignoring: %m"); } diff --git a/src/core/efi-random.h b/src/core/efi-random.h index 7d20fff57d..87166c9e3f 100644 --- a/src/core/efi-random.h +++ b/src/core/efi-random.h @@ -1,4 +1,4 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once -int efi_take_random_seed(void); +void lock_down_efi_variables(void); diff --git a/src/core/main.c b/src/core/main.c index cc725e6c42..119c518664 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -2831,8 +2831,8 @@ int main(int argc, char *argv[]) { goto finish; } - /* The efivarfs is now mounted, let's read the random seed off it */ - (void) efi_take_random_seed(); + /* The efivarfs is now mounted, let's lock down the system token. */ + lock_down_efi_variables(); /* Cache command-line options passed from EFI variables */ if (!skip_setup) diff --git a/src/random-seed/random-seed.c b/src/random-seed/random-seed.c index 04c2a29762..ccb8e47e19 100644 --- a/src/random-seed/random-seed.c +++ b/src/random-seed/random-seed.c @@ -16,7 +16,10 @@ #include "alloc-util.h" #include "build.h" +#include "chase-symlinks.h" +#include "efi-loader.h" #include "fd-util.h" +#include "find-esp.h" #include "fs-util.h" #include "io-util.h" #include "log.h" @@ -26,6 +29,7 @@ #include "mkdir.h" #include "parse-argument.h" #include "parse-util.h" +#include "path-util.h" #include "pretty-print.h" #include "random-util.h" #include "string-table.h" @@ -185,7 +189,7 @@ static int load_seed_file( if (ret_hash_state) { struct sha256_ctx *hash_state; - hash_state = malloc(sizeof(struct sha256_ctx)); + hash_state = new(struct sha256_ctx, 1); if (!hash_state) return log_oom(); @@ -311,6 +315,101 @@ static int save_seed_file( return 0; } +static int refresh_boot_seed(void) { + uint8_t buffer[RANDOM_EFI_SEED_SIZE]; + struct sha256_ctx hash_state; + _cleanup_free_ void *seed_file_bytes = NULL; + _cleanup_free_ char *esp_path = NULL; + _cleanup_close_ int seed_fd = -1; + size_t len; + ssize_t r; + + assert_cc(RANDOM_EFI_SEED_SIZE == SHA256_DIGEST_SIZE); + + r = find_esp_and_warn(NULL, NULL, /* unprivileged_mode= */ false, &esp_path, + NULL, NULL, NULL, NULL, NULL); + if (r < 0) { + if (r == -ENOKEY) { + log_debug_errno(r, "Couldn't find any ESP, so not updating ESP random seed."); + return 0; + } + return r; /* find_esp_and_warn() already logged */ + } + + seed_fd = chase_symlinks_and_open("/loader/random-seed", esp_path, + CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, + O_RDWR|O_CLOEXEC|O_NOCTTY, NULL); + if (seed_fd == -ENOENT) { + uint64_t features; + + r = efi_loader_get_features(&features); + if (r == 0 && FLAGS_SET(features, EFI_LOADER_FEATURE_RANDOM_SEED)) { + int dir_fd = chase_symlinks_and_open("/loader", esp_path, + CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, + O_DIRECTORY|O_CLOEXEC|O_NOCTTY, NULL); + if (dir_fd >= 0) { + seed_fd = openat(dir_fd, "random-seed", O_CREAT|O_EXCL|O_RDWR|O_CLOEXEC|O_NOCTTY, 0600); + close(dir_fd); + } + } + } + if (seed_fd < 0) { + log_debug_errno(seed_fd, "Failed to open EFI seed path: %m"); + return 0; + } + r = random_seed_size(seed_fd, &len); + if (r < 0) + return log_error_errno(r, "Failed to determine EFI seed path length: %m"); + seed_file_bytes = malloc(len); + if (!seed_file_bytes) + return log_oom(); + r = loop_read(seed_fd, seed_file_bytes, len, false); + if (r < 0) + return log_error_errno(r, "Failed to read EFI seed file: %m"); + + /* Hash the old seed in so that we never regress in entropy. */ + sha256_init_ctx(&hash_state); + sha256_process_bytes(&r, sizeof(r), &hash_state); + sha256_process_bytes(seed_file_bytes, r, &hash_state); + + /* We're doing this opportunistically, so if the seeding dance before didn't manage to initialize the + * RNG, there's no point in doing it here. Secondly, getrandom(GRND_NONBLOCK) has been around longer + * than EFI seeding anyway, so there's no point in having non-getrandom() fallbacks here. So if this + * fails, just return early to cut our losses. */ + r = getrandom(buffer, sizeof(buffer), GRND_NONBLOCK); + if (r < 0) { + if (errno == EAGAIN) { + log_debug_errno(errno, "Random pool not initialized yet, so skipping EFI seed update"); + return 0; + } + if (errno == ENOSYS) { + log_debug_errno(errno, "getrandom() not available, so skipping EFI seed update"); + return 0; + } + return log_error_errno(errno, "Failed to generate random bytes for EFI seed: %m"); + } + assert(r == sizeof(buffer)); + + /* Hash the new seed into the state containing the old one to generate our final seed. */ + sha256_process_bytes(&r, sizeof(r), &hash_state); + sha256_process_bytes(buffer, r, &hash_state); + sha256_finish_ctx(&hash_state, buffer); + + if (lseek(seed_fd, 0, SEEK_SET) < 0) + return log_error_errno(errno, "Failed to seek to beginning of EFI seed file: %m"); + r = loop_write(seed_fd, buffer, sizeof(buffer), false); + if (r < 0) + return log_error_errno(r, "Failed to write new EFI seed file: %m"); + if (ftruncate(seed_fd, sizeof(buffer)) < 0) + return log_error_errno(errno, "Failed to truncate EFI seed file: %m"); + r = fsync_full(seed_fd); + if (r < 0) + return log_error_errno(errno, "Failed to fsync EFI seed file: %m"); + + log_debug("Updated random seed in ESP"); + return 0; +} + static int help(int argc, char *argv[], void *userdata) { _cleanup_free_ char *link = NULL; int r; @@ -402,15 +501,15 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to create directory " RANDOM_SEED_DIR ": %m"); + random_fd = open("/dev/urandom", O_RDWR|O_CLOEXEC|O_NOCTTY); + if (random_fd < 0) + return log_error_errno(errno, "Failed to open /dev/urandom: %m"); + /* When we load the seed we read it and write it to the device and then immediately update the saved * seed with new data, to make sure the next boot gets seeded differently. */ switch (arg_action) { case ACTION_LOAD: - random_fd = open("/dev/urandom", O_RDWR|O_CLOEXEC|O_NOCTTY); - if (random_fd < 0) - return log_error_errno(errno, "Failed to open /dev/urandom: %m"); - /* First, let's write the machine ID into /dev/urandom, not crediting entropy. See * load_machine_id() for an explanation why. */ load_machine_id(random_fd); @@ -428,8 +527,10 @@ static int run(int argc, char *argv[]) { log_full_errno(level, open_rw_error, "Failed to open " RANDOM_SEED " for writing: %m"); log_full_errno(level, errno, "Failed to open " RANDOM_SEED " for reading: %m"); + r = -errno; - return missing ? 0 : -errno; + (void) refresh_boot_seed(); + return missing ? 0 : r; } } else write_seed_file = true; @@ -439,10 +540,7 @@ static int run(int argc, char *argv[]) { break; case ACTION_SAVE: - random_fd = open("/dev/urandom", O_RDONLY|O_CLOEXEC|O_NOCTTY); - if (random_fd < 0) - return log_error_errno(errno, "Failed to open /dev/urandom: %m"); - + (void) refresh_boot_seed(); seed_fd = open(RANDOM_SEED, O_WRONLY|O_CLOEXEC|O_NOCTTY|O_CREAT, 0600); if (seed_fd < 0) return log_error_errno(errno, "Failed to open " RANDOM_SEED ": %m"); @@ -460,9 +558,11 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - if (read_seed_file) + if (read_seed_file) { r = load_seed_file(seed_fd, random_fd, seed_size, write_seed_file ? &hash_state : NULL); + (void) refresh_boot_seed(); + } if (r >= 0 && write_seed_file) r = save_seed_file(seed_fd, random_fd, seed_size, synchronous, hash_state); diff --git a/units/systemd-boot-system-token.service b/units/systemd-boot-system-token.service index 662a1fda04..5a56d7c331 100644 --- a/units/systemd-boot-system-token.service +++ b/units/systemd-boot-system-token.service @@ -26,9 +26,6 @@ ConditionPathExists=/sys/firmware/efi/efivars/LoaderFeatures-4a67b082-0a4c-41cf- # Only run this if there is no system token defined yet, or … ConditionPathExists=|!/sys/firmware/efi/efivars/LoaderSystemToken-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f -# … if the boot loader didn't pass the OS a random seed (and thus probably was missing the random seed file) -ConditionPathExists=|!/sys/firmware/efi/efivars/LoaderRandomSeed-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f - [Service] Type=oneshot RemainAfterExit=yes |