diff --git a/man/systemd-sbsign.xml b/man/systemd-sbsign.xml new file mode 100644 index 0000000000..cd7d06d79f --- /dev/null +++ b/man/systemd-sbsign.xml @@ -0,0 +1,111 @@ + + + + + + + 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 . + + + + + + + + + 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. + + + + + + + + + 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/man/ukify.xml b/man/ukify.xml index c78a12ada0..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 @@ -440,9 +442,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. @@ -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/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 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/measure.c b/src/boot/measure.c index ed253b79f0..eacf90f08c 100644 --- a/src/boot/measure.c +++ b/src/boot/measure.c @@ -839,7 +839,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/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..7dc2cc86af --- /dev/null +++ b/src/boot/sbsign.c @@ -0,0 +1,523 @@ +/* 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" + " 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" + " --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 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 }, + { "validate-key", VERB_ANY, 1, 0, verb_validate_key }, + {} + }; + 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/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.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 c9acd40f22..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); @@ -134,59 +135,48 @@ 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; 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* UI_destroy_method(UI_METHOD *p) { +static inline void* ASN1_TYPE_free(ASN1_TYPE *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; +static inline void* ASN1_STRING_free(ASN1_STRING *p) { + assert(p == NULL); + return NULL; } #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); +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; 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); diff --git a/src/shared/pe-binary.c b/src/shared/pe-binary.c index 3893ffaf97..c50aad18f3 100644 --- a/src/shared/pe-binary.c +++ b/src/shared/pe-binary.c @@ -1,12 +1,16 @@ /* 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" bool pe_header_is_64bit(const PeHeader *h) { assert(h); @@ -284,3 +288,312 @@ 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 +} + +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]; + +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..eb0c8b3100 100644 --- a/src/shared/pe-binary.h +++ b/src/shared/pe-binary.h @@ -3,7 +3,12 @@ #include +#include "openssl-util.h" +#include "macro-fundamental.h" #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 */ @@ -147,3 +152,9 @@ 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 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/src/ukify/ukify.py b/src/ukify/ukify.py index 02220dd983..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 @@ -526,6 +527,45 @@ 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 [] + ), + *( + ['--private-key-source', f'provider:{opts.signing_provider}'] + if opts.signing_provider 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 @@ -711,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 ()] @@ -965,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 @@ -1477,6 +1521,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)") @@ -1622,9 +1668,15 @@ 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'), + choices=('sbsign', 'pesign', 'systemd-sbsign'), action=SignToolAction, dest='signtool', help=( @@ -1637,7 +1689,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/provider designation for SB signing', # noqa: E501 config_key='UKI/SecureBootPrivateKey', ), ConfigItem( @@ -1686,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, ), @@ -1696,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, ), @@ -1927,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: @@ -1940,11 +1995,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: @@ -1953,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 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..891a2ae8af --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.sbsign.sh @@ -0,0 +1,60 @@ +#!/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 +} + +testcase_validate_key() { + /usr/lib/systemd/systemd-sbsign validate-key --certificate /tmp/sb.crt --private-key /tmp/sb.key +} + +run_testcases