diff --git a/man/rules/meson.build b/man/rules/meson.build index 9a383b985e..3454668cab 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1088,6 +1088,10 @@ manpages = [ 'systemd-tmpfiles-setup-dev.service', 'systemd-tmpfiles-setup.service'], ''], + ['systemd-tpm2-setup.service', + '8', + ['systemd-tpm2-setup', 'systemd-tpm2-setup-early.service'], + 'ENABLE_BOOTLOADER'], ['systemd-tty-ask-password-agent', '1', [], ''], ['systemd-udev-settle.service', '8', [], ''], ['systemd-udevd.service', diff --git a/man/systemd-tpm2-setup.service.xml b/man/systemd-tpm2-setup.service.xml new file mode 100644 index 0000000000..77502d5084 --- /dev/null +++ b/man/systemd-tpm2-setup.service.xml @@ -0,0 +1,74 @@ + + + + + + + + systemd-tpm2-setup.service + systemd + + + + systemd-tpm2-setup.service + 8 + + + + systemd-tpm2-setup.service + systemd-tpm2-setup-early.service + systemd-tpm2-setup + Set up the TPM2 Storage Root Key (SRK) at boot + + + + systemd-tpm2-setup.service + /usr/lib/systemd/systemd-tpm2-setup + + + + Description + + systemd-tpm2-setup.service and + systemd-tpm2-setup-early.service are services that generate the Storage Root Key + (SRK) if it hasn't been generated yet, and stores it in the TPM. + + The services will store the public key of the SRK key pair in a PEM file in + /run/systemd/tpm2-srk-public-key.pem and + /var/lib/systemd/tpm2-srk-public-key.pem. + + systemd-tpm2-setup-early.service runs very early at boot (possibly in the + initrd), and writes the SRK public key to /run/systemd/tpm2-srk-public-key.pem (as + /var/ is generally not accessible this early yet), while + systemd-tpm2-setup.service runs during a later boot phase and saves the public key + to /var/lib/systemd/tpm2-srk-public-key.pem. + + + + Files + + + + /run/systemd/tpm2-srk-public-key.pem + + The SRK public key in PEM format, written during early boot. + + + + /var/lib/systemd/tpm2-srk-public-key.pem + + The SRK public key in PEM format, written during later boot (once + /var/ is available). + + + + + + See Also + + systemd1 + + + diff --git a/meson.build b/meson.build index 90705b3158..44eac94fdf 100644 --- a/meson.build +++ b/meson.build @@ -2186,6 +2186,7 @@ subdir('src/sysusers') subdir('src/sysv-generator') subdir('src/timedate') subdir('src/timesync') +subdir('src/tpm2-setup') subdir('src/tmpfiles') subdir('src/tty-ask-password-agent') subdir('src/update-done') diff --git a/src/shared/generator.c b/src/shared/generator.c index aced4949a9..5626587269 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -741,7 +741,7 @@ int generator_write_cryptsetup_unit_section( fprintf(f, "\n" "DefaultDependencies=no\n" - "After=cryptsetup-pre.target systemd-udevd-kernel.socket\n" + "After=cryptsetup-pre.target systemd-udevd-kernel.socket systemd-tpm2-setup-early.service\n" "Before=blockdev@dev-mapper-%%i.target\n" "Wants=blockdev@dev-mapper-%%i.target\n" "IgnoreOnIsolate=true\n"); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 64dcc5503b..3374bd4f78 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -148,7 +148,7 @@ int dlopen_tpm2(void) { DLSYM_ARG(Tss2_MU_TPMT_PUBLIC_Marshal)); } -static void Esys_Freep(void *p) { +void Esys_Freep(void *p) { if (*(void**) p) sym_Esys_Free(*(void**) p); } @@ -1175,7 +1175,7 @@ static int tpm2_get_srk( } /* Get the SRK, creating one if needed. Returns 0 on success, or < 0 on error. */ -static int tpm2_get_or_create_srk( +int tpm2_get_or_create_srk( Tpm2Context *c, const Tpm2Handle *session, TPM2B_PUBLIC **ret_public, @@ -1189,7 +1189,7 @@ static int tpm2_get_or_create_srk( if (r < 0) return r; if (r == 1) - return 0; + return 0; /* 0 → SRK already set up */ /* No SRK, create and persist one */ TPM2B_PUBLIC template = { .size = sizeof(TPMT_PUBLIC), }; @@ -1223,7 +1223,7 @@ static int tpm2_get_or_create_srk( /* This should never happen. */ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "SRK we just persisted couldn't be found."); - return 0; + return 1; /* > 0 → SRK newly set up */ } /* Utility functions for TPMS_PCR_SELECTION. */ diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 7253372293..8df41f001d 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -67,6 +67,8 @@ typedef struct { #define _tpm2_handle(c, h) { .tpm2_context = (c), .esys_handle = (h), } static const Tpm2Handle TPM2_HANDLE_NONE = _tpm2_handle(NULL, ESYS_TR_NONE); +void Esys_Freep(void *p); + int tpm2_handle_new(Tpm2Context *context, Tpm2Handle **ret_handle); Tpm2Handle *tpm2_handle_free(Tpm2Handle *handle); DEFINE_TRIVIAL_CLEANUP_FUNC(Tpm2Handle*, tpm2_handle_free); @@ -188,6 +190,8 @@ int tpm2_calculate_sealing_policy(const Tpm2PCRValue *pcr_values, size_t n_pcr_v int tpm2_marshal_blob(const TPM2B_PUBLIC *public, const TPM2B_PRIVATE *private, void **ret_blob, size_t *ret_blob_size); int tpm2_unmarshal_blob(const void *blob, size_t blob_size, TPM2B_PUBLIC *ret_public, TPM2B_PRIVATE *ret_private); +int tpm2_get_or_create_srk(Tpm2Context *c, const Tpm2Handle *session, TPM2B_PUBLIC **ret_public, TPM2B_NAME **ret_name, TPM2B_NAME **ret_qname, Tpm2Handle **ret_handle); + int tpm2_seal(Tpm2Context *c, const TPM2B_DIGEST *policy, const char *pin, void **ret_secret, size_t *ret_secret_size, void **ret_blob, size_t *ret_blob_size, uint16_t *ret_primary_alg, void **ret_srk_buf, size_t *ret_srk_buf_size); int tpm2_unseal(const char *device, uint32_t hash_pcr_mask, uint16_t pcr_bank, const void *pubkey, size_t pubkey_size, uint32_t pubkey_pcr_mask, JsonVariant *signature, const char *pin, uint16_t primary_alg, const void *blob, size_t blob_size, const void *policy_hash, size_t policy_hash_size, const void *srk_buf, size_t srk_buf_size, void **ret_secret, size_t *ret_secret_size); diff --git a/src/tpm2-setup/meson.build b/src/tpm2-setup/meson.build new file mode 100644 index 0000000000..c85721c98e --- /dev/null +++ b/src/tpm2-setup/meson.build @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + libexec_template + { + 'name' : 'systemd-tpm2-setup', + 'sources' : files('tpm2-setup.c'), + 'conditions' : [ + 'ENABLE_BOOTLOADER', + 'HAVE_OPENSSL', + 'HAVE_TPM2', + ], + 'dependencies' : [ + libopenssl, + ], + }, +] diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c new file mode 100644 index 0000000000..b88e7ec82f --- /dev/null +++ b/src/tpm2-setup/tpm2-setup.c @@ -0,0 +1,327 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "build.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "hexdecoct.h" +#include "main-func.h" +#include "mkdir.h" +#include "parse-util.h" +#include "pretty-print.h" +#include "terminal-util.h" +#include "tmpfile-util.h" +#include "tpm2-util.h" + +static char *arg_tpm2_device = NULL; +static bool arg_early = false; + +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); + +#define TPM2_SRK_PEM_PERSISTENT_PATH "/var/lib/systemd/tpm2-srk-public-key.pem" +#define TPM2_SRK_PEM_RUNTIME_PATH "/run/systemd/tpm2-srk-public-key.pem" + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-tpm2-setup", "8", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] COMMAND\n" + "\n%5$sSet up the TPM2 Storage Root Key (SRK).%6$s\n" + "\n%3$sOptions:%4$s\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --tpm2-device=PATH\n" + " Pick TPM2 device\n" + " --early=BOOL Store SRK public key in /run/ rather than /var/lib/\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_TPM2_DEVICE, + ARG_EARLY, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, + { "early", required_argument, NULL, ARG_EARLY }, + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + + case 'h': + return help(0, NULL, NULL); + + case ARG_VERSION: + return version(); + + case ARG_TPM2_DEVICE: + if (streq(optarg, "list")) + return tpm2_list_devices(); + + if (free_and_strdup(&arg_tpm2_device, streq(optarg, "auto") ? NULL : optarg) < 0) + return log_oom(); + + break; + + case ARG_EARLY: + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --early= argument: %s", optarg); + + arg_early = r; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (optind != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program expects no argument."); + + return 1; +} + +struct public_key_data { + EVP_PKEY *pkey; + void *fingerprint; + size_t fingerprint_size; + char *fingerprint_hex; + char *path; +}; + +static void public_key_data_done(struct public_key_data *d) { + assert(d); + + if (d->pkey) { + EVP_PKEY_free(d->pkey); + d->pkey = NULL; + } + d->fingerprint = mfree(d->fingerprint); + d->fingerprint_size = 0; + d->fingerprint_hex = mfree(d->fingerprint_hex); + d->path = mfree(d->path); +} + +static int public_key_make_fingerprint(struct public_key_data *d) { + int r; + + assert(d); + assert(d->pkey); + assert(!d->fingerprint); + assert(!d->fingerprint_hex); + + r = pubkey_fingerprint(d->pkey, EVP_sha256(), &d->fingerprint, &d->fingerprint_size); + if (r < 0) + return log_error_errno(r, "Failed to calculate fingerprint of public key: %m"); + + d->fingerprint_hex = hexmem(d->fingerprint, d->fingerprint_size); + if (!d->fingerprint_hex) + return log_oom(); + + return 0; +} + +static int load_public_key_disk(const char *path, struct public_key_data *ret) { + _cleanup_(public_key_data_done) struct public_key_data data = {}; + _cleanup_free_ char *blob = NULL; + size_t blob_size; + int r; + + assert(path); + assert(ret); + + r = read_full_file(path, &blob, &blob_size); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Failed to read '%s': %m", path); + + log_debug("SRK public key file '%s' does not exist.", path); + } else { + log_debug("Loaded SRK public key from '%s'.", path); + + r = openssl_pkey_from_pem(blob, blob_size, &data.pkey); + if (r < 0) + return log_error_errno(r, "Failed to parse SRK public key file '%s': %m", path); + + r = public_key_make_fingerprint(&data); + if (r < 0) + return r; + + log_debug("Loaded SRK public key fingerprint: %s", data.fingerprint_hex); + } + + data.path = strdup(path); + if (!data.path) + return log_oom(); + + *ret = data; + data = (struct public_key_data) {}; + + return 0; +} + +static int load_public_key_tpm2(struct public_key_data *ret) { + _cleanup_(public_key_data_done) struct public_key_data data = {}; + _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; + _cleanup_(Esys_Freep) TPM2B_PUBLIC *public = NULL; + int r; + + assert(ret); + + r = tpm2_context_new(arg_tpm2_device, &c); + if (r < 0) + return r; + + r = tpm2_get_or_create_srk( + c, + /* session= */ NULL, + &public, + /* ret_name= */ NULL, + /* ret_qname= */ NULL, + NULL); + if (r < 0) + return r; + if (r > 0) + log_info("New SRK generated and stored in the TPM."); + else + log_info("SRK already stored in the TPM."); + + r = tpm2_tpm2b_public_to_openssl_pkey(public, &data.pkey); + if (r < 0) + return log_error_errno(r, "Failed to convert TPM2 SRK public key to OpenSSL public key: %m"); + + r = public_key_make_fingerprint(&data); + if (r < 0) + return r; + + log_info("SRK fingerprint is %s.", data.fingerprint_hex); + + *ret = data; + data = (struct public_key_data) {}; + + return 0; +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + umask(0022); + + _cleanup_(public_key_data_done) struct public_key_data runtime_key = {}, persistent_key = {}, tpm2_key = {}; + + r = load_public_key_disk(TPM2_SRK_PEM_RUNTIME_PATH, &runtime_key); + if (r < 0) + return r; + + if (!arg_early) { + r = load_public_key_disk(TPM2_SRK_PEM_PERSISTENT_PATH, &persistent_key); + if (r < 0) + return r; + + if (runtime_key.pkey && persistent_key.pkey && + memcmp_nn(runtime_key.fingerprint, runtime_key.fingerprint_size, + persistent_key.fingerprint, persistent_key.fingerprint_size) != 0) { + + /* One of those days we might want to add a stricter policy option here, that refuses + * to boot when the SRK changes. For now, let's just warn and proceed, in order not + * to break OS images that are moved around PCs. */ + + log_notice("Saved persistent SRK (%s) and runtime SRK differ (fingerprint %s vs. %s), updating persistent SRK.", + persistent_key.path, persistent_key.fingerprint_hex, runtime_key.fingerprint_hex); + + public_key_data_done(&persistent_key); + } + } + + r = load_public_key_tpm2(&tpm2_key); + if (r < 0) + return r; + + assert(tpm2_key.pkey); + + if (runtime_key.pkey) { + if (memcmp_nn(tpm2_key.fingerprint, tpm2_key.fingerprint_size, + runtime_key.fingerprint, runtime_key.fingerprint_size) != 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Saved runtime SRK differs from TPM SRK, refusing."); + + if (arg_early) { + log_info("SRK saved in '%s' matches SRK in TPM2.", runtime_key.path); + return 0; + } + } + + if (persistent_key.pkey) { + if (memcmp_nn(tpm2_key.fingerprint, tpm2_key.fingerprint_size, + persistent_key.fingerprint, persistent_key.fingerprint_size) == 0) { + log_info("SRK saved in '%s' matches SRK in TPM2.", persistent_key.path); + return 0; + } + + /* As above, we probably want a stricter policy option here, one day. */ + + log_notice("Saved persistent SRK (%s) and TPM SRK differ (fingerprint %s vs. %s), updating persistent SRK.", + persistent_key.path, persistent_key.fingerprint_hex, tpm2_key.fingerprint_hex); + + public_key_data_done(&persistent_key); + } + + const char *path = arg_early ? TPM2_SRK_PEM_RUNTIME_PATH : TPM2_SRK_PEM_PERSISTENT_PATH; + + (void) mkdir_parents(path, 0755); + + /* Write out public key (note that we only do that as a help to the user, we don't make use of this ever */ + _cleanup_(unlink_and_freep) char *t = NULL; + _cleanup_fclose_ FILE *f = NULL; + r = fopen_tmpfile_linkable(path, O_WRONLY, &t, &f); + if (r < 0) + return log_error_errno(r, "Failed to open SRK public key file '%s' for writing: %m", path); + + if (PEM_write_PUBKEY(f, tpm2_key.pkey) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write SRK public key file '%s'.", path); + + if (fchmod(fileno(f), 0444) < 0) + return log_error_errno(errno, "Failed to adjust access mode of SRK public key file '%s' to 0444: %m", path); + + r = flink_tmpfile(f, t, path, LINK_TMPFILE_SYNC|LINK_TMPFILE_REPLACE); + if (r < 0) + return log_error_errno(r, "Failed to move SRK public key file to '%s': %m", path); + + log_info("SRK public key saved to '%s'.", path); + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/test/units/testsuite-70.sh b/test/units/testsuite-70.sh index 7f7183da08..8358c48434 100755 --- a/test/units/testsuite-70.sh +++ b/test/units/testsuite-70.sh @@ -5,6 +5,7 @@ set -o pipefail SD_MEASURE="/usr/lib/systemd/systemd-measure" SD_PCREXTEND="/usr/lib/systemd/systemd-pcrextend" +SD_TPM2SETUP="/usr/lib/systemd/systemd-tpm2-setup" export SYSTEMD_LOG_LEVEL=debug cryptsetup_has_token_plugin_support() { @@ -372,4 +373,12 @@ systemd-cryptenroll --tpm2-pcrs=boot-loader-code+boot-loader-config "$img" (! systemd-cryptenroll --wipe-slot=10240000 "$img") (! systemd-cryptenroll --fido2-device=auto --unlock-fido2-device=auto "$img") +# Run this, just to get sanitizer coverage. The tools should be idempotent, hence run the multiple times. +if [[ -x "$SD_TPM2SETUP" ]]; then + "$SD_TPM2SETUP" --early=yes + "$SD_TPM2SETUP" --early=yes + "$SD_TPM2SETUP" --early=no + "$SD_TPM2SETUP" --early=no +fi + touch /testok diff --git a/units/meson.build b/units/meson.build index 2fb87934b8..e1de2b4a6d 100644 --- a/units/meson.build +++ b/units/meson.build @@ -458,6 +458,16 @@ units = [ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], 'symlinks' : ['sysinit.target.wants/'], }, + { + 'file' : 'systemd-tpm2-setup.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + 'symlinks' : ['sysinit.target.wants/'], + }, + { + 'file' : 'systemd-tpm2-setup-early.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + 'symlinks' : ['sysinit.target.wants/'], + }, { 'file' : 'systemd-portabled.service.in', 'conditions' : ['ENABLE_PORTABLED'], diff --git a/units/systemd-tpm2-setup-early.service.in b/units/systemd-tpm2-setup-early.service.in new file mode 100644 index 0000000000..c1597ea3f9 --- /dev/null +++ b/units/systemd-tpm2-setup-early.service.in @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=TPM2 SRK Setup (Early) +Documentation=man:systemd-tpm2-setup.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=sysinit.target shutdown.target +ConditionSecurity=measured-uki +ConditionPathExists=!/run/systemd/tpm2-srk-public-key.pem + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-tpm2-setup --early=yes diff --git a/units/systemd-tpm2-setup.service.in b/units/systemd-tpm2-setup.service.in new file mode 100644 index 0000000000..6c99f3af0a --- /dev/null +++ b/units/systemd-tpm2-setup.service.in @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=TPM2 SRK Setup +Documentation=man:systemd-tpm2-setup.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=systemd-tpm2-setup-early.service systemd-remount-fs.service +Before=sysinit.target shutdown.target +RequiresMountsFor=/var/lib/systemd/tpm2-srk-public-key.pem +ConditionSecurity=measured-uki +ConditionPathExists=!/etc/initrd-release + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-tpm2-setup