From 4b1ad0398e7b0524eac87e1b6c4fdcb8c2c40294 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 8 Nov 2024 11:34:21 +0100 Subject: [PATCH] Introduce systemd-keyutil to do various key/certificate operations Let's gather generic key/certificate operations in a new tool systemd-keyutil instead of spreading them across various special purpose tools. Fixes #35087 --- man/rules/meson.build | 1 + man/systemd-keyutil.xml | 105 +++++++++ man/systemd-measure.xml | 10 - man/systemd-sbsign.xml | 16 -- meson.build | 3 +- src/keyutil/keyutil.c | 292 ++++++++++++++++++++++++ src/keyutil/meson.build | 12 + src/measure/measure.c | 89 -------- src/sbsign/sbsign.c | 54 ----- src/ukify/ukify.py | 12 +- test/units/TEST-74-AUX-UTILS.keyutil.sh | 50 ++++ test/units/TEST-74-AUX-UTILS.sbsign.sh | 4 - 12 files changed, 468 insertions(+), 180 deletions(-) create mode 100644 man/systemd-keyutil.xml create mode 100644 src/keyutil/keyutil.c create mode 100644 src/keyutil/meson.build create mode 100755 test/units/TEST-74-AUX-UTILS.keyutil.sh diff --git a/man/rules/meson.build b/man/rules/meson.build index 7d2c62f5742..e76cb0223b5 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -992,6 +992,7 @@ manpages = [ 'systemd-journald@.service', 'systemd-journald@.socket'], ''], + ['systemd-keyutil', '1', [], ''], ['systemd-localed.service', '8', ['systemd-localed'], 'ENABLE_LOCALED'], ['systemd-logind.service', '8', ['systemd-logind'], 'ENABLE_LOGIND'], ['systemd-machine-id-commit.service', '8', [], ''], diff --git a/man/systemd-keyutil.xml b/man/systemd-keyutil.xml new file mode 100644 index 00000000000..99d4d903b4b --- /dev/null +++ b/man/systemd-keyutil.xml @@ -0,0 +1,105 @@ + + + + + + + systemd-keyutil + systemd + + + + systemd-keyutil + 1 + + + + systemd-keyutil + Perform various operations on private keys and X.509 certificates + + + + + systemd-keyutil + OPTIONS + COMMAND + + + + + Description + + systemd-keyutil can be used to perform various operations on private keys and + X.509 certificates. + + + + Commands + + + + + + Checks that we can load the private key and certificate specified with + and respectively. + + As a side effect, if the private key is loaded from a PIN-protected hardware token, this + command can be used to cache the PIN in the kernel keyring. The + $SYSTEMD_ASK_PASSWORD_KEYRING_TIMEOUT_SEC and + $SYSTEMD_ASK_PASSWORD_KEYRING_TYPE environment variables can be used to control + how long and in which kernel keyring the PIN is cached. + + + + + + + public + + This commands prints the public key in PEM format extracted from either the + certificate given with or the private key given with + . + + + + + + + + Options + The following options are understood: + + + + + + + + + Set the private key and certificate to use. 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. + + + + + + + + + + + See Also + + systemd-sbsign1 + systemd-measure1 + + + diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml index 5ca373f1814..c7e5a5e9e21 100644 --- a/man/systemd-measure.xml +++ b/man/systemd-measure.xml @@ -104,16 +104,6 @@ - - - pcrpkey - - This commands prints the public key either given with , - or extracted from the certificate given with or the private key given - with . - - - diff --git a/man/systemd-sbsign.xml b/man/systemd-sbsign.xml index 1248377845f..57b685f8c38 100644 --- a/man/systemd-sbsign.xml +++ b/man/systemd-sbsign.xml @@ -49,22 +49,6 @@ - - - - - Checks that we can load the private key specified with - . - - As a side effect, if the private key is loaded from a PIN-protected hardware token, this - command can be used to cache the PIN in the kernel keyring. The - $SYSTEMD_ASK_PASSWORD_KEYRING_TIMEOUT_SEC and - $SYSTEMD_ASK_PASSWORD_KEYRING_TYPE environment variables can be used to control - how long and in which kernel keyring the PIN is cached. - - - - diff --git a/meson.build b/meson.build index 6b62dfa052d..fbab9300c54 100644 --- a/meson.build +++ b/meson.build @@ -2377,6 +2377,7 @@ subdir('src/integritysetup') subdir('src/journal') subdir('src/journal-remote') subdir('src/kernel-install') +subdir('src/keyutil') subdir('src/locale') subdir('src/login') subdir('src/machine') @@ -2698,7 +2699,7 @@ endif mkosi_depends = public_programs -foreach executable : ['systemd-journal-remote', 'systemd-measure'] +foreach executable : ['systemd-journal-remote', 'systemd-measure', 'systemd-sbsign', 'systemd-keyutil'] if executable in executables_by_name mkosi_depends += [executables_by_name[executable]] endif diff --git a/src/keyutil/keyutil.c b/src/keyutil/keyutil.c new file mode 100644 index 00000000000..70176c76c73 --- /dev/null +++ b/src/keyutil/keyutil.c @@ -0,0 +1,292 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "alloc-util.h" +#include "ask-password-api.h" +#include "build.h" +#include "fd-util.h" +#include "main-func.h" +#include "memstream-util.h" +#include "openssl-util.h" +#include "parse-argument.h" +#include "pretty-print.h" +#include "verbs.h" + +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 char *arg_certificate = NULL; +static char *arg_certificate_source = NULL; +static CertificateSourceType arg_certificate_source_type = OPENSSL_CERTIFICATE_SOURCE_FILE; + +STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep); +STATIC_DESTRUCTOR_REGISTER(arg_private_key_source, freep); +STATIC_DESTRUCTOR_REGISTER(arg_certificate, freep); +STATIC_DESTRUCTOR_REGISTER(arg_certificate_source, freep); + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-keyutil", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] COMMAND ...\n" + "\n%5$sPerform various operations on private keys and certificates.%6$s\n" + "\n%3$sCommands:%4$s\n" + " validate Load and validate the given certificate and private key\n" + " public Extract a public key\n" + "\n%3$sOptions:%4$s\n" + " -h --help Show this help\n" + " --version Print version\n" + " --private-key=KEY Private key in PEM format\n" + " --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" + " --certificate=PATH|URI\n" + " PEM certificate to use for signing, or a provider\n" + " specific designation if --certificate-source= is used\n" + " --certificate-source=file|provider:PROVIDER\n" + " Specify how to interpret the certificate from\n" + " --certificate=. Allows the certificate to be loaded\n" + " from an OpenSSL provider\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), + ansi_normal(), + ansi_highlight(), + ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_PRIVATE_KEY, + ARG_PRIVATE_KEY_SOURCE, + ARG_CERTIFICATE, + ARG_CERTIFICATE_SOURCE, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "private-key", required_argument, NULL, ARG_PRIVATE_KEY }, + { "private-key-source", required_argument, NULL, ARG_PRIVATE_KEY_SOURCE }, + { "certificate", required_argument, NULL, ARG_CERTIFICATE }, + { "certificate-source", required_argument, NULL, ARG_CERTIFICATE_SOURCE }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + + case 'h': + help(0, NULL, NULL); + return 0; + + case ARG_VERSION: + return version(); + + case ARG_PRIVATE_KEY: + r = free_and_strdup_warn(&arg_private_key, optarg); + if (r < 0) + return r; + + break; + + case ARG_PRIVATE_KEY_SOURCE: + r = parse_openssl_key_source_argument( + optarg, + &arg_private_key_source, + &arg_private_key_source_type); + if (r < 0) + return r; + + break; + + case ARG_CERTIFICATE: + r = free_and_strdup_warn(&arg_certificate, optarg); + if (r < 0) + return r; + break; + + case ARG_CERTIFICATE_SOURCE: + r = parse_openssl_certificate_source_argument( + optarg, + &arg_certificate_source, + &arg_certificate_source_type); + if (r < 0) + return r; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (arg_private_key_source && !arg_certificate) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "When using --private-key-source=, --certificate= must be specified."); + + return 1; +} + +static int verb_validate(int argc, char *argv[], void *userdata) { + _cleanup_(X509_freep) X509 *certificate = NULL; + _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; + _cleanup_(EVP_PKEY_freep) EVP_PKEY *private_key = NULL; + int r; + + if (!arg_certificate) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "No certificate specified, use --certificate="); + + if (!arg_private_key) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "No private key specified, use --private-key=."); + + 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_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) { + .id = "keyutil-private-key-pin", + .keyring = arg_private_key, + .credential = "keyutil.private-key-pin", + }, + &private_key, + &ui); + if (r < 0) + return log_error_errno(r, "Failed to load private key from %s: %m", arg_private_key); + + puts("OK"); + return 0; +} + +static int verb_public(int argc, char *argv[], void *userdata) { + _cleanup_(EVP_PKEY_freep) EVP_PKEY *public_key = NULL; + int r; + + if (arg_certificate) { + _cleanup_(X509_freep) X509 *certificate = NULL; + + 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); + + public_key = X509_get_pubkey(certificate); + if (!public_key) + return log_error_errno( + SYNTHETIC_ERRNO(EIO), + "Failed to extract public key from certificate %s.", + arg_certificate); + + } else if (arg_private_key) { + _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; + _cleanup_(EVP_PKEY_freep) EVP_PKEY *private_key = NULL; + + 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) { + .id = "keyutil-private-key-pin", + .keyring = arg_private_key, + .credential = "keyutil.private-key-pin", + }, + &private_key, + &ui); + if (r < 0) + return log_error_errno(r, "Failed to load private key from %s: %m", arg_private_key); + + _cleanup_(memstream_done) MemStream m = {}; + FILE *tf = memstream_init(&m); + if (!tf) + return log_oom(); + + if (i2d_PUBKEY_fp(tf, private_key) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to extract public key from private key file '%s'.", arg_private_key); + + fflush(tf); + rewind(tf); + + if (!d2i_PUBKEY_fp(tf, &public_key)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to parse extracted public key of private key file '%s'.", arg_private_key); + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "One of --certificate=, or --private-key= must be specified"); + + if (PEM_write_PUBKEY(stdout, public_key) == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write public key to stdout"); + + return 0; +} + +static int run(int argc, char *argv[]) { + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "validate", VERB_ANY, 1, 0, verb_validate }, + { "public", VERB_ANY, 1, 0, verb_public }, + {} + }; + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/keyutil/meson.build b/src/keyutil/meson.build new file mode 100644 index 00000000000..956f6039895 --- /dev/null +++ b/src/keyutil/meson.build @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + libexec_template + { + 'name' : 'systemd-keyutil', + 'conditions' : [ + 'HAVE_OPENSSL', + ], + 'sources' : files('keyutil.c'), + 'dependencies' : libopenssl, + }, +] diff --git a/src/measure/measure.c b/src/measure/measure.c index ac294d28b36..979426c18fd 100644 --- a/src/measure/measure.c +++ b/src/measure/measure.c @@ -77,7 +77,6 @@ static int help(int argc, char *argv[], void *userdata) { " status Show current PCR values\n" " calculate Calculate expected PCR values\n" " sign Calculate and sign expected PCR values\n" - " pcrpkey Extract the PCR public key\n" "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" " --version Print version\n" @@ -1174,100 +1173,12 @@ static int verb_status(int argc, char *argv[], void *userdata) { return 0; } -static int verb_pcrpkey(int argc, char *argv[], void *userdata) { - _cleanup_(EVP_PKEY_freep) EVP_PKEY *public_key = NULL; - int r; - - if (arg_public_key) { - _cleanup_fclose_ FILE *public_keyf = NULL; - - public_keyf = fopen(arg_public_key, "re"); - if (!public_keyf) - return log_error_errno(errno, "Failed to open public key file '%s': %m", arg_public_key); - - public_key = PEM_read_PUBKEY(public_keyf, NULL, NULL, NULL); - if (!public_key) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key '%s'.", arg_public_key); - - } else if (arg_certificate) { - _cleanup_(X509_freep) X509 *certificate = NULL; - - 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); - - public_key = X509_get_pubkey(certificate); - if (!public_key) - return log_error_errno( - SYNTHETIC_ERRNO(EIO), - "Failed to extract public key from certificate %s.", - arg_certificate); - - } else if (arg_private_key) { - _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; - _cleanup_(EVP_PKEY_freep) EVP_PKEY *private_key = NULL; - - 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) { - .id = "measure-private-key-pin", - .keyring = arg_private_key, - .credential = "measure.private-key-pin", - }, - &private_key, - &ui); - if (r < 0) - return log_error_errno(r, "Failed to load private key from %s: %m", arg_private_key); - - _cleanup_(memstream_done) MemStream m = {}; - FILE *tf = memstream_init(&m); - if (!tf) - return log_oom(); - - if (i2d_PUBKEY_fp(tf, private_key) != 1) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to extract public key from private key file '%s'.", arg_private_key); - - fflush(tf); - rewind(tf); - - if (!d2i_PUBKEY_fp(tf, &public_key)) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to parse extracted public key of private key file '%s'.", arg_private_key); - } else - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "One of --public-key=, --certificate=, or --private-key= must be specified"); - - if (PEM_write_PUBKEY(stdout, public_key) == 0) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write public key to stdout"); - - return 0; -} - static int measure_main(int argc, char *argv[]) { static const Verb verbs[] = { { "help", VERB_ANY, VERB_ANY, 0, help }, { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status }, { "calculate", VERB_ANY, 1, 0, verb_calculate }, { "sign", VERB_ANY, 1, 0, verb_sign }, - { "pcrpkey", VERB_ANY, 1, 0, verb_pcrpkey }, {} }; diff --git a/src/sbsign/sbsign.c b/src/sbsign/sbsign.c index d65f28b4c4e..81970a7302b 100644 --- a/src/sbsign/sbsign.c +++ b/src/sbsign/sbsign.c @@ -45,7 +45,6 @@ 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" - " validate-key Load and validate the given certificate and private key\n" "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" " --version Print version\n" @@ -498,63 +497,10 @@ static int verb_sign(int argc, char *argv[], void *userdata) { return 0; } -static int verb_validate_key(int argc, char *argv[], void *userdata) { - _cleanup_(X509_freep) X509 *certificate = NULL; - _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; - _cleanup_(EVP_PKEY_freep) EVP_PKEY *private_key = NULL; - int r; - - if (!arg_certificate) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "No certificate specified, use --certificate="); - - if (!arg_private_key) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "No private key specified, use --private-key=."); - - 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_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) { - .id = "sbsign-private-key-pin", - .keyring = arg_private_key, - .credential = "sbsign.private-key-pin", - }, - &private_key, - &ui); - if (r < 0) - return log_error_errno(r, "Failed to load private key from %s: %m", arg_private_key); - - puts("OK"); - 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 }, - { "validate-key", VERB_ANY, 1, 0, verb_validate_key }, {} }; int r; diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 355e3f99f41..ae3e1d03b4a 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -1017,8 +1017,8 @@ def make_uki(opts: UkifyConfig) -> None: pcrpkey: Union[bytes, Path, None] = opts.pcrpkey if pcrpkey is None: - measure_tool = find_tool('systemd-measure', '/usr/lib/systemd/systemd-measure') - cmd = [measure_tool, 'pcrpkey'] + measure_tool = find_tool('systemd-keyutil', '/usr/lib/systemd/systemd-keyutil') + cmd = [measure_tool, 'public'] if opts.pcr_public_keys and len(opts.pcr_public_keys) == 1: # If we're using an engine or provider, the public key will be an X.509 certificate. @@ -1026,11 +1026,11 @@ def make_uki(opts: UkifyConfig) -> None: cmd += ['--certificate', opts.pcr_public_keys[0]] if opts.certificate_provider: cmd += ['--certificate-source', f'provider:{opts.certificate_provider}'] - else: - cmd += ['--public-key', opts.pcr_public_keys[0]] - print('+', shell_join(cmd)) - pcrpkey = subprocess.check_output(cmd) + print('+', shell_join(cmd)) + pcrpkey = subprocess.check_output(cmd) + else: + pcrpkey = Path(opts.pcr_public_keys[0]) elif opts.pcr_private_keys and len(opts.pcr_private_keys) == 1: cmd += ['--private-key', Path(opts.pcr_private_keys[0])] diff --git a/test/units/TEST-74-AUX-UTILS.keyutil.sh b/test/units/TEST-74-AUX-UTILS.keyutil.sh new file mode 100755 index 00000000000..bbbbf9fd676 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.keyutil.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +if ! command -v /usr/lib/systemd/systemd-keyutil >/dev/null; then + echo "systemd-keyutil not found, skipping." + exit 0 +fi + +cat >/tmp/openssl.conf <