From 48c5a4cd67591ac3f9435f35607bca4b60c8af10 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 5 Nov 2024 14:14:03 +0100 Subject: [PATCH 1/7] mkosi: Add ruff and mypy to tools tree packages --- mkosi.conf.d/05-tools/mkosi.conf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mkosi.conf.d/05-tools/mkosi.conf b/mkosi.conf.d/05-tools/mkosi.conf index 6da7766dc1..8ac2ebe24d 100644 --- a/mkosi.conf.d/05-tools/mkosi.conf +++ b/mkosi.conf.d/05-tools/mkosi.conf @@ -2,7 +2,9 @@ [Build] ToolsTreePackages= - meson gcc gperf + meson + mypy pkgconf + ruff From cf0238d8543e481831ec47c33f5c0b8350f18a8e Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Sun, 3 Nov 2024 18:48:53 +0100 Subject: [PATCH 2/7] pcrlock: Move pe_hash() and uki_hash() to pe-binary.h Let's move these to shared so we can reuse pe_hash() in the upcoming systemd-sbsign. --- src/pcrlock/meson.build | 1 - src/pcrlock/pcrlock.c | 2 +- src/pcrlock/pehash.c | 264 ------------------------------------- src/pcrlock/pehash.h | 11 -- src/shared/openssl-util.h | 1 + src/shared/pe-binary.c | 268 +++++++++++++++++++++++++++++++++++++- src/shared/pe-binary.h | 7 + 7 files changed, 276 insertions(+), 278 deletions(-) delete mode 100644 src/pcrlock/pehash.c delete mode 100644 src/pcrlock/pehash.h diff --git a/src/pcrlock/meson.build b/src/pcrlock/meson.build index a31b30bb15..8c8728a3a0 100644 --- a/src/pcrlock/meson.build +++ b/src/pcrlock/meson.build @@ -10,7 +10,6 @@ executables += [ 'sources' : files( 'pcrlock.c', 'pcrlock-firmware.c', - 'pehash.c', ), 'dependencies' : [ libm, diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index c1915761ee..d824914ae7 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -40,7 +40,7 @@ #include "path-util.h" #include "pcrextend-util.h" #include "pcrlock-firmware.h" -#include "pehash.h" +#include "pe-binary.h" #include "pretty-print.h" #include "proc-cmdline.h" #include "random-util.h" diff --git a/src/pcrlock/pehash.c b/src/pcrlock/pehash.c deleted file mode 100644 index 39ed61cc2e..0000000000 --- a/src/pcrlock/pehash.c +++ /dev/null @@ -1,264 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ - -#include -#include - -#include "alloc-util.h" -#include "hexdecoct.h" -#include "pe-binary.h" -#include "pehash.h" -#include "sort-util.h" -#include "stat-util.h" -#include "string-table.h" - -/* Implements: - * - * https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/authenticode_pe.docx - * → Section "Calculating the PE Image Hash" - */ - -#define IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE 4U - -static int hash_file(int fd, EVP_MD_CTX *md_ctx, uint64_t offset, uint64_t size) { - uint8_t buffer[64*1024]; - - log_debug("Hashing %" PRIu64 " @ %" PRIu64 " → %" PRIu64, size, offset, offset + size); - - while (size > 0) { - size_t m = MIN(size, sizeof(buffer)); - ssize_t n; - - n = pread(fd, buffer, m, offset); - if (n < 0) - return log_debug_errno(errno, "Failed to read file for hashing: %m"); - if ((size_t) n != m) - return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Short read while hashing."); - - if (EVP_DigestUpdate(md_ctx, buffer, m) != 1) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to hash data."); - - offset += m; - size -= m; - } - - return 0; -} - -static int section_offset_cmp(const IMAGE_SECTION_HEADER *a, const IMAGE_SECTION_HEADER *b) { - return CMP(ASSERT_PTR(a)->PointerToRawData, ASSERT_PTR(b)->PointerToRawData); -} - -int pe_hash(int fd, - const EVP_MD *md, - void **ret_hash, - size_t *ret_hash_size) { - - _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mdctx = NULL; - _cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL; - _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL; - _cleanup_free_ PeHeader *pe_header = NULL; - const IMAGE_DATA_DIRECTORY *certificate_table; - struct stat st; - uint64_t p, q; - int r; - - assert(fd >= 0); - assert(md); - assert(ret_hash_size); - assert(ret_hash); - - if (fstat(fd, &st) < 0) - return log_debug_errno(errno, "Failed to stat file: %m"); - r = stat_verify_regular(&st); - if (r < 0) - return log_debug_errno(r, "Not a regular file: %m"); - - r = pe_load_headers(fd, &dos_header, &pe_header); - if (r < 0) - return r; - - r = pe_load_sections(fd, dos_header, pe_header, §ions); - if (r < 0) - return r; - - certificate_table = pe_header_get_data_directory(pe_header, IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE); - if (!certificate_table) - return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "File lacks certificate table."); - - mdctx = EVP_MD_CTX_new(); - if (!mdctx) - return log_oom_debug(); - - if (EVP_DigestInit_ex(mdctx, md, NULL) != 1) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to allocate message digest."); - - /* Everything from beginning of file to CheckSum field in PE header */ - p = (uint64_t) dos_header->e_lfanew + - offsetof(PeHeader, optional.CheckSum); - r = hash_file(fd, mdctx, 0, p); - if (r < 0) - return r; - p += sizeof(le32_t); - - /* Everything between the CheckSum field and the Image Data Directory Entry for the Certification Table */ - q = (uint64_t) dos_header->e_lfanew + - PE_HEADER_OPTIONAL_FIELD_OFFSET(pe_header, DataDirectory[IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE]); - r = hash_file(fd, mdctx, p, q - p); - if (r < 0) - return r; - q += sizeof(IMAGE_DATA_DIRECTORY); - - /* The rest of the header + the section table */ - p = pe_header->optional.SizeOfHeaders; - if (p < q) - return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "SizeOfHeaders too short."); - r = hash_file(fd, mdctx, q, p - q); - if (r < 0) - return r; - - /* Sort by location in file */ - typesafe_qsort(sections, pe_header->pe.NumberOfSections, section_offset_cmp); - - FOREACH_ARRAY(section, sections, pe_header->pe.NumberOfSections) { - r = hash_file(fd, mdctx, section->PointerToRawData, section->SizeOfRawData); - if (r < 0) - return r; - - p += section->SizeOfRawData; - } - - if ((uint64_t) st.st_size > p) { - - if (st.st_size - p < certificate_table->Size) - return log_debug_errno(errno, "No space for certificate table, refusing."); - - r = hash_file(fd, mdctx, p, st.st_size - p - certificate_table->Size); - if (r < 0) - return r; - - /* If the file size is not a multiple of 8 bytes, pad the hash with zero bytes. */ - if (st.st_size % 8 != 0 && EVP_DigestUpdate(mdctx, (const uint8_t[8]) {}, 8 - (st.st_size % 8)) != 1) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to hash data."); - } - - int hsz = EVP_MD_CTX_size(mdctx); - if (hsz < 0) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to get hash size."); - - unsigned hash_size = (unsigned) hsz; - _cleanup_free_ void *hash = malloc(hsz); - if (!hash) - return log_oom_debug(); - - if (EVP_DigestFinal_ex(mdctx, hash, &hash_size) != 1) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize hash function."); - - assert(hash_size == (unsigned) hsz); - - *ret_hash = TAKE_PTR(hash); - *ret_hash_size = hash_size; - - return 0; -} - -typedef void* SectionHashArray[_UNIFIED_SECTION_MAX]; - -static void section_hash_array_done(SectionHashArray *array) { - assert(array); - - for (size_t i = 0; i < _UNIFIED_SECTION_MAX; i++) - free((*array)[i]); -} - -int uki_hash(int fd, - const EVP_MD *md, - void* ret_hashes[static _UNIFIED_SECTION_MAX], - size_t *ret_hash_size) { - - _cleanup_(section_hash_array_done) SectionHashArray hashes = {}; - _cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL; - _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL; - _cleanup_free_ PeHeader *pe_header = NULL; - int r; - - assert(fd >= 0); - assert(ret_hashes); - assert(ret_hash_size); - - r = pe_load_headers(fd, &dos_header, &pe_header); - if (r < 0) - return r; - - r = pe_load_sections(fd, dos_header, pe_header, §ions); - if (r < 0) - return r; - - int hsz = EVP_MD_size(md); - if (hsz < 0) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to get hash size."); - - FOREACH_ARRAY(section, sections, pe_header->pe.NumberOfSections) { - _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mdctx = NULL; - _cleanup_free_ char *n = NULL; - ssize_t i; - - n = memdup_suffix0(section->Name, sizeof(section->Name)); - if (!n) - return log_oom_debug(); - - i = string_table_lookup(unified_sections, _UNIFIED_SECTION_MAX, n); - if (i < 0) - continue; - - if (hashes[i]) - return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate section"); - - mdctx = EVP_MD_CTX_new(); - if (!mdctx) - return log_oom_debug(); - - if (EVP_DigestInit_ex(mdctx, md, NULL) != 1) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to allocate message digest."); - - r = hash_file(fd, mdctx, section->PointerToRawData, MIN(section->VirtualSize, section->SizeOfRawData)); - if (r < 0) - return r; - - if (section->SizeOfRawData < section->VirtualSize) { - uint8_t zeroes[1024] = {}; - size_t remaining = section->VirtualSize - section->SizeOfRawData; - - while (remaining > 0) { - size_t sz = MIN(sizeof(zeroes), remaining); - - if (EVP_DigestUpdate(mdctx, zeroes, sz) != 1) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to hash data."); - - remaining -= sz; - } - } - - hashes[i] = malloc(hsz); - if (!hashes[i]) - return log_oom_debug(); - - unsigned hash_size = (unsigned) hsz; - if (EVP_DigestFinal_ex(mdctx, hashes[i], &hash_size) != 1) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize hash function."); - - assert(hash_size == (unsigned) hsz); - - if (DEBUG_LOGGING) { - _cleanup_free_ char *hs = NULL; - - hs = hexmem(hashes[i], hsz); - log_debug("Section %s with %s is %s.", n, EVP_MD_name(md), strna(hs)); - } - } - - memcpy(ret_hashes, hashes, sizeof(hashes)); - zero(hashes); - *ret_hash_size = (unsigned) hsz; - - return 0; -} diff --git a/src/pcrlock/pehash.h b/src/pcrlock/pehash.h deleted file mode 100644 index 26f2fb1e7b..0000000000 --- a/src/pcrlock/pehash.h +++ /dev/null @@ -1,11 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ -#pragma once - -#include - -#include "openssl-util.h" -#include "uki.h" - -int pe_hash(int fd, const EVP_MD *md, void **ret_hash, size_t *ret_hash_size); - -int uki_hash(int fd, const EVP_MD *md, void *ret_hashes[static _UNIFIED_SECTION_MAX], size_t *ret_hash_size); diff --git a/src/shared/openssl-util.h b/src/shared/openssl-util.h index c9acd40f22..f70de86a09 100644 --- a/src/shared/openssl-util.h +++ b/src/shared/openssl-util.h @@ -140,6 +140,7 @@ int openssl_load_key_from_token(KeySourceType private_key_source_type, const cha typedef struct X509 X509; typedef struct EVP_PKEY EVP_PKEY; +typedef struct EVP_MD EVP_MD; typedef struct UI_METHOD UI_METHOD; static inline void *X509_free(X509 *p) { diff --git a/src/shared/pe-binary.c b/src/shared/pe-binary.c index 3893ffaf97..60d0886d22 100644 --- a/src/shared/pe-binary.c +++ b/src/shared/pe-binary.c @@ -1,12 +1,18 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include "alloc-util.h" +#include "hexdecoct.h" #include "log.h" #include "pe-binary.h" +#include "sort-util.h" +#include "stat-util.h" +#include "string-table.h" #include "string-util.h" -#include "uki.h" + +#define IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE 4U bool pe_header_is_64bit(const PeHeader *h) { assert(h); @@ -284,3 +290,263 @@ bool pe_is_native(const PeHeader *pe_header) { return false; #endif } + +/* Implements: + * + * https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/authenticode_pe.docx + * → Section "Calculating the PE Image Hash" + */ + +#if HAVE_OPENSSL +static int hash_file(int fd, EVP_MD_CTX *md_ctx, uint64_t offset, uint64_t size) { + uint8_t buffer[64*1024]; + + log_debug("Hashing %" PRIu64 " @ %" PRIu64 " → %" PRIu64, size, offset, offset + size); + + while (size > 0) { + size_t m = MIN(size, sizeof(buffer)); + ssize_t n; + + n = pread(fd, buffer, m, offset); + if (n < 0) + return log_debug_errno(errno, "Failed to read file for hashing: %m"); + if ((size_t) n != m) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Short read while hashing."); + + if (EVP_DigestUpdate(md_ctx, buffer, m) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to hash data."); + + offset += m; + size -= m; + } + + return 0; +} + +static int section_offset_cmp(const IMAGE_SECTION_HEADER *a, const IMAGE_SECTION_HEADER *b) { + return CMP(ASSERT_PTR(a)->PointerToRawData, ASSERT_PTR(b)->PointerToRawData); +} +#endif + +int pe_hash(int fd, + const EVP_MD *md, + void **ret_hash, + size_t *ret_hash_size) { +#if HAVE_OPENSSL + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mdctx = NULL; + _cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL; + _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL; + _cleanup_free_ PeHeader *pe_header = NULL; + const IMAGE_DATA_DIRECTORY *certificate_table; + struct stat st; + uint64_t p, q; + int r; + + assert(fd >= 0); + assert(md); + assert(ret_hash_size); + assert(ret_hash); + + if (fstat(fd, &st) < 0) + return log_debug_errno(errno, "Failed to stat file: %m"); + r = stat_verify_regular(&st); + if (r < 0) + return log_debug_errno(r, "Not a regular file: %m"); + + r = pe_load_headers(fd, &dos_header, &pe_header); + if (r < 0) + return r; + + r = pe_load_sections(fd, dos_header, pe_header, §ions); + if (r < 0) + return r; + + certificate_table = pe_header_get_data_directory(pe_header, IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE); + if (!certificate_table) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "File lacks certificate table."); + + mdctx = EVP_MD_CTX_new(); + if (!mdctx) + return log_oom_debug(); + + if (EVP_DigestInit_ex(mdctx, md, NULL) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to allocate message digest."); + + /* Everything from beginning of file to CheckSum field in PE header */ + p = (uint64_t) dos_header->e_lfanew + + offsetof(PeHeader, optional.CheckSum); + r = hash_file(fd, mdctx, 0, p); + if (r < 0) + return r; + p += sizeof(le32_t); + + /* Everything between the CheckSum field and the Image Data Directory Entry for the Certification Table */ + q = (uint64_t) dos_header->e_lfanew + + PE_HEADER_OPTIONAL_FIELD_OFFSET(pe_header, DataDirectory[IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE]); + r = hash_file(fd, mdctx, p, q - p); + if (r < 0) + return r; + q += sizeof(IMAGE_DATA_DIRECTORY); + + /* The rest of the header + the section table */ + p = pe_header->optional.SizeOfHeaders; + if (p < q) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "SizeOfHeaders too short."); + r = hash_file(fd, mdctx, q, p - q); + if (r < 0) + return r; + + /* Sort by location in file */ + typesafe_qsort(sections, pe_header->pe.NumberOfSections, section_offset_cmp); + + FOREACH_ARRAY(section, sections, pe_header->pe.NumberOfSections) { + r = hash_file(fd, mdctx, section->PointerToRawData, section->SizeOfRawData); + if (r < 0) + return r; + + p += section->SizeOfRawData; + } + + if ((uint64_t) st.st_size > p) { + + if (st.st_size - p < certificate_table->Size) + return log_debug_errno(errno, "No space for certificate table, refusing."); + + r = hash_file(fd, mdctx, p, st.st_size - p - certificate_table->Size); + if (r < 0) + return r; + + /* If the file size is not a multiple of 8 bytes, pad the hash with zero bytes. */ + if (st.st_size % 8 != 0 && EVP_DigestUpdate(mdctx, (const uint8_t[8]) {}, 8 - (st.st_size % 8)) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to hash data."); + } + + int hsz = EVP_MD_CTX_size(mdctx); + if (hsz < 0) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to get hash size."); + + unsigned hash_size = (unsigned) hsz; + _cleanup_free_ void *hash = malloc(hsz); + if (!hash) + return log_oom_debug(); + + if (EVP_DigestFinal_ex(mdctx, hash, &hash_size) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize hash function."); + + assert(hash_size == (unsigned) hsz); + + *ret_hash = TAKE_PTR(hash); + *ret_hash_size = hash_size; + + return 0; +#else + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL is not supported, cannot calculate PE hash."); +#endif +} + +#if HAVE_OPENSSL +typedef void* SectionHashArray[_UNIFIED_SECTION_MAX]; + +static void section_hash_array_done(SectionHashArray *array) { + assert(array); + + for (size_t i = 0; i < _UNIFIED_SECTION_MAX; i++) + free((*array)[i]); +} +#endif + +int uki_hash(int fd, + const EVP_MD *md, + void* ret_hashes[static _UNIFIED_SECTION_MAX], + size_t *ret_hash_size) { +#if HAVE_OPENSSL + _cleanup_(section_hash_array_done) SectionHashArray hashes = {}; + _cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL; + _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL; + _cleanup_free_ PeHeader *pe_header = NULL; + int r; + + assert(fd >= 0); + assert(ret_hashes); + assert(ret_hash_size); + + r = pe_load_headers(fd, &dos_header, &pe_header); + if (r < 0) + return r; + + r = pe_load_sections(fd, dos_header, pe_header, §ions); + if (r < 0) + return r; + + int hsz = EVP_MD_size(md); + if (hsz < 0) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to get hash size."); + + FOREACH_ARRAY(section, sections, pe_header->pe.NumberOfSections) { + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mdctx = NULL; + _cleanup_free_ char *n = NULL; + ssize_t i; + + n = memdup_suffix0(section->Name, sizeof(section->Name)); + if (!n) + return log_oom_debug(); + + i = string_table_lookup(unified_sections, _UNIFIED_SECTION_MAX, n); + if (i < 0) + continue; + + if (hashes[i]) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate section"); + + mdctx = EVP_MD_CTX_new(); + if (!mdctx) + return log_oom_debug(); + + if (EVP_DigestInit_ex(mdctx, md, NULL) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to allocate message digest."); + + r = hash_file(fd, mdctx, section->PointerToRawData, MIN(section->VirtualSize, section->SizeOfRawData)); + if (r < 0) + return r; + + if (section->SizeOfRawData < section->VirtualSize) { + uint8_t zeroes[1024] = {}; + size_t remaining = section->VirtualSize - section->SizeOfRawData; + + while (remaining > 0) { + size_t sz = MIN(sizeof(zeroes), remaining); + + if (EVP_DigestUpdate(mdctx, zeroes, sz) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to hash data."); + + remaining -= sz; + } + } + + hashes[i] = malloc(hsz); + if (!hashes[i]) + return log_oom_debug(); + + unsigned hash_size = (unsigned) hsz; + if (EVP_DigestFinal_ex(mdctx, hashes[i], &hash_size) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize hash function."); + + assert(hash_size == (unsigned) hsz); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *hs = NULL; + + hs = hexmem(hashes[i], hsz); + log_debug("Section %s with %s is %s.", n, EVP_MD_name(md), strna(hs)); + } + } + + memcpy(ret_hashes, hashes, sizeof(hashes)); + zero(hashes); + *ret_hash_size = (unsigned) hsz; + + return 0; +#else + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL is not supported, cannot calculate UKI hash."); +#endif +} diff --git a/src/shared/pe-binary.h b/src/shared/pe-binary.h index f508989185..1b86b93c03 100644 --- a/src/shared/pe-binary.h +++ b/src/shared/pe-binary.h @@ -3,7 +3,10 @@ #include +#include "openssl-util.h" +#include "macro-fundamental.h" #include "sparse-endian.h" +#include "uki.h" /* When naming things we try to stay close to the official Windows APIs as per: * → https://learn.microsoft.com/en-us/windows/win32/debug/pe-format */ @@ -147,3 +150,7 @@ bool pe_is_uki(const PeHeader *pe_header, const IMAGE_SECTION_HEADER *sections); bool pe_is_addon(const PeHeader *pe_header, const IMAGE_SECTION_HEADER *sections); bool pe_is_native(const PeHeader *pe_header); + +int pe_hash(int fd, const EVP_MD *md, void **ret_hash, size_t *ret_hash_size); + +int uki_hash(int fd, const EVP_MD *md, void *ret_hashes[static _UNIFIED_SECTION_MAX], size_t *ret_hash_size); From 0bf70b1984dbef2df289a99976165cb3c90a0781 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 5 Nov 2024 14:48:59 +0100 Subject: [PATCH 3/7] openssl-util: Set default UI method instead of setting engine method While for engines we have ENGINE_ctrl() to set the UI method for the second PIN prompt, for openssl providers we don't have such a feature which means we get the default openssl UI for the second pin prompt. Instead, let's set the default UI method which does get used for the second pin prompt by the pkcs11 provider. --- src/boot/measure.c | 2 +- src/shared/openssl-util.c | 163 ++++++++++++++++++-------------------- src/shared/openssl-util.h | 28 +------ 3 files changed, 78 insertions(+), 115 deletions(-) diff --git a/src/boot/measure.c b/src/boot/measure.c index 3c409f8bd9..a1c05a0f72 100644 --- a/src/boot/measure.c +++ b/src/boot/measure.c @@ -834,7 +834,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { /* When signing we only support JSON output */ arg_json_format_flags &= ~SD_JSON_FORMAT_OFF; - /* This must be done before openssl_load_key_from_token() otherwise it will get stuck */ + /* This must be done before openssl_load_private_key() otherwise it will get stuck */ if (arg_certificate) { r = openssl_load_x509_certificate(arg_certificate, &certificate); if (r < 0) diff --git a/src/shared/openssl-util.c b/src/shared/openssl-util.c index 818f4a5f19..5688d54114 100644 --- a/src/shared/openssl-util.c +++ b/src/shared/openssl-util.c @@ -24,6 +24,8 @@ DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(ENGINE*, ENGINE_free, NULL); REENABLE_WARNING; # endif +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(UI_METHOD*, UI_destroy_method, NULL); + /* For each error in the OpenSSL thread error queue, log the provided message and the OpenSSL error * string. If there are no errors in the OpenSSL thread queue, this logs the message with "No OpenSSL * errors." This logs at level debug. Returns -EIO (or -ENOMEM). */ @@ -1315,12 +1317,10 @@ int pkey_generate_volume_keys( static int load_key_from_provider( const char *provider, const char *private_key_uri, - OpenSSLAskPasswordUI *ui, EVP_PKEY **ret) { assert(provider); assert(private_key_uri); - assert(ui); assert(ret); #if OPENSSL_VERSION_MAJOR >= 3 @@ -1333,8 +1333,8 @@ static int load_key_from_provider( _cleanup_(OSSL_STORE_closep) OSSL_STORE_CTX *store = OSSL_STORE_open( private_key_uri, - ui->method, - &ui->request, + /*ui_method=*/ NULL, + /*ui_method=*/ NULL, /* post_process= */ NULL, /* post_process_data= */ NULL); if (!store) @@ -1356,10 +1356,9 @@ static int load_key_from_provider( #endif } -static int load_key_from_engine(const char *engine, const char *private_key_uri, OpenSSLAskPasswordUI *ui, EVP_PKEY **ret) { +static int load_key_from_engine(const char *engine, const char *private_key_uri, EVP_PKEY **ret) { assert(engine); assert(private_key_uri); - assert(ui); assert(ret); #if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0) @@ -1371,13 +1370,7 @@ static int load_key_from_engine(const char *engine, const char *private_key_uri, if (ENGINE_init(e) == 0) return log_openssl_errors("Failed to initialize signing engine '%s'", engine); - if (ENGINE_ctrl(e, ENGINE_CTRL_SET_USER_INTERFACE, /*i=*/ 0, ui->method, /*f=*/ NULL) <= 0) - return log_openssl_errors("Failed to set engine user interface"); - - if (ENGINE_ctrl(e, ENGINE_CTRL_SET_CALLBACK_DATA, /*i=*/ 0, &ui->request, /*f=*/ NULL) <= 0) - return log_openssl_errors("Failed to set engine user interface data"); - - _cleanup_(EVP_PKEY_freep) EVP_PKEY *private_key = ENGINE_load_private_key(e, private_key_uri, ui->method, &ui->request); + _cleanup_(EVP_PKEY_freep) EVP_PKEY *private_key = ENGINE_load_private_key(e, private_key_uri, /*ui_method=*/ NULL, /*callback_data=*/ NULL); if (!private_key) return log_openssl_errors("Failed to load private key from '%s'", private_key_uri); REENABLE_WARNING; @@ -1390,36 +1383,13 @@ static int load_key_from_engine(const char *engine, const char *private_key_uri, #endif } -int openssl_load_key_from_token( - KeySourceType private_key_source_type, - const char *private_key_source, - const char *private_key, - OpenSSLAskPasswordUI *ui, - EVP_PKEY **ret_private_key) { - - assert(IN_SET(private_key_source_type, OPENSSL_KEY_SOURCE_ENGINE, OPENSSL_KEY_SOURCE_PROVIDER)); - assert(private_key_source); - assert(ui); - assert(private_key); - - switch (private_key_source_type) { - - case OPENSSL_KEY_SOURCE_ENGINE: - return load_key_from_engine(private_key_source, private_key, ui, ret_private_key); - case OPENSSL_KEY_SOURCE_PROVIDER: - return load_key_from_provider(private_key_source, private_key, ui, ret_private_key); - default: - assert_not_reached(); - } -} - static int openssl_ask_password_ui_read(UI *ui, UI_STRING *uis) { int r; switch(UI_get_string_type(uis)) { case UIT_PROMPT: { /* If no ask password request was configured use the default openssl UI. */ - AskPasswordRequest *req = UI_get0_user_data(ui); + AskPasswordRequest *req = (AskPasswordRequest*) UI_method_get_ex_data(UI_get_method(ui), 0); if (!req) return (UI_method_get_reader(UI_OpenSSL()))(ui, uis); @@ -1448,10 +1418,41 @@ static int openssl_ask_password_ui_read(UI *ui, UI_STRING *uis) { return (UI_method_get_reader(UI_OpenSSL()))(ui, uis); } } -#endif -int openssl_ask_password_ui_new(OpenSSLAskPasswordUI **ret) { -#if HAVE_OPENSSL +static int openssl_load_private_key_from_file(const char *path, EVP_PKEY **ret) { + _cleanup_(erase_and_freep) char *rawkey = NULL; + _cleanup_(BIO_freep) BIO *kb = NULL; + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pk = NULL; + size_t rawkeysz; + int r; + + assert(path); + assert(ret); + + r = read_full_file_full( + AT_FDCWD, path, UINT64_MAX, SIZE_MAX, + READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, + NULL, + &rawkey, &rawkeysz); + if (r < 0) + return log_debug_errno(r, "Failed to read key file '%s': %m", path); + + kb = BIO_new_mem_buf(rawkey, rawkeysz); + if (!kb) + return log_oom_debug(); + + pk = PEM_read_bio_PrivateKey(kb, NULL, NULL, NULL); + if (!pk) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse PEM private key: %s", + ERR_error_string(ERR_get_error(), NULL)); + + if (ret) + *ret = TAKE_PTR(pk); + + return 0; +} + +static int openssl_ask_password_ui_new(const AskPasswordRequest *request, OpenSSLAskPasswordUI **ret) { assert(ret); _cleanup_(UI_destroy_methodp) UI_METHOD *method = UI_create_method("systemd-ask-password"); @@ -1467,12 +1468,31 @@ int openssl_ask_password_ui_new(OpenSSLAskPasswordUI **ret) { *ui = (OpenSSLAskPasswordUI) { .method = TAKE_PTR(method), + .request = *request, }; + UI_set_default_method(ui->method); + + if (UI_method_set_ex_data(ui->method, 0, &ui->request) == 0) + return log_openssl_errors("Failed to set extra data for UI method"); + *ret = TAKE_PTR(ui); return 0; +} +#endif + +OpenSSLAskPasswordUI* openssl_ask_password_ui_free(OpenSSLAskPasswordUI *ui) { +#if HAVE_OPENSSL + if (!ui) + return NULL; + + assert(UI_get_default_method() == ui->method); + UI_set_default_method(UI_OpenSSL()); + UI_destroy_method(ui->method); + return mfree(ui); #else - return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL is not supported, cannot create ask-password user interface."); + assert(ui == NULL); + return NULL; #endif } @@ -1531,43 +1551,6 @@ int openssl_load_x509_certificate(const char *path, X509 **ret) { #endif } -static int openssl_load_private_key_from_file(const char *path, EVP_PKEY **ret) { -#if HAVE_OPENSSL - _cleanup_(erase_and_freep) char *rawkey = NULL; - _cleanup_(BIO_freep) BIO *kb = NULL; - _cleanup_(EVP_PKEY_freep) EVP_PKEY *pk = NULL; - size_t rawkeysz; - int r; - - assert(path); - assert(ret); - - r = read_full_file_full( - AT_FDCWD, path, UINT64_MAX, SIZE_MAX, - READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, - NULL, - &rawkey, &rawkeysz); - if (r < 0) - return log_debug_errno(r, "Failed to read key file '%s': %m", path); - - kb = BIO_new_mem_buf(rawkey, rawkeysz); - if (!kb) - return log_oom_debug(); - - pk = PEM_read_bio_PrivateKey(kb, NULL, NULL, NULL); - if (!pk) - return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse PEM private key: %s", - ERR_error_string(ERR_get_error(), NULL)); - - if (ret) - *ret = TAKE_PTR(pk); - - return 0; -#else - return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL is not supported, cannot load private key."); -#endif -} - int openssl_load_private_key( KeySourceType private_key_source_type, const char *private_key_source, @@ -1575,7 +1558,7 @@ int openssl_load_private_key( const AskPasswordRequest *request, EVP_PKEY **ret_private_key, OpenSSLAskPasswordUI **ret_user_interface) { - +#if HAVE_OPENSSL int r; assert(private_key); @@ -1589,18 +1572,21 @@ int openssl_load_private_key( *ret_user_interface = NULL; } else { _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; - r = openssl_ask_password_ui_new(&ui); + r = openssl_ask_password_ui_new(request, &ui); if (r < 0) return log_debug_errno(r, "Failed to allocate ask-password user interface: %m"); - ui->request = *request; + switch (private_key_source_type) { - r = openssl_load_key_from_token( - private_key_source_type, - private_key_source, - private_key, - ui, - ret_private_key); + case OPENSSL_KEY_SOURCE_ENGINE: + r = load_key_from_engine(private_key_source, private_key, ret_private_key); + break; + case OPENSSL_KEY_SOURCE_PROVIDER: + r = load_key_from_provider(private_key_source, private_key, ret_private_key); + break; + default: + assert_not_reached(); + } if (r < 0) return log_debug_errno( r, @@ -1612,6 +1598,9 @@ int openssl_load_private_key( } return 0; +#else + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL is not supported, cannot load private key."); +#endif } int parse_openssl_key_source_argument( diff --git a/src/shared/openssl-util.h b/src/shared/openssl-util.h index f70de86a09..f7087eb392 100644 --- a/src/shared/openssl-util.h +++ b/src/shared/openssl-util.h @@ -134,8 +134,6 @@ int pubkey_fingerprint(EVP_PKEY *pk, const EVP_MD *md, void **ret, size_t *ret_s int digest_and_sign(const EVP_MD *md, EVP_PKEY *privkey, const void *data, size_t size, void **ret, size_t *ret_size); -int openssl_load_key_from_token(KeySourceType private_key_source_type, const char *private_key_source, const char *private_key, OpenSSLAskPasswordUI *ui, EVP_PKEY **ret_private_key); - #else typedef struct X509 X509; @@ -153,41 +151,17 @@ static inline void *EVP_PKEY_free(EVP_PKEY *p) { return NULL; } -static inline void* UI_destroy_method(UI_METHOD *p) { - assert(p == NULL); - return NULL; -} - -static inline int openssl_load_key_from_token( - KeySourceType private_key_source_type, - const char *private_key_source, - const char *private_key, - OpenSSLAskPasswordUI *ui, - EVP_PKEY **ret_private_key) { - - return -EOPNOTSUPP; -} - #endif DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(X509*, X509_free, NULL); DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(EVP_PKEY*, EVP_PKEY_free, NULL); -DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(UI_METHOD*, UI_destroy_method, NULL); struct OpenSSLAskPasswordUI { AskPasswordRequest request; UI_METHOD *method; }; -int openssl_ask_password_ui_new(OpenSSLAskPasswordUI **ret); - -static inline OpenSSLAskPasswordUI* openssl_ask_password_ui_free(OpenSSLAskPasswordUI *ui) { - if (!ui) - return NULL; - - UI_destroy_method(ui->method); - return mfree(ui); -} +OpenSSLAskPasswordUI* openssl_ask_password_ui_free(OpenSSLAskPasswordUI *ui); DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(OpenSSLAskPasswordUI*, openssl_ask_password_ui_free, NULL); From 5f163921e9ff6d735798db259c47543822f81b5c Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 5 Nov 2024 00:36:32 +0100 Subject: [PATCH 4/7] Introduce systemd-sbsign to do secure boot signing Currently in mkosi and ukify we use sbsigntools to do secure boot signing. This has multiple issues: - sbsigntools is practically unmaintained, sbvarsign is completely broken with the latest gnu-efi when built without -fshort-wchar and upstream has completely ignored my bug report about this. - sbsigntools only supports openssl engines and not the new providers API. - sbsigntools doesn't allow us to cache hardware token pins in the kernel keyring like we do nowadays when we sign stuff ourselves in systemd-repart or systemd-measure There are alternative tools like sbctl and pesign but these do not support caching hardware token pins in the kernel keyring either. To get around the issues with sbsigntools, let's introduce our own tool systemd-sbsign to do secure boot signing. This allows us to take advantage of our own openssl infra so that hardware token pins are cached in the kernel keyring as expected and we get openssl provider support as well. --- man/systemd-sbsign.xml | 95 +++++ src/boot/authenticode.h | 135 +++++++ src/boot/meson.build | 8 + src/boot/sbsign.c | 488 +++++++++++++++++++++++++ src/shared/openssl-util.h | 19 +- src/shared/pe-binary.c | 51 ++- src/shared/pe-binary.h | 4 + test/units/TEST-74-AUX-UTILS.sbsign.sh | 56 +++ 8 files changed, 852 insertions(+), 4 deletions(-) create mode 100644 man/systemd-sbsign.xml create mode 100644 src/boot/authenticode.h create mode 100644 src/boot/sbsign.c create mode 100755 test/units/TEST-74-AUX-UTILS.sbsign.sh diff --git a/man/systemd-sbsign.xml b/man/systemd-sbsign.xml new file mode 100644 index 0000000000..d7095e821c --- /dev/null +++ b/man/systemd-sbsign.xml @@ -0,0 +1,95 @@ + + + + + + + systemd-sbsign + systemd + + + + systemd-sbsign + 1 + + + + systemd-sbsign + Sign PE binaries for EFI Secure Boot + + + + + systemd-sbsign + OPTIONS + COMMAND + + + + + Description + + systemd-sbsign can be used to sign PE binaries for EFI Secure Boot. + + + + Commands + + + + + + Signs the given PE binary for EFI Secure Boot. Takes a path to a PE binary as its + argument. If the PE binary already has a certificate table, the new signature will be added to it. + Otherwise a new certificate table will be created. The signed PE binary will be written to the path + specified with . + + + + + + + + + Options + The following options are understood: + + + + + + Specifies the path where to write the signed PE binary. + + + + + + + + + + Set the Secure Boot private key and certificate for use with the + sign. The option takes a path to a PEM encoded + X.509 certificate. The option can take a path or a URI that will be + passed to the OpenSSL engine or provider, as specified by as a + type:name tuple, such as engine:pkcs11. The specified OpenSSL + signing engine or provider will be used to sign the PE binary. + + + + + + + + + + + + See Also + + bootctl1 + + + diff --git a/src/boot/authenticode.h b/src/boot/authenticode.h new file mode 100644 index 0000000000..98a8374805 --- /dev/null +++ b/src/boot/authenticode.h @@ -0,0 +1,135 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "macro.h" + +#define SPC_INDIRECT_DATA_OBJID "1.3.6.1.4.1.311.2.1.4" +#define SPC_PE_IMAGE_DATA_OBJID "1.3.6.1.4.1.311.2.1.15" + +typedef struct { + ASN1_OBJECT *type; + ASN1_TYPE *value; +} SpcAttributeTypeAndOptionalValue; + +DECLARE_ASN1_FUNCTIONS(SpcAttributeTypeAndOptionalValue); + +ASN1_SEQUENCE(SpcAttributeTypeAndOptionalValue) = { + ASN1_SIMPLE(SpcAttributeTypeAndOptionalValue, type, ASN1_OBJECT), + ASN1_OPT(SpcAttributeTypeAndOptionalValue, value, ASN1_ANY) +} ASN1_SEQUENCE_END(SpcAttributeTypeAndOptionalValue); + +IMPLEMENT_ASN1_FUNCTIONS(SpcAttributeTypeAndOptionalValue); + +typedef struct { + ASN1_OBJECT *algorithm; + ASN1_TYPE *parameters; +} AlgorithmIdentifier; + +DECLARE_ASN1_FUNCTIONS(AlgorithmIdentifier); + +ASN1_SEQUENCE(AlgorithmIdentifier) = { + ASN1_SIMPLE(AlgorithmIdentifier, algorithm, ASN1_OBJECT), + ASN1_OPT(AlgorithmIdentifier, parameters, ASN1_ANY) +} ASN1_SEQUENCE_END(AlgorithmIdentifier) + +IMPLEMENT_ASN1_FUNCTIONS(AlgorithmIdentifier); + +typedef struct { + AlgorithmIdentifier *digestAlgorithm; + ASN1_OCTET_STRING *digest; +} DigestInfo; + +DECLARE_ASN1_FUNCTIONS(DigestInfo); + +ASN1_SEQUENCE(DigestInfo) = { + ASN1_SIMPLE(DigestInfo, digestAlgorithm, AlgorithmIdentifier), + ASN1_SIMPLE(DigestInfo, digest, ASN1_OCTET_STRING) +} ASN1_SEQUENCE_END(DigestInfo); + +IMPLEMENT_ASN1_FUNCTIONS(DigestInfo); + +typedef struct { + SpcAttributeTypeAndOptionalValue *data; + DigestInfo *messageDigest; +} SpcIndirectDataContent; + +DECLARE_ASN1_FUNCTIONS(SpcIndirectDataContent); + +ASN1_SEQUENCE(SpcIndirectDataContent) = { + ASN1_SIMPLE(SpcIndirectDataContent, data, SpcAttributeTypeAndOptionalValue), + ASN1_SIMPLE(SpcIndirectDataContent, messageDigest, DigestInfo) +} ASN1_SEQUENCE_END(SpcIndirectDataContent); + +IMPLEMENT_ASN1_FUNCTIONS(SpcIndirectDataContent); + +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(SpcIndirectDataContent*, SpcIndirectDataContent_free, NULL); + +typedef struct { + int type; + union { + ASN1_BMPSTRING *unicode; + ASN1_IA5STRING *ascii; + } value; +} SpcString; + +DECLARE_ASN1_FUNCTIONS(SpcString); + +ASN1_CHOICE(SpcString) = { + ASN1_IMP_OPT(SpcString, value.unicode, ASN1_BMPSTRING, 0), + ASN1_IMP_OPT(SpcString, value.ascii, ASN1_IA5STRING, 1) +} ASN1_CHOICE_END(SpcString); + +IMPLEMENT_ASN1_FUNCTIONS(SpcString); + +typedef struct { + ASN1_OCTET_STRING *classId; + ASN1_OCTET_STRING *serializedData; +} SpcSerializedObject; + +DECLARE_ASN1_FUNCTIONS(SpcSerializedObject); + +ASN1_SEQUENCE(SpcSerializedObject) = { + ASN1_SIMPLE(SpcSerializedObject, classId, ASN1_OCTET_STRING), + ASN1_SIMPLE(SpcSerializedObject, serializedData, ASN1_OCTET_STRING) +} ASN1_SEQUENCE_END(SpcSerializedObject); + +IMPLEMENT_ASN1_FUNCTIONS(SpcSerializedObject); + +typedef struct { + int type; + union { + ASN1_IA5STRING *url; + SpcSerializedObject *moniker; + SpcString *file; + } value; +} SpcLink; + +DECLARE_ASN1_FUNCTIONS(SpcLink); + +ASN1_CHOICE(SpcLink) = { + ASN1_IMP_OPT(SpcLink, value.url, ASN1_IA5STRING, 0), + ASN1_IMP_OPT(SpcLink, value.moniker, SpcSerializedObject, 1), + ASN1_EXP_OPT(SpcLink, value.file, SpcString, 2) +} ASN1_CHOICE_END(SpcLink); + +IMPLEMENT_ASN1_FUNCTIONS(SpcLink); + +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(SpcLink*, SpcLink_free, NULL); + +typedef struct { + ASN1_BIT_STRING *flags; + SpcLink *file; +} SpcPeImageData; + +DECLARE_ASN1_FUNCTIONS(SpcPeImageData); + +ASN1_SEQUENCE(SpcPeImageData) = { + ASN1_SIMPLE(SpcPeImageData, flags, ASN1_BIT_STRING), + ASN1_EXP_OPT(SpcPeImageData, file, SpcLink, 0) +} ASN1_SEQUENCE_END(SpcPeImageData) + +IMPLEMENT_ASN1_FUNCTIONS(SpcPeImageData); + +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(SpcPeImageData*, SpcPeImageData_free, NULL); diff --git a/src/boot/meson.build b/src/boot/meson.build index 9c28c623d4..a72388baff 100644 --- a/src/boot/meson.build +++ b/src/boot/meson.build @@ -62,6 +62,14 @@ executables += [ 'sources' : files('measure.c'), 'dependencies' : libopenssl, }, + libexec_template + { + 'name' : 'systemd-sbsign', + 'conditions' : [ + 'HAVE_OPENSSL', + ], + 'sources' : files('sbsign.c'), + 'dependencies' : libopenssl, + }, libexec_template + { 'name' : 'systemd-boot-check-no-failures', 'sources' : files('boot-check-no-failures.c'), diff --git a/src/boot/sbsign.c b/src/boot/sbsign.c new file mode 100644 index 0000000000..2cddc62905 --- /dev/null +++ b/src/boot/sbsign.c @@ -0,0 +1,488 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "ansi-color.h" +#include "authenticode.h" +#include "build.h" +#include "copy.h" +#include "efi-fundamental.h" +#include "fd-util.h" +#include "log.h" +#include "main-func.h" +#include "openssl-util.h" +#include "parse-argument.h" +#include "pe-binary.h" +#include "pretty-print.h" +#include "stat-util.h" +#include "tmpfile-util.h" +#include "verbs.h" + +static PagerFlags arg_pager_flags = 0; +static char *arg_output = NULL; +static char *arg_certificate = NULL; +static char *arg_private_key = NULL; +static KeySourceType arg_private_key_source_type = OPENSSL_KEY_SOURCE_FILE; +static char *arg_private_key_source = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_output, freep); +STATIC_DESTRUCTOR_REGISTER(arg_certificate, freep); +STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep); +STATIC_DESTRUCTOR_REGISTER(arg_private_key_source, freep); + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-sbsign", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] COMMAND ...\n" + "\n%5$sSign binaries for EFI Secure Boot%6$s\n" + "\n%3$sCommands:%4$s\n" + " sign EXEFILE Sign the given binary for EFI Secure Boot\n" + "\n%3$sOptions:%4$s\n" + " -h --help Show this help\n" + " --version Print version\n" + " --no-pager Do not pipe output into a pager\n" + " --output Where to write the signed PE binary\n" + " --certificate=PATH PEM certificate to use when signing with a URI\n" + " --private-key=KEY Private key (PEM) to sign with\n" + " --private-key-source=file|provider:PROVIDER|engine:ENGINE\n" + " Specify how to use KEY for --private-key=. Allows\n" + " an OpenSSL engine/provider to be used for signing\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), + ansi_normal(), + ansi_highlight(), + ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_OUTPUT, + ARG_CERTIFICATE, + ARG_PRIVATE_KEY, + ARG_PRIVATE_KEY_SOURCE, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "version", no_argument, NULL, ARG_VERSION }, + { "output", required_argument, NULL, ARG_OUTPUT }, + { "certificate", required_argument, NULL, ARG_CERTIFICATE }, + { "private-key", required_argument, NULL, ARG_PRIVATE_KEY }, + { "private-key-source", required_argument, NULL, ARG_PRIVATE_KEY_SOURCE }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hjc", options, NULL)) >= 0) + switch (c) { + + case 'h': + help(0, NULL, NULL); + return 0; + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_OUTPUT: + r = parse_path_argument(optarg, /*suppress_root=*/ false, &arg_output); + if (r < 0) + return r; + + break; + + case ARG_CERTIFICATE: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_certificate); + if (r < 0) + return r; + + break; + + case ARG_PRIVATE_KEY: + r = free_and_strdup_warn(&arg_private_key, optarg); + if (r < 0) + return r; + + break; + + case ARG_PRIVATE_KEY_SOURCE: + r = parse_openssl_key_source_argument( + optarg, + &arg_private_key_source, + &arg_private_key_source_type); + if (r < 0) + return r; + + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (arg_private_key_source && !arg_certificate) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "When using --private-key-source=, --certificate= must be specified."); + + return 1; +} + +static int verb_sign(int argc, char *argv[], void *userdata) { + _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; + _cleanup_(EVP_PKEY_freep) EVP_PKEY *private_key = NULL; + _cleanup_(X509_freep) X509 *certificate = NULL; + int r; + + if (argc < 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No input file specified"); + + if (!arg_certificate) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "No certificate specified, use --certificate="); + + if (!arg_private_key) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "No private key specified, use --private-key=."); + + if (!arg_output) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No output specified, use --output="); + + r = openssl_load_x509_certificate(arg_certificate, &certificate); + if (r < 0) + return log_error_errno(r, "Failed to load X.509 certificate from %s: %m", arg_certificate); + + if (arg_private_key_source_type == OPENSSL_KEY_SOURCE_FILE) { + r = parse_path_argument(arg_private_key, /* suppress_root= */ false, &arg_private_key); + if (r < 0) + return log_error_errno(r, "Failed to parse private key path %s: %m", arg_private_key); + } + + r = openssl_load_private_key( + arg_private_key_source_type, + arg_private_key_source, + arg_private_key, + &(AskPasswordRequest) { + .id = "sbsign-private-key-pin", + .keyring = arg_private_key, + .credential = "sbsign.private-key-pin", + }, + &private_key, + &ui); + if (r < 0) + return log_error_errno(r, "Failed to load private key from %s: %m", arg_private_key); + + _cleanup_(PKCS7_freep) PKCS7 *p7 = NULL; + p7 = PKCS7_sign(certificate, private_key, /*certs=*/ NULL, /*data=*/ NULL, PKCS7_BINARY|PKCS7_PARTIAL); + if (!p7) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate pkcs7 signing context: %s", + ERR_error_string(ERR_get_error(), NULL)); + + STACK_OF(PKCS7_SIGNER_INFO) *si_stack = PKCS7_get_signer_info(p7); + if (!si_stack) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get pkcs7 signer info stack: %s", + ERR_error_string(ERR_get_error(), NULL)); + + PKCS7_SIGNER_INFO *si = sk_PKCS7_SIGNER_INFO_value(si_stack, 0); + if (!si) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get pkcs7 signer info: %s", + ERR_error_string(ERR_get_error(), NULL)); + + int idcnid = OBJ_create(SPC_INDIRECT_DATA_OBJID, "spcIndirectDataContext", "Indirect Data Context"); + + if (PKCS7_add_signed_attribute(si, NID_pkcs9_contentType, V_ASN1_OBJECT, OBJ_nid2obj(idcnid)) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to add signed attribute to pkcs7 signer info: %s", + ERR_error_string(ERR_get_error(), NULL)); + + _cleanup_close_ int srcfd = open(argv[1], O_RDONLY|O_CLOEXEC); + if (srcfd < 0) + return log_error_errno(errno, "Failed to open %s: %m", argv[1]); + + struct stat st; + if (fstat(srcfd, &st) < 0) + return log_debug_errno(errno, "Failed to stat %s: %m", argv[1]); + + r = stat_verify_regular(&st); + if (r < 0) + return log_debug_errno(r, "%s is not a regular file: %m", argv[1]); + + _cleanup_(unlink_and_freep) char *tmp = NULL; + _cleanup_close_ int dstfd = open_tmpfile_linkable(arg_output, O_RDWR|O_CLOEXEC, &tmp); + if (dstfd < 0) + return log_error_errno(r, "Failed to open temporary file: %m"); + + r = copy_bytes(srcfd, dstfd, UINT64_MAX, COPY_REFLINK); + if (r < 0) + return log_error_errno(r, "Failed to copy %s to %s: %m", argv[1], tmp); + + _cleanup_free_ void *hash = NULL; + size_t hashsz; + r = pe_hash(dstfd, EVP_sha256(), &hash, &hashsz); + if (r < 0) + return log_error_errno(r, "Failed to hash PE binary %s: %m", argv[0]); + + /* <<>> in unicode bytes. */ + static const uint8_t obsolete[] = { + 0x00, 0x3c, 0x00, 0x3c, 0x00, 0x3c, 0x00, 0x4f, + 0x00, 0x62, 0x00, 0x73, 0x00, 0x6f, 0x00, 0x6c, + 0x00, 0x65, 0x00, 0x74, 0x00, 0x65, 0x00, 0x3e, + 0x00, 0x3e, 0x00, 0x3e + }; + + _cleanup_(SpcLink_freep) SpcLink *link = SpcLink_new(); + if (!link) + return log_oom(); + + link->type = 2; + link->value.file = SpcString_new(); + if (!link->value.file) + return log_oom(); + + link->value.file->type = 0; + link->value.file->value.unicode = ASN1_BMPSTRING_new(); + if (!link->value.file->value.unicode) + return log_oom(); + + if (ASN1_STRING_set(link->value.file->value.unicode, obsolete, sizeof(obsolete)) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set ASN1 string: %s", + ERR_error_string(ERR_get_error(), NULL)); + + _cleanup_(SpcPeImageData_freep) SpcPeImageData *peid = SpcPeImageData_new(); + if (!peid) + return log_oom(); + + if (ASN1_BIT_STRING_set_bit(peid->flags, 0, 1) == 0) + return log_oom(); + + peid->file = TAKE_PTR(link); + + _cleanup_free_ uint8_t *peidraw = NULL; + int peidrawsz = i2d_SpcPeImageData(peid, &peidraw); + if (peidrawsz < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert SpcPeImageData to BER: %s", + ERR_error_string(ERR_get_error(), NULL)); + + _cleanup_(SpcIndirectDataContent_freep) SpcIndirectDataContent *idc = SpcIndirectDataContent_new(); + idc->data->value = ASN1_TYPE_new(); + if (!idc->data->value) + return log_oom(); + + idc->data->value->type = V_ASN1_SEQUENCE; + idc->data->value->value.sequence = ASN1_STRING_new(); + if (!idc->data->value->value.sequence) + return log_oom(); + + idc->data->type = OBJ_txt2obj(SPC_PE_IMAGE_DATA_OBJID, /*no_name=*/ 1); + if (!idc->data->type) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get SpcPeImageData object: %s", + ERR_error_string(ERR_get_error(), NULL)); + + idc->data->value->value.sequence->data = TAKE_PTR(peidraw); + idc->data->value->value.sequence->length = peidrawsz; + idc->messageDigest->digestAlgorithm->algorithm = OBJ_nid2obj(NID_sha256); + if (!idc->messageDigest->digestAlgorithm->algorithm) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get SHA256 object: %s", + ERR_error_string(ERR_get_error(), NULL)); + + idc->messageDigest->digestAlgorithm->parameters = ASN1_TYPE_new(); + if (!idc->messageDigest->digestAlgorithm->parameters) + return log_oom(); + + idc->messageDigest->digestAlgorithm->parameters->type = V_ASN1_NULL; + + if (ASN1_OCTET_STRING_set(idc->messageDigest->digest, hash, hashsz) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set digest: %s", + ERR_error_string(ERR_get_error(), NULL)); + + _cleanup_free_ uint8_t *idcraw = NULL; + int idcrawsz = i2d_SpcIndirectDataContent(idc, &idcraw); + if (idcrawsz < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert SpcIndirectDataContent to BER: %s", + ERR_error_string(ERR_get_error(), NULL)); + + _cleanup_(BIO_free_allp) BIO *bio = PKCS7_dataInit(p7, NULL); + if (!bio) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to create PKCS7 data bio: %s", + ERR_error_string(ERR_get_error(), NULL)); + + int tag, class; + long psz; + const uint8_t *p = idcraw; + + /* This function weirdly enough reports errors by setting the 0x80 bit in its return value. */ + if (ASN1_get_object(&p, &psz, &tag, &class, idcrawsz) & 0x80) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse ASN.1 object: %s", + ERR_error_string(ERR_get_error(), NULL)); + + if (BIO_write(bio, p, psz) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write to PKCS7 data bio: %s", + ERR_error_string(ERR_get_error(), NULL)); + + if (PKCS7_final(p7, bio, PKCS7_BINARY) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to sign data: %s", + ERR_error_string(ERR_get_error(), NULL)); + + _cleanup_(PKCS7_freep) PKCS7 *p7c = PKCS7_new(); + if (!p7c) + return log_oom(); + + p7c->type = OBJ_nid2obj(idcnid); + if (!p7c->type) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get SpcIndirectDataContent object: %s", + ERR_error_string(ERR_get_error(), NULL)); + + p7c->d.other = ASN1_TYPE_new(); + if (!p7c->d.other) + return log_oom(); + + p7c->d.other->type = V_ASN1_SEQUENCE; + p7c->d.other->value.sequence = ASN1_STRING_new(); + if (!p7c->d.other->value.sequence) + return log_oom(); + + if (ASN1_STRING_set(p7c->d.other->value.sequence, idcraw, idcrawsz) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set ASN1 string: %s", + ERR_error_string(ERR_get_error(), NULL)); + + if (PKCS7_set_content(p7, p7c) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set PKCS7 data: %s", + ERR_error_string(ERR_get_error(), NULL)); + + TAKE_PTR(p7c); + + _cleanup_free_ uint8_t *sig = NULL; + int sigsz = i2d_PKCS7(p7, &sig); + if (sigsz < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert PKCS7 signature to DER: %s", + ERR_error_string(ERR_get_error(), NULL)); + + _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL; + _cleanup_free_ PeHeader *pe_header = NULL; + r = pe_load_headers(srcfd, &dos_header, &pe_header); + if (r < 0) + return log_error_errno(r, "Failed to load headers from PE file: %m"); + + const IMAGE_DATA_DIRECTORY *certificate_table; + certificate_table = pe_header_get_data_directory(pe_header, IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE); + if (!certificate_table) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "File lacks certificate table."); + + off_t end = st.st_size; + ssize_t n; + + if (st.st_size % 8 != 0) { + if (certificate_table->VirtualAddress != 0) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Certificate table is not aligned to 8 bytes"); + + n = pwrite(dstfd, (const uint8_t[8]) {}, 8 - (st.st_size % 8), st.st_size); + if (n < 0) + return log_error_errno(errno, "Failed to write zero padding: %m"); + if (n != 8 - (st.st_size % 8)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while writing zero padding."); + + end += n; + } + + uint32_t certsz = offsetof(WIN_CERTIFICATE, bCertificate) + sigsz; + n = pwrite(dstfd, + &(WIN_CERTIFICATE) { + .wRevision = htole16(0x200), + .wCertificateType = htole16(0x0002), /* PKCS7 signedData */ + .dwLength = htole32(ROUND_UP(certsz, 8)), + }, + sizeof(WIN_CERTIFICATE), + end); + if (n < 0) + return log_error_errno(errno, "Failed to write certificate header: %m"); + if (n != sizeof(WIN_CERTIFICATE)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while writing certificate header."); + + end += n; + + n = pwrite(dstfd, sig, sigsz, end); + if (n < 0) + return log_error_errno(errno, "Failed to write signature: %m"); + if (n != sigsz) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while writing signature."); + + end += n; + + if (certsz % 8 != 0) { + n = pwrite(dstfd, (const uint8_t[8]) {}, 8 - (certsz % 8), end); + if (n < 0) + return log_error_errno(errno, "Failed to write zero padding: %m"); + if ((size_t) n != 8 - (certsz % 8)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while writing zero padding."); + } + + n = pwrite(dstfd, + &(IMAGE_DATA_DIRECTORY) { + .VirtualAddress = certificate_table->VirtualAddress ?: htole32(ROUND_UP(st.st_size, 8)), + .Size = htole32(le32toh(certificate_table->Size) + ROUND_UP(certsz, 8)), + }, + sizeof(IMAGE_DATA_DIRECTORY), + le32toh(dos_header->e_lfanew) + PE_HEADER_OPTIONAL_FIELD_OFFSET(pe_header, DataDirectory[IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE])); + if (n < 0) + return log_error_errno(errno, "Failed to update PE certificate table: %m"); + if ((size_t) n != sizeof(IMAGE_DATA_DIRECTORY)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while updating PE certificate table."); + + uint32_t checksum; + r = pe_checksum(dstfd, &checksum); + if (r < 0) + return log_error_errno(r, "Failed to calculate PE file checksum: %m"); + + n = pwrite(dstfd, + &(le32_t) { htole32(checksum) }, + sizeof(le32_t), + le32toh(dos_header->e_lfanew) + offsetof(PeHeader, optional.CheckSum)); + if (n < 0) + return log_error_errno(errno, "Failed to update PE checksum: %m"); + if ((size_t) n != sizeof(le32_t)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while updating PE checksum."); + + r = link_tmpfile(dstfd, tmp, arg_output, LINK_TMPFILE_REPLACE|LINK_TMPFILE_SYNC); + if (r < 0) + return log_error_errno(r, "Failed to link temporary file to %s: %m", arg_output); + + log_info("Wrote signed PE binary to %s", arg_output); + return 0; +} + +static int run(int argc, char *argv[]) { + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "sign", 2, 2, 0, verb_sign }, + {} + }; + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/shared/openssl-util.h b/src/shared/openssl-util.h index f7087eb392..853aded2c7 100644 --- a/src/shared/openssl-util.h +++ b/src/shared/openssl-util.h @@ -55,6 +55,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(ECDSA_SIG*, ECDSA_SIG_free, NULL); DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(PKCS7*, PKCS7_free, NULL); DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(SSL*, SSL_free, NULL); DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(BIO*, BIO_free, NULL); +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(BIO*, BIO_free_all, NULL); DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(EVP_MD_CTX*, EVP_MD_CTX_free, NULL); DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(ASN1_OCTET_STRING*, ASN1_OCTET_STRING_free, NULL); @@ -140,13 +141,25 @@ typedef struct X509 X509; typedef struct EVP_PKEY EVP_PKEY; typedef struct EVP_MD EVP_MD; typedef struct UI_METHOD UI_METHOD; +typedef struct ASN1_TYPE ASN1_TYPE; +typedef struct ASN1_STRING ASN1_STRING; -static inline void *X509_free(X509 *p) { +static inline void* X509_free(X509 *p) { assert(p == NULL); return NULL; } -static inline void *EVP_PKEY_free(EVP_PKEY *p) { +static inline void* EVP_PKEY_free(EVP_PKEY *p) { + assert(p == NULL); + return NULL; +} + +static inline void* ASN1_TYPE_free(ASN1_TYPE *p) { + assert(p == NULL); + return NULL; +} + +static inline void* ASN1_STRING_free(ASN1_STRING *p) { assert(p == NULL); return NULL; } @@ -155,6 +168,8 @@ static inline void *EVP_PKEY_free(EVP_PKEY *p) { DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(X509*, X509_free, NULL); DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(EVP_PKEY*, EVP_PKEY_free, NULL); +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(ASN1_TYPE*, ASN1_TYPE_free, NULL); +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(ASN1_STRING*, ASN1_STRING_free, NULL); struct OpenSSLAskPasswordUI { AskPasswordRequest request; diff --git a/src/shared/pe-binary.c b/src/shared/pe-binary.c index 60d0886d22..c50aad18f3 100644 --- a/src/shared/pe-binary.c +++ b/src/shared/pe-binary.c @@ -12,8 +12,6 @@ #include "string-table.h" #include "string-util.h" -#define IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE 4U - bool pe_header_is_64bit(const PeHeader *h) { assert(h); @@ -444,6 +442,55 @@ int pe_hash(int fd, #endif } +int pe_checksum(int fd, uint32_t *ret) { + _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL; + _cleanup_free_ PeHeader *pe_header = NULL; + struct stat st; + int r; + + assert(fd >= 0); + assert(ret); + + if (fstat(fd, &st) < 0) + return log_debug_errno(errno, "Failed to stat file: %m"); + + r = pe_load_headers(fd, &dos_header, &pe_header); + if (r < 0) + return r; + + uint32_t checksum = 0, checksum_offset = le32toh(dos_header->e_lfanew) + offsetof(PeHeader, optional.CheckSum); + size_t off = 0; + for (;;) { + uint16_t buf[32*1024]; + + ssize_t n = pread(fd, buf, sizeof(buf), off); + if (n == 0) + break; + if (n < 0) + return log_debug_errno(errno, "Failed to read from PE file: %m"); + if (n % sizeof(uint16_t) != 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Short read from PE file"); + + for (size_t i = 0; i < (size_t) n / 2; i++) { + if (off + i >= checksum_offset && off + i < checksum_offset + sizeof(pe_header->optional.CheckSum)) + continue; + + uint16_t val = le16toh(buf[i]); + + checksum += val; + checksum = (checksum >> 16) + (checksum & 0xffff); + } + + off += n; + } + + checksum = (checksum >> 16) + (checksum & 0xffff); + checksum += off; + + *ret = checksum; + return 0; +} + #if HAVE_OPENSSL typedef void* SectionHashArray[_UNIFIED_SECTION_MAX]; diff --git a/src/shared/pe-binary.h b/src/shared/pe-binary.h index 1b86b93c03..eb0c8b3100 100644 --- a/src/shared/pe-binary.h +++ b/src/shared/pe-binary.h @@ -8,6 +8,8 @@ #include "sparse-endian.h" #include "uki.h" +#define IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE 4U + /* When naming things we try to stay close to the official Windows APIs as per: * → https://learn.microsoft.com/en-us/windows/win32/debug/pe-format */ @@ -153,4 +155,6 @@ bool pe_is_native(const PeHeader *pe_header); int pe_hash(int fd, const EVP_MD *md, void **ret_hash, size_t *ret_hash_size); +int pe_checksum(int fd, uint32_t *ret); + int uki_hash(int fd, const EVP_MD *md, void *ret_hashes[static _UNIFIED_SECTION_MAX], size_t *ret_hash_size); diff --git a/test/units/TEST-74-AUX-UTILS.sbsign.sh b/test/units/TEST-74-AUX-UTILS.sbsign.sh new file mode 100755 index 0000000000..fc186517d1 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.sbsign.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +if ! command -v /usr/lib/systemd/systemd-sbsign >/dev/null; then + echo "systemd-sbsign not found, skipping." + exit 0 +fi + +if [[ ! -d /usr/lib/systemd/boot/efi ]]; then + echo "systemd-boot is not installed, skipping." + exit 0 +fi + +cat >/tmp/openssl.conf </dev/null; then + echo "sbverify not found, skipping." + exit 0 + fi + + SD_BOOT="$(find /usr/lib/systemd/boot/efi/ -name "systemd-boot*.efi" | head -n1)" + + (! sbverify --cert /tmp/sb.crt "$SD_BOOT") + /usr/lib/systemd/systemd-sbsign sign --certificate /tmp/sb.crt --private-key /tmp/sb.key --output /tmp/sdboot "$SD_BOOT" + sbverify --cert /tmp/sb.crt /tmp/sdboot + + # Make sure appending signatures to an existing certificate table works as well. + /usr/lib/systemd/systemd-sbsign sign --certificate /tmp/sb.crt --private-key /tmp/sb.key --output /tmp/sdboot /tmp/sdboot + sbverify --cert /tmp/sb.crt /tmp/sdboot +} + +run_testcases From 8cbd9d8328d58a69414b3ce845b29476da155f57 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 5 Nov 2024 13:43:02 +0100 Subject: [PATCH 5/7] sbsign: Add validate-key verb This verb checks that we can load the specified private key. --- man/systemd-sbsign.xml | 16 +++++++++++ src/boot/sbsign.c | 39 ++++++++++++++++++++++++-- test/units/TEST-74-AUX-UTILS.sbsign.sh | 4 +++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/man/systemd-sbsign.xml b/man/systemd-sbsign.xml index d7095e821c..cd7d06d79f 100644 --- a/man/systemd-sbsign.xml +++ b/man/systemd-sbsign.xml @@ -49,6 +49,22 @@ + + + + + Checks that we can load the private key specified with + . + + As a side effect, if the private key is loaded from a PIN-protected hardware token, this + command can be used to cache the PIN in the kernel keyring. The + $SYSTEMD_ASK_PASSWORD_KEYRING_TIMEOUT_SEC and + $SYSTEMD_ASK_PASSWORD_KEYRING_TYPE environment variables can be used to control + how long and in which kernel keyring the PIN is cached. + + + + diff --git a/src/boot/sbsign.c b/src/boot/sbsign.c index 2cddc62905..7dc2cc86af 100644 --- a/src/boot/sbsign.c +++ b/src/boot/sbsign.c @@ -42,6 +42,7 @@ static int help(int argc, char *argv[], void *userdata) { "\n%5$sSign binaries for EFI Secure Boot%6$s\n" "\n%3$sCommands:%4$s\n" " sign EXEFILE Sign the given binary for EFI Secure Boot\n" + " validate-key Load and validate the given private key\n" "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" " --version Print version\n" @@ -468,10 +469,44 @@ static int verb_sign(int argc, char *argv[], void *userdata) { return 0; } +static int verb_validate_key(int argc, char *argv[], void *userdata) { + _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; + _cleanup_(EVP_PKEY_freep) EVP_PKEY *private_key = NULL; + int r; + + if (!arg_private_key) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "No private key specified, use --private-key=."); + + if (arg_private_key_source_type == OPENSSL_KEY_SOURCE_FILE) { + r = parse_path_argument(arg_private_key, /* suppress_root= */ false, &arg_private_key); + if (r < 0) + return log_error_errno(r, "Failed to parse private key path %s: %m", arg_private_key); + } + + r = openssl_load_private_key( + arg_private_key_source_type, + arg_private_key_source, + arg_private_key, + &(AskPasswordRequest) { + .id = "sbsign-private-key-pin", + .keyring = arg_private_key, + .credential = "sbsign.private-key-pin", + }, + &private_key, + &ui); + if (r < 0) + return log_error_errno(r, "Failed to load private key from %s: %m", arg_private_key); + + puts("OK"); + return 0; +} + static int run(int argc, char *argv[]) { static const Verb verbs[] = { - { "help", VERB_ANY, VERB_ANY, 0, help }, - { "sign", 2, 2, 0, verb_sign }, + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "sign", 2, 2, 0, verb_sign }, + { "validate-key", VERB_ANY, 1, 0, verb_validate_key }, {} }; int r; diff --git a/test/units/TEST-74-AUX-UTILS.sbsign.sh b/test/units/TEST-74-AUX-UTILS.sbsign.sh index fc186517d1..891a2ae8af 100755 --- a/test/units/TEST-74-AUX-UTILS.sbsign.sh +++ b/test/units/TEST-74-AUX-UTILS.sbsign.sh @@ -53,4 +53,8 @@ testcase_sign_systemd_boot() { sbverify --cert /tmp/sb.crt /tmp/sdboot } +testcase_validate_key() { + /usr/lib/systemd/systemd-sbsign validate-key --certificate /tmp/sb.crt --private-key /tmp/sb.key +} + run_testcases From d835c4476b84eb31f0b433ee31edb6b0cd4fd694 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 5 Nov 2024 13:44:18 +0100 Subject: [PATCH 6/7] ukify: Add support for systemd-sbsign --- man/ukify.xml | 6 +++--- src/ukify/ukify.py | 45 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/man/ukify.xml b/man/ukify.xml index c78a12ada0..ffc406f6ce 100644 --- a/man/ukify.xml +++ b/man/ukify.xml @@ -440,9 +440,9 @@ SecureBootSigningTool=SIGNER - Whether to use sbsign or pesign. - Depending on this choice, different parameters are required in order to sign an image. - Defaults to sbsign. + Whether to use sbsign, pesign, or + systemd-sbsign. Depending on this choice, different parameters are required in + order to sign an image. Defaults to sbsign. diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 02220dd983..60f64dc817 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -526,6 +526,40 @@ class SbSign(SignTool): return 'No signature table present' in info +class SystemdSbSign(SignTool): + @staticmethod + def sign(input_f: str, output_f: str, opts: UkifyConfig) -> None: + assert opts.sb_key is not None + assert opts.sb_cert is not None + + tool = find_tool( + 'systemd-sbsign', + '/usr/lib/systemd/systemd-sbsign', + opts=opts, + msg='systemd-sbsign, required for signing, is not installed', + ) + cmd = [ + tool, + "sign", + '--private-key', opts.sb_key, + '--certificate', opts.sb_cert, + *( + ['--private-key-source', f'engine:{opts.signing_engine}'] + if opts.signing_engine is not None + else [] + ), + input_f, + '--output', output_f, + ] # fmt: skip + + print('+', shell_join(cmd)) + subprocess.check_call(cmd) + + @staticmethod + def verify(opts: UkifyConfig) -> bool: + raise NotImplementedError('systemd-sbsign cannot yet verify if existing PE binaries are signed') + + def parse_banks(s: str) -> list[str]: banks = re.split(r',|\s+', s) # TODO: do some sanity checking here @@ -1477,6 +1511,8 @@ class SignToolAction(argparse.Action): setattr(namespace, 'signtool', SbSign) elif values == 'pesign': setattr(namespace, 'signtool', PeSign) + elif values == 'systemd-sbsign': + setattr(namespace, 'signtool', SystemdSbSign) else: raise ValueError(f"Unknown signtool '{values}' (this is unreachable)") @@ -1624,7 +1660,7 @@ CONFIG_ITEMS = [ ), ConfigItem( '--signtool', - choices=('sbsign', 'pesign'), + choices=('sbsign', 'pesign', 'systemd-sbsign'), action=SignToolAction, dest='signtool', help=( @@ -1637,7 +1673,7 @@ CONFIG_ITEMS = [ ConfigItem( '--secureboot-private-key', dest='sb_key', - help='required by --signtool=sbsign. Path to key file or engine-specific designation for SB signing', + help='required by --signtool=sbsign|systemd-sbsign. Path to key file or engine-specific designation for SB signing', # noqa: E501 config_key='UKI/SecureBootPrivateKey', ), ConfigItem( @@ -1940,11 +1976,12 @@ def finalize_options(opts: argparse.Namespace) -> None: ) elif bool(opts.sb_key) and bool(opts.sb_cert): # both param given, infer sbsign and in case it was given, ensure signtool=sbsign - if opts.signtool and opts.signtool != SbSign: + if opts.signtool and opts.signtool not in (SbSign, SystemdSbSign): raise ValueError( f'Cannot provide --signtool={opts.signtool} with --secureboot-private-key= and --secureboot-certificate=' # noqa: E501 ) - opts.signtool = SbSign + if not opts.signtool: + opts.signtool = SbSign elif bool(opts.sb_cert_name): # sb_cert_name given, infer pesign and in case it was given, ensure signtool=pesign if opts.signtool and opts.signtool != PeSign: From 65fbf3b1948bd1c4b063f2825a35644563c1b30f Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 5 Nov 2024 22:24:17 +0100 Subject: [PATCH 7/7] ukify: Add --signing-provider= option --- man/ukify.xml | 35 ++++++++++++++++++++++++----------- src/ukify/ukify.py | 36 +++++++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/man/ukify.xml b/man/ukify.xml index ffc406f6ce..6a697ee6e1 100644 --- a/man/ukify.xml +++ b/man/ukify.xml @@ -100,10 +100,12 @@ the n-th boot phase path set will be signed by the n-th key. This can be used to build different trust policies for different phases of the boot. In the config file, PCRPrivateKey=, PCRPublicKey=, and Phases= are grouped into separate sections, - describing separate boot phases. If SigningEngine=/ - is specified, then the private keys arguments will be passed verbatim to OpenSSL as URIs, and the public - key arguments will be loaded as X.509 certificates, so that signing can be performed with an OpenSSL - engine. + describing separate boot phases. If one of + SigningEngine=/ or + SigningProvider=/ is specified, then the private + key arguments will be passed verbatim to OpenSSL as URIs, and the public key arguments will be loaded + as X.509 certificates, so that signing can be performed with an OpenSSL engine or provider + respectively. If a SecureBoot signing key is provided via the SecureBootPrivateKey=/ option, the resulting @@ -452,8 +454,9 @@ A path to a private key to use for signing of the resulting binary. If the - SigningEngine=/ option is used, this may also be - an engine-specific designation. This option is required by + SigningEngine=/ or + SigningProvider=/ option is used, this may + also be an engine or provider specific designation. This option is required by SecureBootSigningTool=sbsign/. @@ -464,8 +467,9 @@ A path to a certificate to use for signing of the resulting binary. If the - SigningEngine=/ option is used, this may also - be an engine-specific designation. This option is required by + SigningEngine=/ or + SigningProvider=/ option is used, this may + also be an engine or provider specific designation. This option is required by SecureBootSigningTool=sbsign/. @@ -506,14 +510,23 @@ SigningEngine=ENGINE - An "engine" for signing of the resulting binary. This option is currently passed - verbatim to the option of - sbsign1. + An OpenSSL engine to be used for signing the resulting binary and PCR measurements. + + SigningProvider=PROVIDER + + + An OpenSSL provider to be used for signing the resulting binary and PCR + measurements. This option can only be used when using systemd-sbsign as the + signing tool. + + + + SignKernel=BOOL diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 60f64dc817..ef4e9264c2 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -263,6 +263,7 @@ class UkifyConfig: sections_by_name: dict[str, 'Section'] sign_kernel: bool signing_engine: Optional[str] + signing_provider: Optional[str] signtool: Optional[type['SignTool']] splash: Optional[Path] stub: Path @@ -548,6 +549,11 @@ class SystemdSbSign(SignTool): if opts.signing_engine is not None else [] ), + *( + ['--private-key-source', f'provider:{opts.signing_provider}'] + if opts.signing_provider is not None + else [] + ), input_f, '--output', output_f, ] # fmt: skip @@ -745,6 +751,10 @@ def call_systemd_measure(uki: UKI, opts: UkifyConfig, profile_start: int = 0) -> assert pub_key extra += [f'--private-key-source=engine:{opts.signing_engine}'] extra += [f'--certificate={pub_key}'] + elif opts.signing_provider is not None: + assert pub_key + extra += [f'--private-key-source=provider:{opts.signing_provider}'] + extra += [f'--certificate={pub_key}'] elif pub_key: extra += [f'--public-key={pub_key}'] extra += [f'--phase={phase_path}' for phase_path in group or ()] @@ -999,9 +1009,9 @@ def make_uki(opts: UkifyConfig) -> None: if pcrpkey is None: if opts.pcr_public_keys and len(opts.pcr_public_keys) == 1: pcrpkey = opts.pcr_public_keys[0] - # If we are getting a certificate when using an engine, we need to convert it to public key - # format - if opts.signing_engine is not None and Path(pcrpkey).exists(): + # If we are getting a certificate when using an engine or provider, we need to convert it to + # public key format. + if (opts.signing_engine or opts.signing_provider) and Path(pcrpkey).exists(): from cryptography.hazmat.primitives import serialization from cryptography.x509 import load_pem_x509_certificate @@ -1658,6 +1668,12 @@ CONFIG_ITEMS = [ help='OpenSSL engine to use for signing', config_key='UKI/SigningEngine', ), + ConfigItem( + '--signing-provider', + metavar='PROVIDER', + help='OpenSSL provider to use for signing', + config_key='UKI/SigningProvider', + ), ConfigItem( '--signtool', choices=('sbsign', 'pesign', 'systemd-sbsign'), @@ -1673,7 +1689,7 @@ CONFIG_ITEMS = [ ConfigItem( '--secureboot-private-key', dest='sb_key', - help='required by --signtool=sbsign|systemd-sbsign. Path to key file or engine-specific designation for SB signing', # noqa: E501 + help='required by --signtool=sbsign|systemd-sbsign. Path to key file or engine/provider designation for SB signing', # noqa: E501 config_key='UKI/SecureBootPrivateKey', ), ConfigItem( @@ -1722,7 +1738,7 @@ CONFIG_ITEMS = [ '--pcr-private-key', dest='pcr_private_keys', action='append', - help='private part of the keypair or engine-specific designation for signing PCR signatures', + help='private part of the keypair or engine/provider designation for signing PCR signatures', config_key='PCRSignature:/PCRPrivateKey', config_push=ConfigItem.config_set_group, ), @@ -1732,7 +1748,7 @@ CONFIG_ITEMS = [ metavar='PATH', type=Path, action='append', - help='public part of the keypair or engine-specific designation for signing PCR signatures', + help='public part of the keypair or engine/provider designation for signing PCR signatures', config_key='PCRSignature:/PCRPublicKey', config_push=ConfigItem.config_set_group, ), @@ -1963,7 +1979,10 @@ def finalize_options(opts: argparse.Namespace) -> None: else: opts.stub = Path(f'/usr/lib/systemd/boot/efi/addon{opts.efi_arch}.efi.stub') - if opts.signing_engine is None: + if opts.signing_engine and opts.signing_provider: + raise ValueError('Only one of --signing-engine= and --signing-provider= may be specified') + + if opts.signing_engine is None and opts.signing_provider is None: if opts.sb_key: opts.sb_key = Path(opts.sb_key) if opts.sb_cert: @@ -1990,6 +2009,9 @@ def finalize_options(opts: argparse.Namespace) -> None: ) opts.signtool = PeSign + if opts.signing_provider and opts.signtool != SystemdSbSign: + raise ValueError('--signing-provider= can only be used with--signtool=systemd-sbsign') + if opts.sign_kernel and not opts.sb_key and not opts.sb_cert_name: raise ValueError( '--sign-kernel requires either --secureboot-private-key= and --secureboot-certificate= (for sbsign) or --secureboot-certificate-name= (for pesign) to be specified' # noqa: E501