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.
This commit is contained in:
Daan De Meyer
2025-02-22 00:31:41 +01:00
parent 6f3ef02067
commit 8c0098d486
4 changed files with 266 additions and 41 deletions

View 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>
@@ -85,11 +86,63 @@
<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">

View 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)

View 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);

View File

@@ -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