boot: implement kernel EFI RNG seed protocol with proper hashing

Rather than passing seeds up to userspace via EFI variables, pass seeds
directly to the kernel's EFI stub loader, via LINUX_EFI_RANDOM_SEED_TABLE_GUID.
EFI variables can potentially leak and suffer from forward secrecy
issues, and processing these with userspace means that they are
initialized much too late in boot to be useful. In contrast,
LINUX_EFI_RANDOM_SEED_TABLE_GUID uses EFI configuration tables, and so
is hidden from userspace entirely, and is parsed extremely early on by
the kernel, so that every single call to get_random_bytes() by the
kernel is seeded.

In order to do this properly, we use a bit more robust hashing scheme,
and make sure that each input is properly memzeroed out after use. The
scheme is:

    key = HASH(LABEL || sizeof(input1) || input1 || ... || sizeof(inputN) || inputN)
    new_disk_seed = HASH(key || 0)
    seed_for_linux = HASH(key || 1)

The various inputs are:
- LINUX_EFI_RANDOM_SEED_TABLE_GUID from prior bootloaders
- 256 bits of seed from EFI's RNG
- The (immutable) system token, from its EFI variable
- The prior on-disk seed
- The UEFI monotonic counter
- A timestamp

This also adjusts the secure boot semantics, so that the operation is
only aborted if it's not possible to get random bytes from EFI's RNG or
a prior boot stage. With the proper hashing scheme, this should make
boot seeds safe even on secure boot.

There is currently a bug in Linux's EFI stub in which if the EFI stub
manages to generate random bytes on its own using EFI's RNG, it will
ignore what the bootloader passes. That's annoying, but it means that
either way, via systemd-boot or via EFI stub's mechanism, the RNG *does*
get initialized in a good safe way. And this bug is now fixed in the
efi.git tree, and will hopefully be backported to older kernels.

As the kernel recommends, the resultant seeds are 256 bits and are
allocated using pool memory of type EfiACPIReclaimMemory, so that it
gets freed at the right moment in boot.
This commit is contained in:
Jason A. Donenfeld 2022-11-09 12:44:37 +01:00
parent 87172c3df6
commit 0be72218f1
13 changed files with 242 additions and 291 deletions

View file

@ -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"
}
/**

View file

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

View file

@ -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.
before the OS is invoked.
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.
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

View file

@ -435,28 +435,6 @@
to view this data. </para></listitem>
</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>

View file

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

View file

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

View file

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

View file

@ -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_startcounter_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);
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);
/* 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;
/* 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 write random seed to EFI variable: %r", err);
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 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;
}

View file

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

View file

@ -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;
}
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;
r = chattr_fd(fd, 0, FS_IMMUTABLE_FL, NULL);
if (r < 0)
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");
}

View file

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

View file

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

View file

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