diff --git a/man/loader.conf.xml b/man/loader.conf.xml index 3606de5704..acddb193d1 100644 --- a/man/loader.conf.xml +++ b/man/loader.conf.xml @@ -222,6 +222,51 @@ Currently, only x86 is supported, where it uses the PC speaker. + + secure-boot-enroll + + Danger: this feature might soft-brick your device if used improperly. + + Takes one of off, manual or force. + Controls the enrollment of secure boot keys. If set to off, no action whatsoever + is taken. If set to manual (the default) and the UEFI firmware is in setup-mode + then entries to manually enroll Secure Boot variables are created in the boot menu. If set to + force, in addition, if a directory named /loader/keys/auto/ + exists on the ESP then the keys in that directory are enrolled automatically. + + The different sets of variables can be set up under /loader/keys/NAME + where NAME is the name that is going to be used as the name of the entry. + This allows to ship multiple sets of Secure Boot variables and choose which one to enroll at runtime. + + Supported secure boot variables are one database for authorized images, one key exchange key (KEK) + and one platform key (PK). For more information, refer to the UEFI specification, + under Secure Boot and Driver Signing. Another resource that describe the interplay of the different variables is the + + EDK2 documentation. + + A complete set of UEFI variable includes db.esl, KEK.esl + and PK.esl. Note that these files need to be authenticated UEFI variables. See + below for an example of how to generate them from regular X.509 keys. + + uuid=$(systemd-id128 new --) +for key in PK KEK db; do + openssl req -new -x509 -subj "/CN=${key}/ -keyout "${key}.key" -out "${key}.crt" + openssl x509 -outform DER -in "${key}.crt" -out "${key}.cer" + cert-to-efi-sig-list -g "${uuid}" "${key}.crt" "${key}.tmp" +done + +sign-efi-sig-list -c PK.crt -k PK.key PK PK.tmp PK.esl +sign-efi-sig-list -c PK.crt -k PK.key KEK KEK.tmp KEK.esl +sign-efi-sig-list -c KEK.crt -k KEK.key db db.tmp db.esl + + + This feature is considered dangerous because even if all the required files are signed with the + keys being loaded, some files necessary for the system to function properly still won't be. This + is especially the case with Option ROMs (e.g. for storage controllers or graphics cards). See + Secure Boot and Option ROMs + for more details. + + reboot-for-bitlocker diff --git a/man/systemd-boot.xml b/man/systemd-boot.xml index 393c0312fa..02790370df 100644 --- a/man/systemd-boot.xml +++ b/man/systemd-boot.xml @@ -55,6 +55,9 @@ The EFI Shell binary, if installed. A reboot into the UEFI firmware setup option, if supported by the firmware. + + Secure boot variables enrollement if the UEFI firmware is in setup-mode and files are provided + on the ESP. systemd-boot supports the following features: @@ -91,6 +94,9 @@ The boot manager optionally reads a random seed from the ESP partition, combines it with a 'system token' stored in a persistent EFI variable and derives a random seed to use by the OS as entropy pool initialization, providing a full entropy pool during early boot. + + The boot manager allows for secure boot variables to be enrolled if the UEFI firmware is + in setup-mode. Additionally, variables can be automatically enrolled if configured. bootctl1 @@ -311,6 +317,12 @@ extension of the EFI architecture ID followed by .efi (e.g. for x86-64 this means a suffix of x64.efi). This may be used to automatically load file system drivers and similar, to extend the native firmware support. + + Enrollment of Secure Boot variables can be performed manually or automatically if files are available + under /keys/NAME/{db,KEK,PK}.esl, NAME + being the display name for the set of variables in the menu. If one of the sets is named auto + then it might be enrolled automatically depending on whether secure-boot-enroll is set + to force or not. diff --git a/src/boot/efi/boot.c b/src/boot/efi/boot.c index b3f424d8ba..db0bbab0f2 100644 --- a/src/boot/efi/boot.c +++ b/src/boot/efi/boot.c @@ -48,6 +48,7 @@ enum loader_type { LOADER_EFI, LOADER_LINUX, /* Boot loader spec type #1 entries */ LOADER_UNIFIED_LINUX, /* Boot loader spec type #2 entries */ + LOADER_SECURE_BOOT_KEYS, }; typedef struct { @@ -88,6 +89,7 @@ typedef struct { bool auto_entries; bool auto_firmware; bool reboot_for_bitlocker; + secure_boot_enroll secure_boot_enroll; bool force_menu; bool use_saved_entry; bool use_saved_entry_efivar; @@ -528,6 +530,17 @@ static void print_status(Config *config, char16_t *loaded_image_path) { ps_bool(L" reboot-for-bitlocker: %s\n", config->reboot_for_bitlocker); ps_string(L" random-seed-mode: %s\n", random_seed_modes_table[config->random_seed_mode]); + switch (config->secure_boot_enroll) { + case ENROLL_OFF: + Print(L" secure-boot-enroll: off\n"); break; + case ENROLL_MANUAL: + Print(L" secure-boot-enroll: manual\n"); break; + case ENROLL_FORCE: + Print(L" secure-boot-enroll: force\n"); break; + default: + assert_not_reached(); + } + switch (config->console_mode) { case CONSOLE_MODE_AUTO: Print(L" console-mode (config): %s\n", L"auto"); break; @@ -1217,6 +1230,17 @@ static void config_defaults_load_from_file(Config *config, char *content) { err = parse_boolean(value, &config->reboot_for_bitlocker); if (err != EFI_SUCCESS) log_error_stall(L"Error parsing 'reboot-for-bitlocker' config option: %a", value); + } + + if (streq8(key, "secure-boot-enroll")) { + if (streq8(value, "manual")) + config->secure_boot_enroll = ENROLL_MANUAL; + else if (streq8(value, "force")) + config->secure_boot_enroll = ENROLL_FORCE; + else if (streq8(value, "off")) + config->secure_boot_enroll = ENROLL_OFF; + else + log_error_stall(L"Error parsing 'secure-boot-enroll' config option: %a", value); continue; } @@ -1519,6 +1543,7 @@ static void config_load_defaults(Config *config, EFI_FILE *root_dir) { .auto_entries = true, .auto_firmware = true, .reboot_for_bitlocker = false, + .secure_boot_enroll = ENROLL_MANUAL, .random_seed_mode = RANDOM_SEED_WITH_SYSTEM_TOKEN, .idx_default_efivar = IDX_INVALID, .console_mode = CONSOLE_MODE_KEEP, @@ -2429,6 +2454,55 @@ static void save_selected_entry(const Config *config, const ConfigEntry *entry) (void) efivar_set(LOADER_GUID, L"LoaderEntryLastBooted", NULL, EFI_VARIABLE_NON_VOLATILE); } +static EFI_STATUS secure_boot_discover_keys(Config *config, EFI_FILE *root_dir) { + EFI_STATUS err; + _cleanup_(file_closep) EFI_FILE *keys_basedir = NULL; + + if (secure_boot_mode() != SECURE_BOOT_SETUP) + return EFI_SUCCESS; + + /* the lack of a 'keys' directory is not fatal and is silently ignored */ + err = open_directory(root_dir, u"\\loader\\keys", &keys_basedir); + if (err == EFI_NOT_FOUND) + return EFI_SUCCESS; + if (err != EFI_SUCCESS) + return err; + + for (;;) { + _cleanup_free_ EFI_FILE_INFO *dirent = NULL; + size_t dirent_size = 0; + ConfigEntry *entry = NULL; + + err = readdir_harder(keys_basedir, &dirent, &dirent_size); + if (err != EFI_SUCCESS || !dirent) + return err; + + if (dirent->FileName[0] == '.') + continue; + + if (!FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY)) + continue; + + entry = xnew(ConfigEntry, 1); + *entry = (ConfigEntry) { + .id = xpool_print(L"secure-boot-keys-%s", dirent->FileName), + .title = xpool_print(L"Enroll Secure Boot keys: %s", dirent->FileName), + .path = xpool_print(L"\\loader\\keys\\%s", dirent->FileName), + .type = LOADER_SECURE_BOOT_KEYS, + .tries_done = -1, + .tries_left = -1, + }; + config_add_entry(config, entry); + + if (config->secure_boot_enroll == ENROLL_FORCE && strcaseeq16(dirent->FileName, u"auto")) + /* if we auto enroll sucessfully this call does not return, if it fails we still + * want to add other potential entries to the menu */ + secure_boot_enroll_at(root_dir, entry->path); + } + + return EFI_SUCCESS; +} + static void export_variables( EFI_LOADED_IMAGE_PROTOCOL *loaded_image, const char16_t *loaded_image_path, @@ -2518,6 +2592,13 @@ static void config_load_all_entries( config_add_entry(config, entry); } + /* find if secure boot signing keys exist and autoload them if necessary + otherwise creates menu entries so that the user can load them manually + if the secure-boot-enroll variable is set to no (the default), we do not + even search for keys on the ESP */ + if (config->secure_boot_enroll != ENROLL_OFF) + secure_boot_discover_keys(config, root_dir); + if (config->entry_count == 0) return; @@ -2606,6 +2687,14 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { break; } + /* if auto enrollment is activated, we try to load keys for the given entry. */ + if (entry->type == LOADER_SECURE_BOOT_KEYS && config.secure_boot_enroll != ENROLL_OFF) { + err = secure_boot_enroll_at(root_dir, entry->path); + if (err == EFI_SUCCESS) + return EFI_SUCCESS; + continue; + } + /* Run special entry like "reboot" now. Those that have a loader * will be handled by image_start() instead. */ if (entry->call && !entry->loader) { diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build index 0129fcd070..83f7384530 100644 --- a/src/boot/efi/meson.build +++ b/src/boot/efi/meson.build @@ -355,6 +355,7 @@ efi_headers = files( common_sources = files( 'assert.c', + 'console.c', 'devicetree.c', 'disk.c', 'efi-string.c', @@ -369,7 +370,6 @@ common_sources = files( systemd_boot_sources = files( 'boot.c', - 'console.c', 'drivers.c', 'random-seed.c', 'shim.c', diff --git a/src/boot/efi/missing_efi.h b/src/boot/efi/missing_efi.h index f9700e3422..4e80acca56 100644 --- a/src/boot/efi/missing_efi.h +++ b/src/boot/efi/missing_efi.h @@ -385,3 +385,10 @@ typedef struct _EFI_CONSOLE_CONTROL_PROTOCOL { } EFI_CONSOLE_CONTROL_PROTOCOL; #endif + +#ifndef EFI_IMAGE_SECURITY_DATABASE_VARIABLE + +#define EFI_IMAGE_SECURITY_DATABASE_VARIABLE \ + { 0xd719b2cb, 0x3d3a, 0x4596, {0xa3, 0xbc, 0xda, 0xd0, 0xe, 0x67, 0x65, 0x6f }} + +#endif diff --git a/src/boot/efi/secure-boot.c b/src/boot/efi/secure-boot.c index 31f634a4d7..1da1026df5 100644 --- a/src/boot/efi/secure-boot.c +++ b/src/boot/efi/secure-boot.c @@ -2,6 +2,7 @@ #include "sbat.h" #include "secure-boot.h" +#include "console.h" #include "util.h" bool secure_boot_enabled(void) { @@ -33,3 +34,90 @@ SecureBootMode secure_boot_mode(void) { #ifdef SBAT_DISTRO static const char sbat[] _used_ _section_(".sbat") = SBAT_SECTION_TEXT; #endif + +EFI_STATUS secure_boot_enroll_at(EFI_FILE *root_dir, const char16_t *path) { + assert(root_dir); + assert(path); + + EFI_STATUS err; + + clear_screen(COLOR_NORMAL); + + Print(L"Enrolling secure boot keys from directory: \\loader\\keys\\%s\n" + L"Warning: Enrolling custom Secure Boot keys might soft-brick your machine!\n", + path); + + unsigned timeout_sec = 15; + for(;;) { + PrintAt(0, ST->ConOut->Mode->CursorRow, L"Enrolling in %2u s, press any key to abort.", timeout_sec); + + uint64_t key; + err = console_key_read(&key, 1000 * 1000); + if (err == EFI_NOT_READY) + continue; + if (err == EFI_TIMEOUT) { + if (timeout_sec == 0) /* continue enrolling keys */ + break; + timeout_sec--; + continue; + } + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Error waiting for user input to enroll Secure Boot keys: %r", err); + + /* user aborted, returning EFI_SUCCESS here allows the user to go back to the menu */ + return EFI_SUCCESS; + } + + _cleanup_(file_closep) EFI_FILE *dir = NULL; + + err = open_directory(root_dir, path, &dir); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed opening keys directory %s: %r", path, err); + + struct { + const char16_t *name; + const char16_t *filename; + const EFI_GUID vendor; + char *buffer; + size_t size; + } sb_vars[] = { + { u"db", u"db.esl", EFI_IMAGE_SECURITY_DATABASE_VARIABLE, NULL, 0 }, + { u"KEK", u"KEK.esl", EFI_GLOBAL_VARIABLE, NULL, 0 }, + { u"PK", u"PK.esl", EFI_GLOBAL_VARIABLE, NULL, 0 }, + }; + + /* Make sure all keys files exist before we start enrolling them by loading them from the disk first. */ + for (size_t i = 0; i < ELEMENTSOF(sb_vars); i++) { + err = file_read(dir, sb_vars[i].filename, 0, 0, &sb_vars[i].buffer, &sb_vars[i].size); + if (err != EFI_SUCCESS) { + log_error_stall(L"Failed reading file %s\\%s: %r", path, sb_vars[i].filename, err); + goto out_deallocate; + } + } + + for (size_t i = 0; i < ELEMENTSOF(sb_vars); i++) { + uint32_t sb_vars_opts = + EFI_VARIABLE_NON_VOLATILE | + EFI_VARIABLE_BOOTSERVICE_ACCESS | + EFI_VARIABLE_RUNTIME_ACCESS | + EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS; + + err = efivar_set_raw(&sb_vars[i].vendor, sb_vars[i].name, sb_vars[i].buffer, sb_vars[i].size, sb_vars_opts); + if (err != EFI_SUCCESS) { + log_error_stall(L"Failed to write %s secure boot variable: %r", sb_vars[i].name, err); + goto out_deallocate; + } + } + + /* The system should be in secure boot mode now and we could continue a regular boot. But at least + * TPM PCR7 measurements should change on next boot. Reboot now so that any OS we load does not end + * up relying on the old PCR state. */ + RT->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL); + assert_not_reached(); + +out_deallocate: + for (size_t i = 0; i < ELEMENTSOF(sb_vars); i++) + FreePool(sb_vars[i].buffer); + + return err; +} diff --git a/src/boot/efi/secure-boot.h b/src/boot/efi/secure-boot.h index ce43423fce..ff434ed1ad 100644 --- a/src/boot/efi/secure-boot.h +++ b/src/boot/efi/secure-boot.h @@ -4,5 +4,13 @@ #include #include "efivars-fundamental.h" +typedef enum { + ENROLL_OFF, /* no Secure Boot key enrollment whatsoever, even manual entries are not generated */ + ENROLL_MANUAL, /* Secure Boot key enrollment is strictly manual: manual entries are generated and need to be selected by the user */ + ENROLL_FORCE, /* Secure Boot key enrollment may be automatic if it is available but might not be safe */ +} secure_boot_enroll; + bool secure_boot_enabled(void); SecureBootMode secure_boot_mode(void); + +EFI_STATUS secure_boot_enroll_at(EFI_FILE *root_dir, const char16_t *path);