diff --git a/catalog/systemd.catalog.in b/catalog/systemd.catalog.in index 8cd284c195..56307003f9 100644 --- a/catalog/systemd.catalog.in +++ b/catalog/systemd.catalog.in @@ -527,3 +527,15 @@ Support: %SUPPORT_URL% For the first time during the current boot an NTP synchronization has been acquired and the local system clock adjustment has been initiated. + +-- 3f7d5ef3e54f4302b4f0b143bb270cab +Subject: TPM PCR Extended +Defined-By: systemd +Support: %SUPPORT_URL% + +The string '@MEASURING@' has been extended into Trusted Platform Module's (TPM) +Platform Configuration Register (PCR) @PCR@, on banks @BANKS@. + +Whenever the system transitions to a new runtime phase, a different string is +extended into the specified PCR, to ensure that security policies for TPM-bound +secrets and other resources are limited to specific phases of the runtime. diff --git a/man/rules/meson.build b/man/rules/meson.build index 4f3fe0da7c..a250326a4d 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -966,6 +966,10 @@ manpages = [ ['systemd-nspawn', '1', [], ''], ['systemd-oomd.service', '8', ['systemd-oomd'], 'ENABLE_OOMD'], ['systemd-path', '1', [], ''], + ['systemd-pcrphase.service', + '8', + ['systemd-pcrphase', 'systemd-pcrphase-initrd.service'], + 'HAVE_GNU_EFI'], ['systemd-portabled.service', '8', ['systemd-portabled'], 'ENABLE_PORTABLED'], ['systemd-pstore.service', '8', ['systemd-pstore'], 'ENABLE_PSTORE'], ['systemd-quotacheck.service', diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml index 69ac348184..ab5c9787d6 100644 --- a/man/systemd-measure.xml +++ b/man/systemd-measure.xml @@ -250,7 +250,8 @@ systemd-stub7, objcopy1, systemd-creds1, - systemd-cryptsetup@.service8 + systemd-cryptsetup@.service8, + systemd-pcrphase.service1 diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml new file mode 100644 index 0000000000..61f1fe06fd --- /dev/null +++ b/man/systemd-pcrphase.service.xml @@ -0,0 +1,134 @@ + + + + + + + + systemd-pcrphase.service + systemd + + + + systemd-pcrphase.service + 8 + + + + systemd-pcrphase.service + systemd-pcrphase-initrd.service + systemd-pcrphase + Mark current boot process as successful + + + + systemd-pcrphase.service + systemd-pcrphase-initrd.service + /usr/lib/systemd/system-pcrphase STRING + + + + Description + + systemd-pcrphase.service and + systemd-pcrphase-initrd.service are system services that measure specific strings + into TPM2 PCR 11 during boot. + + These services require + systemd-stub7 to be + used in a unified kernel image (UKI) setup. They execute no operation when invoked when the stub has not + been used to invoke the kernel. The stub will measure the invoked kernel and associated vendor resources + into PCR 11 before handing control to it; once userspace is invoked these services then will extend + certain literal strings indicating various phases of the boot process into TPM2 PCR 11. During a regular + boot process the following strings are extended into PCR 11. + + + enter-initrd is extended into PCR 11 early when the initrd + initializes, before activating system extension images for the initrd. It is supposed to act as barrier + between the time where the kernel initializes, and where the initrd starts operating and enables + system extension images, i.e. code shipped outside of the UKI. (This string is extended at start of + systemd-pcrphase-initrd.service.) + + leave-initrd is extended into PCR 11 when the initrd is about to + transition into the host file system, i.e. when it achieved its purpose. It is supposed to act as + barrier between kernel/initrd code and host OS code. (This string is extended at stop of + systemd-pcrphase-initrd.service.) + + ready is extended into PCR 11 during later boot-up, after remote + file systems have been activated (i.e. after remote-fs.target), but before users + are permitted to log in (i.e. before systemd-user-sessions.service). It is + supposed to act as barrier between the time where unprivileged regular users are still prohibited to + log in and where they are allowed to log in. (This string is extended at start of + systemd-pcrphase.service.) + + shutdown is extended into PCR 11 during system shutdown. It is + supposed to act as barrier between the time the system is fully up and running and where it is about to + shut down. (This string is extended at stop of + systemd-pcrphase.service.) + + + During a regular system lifecycle, the strings enter-initrd → + leave-initrdreadyshutdown are extended into + PCR 11, one after the other. + + Specific phases of the boot process may be referenced via the series of strings measured, separated + by colons (the "boot path"). For example, the boot path for the regular system runtime is + enter-initrd:leave-initrd:ready, while the one for the initrd is just + enter-initrd. The boot path for the the boot phase before the initrd, is an empty + string; because that's hard to pass around a single colon (:) may be used + instead. Note that the aforementioned four strings are just the default strings and individual systems + might measure other strings at other times, and thus implement different and more fine-grained boot + phases to bind policy to. + + By binding policy of TPM2 objects to a specific boot path it is possible to restrict access to them + to specific phases of the boot process, for example making it impossible to access the root file system's + encryption key after the system transitioned from the initrd into the host root file system. + + Use + systemd-measure1 to + pre-calculate expected PCR 11 values for specific boot phases (via the switch). + + + + Options + + The /usr/lib/systemd/system-pcrphase executable may also be invoked from the + command line, where it expects the word to extend into PCR 11, as well as the following switches: + + + + + + Takes the PCR banks to extend the specified word into. If not specified the tool + automatically determines all enabled PCR banks and measures the word into all of + them. + + + + PATH + + Controls which TPM2 device to use. Expects a device node path referring to the TPM2 + chip (e.g. /dev/tpmrm0). Alternatively the special value auto + may be specified, in order to automatically determine the device node of a suitable TPM2 device (of + which there must be exactly one). The special value list may be used to enumerate + all suitable TPM2 devices currently discovered. + + + + + + + + + + See Also + + systemd1, + systemd-stub7, + systemd-measure1 + + + + diff --git a/meson.build b/meson.build index a1bedc220b..6022617832 100644 --- a/meson.build +++ b/meson.build @@ -2566,6 +2566,15 @@ if conf.get('HAVE_BLKID') == 1 and conf.get('HAVE_GNU_EFI') == 1 install_rpath : rootpkglibdir, install : true, install_dir : rootlibexecdir) + executable( + 'systemd-pcrphase', + 'src/boot/pcrphase.c', + include_directories : includes, + link_with : [libshared], + dependencies : [libopenssl, tpm2], + install_rpath : rootpkglibdir, + install : true, + install_dir : rootlibexecdir) endif endif diff --git a/src/boot/pcrphase.c b/src/boot/pcrphase.c new file mode 100644 index 0000000000..3be89bc286 --- /dev/null +++ b/src/boot/pcrphase.c @@ -0,0 +1,262 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include + +#include "efivars.h" +#include "main-func.h" +#include "openssl-util.h" +#include "parse-util.h" +#include "pretty-print.h" +#include "tpm-pcr.h" +#include "tpm2-util.h" + +static char *arg_tpm2_device = NULL; +static char **arg_banks = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-pcrphase", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] COMMAND ...\n" + "\n%5$sMeasure boot phase into TPM2 PCR 11.%6$s\n" + "\n%3$sOptions:%4$s\n" + " -h --help Show this help\n" + " --version Print version\n" + " --bank=DIGEST Select TPM bank (SHA1, SHA256)\n" + " --tpm2-device=PATH Use specified TPM2 device\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_BANK, + ARG_TPM2_DEVICE, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "bank", required_argument, NULL, ARG_BANK }, + { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, + {} + }; + + int c; + + 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_BANK: { + const EVP_MD *implementation; + + implementation = EVP_get_digestbyname(optarg); + if (!implementation) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown bank '%s', refusing.", optarg); + + if (strv_extend(&arg_banks, EVP_MD_name(implementation)) < 0) + return log_oom(); + + break; + } + + case ARG_TPM2_DEVICE: { + _cleanup_free_ char *device = NULL; + + if (streq(optarg, "list")) + return tpm2_list_devices(); + + if (!streq(optarg, "auto")) { + device = strdup(optarg); + if (!device) + return log_oom(); + } + + free_and_replace(arg_tpm2_device, device); + break; + } + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + return 1; +} + +static int determine_banks(struct tpm2_context *c) { + _cleanup_free_ TPMI_ALG_HASH *algs = NULL; + int n_algs, r; + + assert(c); + + if (!strv_isempty(arg_banks)) /* Explicitly configured? Then use that */ + return 0; + + n_algs = tpm2_get_good_pcr_banks(c->esys_context, UINT32_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE, &algs); + if (n_algs <= 0) + return n_algs; + + for (int i = 0; i < n_algs; i++) { + const EVP_MD *implementation; + const char *salg; + + salg = tpm2_pcr_bank_to_string(algs[i]); + if (!salg) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 operates with unknown PCR algorithm, can't measure."); + + implementation = EVP_get_digestbyname(salg); + if (!implementation) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 operates with unsupported PCR algorithm, can't measure."); + + r = strv_extend(&arg_banks, EVP_MD_name(implementation)); + if (r < 0) + return log_oom(); + } + + return 0; +} + +static int run(int argc, char *argv[]) { + _cleanup_(tpm2_context_destroy) struct tpm2_context c = {}; + _cleanup_free_ char *joined = NULL, *pcr_string = NULL; + const char *word; + unsigned pcr_nr; + size_t length; + TSS2_RC rc; + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (optind+1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single argument."); + + word = argv[optind]; + + /* Refuse to measure an empty word. We want to be able to write the series of measured words + * separated by colons, where multiple separating colons are collapsed. Thus it makes sense to + * disallow an empty word to avoid ambiguities. */ + if (isempty(word)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "String to measure cannot be empty, refusing."); + + length = strlen(word); + + /* Skip logic if sd-stub is not used, after all PCR 11 might have a very different purpose then. */ + r = efi_get_variable_string(EFI_LOADER_VARIABLE(StubPcrKernelImage), &pcr_string); + if (r == -ENOENT) { + log_info("Kernel stub did not measure kernel image into PCR %u, skipping measurement.", TPM_PCR_INDEX_KERNEL_IMAGE); + return EXIT_SUCCESS; + } + if (r < 0) + return log_error_errno(r, "Failed to read StubPcrKernelImage EFI variable: %m"); + + /* Let's validate that the stub announced PCR 11 as we expected. */ + r = safe_atou(pcr_string, &pcr_nr); + if (r < 0) + return log_error_errno(r, "Failed to parse StubPcrKernelImage EFI variable: %s", pcr_string); + if (pcr_nr != TPM_PCR_INDEX_KERNEL_IMAGE) + return log_error_errno(SYNTHETIC_ERRNO(EREMOTE), "Kernel stub measured kernel image into PCR %u, which is different than expected %u.", pcr_nr, TPM_PCR_INDEX_KERNEL_IMAGE); + + r = dlopen_tpm2(); + if (r < 0) + return log_error_errno(r, "Failed to load TPM2 libraries: %m"); + + r = tpm2_context_init(arg_tpm2_device, &c); + if (r < 0) + return r; + + r = determine_banks(&c); + if (r < 0) + return r; + if (strv_isempty(arg_banks)) /* Still none? */ + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Found a TPM2 without enabled PCR banks. Can't operate."); + + TPML_DIGEST_VALUES values = {}; + STRV_FOREACH(bank, arg_banks) { + const EVP_MD *implementation; + int id; + + assert_se(implementation = EVP_get_digestbyname(*bank)); + + if (values.count >= ELEMENTSOF(values.digests)) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many banks selected."); + + if ((size_t) EVP_MD_size(implementation) > sizeof(values.digests[values.count].digest)) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Hash result too large for TPM2."); + + id = tpm2_pcr_bank_from_string(EVP_MD_name(implementation)); + if (id < 0) + return log_error_errno(id, "Can't map hash name to TPM2."); + + values.digests[values.count].hashAlg = id; + + if (EVP_Digest(word, length, (unsigned char*) &values.digests[values.count].digest, NULL, implementation, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash word."); + + values.count++; + } + + joined = strv_join(arg_banks, ", "); + if (!joined) + return log_oom(); + + log_debug("Measuring '%s' into PCR index %u, banks %s.", word, TPM_PCR_INDEX_KERNEL_IMAGE, joined); + + rc = sym_Esys_PCR_Extend( + c.esys_context, + ESYS_TR_PCR0 + TPM_PCR_INDEX_KERNEL_IMAGE, /* → PCR 11 */ + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &values); + if (rc != TSS2_RC_SUCCESS) + return log_error_errno( + SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to measure '%s': %s", + word, + sym_Tss2_RC_Decode(rc)); + + log_struct(LOG_INFO, + "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR, + LOG_MESSAGE("Successfully extended PCR index %u with '%s' (banks %s).", TPM_PCR_INDEX_KERNEL_IMAGE, word, joined), + "MEASURING=%s", word, + "PCR=%u", TPM_PCR_INDEX_KERNEL_IMAGE, + "BANKS=%s", joined); + + return EXIT_SUCCESS; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/systemd/sd-messages.h b/src/systemd/sd-messages.h index ffb9ba4739..51241c9426 100644 --- a/src/systemd/sd-messages.h +++ b/src/systemd/sd-messages.h @@ -189,6 +189,9 @@ _SD_BEGIN_DECLARATIONS; #define SD_MESSAGE_SHUTDOWN_CANCELED SD_ID128_MAKE(24,9f,6f,b9,e6,e2,42,8c,96,f3,f0,87,56,81,ff,a3) #define SD_MESSAGE_SHUTDOWN_CANCELED_STR SD_ID128_MAKE_STR(24,9f,6f,b9,e6,e2,42,8c,96,f3,f0,87,56,81,ff,a3) +#define SD_MESSAGE_TPM_PCR_EXTEND SD_ID128_MAKE(3f,7d,5e,f3,e5,4f,43,02,b4,f0,b1,43,bb,27,0c,ab) +#define SD_MESSAGE_TPM_PCR_EXTEND_STR SD_ID128_MAKE_STR(3f,7d,5e,f3,e5,4f,43,02,b4,f0,b1,43,bb,27,0c,ab) + _SD_END_DECLARATIONS; #endif