From 83bf58f39dbe947c1b4f394667f42a0c557fd94c Mon Sep 17 00:00:00 2001 From: Ani Sinha Date: Fri, 8 Nov 2024 12:01:51 +0530 Subject: [PATCH] uki: introduce support for a .efifw section UKIs can be used to bundle uefi firmwares that can be measured and used on a confidential computing environment. There can be more than one firmware blob bundle, each one for a specific platform. Also firmware images can themselves be containers like IGVM files that can in turn bundle the actual firmware blob. This change is specifically for uefi firmwares, not IGVM container files. This change adds support to introduce a .efifw section in UKI that can be used for firmware blobs/images. There can be multiple such sections and each section can contain a single firmware image. The matching .hwids entry for a specific platform can be used to select the most appropriate firmware blob. ukify tool has been also changed to support addition of a firmware image in UKI. Since firmware gets measured automatically, we do not need to measure it separately as a part of the UKI. --- man/systemd-stub.xml | 9 +++++ src/boot/efifirmware.c | 84 ++++++++++++++++++++++++++++++++++++++++++ src/boot/efifirmware.h | 31 ++++++++++++++++ src/boot/meson.build | 1 + src/boot/pe.c | 42 +++++++++++++++++++++ src/fundamental/uki.c | 1 + src/fundamental/uki.h | 1 + src/measure/measure.c | 3 +- src/ukify/ukify.py | 64 +++++++++++++++++++++++++++++++- 9 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 src/boot/efifirmware.c create mode 100644 src/boot/efifirmware.h diff --git a/man/systemd-stub.xml b/man/systemd-stub.xml index 764fd32ddd5..09de149d42d 100644 --- a/man/systemd-stub.xml +++ b/man/systemd-stub.xml @@ -91,6 +91,15 @@ the same matching procedure. If a match is found, that .dtbauto section will be loaded and will override .dtb if present. + Zero or more .efifw sections for the firmware image. It works + in many ways similar to .dtbauto sections. systemd-stub + will always use the first matching one. The match is performed by first selecting the most appropriate + entry in the .hwids section based on the hardware IDs supplied by SMBIOS (see below). + If a suitable entry is found, the fwid string from that entry will be used to + perform the matching procedure for firmware blobs in .efifw section. The first + matching firmware will be loaded. + + Zero or more .hwids sections with hardware IDs of the machines to match DeviceTrees. systemd-stub will use the SMBIOS data to calculate hardware IDs of the machine (as per + +static bool efifw_validate_header( + const void *blob, + size_t blob_len, + const char **ret_fwid, + const char **ret_payload) { + + if ((uintptr_t) blob % alignof(EfiFwHeader) != 0) + return false; + + size_t base_sz = offsetof(EfiFwHeader, payload); + + /* at least the base size of the header must be in memory */ + if (blob_len < base_sz) + return false; + + const EfiFwHeader *fw_header = ASSERT_PTR(blob); + + if (fw_header->magic != FWHEADERMAGIC) + return false; + + uint32_t header_len = fw_header->header_len; + + /* header_len must not be malformed */ + if (header_len < base_sz) + return false; + + uint32_t fwid_len = fw_header->fwid_len; + uint32_t payload_len = fw_header->payload_len; + size_t total_computed_size; + + /* check for unusually large values of payload_len, header_len or fwid_len */ + if (!ADD_SAFE(&total_computed_size, header_len, fwid_len) || + !ADD_SAFE(&total_computed_size, total_computed_size, payload_len)) + return false; + + /* see if entire size of the base header is present in memory */ + if (blob_len < total_computed_size) + return false; + + const char *fwid = (const char*)blob + header_len; + const char *payload = fwid + fwid_len; + + /* check that fwid points to a NUL terminated string */ + if (memchr(fwid, 0, fwid_len) != fwid + fwid_len - 1) + return false; + + if (ret_fwid) + *ret_fwid = fwid; + + if (ret_payload) + *ret_payload = payload; + return true; +} + +static const char* efifw_get_fwid( + const void *efifwblob, + size_t efifwblob_len) { + + const char* fwid; + if (!efifw_validate_header(efifwblob, efifwblob_len, &fwid, NULL)) + return NULL; + + return fwid; +} + +EFI_STATUS efifirmware_match_by_fwid( + const void *uki_efifw, + size_t uki_efifw_len, + const char *fwid) { + + assert(fwid); + + const char *fwblob_fwid = efifw_get_fwid(uki_efifw, uki_efifw_len); + if (!fwblob_fwid) + return EFI_INVALID_PARAMETER; + + return streq8(fwblob_fwid, fwid) ? EFI_SUCCESS : EFI_NOT_FOUND; +} diff --git a/src/boot/efifirmware.h b/src/boot/efifirmware.h new file mode 100644 index 00000000000..888de35a983 --- /dev/null +++ b/src/boot/efifirmware.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define FWHEADERMAGIC (UINT32_C(0xfeeddead)) + +/* The structure of the efifw UKI blob is the following: + * --------------------------------------------------------- + * EfiFw header|fwid|payload| reserved for future attributes + * --------------------------------------------------------- + * The base header defines the length of full header, fwid and payload. + * The fwid is a NUL terminated string. + * The payload contains the actual efi firmware. + */ +typedef struct EfiFwHeader { + uint32_t magic; /* magic number that defines Efifw */ + uint32_t header_len; /* total length of header including all attributes */ + uint32_t fwid_len; /* length including the NUL terminator */ + uint32_t payload_len; /* actual length of the efi firmware binary image */ + + /* The header might be extended in the future to add additional + * parameters. header_len will increase to indicate presence of these + * additional attributes. + */ + + /* next comes payload which is fwid and efi firmware binary blob */ + uint8_t payload[] _alignas_(uint64_t); +} EfiFwHeader; + +EFI_STATUS efifirmware_match_by_fwid(const void *uki_efifw, size_t uki_efifw_length, const char *fwid); diff --git a/src/boot/meson.build b/src/boot/meson.build index f7269962334..31f36ea46d4 100644 --- a/src/boot/meson.build +++ b/src/boot/meson.build @@ -284,6 +284,7 @@ libefi_sources = files( 'console.c', 'device-path-util.c', 'devicetree.c', + 'efifirmware.c', 'drivers.c', 'efi-string.c', 'efivars.c', diff --git a/src/boot/pe.c b/src/boot/pe.c index 97ea8d7220a..348296f0016 100644 --- a/src/boot/pe.c +++ b/src/boot/pe.c @@ -2,6 +2,7 @@ #include "chid.h" #include "devicetree.h" +#include "efifirmware.h" #include "pe.h" #include "util.h" @@ -195,6 +196,33 @@ static bool pe_use_this_dtb( return false; } +static bool pe_use_this_firmware( + const void *efifw, + size_t efifw_size, + const void *base, + const Device *device, + size_t section_nb) { + + assert(efifw); + + EFI_STATUS err; + + /* if there is no hwids section, there is nothing much we can do */ + if (!device || !base) + return false; + + const char *fwid = device_get_fwid(base, device); + if (!fwid) + return false; + + err = efifirmware_match_by_fwid(efifw, efifw_size, fwid); + if (err == EFI_SUCCESS) + return true; + if (err == EFI_INVALID_PARAMETER) + log_error_status(err, "Found bad efifw blob in PE section %zu", section_nb); + return false; +} + static void pe_locate_sections_internal( const PeSectionHeader section_table[], size_t n_section_table, @@ -255,6 +283,20 @@ static void pe_locate_sections_internal( continue; } + /* handle efifw section which works very much like .dtbauto */ + if (pe_section_name_equal(section_names[i], ".efifw")) { + /* can't match without validate_base */ + if (!validate_base) + break; + if (!pe_use_this_firmware( + (const uint8_t *) SIZE_TO_PTR(validate_base) + j->VirtualAddress, + j->VirtualSize, + device_table, + device, + (PTR_TO_SIZE(j) - PTR_TO_SIZE(section_table)) / sizeof(*j))) + continue; + } + /* At this time, the sizes and offsets have been validated. Store them away */ sections[i] = (PeSectionVector) { .memory_size = j->VirtualSize, diff --git a/src/fundamental/uki.c b/src/fundamental/uki.c index 441d466a97e..06361cc5271 100644 --- a/src/fundamental/uki.c +++ b/src/fundamental/uki.c @@ -23,5 +23,6 @@ const char* const unified_sections[_UNIFIED_SECTION_MAX + 1] = { [UNIFIED_SECTION_PROFILE] = ".profile", [UNIFIED_SECTION_DTBAUTO] = ".dtbauto", [UNIFIED_SECTION_HWIDS] = ".hwids", + [UNIFIED_SECTION_EFIFW] = ".efifw", NULL, }; diff --git a/src/fundamental/uki.h b/src/fundamental/uki.h index 4b6195f9b70..6752b2ae77c 100644 --- a/src/fundamental/uki.h +++ b/src/fundamental/uki.h @@ -20,6 +20,7 @@ typedef enum UnifiedSection { UNIFIED_SECTION_PROFILE, UNIFIED_SECTION_DTBAUTO, UNIFIED_SECTION_HWIDS, + UNIFIED_SECTION_EFIFW, _UNIFIED_SECTION_MAX, } UnifiedSection; diff --git a/src/measure/measure.c b/src/measure/measure.c index dcae8528e07..c8e4d59688e 100644 --- a/src/measure/measure.c +++ b/src/measure/measure.c @@ -160,8 +160,9 @@ static int parse_argv(int argc, char *argv[]) { ARG_PCRPKEY, ARG_PROFILE, ARG_DTBAUTO, + ARG_HWIDS, _ARG_SECTION_LAST, - ARG_HWIDS = _ARG_SECTION_LAST, + ARG_EFIFW = _ARG_SECTION_LAST, ARG_BANK, ARG_PRIVATE_KEY, ARG_PRIVATE_KEY_SOURCE, diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 5f821297c12..3bf0f458573 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -262,6 +262,7 @@ class UkifyConfig: efi_arch: str hwids: Path initrd: list[Path] + efifw: list[Path] join_profiles: list[Path] sign_profiles: list[str] json: Union[Literal['pretty'], Literal['short'], Literal['off']] @@ -391,6 +392,7 @@ DEFAULT_SECTIONS_TO_SHOW = { '.dtb': 'binary', '.dtbauto': 'binary', '.hwids': 'binary', + '.efifw': 'binary', '.cmdline': 'text', '.osrel': 'text', '.uname': 'text', @@ -473,7 +475,10 @@ class UKI: if s.name == '.profile': start = i + 1 - if any(section.name == s.name for s in self.sections[start:] if s.name != '.dtbauto'): + multiple_allowed_sections = ['.dtbauto', '.efifw'] + if any( + section.name == s.name for s in self.sections[start:] if s.name not in multiple_allowed_sections + ): raise ValueError(f'Duplicate section {section.name}') self.sections += [section] @@ -665,7 +670,10 @@ def check_inputs(opts: UkifyConfig) -> None: elif isinstance(value, list): for item in value: if isinstance(item, Path): - item.open().close() + if item.is_dir(): + item.iterdir() + else: + item.open().close() check_splash(opts.splash) @@ -1049,6 +1057,10 @@ NULL_DEVICE = b'\0' * DEVICE_STRUCT_SIZE DEVICE_TYPE_DEVICETREE = 1 DEVICE_TYPE_UEFI_FW = 2 +# Keep in sync with efifirmware.h +FWHEADERMAGIC = 'feeddead' +EFIFW_HEADER_SIZE = 4 + 4 + 4 + 4 + def device_make_descriptor(device_type: int, size: int) -> int: return (size) | (device_type << 28) @@ -1139,6 +1151,42 @@ def parse_hwid_dir(path: Path) -> bytes: return devices_blob + strings_blob +def parse_efifw_dir(path: Path) -> bytes: + if not path.is_dir(): + raise ValueError(f'{path} is not a directory or it does not exist.') + + # only one firmware image must be present in the directory + # to uniquely identify that firmware with its ID. + if len(list(path.glob('*'))) != 1: + raise ValueError(f'{path} must contain exactly one firmware image file.') + + payload_blob = b'' + for fw in path.iterdir(): + payload_blob += fw.read_bytes() + + payload_len = len(payload_blob) + if payload_len == 0: + raise ValueError(f'{fw} is a zero byte file!') + + dirname = path.parts[-1] + # firmware id is the name of the directory the firmware bundle is in, + # terminated by NULL. + fwid = b'' + dirname.encode() + b'\0' + fwid_len = len(fwid) + magic = bytes.fromhex(FWHEADERMAGIC) + + efifw_header_blob = b'' + efifw_header_blob += struct.pack(' None: ('.splash', opts.splash, True), ('.pcrpkey', pcrpkey, True), ('.initrd', initrd, True), + *(('.efifw', parse_efifw_dir(fw), False) for fw in opts.efifw), ('.ucode', opts.microcode, True), ] # fmt: skip @@ -1281,6 +1330,7 @@ def make_uki(opts: UkifyConfig) -> None: '.osrel', '.cmdline', '.initrd', + '.efifw', '.ucode', '.splash', '.dtb', @@ -1756,6 +1806,16 @@ CONFIG_ITEMS = [ config_key='UKI/Initrd', config_push=ConfigItem.config_list_prepend, ), + ConfigItem( + '--efifw', + metavar='DIR', + type=Path, + action='append', + default=[], + help='Directory with efi firmware binary file [.efifw section]', + config_key='UKI/Firmware', + config_push=ConfigItem.config_list_prepend, + ), ConfigItem( '--microcode', metavar='UCODE', -- 2.47.3