From 6f3ef02067a0bd7ccaafc7d87e07e3836ae1a2c9 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 21 Feb 2025 12:22:45 +0100 Subject: [PATCH] sbsign: Split out functions and switch to lower level APIs Preparation for adding offline signing support. Some additional features and fixes are included as well: - We make sure to add an empty SMIMECAP attribute instead of a populated one to mimick pesign more. - We switch to PKCS7_dataFinal() instead of PKCS7_final() as all that the latter does is an unnecessary copy before calling PKCS7_dataFinal(). - We add support for passing in the signing time via $SOURCE_DATE_EPOCH. --- src/sbsign/sbsign.c | 319 +++++++++++++++++++++++++------------- src/shared/openssl-util.h | 11 ++ 2 files changed, 221 insertions(+), 109 deletions(-) diff --git a/src/sbsign/sbsign.c b/src/sbsign/sbsign.c index ec38d185a69..37ac0bec49d 100644 --- a/src/sbsign/sbsign.c +++ b/src/sbsign/sbsign.c @@ -7,6 +7,7 @@ #include "build.h" #include "copy.h" #include "efi-fundamental.h" +#include "env-util.h" #include "fd-util.h" #include "log.h" #include "main-func.h" @@ -158,111 +159,13 @@ static int parse_argv(int argc, char *argv[]) { 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="); - - if (arg_certificate_source_type == OPENSSL_CERTIFICATE_SOURCE_FILE) { - r = parse_path_argument(arg_certificate, /*suppress_root=*/ false, &arg_certificate); - if (r < 0) - return r; - } - - r = openssl_load_x509_certificate( - arg_certificate_source_type, - arg_certificate_source, - 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) { - .tty_fd = -EBADF, - .id = "sbsign-private-key-pin", - .keyring = arg_private_key, - .credential = "sbsign.private-key-pin", - .until = USEC_INFINITY, - .hup_fd = -EBADF, - }, - &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)); +static int spc_indirect_data_content_new(const void *digest, size_t digestsz, uint8_t **ret_idc, size_t *ret_idcsz) { + assert(digest); + assert(ret_idc); + assert(ret_idcsz); - 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_error_errno(errno, "Failed to stat %s: %m", argv[1]); - - r = stat_verify_regular(&st); - if (r < 0) - return log_error_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(dstfd, "Failed to open temporary file: %m"); - - r = fchmod_umask(dstfd, 0666); - if (r < 0) - log_debug_errno(r, "Failed to change temporary file mode: %m"); - - _cleanup_free_ void *hash = NULL; - size_t hashsz; - r = pe_hash(srcfd, EVP_sha256(), &hash, &hashsz); - if (r < 0) - return log_error_errno(r, "Failed to hash PE binary %s: %m", argv[0]); + /* This function allocates and populates a new SpcIndirectDataContent object. See the authenticode + * spec https://aka.ms/AuthenticodeSpec for more information on the individual fields. */ /* <<>> in unicode bytes. */ static const uint8_t obsolete[] = { @@ -330,7 +233,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { idc->messageDigest->digestAlgorithm->parameters->type = V_ASN1_NULL; - if (ASN1_OCTET_STRING_set(idc->messageDigest->digest, hash, hashsz) == 0) + if (ASN1_OCTET_STRING_set(idc->messageDigest->digest, digest, digestsz) == 0) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set digest: %s", ERR_error_string(ERR_get_error(), NULL)); @@ -340,6 +243,98 @@ static int verb_sign(int argc, char *argv[], void *userdata) { return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert SpcIndirectDataContent to BER: %s", ERR_error_string(ERR_get_error(), NULL)); + *ret_idc = TAKE_PTR(idcraw); + *ret_idcsz = (size_t) idcrawsz; + + return 0; +} + +static int asn1_timestamp(ASN1_TIME **ret) { + ASN1_TIME *time; + uint64_t epoch = UINT64_MAX; + int r; + + assert(ret); + + r = secure_getenv_uint64("SOURCE_DATE_EPOCH", &epoch); + if (r != -ENXIO) + log_debug_errno(r, "Failed to parse $SOURCE_DATE_EPOCH, ignoring: %m"); + + if (epoch == UINT64_MAX) { + time = X509_gmtime_adj(NULL, 0); + if (!time) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get current time: %s", + ERR_error_string(ERR_get_error(), NULL)); + } else { + time = ASN1_TIME_set(NULL, (time_t) (epoch / USEC_PER_SEC)); + if (!time) + return log_oom(); + } + + *ret = TAKE_PTR(time); + + return 0; +} + +static int pkcs7_new_with_attributes(X509 *certificate, EVP_PKEY *private_key, PKCS7 **ret_p7, PKCS7_SIGNER_INFO **ret_si) { + int r; + + /* This function sets up a new PKCS#7 signing context with the signed attributes required for + * authenticode signing. */ + + assert(certificate); + assert(private_key); + assert(ret_p7); + assert(ret_si); + + _cleanup_(PKCS7_freep) PKCS7 *p7 = NULL; + PKCS7_SIGNER_INFO *si = NULL; /* avoid false maybe-uninitialized warning */ + r = pkcs7_new(certificate, private_key, &p7, &si); + if (r < 0) + return log_error_errno(r, "Failed to allocate PKCS# context: %m"); + + /* Add an empty SMIMECAP attribute to indicate we don't have any SMIME capabilities. */ + + _cleanup_(x509_algor_free_manyp) STACK_OF(X509_ALGOR) *smcap = sk_X509_ALGOR_new_null(); + if (!smcap) + return log_oom(); + + if (PKCS7_add_attrib_smimecap(si, smcap) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to add smimecap signed attribute to signer info: %s", + ERR_error_string(ERR_get_error(), NULL)); + + if (PKCS7_add_attrib_content_type(si, NULL) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to add content type signed attribute to signer info: %s", + ERR_error_string(ERR_get_error(), NULL)); + + _cleanup_(ASN1_TIME_freep) ASN1_TIME *time = NULL; + r = asn1_timestamp(&time); + if (r < 0) + return r; + + if (PKCS7_add0_attrib_signing_time(si, time) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to add signing time signed attribute to signer info: %s", + ERR_error_string(ERR_get_error(), NULL)); + + TAKE_PTR(time); + + ASN1_OBJECT *idc = OBJ_txt2obj(SPC_INDIRECT_DATA_OBJID, /* no_name= */ true); + if (!idc) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get SpcIndirectDataContent object: %s", + ERR_error_string(ERR_get_error(), NULL)); + + if (PKCS7_add_signed_attribute(si, NID_pkcs9_contentType, V_ASN1_OBJECT, idc) == 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)); + + *ret_p7 = TAKE_PTR(p7); + *ret_si = TAKE_PTR(si); + return 0; +} + +static int pkcs7_populate_data_bio(PKCS7* p7, const void *data, size_t size, BIO **ret) { + assert(ret); + _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", @@ -347,10 +342,10 @@ static int verb_sign(int argc, char *argv[], void *userdata) { int tag, class; long psz; - const uint8_t *p = idcraw; + const uint8_t *p = data; /* 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) + if (ASN1_get_object(&p, &psz, &tag, &class, size) & 0x80) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse ASN.1 object: %s", ERR_error_string(ERR_get_error(), NULL)); @@ -358,7 +353,113 @@ static int verb_sign(int argc, char *argv[], void *userdata) { 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) + *ret = TAKE_PTR(bio); + + return 0; +} + +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="); + + if (arg_certificate_source_type == OPENSSL_CERTIFICATE_SOURCE_FILE) { + r = parse_path_argument(arg_certificate, /*suppress_root=*/ false, &arg_certificate); + if (r < 0) + return r; + } + + r = openssl_load_x509_certificate( + arg_certificate_source_type, + arg_certificate_source, + 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) { + .tty_fd = -EBADF, + .id = "sbsign-private-key-pin", + .keyring = arg_private_key, + .credential = "sbsign.private-key-pin", + .until = USEC_INFINITY, + .hup_fd = -EBADF, + }, + &private_key, + &ui); + if (r < 0) + return log_error_errno(r, "Failed to load private key from %s: %m", arg_private_key); + + _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_error_errno(errno, "Failed to stat %s: %m", argv[1]); + + r = stat_verify_regular(&st); + if (r < 0) + return log_error_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(dstfd, "Failed to open temporary file: %m"); + + r = fchmod_umask(dstfd, 0666); + if (r < 0) + log_debug_errno(r, "Failed to change temporary file mode: %m"); + + _cleanup_free_ void *pehash = NULL; + size_t pehashsz; + r = pe_hash(srcfd, EVP_sha256(), &pehash, &pehashsz); + if (r < 0) + return log_error_errno(r, "Failed to hash PE binary %s: %m", argv[0]); + + _cleanup_free_ uint8_t *idcraw = NULL; + size_t idcrawsz = 0; /* avoid false maybe-uninitialized warning */ + r = spc_indirect_data_content_new(pehash, pehashsz, &idcraw, &idcrawsz); + if (r < 0) + return r; + + _cleanup_(PKCS7_freep) PKCS7 *p7 = NULL; + PKCS7_SIGNER_INFO *si; + r = pkcs7_new_with_attributes(certificate, private_key, &p7, &si); + if (r < 0) + return r; + + _cleanup_(BIO_free_allp) BIO *bio = NULL; + r = pkcs7_populate_data_bio(p7, idcraw, idcrawsz, &bio); + if (r < 0) + return r; + + if (PKCS7_dataFinal(p7, bio) == 0) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to sign data: %s", ERR_error_string(ERR_get_error(), NULL)); @@ -366,7 +467,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { if (!p7c) return log_oom(); - p7c->type = OBJ_nid2obj(idcnid); + p7c->type = OBJ_txt2obj(SPC_INDIRECT_DATA_OBJID, /* no_name= */ true); if (!p7c->type) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get SpcIndirectDataContent object: %s", ERR_error_string(ERR_get_error(), NULL)); diff --git a/src/shared/openssl-util.h b/src/shared/openssl-util.h index 73fd72001f8..757875f6183 100644 --- a/src/shared/openssl-util.h +++ b/src/shared/openssl-util.h @@ -68,6 +68,17 @@ 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); +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(ASN1_TIME*, ASN1_TIME_free, NULL); + +static inline STACK_OF(X509_ALGOR) *x509_algor_free_many(STACK_OF(X509_ALGOR) *attrs) { + if (!attrs) + return NULL; + + sk_X509_ALGOR_pop_free(attrs, X509_ALGOR_free); + return NULL; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(STACK_OF(X509_ALGOR)*, x509_algor_free_many, NULL); #if OPENSSL_VERSION_MAJOR >= 3 DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(EVP_CIPHER*, EVP_CIPHER_free, NULL); -- 2.47.3