]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sbsign: Add support for offline signing
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 21 Feb 2025 23:31:41 +0000 (00:31 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 27 Feb 2025 12:47:07 +0000 (13:47 +0100)
Add new options --prepare-offline-signing, --signed-data= and
--signed-data-signature= which allow for offline signing in a
similar manner to pesign.

man/systemd-sbsign.xml
src/sbsign/sbsign.c
src/shared/openssl-util.h
test/units/TEST-74-AUX-UTILS.sbsign.sh

index 3c0bcf13eead79404cf662340d6e8ac4aa9a102f..72b286256a3abe43217d84cb53f9041cfe9fc4d9 100644 (file)
@@ -60,7 +60,8 @@
       <varlistentry>
         <term><option>--output=<replaceable>PATH</replaceable></option></term>
 
-        <listitem><para>Specifies the path where to write the signed PE binary.</para>
+        <listitem><para>Specifies the path where to write the signed PE binary or the data to be signed
+        offline when using the <option>--prepare-offline-signing</option> option.</para>
 
         <xi:include href="version-info.xml" xpointer="v257"/></listitem>
       </varlistentry>
         <xi:include href="version-info.xml" xpointer="v257"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><option>--prepare-offline-signing</option></term>
+
+        <listitem><para>When this option is specified, the <command>sign</command> command writes the data
+        that should be signed to the path specified with <option>--output=</option> instead of writing the
+        signed PE binary. This data can then be signed out of band after which the signature can be attached
+        to the PE binary using the <option>--signed-data=</option> and
+        <option>--signed-data-signature=</option> options.</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--signed-data=<replaceable>PATH</replaceable></option></term>
+        <term><option>--signed-data-signature=<replaceable>PATH</replaceable></option></term>
+
+        <listitem><para>Configure the signed data (as written to the path specified with
+        <option>--output=</option> when using the <option>--prepare-offline-signing</option> option) and
+        corresponding signature for the <command>sign</command> command.</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
       <xi:include href="standard-options.xml" xpointer="help"/>
       <xi:include href="standard-options.xml" xpointer="version"/>
     </variablelist>
   </refsect1>
 
+  <refsect1>
+    <title>Examples</title>
+
+    <example>
+      <title>Offline EFI secure boot signing of a PE binary</title>
+
+      <para>The following does offline secure boot signing of systemd-boot:</para>
+
+      <programlisting>SD_BOOT="$(find /usr/lib/systemd/boot/efi/ -name "systemd-boot*.efi" | head -n1)"
+# Extract the data that should be signed offline.
+/usr/lib/systemd/systemd-sbsign \
+    sign \
+    --certificate=secure-boot-certificate.pem \
+    --output=signed-data.bin \
+    --prepare-offline-signing \
+    "$SD_BOOT"
+# Sign the data out-of-band. This step usually happens out-of-band on a separate system.
+openssl dgst -sha256 -sign secure-boot-private-key.pem -out signed-data.sig signed-data.bin
+# Attach the signed data and its signature to the systemd-boot PE binary.
+/usr/lib/systemd/systemd-sbsign \
+    sign \
+    --certificate=secure-boot-certificate.pem \
+    --output="$SD_BOOT.signed" \
+    --signed-data=signed-data.bin \
+    --signed-data-signature=signed-data.sig \
+    "$SD_BOOT"</programlisting>
+    </example>
+  </refsect1>
+
   <refsect1>
     <title>See Also</title>
     <para><simplelist type="inline">
index 37ac0bec49da616ad526e21357e0f4af29233535..1c94db0516d004756bdfea3852122dd35e358848 100644 (file)
@@ -9,6 +9,8 @@
 #include "efi-fundamental.h"
 #include "env-util.h"
 #include "fd-util.h"
+#include "fileio.h"
+#include "io-util.h"
 #include "log.h"
 #include "main-func.h"
 #include "openssl-util.h"
@@ -26,12 +28,17 @@ static char *arg_certificate_source = 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 bool arg_prepare_offline_signing = false;
+static char *arg_signed_data = NULL;
+static char *arg_signed_data_signature = NULL;
 
 STATIC_DESTRUCTOR_REGISTER(arg_output, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_certificate, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_certificate_source, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_private_key_source, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_signed_data, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_signed_data_signature, freep);
 
 static int help(int argc, char *argv[], void *userdata) {
         _cleanup_free_ char *link = NULL;
@@ -79,16 +86,22 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_CERTIFICATE_SOURCE,
                 ARG_PRIVATE_KEY,
                 ARG_PRIVATE_KEY_SOURCE,
+                ARG_PREPARE_OFFLINE_SIGNING,
+                ARG_SIGNED_DATA,
+                ARG_SIGNED_DATA_SIGNATURE,
         };
 
         static const struct option options[] = {
-                { "help",               no_argument,       NULL, 'h'                    },
-                { "version",            no_argument,       NULL, ARG_VERSION            },
-                { "output",             required_argument, NULL, ARG_OUTPUT             },
-                { "certificate",        required_argument, NULL, ARG_CERTIFICATE        },
-                { "certificate-source", required_argument, NULL, ARG_CERTIFICATE_SOURCE },
-                { "private-key",        required_argument, NULL, ARG_PRIVATE_KEY        },
-                { "private-key-source", required_argument, NULL, ARG_PRIVATE_KEY_SOURCE },
+                { "help",                    no_argument,       NULL, 'h'                         },
+                { "version",                 no_argument,       NULL, ARG_VERSION                 },
+                { "output",                  required_argument, NULL, ARG_OUTPUT                  },
+                { "certificate",             required_argument, NULL, ARG_CERTIFICATE             },
+                { "certificate-source",      required_argument, NULL, ARG_CERTIFICATE_SOURCE      },
+                { "private-key",             required_argument, NULL, ARG_PRIVATE_KEY             },
+                { "private-key-source",      required_argument, NULL, ARG_PRIVATE_KEY_SOURCE      },
+                { "prepare-offline-signing", no_argument,       NULL, ARG_PREPARE_OFFLINE_SIGNING },
+                { "signed-data",             required_argument, NULL, ARG_SIGNED_DATA             },
+                { "signed-data-signature",   required_argument, NULL, ARG_SIGNED_DATA_SIGNATURE   },
                 {}
         };
 
@@ -146,6 +159,26 @@ static int parse_argv(int argc, char *argv[]) {
 
                         break;
 
+                case ARG_PREPARE_OFFLINE_SIGNING:
+                        arg_prepare_offline_signing = true;
+                        break;
+
+                case ARG_SIGNED_DATA: {
+                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_signed_data);
+                        if (r < 0)
+                                return r;
+
+                        break;
+                }
+
+                case ARG_SIGNED_DATA_SIGNATURE: {
+                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_signed_data_signature);
+                        if (r < 0)
+                                return r;
+
+                        break;
+                }
+
                 case '?':
                         return -EINVAL;
 
