From 1421943a7141a4e8aed3053e5544a049fad07845 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 16 Sep 2022 23:15:41 +0200 Subject: [PATCH 1/9] tpm2-util: pick up Esys_PCR_Extend() symbol too --- src/shared/tpm2-util.c | 2 ++ src/shared/tpm2-util.h | 1 + 2 files changed, 3 insertions(+) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index a7af7528d7..da363f2bda 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -37,6 +37,7 @@ TSS2_RC (*sym_Esys_GetRandom)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_ TSS2_RC (*sym_Esys_Initialize)(ESYS_CONTEXT **esys_context, TSS2_TCTI_CONTEXT *tcti, TSS2_ABI_VERSION *abiVersion) = NULL; TSS2_RC (*sym_Esys_Load)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_PRIVATE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR *objectHandle) = NULL; TSS2_RC (*sym_Esys_LoadExternal)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_SENSITIVE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR hierarchy, ESYS_TR *objectHandle); +TSS2_RC (*sym_Esys_PCR_Extend)(ESYS_CONTEXT *esysContext, ESYS_TR pcrHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPML_DIGEST_VALUES *digests); TSS2_RC (*sym_Esys_PCR_Read)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1,ESYS_TR shandle2, ESYS_TR shandle3, const TPML_PCR_SELECTION *pcrSelectionIn, UINT32 *pcrUpdateCounter, TPML_PCR_SELECTION **pcrSelectionOut, TPML_DIGEST **pcrValues); TSS2_RC (*sym_Esys_PolicyAuthorize)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_DIGEST *approvedPolicy, const TPM2B_NONCE *policyRef, const TPM2B_NAME *keySign, const TPMT_TK_VERIFIED *checkTicket); TSS2_RC (*sym_Esys_PolicyAuthValue)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3) = NULL; @@ -72,6 +73,7 @@ int dlopen_tpm2(void) { DLSYM_ARG(Esys_Initialize), DLSYM_ARG(Esys_Load), DLSYM_ARG(Esys_LoadExternal), + DLSYM_ARG(Esys_PCR_Extend), DLSYM_ARG(Esys_PCR_Read), DLSYM_ARG(Esys_PolicyAuthorize), DLSYM_ARG(Esys_PolicyAuthValue), diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index cd6bcc91b3..7b5982febf 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -26,6 +26,7 @@ extern TSS2_RC (*sym_Esys_GetRandom)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1 extern TSS2_RC (*sym_Esys_Initialize)(ESYS_CONTEXT **esys_context, TSS2_TCTI_CONTEXT *tcti, TSS2_ABI_VERSION *abiVersion); extern TSS2_RC (*sym_Esys_Load)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_PRIVATE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR *objectHandle); extern TSS2_RC (*sym_Esys_LoadExternal)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_SENSITIVE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR hierarchy, ESYS_TR *objectHandle); +extern TSS2_RC (*sym_Esys_PCR_Extend)(ESYS_CONTEXT *esysContext, ESYS_TR pcrHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPML_DIGEST_VALUES *digests); extern TSS2_RC (*sym_Esys_PCR_Read)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1,ESYS_TR shandle2, ESYS_TR shandle3, const TPML_PCR_SELECTION *pcrSelectionIn, UINT32 *pcrUpdateCounter, TPML_PCR_SELECTION **pcrSelectionOut, TPML_DIGEST **pcrValues); extern TSS2_RC (*sym_Esys_PolicyAuthorize)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_DIGEST *approvedPolicy, const TPM2B_NONCE *policyRef, const TPM2B_NAME *keySign, const TPMT_TK_VERIFIED *checkTicket); extern TSS2_RC (*sym_Esys_PolicyAuthValue)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3); From 59fafaee5dbd3274c8752dbeb30bd1d2b5448f05 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 16 Sep 2022 23:17:48 +0200 Subject: [PATCH 2/9] tpm2-util: split out code that checks if bank has 24 pcrs into helper function of its own Just some refactoring, not change in behaviour. --- src/shared/tpm2-util.c | 60 ++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index da363f2bda..9a16f52859 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -501,6 +501,36 @@ static int tpm2_pcr_mask_good( return good; } +static int tpm2_bank_has24(const TPMS_PCR_SELECTION *selection) { + + assert(selection); + + /* As per https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClient_PFP_r1p05_v23_pub.pdf a + * TPM2 on a Client PC must have at least 24 PCRs. If this TPM has less, just skip over it. */ + if (selection->sizeofSelect < TPM2_PCRS_MAX/8) { + log_debug("Skipping TPM2 PCR bank %s with fewer than 24 PCRs.", + strna(tpm2_pcr_bank_to_string(selection->hash))); + return false; + } + + assert_cc(TPM2_PCRS_MAX % 8 == 0); + + /* It's not enough to check how many PCRs there are, we also need to check that the 24 are + * enabled for this bank. Otherwise this TPM doesn't qualify. */ + bool valid = true; + for (size_t j = 0; j < TPM2_PCRS_MAX/8; j++) + if (selection->pcrSelect[j] != 0xFF) { + valid = false; + break; + } + + if (!valid) + log_debug("TPM2 PCR bank %s has fewer than 24 PCR bits enabled, ignoring.", + strna(tpm2_pcr_bank_to_string(selection->hash))); + + return valid; +} + static int tpm2_get_best_pcr_bank( ESYS_CONTEXT *c, uint32_t pcr_mask, @@ -510,6 +540,7 @@ static int tpm2_get_best_pcr_bank( TPMI_ALG_HASH supported_hash = 0, hash_with_valid_pcr = 0; TPMI_YES_NO more; TSS2_RC rc; + int r; assert(c); @@ -530,38 +561,17 @@ static int tpm2_get_best_pcr_bank( assert(pcap->capability == TPM2_CAP_PCRS); for (size_t i = 0; i < pcap->data.assignedPCR.count; i++) { - bool valid = true; int good; /* For now we are only interested in the SHA1 and SHA256 banks */ if (!IN_SET(pcap->data.assignedPCR.pcrSelections[i].hash, TPM2_ALG_SHA256, TPM2_ALG_SHA1)) continue; - /* As per - * https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClient_PFP_r1p05_v23_pub.pdf a - * TPM2 on a Client PC must have at least 24 PCRs. If this TPM has less, just skip over - * it. */ - if (pcap->data.assignedPCR.pcrSelections[i].sizeofSelect < TPM2_PCRS_MAX/8) { - log_debug("Skipping TPM2 PCR bank %s with fewer than 24 PCRs.", - strna(tpm2_pcr_bank_to_string(pcap->data.assignedPCR.pcrSelections[i].hash))); + r = tpm2_bank_has24(pcap->data.assignedPCR.pcrSelections + i); + if (r < 0) + return r; + if (!r) continue; - } - - assert_cc(TPM2_PCRS_MAX % 8 == 0); - - /* It's not enough to check how many PCRs there are, we also need to check that the 24 are - * enabled for this bank. Otherwise this TPM doesn't qualify. */ - for (size_t j = 0; j < TPM2_PCRS_MAX/8; j++) - if (pcap->data.assignedPCR.pcrSelections[i].pcrSelect[j] != 0xFF) { - valid = false; - break; - } - - if (!valid) { - log_debug("TPM2 PCR bank %s has fewer than 24 PCR bits enabled, ignoring.", - strna(tpm2_pcr_bank_to_string(pcap->data.assignedPCR.pcrSelections[i].hash))); - continue; - } good = tpm2_pcr_mask_good(c, pcap->data.assignedPCR.pcrSelections[i].hash, pcr_mask); if (good < 0) From c5bf1f85cba71b8b8a0c7d703f05f2c8834ae9f8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 16 Sep 2022 23:31:09 +0200 Subject: [PATCH 3/9] tpm2-util: add helper for determining enabled/used PCR banks --- src/shared/tpm2-util.c | 79 ++++++++++++++++++++++++++++++++++++++++++ src/shared/tpm2-util.h | 2 ++ 2 files changed, 81 insertions(+) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 9a16f52859..d4b0ff5117 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -628,6 +628,85 @@ static int tpm2_get_best_pcr_bank( return 0; } +int tpm2_get_good_pcr_banks( + ESYS_CONTEXT *c, + uint32_t pcr_mask, + TPMI_ALG_HASH **ret) { + + _cleanup_free_ TPMI_ALG_HASH *good_banks = NULL, *fallback_banks = NULL; + _cleanup_(Esys_Freep) TPMS_CAPABILITY_DATA *pcap = NULL; + size_t n_good_banks = 0, n_fallback_banks = 0; + TPMI_YES_NO more; + TSS2_RC rc; + int r; + + assert(c); + assert(ret); + + rc = sym_Esys_GetCapability( + c, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + TPM2_CAP_PCRS, + 0, + 1, + &more, + &pcap); + if (rc != TSS2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to determine TPM2 PCR bank capabilities: %s", sym_Tss2_RC_Decode(rc)); + + assert(pcap->capability == TPM2_CAP_PCRS); + + for (size_t i = 0; i < pcap->data.assignedPCR.count; i++) { + + /* Let's see if this bank is superficially OK, i.e. has at least 24 enabled registers */ + r = tpm2_bank_has24(pcap->data.assignedPCR.pcrSelections + i); + if (r < 0) + return r; + if (!r) + continue; + + /* Let's now see if this bank has any of the selected PCRs actually initialized */ + r = tpm2_pcr_mask_good(c, pcap->data.assignedPCR.pcrSelections[i].hash, pcr_mask); + if (r < 0) + return r; + + if (n_good_banks + n_fallback_banks >= INT_MAX) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many good TPM2 banks?"); + + if (r) { + if (!GREEDY_REALLOC(good_banks, n_good_banks+1)) + return log_oom(); + + good_banks[n_good_banks++] = pcap->data.assignedPCR.pcrSelections[i].hash; + } else { + if (!GREEDY_REALLOC(fallback_banks, n_fallback_banks+1)) + return log_oom(); + + fallback_banks[n_fallback_banks++] = pcap->data.assignedPCR.pcrSelections[i].hash; + } + } + + /* Preferably, use the good banks (i.e. the ones the PCR values are actually initialized so + * far). Otherwise use the fallback banks (i.e. which exist and are enabled, but so far not used. */ + if (n_good_banks > 0) { + log_debug("Found %zu fully initialized TPM2 banks.", n_good_banks); + *ret = TAKE_PTR(good_banks); + return (int) n_good_banks; + } + if (n_fallback_banks > 0) { + log_debug("Found %zu enabled but un-initialized TPM2 banks.", n_fallback_banks); + *ret = TAKE_PTR(fallback_banks); + return (int) n_fallback_banks; + } + + /* No suitable banks found. */ + *ret = NULL; + return 0; +} + static void hash_pin(const char *pin, size_t len, TPM2B_AUTH *auth) { struct sha256_ctx hash; diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 7b5982febf..f074a41fad 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -67,6 +67,8 @@ static inline void Esys_Freep(void *p) { sym_Esys_Free(*(void**) p); } +int tpm2_get_good_pcr_banks(ESYS_CONTEXT *c, uint32_t pcr_mask, TPMI_ALG_HASH **ret_banks); + #else struct tpm2_context; #endif From 708d7524790c962a5b49bcf8bc5556f4acda3d08 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 16 Sep 2022 23:57:26 +0200 Subject: [PATCH 4/9] boot: add new pcrphase tool to measure barrier strings into PCR 11 --- catalog/systemd.catalog.in | 12 ++ man/rules/meson.build | 4 + man/systemd-measure.xml | 3 +- man/systemd-pcrphase.service.xml | 134 ++++++++++++++++ meson.build | 9 ++ src/boot/pcrphase.c | 262 +++++++++++++++++++++++++++++++ src/systemd/sd-messages.h | 3 + 7 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 man/systemd-pcrphase.service.xml create mode 100644 src/boot/pcrphase.c 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-initrd → ready → shutdown 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 From 40f1856791cc157fb5b88af35e50fbb15e6085ba Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 17 Sep 2022 00:50:38 +0200 Subject: [PATCH 5/9] units: add pcrphase units --- units/meson.build | 4 ++++ units/systemd-pcrphase-initrd.service.in | 24 ++++++++++++++++++++++++ units/systemd-pcrphase.service.in | 23 +++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 units/systemd-pcrphase-initrd.service.in create mode 100644 units/systemd-pcrphase.service.in diff --git a/units/meson.build b/units/meson.build index 2010a5566f..072640e992 100644 --- a/units/meson.build +++ b/units/meson.build @@ -260,6 +260,10 @@ in_units = [ 'sysinit.target.wants/ initrd-root-fs.target.wants/'], ['user-runtime-dir@.service', ''], ['user@.service', ''], + ['systemd-pcrphase-initrd.service', 'HAVE_GNU_EFI ENABLE_INITRD', + 'initrd.target.wants/'], + ['systemd-pcrphase.service', 'HAVE_GNU_EFI', + 'sysinit.target.wants/'], ] add_wants = [] diff --git a/units/systemd-pcrphase-initrd.service.in b/units/systemd-pcrphase-initrd.service.in new file mode 100644 index 0000000000..c1ad5ef844 --- /dev/null +++ b/units/systemd-pcrphase-initrd.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 PCR Barrier (initrd) +Documentation=man:systemd-pcrphase-initrd.service(8) +DefaultDependencies=no +Conflicts=shutdown.target initrd-switch-root.target +Before=sysinit.target cryptsetup-pre.target cryptsetup.target shutdown.target initrd-switch-root.target systemd-sysext.service +AssertPathExists=/etc/initrd-release +ConditionSecurity=tpm2 +ConditionPathExists=/sys/firmware/efi/efivars/StubPcrKernelImage-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{ROOTLIBEXECDIR}}/systemd-pcrphase enter-initrd +ExecStop={{ROOTLIBEXECDIR}}/systemd-pcrphase leave-initrd diff --git a/units/systemd-pcrphase.service.in b/units/systemd-pcrphase.service.in new file mode 100644 index 0000000000..1ef8ed6e6a --- /dev/null +++ b/units/systemd-pcrphase.service.in @@ -0,0 +1,23 @@ +# 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 PCR Barrier (Host) +Documentation=man:systemd-pcrphase.service(8) +After=remote-fs.target remote-cryptsetup.target +Before=systemd-user-sessions.service +AssertPathExists=!/etc/initrd-release +ConditionSecurity=tpm2 +ConditionPathExists=/sys/firmware/efi/efivars/StubPcrKernelImage-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{ROOTLIBEXECDIR}}/systemd-pcrphase ready +ExecStop={{ROOTLIBEXECDIR}}/systemd-pcrphase shutdown From 6ca0016398d97c1f51644088708f9ce865692e01 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 17 Sep 2022 15:22:54 +0200 Subject: [PATCH 6/9] measure: allow pre-calculating PCR values for multiple boot phases --- man/systemd-measure.xml | 22 ++++ src/boot/measure.c | 214 +++++++++++++++++++++++++++++++++---- test/units/testsuite-70.sh | 25 +++-- 3 files changed, 233 insertions(+), 28 deletions(-) diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml index ab5c9787d6..4e37d000e2 100644 --- a/man/systemd-measure.xml +++ b/man/systemd-measure.xml @@ -156,6 +156,28 @@ all suitable TPM2 devices currently discovered. + + PHASE + + Controls which boot phase(s) to calculate expected PCR 11 values for. This takes a + series of colon-separated strings that encode boot "paths" for entering a specific phase of the boot + process. Each of the specified strings is measured by the + systemd-pcrphase-initrd.service and + systemd-pcrphase.service8 + into PCR 11 during different milestones of the boot process. This switch may be specified multiple + times to calculate PCR values for multiple boot phases at once. If not used defaults to + enter-initrd, enter-initrd:leave-initrd, + enter-initrd:leave-initrd:ready, i.e. calculates expected PCR values for the boot + phase in the initrd, during early boot, and during system runtime, but excluding the phases before + the initrd or when shutting down. This setting is honoured both by calculate and + sign. When used with the latter it's particularly useful for generating PCR + signatures that can only be used for unlocking resources during specific parts of the boot + process. + + For further details about PCR boot phases, see + systemd-pcrphase.service8. + + diff --git a/src/boot/measure.c b/src/boot/measure.c index ec67e0cdb6..fd83cd8ba5 100644 --- a/src/boot/measure.c +++ b/src/boot/measure.c @@ -31,11 +31,13 @@ static char *arg_public_key = NULL; static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_OFF; static PagerFlags arg_pager_flags = 0; static bool arg_current = false; +static char **arg_phase = NULL; STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_public_key, freep); +STATIC_DESTRUCTOR_REGISTER(arg_phase, strv_freep); static inline void free_sections(char*(*sections)[_UNIFIED_SECTION_MAX]) { for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++) @@ -63,6 +65,7 @@ static int help(int argc, char *argv[], void *userdata) { " --version Print version\n" " --no-pager Do not pipe output into a pager\n" " -c --current Use current PCR values\n" + " --phase=PHASE Specify a boot phase to sign for\n" " --bank=DIGEST Select TPM bank (SHA1, SHA256)\n" " --tpm2-device=PATH Use specified TPM2 device\n" " --private-key=KEY Private key (PEM) to sign with\n" @@ -89,6 +92,21 @@ static int help(int argc, char *argv[], void *userdata) { return 0; } +static char *normalize_phase(const char *s) { + _cleanup_strv_free_ char **l = NULL; + + /* Let's normalize phase expressions. We split the series of colon-separated words up, then remove + * all empty ones, and glue them back together again. In other words we remove duplicate ":", as well + * as leading and trailing ones. */ + + l = strv_split(s, ":"); /* Split series of words */ + if (!l) + return NULL; + + /* Remove all empty words and glue things back together */ + return strv_join(strv_remove(l, ""), ":"); +} + static int parse_argv(int argc, char *argv[]) { enum { ARG_VERSION = 0x100, @@ -108,6 +126,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_PUBLIC_KEY, ARG_TPM2_DEVICE, ARG_JSON, + ARG_PHASE, }; static const struct option options[] = { @@ -127,6 +146,7 @@ static int parse_argv(int argc, char *argv[]) { { "private-key", required_argument, NULL, ARG_PRIVATE_KEY }, { "public-key", required_argument, NULL, ARG_PUBLIC_KEY }, { "json", required_argument, NULL, ARG_JSON }, + { "phase", required_argument, NULL, ARG_PHASE }, {} }; @@ -219,6 +239,20 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_PHASE: { + char *n; + + n = normalize_phase(optarg); + if (!n) + return log_oom(); + + r = strv_consume(&arg_phase, TAKE_PTR(n)); + if (r < 0) + return r; + + break; + } + case '?': return -EINVAL; @@ -241,14 +275,36 @@ static int parse_argv(int argc, char *argv[]) { if (arg_sections[us]) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --current switch cannot be used in combination with --linux= and related switches."); + if (strv_isempty(arg_phase)) { + /* If no phases are specifically selected, pick everything from the beginning of the initrd + * to the beginning of shutdown. */ + if (strv_extend_strv(&arg_phase, + STRV_MAKE("enter-initrd", + "enter-initrd:leave-initrd", + "enter-initrd:leave-initrd:ready"), + /* filter_duplicates= */ false) < 0) + return log_oom(); + } else { + strv_sort(arg_phase); + strv_uniq(arg_phase); + } + + _cleanup_free_ char *j = NULL; + j = strv_join(arg_phase, ", "); + if (!j) + return log_oom(); + + log_debug("Measuring boot phases: %s", j); return 1; } +/* The PCR 11 state for one specific bank */ typedef struct PcrState { char *bank; const EVP_MD *md; void *value; size_t value_size; + void *saved_value; /* A copy of the original value we calculated, used by pcr_states_save()/pcr_states_restore() to come later back to */ } PcrState; static void pcr_state_free_all(PcrState **pcr_state) { @@ -260,6 +316,7 @@ static void pcr_state_free_all(PcrState **pcr_state) { for (size_t i = 0; (*pcr_state)[i].value; i++) { free((*pcr_state)[i].bank); free((*pcr_state)[i].value); + free((*pcr_state)[i].saved_value); } *pcr_state = mfree(*pcr_state); @@ -320,6 +377,8 @@ static int measure_kernel(PcrState *pcr_states, size_t n) { assert(n > 0); assert(pcr_states); + /* Virtually measures the components of a unified kernel image into PCR 11 */ + if (arg_current) { /* Shortcut things, if we should just use the current PCR value */ @@ -432,6 +491,54 @@ static int measure_kernel(PcrState *pcr_states, size_t n) { return 0; } +static int measure_phase(PcrState *pcr_states, size_t n, const char *phase) { + _cleanup_strv_free_ char **l = NULL; + int r; + + assert(pcr_states); + assert(n > 0); + + /* Measure a phase string into PCR 11. This splits up the "phase" expression at colons, and then + * virtually extends each specified word into PCR 11, to model how during boot we measure a series of + * words into PCR 11, one for each phase. */ + + l = strv_split(phase, ":"); + if (!l) + return log_oom(); + + STRV_FOREACH(word, l) { + size_t wl; + + if (isempty(*word)) + continue; + + wl = strlen(*word); + + for (size_t i = 0; i < n; i++) { /* For each bank */ + _cleanup_free_ void *b = NULL; + int bsz; + + bsz = EVP_MD_size(pcr_states[i].md); + assert(bsz > 0); + + b = malloc(bsz); + if (!b) + return log_oom(); + + /* First hash the word itself */ + if (EVP_Digest(*word, wl, b, NULL, pcr_states[i].md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash word '%s'.", *word); + + /* And then extend the PCR with the resulting hash */ + r = pcr_state_extend(pcr_states + i, b, bsz); + if (r < 0) + return r; + } + } + + return 0; +} + static int pcr_states_allocate(PcrState **ret) { _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; size_t n = 0; @@ -473,6 +580,39 @@ static int pcr_states_allocate(PcrState **ret) { return (int) n; } +static int pcr_states_save(PcrState *pcr_states, size_t n) { + assert(pcr_states); + assert(n > 0); + + for (size_t i = 0; i < n; i++) { + _cleanup_free_ void *saved = NULL; + + if (!pcr_states[i].value) + continue; + + saved = memdup(pcr_states[i].value, pcr_states[i].value_size); + if (!saved) + return log_oom(); + + free_and_replace(pcr_states[i].saved_value, saved); + } + + return 0; +} + +static void pcr_states_restore(PcrState *pcr_states, size_t n) { + assert(pcr_states); + assert(n > 0); + + for (size_t i = 0; i < n; i++) { + + assert(pcr_states[i].value); + assert(pcr_states[i].saved_value); + + memcpy(pcr_states[i].value, pcr_states[i].saved_value, pcr_states[i].value_size); + } +} + static int verb_calculate(int argc, char *argv[], void *userdata) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; @@ -492,34 +632,64 @@ static int verb_calculate(int argc, char *argv[], void *userdata) { if (r < 0) return r; - for (size_t i = 0; i < n; i++) { - if (arg_json_format_flags & JSON_FORMAT_OFF) { - _cleanup_free_ char *hd = NULL; + /* Save the current state, so that we later can restore to it. This way we can measure the PCR values + * for multiple different boot phases without heaving to start from zero each time */ + r = pcr_states_save(pcr_states, n); + if (r < 0) + return r; - hd = hexmem(pcr_states[i].value, pcr_states[i].value_size); - if (!hd) - return log_oom(); + STRV_FOREACH(phase, arg_phase) { - printf("%" PRIu32 ":%s=%s\n", TPM_PCR_INDEX_KERNEL_IMAGE, pcr_states[i].bank, hd); - } else { - _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL; + r = measure_phase(pcr_states, n, *phase); + if (r < 0) + return r; - r = json_build(&bv, - JSON_BUILD_ARRAY( - JSON_BUILD_OBJECT( - JSON_BUILD_PAIR("pcr", JSON_BUILD_INTEGER(TPM_PCR_INDEX_KERNEL_IMAGE)), - JSON_BUILD_PAIR("hash", JSON_BUILD_HEX(pcr_states[i].value, pcr_states[i].value_size)) - ) - ) - ); - if (r < 0) - return log_error_errno(r, "Failed to build JSON object: %m"); + for (size_t i = 0; i < n; i++) { + if (arg_json_format_flags & JSON_FORMAT_OFF) { + _cleanup_free_ char *hd = NULL; - r = json_variant_set_field(&w, pcr_states[i].bank, bv); - if (r < 0) - return log_error_errno(r, "Failed to add bank info to object: %m"); + if (i == 0) { + fflush(stdout); + fprintf(stderr, "%s# PCR[%" PRIu32 "] Phase <%s>%s\n", + ansi_grey(), + TPM_PCR_INDEX_KERNEL_IMAGE, + isempty(*phase) ? ":" : *phase, + ansi_normal()); + fflush(stderr); + } + hd = hexmem(pcr_states[i].value, pcr_states[i].value_size); + if (!hd) + return log_oom(); + + printf("%" PRIu32 ":%s=%s\n", TPM_PCR_INDEX_KERNEL_IMAGE, pcr_states[i].bank, hd); + } else { + _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL, *array = NULL; + + array = json_variant_ref(json_variant_by_key(w, pcr_states[i].bank)); + + r = json_build(&bv, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(!isempty(*phase), "phase", JSON_BUILD_STRING(*phase)), + JSON_BUILD_PAIR("pcr", JSON_BUILD_INTEGER(TPM_PCR_INDEX_KERNEL_IMAGE)), + JSON_BUILD_PAIR("hash", JSON_BUILD_HEX(pcr_states[i].value, pcr_states[i].value_size)) + ) + ); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + r = json_variant_append_array(&array, bv); + if (r < 0) + return log_error_errno(r, "Failed to append JSON object to array: %m"); + + r = json_variant_set_field(&w, pcr_states[i].bank, array); + if (r < 0) + return log_error_errno(r, "Failed to add bank info to object: %m"); + } } + + /* Return to the original kernel measurement for the next phase calculation */ + pcr_states_restore(pcr_states, n); } if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { diff --git a/test/units/testsuite-70.sh b/test/units/testsuite-70.sh index e2219e89d3..97536c812c 100755 --- a/test/units/testsuite-70.sh +++ b/test/units/testsuite-70.sh @@ -69,14 +69,27 @@ if [[ -e /usr/lib/systemd/systemd-measure ]]; then 11:sha384=5573f9b2caf55b1d0a6a701f890662d682af961899f0419cf1e2d5ea4a6a68c1f25bd4f5b8a0865eeee82af90f5cb087 11:sha512=961305d7e9981d6606d1ce97b3a9a1f92610cac033e9c39064895f0e306abc1680463d55767bd98e751eae115bdef3675a9ee1d29ed37da7885b1db45bb2555b EOF - - /usr/lib/systemd/systemd-measure calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 | cmp - /tmp/result + /usr/lib/systemd/systemd-measure calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 --phase=: | cmp - /tmp/result cat >/tmp/result.json </tmp/result </tmp/result.json < /dev/null && { echo 'unexpected success'; exit 1; } # Sign new PCR state, decrypting should work now. - /usr/lib/systemd/systemd-measure sign --current --bank=sha1 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" > "/tmp/pcrsign.sig2" + /usr/lib/systemd/systemd-measure sign --current --bank=sha1 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: > "/tmp/pcrsign.sig2" systemd-creds decrypt /tmp/pcrtestdata.encrypted - --tpm2-signature="/tmp/pcrsign.sig2" | cmp - /tmp/pcrtestdata # Now, do the same, but with a cryptsetup binding @@ -121,7 +134,7 @@ if [ -e /usr/lib/systemd/systemd-measure ] && \ SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=1 /usr/lib/systemd/systemd-cryptsetup attach test-volume2 $img - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig2",headless=1 && { echo 'unexpected success'; exit 1; } # But once we sign the current PCRs, we should be able to unlock again - /usr/lib/systemd/systemd-measure sign --current --bank=sha1 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" > "/tmp/pcrsign.sig3" + /usr/lib/systemd/systemd-measure sign --current --bank=sha1 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: > "/tmp/pcrsign.sig3" SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=0 /usr/lib/systemd/systemd-cryptsetup attach test-volume2 $img - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig3",headless=1 /usr/lib/systemd/systemd-cryptsetup detach test-volume2 SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=1 /usr/lib/systemd/systemd-cryptsetup attach test-volume2 $img - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig3",headless=1 From a434e25f52dea1af4f256794102003fd002be34b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 22 Sep 2022 16:19:58 +0200 Subject: [PATCH 7/9] measure: make --public-key= optional if "sign" is called We can derive the public key from the private key, so let's do that, to make things a bit easier. --- man/systemd-measure.xml | 5 ++++- src/boot/measure.c | 41 +++++++++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml index 4e37d000e2..459ed6592f 100644 --- a/man/systemd-measure.xml +++ b/man/systemd-measure.xml @@ -143,7 +143,10 @@ unified kernel image, the latter picks the public key of the key pair used to sign the resulting PCR 11 values. The former is the key that the booted system will likely use to lock disk and credential encryption to, the latter is the key used for unlocking such resources again. Hence, typically the - same PEM key should be supplied in both cases. + same PEM key should be supplied in both cases. + + If the is not specified but is + specified the public key is automatically derived from the private key. diff --git a/src/boot/measure.c b/src/boot/measure.c index fd83cd8ba5..05100cf541 100644 --- a/src/boot/measure.c +++ b/src/boot/measure.c @@ -708,7 +708,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; _cleanup_(EVP_PKEY_freep) EVP_PKEY *privkey = NULL, *pubkey = NULL; _cleanup_(tpm2_context_destroy) struct tpm2_context c = {}; - _cleanup_fclose_ FILE *privkeyf = NULL , *pubkeyf = NULL; + _cleanup_fclose_ FILE *privkeyf = NULL; ESYS_TR session_handle = ESYS_TR_NONE; TSS2_RC rc; size_t n; @@ -719,8 +719,6 @@ static int verb_sign(int argc, char *argv[], void *userdata) { if (!arg_private_key) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No private key specified, use --private-key=."); - if (!arg_public_key) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No public key specified, use --public-key=."); /* When signing we only support JSON output */ arg_json_format_flags &= ~JSON_FORMAT_OFF; @@ -729,17 +727,40 @@ static int verb_sign(int argc, char *argv[], void *userdata) { if (!privkeyf) return log_error_errno(errno, "Failed to open private key file '%s': %m", arg_private_key); - pubkeyf = fopen(arg_public_key, "re"); - if (!pubkeyf) - return log_error_errno(errno, "Failed to open public key file '%s': %m", arg_public_key); - privkey = PEM_read_PrivateKey(privkeyf, NULL, NULL, NULL); if (!privkey) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse private key '%s'.", arg_private_key); - pubkey = PEM_read_PUBKEY(pubkeyf, NULL, NULL, NULL); - if (!pubkey) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key '%s'.", arg_public_key); + if (arg_public_key) { + _cleanup_fclose_ FILE *pubkeyf = NULL; + + pubkeyf = fopen(arg_public_key, "re"); + if (!pubkeyf) + return log_error_errno(errno, "Failed to open public key file '%s': %m", arg_public_key); + + pubkey = PEM_read_PUBKEY(pubkeyf, NULL, NULL, NULL); + if (!pubkey) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key '%s'.", arg_public_key); + } else { + _cleanup_free_ char *data = NULL; + _cleanup_fclose_ FILE *tf = NULL; + size_t sz; + + /* No public key was specified, let's derive it automatically, if we can */ + + tf = open_memstream_unlocked(&data, &sz); + if (!tf) + return log_oom(); + + if (i2d_PUBKEY_fp(tf, privkey) != 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, &pubkey)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse extracted public key of private key file '%s'.", arg_private_key); + } r = pcr_states_allocate(&pcr_states); if (r < 0) From 1a9c67a98a7daf8781e144218efa5a37d30266da Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 22 Sep 2022 16:20:38 +0200 Subject: [PATCH 8/9] measure: clarify we actually try to calculate for all four banks by default --- src/boot/measure.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/boot/measure.c b/src/boot/measure.c index 05100cf541..e285277402 100644 --- a/src/boot/measure.c +++ b/src/boot/measure.c @@ -66,7 +66,7 @@ static int help(int argc, char *argv[], void *userdata) { " --no-pager Do not pipe output into a pager\n" " -c --current Use current PCR values\n" " --phase=PHASE Specify a boot phase to sign for\n" - " --bank=DIGEST Select TPM bank (SHA1, SHA256)\n" + " --bank=DIGEST Select TPM bank (SHA1, SHA256, SHA384, SHA512)\n" " --tpm2-device=PATH Use specified TPM2 device\n" " --private-key=KEY Private key (PEM) to sign with\n" " --public-key=KEY Public key (PEM) to validate against\n" From 7dad7811023c338dba8a7ce94be320aa31e9f13e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 19 Sep 2022 12:52:40 +0200 Subject: [PATCH 9/9] update TODO --- TODO | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/TODO b/TODO index d9c7c6df6a..2bfbbdfa3b 100644 --- a/TODO +++ b/TODO @@ -119,6 +119,12 @@ Deprecations and removals: Features: +* systemd-measure: only require private key to be set when signing. iiuc we can + generate the public key from it anyway. + +* automatically propagate LUKS password credential into cryptsetup from host, + so that one can unlock LUKS via VM hypervisor supplied password. + * add ability to path_is_valid() to classify paths that refer to a dir from those which may refer to anything, and use that in various places to filter early. i.e. stuff ending in "/", "/." and "/.." definitely refers to a @@ -154,11 +160,6 @@ Features: * tmpfiles: currently if we fail to create an inode, we stat it first, and only then O_PATH open it. Reverse that. -* during the initrd → host transition measure a fixed value into TPM PCR 11 - (where we already measure the UKI into), so that unlock policies for disk - enryption/credential encryption can be put together that only work in the - initrd or only on the host (or both). - * Add support for extra verity configuration options to systemd-repart (FEC, hash type, etc) @@ -737,8 +738,16 @@ Features: one. * we probably should extend the root verity hash of the root fs into some PCR - on boot. (i.e. maybe add a crypttab option tpm2-measure=8 or so to measure it - into PCR 8) + on boot. (i.e. maybe add a veritytab option tpm2-measure=12 or so to measure + it into PCR 12); Similar: we probably should extend the LUKS volume key of + the root fs into some PCR on boot. (i.e. maybe add a crypttab option + tpm2-measure=15 or so to measure it into PCR 15); once both are in place + update gpt-auto-discovery to generate these by default for the partitions it + discovers. Static vendor stuff should probably end up in PCR 12 (i.e. the + verity hash), with local keys in PCR 15 (i.e. the encryption volume + key). That way, we nicely distinguish resources supplied by the OS vendor + (i.e. sysext, root verity) from those inherently local (i.e. encryption key), + which is useful if they shall be signed separately. * add a "policy" to the dissection logic. i.e. a bit mask what is OK to mount, what must be read-only, what requires encryption, and what requires @@ -765,7 +774,6 @@ Features: * sysupdate: - add fuzzing to the pattern parser - support casync as download mechanism - - direct TPM2 PCR change handling, possible renrolling LUKS2 media if needed. - "systemd-sysupdate update --all" support, that iterates through all components defined on the host, plus all images installed into /var/lib/machines/, /var/lib/portable/ and so on. @@ -847,10 +855,6 @@ Features: * add tpm.target or so which is delayed until TPM2 device showed up in case firmware indicates there is one. -* Add concept for upgrading TPM2 enrollments, maybe a new switch - --pcrs=4: or so, i.e. select a PCR to include in the hash, and then - override its hash - * TPM2: auto-reenroll in cryptsetup, as fallback for hosed firmware upgrades and such @@ -1606,14 +1610,6 @@ Features: * firstboot: make it useful to be run immediately after yum --installroot to set up a machine. (most specifically, make --copy-root-password work even if /etc/passwd already exists -* efi stub: optionally, load initrd from disk as a separate file, HMAC check it - with key from TPM, bound to PCR, refusing if failing. This would then allow - traditional distros that generate initrds locally to secure them with TPM: - after generating the initrd, do the HMAC calculation, put result in initrd - filename, done. This would then bind the validity of the initrd to the local - host, and used kernel, and means people cannot change initrd or kernel - without booting the kernel + initrd. - * EFI: - honor language efi variables for default language selection (if there are any?) - honor timezone efi variables for default timezone selection (if there are any?)