mirror of
https://github.com/systemd/systemd.git
synced 2026-08-09 09:32:04 +00:00
If the read-only FAT file attribute is set on /loader/random-seed, don't update the seed file — and hence don't use it either, since a seed we cannot update would be the same on every boot. This gives users an explicit way to turn off random seed handling by marking the file read-only, useful for example in pre-built OS images that are replicated to many systems, where the baked-in seed is shared and hence must not be credited. The check is done upfront in process_random_seed(), before any other work, mirroring the existing check for read-only volumes. This covers both systemd-boot and systemd-stub, which share this code. Inspired-by: #42979
432 lines
20 KiB
C
432 lines
20 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
|
|
#include "efi-efivars.h"
|
|
#include "efi-log.h"
|
|
#include "memory-util.h"
|
|
#include "proto/rng.h"
|
|
#include "random-seed.h"
|
|
#include "secure-boot.h"
|
|
#include "sha256.h"
|
|
#include "util.h"
|
|
|
|
#define RANDOM_MAX_SIZE_MIN (32U)
|
|
#define RANDOM_MAX_SIZE_MAX (32U*1024U)
|
|
|
|
/* SHA256 gives us 256/8=32 bytes */
|
|
#define HASH_VALUE_SIZE 32
|
|
|
|
/* 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"
|
|
|
|
#define RANDOM_SEED_PATH u"\\loader\\random-seed"
|
|
|
|
static EFI_STATUS acquire_rng(void *ret, size_t size) {
|
|
EFI_RNG_PROTOCOL *rng;
|
|
EFI_STATUS err;
|
|
|
|
assert(ret);
|
|
|
|
/* Try to acquire the specified number of bytes from the UEFI RNG */
|
|
|
|
err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_RNG_PROTOCOL), NULL, (void **) &rng);
|
|
if (err != EFI_SUCCESS)
|
|
return err;
|
|
if (!rng)
|
|
return EFI_UNSUPPORTED;
|
|
|
|
err = rng->GetRNG(rng, NULL, size, ret);
|
|
/* On some systems the RNG might not be ready during early boot, handle gracefully and don't log. */
|
|
if (err == EFI_NOT_READY)
|
|
return err;
|
|
if (err != EFI_SUCCESS)
|
|
return log_warning_status(err, "Failed to acquire RNG data, proceeding without: %m");
|
|
return EFI_SUCCESS;
|
|
}
|
|
|
|
static EFI_STATUS acquire_system_token(void **ret, size_t *ret_size) {
|
|
_cleanup_free_ char *data = NULL;
|
|
EFI_STATUS err;
|
|
size_t size;
|
|
|
|
assert(ret);
|
|
assert(ret_size);
|
|
|
|
err = efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderSystemToken", (void**) &data, &size);
|
|
if (err != EFI_SUCCESS) {
|
|
if (err != EFI_NOT_FOUND)
|
|
log_error_status(err, "Failed to read LoaderSystemToken EFI variable: %m");
|
|
return err;
|
|
}
|
|
|
|
if (size <= 0)
|
|
return log_error_status(EFI_NOT_FOUND, "System token too short, ignoring.");
|
|
|
|
*ret = TAKE_PTR(data);
|
|
*ret_size = size;
|
|
|
|
return EFI_SUCCESS;
|
|
}
|
|
|
|
static void validate_sha256(void) {
|
|
|
|
#ifdef EFI_DEBUG
|
|
/* Let's validate our SHA256 implementation. We stole it from glibc, and converted it to UEFI
|
|
* style. We better check whether it does the right stuff. We use the simpler test vectors from the
|
|
* SHA spec. Note that we strip this out in optimization builds. */
|
|
|
|
static const struct {
|
|
const char *string;
|
|
uint8_t hash[HASH_VALUE_SIZE];
|
|
} array[] = {
|
|
{ "abc",
|
|
{ 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,
|
|
0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
|
|
0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,
|
|
0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad }},
|
|
|
|
{ "",
|
|
{ 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
|
|
0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
|
|
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
|
|
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55 }},
|
|
|
|
{ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
|
|
{ 0x24, 0x8d, 0x6a, 0x61, 0xd2, 0x06, 0x38, 0xb8,
|
|
0xe5, 0xc0, 0x26, 0x93, 0x0c, 0x3e, 0x60, 0x39,
|
|
0xa3, 0x3c, 0xe4, 0x59, 0x64, 0xff, 0x21, 0x67,
|
|
0xf6, 0xec, 0xed, 0xd4, 0x19, 0xdb, 0x06, 0xc1 }},
|
|
|
|
{ "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu",
|
|
{ 0xcf, 0x5b, 0x16, 0xa7, 0x78, 0xaf, 0x83, 0x80,
|
|
0x03, 0x6c, 0xe5, 0x9e, 0x7b, 0x04, 0x92, 0x37,
|
|
0x0b, 0x24, 0x9b, 0x11, 0xe8, 0xf0, 0x7a, 0x51,
|
|
0xaf, 0xac, 0x45, 0x03, 0x7a, 0xfe, 0xe9, 0xd1 }},
|
|
};
|
|
|
|
FOREACH_ELEMENT(i, array)
|
|
assert(memcmp(SHA256_DIRECT(i->string, strlen8(i->string)), i->hash, HASH_VALUE_SIZE) == 0);
|
|
#endif
|
|
}
|
|
|
|
static bool random_seed_file_read_only(EFI_FILE *root_dir) {
|
|
_cleanup_file_close_ EFI_FILE *handle = NULL;
|
|
_cleanup_free_ EFI_FILE_INFO *info = NULL;
|
|
EFI_STATUS err;
|
|
|
|
assert(root_dir);
|
|
|
|
/* Checks whether the random seed file exists and has the read-only file attribute set. */
|
|
|
|
err = root_dir->Open(root_dir, &handle, (char16_t *) RANDOM_SEED_PATH, EFI_FILE_MODE_READ, 0);
|
|
if (err != EFI_SUCCESS) {
|
|
if (err != EFI_NOT_FOUND)
|
|
log_debug_status(err, "Failed to open random seed file for reading, ignoring: %m");
|
|
return false;
|
|
}
|
|
|
|
err = get_file_info(handle, &info, /* ret_size= */ NULL);
|
|
if (err != EFI_SUCCESS) {
|
|
log_debug_status(err, "Failed to get file info of random seed file, ignoring: %m");
|
|
return false;
|
|
}
|
|
|
|
return FLAGS_SET(info->Attribute, EFI_FILE_READ_ONLY);
|
|
}
|
|
|
|
EFI_STATUS process_random_seed(EFI_FILE *root_dir) {
|
|
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_close_ EFI_FILE *handle = NULL;
|
|
_cleanup_free_ EFI_FILE_INFO *info = NULL;
|
|
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;
|
|
|
|
CLEANUP_ERASE(random_bytes);
|
|
CLEANUP_ERASE(hash_key);
|
|
CLEANUP_ERASE(hash);
|
|
|
|
assert(root_dir);
|
|
assert_cc(DESIRED_SEED_SIZE == HASH_VALUE_SIZE);
|
|
|
|
validate_sha256();
|
|
|
|
/* If the volume is read-only we cannot update the random seed, but if we cannot update it, then we
|
|
* really don't want to use it since it would be the same on every boot. */
|
|
bool volume_ro;
|
|
err = get_volume_ro(root_dir, &volume_ro);
|
|
if (err != EFI_SUCCESS)
|
|
log_debug_status(err, "Failed to determine if volume is read-only, assuming not: %m");
|
|
else if (volume_ro) {
|
|
log_debug("Volume is read-only, not updating random seed.");
|
|
return EFI_SUCCESS;
|
|
}
|
|
|
|
/* Similarly, if the random seed file is marked read-only, take this as a hint that the seed shall
|
|
* not be updated — and hence not be used either, since a seed we cannot update would be the same on
|
|
* every boot. This provides a way to explicitly turn off random seed handling, for example for
|
|
* pre-built OS images replicated to many systems. */
|
|
if (random_seed_file_read_only(root_dir))
|
|
goto read_only;
|
|
|
|
/* 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);
|
|
|
|
previous_seed_table = find_configuration_table(MAKE_GUID_PTR(LINUX_EFI_RANDOM_SEED_TABLE));
|
|
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. */
|
|
size = 0;
|
|
err = acquire_system_token(&system_token, &size);
|
|
if ((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);
|
|
}
|
|
|
|
bool created = false;
|
|
err = root_dir->Open(
|
|
root_dir,
|
|
&handle,
|
|
(char16_t *) RANDOM_SEED_PATH,
|
|
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. Get a handle to the \loader\ directory — open it read-only if it already
|
|
* exists, or create it (requiring write access) only when it is missing (e.g. on
|
|
* systems using UKI+EFISTUB without systemd-boot installed). We avoid requesting
|
|
* write access on an already-present directory because some firmware
|
|
* implementations refuse it, which would abort seed creation unnecessarily. */
|
|
_cleanup_file_close_ EFI_FILE *dir_handle = NULL;
|
|
err = root_dir->Open(
|
|
root_dir,
|
|
&dir_handle,
|
|
(char16_t *) u"\\loader",
|
|
EFI_FILE_MODE_READ,
|
|
0);
|
|
if (err == EFI_NOT_FOUND)
|
|
err = root_dir->Open(
|
|
root_dir,
|
|
&dir_handle,
|
|
(char16_t *) u"\\loader",
|
|
EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE,
|
|
EFI_FILE_DIRECTORY);
|
|
if (err != EFI_SUCCESS)
|
|
return log_full(err,
|
|
EFI_STATUS_IS_WRITE_REFUSED(err) ? LOG_DEBUG : LOG_ERR,
|
|
"Failed to open or create loader directory: %m");
|
|
|
|
err = dir_handle->Open(
|
|
dir_handle,
|
|
&handle,
|
|
(char16_t *) u"random-seed",
|
|
EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE,
|
|
0);
|
|
if (err != EFI_SUCCESS)
|
|
return log_full(err,
|
|
EFI_STATUS_IS_WRITE_REFUSED(err) ? LOG_DEBUG : LOG_ERR,
|
|
"Failed to open random seed file: %m");
|
|
created = true;
|
|
|
|
} else if (err != EFI_SUCCESS)
|
|
return log_full(err,
|
|
err == EFI_NOT_FOUND || EFI_STATUS_IS_WRITE_REFUSED(err) ? LOG_DEBUG : LOG_ERR,
|
|
"Failed to open random seed file: %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");
|
|
|
|
if (FLAGS_SET(info->Attribute, EFI_FILE_READ_ONLY))
|
|
goto read_only;
|
|
|
|
/* 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;
|
|
}
|
|
|
|
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.");
|
|
|
|
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
|
|
* persistent, the random seed we generate will still be different on every single boot. */
|
|
err = BS->GetNextMonotonicCount(&uefi_monotonic_counter);
|
|
if (err != EFI_SUCCESS && !seeded_by_efi)
|
|
return log_error_status(err, "Failed to acquire UEFI monotonic counter: %m");
|
|
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. */
|
|
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");
|
|
wsize = info->FileSize - size;
|
|
err = handle->Write(handle, &wsize, seed /* All zeros now */);
|
|
if (err != EFI_SUCCESS)
|
|
return log_full(err,
|
|
EFI_STATUS_IS_WRITE_REFUSED(err) ? LOG_DEBUG : LOG_ERR,
|
|
"Failed to write random seed file: %m");
|
|
if (wsize != info->FileSize - size)
|
|
return log_error_status(EFI_PROTOCOL_ERROR, "Short write on random seed file.");
|
|
err = handle->Flush(handle);
|
|
if (err != EFI_SUCCESS)
|
|
return log_full(err,
|
|
EFI_STATUS_IS_WRITE_REFUSED(err) ? LOG_DEBUG : LOG_ERR,
|
|
"Failed to flush random seed file: %m");
|
|
err = handle->SetPosition(handle, 0);
|
|
if (err != EFI_SUCCESS)
|
|
return log_error_status(err, "Failed to seek to beginning of random seed file: %m");
|
|
|
|
/* We could truncate the file here with something like:
|
|
*
|
|
* info->FileSize = size;
|
|
* err = handle->SetInfo(handle, &GenericFileInfo, info->Size, info);
|
|
* if (err != EFI_SUCCESS)
|
|
* return log_error_status(err, "Failed to truncate random seed file: %u");
|
|
*
|
|
* But this is considered slightly risky, because EFI filesystem drivers are a little bit
|
|
* flimsy. So instead we rely on userspace eventually truncating this when it writes a new
|
|
* seed. For now the best we do is zero it. */
|
|
}
|
|
|
|
/* Update the random seed on disk before we use it */
|
|
wsize = size;
|
|
err = handle->Write(handle, &wsize, random_bytes);
|
|
if (err != EFI_SUCCESS)
|
|
return log_full(err,
|
|
EFI_STATUS_IS_WRITE_REFUSED(err) ? LOG_DEBUG : LOG_ERR,
|
|
"Failed to write random seed file: %m");
|
|
if (wsize != size)
|
|
return log_error_status(EFI_PROTOCOL_ERROR, "Short write on random seed file.");
|
|
err = handle->Flush(handle);
|
|
if (err != EFI_SUCCESS)
|
|
return log_full(err,
|
|
EFI_STATUS_IS_WRITE_REFUSED(err) ? LOG_DEBUG : LOG_ERR,
|
|
"Failed to flush random seed file: %m");
|
|
|
|
err = BS->AllocatePool(EfiACPIReclaimMemory,
|
|
offsetof(struct linux_efi_random_seed, seed) + DESIRED_SEED_SIZE,
|
|
(void **) &new_seed_table);
|
|
if (err != EFI_SUCCESS)
|
|
return log_error_status(err, "Failed to allocate EFI table for random seed: %m");
|
|
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(MAKE_GUID_PTR(LINUX_EFI_RANDOM_SEED_TABLE), new_seed_table);
|
|
if (err != EFI_SUCCESS)
|
|
return log_error_status(err, "Failed to install EFI table for random seed: %m");
|
|
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;
|
|
|
|
read_only:
|
|
log_debug("Random seed file is read-only, not updating random seed.");
|
|
return EFI_SUCCESS;
|
|
}
|