From: Daan De Meyer Date: Mon, 4 Nov 2024 23:36:32 +0000 (+0100) Subject: Introduce systemd-sbsign to do secure boot signing X-Git-Tag: v257-rc1~5^2~3 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=5f163921e9ff6d735798db259c47543822f81b5c;p=thirdparty%2Fsystemd.git 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. --- diff --git a/man/systemd-sbsign.xml b/man/systemd-sbsign.xml new file mode 100644 index 00000000000..d7095e821c0 --- /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 00000000000..98a8374805c --- /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 9c28c623d4d..a72388baff2 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 00000000000..2cddc629055 --- /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 f7087eb3928..853aded2c74 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 60d0886d22a..c50aad18f37 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 1b86b93c037..eb0c8b3100d 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 00000000000..fc186517d12 --- /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