diff --git a/docs/TPM2_PCR_MEASUREMENTS.md b/docs/TPM2_PCR_MEASUREMENTS.md index abd280c004..b05739a8c7 100644 --- a/docs/TPM2_PCR_MEASUREMENTS.md +++ b/docs/TPM2_PCR_MEASUREMENTS.md @@ -199,6 +199,22 @@ initrd" in UTF-16. → **Measured hash** covers the per-UKI sysext cpio archive (which is generated on-the-fly by `systemd-stub`). +## PCR Measurements Made by `systemd-tpm2-setup` (Userspace) + +### PCR 9, NvPCR Initializations + +The `systemd-tpm2-setup.service` service initializes any NvPCRs defined via +`*.nvpcr` files. For each initialized NvPCR it will measure an event into PCR +9. + +→ **Measured hash** covers the string `nvpcr-init:`, suffixed by the NvPCR +name, suffixed by `:0x`, suffixed by the NV Index handle (formatted in +hexadecimal), suffixed by a colon, suffixed by the hash function used, in +lowercase (i.e. `sha256` or so), suffixed by a colon, and finally suffixed by +the state of the NvPCR after its initialization with the anchor measurement, in +hexadecimal. Example: +`nvpcr-init:hardware:0x1d10200:sha256:de3857f637c61e82f02e3722e1b207585fe9711045d863238904be8db10683f2` + ## PCR/NvPCR Measurements Made by `systemd-pcrextend` (Userspace) ### PCR 11, boot phases @@ -244,6 +260,15 @@ colon-separated strings, identifying the file system type, UUID, label as well as the GPT partition entry UUID, entry type UUID and entry label (in UTF-8, without trailing NUL bytes). +### PCR 9, NvPCR initialization separator + +After completion of `systemd-tpm2-setup.service` (which initializes all NvPCRs +and measures their initial state) at arly boot the `systemd-pcrnvdone.service` +service will measure a separator event into PCR 9, isolating the early-boot +NvPCR initializations from any later additions. + +→ **Measured hash** covers the string `nvpcr-separator`. + ## PCR/NvPCR Measurements Made by `systemd-cryptsetup` (Userspace) ### PCR 15, volume key diff --git a/man/rules/meson.build b/man/rules/meson.build index 26eddb7791..667d538d4b 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1106,8 +1106,10 @@ manpages = [ 'systemd-pcrfs-root.service', 'systemd-pcrfs@.service', 'systemd-pcrmachine.service', + 'systemd-pcrnvdone.service', 'systemd-pcrphase-initrd.service', - 'systemd-pcrphase-sysinit.service'], + 'systemd-pcrphase-sysinit.service', + 'systemd-pcrproduct.service'], 'ENABLE_BOOTLOADER HAVE_OPENSSL HAVE_TPM2'], ['systemd-portabled.service', '8', ['systemd-portabled'], 'ENABLE_PORTABLED'], ['systemd-poweroff.service', diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml index b95007dfcb..6b5ff05c3d 100644 --- a/man/systemd-pcrphase.service.xml +++ b/man/systemd-pcrphase.service.xml @@ -24,6 +24,7 @@ systemd-pcrproduct.service systemd-pcrfs-root.service systemd-pcrfs@.service + systemd-pcrnvdone.service systemd-pcrextend Measure boot phases, machine ID, product UUID and file system identity into TPM PCRs and NvPCRs @@ -33,8 +34,10 @@ systemd-pcrphase-sysinit.service systemd-pcrphase-initrd.service systemd-pcrmachine.service + systemd-pcrproduct.service systemd-pcrfs-root.service systemd-pcrfs@.service + systemd-pcrnvdone.service /usr/lib/systemd/systemd-pcrextend STRING @@ -54,6 +57,9 @@ product UUID (as provided by one of SMBIOS, Devicetree, …) into a NvPCR named hardware. + systemd-pcrnvdone.service is a system service that measures a separator event + into PCR 9 once all NvPCRs have completed initialization. + systemd-pcrfs-root.service and systemd-pcrfs@.service are services that measure file system identity information (i.e. mount point, file system type, label and UUID, partition label and UUID) into PCR 15. systemd-pcrfs-root.service does so for @@ -240,6 +246,17 @@ + + + + Set the event log event type for this measurement. Pass help for a + list of currently defined identifiers. Defaults to an appropriate value for + , , , and + otherwise to phase. + + + + diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index 7f552a687f..d434434e62 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -1586,7 +1586,8 @@ tpm2 - Trusted Platform Module 2.0 (TPM2) + Trusted Platform Module 2.0 (TPM2) (with full UEFI support, including the TCG PC Client + Platform Firmware Profile) cvm diff --git a/src/boot/measure.c b/src/boot/measure.c index b1433ff1b5..22129cb87d 100644 --- a/src/boot/measure.c +++ b/src/boot/measure.c @@ -198,8 +198,8 @@ uint32_t tpm_get_active_pcr_banks(void) { /* GetActivePcrBanks() was added only in version 1.1 of the spec */ if (version.Major < 1 || (version.Major == 1 && version.Minor < 1)) { - log_debug("TCG protocol too old for GetActivePcrBanks(), claiming no active banks."); - return 0; + log_debug("TCG protocol too old for GetActivePcrBanks(), returning wildcard bank information."); + return UINT32_MAX; } uint32_t active_pcr_banks = 0; diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index 18d51ca214..7af7d3211e 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -15,6 +15,7 @@ #include "parse-argument.h" #include "pcrextend-util.h" #include "pretty-print.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" #include "tpm2-pcr.h" @@ -32,6 +33,7 @@ static unsigned arg_pcr_index = UINT_MAX; static char *arg_nvpcr_name = NULL; static bool arg_varlink = false; static bool arg_early = false; +static Tpm2UserspaceEventType arg_event_type = _TPM2_USERSPACE_EVENT_TYPE_INVALID; STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); @@ -65,6 +67,7 @@ static int help(int argc, char *argv[], void *userdata) { " --machine-id Measure machine ID into PCR 15\n" " --product-id Measure SMBIOS product ID into NvPCR 'hardware'\n" " --early Run in early boot mode, without access to /var/\n" + " --event-type=TYPE Event type to include in the event log\n" "\nSee the %2$s for details.\n", program_invocation_short_name, link, @@ -88,6 +91,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_MACHINE_ID, ARG_PRODUCT_ID, ARG_EARLY, + ARG_EVENT_TYPE, }; static const struct option options[] = { @@ -102,6 +106,7 @@ static int parse_argv(int argc, char *argv[]) { { "machine-id", no_argument, NULL, ARG_MACHINE_ID }, { "product-id", no_argument, NULL, ARG_PRODUCT_ID }, { "early", no_argument, NULL, ARG_EARLY }, + { "event-type", required_argument, NULL, ARG_EVENT_TYPE }, {} }; @@ -189,6 +194,15 @@ static int parse_argv(int argc, char *argv[]) { arg_early = true; break; + case ARG_EVENT_TYPE: + if (streq(optarg, "help")) + return DUMP_STRING_TABLE(tpm2_userspace_event_type, Tpm2UserspaceEventType, _TPM2_USERSPACE_EVENT_TYPE_MAX); + + arg_event_type = tpm2_userspace_event_type_from_string(optarg); + if (arg_event_type < 0) + return log_error_errno(arg_event_type, "Failed to parse --event-type= argument: %s", optarg); + break; + case '?': return -EINVAL; @@ -446,7 +460,7 @@ static int vl_server(void) { static int run(int argc, char *argv[]) { _cleanup_free_ char *word = NULL; - Tpm2UserspaceEventType event; + Tpm2UserspaceEventType event = _TPM2_USERSPACE_EVENT_TYPE_INVALID; int r; log_setup(); @@ -506,6 +520,10 @@ static int run(int argc, char *argv[]) { event = TPM2_EVENT_PHASE; } + /* Override with explicitly configured event type */ + if (arg_event_type >= 0) + event = arg_event_type; + if (arg_graceful && !tpm2_is_fully_supported()) { log_notice("No complete TPM2 support detected, exiting gracefully."); return EXIT_SUCCESS; diff --git a/src/shared/efi-api.c b/src/shared/efi-api.c index bbc410ca50..ad9cbab74e 100644 --- a/src/shared/efi-api.c +++ b/src/shared/efi-api.c @@ -15,8 +15,14 @@ #include "stat-util.h" #include "stdio-util.h" #include "string-util.h" +#include "tpm2-util.h" #include "utf8.h" +#define EFI_TCG2_BOOT_HASH_ALG_SHA1 0x01 +#define EFI_TCG2_BOOT_HASH_ALG_SHA256 0x02 +#define EFI_TCG2_BOOT_HASH_ALG_SHA384 0x04 +#define EFI_TCG2_BOOT_HASH_ALG_SHA512 0x08 + #define LOAD_OPTION_ACTIVE 0x00000001 #define MEDIA_DEVICE_PATH 0x04 #define MEDIA_HARDDRIVE_DP 0x01 @@ -517,24 +523,71 @@ int efi_get_boot_options(uint16_t **ret_options) { #endif } +int efi_get_active_pcr_banks(uint32_t *ret) { #if ENABLE_EFI -static int loader_has_tpm2(void) { - _cleanup_free_ char *active_pcr_banks = NULL; - uint32_t active_pcr_banks_value; + static uint32_t cache = 0; + static bool cache_valid = false; int r; - r = efi_get_variable_string(EFI_LOADER_VARIABLE_STR("LoaderTpm2ActivePcrBanks"), &active_pcr_banks); - if (r < 0) { - if (r != -ENOENT) - log_debug_errno(r, "Failed to read LoaderTpm2ActivePcrBanks variable: %m"); - return r; + /* Returns the enabled PCR banks as bitmask, as reported by firmware. If the bitmask is returned as + * UINT32_MAX, the firmware supports the TCG protocol, but in a version too old to report this + * information. */ + + if (!cache_valid) { + _cleanup_free_ char *active_pcr_banks = NULL; + r = efi_get_variable_string(EFI_LOADER_VARIABLE_STR("LoaderTpm2ActivePcrBanks"), &active_pcr_banks); + if (r < 0) + return log_debug_errno(r, "Failed to read LoaderTpm2ActivePcrBanks variable: %m"); + + uint32_t efi_bits; + r = safe_atou32_full(active_pcr_banks, 16, &efi_bits); + if (r < 0) + return log_debug_errno(r, "Failed to parse LoaderTpm2ActivePcrBanks variable: %m"); + + if (efi_bits == UINT32_MAX) + /* UINT32_MAX means that the firmware API doesn't implement GetActivePcrBanks() and caller must guess */ + cache = UINT32_MAX; + else { + /* EFI TPM protocol uses different bit values for the hash algorithms, let's convert */ + static const struct { + uint32_t efi; + uint32_t tcg; + } table[] = { + { EFI_TCG2_BOOT_HASH_ALG_SHA1, 1U << TPM2_ALG_SHA1 }, + { EFI_TCG2_BOOT_HASH_ALG_SHA256, 1U << TPM2_ALG_SHA256 }, + { EFI_TCG2_BOOT_HASH_ALG_SHA384, 1U << TPM2_ALG_SHA384 }, + { EFI_TCG2_BOOT_HASH_ALG_SHA512, 1U << TPM2_ALG_SHA512 }, + }; + + uint32_t tcg_bits = 0; + FOREACH_ELEMENT(t, table) + SET_FLAG(tcg_bits, t->tcg, efi_bits & t->efi); + + cache = tcg_bits; + } + + cache_valid = true; } - r = safe_atou32_full(active_pcr_banks, 16, &active_pcr_banks_value); - if (r < 0) - return log_debug_errno(r, "Failed to parse LoaderTpm2ActivePcrBanks variable: %m"); + if (ret) + *ret = cache; - return active_pcr_banks_value != 0; + return 0; +#else + return -EOPNOTSUPP; +#endif +} + +#if ENABLE_EFI +static int loader_has_tpm2(void) { + uint32_t active_pcr_banks; + int r; + + r = efi_get_active_pcr_banks(&active_pcr_banks); + if (r < 0) + return r; + + return active_pcr_banks != 0; } #endif diff --git a/src/shared/efi-api.h b/src/shared/efi-api.h index 6fb3efb88a..98c55f8a38 100644 --- a/src/shared/efi-api.h +++ b/src/shared/efi-api.h @@ -18,6 +18,7 @@ int efi_get_boot_order(uint16_t **ret_order); int efi_set_boot_order(const uint16_t *order, size_t n); int efi_get_boot_options(uint16_t **ret_options); +int efi_get_active_pcr_banks(uint32_t *ret); bool efi_has_tpm2(void); sd_id128_t efi_guid_to_id128(const void *guid); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index da74ca06a5..c74162302c 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -2697,6 +2697,28 @@ int tpm2_get_best_pcr_bank( assert(c); assert(ret); + uint32_t efi_banks; + r = efi_get_active_pcr_banks(&efi_banks); + if (r < 0) { + if (r != -ENOENT) + return r; + + /* If variable is not set use guesswork below */ + log_debug("Boot loader didn't set the LoaderTpm2ActivePcrBanks EFI variable, we have to guess the used PCR banks."); + } else if (efi_banks == UINT32_MAX) + log_debug("Boot loader set the LoaderTpm2ActivePcrBanks EFI variable to indicate that the GetActivePcrBanks() API is not available in the firmware. We have to guess the used PCR banks."); + else { + if (BIT_SET(efi_banks, TPM2_ALG_SHA256)) + *ret = TPM2_ALG_SHA256; + else if (BIT_SET(efi_banks, TPM2_ALG_SHA1)) + *ret = TPM2_ALG_SHA1; + else + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Firmware reports neither SHA1 nor SHA256 PCR banks, cannot operate."); + + log_debug("Picked best PCR bank %s based on firmware reported banks.", tpm2_hash_alg_to_string(*ret)); + return 0; + } + if (pcr_mask == 0) { log_debug("Asked to pick best PCR bank but no PCRs selected we could derive this from. Defaulting to SHA256."); *ret = TPM2_ALG_SHA256; /* if no PCRs are selected this doesn't matter anyway... */ @@ -2784,6 +2806,32 @@ int tpm2_get_good_pcr_banks( assert(c); assert(ret); + uint32_t efi_banks; + r = efi_get_active_pcr_banks(&efi_banks); + if (r < 0) { + if (r != -ENOENT) + return r; + + /* If the variable is not set we have to guess via the code below */ + log_debug("Boot loader didn't set the LoaderTpm2ActivePcrBanks EFI variable, we have to guess the used PCR banks."); + } else if (efi_banks == UINT32_MAX) + log_debug("Boot loader set the LoaderTpm2ActivePcrBanks EFI variable to indicate that the GetActivePcrBanks() API is not available in the firmware. We have to guess the used PCR banks."); + else { + FOREACH_ARRAY(hash, tpm2_hash_algorithms, TPM2_N_HASH_ALGORITHMS) { + if (!BIT_SET(efi_banks, *hash)) + continue; + + if (!GREEDY_REALLOC(good_banks, n_good_banks+1)) + return log_oom_debug(); + + good_banks[n_good_banks++] = *hash; + } + + log_debug("Found %zu initialized TPM2 banks reported by firmware.", n_good_banks); + *ret = TAKE_PTR(good_banks); + return (int) n_good_banks; + } + FOREACH_TPMS_PCR_SELECTION_IN_TPML_PCR_SELECTION(selection, &c->capability_pcrs) { TPMI_ALG_HASH hash = selection->hash; @@ -6420,11 +6468,14 @@ static int json_dispatch_tpm2_algorithm(const char *name, sd_json_variant *varia } static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MAX] = { - [TPM2_EVENT_PHASE] = "phase", - [TPM2_EVENT_FILESYSTEM] = "filesystem", - [TPM2_EVENT_VOLUME_KEY] = "volume-key", - [TPM2_EVENT_MACHINE_ID] = "machine-id", - [TPM2_EVENT_PRODUCT_ID] = "product-id", + [TPM2_EVENT_PHASE] = "phase", + [TPM2_EVENT_FILESYSTEM] = "filesystem", + [TPM2_EVENT_VOLUME_KEY] = "volume-key", + [TPM2_EVENT_MACHINE_ID] = "machine-id", + [TPM2_EVENT_PRODUCT_ID] = "product-id", + [TPM2_EVENT_KEYSLOT] = "keyslot", + [TPM2_EVENT_NVPCR_INIT] = "nvpcr-init", + [TPM2_EVENT_NVPCR_SEPARATOR] = "nvpcr-separator", }; DEFINE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType); @@ -7367,6 +7418,37 @@ int tpm2_nvpcr_initialize( return log_debug_errno(r, "Failed to write anchor file: %m"); tpm2_userspace_log_clean(log_fd); + log_fd = safe_close(log_fd); + + /* Now also measure the initialization into PCR 9, so that there's a trace of it in regular PCRs. You + * might wonder why PCR 9? Well, we have very few PCRs available, and PCR 9 appears to be the least + * bad for this. It typically contains stuff that in our world is hard to predict anyway + * (i.e. possibly some overly verbose Grub stuff, as well as all initrds – those generated on-the-fly + * and those prepared beforehand – mangled into one), quite differently from all other PCRs we could + * use. Moreover PCR 11 already contains most stuff from PCR 9, as it contains the same data + * (i.e. initrds) in a more sensible fashion, clearly separated from on-the-fly generated ones. Note + * that we only do all this measurement stuff if we are booted as UKI, and hence when PCR 11 is + * available, but PCR 9 is not predictable. */ + _cleanup_strv_free_ char **banks = NULL; + r = tpm2_get_good_pcr_banks_strv(c, UINT32_C(1) << TPM2_PCR_KERNEL_INITRD, &banks); + if (r < 0) + return log_error_errno(r, "Could not verify PCR banks: %m"); + + _cleanup_free_ char *word = NULL; + if (asprintf(&word, "nvpcr-init:%s:0x%x:%s:%s", name, p.nv_index, tpm2_hash_alg_to_string(p.algorithm), h) < 0) + return log_oom(); + + r = tpm2_pcr_extend_bytes( + c, + banks, + TPM2_PCR_KERNEL_INITRD, + &IOVEC_MAKE_STRING(word), + /* secret= */ NULL, + TPM2_EVENT_NVPCR_INIT, + word); + if (r < 0) + return log_error_errno(r, "Could not extend PCR %i: %m", TPM2_PCR_KERNEL_INITRD); + return 1; #else /* HAVE_OPENSSL */ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index abf8adfdaf..59b7ed9984 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -144,6 +144,8 @@ typedef enum Tpm2UserspaceEventType { TPM2_EVENT_MACHINE_ID, TPM2_EVENT_PRODUCT_ID, TPM2_EVENT_KEYSLOT, + TPM2_EVENT_NVPCR_INIT, + TPM2_EVENT_NVPCR_SEPARATOR, _TPM2_USERSPACE_EVENT_TYPE_MAX, _TPM2_USERSPACE_EVENT_TYPE_INVALID = -EINVAL, } Tpm2UserspaceEventType; diff --git a/units/meson.build b/units/meson.build index bd788f6d0b..8e5b645f91 100644 --- a/units/meson.build +++ b/units/meson.build @@ -581,6 +581,11 @@ units = [ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], 'symlinks' : ['sysinit.target.wants/'], }, + { + 'file' : 'systemd-pcrnvdone.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + 'symlinks' : ['sysinit.target.wants/'], + }, { 'file' : 'systemd-tpm2-clear.service.in', 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], diff --git a/units/systemd-pcrnvdone.service.in b/units/systemd-pcrnvdone.service.in new file mode 100644 index 0000000000..e0dd9a8820 --- /dev/null +++ b/units/systemd-pcrnvdone.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=TPM PCR NvPCR Initialization Separator +Documentation=man:systemd-pcrnvdone.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=systemd-tpm2-setup-early.service systemd-tpm2-setup.service +Before=sysinit.target shutdown.target +ConditionSecurity=measured-uki +ConditionPathExists=!/etc/initrd-release +FailureAction=reboot-force + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-pcrextend --graceful --pcr=kernel-initrd --event-type=nvpcr-separator nvpcr-separator diff --git a/units/systemd-pcrproduct.service.in b/units/systemd-pcrproduct.service.in index a70fff19df..737ebff3bc 100644 --- a/units/systemd-pcrproduct.service.in +++ b/units/systemd-pcrproduct.service.in @@ -8,7 +8,7 @@ # (at your option) any later version. [Unit] -Description=TPM PCR Product ID Measurement +Description=TPM NvPCR Product ID Measurement Documentation=man:systemd-pcrproduct.service(8) DefaultDependencies=no Conflicts=shutdown.target