diff --git a/man/systemd-sbsign.xml b/man/systemd-sbsign.xml index 72b286256a3..53c90820162 100644 --- a/man/systemd-sbsign.xml +++ b/man/systemd-sbsign.xml @@ -49,6 +49,17 @@ + + + + Generates and signs a secure boot signature database. The signed secure boot + signature database will be written to the path specified with . The secure + boot database identifier should be specified with the + option. + + + + @@ -60,8 +71,9 @@ - Specifies the path where to write the signed PE binary or the data to be signed - offline when using the option. + Specifies the path where to write the signed PE binary, signed secure boot signature + database or the data to be signed offline when using the + option. @@ -73,15 +85,16 @@ Set the Secure Boot private key and certificate for use with the - sign. The option takes a path to a PEM encoded - X.509 certificate or a URI that's passed to the OpenSSL provider configured with - . The takes one of - file or provider, with the latter being followed by a specific - provider identifier, separated with a colon, e.g. provider:pkcs11. The - option can take a path or a URI that will be passed to the OpenSSL - engine or provider, as specified by as a - type:name tuple, such as engine:pkcs11. The specified OpenSSL - signing engine or provider will be used to sign the PE binary. + sign and sign-secure-boot-database commands. The + option takes a path to a PEM encoded X.509 certificate or a URI + that's passed to the OpenSSL provider configured with . The + option takes one of file or + provider, with the latter being followed by a specific provider identifier, + separated with a colon, e.g. provider:pkcs11. The + option can take a path or a URI that will be passed to the OpenSSL engine or provider, as specified + by as a type:name tuple, such as + engine:pkcs11. The specified OpenSSL signing engine or provider will be used to + sign the PE binary or secure boot signature database. @@ -91,9 +104,9 @@ 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. + signed PE binary or signed secure boot signature database. This data can then be signed out of band + after which the signature can be attached to the PE binary or secure boot signature database using + the and options. @@ -104,7 +117,19 @@ Configure the signed data (as written to the path specified with when using the option) and - corresponding signature for the sign command. + corresponding signature for the sign and + sign-secure-boot-database commands. + + + + + + + + Specify the secure boot signature database that should be generated and signed with + the sign-secure-boot-database command. Takes one of the strings + PK, KEK, db or dbx. + diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c index 4171e4aeee0..cbff2a131a8 100644 --- a/src/bootctl/bootctl-install.c +++ b/src/bootctl/bootctl-install.c @@ -582,7 +582,7 @@ static int efi_timestamp(EFI_TIME *ret) { assert(ret); r = secure_getenv_uint64("SOURCE_DATE_EPOCH", &epoch); - if (r != -ENXIO) + if (r < 0 && r != -ENXIO) log_debug_errno(r, "Failed to parse $SOURCE_DATE_EPOCH, ignoring: %m"); r = localtime_or_gmtime_usec(epoch != UINT64_MAX ? epoch : now(CLOCK_REALTIME), /*utc=*/ true, &tm); diff --git a/src/sbsign/sbsign.c b/src/sbsign/sbsign.c index b2f0d861ce1..a0c8c3758f0 100644 --- a/src/sbsign/sbsign.c +++ b/src/sbsign/sbsign.c @@ -7,6 +7,7 @@ #include "build.h" #include "copy.h" #include "efi-fundamental.h" +#include "efivars.h" #include "env-util.h" #include "fd-util.h" #include "fileio.h" @@ -18,6 +19,7 @@ #include "pe-binary.h" #include "pretty-print.h" #include "stat-util.h" +#include "strv.h" #include "tmpfile-util.h" #include "verbs.h" @@ -31,6 +33,7 @@ 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 char *arg_secure_boot_database = NULL; STATIC_DESTRUCTOR_REGISTER(arg_output, freep); STATIC_DESTRUCTOR_REGISTER(arg_certificate, freep); @@ -40,6 +43,129 @@ STATIC_DESTRUCTOR_REGISTER(arg_private_key_source, freep); STATIC_DESTRUCTOR_REGISTER(arg_signed_data, freep); STATIC_DESTRUCTOR_REGISTER(arg_signed_data_signature, freep); +typedef struct Context { + OpenSSLAskPasswordUI *ui; + EVP_PKEY *private_key; + X509 *certificate; + int srcfd; + struct stat srcfd_stat; + int dstfd; + char *tmp; +} Context; + +#define CONTEXT_NULL (Context) { .srcfd = -EBADF, .dstfd = -EBADF } + +static int context_populate(int argc, char *argv[], Context *ret) { + _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_close_ int srcfd = -EBADF; + struct stat srcfd_stat = {}; + _cleanup_(unlink_and_freep) char *tmp = NULL; + _cleanup_close_ int dstfd = -EBADF; + int r; + + assert(argv); + assert(ret); + + if (!arg_certificate) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No certificate specified, use --certificate="); + + 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) { + 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); + } + + if (argc >= 2) { + srcfd = open(argv[1], O_RDONLY|O_CLOEXEC); + if (srcfd < 0) + return log_error_errno(errno, "Failed to open %s: %m", argv[1]); + + if (fstat(srcfd, &srcfd_stat) < 0) + return log_error_errno(errno, "Failed to stat %s: %m", argv[1]); + + r = stat_verify_regular(&srcfd_stat); + if (r < 0) + return log_error_errno(r, "%s is not a regular file: %m", argv[1]); + } + + if (arg_output) { + 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"); + } + + *ret = (Context) { + .ui = TAKE_PTR(ui), + .private_key = TAKE_PTR(private_key), + .certificate = TAKE_PTR(certificate), + .srcfd = TAKE_FD(srcfd), + .srcfd_stat = srcfd_stat, + .tmp = TAKE_PTR(tmp), + .dstfd = TAKE_FD(dstfd), + }; + + return 0; +} + +static void context_done(Context *context) { + assert(context); + + context->ui = openssl_ask_password_ui_free(context->ui); + + if (context->private_key) { + EVP_PKEY_free(context->private_key); + context->private_key = NULL; + } + + if (context->certificate) { + X509_free(context->certificate); + context->certificate = NULL; + } + + context->srcfd = safe_close(context->srcfd); + context->tmp = unlink_and_free(context->tmp); + context->dstfd = safe_close(context->dstfd); +} + static int help(int argc, char *argv[], void *userdata) { _cleanup_free_ char *link = NULL; int r; @@ -52,10 +178,13 @@ static int help(int argc, char *argv[], void *userdata) { "\n%5$sSign binaries for EFI Secure Boot%6$s\n" "\n%3$sCommands:%4$s\n" " sign EXEFILE Sign the given binary for EFI Secure Boot\n" + " sign-secure-boot-database\n" + " Generate and sign a UEFI Secure Boot database\n" + " for Secure Boot auto-enrollment\n" "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" " --version Print version\n" - " --output Where to write the signed PE binary\n" + " --output=PATH Where to write the output\n" " --certificate=PATH|URI\n" " PEM certificate to use for signing, or a provider\n" " specific designation if --certificate-source= is used\n" @@ -67,6 +196,13 @@ static int help(int argc, char *argv[], void *userdata) { " --private-key-source=file|provider:PROVIDER|engine:ENGINE\n" " Specify how to use KEY for --private-key=. Allows\n" " an OpenSSL engine/provider to be used for signing\n" + " --prepare-offline-signing\n" + " Write the data that should be signed instead of the signed data" + " --signed-data=PATH Path to the data that was signed offline\n" + " --signed-data-signature=PATH\n" + " Path to the raw signature of the data that was signed offline\n" + " --secure-boot-database=PK|KEK|db|dbx\n" + " Which UEFI Secure Boot database to generate and sign\n" "\nSee the %2$s for details.\n", program_invocation_short_name, link, @@ -89,6 +225,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_PREPARE_OFFLINE_SIGNING, ARG_SIGNED_DATA, ARG_SIGNED_DATA_SIGNATURE, + ARG_SECURE_BOOT_DATABASE, }; static const struct option options[] = { @@ -102,6 +239,7 @@ static int parse_argv(int argc, char *argv[]) { { "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 }, + { "secure-boot-database", required_argument, NULL, ARG_SECURE_BOOT_DATABASE }, {} }; @@ -177,6 +315,12 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_SECURE_BOOT_DATABASE: + r = free_and_strdup(&arg_secure_boot_database, optarg); + if (r < 0) + return log_oom(); + + break; case '?': return -EINVAL; @@ -294,7 +438,7 @@ static int asn1_timestamp(ASN1_TIME **ret) { assert(ret); r = secure_getenv_uint64("SOURCE_DATE_EPOCH", &epoch); - if (r != -ENXIO) + if (r < 0 && r != -ENXIO) log_debug_errno(r, "Failed to parse $SOURCE_DATE_EPOCH, ignoring: %m"); if (epoch == UINT64_MAX) { @@ -333,7 +477,7 @@ static int pkcs7_new_with_attributes( 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"); + return log_error_errno(r, "Failed to allocate PKCS#7 context: %m"); if (signed_attributes) { si->auth_attr = signed_attributes; @@ -373,7 +517,7 @@ static int pkcs7_new_with_attributes( 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", + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to add signed attribute to PKCS#7 signer info: %s", ERR_error_string(ERR_get_error(), NULL)); *ret_p7 = TAKE_PTR(p7); @@ -386,7 +530,7 @@ static int pkcs7_populate_data_bio(PKCS7* p7, const void *data, size_t size, BIO _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", + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to create PKCS#7 data bio: %s", ERR_error_string(ERR_get_error(), NULL)); int tag, class; @@ -399,7 +543,7 @@ static int pkcs7_populate_data_bio(PKCS7* p7, const void *data, size_t size, BIO ERR_error_string(ERR_get_error(), NULL)); if (BIO_write(bio, p, psz) < 0) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write to PKCS7 data bio: %s", + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write to PKCS#7 data bio: %s", ERR_error_string(ERR_get_error(), NULL)); *ret = TAKE_PTR(bio); @@ -437,9 +581,7 @@ static int pkcs7_add_digest_attribute(PKCS7 *p7, BIO *data, PKCS7_SIGNER_INFO *s } 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_(context_done) Context ctx = CONTEXT_NULL; _cleanup_(x509_attribute_free_manyp) STACK_OF(X509_ATTRIBUTE) *signed_attributes = NULL; _cleanup_(iovec_done) struct iovec signed_attributes_signature = {}; int r; @@ -447,10 +589,6 @@ static int verb_sign(int argc, char *argv[], void *userdata) { 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 && !arg_signed_data_signature && !arg_prepare_offline_signing) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No private key or signed data signature specified, use --private-key= or --signed-data-signature=."); @@ -458,44 +596,9 @@ static int verb_sign(int argc, char *argv[], void *userdata) { 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); + r = context_populate(argc, argv, &ctx); if (r < 0) - return log_error_errno(r, "Failed to load X.509 certificate from %s: %m", arg_certificate); - - 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 load private key from %s: %m", arg_private_key); - } + return r; if (arg_signed_data) { _cleanup_free_ void *content = NULL; @@ -508,7 +611,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { 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)); + ERR_error_string(ERR_get_error(), NULL)); } if (arg_signed_data_signature) { @@ -522,30 +625,9 @@ static int verb_sign(int argc, char *argv[], void *userdata) { signed_attributes_signature = IOVEC_MAKE(TAKE_PTR(content), contentsz); } - _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); + r = pe_hash(ctx.srcfd, EVP_sha256(), &pehash, &pehashsz); if (r < 0) return log_error_errno(r, "Failed to hash PE binary %s: %m", argv[0]); @@ -557,18 +639,18 @@ static int verb_sign(int argc, char *argv[], void *userdata) { _cleanup_(PKCS7_freep) PKCS7 *p7 = NULL; PKCS7_SIGNER_INFO *si = NULL; /* avoid false maybe-uninitialized warning */ - r = pkcs7_new_with_attributes(certificate, private_key, signed_attributes, &p7, &si); + r = pkcs7_new_with_attributes(ctx.certificate, ctx.private_key, signed_attributes, &p7, &si); if (r < 0) return r; TAKE_PTR(signed_attributes); - 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; + _cleanup_(BIO_free_allp) BIO *bio = NULL; + r = pkcs7_populate_data_bio(p7, idcraw, idcrawsz, &bio); + if (r < 0) + return r; + if (arg_prepare_offline_signing) { r = pkcs7_add_digest_attribute(p7, bio, si); if (r < 0) return r; @@ -579,11 +661,11 @@ static int verb_sign(int argc, char *argv[], void *userdata) { 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); + r = loop_write(ctx.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); + r = link_tmpfile(ctx.dstfd, ctx.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); @@ -591,18 +673,18 @@ static int verb_sign(int argc, char *argv[], void *userdata) { return 0; } - 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 (iovec_is_set(&signed_attributes_signature)) { + ASN1_STRING_set0(si->enc_digest, + TAKE_PTR(signed_attributes_signature.iov_base), + signed_attributes_signature.iov_len); - if (PKCS7_dataFinal(p7, bio) == 0) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to sign data: %s", + if (PKCS7_signatureVerify(bio, p7, si, ctx.certificate) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "PKCS#7 signature validation failed: %s", ERR_error_string(ERR_get_error(), NULL)); - } + + } else 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) @@ -627,7 +709,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { ERR_error_string(ERR_get_error(), NULL)); if (PKCS7_set_content(p7, p7c) == 0) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set PKCS7 data: %s", + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set PKCS#7 data: %s", ERR_error_string(ERR_get_error(), NULL)); TAKE_PTR(p7c); @@ -635,12 +717,12 @@ static int verb_sign(int argc, char *argv[], void *userdata) { _cleanup_free_ uint8_t *sig = NULL; int sigsz = i2d_PKCS7(p7, &sig); if (sigsz < 0) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert PKCS7 signature to DER: %s", + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert PKCS#7 signature to DER: %s", ERR_error_string(ERR_get_error(), NULL)); _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL; _cleanup_free_ PeHeader *pe_header = NULL; - r = pe_load_headers(srcfd, &dos_header, &pe_header); + r = pe_load_headers(ctx.srcfd, &dos_header, &pe_header); if (r < 0) return log_error_errno(r, "Failed to load headers from PE file: %m"); @@ -649,31 +731,31 @@ static int verb_sign(int argc, char *argv[], void *userdata) { if (!certificate_table) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "File lacks certificate table."); - r = copy_bytes(srcfd, dstfd, UINT64_MAX, COPY_REFLINK); + r = copy_bytes(ctx.srcfd, ctx.dstfd, UINT64_MAX, COPY_REFLINK); if (r < 0) - return log_error_errno(r, "Failed to copy %s to %s: %m", argv[1], tmp); + return log_error_errno(r, "Failed to copy %s to %s: %m", argv[1], ctx.tmp); - off_t end = st.st_size; + off_t end = ctx.srcfd_stat.st_size; ssize_t n; - if (st.st_size % 8 != 0) { + if (ctx.srcfd_stat.st_size % 8 != 0) { if (certificate_table->VirtualAddress != 0) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Certificate table is not aligned to 8 bytes"); - n = pwrite(dstfd, (const uint8_t[8]) {}, 8 - (st.st_size % 8), st.st_size); + n = pwrite(ctx.dstfd, (const uint8_t[8]) {}, 8 - (ctx.srcfd_stat.st_size % 8), ctx.srcfd_stat.st_size); if (n < 0) return log_error_errno(errno, "Failed to write zero padding: %m"); - if (n != 8 - (st.st_size % 8)) + if (n != 8 - (ctx.srcfd_stat.st_size % 8)) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while writing zero padding."); end += n; } uint32_t certsz = offsetof(WIN_CERTIFICATE, bCertificate) + sigsz; - n = pwrite(dstfd, + n = pwrite(ctx.dstfd, &(WIN_CERTIFICATE) { .wRevision = htole16(0x200), - .wCertificateType = htole16(0x0002), /* PKCS7 signedData */ + .wCertificateType = htole16(0x0002), /* PKCS#7 signedData */ .dwLength = htole32(ROUND_UP(certsz, 8)), }, sizeof(WIN_CERTIFICATE), @@ -685,7 +767,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { end += n; - n = pwrite(dstfd, sig, sigsz, end); + n = pwrite(ctx.dstfd, sig, sigsz, end); if (n < 0) return log_error_errno(errno, "Failed to write signature: %m"); if (n != sigsz) @@ -694,16 +776,16 @@ static int verb_sign(int argc, char *argv[], void *userdata) { end += n; if (certsz % 8 != 0) { - n = pwrite(dstfd, (const uint8_t[8]) {}, 8 - (certsz % 8), end); + n = pwrite(ctx.dstfd, (const uint8_t[8]) {}, 8 - (certsz % 8), end); if (n < 0) return log_error_errno(errno, "Failed to write zero padding: %m"); if ((size_t) n != 8 - (certsz % 8)) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while writing zero padding."); } - n = pwrite(dstfd, + n = pwrite(ctx.dstfd, &(IMAGE_DATA_DIRECTORY) { - .VirtualAddress = certificate_table->VirtualAddress ?: htole32(ROUND_UP(st.st_size, 8)), + .VirtualAddress = certificate_table->VirtualAddress ?: htole32(ROUND_UP(ctx.srcfd_stat.st_size, 8)), .Size = htole32(le32toh(certificate_table->Size) + ROUND_UP(certsz, 8)), }, sizeof(IMAGE_DATA_DIRECTORY), @@ -714,11 +796,11 @@ static int verb_sign(int argc, char *argv[], void *userdata) { return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while updating PE certificate table."); uint32_t checksum; - r = pe_checksum(dstfd, &checksum); + r = pe_checksum(ctx.dstfd, &checksum); if (r < 0) return log_error_errno(r, "Failed to calculate PE file checksum: %m"); - n = pwrite(dstfd, + n = pwrite(ctx.dstfd, &(le32_t) { htole32(checksum) }, sizeof(le32_t), le32toh(dos_header->e_lfanew) + offsetof(PeHeader, optional.CheckSum)); @@ -727,7 +809,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { if ((size_t) n != sizeof(le32_t)) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while updating PE checksum."); - r = link_tmpfile(dstfd, tmp, arg_output, LINK_TMPFILE_REPLACE|LINK_TMPFILE_SYNC); + r = link_tmpfile(ctx.dstfd, ctx.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); @@ -735,10 +817,267 @@ static int verb_sign(int argc, char *argv[], void *userdata) { return 0; } +static int efi_timestamp(EFI_TIME *ret) { + uint64_t epoch = UINT64_MAX; + struct tm tm = {}; + int r; + + assert(ret); + + r = secure_getenv_uint64("SOURCE_DATE_EPOCH", &epoch); + if (r < 0 && r != -ENXIO) + log_debug_errno(r, "Failed to parse $SOURCE_DATE_EPOCH, ignoring: %m"); + + r = localtime_or_gmtime_usec(epoch != UINT64_MAX ? epoch : now(CLOCK_REALTIME), /*utc=*/ true, &tm); + if (r < 0) + return log_error_errno(r, "Failed to convert timestamp to calendar time: %m"); + + *ret = (EFI_TIME) { + .Year = 1900 + tm.tm_year, + /* tm_mon starts at 0, EFI_TIME months start at 1. */ + .Month = tm.tm_mon + 1, + .Day = tm.tm_mday, + .Hour = tm.tm_hour, + .Minute = tm.tm_min, + .Second = tm.tm_sec, + }; + + return 0; +} + +static int populate_secure_boot_database_bio( + const char16_t *db, + const EFI_GUID *guid, + uint32_t attrs, + const EFI_TIME *timestamp, + const EFI_SIGNATURE_LIST *siglist, + size_t siglistsz, + BIO **ret) { + + assert(db); + assert(guid); + assert(timestamp); + assert(siglist); + assert(ret); + + _cleanup_(BIO_freep) BIO *bio = NULL; + bio = BIO_new(BIO_s_mem()); + if (!bio) + return log_oom(); + + /* Don't count the trailing NUL terminator. */ + if (BIO_write(bio, db, char16_strsize(db) - sizeof(char16_t)) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write variable name to bio"); + + if (BIO_write(bio, guid, sizeof(*guid)) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write variable GUID to bio"); + + if (BIO_write(bio, &attrs, sizeof(attrs)) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write variable attributes to bio"); + + if (BIO_write(bio, timestamp, sizeof(*timestamp)) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write timestamp to bio"); + + if (BIO_write(bio, siglist, siglistsz) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write signature list to bio"); + + *ret = TAKE_PTR(bio); + + return 0; +} + +static int verb_sign_secure_boot_database(int argc, char *argv[], void *userdata) { + static const uint32_t attrs = + EFI_VARIABLE_NON_VOLATILE| + EFI_VARIABLE_BOOTSERVICE_ACCESS| + EFI_VARIABLE_RUNTIME_ACCESS| + EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS; + _cleanup_(context_done) Context ctx = CONTEXT_NULL; + _cleanup_(iovec_done) struct iovec signed_data = {}; + _cleanup_(iovec_done) struct iovec signed_data_signature = {}; + int r; + + if (!arg_private_key && !arg_signed_data_signature && !arg_prepare_offline_signing) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "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="); + + + if (!arg_secure_boot_database) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "No secure boot database identifier specified, use --secure-boot-database="); + + if (!STR_IN_SET(arg_secure_boot_database, "PK", "KEK", "db", "dbx")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Secure Boot database identifier '%s' is not valid", arg_secure_boot_database); + + r = context_populate(argc, argv, &ctx); + if (r < 0) + return r; + + if (arg_signed_data) { + r = read_full_file(arg_signed_data, (char**) &signed_data.iov_base, &signed_data.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to read secure boot database signed data file '%s': %m", arg_signed_data); + } + + if (arg_signed_data_signature) { + r = read_full_file(arg_signed_data_signature, (char**) &signed_data_signature.iov_base, &signed_data_signature.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to read secure boot database signature file '%s': %m", arg_signed_data_signature); + } + + _cleanup_free_ uint8_t *dercert = NULL; + int dercertsz = i2d_X509(ctx.certificate, &dercert); + if (dercertsz < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert X.509 certificate to DER: %s", + ERR_error_string(ERR_get_error(), NULL)); + + uint32_t siglistsz = offsetof(EFI_SIGNATURE_LIST, Signatures) + offsetof(EFI_SIGNATURE_DATA, SignatureData) + dercertsz; + /* We use malloc0() to zero-initialize the SignatureOwner field of Signatures[0]. */ + _cleanup_free_ EFI_SIGNATURE_LIST *siglist = malloc0(siglistsz); + if (!siglist) + return log_oom(); + + *siglist = (EFI_SIGNATURE_LIST) { + .SignatureType = EFI_CERT_X509_GUID, + .SignatureListSize = siglistsz, + .SignatureSize = offsetof(EFI_SIGNATURE_DATA, SignatureData) + dercertsz, + }; + + memcpy(siglist->Signatures[0].SignatureData, dercert, dercertsz); + + _cleanup_free_ char16_t *db16 = utf8_to_utf16(arg_secure_boot_database, SIZE_MAX); + if (!db16) + return log_oom(); + + EFI_TIME timestamp; + if (iovec_is_set(&signed_data)) { + /* Don't count the trailing NUL terminator. */ + size_t db16sz = char16_strsize(db16) - sizeof(char16_t); + size_t expectedsz = db16sz + sizeof(EFI_GUID) + sizeof(attrs) + sizeof(timestamp) + siglistsz; + + if (signed_data.iov_len != expectedsz) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), + "The secure boot database signed data file size does not match the expected size (%s != %s)", + FORMAT_BYTES(signed_data.iov_len), + FORMAT_BYTES(expectedsz)); + + /* The signed data includes a timestamp which also has to go in the EFI variable descriptor + * which includes the signature and they have to match, so we extract the timestamp from the + * signed data so we can store it in the EFI variable descriptor later. */ + size_t tsoffset = db16sz + sizeof(EFI_GUID) + sizeof(attrs); + memcpy_safe(×tamp, (uint8_t*) signed_data.iov_base + tsoffset, sizeof(timestamp)); + } else { + r = efi_timestamp(×tamp); + if (r < 0) + return r; + } + + EFI_GUID *guid = STR_IN_SET(arg_secure_boot_database, "PK", "KEK") ? &(EFI_GUID) EFI_GLOBAL_VARIABLE + : &(EFI_GUID) EFI_IMAGE_SECURITY_DATABASE_GUID; + + _cleanup_(BIO_freep) BIO *bio = NULL; + r = populate_secure_boot_database_bio(db16, guid, attrs, ×tamp, siglist, siglistsz, &bio); + if (r < 0) + return r; + + if (arg_prepare_offline_signing) { + char *buf; + long bufsz = BIO_get_mem_data(bio, &buf); + + r = loop_write(ctx.dstfd, buf, bufsz); + if (r < 0) + return log_error_errno(r, "Failed to write secure boot database unsigned data blob to temporary file: %m"); + + r = link_tmpfile(ctx.dstfd, ctx.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 secure boot database unsigned data blob to %s", arg_output); + return 0; + } + + _cleanup_(PKCS7_freep) PKCS7 *p7 = NULL; + PKCS7_SIGNER_INFO *si = NULL; /* avoid false maybe-uninitialized warning */ + r = pkcs7_new(ctx.certificate, ctx.private_key, &p7, &si); + if (r < 0) + return log_error_errno(r, "Failed to allocate PKCS#7 context: %m"); + + if (PKCS7_set_detached(p7, true) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set PKCS#7 detached attribute: %s", + ERR_error_string(ERR_get_error(), NULL)); + + _cleanup_(BIO_free_allp) BIO *p7bio = PKCS7_dataInit(p7, NULL); + if (!p7bio) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to create PKCS#7 data bio: %s", + ERR_error_string(ERR_get_error(), NULL)); + + if (SMIME_crlf_copy(bio, p7bio, PKCS7_BINARY) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to copy unsigned data to PKCS#7 data bio: %s", + ERR_error_string(ERR_get_error(), NULL)); + + if (iovec_is_set(&signed_data_signature)) { + ASN1_STRING_set0(si->enc_digest, + TAKE_PTR(signed_data_signature.iov_base), + signed_data_signature.iov_len); + + if (PKCS7_signatureVerify(p7bio, p7, si, ctx.certificate) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "PKCS#7 signature validation failed: %s", + ERR_error_string(ERR_get_error(), NULL)); + + } else if (PKCS7_dataFinal(p7, p7bio) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to sign PKCS#7 data: %s", + ERR_error_string(ERR_get_error(), NULL)); + + _cleanup_free_ uint8_t *sig = NULL; + int sigsz = i2d_PKCS7(p7, &sig); + if (sigsz < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert PKCS#7 signature to DER: %s", + ERR_error_string(ERR_get_error(), NULL)); + + size_t authsz = offsetof(EFI_VARIABLE_AUTHENTICATION_2, AuthInfo.CertData) + sigsz; + _cleanup_free_ EFI_VARIABLE_AUTHENTICATION_2 *auth = malloc(authsz); + if (!auth) + return log_oom(); + + *auth = (EFI_VARIABLE_AUTHENTICATION_2) { + .TimeStamp = timestamp, + .AuthInfo = { + .Hdr = { + .dwLength = offsetof(WIN_CERTIFICATE_UEFI_GUID, CertData) + sigsz, + .wRevision = 0x0200, + .wCertificateType = 0x0EF1, /* WIN_CERT_TYPE_EFI_GUID */ + }, + .CertType = EFI_CERT_TYPE_PKCS7_GUID, + } + }; + + memcpy(auth->AuthInfo.CertData, sig, sigsz); + + r = loop_write(ctx.dstfd, auth, authsz); + if (r < 0) + return log_error_errno(r, "Failed to write authentication descriptor to secure boot database file: %m"); + + r = loop_write(ctx.dstfd, siglist, siglistsz); + if (r < 0) + return log_error_errno(r, "Failed to write signature list to secure boot database file: %m"); + + r = link_tmpfile(ctx.dstfd, ctx.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 signed secure boot database to %s", arg_output); + return 0; +} + static int run(int argc, char *argv[]) { static const Verb verbs[] = { - { "help", VERB_ANY, VERB_ANY, 0, help }, - { "sign", 2, 2, 0, verb_sign }, + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "sign", 2, 2, 0, verb_sign }, + { "sign-secure-boot-database", 1, 1, 0, verb_sign_secure_boot_database }, {} }; int r; @@ -749,7 +1088,7 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; - return dispatch_verb(argc, argv, verbs, NULL); + return dispatch_verb(argc, argv, verbs, /* userdata= */ NULL); } DEFINE_MAIN_FUNCTION(run); diff --git a/test/units/TEST-74-AUX-UTILS.sbsign.sh b/test/units/TEST-74-AUX-UTILS.sbsign.sh index d7bd4a2c808..addeac5d95e 100755 --- a/test/units/TEST-74-AUX-UTILS.sbsign.sh +++ b/test/units/TEST-74-AUX-UTILS.sbsign.sh @@ -43,8 +43,8 @@ testcase_sign_systemd_boot() { fi SD_BOOT="$(find /usr/lib/systemd/boot/efi/ -name "systemd-boot*.efi" | head -n1)" - (! sbverify --cert /tmp/sb.crt "$SD_BOOT") + /usr/lib/systemd/systemd-sbsign sign --certificate /tmp/sb.crt --private-key /tmp/sb.key --output /tmp/sdboot "$SD_BOOT" sbverify --cert /tmp/sb.crt /tmp/sdboot @@ -60,18 +60,84 @@ testcase_sign_systemd_boot_offline() { fi SD_BOOT="$(find /usr/lib/systemd/boot/efi/ -name "systemd-boot*.efi" | head -n1)" + (! sbverify --cert /tmp/sb.crt "$SD_BOOT") + + export SOURCE_DATE_EPOCH="123" - /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 \ + --private-key /tmp/sb.key \ + --output /tmp/sdboot-signed-online \ + "$SD_BOOT" + + sbverify --cert /tmp/sb.crt /tmp/sdboot-signed-online + + /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 + + # Make sure systemd-sbsign can't pick up the timestamp from the environment when + # attaching the signature. + unset SOURCE_DATE_EPOCH + + /usr/lib/systemd/systemd-sbsign \ + sign \ + --certificate /tmp/sb.crt \ + --output /tmp/sdboot-signed-offline \ --signed-data /tmp/signed-data.bin \ --signed-data-signature /tmp/signed-data.sig \ "$SD_BOOT" - sbverify --cert /tmp/sb.crt /tmp/sdboot + sbverify --cert /tmp/sb.crt /tmp/sdboot-signed-offline + + cmp /tmp/sdboot-signed-online /tmp/sdboot-signed-offline +} + +testcase_sign_secure_boot_database() { + /usr/lib/systemd/systemd-sbsign \ + sign-secure-boot-database \ + --certificate /tmp/sb.crt \ + --private-key /tmp/sb.key \ + --output /tmp/PK.signed \ + --secure-boot-database PK +} + +testcase_sign_secure_boot_database_offline() { + export SOURCE_DATE_EPOCH="123" + + /usr/lib/systemd/systemd-sbsign \ + sign-secure-boot-database \ + --certificate /tmp/sb.crt \ + --private-key /tmp/sb.key \ + --output /tmp/PK.signed-online \ + --secure-boot-database PK + + /usr/lib/systemd/systemd-sbsign \ + sign-secure-boot-database \ + --certificate /tmp/sb.crt \ + --output /tmp/signed-data.bin \ + --prepare-offline-signing \ + --secure-boot-database PK + openssl dgst -sha256 -sign /tmp/sb.key -out /tmp/signed-data.sig /tmp/signed-data.bin + + # Make sure systemd-sbsign can't pick up the timestamp from the environment when + # attaching the signature. + unset SOURCE_DATE_EPOCH + + /usr/lib/systemd/systemd-sbsign \ + sign-secure-boot-database \ + --certificate /tmp/sb.crt \ + --output /tmp/PK.signed-offline \ + --signed-data /tmp/signed-data.bin \ + --signed-data-signature /tmp/signed-data.sig \ + --secure-boot-database PK + + cmp /tmp/PK.signed-offline /tmp/PK.signed-online } run_testcases