diff --git a/man/systemd-sbsign.xml b/man/systemd-sbsign.xml
index 3c0bcf13ee..72b286256a 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 37ac0bec49..1c94db0516 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 757875f618..4ed7a4891f 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 fc186517d1..d7bd4a2c80 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