]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sbsign: Split out functions and switch to lower level APIs
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 21 Feb 2025 11:22:45 +0000 (12:22 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 27 Feb 2025 12:47:05 +0000 (13:47 +0100)
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
src/shared/openssl-util.h

index ec38d185a6955ed2d43b4b388b2a2e06341b7a49..37ac0bec49da616ad526e21357e0f4af29233535 100644 (file)
@@ -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. */
 
         /* <<<Obsolete>>> 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));
index 73fd72001f875363117a629c4eb0ebcb5274ccbd..757875f61836ab0512eaeea79e8fb33e39b2dbcb 100644 (file)
@@ -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);