@@ -156,6 +189,12 @@ static int parse_argv(int argc, char *argv[]) {
         if (arg_private_key_source && !arg_certificate)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "When using --private-key-source=, --certificate= must be specified.");
 
+        if (!!arg_signed_data != !!arg_signed_data_signature)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--signed-data= and --signed-data-signature= must always be used together.");
+
+        if (arg_prepare_offline_signing && (arg_private_key || arg_signed_data || arg_signed_data_signature))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--prepare-offline-signing cannot be used with --private-key=, --signed-data= or --signed-data-signature=");
+
         return 1;
 }
 
@@ -276,14 +315,19 @@ static int asn1_timestamp(ASN1_TIME **ret) {
         return 0;
 }
 
-static int pkcs7_new_with_attributes(X509 *certificate, EVP_PKEY *private_key, PKCS7 **ret_p7, PKCS7_SIGNER_INFO **ret_si) {
+static int pkcs7_new_with_attributes(
+                X509 *certificate,
+                EVP_PKEY *private_key,
+                STACK_OF(X509_ATTRIBUTE) *signed_attributes,
+                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);
 
@@ -293,8 +337,15 @@ static int pkcs7_new_with_attributes(X509 *certificate, EVP_PKEY *private_key, P
         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. */
+        if (signed_attributes) {
+                si->auth_attr = signed_attributes;
+
+                *ret_p7 = TAKE_PTR(p7);
+                *ret_si = TAKE_PTR(si);
+                return 0;
+        }
 
+        /* 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();
@@ -358,10 +409,41 @@ static int pkcs7_populate_data_bio(PKCS7* p7, const void *data, size_t size, BIO
         return 0;
 }
 
+static int pkcs7_add_digest_attribute(PKCS7 *p7, BIO *data, PKCS7_SIGNER_INFO *si) {
+        assert(p7);
+        assert(data);
+        assert(si);
+
+        BIO *mdbio = BIO_find_type(data, BIO_TYPE_MD);
+        if (!mdbio)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to find digest bio: %s",
+                                       ERR_error_string(ERR_get_error(), NULL));
+
+        EVP_MD_CTX *mdc;
+        if (BIO_get_md_ctx(mdbio, &mdc) <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get digest context from bio: %s",
+                                       ERR_error_string(ERR_get_error(), NULL));
+
+        unsigned char digest[EVP_MAX_MD_SIZE];
+        unsigned digestsz;
+
+        if (EVP_DigestFinal_ex(mdc, digest, &digestsz) == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get digest: %s",
+                                       ERR_error_string(ERR_get_error(), NULL));
+
+        if (PKCS7_add1_attrib_digest(si, digest, digestsz) == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to add PKCS9 message digest signed attribute to signer info: %s",
+                                       ERR_error_string(ERR_get_error(), NULL));
+
+        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;
+        _cleanup_(x509_attribute_free_manyp) STACK_OF(X509_ATTRIBUTE) *signed_attributes = NULL;
+        _cleanup_(iovec_done) struct iovec signed_attributes_signature = {};
         int r;
 
         if (argc < 2)
@@ -371,9 +453,9 @@ static int verb_sign(int argc, char *argv[], void *userdata) {
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                        "No certificate specified, use --certificate=");
 
-        if (!arg_private_key)
+        if (!arg_private_key && !arg_signed_data_signature && !arg_prepare_offline_signing)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                       "No private key specified, use --private-key=.");
+                                       "No private key or signed data signature specified, use --private-key= or --signed-data-signature=.");
 
         if (!arg_output)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No output specified, use --output=");
@@ -392,28 +474,55 @@ static int verb_sign(int argc, char *argv[], void *userdata) {
         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 (arg_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) {
+                                        .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 parse private key path %s: %m", arg_private_key);
+                        return log_error_errno(r, "Failed to load private key from %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);
+        if (arg_signed_data) {
+                _cleanup_free_ void *content = NULL;
+                size_t contentsz;
+
+                r = read_full_file(arg_signed_data, (char**) &content, &contentsz);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to read signed attributes file '%s': %m", arg_signed_data);
+
+                const uint8_t *p = content;
+                if (!ASN1_item_d2i((ASN1_VALUE **) &signed_attributes, &p, contentsz, ASN1_ITEM_rptr(PKCS7_ATTR_SIGN)))
+                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse signed attributes: %s",
+                                               ERR_error_string(ERR_get_error(), NULL));
+        }
+
+        if (arg_signed_data_signature) {
+                _cleanup_free_ void *content = NULL;
+                size_t contentsz;
+
+                r = read_full_file(arg_signed_data_signature, (char**) &content, &contentsz);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to read signed attributes signature file '%s': %m", arg_signed_data_signature);
+
+                signed_attributes_signature = IOVEC_MAKE(TAKE_PTR(content), contentsz);
+        }
 
         _cleanup_close_ int srcfd = open(argv[1], O_RDONLY|O_CLOEXEC);
         if (srcfd < 0)
@@ -450,18 +559,50 @@ static int verb_sign(int argc, char *argv[], void *userdata) {
 
         _cleanup_(PKCS7_freep) PKCS7 *p7 = NULL;
         PKCS7_SIGNER_INFO *si;
-        r = pkcs7_new_with_attributes(certificate, private_key, &p7, &si);
+        r = pkcs7_new_with_attributes(certificate, private_key, signed_attributes, &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;
+        TAKE_PTR(signed_attributes);
 
-        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));
+        if (arg_prepare_offline_signing) {
+                _cleanup_(BIO_free_allp) BIO *bio = NULL;
+                r = pkcs7_populate_data_bio(p7, idcraw, idcrawsz, &bio);
+                if (r < 0)
+                        return r;
+
+                r = pkcs7_add_digest_attribute(p7, bio, si);
+                if (r < 0)
+                        return r;
+
+                _cleanup_free_ unsigned char *abuf = NULL;
+                int alen = ASN1_item_i2d((ASN1_VALUE *)si->auth_attr, &abuf, ASN1_ITEM_rptr(PKCS7_ATTR_SIGN));
+                if (alen < 0 || !abuf)
+                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert signed attributes ASN.1 to DER: %s",
+                                               ERR_error_string(ERR_get_error(), NULL));
+
+                r = loop_write(dstfd, abuf, alen);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to write PKCS#7 DER-encoded signed attributes blob to temporary file: %m");
+
+                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 PKCS#7 DER-encoded signed attributes blob to %s", arg_output);
+                return 0;
+        } else if (iovec_is_set(&signed_attributes_signature))
+                ASN1_STRING_set0(si->enc_digest, TAKE_PTR(signed_attributes_signature.iov_base), signed_attributes_signature.iov_len);
+        else {
+                _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));
+        }
 
         _cleanup_(PKCS7_freep) PKCS7 *p7c = PKCS7_new();
         if (!p7c)
index 757875f61836ab0512eaeea79e8fb33e39b2dbcb..4ed7a4891f77366d0d2d654457cd1db12374c2fd 100644 (file)
@@ -80,6 +80,16 @@ static inline STACK_OF(X509_ALGOR) *x509_algor_free_many(STACK_OF(X509_ALGOR) *a
 
 DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(STACK_OF(X509_ALGOR)*, x509_algor_free_many, NULL);
 
+static inline STACK_OF(X509_ATTRIBUTE) *x509_attribute_free_many(STACK_OF(X509_ATTRIBUTE) *attrs) {
+        if (!attrs)
+                return NULL;
+
+        sk_X509_ATTRIBUTE_pop_free(attrs, X509_ATTRIBUTE_free);
+        return NULL;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(STACK_OF(X509_ATTRIBUTE)*, x509_attribute_free_many, NULL);
+
 #if OPENSSL_VERSION_MAJOR >= 3
 DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(EVP_CIPHER*, EVP_CIPHER_free, NULL);
 DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(EVP_KDF*, EVP_KDF_free, NULL);
index fc186517d129b89401d68961af6c83b75b73f574..d7bd4a2c808fa2a3093e0be1ba22294a46ea3b9b 100755 (executable)
@@ -39,7 +39,7 @@ openssl req -config /tmp/openssl.conf -subj="/CN=waldo" \
 testcase_sign_systemd_boot() {
     if ! command -v sbverify >/dev/null; then
         echo "sbverify not found, skipping."
-        exit 0
+        return 0
     fi
 
     SD_BOOT="$(find /usr/lib/systemd/boot/efi/ -name "systemd-boot*.efi" | head -n1)"
@@ -53,4 +53,25 @@ testcase_sign_systemd_boot() {
     sbverify --cert /tmp/sb.crt /tmp/sdboot
 }
 
+testcase_sign_systemd_boot_offline() {
+    if ! command -v sbverify >/dev/null; then
+        echo "sbverify not found, skipping."
+        return 0
+    fi
+
+    SD_BOOT="$(find /usr/lib/systemd/boot/efi/ -name "systemd-boot*.efi" | head -n1)"
+
+    /usr/lib/systemd/systemd-sbsign sign --certificate /tmp/sb.crt --output /tmp/signed-data.bin --prepare-offline-signing "$SD_BOOT"
+    openssl dgst -sha256 -sign /tmp/sb.key -out /tmp/signed-data.sig /tmp/signed-data.bin
+    /usr/lib/systemd/systemd-sbsign \
+        sign \
+        --certificate /tmp/sb.crt \
+        --output /tmp/sdboot \
+        --signed-data /tmp/signed-data.bin \
+        --signed-data-signature /tmp/signed-data.sig \
+        "$SD_BOOT"
+
+    sbverify --cert /tmp/sb.crt /tmp/sdboot
+}
+
 run_testcases