You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
390 lines
19 KiB
390 lines
19 KiB
8 months ago
|
From 2f2729382327bde136559a0d0ac15740d76108f9 Mon Sep 17 00:00:00 2001
|
||
|
From: Luca Boccassi <bluca@debian.org>
|
||
|
Date: Fri, 12 May 2023 00:55:58 +0100
|
||
|
Subject: [PATCH] stub: allow loading and verifying cmdline addons
|
||
|
|
||
|
Files placed in /EFI/Linux/UKI.efi.extra.d/ and /loader/addons/ are
|
||
|
opened and verified using the LoadImage protocol, and will thus get
|
||
|
verified via shim/firmware.
|
||
|
If they are valid signed PE files, the .cmdline section will be
|
||
|
extracted and appended. If there are multiple addons in each directory,
|
||
|
they will be parsed in alphanumerical order.
|
||
|
|
||
|
Optionally the .uname sections are also matched if present, so
|
||
|
that they can be used to filter out addons as well if needed, and only
|
||
|
addons that correspond exactly to the UKI being loaded are used.
|
||
|
It is recommended to also always add a .sbat section to addons, so
|
||
|
that they can be mass-revoked with just a policy update.
|
||
|
|
||
|
The files must have a .addon.efi suffix.
|
||
|
|
||
|
Files in the per-UKI directory are parsed, sorted, measured and
|
||
|
appended first. Then, files in the generic directory are processed.
|
||
|
|
||
|
(cherry picked from commit 05c9f9c2517c54b98d55f08f8afa67c79be861e8)
|
||
|
|
||
|
Related: RHEL-16952
|
||
|
---
|
||
|
man/systemd-stub.xml | 32 ++++++
|
||
|
src/boot/efi/cpio.c | 2 +-
|
||
|
src/boot/efi/cpio.h | 2 +
|
||
|
src/boot/efi/meson.build | 2 +-
|
||
|
src/boot/efi/stub.c | 216 +++++++++++++++++++++++++++++++++++++++
|
||
|
5 files changed, 252 insertions(+), 2 deletions(-)
|
||
|
|
||
|
diff --git a/man/systemd-stub.xml b/man/systemd-stub.xml
|
||
|
index edfc299bc2..21035cc944 100644
|
||
|
--- a/man/systemd-stub.xml
|
||
|
+++ b/man/systemd-stub.xml
|
||
|
@@ -28,8 +28,10 @@
|
||
|
<para><filename>/usr/lib/systemd/boot/efi/linuxx64.efi.stub</filename></para>
|
||
|
<para><filename>/usr/lib/systemd/boot/efi/linuxia32.efi.stub</filename></para>
|
||
|
<para><filename>/usr/lib/systemd/boot/efi/linuxaa64.efi.stub</filename></para>
|
||
|
+ <para><filename><replaceable>ESP</replaceable>/.../<replaceable>foo</replaceable>.efi.extra.d/*.addon.efi</filename></para>
|
||
|
<para><filename><replaceable>ESP</replaceable>/.../<replaceable>foo</replaceable>.efi.extra.d/*.cred</filename></para>
|
||
|
<para><filename><replaceable>ESP</replaceable>/.../<replaceable>foo</replaceable>.efi.extra.d/*.raw</filename></para>
|
||
|
+ <para><filename><replaceable>ESP</replaceable>/loader/addons/*.addon.efi</filename></para>
|
||
|
<para><filename><replaceable>ESP</replaceable>/loader/credentials/*.cred</filename></para>
|
||
|
</refsynopsisdiv>
|
||
|
|
||
|
@@ -148,11 +150,41 @@
|
||
|
details on system extension images. The generated <command>cpio</command> archive containing these
|
||
|
system extension images is measured into TPM PCR 13 (if a TPM is present).</para></listitem>
|
||
|
|
||
|
+ <listitem><para>Similarly, files
|
||
|
+ <filename><replaceable>foo</replaceable>.efi.extra.d/*.addon.efi</filename>
|
||
|
+ are loaded and verified as PE binaries, and a <literal>.cmdline</literal> section is parsed from them.
|
||
|
+ In case Secure Boot is enabled, these files will be validated using keys in UEFI DB, Shim's DB or
|
||
|
+ Shim's MOK, and will be rejected otherwise. Additionally, if the both the addon and the UKI contain a
|
||
|
+ a <literal>.uname</literal> section, the addon will be rejected if they do not exactly match. It is
|
||
|
+ recommended to always add a <literal>.sbat</literal> section to all signed addons, so that they may be
|
||
|
+ revoked with a SBAT policy update, without requiring blocklisting via DBX/MOKX. The
|
||
|
+ <citerefentry><refentrytitle>ukify</refentrytitle><manvolnum>1</manvolnum></citerefentry> tool will
|
||
|
+ add a SBAT policy by default if none is passed when building addons. For more information on SBAT see
|
||
|
+ <ulink url="https://github.com/rhboot/shim/blob/main/SBAT.md">Shim's documentation.</ulink>
|
||
|
+ Addons are supposed to be used to pass additional kernel command line parameters, regardless of the
|
||
|
+ kernel image being booted, for example to allow platform vendors to ship platform-specific
|
||
|
+ configuration. The loaded command line addon files are sorted, loaded, measured into TPM PCR 12 (if a
|
||
|
+ TPM is present) and appended to the kernel command line. UKI command line options are listed first,
|
||
|
+ then options from addons in <filename>/loader/addons/*.addon.efi</filename> are appended next, and
|
||
|
+ finally UKI-specific addons are appended last. Addons are always loaded in the same order based on the
|
||
|
+ filename, so that, given the same set of addons, the same set of measurements can be expected in
|
||
|
+ PCR12, however note that the filename is not protected by the PE signature, and as such an attacker
|
||
|
+ with write access to the ESP could potentially rename these files to change the order in which they
|
||
|
+ are loaded, in a way that could alter the functionality of the kernel, as some options might be order
|
||
|
+ dependent. If you sign such addons, you should pay attention to the PCR12 values and make use of an
|
||
|
+ attestation service so that improper use of your signed addons can be detected and dealt with using
|
||
|
+ one of the aforementioned revocation mechanisms.</para></listitem>
|
||
|
+
|
||
|
<listitem><para>Files <filename>/loader/credentials/*.cred</filename> are packed up in a
|
||
|
<command>cpio</command> archive and placed in the <filename>/.extra/global_credentials/</filename>
|
||
|
directory of the initrd file hierarchy. This is supposed to be used to pass additional credentials to
|
||
|
the initrd, regardless of the kernel being booted. The generated <command>cpio</command> archive is
|
||
|
measured into TPM PCR 12 (if a TPM is present)</para></listitem>
|
||
|
+
|
||
|
+ <listitem><para>Additionally, files <filename>/loader/addons/*.addon.efi</filename> are loaded and
|
||
|
+ verified as PE binaries, and a <literal>.cmdline</literal> section is parsed from them. This is
|
||
|
+ supposed to be used to pass additional command line parameters to the kernel, regardless of the kernel
|
||
|
+ being booted.</para></listitem>
|
||
|
</itemizedlist>
|
||
|
|
||
|
<para>These mechanisms may be used to parameterize and extend trusted (i.e. signed), immutable initrd
|
||
|
diff --git a/src/boot/efi/cpio.c b/src/boot/efi/cpio.c
|
||
|
index 0d95d40183..741c11f7ae 100644
|
||
|
--- a/src/boot/efi/cpio.c
|
||
|
+++ b/src/boot/efi/cpio.c
|
||
|
@@ -341,7 +341,7 @@ static EFI_STATUS measure_cpio(
|
||
|
return EFI_SUCCESS;
|
||
|
}
|
||
|
|
||
|
-static char16_t *get_dropin_dir(const EFI_DEVICE_PATH *file_path) {
|
||
|
+char16_t *get_dropin_dir(const EFI_DEVICE_PATH *file_path) {
|
||
|
if (!file_path)
|
||
|
return NULL;
|
||
|
|
||
|
diff --git a/src/boot/efi/cpio.h b/src/boot/efi/cpio.h
|
||
|
index e80e06723c..0f14edbf5f 100644
|
||
|
--- a/src/boot/efi/cpio.h
|
||
|
+++ b/src/boot/efi/cpio.h
|
||
|
@@ -32,3 +32,5 @@ EFI_STATUS pack_cpio_literal(
|
||
|
void **ret_buffer,
|
||
|
size_t *ret_buffer_size,
|
||
|
bool *ret_measured);
|
||
|
+
|
||
|
+char16_t *get_dropin_dir(const EFI_DEVICE_PATH *file_path);
|
||
|
diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build
|
||
|
index 9e5d535b5b..b84ceb8c9f 100644
|
||
|
--- a/src/boot/efi/meson.build
|
||
|
+++ b/src/boot/efi/meson.build
|
||
|
@@ -386,6 +386,7 @@ common_sources = files(
|
||
|
'pe.c',
|
||
|
'random-seed.c',
|
||
|
'secure-boot.c',
|
||
|
+ 'shim.c',
|
||
|
'ticks.c',
|
||
|
'util.c',
|
||
|
'vmm.c',
|
||
|
@@ -393,7 +394,6 @@ common_sources = files(
|
||
|
|
||
|
systemd_boot_sources = files(
|
||
|
'boot.c',
|
||
|
- 'shim.c',
|
||
|
)
|
||
|
|
||
|
stub_sources = files(
|
||
|
diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c
|
||
|
index a195612f0e..2c7c56de3e 100644
|
||
|
--- a/src/boot/efi/stub.c
|
||
|
+++ b/src/boot/efi/stub.c
|
||
|
@@ -13,6 +13,7 @@
|
||
|
#include "pe.h"
|
||
|
#include "random-seed.h"
|
||
|
#include "secure-boot.h"
|
||
|
+#include "shim.h"
|
||
|
#include "splash.h"
|
||
|
#include "tpm-pcr.h"
|
||
|
#include "util.h"
|
||
|
@@ -181,6 +182,189 @@ static bool use_load_options(
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
+static EFI_STATUS load_addons_from_dir(
|
||
|
+ EFI_FILE *root,
|
||
|
+ const char16_t *prefix,
|
||
|
+ char16_t ***items,
|
||
|
+ size_t *n_items,
|
||
|
+ size_t *n_allocated) {
|
||
|
+
|
||
|
+ _cleanup_(file_closep) EFI_FILE *extra_dir = NULL;
|
||
|
+ _cleanup_free_ EFI_FILE_INFO *dirent = NULL;
|
||
|
+ size_t dirent_size = 0;
|
||
|
+ EFI_STATUS err;
|
||
|
+
|
||
|
+ assert(root);
|
||
|
+ assert(prefix);
|
||
|
+ assert(items);
|
||
|
+ assert(n_items);
|
||
|
+ assert(n_allocated);
|
||
|
+
|
||
|
+ err = open_directory(root, prefix, &extra_dir);
|
||
|
+ if (err == EFI_NOT_FOUND)
|
||
|
+ /* No extra subdir, that's totally OK */
|
||
|
+ return EFI_SUCCESS;
|
||
|
+ if (err != EFI_SUCCESS)
|
||
|
+ return log_error_status(err, "Failed to open addons directory '%ls': %m", prefix);
|
||
|
+
|
||
|
+ for (;;) {
|
||
|
+ _cleanup_free_ char16_t *d = NULL;
|
||
|
+
|
||
|
+ err = readdir_harder(extra_dir, &dirent, &dirent_size);
|
||
|
+ if (err != EFI_SUCCESS)
|
||
|
+ return log_error_status(err, "Failed to read addons directory of loaded image: %m");
|
||
|
+ if (!dirent) /* End of directory */
|
||
|
+ break;
|
||
|
+
|
||
|
+ if (dirent->FileName[0] == '.')
|
||
|
+ continue;
|
||
|
+ if (FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY))
|
||
|
+ continue;
|
||
|
+ if (!is_ascii(dirent->FileName))
|
||
|
+ continue;
|
||
|
+ if (strlen16(dirent->FileName) > 255) /* Max filename size on Linux */
|
||
|
+ continue;
|
||
|
+ if (!endswith_no_case(dirent->FileName, u".addon.efi"))
|
||
|
+ continue;
|
||
|
+
|
||
|
+ d = xstrdup16(dirent->FileName);
|
||
|
+
|
||
|
+ if (*n_items + 2 > *n_allocated) {
|
||
|
+ /* We allocate 16 entries at a time, as a matter of optimization */
|
||
|
+ if (*n_items > (SIZE_MAX / sizeof(uint16_t)) - 16) /* Overflow check, just in case */
|
||
|
+ return log_oom();
|
||
|
+
|
||
|
+ size_t m = *n_items + 16;
|
||
|
+ *items = xrealloc(*items, *n_allocated * sizeof(uint16_t *), m * sizeof(uint16_t *));
|
||
|
+ *n_allocated = m;
|
||
|
+ }
|
||
|
+
|
||
|
+ (*items)[(*n_items)++] = TAKE_PTR(d);
|
||
|
+ (*items)[*n_items] = NULL; /* Let's always NUL terminate, to make freeing via strv_free() easy */
|
||
|
+ }
|
||
|
+
|
||
|
+ return EFI_SUCCESS;
|
||
|
+
|
||
|
+}
|
||
|
+
|
||
|
+static EFI_STATUS cmdline_append_and_measure_addons(
|
||
|
+ EFI_HANDLE stub_image,
|
||
|
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
|
||
|
+ const char16_t *prefix,
|
||
|
+ const char *uname,
|
||
|
+ bool *ret_parameters_measured,
|
||
|
+ char16_t **cmdline_append) {
|
||
|
+
|
||
|
+ _cleanup_(strv_freep) char16_t **items = NULL;
|
||
|
+ _cleanup_(file_closep) EFI_FILE *root = NULL;
|
||
|
+ _cleanup_free_ char16_t *buffer = NULL;
|
||
|
+ size_t n_items = 0, n_allocated = 0;
|
||
|
+ EFI_STATUS err;
|
||
|
+
|
||
|
+ assert(stub_image);
|
||
|
+ assert(loaded_image);
|
||
|
+ assert(prefix);
|
||
|
+ assert(ret_parameters_measured);
|
||
|
+ assert(cmdline_append);
|
||
|
+
|
||
|
+ if (!loaded_image->DeviceHandle)
|
||
|
+ return EFI_SUCCESS;
|
||
|
+
|
||
|
+ err = open_volume(loaded_image->DeviceHandle, &root);
|
||
|
+ if (err == EFI_UNSUPPORTED)
|
||
|
+ /* Error will be unsupported if the bootloader doesn't implement the file system protocol on
|
||
|
+ * its file handles. */
|
||
|
+ return EFI_SUCCESS;
|
||
|
+ if (err != EFI_SUCCESS)
|
||
|
+ return log_error_status(err, "Unable to open root directory: %m");
|
||
|
+
|
||
|
+ err = load_addons_from_dir(root, prefix, &items, &n_items, &n_allocated);
|
||
|
+ if (err != EFI_SUCCESS)
|
||
|
+ return err;
|
||
|
+
|
||
|
+ if (n_items == 0)
|
||
|
+ return EFI_SUCCESS; /* Empty directory */
|
||
|
+
|
||
|
+ /* Now, sort the files we found, to make this uniform and stable (and to ensure the TPM measurements
|
||
|
+ * are not dependent on read order) */
|
||
|
+ sort_pointer_array((void**) items, n_items, (compare_pointer_func_t) strcmp16);
|
||
|
+
|
||
|
+ for (size_t i = 0; i < n_items; i++) {
|
||
|
+ size_t addrs[_UNIFIED_SECTION_MAX] = {}, szs[_UNIFIED_SECTION_MAX] = {};
|
||
|
+ _cleanup_free_ EFI_DEVICE_PATH *addon_path = NULL;
|
||
|
+ _cleanup_(unload_imagep) EFI_HANDLE addon = NULL;
|
||
|
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_addon = NULL;
|
||
|
+ _cleanup_free_ char16_t *addon_spath = NULL;
|
||
|
+
|
||
|
+ addon_spath = xasprintf("%ls\\%ls", prefix, items[i]);
|
||
|
+ err = make_file_device_path(loaded_image->DeviceHandle, addon_spath, &addon_path);
|
||
|
+ if (err != EFI_SUCCESS)
|
||
|
+ return log_error_status(err, "Error making device path for %ls: %m", addon_spath);
|
||
|
+
|
||
|
+ /* By using shim_load_image, we cover both the case where the PE files are signed with MoK
|
||
|
+ * and with DB, and running with or without shim. */
|
||
|
+ err = shim_load_image(stub_image, addon_path, &addon);
|
||
|
+ if (err != EFI_SUCCESS) {
|
||
|
+ log_error_status(err,
|
||
|
+ "Failed to read '%ls' from '%ls', ignoring: %m",
|
||
|
+ items[i],
|
||
|
+ addon_spath);
|
||
|
+ continue;
|
||
|
+ }
|
||
|
+
|
||
|
+ err = BS->HandleProtocol(addon,
|
||
|
+ MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL),
|
||
|
+ (void **) &loaded_addon);
|
||
|
+ if (err != EFI_SUCCESS)
|
||
|
+ return log_error_status(err, "Failed to find protocol in %ls: %m", items[i]);
|
||
|
+
|
||
|
+ err = pe_memory_locate_sections(loaded_addon->ImageBase, unified_sections, addrs, szs);
|
||
|
+ if (err != EFI_SUCCESS || szs[UNIFIED_SECTION_CMDLINE] == 0) {
|
||
|
+ if (err == EFI_SUCCESS)
|
||
|
+ err = EFI_NOT_FOUND;
|
||
|
+ log_error_status(err,
|
||
|
+ "Unable to locate embedded .cmdline section in %ls, ignoring: %m",
|
||
|
+ items[i]);
|
||
|
+ continue;
|
||
|
+ }
|
||
|
+
|
||
|
+ /* We want to enforce that addons are not UKIs, i.e.: they must not embed a kernel. */
|
||
|
+ if (szs[UNIFIED_SECTION_LINUX] > 0) {
|
||
|
+ log_error_status(EFI_INVALID_PARAMETER, "%ls is a UKI, not an addon, ignoring: %m", items[i]);
|
||
|
+ continue;
|
||
|
+ }
|
||
|
+
|
||
|
+ /* Also enforce that, in case it is specified, .uname matches as a quick way to allow
|
||
|
+ * enforcing compatibility with a specific UKI only */
|
||
|
+ if (uname && szs[UNIFIED_SECTION_UNAME] > 0 &&
|
||
|
+ !strneq8(uname,
|
||
|
+ (char *)loaded_addon->ImageBase + addrs[UNIFIED_SECTION_UNAME],
|
||
|
+ szs[UNIFIED_SECTION_UNAME])) {
|
||
|
+ log_error(".uname mismatch between %ls and UKI, ignoring", items[i]);
|
||
|
+ continue;
|
||
|
+ }
|
||
|
+
|
||
|
+ _cleanup_free_ char16_t *tmp = TAKE_PTR(buffer),
|
||
|
+ *extra16 = xstrn8_to_16((char *)loaded_addon->ImageBase + addrs[UNIFIED_SECTION_CMDLINE],
|
||
|
+ szs[UNIFIED_SECTION_CMDLINE]);
|
||
|
+ buffer = xasprintf("%ls%ls%ls", strempty(tmp), isempty(tmp) ? u"" : u" ", extra16);
|
||
|
+ }
|
||
|
+
|
||
|
+ mangle_stub_cmdline(buffer);
|
||
|
+
|
||
|
+ if (!isempty(buffer)) {
|
||
|
+ _cleanup_free_ char16_t *tmp = TAKE_PTR(*cmdline_append);
|
||
|
+ bool m = false;
|
||
|
+
|
||
|
+ (void) tpm_log_load_options(buffer, &m);
|
||
|
+ *ret_parameters_measured = m;
|
||
|
+
|
||
|
+ *cmdline_append = xasprintf("%ls%ls%ls", strempty(tmp), isempty(tmp) ? u"" : u" ", buffer);
|
||
|
+ }
|
||
|
+
|
||
|
+ return EFI_SUCCESS;
|
||
|
+}
|
||
|
+
|
||
|
static EFI_STATUS real_main(EFI_HANDLE image) {
|
||
|
_cleanup_free_ void *credential_initrd = NULL, *global_credential_initrd = NULL, *sysext_initrd = NULL, *pcrsig_initrd = NULL, *pcrpkey_initrd = NULL;
|
||
|
size_t credential_initrd_size = 0, global_credential_initrd_size = 0, sysext_initrd_size = 0, pcrsig_initrd_size = 0, pcrpkey_initrd_size = 0;
|
||
|
@@ -191,6 +375,7 @@ static EFI_STATUS real_main(EFI_HANDLE image) {
|
||
|
size_t addrs[_UNIFIED_SECTION_MAX] = {}, szs[_UNIFIED_SECTION_MAX] = {};
|
||
|
_cleanup_free_ char16_t *cmdline = NULL;
|
||
|
int sections_measured = -1, parameters_measured = -1;
|
||
|
+ _cleanup_free_ char *uname = NULL;
|
||
|
bool sysext_measured = false, m;
|
||
|
uint64_t loader_features = 0;
|
||
|
EFI_STATUS err;
|
||
|
@@ -263,6 +448,10 @@ static EFI_STATUS real_main(EFI_HANDLE image) {
|
||
|
/* Show splash screen as early as possible */
|
||
|
graphics_splash((const uint8_t*) loaded_image->ImageBase + addrs[UNIFIED_SECTION_SPLASH], szs[UNIFIED_SECTION_SPLASH]);
|
||
|
|
||
|
+ if (szs[UNIFIED_SECTION_UNAME] > 0)
|
||
|
+ uname = xstrndup8((char *)loaded_image->ImageBase + addrs[UNIFIED_SECTION_UNAME],
|
||
|
+ szs[UNIFIED_SECTION_UNAME]);
|
||
|
+
|
||
|
if (use_load_options(image, loaded_image, szs[UNIFIED_SECTION_CMDLINE] > 0, &cmdline)) {
|
||
|
/* Let's measure the passed kernel command line into the TPM. Note that this possibly
|
||
|
* duplicates what we already did in the boot menu, if that was already used. However, since
|
||
|
@@ -278,6 +467,33 @@ static EFI_STATUS real_main(EFI_HANDLE image) {
|
||
|
mangle_stub_cmdline(cmdline);
|
||
|
}
|
||
|
|
||
|
+ /* If we have any extra command line to add via PE addons, load them now and append, and
|
||
|
+ * measure the additions separately, after the embedded options, but before the smbios ones,
|
||
|
+ * so that the order is reversed from "most hardcoded" to "most dynamic". The global addons are
|
||
|
+ * loaded first, and the image-specific ones later, for the same reason. */
|
||
|
+ err = cmdline_append_and_measure_addons(
|
||
|
+ image,
|
||
|
+ loaded_image,
|
||
|
+ u"\\loader\\addons",
|
||
|
+ uname,
|
||
|
+ &m,
|
||
|
+ &cmdline);
|
||
|
+ if (err != EFI_SUCCESS)
|
||
|
+ log_error_status(err, "Error loading global addons, ignoring: %m");
|
||
|
+ parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m);
|
||
|
+
|
||
|
+ _cleanup_free_ char16_t *dropin_dir = get_dropin_dir(loaded_image->FilePath);
|
||
|
+ err = cmdline_append_and_measure_addons(
|
||
|
+ image,
|
||
|
+ loaded_image,
|
||
|
+ dropin_dir,
|
||
|
+ uname,
|
||
|
+ &m,
|
||
|
+ &cmdline);
|
||
|
+ if (err != EFI_SUCCESS)
|
||
|
+ log_error_status(err, "Error loading UKI-specific addons, ignoring: %m");
|
||
|
+ parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m);
|
||
|
+
|
||
|
const char *extra = smbios_find_oem_string("io.systemd.stub.kernel-cmdline-extra");
|
||
|
if (extra) {
|
||
|
_cleanup_free_ char16_t *tmp = TAKE_PTR(cmdline), *extra16 = xstr8_to_16(extra);
|