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