From: Daan De Meyer Date: Fri, 21 Feb 2025 23:31:41 +0000 (+0100) Subject: sbsign: Add support for offline signing X-Git-Tag: v258-rc1~1231^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8c0098d4861d8a9a617330e30c9ffad099fb641e;p=thirdparty%2Fsystemd.git sbsign: Add support for offline signing Add new options --prepare-offline-signing, --signed-data= and --signed-data-signature= which allow for offline signing in a similar manner to pesign. --- diff --git a/man/systemd-sbsign.xml b/man/systemd-sbsign.xml index 3c0bcf13eea..72b286256a3 100644 --- a/man/systemd-sbsign.xml +++ b/man/systemd-sbsign.xml @@ -60,7 +60,8 @@ - Specifies the path where to write the signed PE binary. + Specifies the path where to write the signed PE binary or the data to be signed + offline when using the option. @@ -85,11 +86,63 @@ + + + + When this option is specified, the sign command writes the data + that should be signed to the path specified with 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 and + options. + + + + + + + + + Configure the signed data (as written to the path specified with + when using the option) and + corresponding signature for the sign command. + + + + + + Examples + + + Offline EFI secure boot signing of a PE binary + + The following does offline secure boot signing of systemd-boot: + + 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" + + + See Also diff --git a/src/sbsign/sbsign.c b/src/sbsign/sbsign.c index 37ac0bec49d..1c94db0516d 100644 --- a/src/sbsign/sbsign.c +++ b/src/sbsign/sbsign.c @@ -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) diff --git a/src/shared/openssl-util.h b/src/shared/openssl-util.h index 757875f6183..4ed7a4891f7 100644 --- a/src/shared/openssl-util.h +++ b/src/shared/openssl-util.h @@ -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); diff --git a/test/units/TEST-74-AUX-UTILS.sbsign.sh b/test/units/TEST-74-AUX-UTILS.sbsign.sh index fc186517d12..d7bd4a2c808 100755 --- a/test/units/TEST-74-AUX-UTILS.sbsign.sh +++ b/test/units/TEST-74-AUX-UTILS.sbsign.sh @@ -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