From e4481cc512f48d423115b10d4ae1c8e1381ff84b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Oct 2022 18:07:46 +0200 Subject: [PATCH 01/15] tpm2-util: split out code that derives "good" TPM2 banks into an strv from pcrphase and generalize it in tpm2-util.c That way we can reuse it later from different places. --- src/boot/pcrphase.c | 28 ++++++--------------------- src/shared/tpm2-util.c | 43 ++++++++++++++++++++++++++++++++++++++++++ src/shared/tpm2-util.h | 1 + 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/src/boot/pcrphase.c b/src/boot/pcrphase.c index 14f79f23c0..6e1a6a39f0 100644 --- a/src/boot/pcrphase.c +++ b/src/boot/pcrphase.c @@ -124,35 +124,19 @@ static int parse_argv(int argc, char *argv[]) { } static int determine_banks(struct tpm2_context *c) { - _cleanup_free_ TPMI_ALG_HASH *algs = NULL; - int n_algs, r; + _cleanup_strv_free_ char **l = NULL; + int 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(); - } + r = tpm2_get_good_pcr_banks_strv(c->esys_context, UINT32_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE, &l); + if (r < 0) + return r; + strv_free_and_replace(arg_banks, l); return 0; } diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 2b34e4fd65..abe15b54a8 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -730,6 +730,49 @@ int tpm2_get_good_pcr_banks( return 0; } +int tpm2_get_good_pcr_banks_strv( + ESYS_CONTEXT *c, + uint32_t pcr_mask, + char ***ret) { + + _cleanup_free_ TPMI_ALG_HASH *algs = NULL; + _cleanup_strv_free_ char **l = NULL; + int n_algs; + + assert(c); + assert(ret); + + n_algs = tpm2_get_good_pcr_banks(c, pcr_mask, &algs); + if (n_algs < 0) + return n_algs; + + for (int i = 0; i < n_algs; i++) { + _cleanup_free_ char *n = NULL; + 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."); + + n = strdup(ASSERT_PTR(EVP_MD_name(implementation))); + if (!n) + return log_oom(); + + ascii_strlower(n); /* OpenSSL uses uppercase digest names, we prefer them lower case. */ + + if (strv_consume(&l, TAKE_PTR(n)) < 0) + return log_oom(); + } + + *ret = TAKE_PTR(l); + 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 c240335ae6..6d83281be0 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -68,6 +68,7 @@ static inline void Esys_Freep(void *p) { } int tpm2_get_good_pcr_banks(ESYS_CONTEXT *c, uint32_t pcr_mask, TPMI_ALG_HASH **ret_banks); +int tpm2_get_good_pcr_banks_strv(ESYS_CONTEXT *c, uint32_t pcr_mask, char ***ret); #else struct tpm2_context; From 15c591d1e2b555070f540cafb1b3d1e564e3410a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Oct 2022 18:20:14 +0200 Subject: [PATCH 02/15] tpm2-util: split out code that extends a PCR from pcrphase This way we can reuse it later outside of pcrphase --- src/boot/pcrphase.c | 42 ++------------------------ src/shared/tpm2-util.c | 67 ++++++++++++++++++++++++++++++++++++++++++ src/shared/tpm2-util.h | 2 ++ 3 files changed, 72 insertions(+), 39 deletions(-) diff --git a/src/boot/pcrphase.c b/src/boot/pcrphase.c index 6e1a6a39f0..a4aa162801 100644 --- a/src/boot/pcrphase.c +++ b/src/boot/pcrphase.c @@ -146,7 +146,6 @@ static int run(int argc, char *argv[]) { const char *word; unsigned pcr_nr; size_t length; - TSS2_RC rc; int r; log_setup(); @@ -215,50 +214,15 @@ static int run(int argc, char *argv[]) { 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)); + r = tpm2_extend_bytes(c.esys_context, arg_banks, TPM_PCR_INDEX_KERNEL_IMAGE, word, length); /* → PCR 11 */ + if (r < 0) + return r; log_struct(LOG_INFO, "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR, diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index abe15b54a8..36023c6de2 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -1919,6 +1919,73 @@ int tpm2_find_device_auto( #endif } +#if HAVE_TPM2 +int tpm2_extend_bytes( + ESYS_CONTEXT *c, + char **banks, + unsigned pcr_index, + const void *data, + size_t sz) { + +#if HAVE_OPENSSL + TPML_DIGEST_VALUES values = {}; + TSS2_RC rc; + + assert(c); + assert(data || sz == 0); + + if (pcr_index >= TPM2_PCRS_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Can't measure into unsupported PCR %u, refusing.", pcr_index); + + if (strv_isempty(banks)) + return 0; + + STRV_FOREACH(bank, 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(data, sz, (unsigned char*) &values.digests[values.count].digest, NULL, implementation, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash word."); + + values.count++; + } + + rc = sym_Esys_PCR_Extend( + c, + ESYS_TR_PCR0 + pcr_index, + 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 into PCR %u: %s", + pcr_index, + sym_Tss2_RC_Decode(rc)); + + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "OpenSSL not supported on this build."); +#endif +} +#endif + int tpm2_parse_pcrs(const char *s, uint32_t *ret) { const char *p = ASSERT_PTR(s); uint32_t mask = 0; diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 6d83281be0..4cab52a949 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -70,6 +70,8 @@ static inline void Esys_Freep(void *p) { int tpm2_get_good_pcr_banks(ESYS_CONTEXT *c, uint32_t pcr_mask, TPMI_ALG_HASH **ret_banks); int tpm2_get_good_pcr_banks_strv(ESYS_CONTEXT *c, uint32_t pcr_mask, char ***ret); +int tpm2_extend_bytes(ESYS_CONTEXT *c, char **banks, unsigned pcr_index, const void *data, size_t sz); + #else struct tpm2_context; #endif From 9885c8745d313588350325e8e2110887bf78c442 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Oct 2022 14:38:35 +0200 Subject: [PATCH 03/15] tpm2-util: optionally do HMAC in tpm2_extend_bytes() in case we process sensitive data When measuring data into a PCR we are supposed to hash the data on the CPU and then pass the hash value over the wire to the TPM2. That's all good as long as the data we intend to measure is not sensitive. Let's be extra careful though if we want to measure sensitive data, for example the root file system volume key. Instead of just hashing that and passing it over the wire to the TPM2, let's do a HMAC signature instead. It's also a hash operation, but should protect our secret reasonably well and not leak direct information about it to wiretappers. --- src/boot/pcrphase.c | 2 +- src/shared/tpm2-util.c | 25 +++++++++++++++++++++---- src/shared/tpm2-util.h | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/boot/pcrphase.c b/src/boot/pcrphase.c index a4aa162801..6d055e3499 100644 --- a/src/boot/pcrphase.c +++ b/src/boot/pcrphase.c @@ -220,7 +220,7 @@ static int run(int argc, char *argv[]) { log_debug("Measuring '%s' into PCR index %u, banks %s.", word, TPM_PCR_INDEX_KERNEL_IMAGE, joined); - r = tpm2_extend_bytes(c.esys_context, arg_banks, TPM_PCR_INDEX_KERNEL_IMAGE, word, length); /* → PCR 11 */ + r = tpm2_extend_bytes(c.esys_context, arg_banks, TPM_PCR_INDEX_KERNEL_IMAGE, word, length, NULL, 0); /* → PCR 11 */ if (r < 0) return r; diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 36023c6de2..1b0adab397 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -1925,14 +1925,22 @@ int tpm2_extend_bytes( char **banks, unsigned pcr_index, const void *data, - size_t sz) { + size_t data_size, + const void *secret, + size_t secret_size) { #if HAVE_OPENSSL TPML_DIGEST_VALUES values = {}; TSS2_RC rc; assert(c); - assert(data || sz == 0); + assert(data || data_size == 0); + assert(secret || secret_size == 0); + + if (data_size == SIZE_MAX) + data_size = strlen(data); + if (secret_size == SIZE_MAX) + secret_size = strlen(secret); if (pcr_index >= TPM2_PCRS_MAX) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Can't measure into unsupported PCR %u, refusing.", pcr_index); @@ -1958,8 +1966,17 @@ int tpm2_extend_bytes( values.digests[values.count].hashAlg = id; - if (EVP_Digest(data, sz, (unsigned char*) &values.digests[values.count].digest, NULL, implementation, NULL) != 1) - return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash word."); + /* So here's a twist: sometimes we want to measure secrets (e.g. root file system volume + * key), but we'd rather not leak a literal hash of the secret to the TPM (given that the + * wire is unprotected, and some other subsystem might use the simple, literal hash of the + * secret for other purposes, maybe because it needs a shorter secret derived from it for + * some unrelated purpose, who knows). Hence we instead measure an HMAC signature of a + * private non-secret string instead. */ + if (secret_size > 0) { + if (!HMAC(implementation, secret, secret_size, data, data_size, (unsigned char*) &values.digests[values.count].digest, NULL)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to calculate HMAC of data to measure."); + } else if (EVP_Digest(data, data_size, (unsigned char*) &values.digests[values.count].digest, NULL, implementation, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash data to measure."); values.count++; } diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 4cab52a949..96e6c31b0a 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -70,7 +70,7 @@ static inline void Esys_Freep(void *p) { int tpm2_get_good_pcr_banks(ESYS_CONTEXT *c, uint32_t pcr_mask, TPMI_ALG_HASH **ret_banks); int tpm2_get_good_pcr_banks_strv(ESYS_CONTEXT *c, uint32_t pcr_mask, char ***ret); -int tpm2_extend_bytes(ESYS_CONTEXT *c, char **banks, unsigned pcr_index, const void *data, size_t sz); +int tpm2_extend_bytes(ESYS_CONTEXT *c, char **banks, unsigned pcr_index, const void *data, size_t data_size, const void *secret, size_t secret_size); #else struct tpm2_context; From 94c0c85e302d00923dc5bbf9d1b937875f1d0c66 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Oct 2022 09:56:32 +0200 Subject: [PATCH 04/15] cryptsetup: add tpm2-measure-pcr= and tpm2-measure-bank= crypttab options These options allow measuring the volume key used for unlocking the volume to a TPM2 PCR. This is ideally used for the volume key of the root file system and can then be used to bind other resources to the root file system volume in a secure way. See: #24503 --- meson.build | 3 +- src/cryptsetup/cryptsetup.c | 226 +++++++++++++++++++++++++++++++++--- src/fundamental/tpm-pcr.h | 3 + 3 files changed, 217 insertions(+), 15 deletions(-) diff --git a/meson.build b/meson.build index 64f2e00be5..921f7300b0 100644 --- a/meson.build +++ b/meson.build @@ -2920,7 +2920,8 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1 link_with : [libshared], dependencies : [libcryptsetup, libp11kit, - versiondep], + versiondep, + libopenssl], install_rpath : rootpkglibdir, install : true, install_dir : rootlibexecdir) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index cd2065f480..5efce63a38 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -8,6 +8,7 @@ #include #include "sd-device.h" +#include "sd-messages.h" #include "alloc-util.h" #include "ask-password-api.h" @@ -38,6 +39,7 @@ #include "random-util.h" #include "string-table.h" #include "strv.h" +#include "tpm-pcr.h" #include "tpm2-util.h" /* internal helper */ @@ -89,13 +91,15 @@ static bool arg_fido2_device_auto = false; static void *arg_fido2_cid = NULL; static size_t arg_fido2_cid_size = 0; static char *arg_fido2_rp_id = NULL; -static char *arg_tpm2_device = NULL; +static char *arg_tpm2_device = NULL; /* These and the following fields are about locking an encypted volume to the local TPM */ static bool arg_tpm2_device_auto = false; static uint32_t arg_tpm2_pcr_mask = UINT32_MAX; static char *arg_tpm2_signature = NULL; static bool arg_tpm2_pin = false; static bool arg_headless = false; static usec_t arg_token_timeout_usec = 30*USEC_PER_SEC; +static unsigned arg_tpm2_measure_pcr = UINT_MAX; /* This and the following field is about measuring the unlocked volume key to the local TPM */ +static char **arg_tpm2_measure_banks = NULL; STATIC_DESTRUCTOR_REGISTER(arg_cipher, freep); STATIC_DESTRUCTOR_REGISTER(arg_hash, freep); @@ -107,6 +111,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_fido2_cid, freep); STATIC_DESTRUCTOR_REGISTER(arg_fido2_rp_id, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_banks, strv_freep); static const char* const passphrase_type_table[_PASSPHRASE_TYPE_MAX] = { [PASSPHRASE_REGULAR] = "passphrase", @@ -420,6 +425,48 @@ static int parse_one_option(const char *option) { arg_tpm2_pin = r; + } else if ((val = startswith(option, "tpm2-measure-pcr="))) { + unsigned pcr; + + r = safe_atou(val, &pcr); + if (r < 0) { + r = parse_boolean(val); + if (r < 0) { + log_error_errno(r, "Failed to parse %s, ignoring: %m", option); + return 0; + } + + pcr = r ? TPM_PCR_INDEX_VOLUME_KEY : UINT_MAX; + } else if (pcr >= TPM2_PCRS_MAX) { + log_error("Selected TPM index for measurement %u outside of allowed range 0…%u, ignoring.", pcr, TPM2_PCRS_MAX-1); + return 0; + } + + arg_tpm2_measure_pcr = pcr; + + } else if ((val = startswith(option, "tpm2-measure-bank="))) { + +#if HAVE_OPENSSL + _cleanup_strv_free_ char **l = NULL; + + l = strv_split(optarg, ":"); + if (!l) + return log_oom(); + + STRV_FOREACH(i, l) { + const EVP_MD *implementation; + + implementation = EVP_get_digestbyname(*i); + if (!implementation) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown bank '%s', refusing.", val); + + if (strv_extend(&arg_tpm2_measure_banks, EVP_MD_name(implementation)) < 0) + return log_oom(); + } +#else + log_error("Build lacks OpenSSL support, cannot measure to PCR banks, ignoring: %s", option); +#endif + } else if ((val = startswith(option, "try-empty-password="))) { r = parse_boolean(val); @@ -762,6 +809,149 @@ static int get_password( return 0; } +static int measure_volume_key( + struct crypt_device *cd, + const char *name, + const void *volume_key, + size_t volume_key_size) { + + int r; + + assert(cd); + assert(name); + assert(volume_key); + assert(volume_key_size > 0); + + if (arg_tpm2_measure_pcr == UINT_MAX) { + log_debug("Not measuring volume key, deactivated."); + return 0; + } + +#if HAVE_TPM2 + r = dlopen_tpm2(); + if (r < 0) + return log_error_errno(r, "Failed to load TPM2 libraries: %m"); + + _cleanup_(tpm2_context_destroy) struct tpm2_context c = {}; + r = tpm2_context_init(arg_tpm2_device, &c); + if (r < 0) + return r; + + _cleanup_strv_free_ char **l = NULL; + if (strv_isempty(arg_tpm2_measure_banks)) { + r = tpm2_get_good_pcr_banks_strv(c.esys_context, UINT32_C(1) << arg_tpm2_measure_pcr, &l); + if (r < 0) + return r; + } + + _cleanup_free_ char *joined = strv_join(l ?: arg_tpm2_measure_banks, ", "); + if (!joined) + return log_oom(); + + /* Note: we don't directly measure the volume key, it might be a security problem to send an + * unprotected direct hash of the secret volume key over the wire to the TPM. Hence let's instead + * send a HMAC signature instead. */ + + _cleanup_free_ char *escaped = NULL; + escaped = xescape(name, ":"); /* avoid ambiguity around ":" once we join things below */ + if (!escaped) + return log_oom(); + + _cleanup_free_ char *s = NULL; + s = strjoin("cryptsetup:", escaped, ":", strempty(crypt_get_uuid(cd))); + if (!s) + return log_oom(); + + r = tpm2_extend_bytes(c.esys_context, l ?: arg_tpm2_measure_banks, arg_tpm2_measure_pcr, s, SIZE_MAX, volume_key, volume_key_size); + if (r < 0) + return r; + + log_struct(LOG_INFO, + "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR, + LOG_MESSAGE("Successfully extended PCR index %u with '%s' and volume key (banks %s).", arg_tpm2_measure_pcr, s, joined), + "MEASURING=%s", s, + "PCR=%u", arg_tpm2_measure_pcr, + "BANKS=%s", joined); + + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring."); +#endif +} + +static int measured_crypt_activate_by_volume_key( + struct crypt_device *cd, + const char *name, + const void *volume_key, + size_t volume_key_size, + uint32_t flags) { + + int r; + + assert(cd); + assert(name); + + /* A wrapper around crypt_activate_by_volume_key() which also measures to a PCR if that's requested. */ + + r = crypt_activate_by_volume_key(cd, name, volume_key, volume_key_size, flags); + if (r < 0) + return r; + + if (volume_key_size == 0) { + log_debug("Not measuring volume key, none specified."); + return r; + } + + (void) measure_volume_key(cd, name, volume_key, volume_key_size); /* OK if fails */ + return r; +} + +static int measured_crypt_activate_by_passphrase( + struct crypt_device *cd, + const char *name, + int keyslot, + const char *passphrase, + size_t passphrase_size, + uint32_t flags) { + + _cleanup_(erase_and_freep) void *vk = NULL; + size_t vks; + int r; + + assert(cd); + + /* A wrapper around crypt_activate_by_passphrase() which also measures to a PCR if that's + * requested. Note that we need the volume key for the measurement, and + * crypt_activate_by_passphrase() doesn't give us access to this. Hence, we operate indirectly, and + * retrieve the volume key first, and then activate through that. */ + + if (arg_tpm2_measure_pcr == UINT_MAX) { + log_debug("Not measuring volume key, deactivated."); + goto shortcut; + } + + r = crypt_get_volume_key_size(cd); + if (r < 0) + return r; + if (r == 0) { + log_debug("Not measuring volume key, none defined."); + goto shortcut; + } + + vk = malloc(vks = r); + if (!vk) + return -ENOMEM; + + r = crypt_volume_key_get(cd, keyslot, vk, &vks, passphrase, passphrase_size); + if (r < 0) + return r; + + return measured_crypt_activate_by_volume_key(cd, name, vk, vks, flags); + +shortcut: + return crypt_activate_by_passphrase(cd, name, keyslot, passphrase, passphrase_size, flags); +} + static int attach_tcrypt( struct crypt_device *cd, const char *name, @@ -830,7 +1020,7 @@ static int attach_tcrypt( return log_error_errno(r, "Failed to load tcrypt superblock on device %s: %m", crypt_get_device_name(cd)); } - r = crypt_activate_by_volume_key(cd, name, NULL, 0, flags); + r = measured_crypt_activate_by_volume_key(cd, name, NULL, 0, flags); if (r < 0) return log_error_errno(r, "Failed to activate tcrypt device %s: %m", crypt_get_device_name(cd)); @@ -928,6 +1118,14 @@ static int run_security_device_monitor( } static bool libcryptsetup_plugins_support(void) { + +#if HAVE_TPM2 + /* Currently, there's no way for us to query the volume key when plugins are used. Hence don't use + * plugins, if measurement has been requested. */ + if (arg_tpm2_measure_pcr != UINT_MAX) + return false; +#endif + #if HAVE_LIBCRYPTSETUP_PLUGINS int r; @@ -1157,7 +1355,7 @@ static int attach_luks_or_plain_or_bitlk_by_fido2( } if (pass_volume_key) - r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + r = measured_crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); else { _cleanup_(erase_and_freep) char *base64_encoded = NULL; ssize_t base64_encoded_size; @@ -1168,7 +1366,7 @@ static int attach_luks_or_plain_or_bitlk_by_fido2( if (base64_encoded_size < 0) return log_oom(); - r = crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, base64_encoded_size, flags); + r = measured_crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, base64_encoded_size, flags); } if (r == -EPERM) { log_error_errno(r, "Failed to activate with FIDO2 decrypted key. (Key incorrect?)"); @@ -1305,7 +1503,7 @@ static int attach_luks_or_plain_or_bitlk_by_pkcs11( assert(decrypted_key); if (pass_volume_key) - r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + r = measured_crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); else { _cleanup_(erase_and_freep) char *base64_encoded = NULL; ssize_t base64_encoded_size; @@ -1322,7 +1520,7 @@ static int attach_luks_or_plain_or_bitlk_by_pkcs11( if (base64_encoded_size < 0) return log_oom(); - r = crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, base64_encoded_size, flags); + r = measured_crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, base64_encoded_size, flags); } if (r == -EPERM) { log_error_errno(r, "Failed to activate with PKCS#11 decrypted key. (Key incorrect?)"); @@ -1594,7 +1792,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( assert(decrypted_key); if (pass_volume_key) - r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + r = measured_crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); else { _cleanup_(erase_and_freep) char *base64_encoded = NULL; ssize_t base64_encoded_size; @@ -1605,7 +1803,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( if (base64_encoded_size < 0) return log_oom(); - r = crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, base64_encoded_size, flags); + r = measured_crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, base64_encoded_size, flags); } if (r == -EPERM) { log_error_errno(r, "Failed to activate with TPM2 decrypted key. (Key incorrect?)"); @@ -1632,9 +1830,9 @@ static int attach_luks_or_plain_or_bitlk_by_key_data( assert(key_data); if (pass_volume_key) - r = crypt_activate_by_volume_key(cd, name, key_data, key_data_size, flags); + r = measured_crypt_activate_by_volume_key(cd, name, key_data, key_data_size, flags); else - r = crypt_activate_by_passphrase(cd, name, arg_key_slot, key_data, key_data_size, flags); + r = measured_crypt_activate_by_passphrase(cd, name, arg_key_slot, key_data, key_data_size, flags); if (r == -EPERM) { log_error_errno(r, "Failed to activate. (Key incorrect?)"); return -EAGAIN; /* Log actual error, but return EAGAIN */ @@ -1685,9 +1883,9 @@ static int attach_luks_or_plain_or_bitlk_by_key_file( return log_error_errno(r, "Failed to read key file '%s': %m", key_file); if (pass_volume_key) - r = crypt_activate_by_volume_key(cd, name, kfdata, kfsize, flags); + r = measured_crypt_activate_by_volume_key(cd, name, kfdata, kfsize, flags); else - r = crypt_activate_by_passphrase(cd, name, arg_key_slot, kfdata, kfsize, flags); + r = measured_crypt_activate_by_passphrase(cd, name, arg_key_slot, kfdata, kfsize, flags); if (r == -EPERM) { log_error_errno(r, "Failed to activate with key file '%s'. (Key data incorrect?)", key_file); return -EAGAIN; /* Log actual error, but return EAGAIN */ @@ -1713,9 +1911,9 @@ static int attach_luks_or_plain_or_bitlk_by_passphrase( r = -EINVAL; STRV_FOREACH(p, passwords) { if (pass_volume_key) - r = crypt_activate_by_volume_key(cd, name, *p, arg_key_size, flags); + r = measured_crypt_activate_by_volume_key(cd, name, *p, arg_key_size, flags); else - r = crypt_activate_by_passphrase(cd, name, arg_key_slot, *p, strlen(*p), flags); + r = measured_crypt_activate_by_passphrase(cd, name, arg_key_slot, *p, strlen(*p), flags); if (r >= 0) break; } diff --git a/src/fundamental/tpm-pcr.h b/src/fundamental/tpm-pcr.h index d57291328d..e12b4ff607 100644 --- a/src/fundamental/tpm-pcr.h +++ b/src/fundamental/tpm-pcr.h @@ -17,6 +17,9 @@ /* This TPM PCR is where we extend the initrd sysext images into which we pass to the booted kernel */ #define TPM_PCR_INDEX_INITRD_SYSEXTS 13U +/* This TPM PCR is where we measure the root fs volume key (and maybe /var/'s) if it is split off */ +#define TPM_PCR_INDEX_VOLUME_KEY 15U + /* List of PE sections that have special meaning for us in unified kernels. This is the canonical order in * which we measure the sections into TPM PCR 11 (see above). PLEASE DO NOT REORDER! */ typedef enum UnifiedSection { From 572f78767f9958559aa4a3060fc5c9a006766240 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Oct 2022 15:27:34 +0200 Subject: [PATCH 05/15] man: document the new crypttab measurement options --- man/crypttab.xml | 22 ++++++++++++++++++++++ man/systemd-cryptenroll.xml | 5 +++++ 2 files changed, 27 insertions(+) diff --git a/man/crypttab.xml b/man/crypttab.xml index 896a62358d..d587f85289 100644 --- a/man/crypttab.xml +++ b/man/crypttab.xml @@ -700,6 +700,28 @@ order). + + + + Controls whether to measure the volume key of the encrypted volume to a TPM2 PCR. If + set to "no" (which is the default) no PCR extension is done. If set to "yes" the volume key is + measured into PCR 15. If set to a decimal integer in the range 0…23 the volume key is measured into + the specified PCR. The volume key is measured along with the activated volume name and its UUID. This + functionality is particularly useful for the encrypted volume backing the root file system, as it + then allows later TPM objects to be securely bound to the root file system and hence the specific + installation. + + + + + + Selects one or more TPM2 PCR banks to measure the volume key into, as configured with + above. Multiple banks may be specified, separated by a colon + character. If not specified automatically determines available and used banks. Expects a message + digest name (e.g. sha1, sha256, …) as argument, to identify the + bank. + + diff --git a/man/systemd-cryptenroll.xml b/man/systemd-cryptenroll.xml index e4b03936a6..a654d492a1 100644 --- a/man/systemd-cryptenroll.xml +++ b/man/systemd-cryptenroll.xml @@ -324,6 +324,11 @@ 14 The shim project measures its "MOK" certificates and hashes into this PCR. + + + 15 + systemd-cryptsetup7 optionally measures the volume key of activated LUKS volumes into this PCR. + From ff386f985bb51a48a11f74f6370dedf1bbfb4658 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Oct 2022 15:54:09 +0200 Subject: [PATCH 06/15] gpt-auto-generator: automatically measure root/var volume keys into PCR 15 let's enable PCR 15 measurements automatically if gpt-auto discovery is used and systemd-stub is also used. --- man/systemd-gpt-auto-generator.xml | 8 +++++ src/gpt-auto-generator/gpt-auto-generator.c | 36 ++++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/man/systemd-gpt-auto-generator.xml b/man/systemd-gpt-auto-generator.xml index 4ccc80994d..bd542cb7f7 100644 --- a/man/systemd-gpt-auto-generator.xml +++ b/man/systemd-gpt-auto-generator.xml @@ -221,6 +221,13 @@ systems, make sure to set the correct default subvolumes on them, using btrfs subvolume set-default. + If the system was booted via + systemd-stub7 and the + stub reported to userspace that the kernel image was measured to a TPM2 PCR, then any discovered root and + /var/ volume identifiers (and volume encryption key in case it is encrypted) will be + automatically measured into PCR 15 on activation, via + systemd-pcrfs@.service8. + systemd-gpt-auto-generator implements systemd.generator7. @@ -278,6 +285,7 @@ systemd.swap5, systemd-fstab-generator8, systemd-cryptsetup@.service8, + systemd-pcrfs@.service8, machine-id5, cryptsetup8, fstab5, diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index 98c0ca0810..1fc7586524 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -52,10 +52,11 @@ static int add_cryptsetup( const char *what, bool rw, bool require, + bool measure, char **ret_device) { #if HAVE_LIBCRYPTSETUP - _cleanup_free_ char *e = NULL, *n = NULL, *d = NULL; + _cleanup_free_ char *e = NULL, *n = NULL, *d = NULL, *options = NULL; _cleanup_fclose_ FILE *f = NULL; int r; @@ -89,7 +90,28 @@ static int add_cryptsetup( "After=%s\n", d, d); - r = generator_write_cryptsetup_service_section(f, id, what, NULL, rw ? NULL : "read-only"); + if (!rw) { + options = strdup("read-only"); + if (!options) + return log_oom(); + } + + if (measure) { + /* We only measure the root volume key into PCR 15 if we are booted with sd-stub (i.e. in a + * UKI), and sd-stub measured the UKI. We do this in order not to step into people's own PCR + * assignment, under the assumption that people who are fine to use sd-stub with its PCR + * assignments are also OK with our PCR 15 use here. */ + + r = efi_get_variable(EFI_LOADER_VARIABLE(StubPcrKernelImage), NULL, NULL, NULL); /* we don't actually care which PCR the UKI used for itself */ + if (r == -ENOENT) + log_debug_errno(r, "Will not measure volume key of volume '%s', because not booted via systemd-stub with measurements enabled.", id); + else if (r < 0) + log_debug_errno(r, "Failed to determine whether booted via systemd-stub with measurements enabled, ignoring: %m"); + else if (!strextend_with_separator(&options, ",", "tpm2-measure-pcr=yes")) + return log_oom(); + } + + r = generator_write_cryptsetup_service_section(f, id, what, NULL, options); if (r < 0) return r; @@ -144,6 +166,7 @@ static int add_mount( const char *fstype, bool rw, bool growfs, + bool measure, const char *options, const char *description, const char *post) { @@ -164,7 +187,7 @@ static int add_mount( log_debug("Adding %s: %s fstype=%s", where, what, fstype ?: "(any)"); if (streq_ptr(fstype, "crypto_LUKS")) { - r = add_cryptsetup(id, what, rw, true, &crypto_what); + r = add_cryptsetup(id, what, rw, /* require= */ true, measure, &crypto_what); if (r < 0) return r; @@ -291,6 +314,7 @@ static int add_partition_mount( p->fstype, p->rw, p->growfs, + /* measure= */ STR_IN_SET(id, "root", "var"), /* by default measure rootfs and /var, since they contain the "identity" of the system */ NULL, description, SPECIAL_LOCAL_FS_TARGET); @@ -315,7 +339,7 @@ static int add_partition_swap(DissectedPartition *p) { } if (streq_ptr(p->fstype, "crypto_LUKS")) { - r = add_cryptsetup("swap", p->node, true, true, &crypto_what); + r = add_cryptsetup("swap", p->node, /* rw= */ true, /* require= */ true, /* measure= */ false, &crypto_what); if (r < 0) return r; what = crypto_what; @@ -384,6 +408,7 @@ static int add_automount( fstype, rw, growfs, + /* measure= */ false, options, description, NULL); @@ -592,7 +617,7 @@ static int add_root_cryptsetup(void) { /* If a device /dev/gpt-auto-root-luks appears, then make it pull in systemd-cryptsetup-root.service, which * sets it up, and causes /dev/gpt-auto-root to appear which is all we are looking for. */ - return add_cryptsetup("root", "/dev/gpt-auto-root-luks", true, false, NULL); + return add_cryptsetup("root", "/dev/gpt-auto-root-luks", /* rw= */ true, /* require= */ false, /* measure= */ true, NULL); #else return 0; #endif @@ -639,6 +664,7 @@ static int add_root_mount(void) { arg_root_fstype, /* rw= */ arg_root_rw > 0, /* growfs= */ false, + /* measure= */ true, arg_root_options, "Root Partition", in_initrd() ? SPECIAL_INITRD_ROOT_FS_TARGET : SPECIAL_LOCAL_FS_TARGET); From 17984c55513fc18f9bd4878c37fa87d278ab1e1d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Oct 2022 23:29:48 +0200 Subject: [PATCH 07/15] pcrphase: make tool more generic, reuse for measuring machine id/fs uuids See: #24503 --- meson.build | 1 + src/boot/pcrphase.c | 210 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 187 insertions(+), 24 deletions(-) diff --git a/meson.build b/meson.build index 921f7300b0..f940926894 100644 --- a/meson.build +++ b/meson.build @@ -2677,6 +2677,7 @@ if conf.get('HAVE_BLKID') == 1 and conf.get('HAVE_GNU_EFI') == 1 link_with : [libshared], dependencies : [libopenssl, tpm2, + libblkid, versiondep], install_rpath : rootpkglibdir, install : true, diff --git a/src/boot/pcrphase.c b/src/boot/pcrphase.c index 6d055e3499..70c919b0f5 100644 --- a/src/boot/pcrphase.c +++ b/src/boot/pcrphase.c @@ -2,13 +2,21 @@ #include +#include #include +#include "blkid-util.h" +#include "blockdev-util.h" #include "build.h" +#include "chase-symlinks.h" #include "efivars.h" #include "env-util.h" +#include "escape.h" +#include "fd-util.h" #include "main-func.h" +#include "mountpoint-util.h" #include "openssl-util.h" +#include "parse-argument.h" #include "parse-util.h" #include "pretty-print.h" #include "tpm-pcr.h" @@ -17,9 +25,12 @@ static bool arg_graceful = false; static char *arg_tpm2_device = NULL; static char **arg_banks = NULL; +static char *arg_file_system = NULL; +static bool arg_machine_id = false; STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); +STATIC_DESTRUCTOR_REGISTER(arg_file_system, freep); static int help(int argc, char *argv[], void *userdata) { _cleanup_free_ char *link = NULL; @@ -29,7 +40,9 @@ static int help(int argc, char *argv[], void *userdata) { if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] WORD ...\n" + printf("%1$s [OPTIONS...] WORD\n" + "%1$s [OPTIONS...] --file-system=PATH\n" + "%1$s [OPTIONS...] --machine-id\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" @@ -37,6 +50,8 @@ static int help(int argc, char *argv[], void *userdata) { " --bank=DIGEST Select TPM bank (SHA1, SHA256)\n" " --tpm2-device=PATH Use specified TPM2 device\n" " --graceful Exit gracefully if no TPM2 device is found\n" + " --file-system=PATH Measure UUID/labels of file system into PCR 15\n" + " --machine-id Measure machine ID into PCR 15\n" "\nSee the %2$s for details.\n", program_invocation_short_name, link, @@ -54,6 +69,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_BANK, ARG_TPM2_DEVICE, ARG_GRACEFUL, + ARG_FILE_SYSTEM, + ARG_MACHINE_ID, }; static const struct option options[] = { @@ -62,10 +79,12 @@ static int parse_argv(int argc, char *argv[]) { { "bank", required_argument, NULL, ARG_BANK }, { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, { "graceful", no_argument, NULL, ARG_GRACEFUL }, + { "file-system", required_argument, NULL, ARG_FILE_SYSTEM }, + { "machine-id", no_argument, NULL, ARG_MACHINE_ID }, {} }; - int c; + int c, r; assert(argc >= 0); assert(argv); @@ -113,6 +132,17 @@ static int parse_argv(int argc, char *argv[]) { arg_graceful = true; break; + case ARG_FILE_SYSTEM: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_file_system); + if (r < 0) + return r; + + break; + + case ARG_MACHINE_ID: + arg_machine_id = true; + break; + case '?': return -EINVAL; @@ -120,10 +150,13 @@ static int parse_argv(int argc, char *argv[]) { assert_not_reached(); } + if (arg_file_system && arg_machine_id) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system= and --machine-id may not be combined."); + return 1; } -static int determine_banks(struct tpm2_context *c) { +static int determine_banks(struct tpm2_context *c, unsigned target_pcr_nr) { _cleanup_strv_free_ char **l = NULL; int r; @@ -132,7 +165,7 @@ static int determine_banks(struct tpm2_context *c) { if (!strv_isempty(arg_banks)) /* Explicitly configured? Then use that */ return 0; - r = tpm2_get_good_pcr_banks_strv(c->esys_context, UINT32_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE, &l); + r = tpm2_get_good_pcr_banks_strv(c->esys_context, UINT32_C(1) << target_pcr_nr, &l); if (r < 0) return r; @@ -140,11 +173,77 @@ static int determine_banks(struct tpm2_context *c) { return 0; } +static int get_file_system_word( + sd_device *d, + const char *prefix, + char **ret) { + + int r; + + assert(d); + assert(prefix); + assert(ret); + + _cleanup_close_ int block_fd = sd_device_open(d, O_RDONLY|O_CLOEXEC|O_NONBLOCK); + if (block_fd < 0) + return block_fd; + + _cleanup_(blkid_free_probep) blkid_probe b = blkid_new_probe(); + if (!b) + return -ENOMEM; + + errno = 0; + r = blkid_probe_set_device(b, block_fd, 0, 0); + if (r != 0) + return errno_or_else(ENOMEM); + + (void) blkid_probe_enable_superblocks(b, 1); + (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE|BLKID_SUBLKS_UUID|BLKID_SUBLKS_LABEL); + (void) blkid_probe_enable_partitions(b, 1); + (void) blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS); + + errno = 0; + r = blkid_do_safeprobe(b); + if (r == _BLKID_SAFEPROBE_ERROR) + return errno_or_else(EIO); + if (IN_SET(r, _BLKID_SAFEPROBE_AMBIGUOUS, _BLKID_SAFEPROBE_NOT_FOUND)) + return -ENOPKG; + + assert(r == _BLKID_SAFEPROBE_FOUND); + + _cleanup_strv_free_ char **l = strv_new(prefix); + if (!l) + return log_oom(); + + FOREACH_STRING(field, "TYPE", "UUID", "LABEL", "PART_ENTRY_UUID", "PART_ENTRY_TYPE", "PART_ENTRY_NAME") { + const char *v = NULL; + + (void) blkid_probe_lookup_value(b, field, &v, NULL); + + _cleanup_free_ char *escaped = xescape(strempty(v), ":"); /* Avoid ambiguity around ":" */ + if (!escaped) + return log_oom(); + + r = strv_consume(&l, TAKE_PTR(escaped)); + if (r < 0) + return log_oom(); + + } + + assert(strv_length(l) == 7); /* We always want 7 components, to avoid ambiguous strings */ + + _cleanup_free_ char *word = strv_join(l, ":"); + if (!word) + return log_oom(); + + *ret = TAKE_PTR(word); + return 0; +} + static int run(int argc, char *argv[]) { + _cleanup_free_ char *joined = NULL, *pcr_string = NULL, *word = NULL; _cleanup_(tpm2_context_destroy) struct tpm2_context c = {}; - _cleanup_free_ char *joined = NULL, *pcr_string = NULL; - const char *word; - unsigned pcr_nr; + unsigned target_pcr_nr, efi_pcr_nr; size_t length; int r; @@ -154,16 +253,79 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; - if (optind+1 != argc) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single argument."); + if (arg_file_system) { + _cleanup_free_ char *normalized = NULL, *normalized_escaped = NULL; + _cleanup_(sd_device_unrefp) sd_device *d = NULL; + _cleanup_close_ int dfd = -EBADF; - word = argv[optind]; + if (optind != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected no argument."); - /* 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."); + dfd = chase_symlinks_and_open(arg_file_system, NULL, 0, O_DIRECTORY|O_CLOEXEC, &normalized); + if (dfd < 0) + return log_error_errno(dfd, "Failed to open path '%s': %m", arg_file_system); + + r = fd_is_mount_point(dfd, NULL, 0); + if (r < 0) + return log_error_errno(r, "Failed to determine if path '%s' is mount point: %m", normalized); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "Specified path '%s' is not a mount point, refusing: %m", normalized); + + normalized_escaped = xescape(normalized, ":"); /* Avoid ambiguity around ":" */ + if (!normalized_escaped) + return log_oom(); + + _cleanup_free_ char* prefix = strjoin("file-system:", normalized_escaped); + if (!prefix) + return log_oom(); + + r = block_device_new_from_fd(dfd, BLOCK_DEVICE_LOOKUP_BACKING, &d); + if (r < 0) { + log_notice_errno(r, "Unable to determine backing block device of '%s', measuring generic fallback file system identity string: %m", arg_file_system); + + word = strjoin(prefix, "::::::"); + if (!word) + return log_oom(); + } else { + r = get_file_system_word(d, prefix, &word); + if (r < 0) + return log_error_errno(r, "Failed to get file system identifier string for '%s': %m", arg_file_system); + } + + target_pcr_nr = TPM_PCR_INDEX_VOLUME_KEY; /* → PCR 15 */ + + } else if (arg_machine_id) { + sd_id128_t mid; + + if (optind != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected no argument."); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return log_error_errno(r, "Failed to acquire machine ID: %m"); + + word = strjoin("machine-id:", SD_ID128_TO_STRING(mid)); + if (!word) + return log_oom(); + + target_pcr_nr = TPM_PCR_INDEX_VOLUME_KEY; /* → PCR 15 */ + + } else { + if (optind+1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single argument."); + + word = strdup(argv[optind]); + if (!word) + return log_oom(); + + /* 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."); + + target_pcr_nr = TPM_PCR_INDEX_KERNEL_IMAGE; /* → PCR 11 */ + } if (arg_graceful && tpm2_support() != TPM2_SUPPORT_FULL) { log_notice("No complete TPM2 support detected, exiting gracefully."); @@ -188,14 +350,14 @@ static int run(int argc, char *argv[]) { return log_error_errno(r, "Failed to read StubPcrKernelImage EFI variable: %m"); else { /* Let's validate that the stub announced PCR 11 as we expected. */ - r = safe_atou(pcr_string, &pcr_nr); + r = safe_atou(pcr_string, &efi_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) { + if (efi_pcr_nr != TPM_PCR_INDEX_KERNEL_IMAGE) { if (b != 0) - 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); + return log_error_errno(SYNTHETIC_ERRNO(EREMOTE), "Kernel stub measured kernel image into PCR %u, which is different than expected %u.", efi_pcr_nr, TPM_PCR_INDEX_KERNEL_IMAGE); else - log_notice("Kernel stub measured kernel image into PCR %u, which is different than expected %u, but told to measure anyway, hence proceeding.", pcr_nr, TPM_PCR_INDEX_KERNEL_IMAGE); + log_notice("Kernel stub measured kernel image into PCR %u, which is different than expected %u, but told to measure anyway, hence proceeding.", efi_pcr_nr, TPM_PCR_INDEX_KERNEL_IMAGE); } else log_debug("Kernel stub reported same PCR %u as we want to use, proceeding.", TPM_PCR_INDEX_KERNEL_IMAGE); } @@ -208,7 +370,7 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - r = determine_banks(&c); + r = determine_banks(&c, target_pcr_nr); if (r < 0) return r; if (strv_isempty(arg_banks)) /* Still none? */ @@ -218,17 +380,17 @@ static int run(int argc, char *argv[]) { if (!joined) return log_oom(); - log_debug("Measuring '%s' into PCR index %u, banks %s.", word, TPM_PCR_INDEX_KERNEL_IMAGE, joined); + log_debug("Measuring '%s' into PCR index %u, banks %s.", word, target_pcr_nr, joined); - r = tpm2_extend_bytes(c.esys_context, arg_banks, TPM_PCR_INDEX_KERNEL_IMAGE, word, length, NULL, 0); /* → PCR 11 */ + r = tpm2_extend_bytes(c.esys_context, arg_banks, target_pcr_nr, word, length, NULL, 0); if (r < 0) return r; 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), + LOG_MESSAGE("Extended PCR index %u with '%s' (banks %s).", target_pcr_nr, word, joined), "MEASURING=%s", word, - "PCR=%u", TPM_PCR_INDEX_KERNEL_IMAGE, + "PCR=%u", target_pcr_nr, "BANKS=%s", joined); return EXIT_SUCCESS; From 072c8f650519f47a575b1e39509599ace21e2c8f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 16 Oct 2022 18:21:12 +0200 Subject: [PATCH 08/15] units: measure /etc/machine-id into PCR 15 during early boot We want PCR 15 to be useful for binding per-system policy to. Let's measure the machine ID into it, to ensure that every OS we can distinguish will get a different PCR (even if the root disk encryption key is already measured into it). --- units/meson.build | 2 ++ units/systemd-pcrmachine.service.in | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 units/systemd-pcrmachine.service.in diff --git a/units/meson.build b/units/meson.build index 69197f0c47..48b24f05c1 100644 --- a/units/meson.build +++ b/units/meson.build @@ -265,6 +265,8 @@ in_units = [ 'sysinit.target.wants/'], ['systemd-pcrphase.service', 'HAVE_GNU_EFI HAVE_OPENSSL HAVE_TPM2', 'sysinit.target.wants/'], + ['systemd-pcrmachine.service', 'HAVE_GNU_EFI HAVE_OPENSSL HAVE_TPM2', + 'sysinit.target.wants/'], ] add_wants = [] diff --git a/units/systemd-pcrmachine.service.in b/units/systemd-pcrmachine.service.in new file mode 100644 index 0000000000..e154a7eec1 --- /dev/null +++ b/units/systemd-pcrmachine.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 Machine ID Measurement +Documentation=man:systemd-pcrmachine.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=sysinit.target shutdown.target +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 --machine-id From 0ba07f907721941f611eaca9521937c467bdfff2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 16 Oct 2022 22:39:31 +0200 Subject: [PATCH 09/15] generator: teach generator_add_symlink() to instantiate specified unit if we want generators to instantiate a template service, we need to teach generator_add_symlink() the concept. Just some preparation for a later commit. While we are at it, modernize the function around path_extract_filename() + path_extract_directory() --- src/shared/generator.c | 52 ++++++++++++++++++++++++++++++++---------- src/shared/generator.h | 6 ++++- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/shared/generator.c b/src/shared/generator.c index 6d95aa72f8..ec0182d05e 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -59,25 +59,53 @@ int generator_open_unit_file( return 0; } -int generator_add_symlink(const char *dir, const char *dst, const char *dep_type, const char *src) { - _cleanup_free_ char *bn = NULL; - const char *from, *to; + +int generator_add_symlink_full( + const char *dir, + const char *dst, + const char *dep_type, + const char *src, + const char *instance) { + + _cleanup_free_ char *dn = NULL, *fn = NULL, *instantiated = NULL, *to = NULL, *from = NULL; int r; - /* Adds a symlink from ./ to (if src is absolute) - * or ../ (otherwise). */ + assert(dir); + assert(dst); + assert(dep_type); + assert(src); - r = path_extract_filename(src, &bn); + /* Adds a symlink from ./ to (if src is absolute) or ../ (otherwise). If + * is specified, then must be a template unit name, and we'll instantiate it. */ + + r = path_extract_directory(src, &dn); + if (r < 0 && r != -EDESTADDRREQ) /* EDESTADDRREQ → just a file name was passed */ + return log_error_errno(r, "Failed to extract directory name from '%s': %m", src); + + r = path_extract_filename(src, &fn); if (r < 0) - return log_error_errno(r, "Failed to extract filename from '%s': %m", src); + return log_error_errno(r, "Failed to extract file name from '%s': %m", src); + if (r == O_DIRECTORY) + return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Expected path to regular file name, but got '%s', refusing.", src); - from = path_is_absolute(src) ? src : strjoina("../", src); - to = strjoina(dir, "/", dst, ".", dep_type, "/", bn); + if (instance) { + r = unit_name_replace_instance(fn, instance, &instantiated); + if (r < 0) + return log_error_errno(r, "Failed to instantiate '%s' for '%s': %m", fn, instance); + } + + from = path_join(dn ?: "..", fn); + if (!from) + return log_oom(); + + to = strjoin(dir, "/", dst, ".", dep_type, "/", instantiated ?: fn); + if (!to) + return log_oom(); (void) mkdir_parents_label(to, 0755); - if (symlink(from, to) < 0) - if (errno != EEXIST) - return log_error_errno(errno, "Failed to create symlink \"%s\": %m", to); + + if (symlink(from, to) < 0 && errno != EEXIST) + return log_error_errno(errno, "Failed to create symlink \"%s\": %m", to); return 0; } diff --git a/src/shared/generator.h b/src/shared/generator.h index 1b4f36ac53..a4049dbd8f 100644 --- a/src/shared/generator.h +++ b/src/shared/generator.h @@ -12,7 +12,11 @@ int generator_open_unit_file( const char *name, FILE **file); -int generator_add_symlink(const char *dir, const char *dst, const char *dep_type, const char *src); +int generator_add_symlink_full(const char *dir, const char *dst, const char *dep_type, const char *src, const char *instance); + +static inline int generator_add_symlink(const char *dir, const char *dst, const char *dep_type, const char *src) { + return generator_add_symlink_full(dir, dst, dep_type, src, NULL); +} int generator_write_fsck_deps( FILE *f, From 50072ccf1bfee8a53563a083a3a52b26f0d5678f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 16 Oct 2022 22:45:17 +0200 Subject: [PATCH 10/15] units: rework growfs units to be just a regular unit that is instantiated The systemd-growfs@.service units are currently written in full for each file system to grow. Which is kinda pointless given that (besides an optional ordering dep) they contain always the same definition. Let's fix that and add a static template for this logic, that the generator simply instantiates (and adds an ordering dep for). This mimics how systemd-fsck@.service is handled. Similar to the wait that for root fs there's a special instance systemd-fsck-root.service we also add a special instance systemd-growfs-root.service for the root fs, since it has slightly different deps. Fixes: #20788 See: #10014 --- src/basic/special.h | 2 + src/shared/generator.c | 126 ++++++++++++++++----------- units/meson.build | 2 + units/systemd-growfs-root.service.in | 22 +++++ units/systemd-growfs@.service.in | 23 +++++ 5 files changed, 126 insertions(+), 49 deletions(-) create mode 100644 units/systemd-growfs-root.service.in create mode 100644 units/systemd-growfs@.service.in diff --git a/src/basic/special.h b/src/basic/special.h index 5d1111fd71..9bb36c5732 100644 --- a/src/basic/special.h +++ b/src/basic/special.h @@ -87,6 +87,8 @@ #define SPECIAL_REMOUNT_FS_SERVICE "systemd-remount-fs.service" #define SPECIAL_VOLATILE_ROOT_SERVICE "systemd-volatile-root.service" #define SPECIAL_UDEVD_SERVICE "systemd-udevd.service" +#define SPECIAL_GROWFS_SERVICE "systemd-growfs@.service" +#define SPECIAL_GROWFS_ROOT_SERVICE "systemd-growfs-root.service" /* Services systemd relies on */ #define SPECIAL_DBUS_SERVICE "dbus.service" diff --git a/src/shared/generator.c b/src/shared/generator.c index ec0182d05e..6c82d0f0ef 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -110,6 +110,64 @@ int generator_add_symlink_full( return 0; } +static int generator_add_ordering( + const char *dir, + const char *src, + const char *order, + const char *dst, + const char *instance) { + + _cleanup_free_ char *instantiated = NULL, *p = NULL, *fn = NULL; + _cleanup_fclose_ FILE *f = NULL; + const char *to; + int r; + + assert(dir); + assert(src); + assert(order); + assert(dst); + + /* Adds in an explicit ordering dependency of type from to . If is + * specified, it is inserted into . */ + + if (instance) { + r = unit_name_replace_instance(dst, instance, &instantiated); + if (r < 0) + return log_error_errno(r, "Failed to instantiate '%s' for '%s': %m", dst, instance); + + to = instantiated; + } else + to = dst; + + fn = strjoin(src, ".d/50-order-", to, ".conf"); + if (!fn) + return log_oom(); + + p = path_join(dir, fn); + if (!p) + return log_oom(); + + (void) mkdir_parents_label(p, 0755); + + r = fopen_unlocked(p, "wxe", &f); + if (r < 0) + return log_error_errno(r, "Failed to create '%s': %m", p); + + fprintf(f, + "# Automatically generated by %s\n\n" + "[Unit]\n" + "%s=%s\n", + program_invocation_short_name, + order, + to); + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to write drop-in '%s': %m", p); + + return 0; +} + static int write_fsck_sysroot_service( const char *unit, /* Either SPECIAL_FSCK_ROOT_SERVICE or SPECIAL_FSCK_USR_SERVICE */ const char *dir, @@ -560,66 +618,36 @@ int generator_hook_up_growfs( const char *where, const char *target) { - _cleanup_free_ char *unit = NULL, *escaped = NULL, *where_unit = NULL, *unit_file = NULL; - _cleanup_fclose_ FILE *f = NULL; + const char *growfs_unit, *growfs_unit_path; + _cleanup_free_ char *where_unit = NULL, *instance = NULL; int r; assert(dir); assert(where); - escaped = cescape(where); - if (!escaped) - return log_oom(); - - r = unit_name_from_path_instance("systemd-growfs", where, ".service", &unit); - if (r < 0) - return log_error_errno(r, "Failed to make unit instance name from path \"%s\": %m", - where); - r = unit_name_from_path(where, ".mount", &where_unit); if (r < 0) - return log_error_errno(r, "Failed to make unit name from path \"%s\": %m", - where); + return log_error_errno(r, "Failed to make unit name from path '%s': %m", where); - unit_file = path_join(dir, unit); - if (!unit_file) - return log_oom(); + if (empty_or_root(where)) { + growfs_unit = SPECIAL_GROWFS_ROOT_SERVICE; + growfs_unit_path = SYSTEM_DATA_UNIT_DIR "/" SPECIAL_GROWFS_ROOT_SERVICE; + } else { + growfs_unit = SPECIAL_GROWFS_SERVICE; + growfs_unit_path = SYSTEM_DATA_UNIT_DIR "/" SPECIAL_GROWFS_SERVICE; - log_debug("Creating %s", unit_file); + r = unit_name_path_escape(where, &instance); + if (r < 0) + return log_error_errno(r, "Failed to escape path '%s': %m", where); + } - f = fopen(unit_file, "wxe"); - if (!f) - return log_error_errno(errno, "Failed to create unit file %s: %m", - unit_file); + if (target) { + r = generator_add_ordering(dir, target, "After", growfs_unit, instance); + if (r < 0) + return r; + } - fprintf(f, - "# Automatically generated by %s\n\n" - "[Unit]\n" - "Description=Grow File System on %%f\n" - "Documentation=man:systemd-growfs@.service(8)\n" - "DefaultDependencies=no\n" - "BindsTo=%%i.mount\n" - "Conflicts=shutdown.target\n" - "After=systemd-repart.service %%i.mount\n" - "Before=shutdown.target%s%s\n", - program_invocation_short_name, - target ? " " : "", - strempty(target)); - - if (empty_or_root(where)) /* Make sure the root fs is actually writable before we resize it */ - fprintf(f, - "After=systemd-remount-fs.service\n"); - - fprintf(f, - "\n" - "[Service]\n" - "Type=oneshot\n" - "RemainAfterExit=yes\n" - "ExecStart="SYSTEMD_GROWFS_PATH " %s\n" - "TimeoutSec=0\n", - escaped); - - return generator_add_symlink(dir, where_unit, "wants", unit); + return generator_add_symlink_full(dir, where_unit, "wants", growfs_unit_path, instance); } int generator_enable_remount_fs_service(const char *dir) { diff --git a/units/meson.build b/units/meson.build index 48b24f05c1..62ce438a3f 100644 --- a/units/meson.build +++ b/units/meson.build @@ -267,6 +267,8 @@ in_units = [ 'sysinit.target.wants/'], ['systemd-pcrmachine.service', 'HAVE_GNU_EFI HAVE_OPENSSL HAVE_TPM2', 'sysinit.target.wants/'], + ['systemd-growfs-root.service', ''], + ['systemd-growfs@.service', ''], ] add_wants = [] diff --git a/units/systemd-growfs-root.service.in b/units/systemd-growfs-root.service.in new file mode 100644 index 0000000000..295bafd5af --- /dev/null +++ b/units/systemd-growfs-root.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=Grow Root File System +Documentation=man:systemd-growfs-root.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=systemd-repart.service systemd-remount-fs.service +Before=shutdown.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{ROOTLIBEXECDIR}}/systemd-growfs / +TimeoutSec=0 diff --git a/units/systemd-growfs@.service.in b/units/systemd-growfs@.service.in new file mode 100644 index 0000000000..7154e4ca76 --- /dev/null +++ b/units/systemd-growfs@.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=Grow File System on %f +Documentation=man:systemd-growfs@.service(8) +DefaultDependencies=no +BindsTo=%i.mount +Conflicts=shutdown.target +After=systemd-repart.service %i.mount +Before=shutdown.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{ROOTLIBEXECDIR}}/systemd-growfs %f +TimeoutSec=0 From 04959faa632272a8fc9cdac3121b2e4af721c1b6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 16 Oct 2022 23:25:04 +0200 Subject: [PATCH 11/15] generators: optionally, measure file systems at boot If we use gpt-auto-generator, automatically measure root fs and /var. Otherwise, add x-systemd.measure option to request this. --- src/basic/special.h | 2 ++ src/fstab-generator/fstab-generator.c | 19 ++++++++--- src/gpt-auto-generator/gpt-auto-generator.c | 6 ++++ src/shared/generator.c | 37 +++++++++++++++++++++ src/shared/generator.h | 4 +++ units/meson.build | 2 ++ units/systemd-pcrfs-root.service.in | 24 +++++++++++++ units/systemd-pcrfs@.service.in | 25 ++++++++++++++ 8 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 units/systemd-pcrfs-root.service.in create mode 100644 units/systemd-pcrfs@.service.in diff --git a/src/basic/special.h b/src/basic/special.h index 9bb36c5732..0e4342eb40 100644 --- a/src/basic/special.h +++ b/src/basic/special.h @@ -89,6 +89,8 @@ #define SPECIAL_UDEVD_SERVICE "systemd-udevd.service" #define SPECIAL_GROWFS_SERVICE "systemd-growfs@.service" #define SPECIAL_GROWFS_ROOT_SERVICE "systemd-growfs-root.service" +#define SPECIAL_PCRFS_SERVICE "systemd-pcrfs@.service" +#define SPECIAL_PCRFS_ROOT_SERVICE "systemd-pcrfs-root.service" /* Services systemd relies on */ #define SPECIAL_DBUS_SERVICE "dbus.service" diff --git a/src/fstab-generator/fstab-generator.c b/src/fstab-generator/fstab-generator.c index b0ea536eb2..4b763bc247 100644 --- a/src/fstab-generator/fstab-generator.c +++ b/src/fstab-generator/fstab-generator.c @@ -40,6 +40,7 @@ typedef enum MountPointFlags { MOUNT_MAKEFS = 1 << 3, MOUNT_GROWFS = 1 << 4, MOUNT_RW_ONLY = 1 << 5, + MOUNT_PCRFS = 1 << 6, } MountPointFlags; static bool arg_sysroot_check = false; @@ -176,6 +177,8 @@ static int add_swap( if (flags & MOUNT_GROWFS) /* TODO: swap devices must be wiped and recreated */ log_warning("%s: growing swap devices is currently unsupported.", what); + if (flags & MOUNT_PCRFS) + log_warning("%s: measuring swap devices is currently unsupported.", what); if (!(flags & MOUNT_NOAUTO)) { r = generator_add_symlink(arg_dest, SPECIAL_SWAP_TARGET, @@ -525,6 +528,12 @@ static int add_mount( return r; } + if (flags & MOUNT_PCRFS) { + r = generator_hook_up_pcrfs(dest, where, target_unit); + if (r < 0) + return r; + } + if (!FLAGS_SET(flags, MOUNT_AUTOMOUNT)) { if (!FLAGS_SET(flags, MOUNT_NOAUTO) && strv_isempty(wanted_by) && strv_isempty(required_by)) { r = generator_add_symlink(dest, target_unit, @@ -658,7 +667,7 @@ static int parse_fstab(bool initrd) { while ((me = getmntent(f))) { _cleanup_free_ char *where = NULL, *what = NULL, *canonical_where = NULL; - bool makefs, growfs, noauto, nofail; + bool makefs, growfs, pcrfs, noauto, nofail; MountPointFlags flags; int k; @@ -718,16 +727,18 @@ static int parse_fstab(bool initrd) { makefs = fstab_test_option(me->mnt_opts, "x-systemd.makefs\0"); growfs = fstab_test_option(me->mnt_opts, "x-systemd.growfs\0"); + pcrfs = fstab_test_option(me->mnt_opts, "x-systemd.pcrfs\0"); noauto = fstab_test_yes_no_option(me->mnt_opts, "noauto\0" "auto\0"); nofail = fstab_test_yes_no_option(me->mnt_opts, "nofail\0" "fail\0"); - log_debug("Found entry what=%s where=%s type=%s makefs=%s growfs=%s noauto=%s nofail=%s", + log_debug("Found entry what=%s where=%s type=%s makefs=%s growfs=%s pcrfs=%s noauto=%s nofail=%s", what, where, me->mnt_type, - yes_no(makefs), yes_no(growfs), + yes_no(makefs), yes_no(growfs), yes_no(pcrfs), yes_no(noauto), yes_no(nofail)); flags = makefs * MOUNT_MAKEFS | growfs * MOUNT_GROWFS | + pcrfs * MOUNT_PCRFS | noauto * MOUNT_NOAUTO | nofail * MOUNT_NOFAIL; @@ -911,7 +922,7 @@ static int add_sysroot_mount(void) { fstype, opts, is_device_path(what) ? 1 : 0, /* passno */ - flags, /* makefs, growfs off, noauto off, nofail off, automount off */ + flags, /* makefs off, pcrfs off, noauto off, nofail off, automount off */ SPECIAL_INITRD_ROOT_FS_TARGET); } diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index 1fc7586524..63bb2a3cbb 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -259,6 +259,12 @@ static int add_mount( return r; } + if (measure) { + r = generator_hook_up_pcrfs(arg_dest, where, post); + if (r < 0) + return r; + } + if (post) { r = generator_add_symlink(arg_dest, post, "requires", unit); if (r < 0) diff --git a/src/shared/generator.c b/src/shared/generator.c index 6c82d0f0ef..64f4a2741c 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -650,6 +650,43 @@ int generator_hook_up_growfs( return generator_add_symlink_full(dir, where_unit, "wants", growfs_unit_path, instance); } +int generator_hook_up_pcrfs( + const char *dir, + const char *where, + const char *target) { + + const char *pcrfs_unit, *pcrfs_unit_path; + _cleanup_free_ char *where_unit = NULL, *instance = NULL; + int r; + + assert(dir); + assert(where); + + r = unit_name_from_path(where, ".mount", &where_unit); + if (r < 0) + return log_error_errno(r, "Failed to make unit name from path '%s': %m", where); + + if (empty_or_root(where)) { + pcrfs_unit = SPECIAL_PCRFS_ROOT_SERVICE; + pcrfs_unit_path = SYSTEM_DATA_UNIT_DIR "/" SPECIAL_PCRFS_ROOT_SERVICE; + } else { + pcrfs_unit = SPECIAL_PCRFS_SERVICE; + pcrfs_unit_path = SYSTEM_DATA_UNIT_DIR "/" SPECIAL_PCRFS_SERVICE; + + r = unit_name_path_escape(where, &instance); + if (r < 0) + return log_error_errno(r, "Failed to escape path '%s': %m", where); + } + + if (target) { + r = generator_add_ordering(dir, target, "After", pcrfs_unit, instance); + if (r < 0) + return r; + } + + return generator_add_symlink_full(dir, where_unit, "wants", pcrfs_unit_path, instance); +} + int generator_enable_remount_fs_service(const char *dir) { /* Pull in systemd-remount-fs.service */ return generator_add_symlink(dir, SPECIAL_LOCAL_FS_TARGET, "wants", diff --git a/src/shared/generator.h b/src/shared/generator.h index a4049dbd8f..111900fd45 100644 --- a/src/shared/generator.h +++ b/src/shared/generator.h @@ -81,6 +81,10 @@ int generator_hook_up_growfs( const char *dir, const char *where, const char *target); +int generator_hook_up_pcrfs( + const char *dir, + const char *where, + const char *target); int generator_enable_remount_fs_service(const char *dir); diff --git a/units/meson.build b/units/meson.build index 62ce438a3f..d59796b62b 100644 --- a/units/meson.build +++ b/units/meson.build @@ -267,6 +267,8 @@ in_units = [ 'sysinit.target.wants/'], ['systemd-pcrmachine.service', 'HAVE_GNU_EFI HAVE_OPENSSL HAVE_TPM2', 'sysinit.target.wants/'], + ['systemd-pcrfs-root.service', ''], + ['systemd-pcrfs@.service', ''], ['systemd-growfs-root.service', ''], ['systemd-growfs@.service', ''], ] diff --git a/units/systemd-pcrfs-root.service.in b/units/systemd-pcrfs-root.service.in new file mode 100644 index 0000000000..b0da413bb4 --- /dev/null +++ b/units/systemd-pcrfs-root.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 Root File System Measurement +Documentation=man:systemd-pcrfs-root.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=systemd-pcrmachine.service +Before=shutdown.target +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 --file-system=/ diff --git a/units/systemd-pcrfs@.service.in b/units/systemd-pcrfs@.service.in new file mode 100644 index 0000000000..ec1ff118c3 --- /dev/null +++ b/units/systemd-pcrfs@.service.in @@ -0,0 +1,25 @@ +# 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 File System Measurement of %f +Documentation=man:systemd-pcrfs@.service(8) +DefaultDependencies=no +BindsTo=%i.mount +Conflicts=shutdown.target +After=%i.mount systemd-pcrfs-root.service +Before=shutdown.target +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 --file-system=%f From 6c51b49ce0892ff923233a6031add4877100f5b0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Oct 2022 14:50:56 +0200 Subject: [PATCH 12/15] tpm2: add common helper for checking if we are running on UKI with TPM measurements Let's introduce a common implementation of a function that checks whether we are booted on a kernel with systemd-stub that has TPM PCR measurements enabled. Do our own userspace measurements only if we detect that. PCRs are scarce and most likely there are projects which already make use of them in other ways. Hence, instead of blindly stepping into their territory let's conditionalize things so that people have to explicitly buy into our PCR assignments before we start measuring things into them. Specifically bind everything to an UKI that reported measurements. This was previously already implemented in systemd-pcrphase, but with this change we expand this to all tools that process PCR measurement settings. The env var to override the check is renamed to SYSTEMD_FORCE_MEASURE, to make it more generic (since we'll use it at multiple places now). This is not a compat break, since the original env var for that was not included in any stable release yet. --- docs/ENVIRONMENT.md | 9 +++-- src/boot/pcrphase.c | 38 +++++--------------- src/cryptsetup/cryptsetup.c | 9 +++++ src/fstab-generator/fstab-generator.c | 12 +++++-- src/gpt-auto-generator/gpt-auto-generator.c | 10 +++--- src/shared/efi-loader.c | 39 +++++++++++++++++++++ src/shared/efi-loader.h | 2 ++ 7 files changed, 80 insertions(+), 39 deletions(-) diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 2b751d9d53..ef0141e1a6 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -506,7 +506,10 @@ SYSTEMD_HOME_DEBUG_SUFFIX=foo \ journal. Note that journal files in compact mode are limited to 4G to allow use of 32-bit offsets. Enabled by default. -`systemd-pcrphase`: +`systemd-pcrphase`, `systemd-cryptsetup`: -* `$SYSTEMD_PCRPHASE_STUB_VERIFY` – Takes a boolean. If false the requested - measurement is done even if no EFI stub usage was reported via EFI variables. +* `$SYSTEMD_FORCE_MEASURE=1` — If set, force measuring of resources (which are + marked for measurement) even if not booted on a kernel equipped with + systemd-stub. Normally, requested measurement of resources is conditionalized + on kernels that have booted with `systemd-stub`. With this environment + variable the test for that my be bypassed, for testing purposes. diff --git a/src/boot/pcrphase.c b/src/boot/pcrphase.c index 70c919b0f5..003e0b8ad8 100644 --- a/src/boot/pcrphase.c +++ b/src/boot/pcrphase.c @@ -9,15 +9,14 @@ #include "blockdev-util.h" #include "build.h" #include "chase-symlinks.h" +#include "efi-loader.h" #include "efivars.h" -#include "env-util.h" #include "escape.h" #include "fd-util.h" #include "main-func.h" #include "mountpoint-util.h" #include "openssl-util.h" #include "parse-argument.h" -#include "parse-util.h" #include "pretty-print.h" #include "tpm-pcr.h" #include "tpm2-util.h" @@ -241,9 +240,9 @@ static int get_file_system_word( } static int run(int argc, char *argv[]) { - _cleanup_free_ char *joined = NULL, *pcr_string = NULL, *word = NULL; _cleanup_(tpm2_context_destroy) struct tpm2_context c = {}; - unsigned target_pcr_nr, efi_pcr_nr; + _cleanup_free_ char *joined = NULL, *word = NULL; + unsigned target_pcr_nr; size_t length; int r; @@ -334,32 +333,13 @@ static int run(int argc, char *argv[]) { length = strlen(word); - int b = getenv_bool("SYSTEMD_PCRPHASE_STUB_VERIFY"); - if (b < 0 && b != -ENXIO) - log_warning_errno(b, "Unable to parse $SYSTEMD_PCRPHASE_STUB_VERIFY value, ignoring."); - /* 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) { - if (b != 0) { - log_info("Kernel stub did not measure kernel image into PCR %u, skipping measurement.", TPM_PCR_INDEX_KERNEL_IMAGE); - return EXIT_SUCCESS; - } else - log_notice("Kernel stub did not measure kernel image into PCR %u, but told to measure anyway, hence proceeding.", TPM_PCR_INDEX_KERNEL_IMAGE); - } else if (r < 0) - return log_error_errno(r, "Failed to read StubPcrKernelImage EFI variable: %m"); - else { - /* Let's validate that the stub announced PCR 11 as we expected. */ - r = safe_atou(pcr_string, &efi_pcr_nr); - if (r < 0) - return log_error_errno(r, "Failed to parse StubPcrKernelImage EFI variable: %s", pcr_string); - if (efi_pcr_nr != TPM_PCR_INDEX_KERNEL_IMAGE) { - if (b != 0) - return log_error_errno(SYNTHETIC_ERRNO(EREMOTE), "Kernel stub measured kernel image into PCR %u, which is different than expected %u.", efi_pcr_nr, TPM_PCR_INDEX_KERNEL_IMAGE); - else - log_notice("Kernel stub measured kernel image into PCR %u, which is different than expected %u, but told to measure anyway, hence proceeding.", efi_pcr_nr, TPM_PCR_INDEX_KERNEL_IMAGE); - } else - log_debug("Kernel stub reported same PCR %u as we want to use, proceeding.", TPM_PCR_INDEX_KERNEL_IMAGE); + r = efi_stub_measured(); + if (r < 0) + return log_error_errno(r, "Failed to detect if we are running on a kernel image with TPM measurement enabled: %m"); + if (r == 0) { + log_info("Kernel stub did not measure kernel image into PCR %u, skipping userspace measurement, too.", TPM_PCR_INDEX_KERNEL_IMAGE); + return EXIT_SUCCESS; } r = dlopen_tpm2(); diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 5efce63a38..2c9d416734 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -19,6 +19,7 @@ #include "cryptsetup-util.h" #include "device-util.h" #include "efi-api.h" +#include "efi-loader.h" #include "env-util.h" #include "escape.h" #include "fileio.h" @@ -827,6 +828,14 @@ static int measure_volume_key( return 0; } + r = efi_stub_measured(); + if (r < 0) + return log_warning_errno(r, "Failed to detect if we are running on a kernel image with TPM measurement enabled: %m"); + if (r == 0) { + log_debug("Kernel stub did not measure kernel image into the expected PCR, skipping userspace measurement, too."); + return 0; + } + #if HAVE_TPM2 r = dlopen_tpm2(); if (r < 0) diff --git a/src/fstab-generator/fstab-generator.c b/src/fstab-generator/fstab-generator.c index 4b763bc247..ed34e0a32f 100644 --- a/src/fstab-generator/fstab-generator.c +++ b/src/fstab-generator/fstab-generator.c @@ -8,6 +8,7 @@ #include "bus-error.h" #include "bus-locator.h" #include "chase-symlinks.h" +#include "efi-loader.h" #include "fd-util.h" #include "fileio.h" #include "fstab-util.h" @@ -529,9 +530,16 @@ static int add_mount( } if (flags & MOUNT_PCRFS) { - r = generator_hook_up_pcrfs(dest, where, target_unit); + r = efi_stub_measured(); if (r < 0) - return r; + log_warning_errno(r, "Failed to detect if we are running on a kernel image with TPM measurement enabled, assuming not: %m"); + else if (r == 0) + log_debug("Kernel stub did not measure kernel image into PCR, skipping userspace measurement, too."); + else { + r = generator_hook_up_pcrfs(dest, where, target_unit); + if (r < 0) + return r; + } } if (!FLAGS_SET(flags, MOUNT_AUTOMOUNT)) { diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index 63bb2a3cbb..9b11318017 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -102,11 +102,11 @@ static int add_cryptsetup( * assignment, under the assumption that people who are fine to use sd-stub with its PCR * assignments are also OK with our PCR 15 use here. */ - r = efi_get_variable(EFI_LOADER_VARIABLE(StubPcrKernelImage), NULL, NULL, NULL); /* we don't actually care which PCR the UKI used for itself */ - if (r == -ENOENT) - log_debug_errno(r, "Will not measure volume key of volume '%s', because not booted via systemd-stub with measurements enabled.", id); - else if (r < 0) - log_debug_errno(r, "Failed to determine whether booted via systemd-stub with measurements enabled, ignoring: %m"); + r = efi_stub_measured(); + if (r < 0) + log_warning_errno(r, "Failed to determine whether booted via systemd-stub with measurements enabled, ignoring: %m"); + else if (r == 0) + log_debug("Will not measure volume key of volume '%s', because not booted via systemd-stub with measurements enabled.", id); else if (!strextend_with_separator(&options, ",", "tpm2-measure-pcr=yes")) return log_oom(); } diff --git a/src/shared/efi-loader.c b/src/shared/efi-loader.c index 1340412cda..621fa082ba 100644 --- a/src/shared/efi-loader.c +++ b/src/shared/efi-loader.c @@ -2,10 +2,12 @@ #include "alloc-util.h" #include "efi-loader.h" +#include "env-util.h" #include "parse-util.h" #include "path-util.h" #include "stat-util.h" #include "strv.h" +#include "tpm-pcr.h" #include "utf8.h" #if ENABLE_EFI @@ -236,6 +238,43 @@ int efi_stub_get_features(uint64_t *ret) { return 0; } +int efi_stub_measured(void) { + _cleanup_free_ char *pcr_string = NULL; + unsigned pcr_nr; + int r; + + /* Checks if we are booted on a kernel with sd-stub which measured the kernel into PCR 11. Or in + * other words, if we are running on a TPM enabled UKI. + * + * Returns == 0 and > 0 depending on the result of the test. Returns -EREMOTE if we detected a stub + * being used, but it measured things into a different PCR than we are configured for in + * userspace. (i.e. we expect PCR 11 being used for this by both sd-stub and us) */ + + r = getenv_bool_secure("SYSTEMD_FORCE_MEASURE"); /* Give user a chance to override the variable test, + * for debugging purposes */ + if (r >= 0) + return r; + if (r != -ENXIO) + log_debug_errno(r, "Failed to parse $SYSTEMD_FORCE_MEASURE, ignoring: %m"); + + if (!is_efi_boot()) + return 0; + + r = efi_get_variable_string(EFI_LOADER_VARIABLE(StubPcrKernelImage), &pcr_string); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + + r = safe_atou(pcr_string, &pcr_nr); + if (r < 0) + return log_debug_errno(r, "Failed to parse StubPcrKernelImage EFI variable: %s", pcr_string); + if (pcr_nr != TPM_PCR_INDEX_KERNEL_IMAGE) + return log_debug_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); + + return 1; +} + int efi_loader_get_config_timeout_one_shot(usec_t *ret) { _cleanup_free_ char *v = NULL; static struct stat cache_stat = {}; diff --git a/src/shared/efi-loader.h b/src/shared/efi-loader.h index 84968869ab..56ccdee9c1 100644 --- a/src/shared/efi-loader.h +++ b/src/shared/efi-loader.h @@ -18,6 +18,8 @@ int efi_loader_get_entries(char ***ret); int efi_loader_get_features(uint64_t *ret); int efi_stub_get_features(uint64_t *ret); +int efi_stub_measured(void); + int efi_loader_get_config_timeout_one_shot(usec_t *ret); int efi_loader_update_entry_one_shot_cache(char **cache, struct stat *cache_stat); From 2bd33c909c0cf02a2a794ac83d66e8b32879c25d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Oct 2022 15:20:53 +0200 Subject: [PATCH 13/15] man: document new machine-id/fs measurement options --- man/rules/meson.build | 5 +++- man/systemd-pcrphase.service.xml | 51 ++++++++++++++++++++++++++++---- man/systemd.mount.xml | 14 +++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/man/rules/meson.build b/man/rules/meson.build index 5be6c9ba46..90324fe1ec 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -966,7 +966,10 @@ manpages = [ ['systemd-path', '1', [], ''], ['systemd-pcrphase.service', '8', - ['systemd-pcrphase', + ['systemd-pcrfs-root.service', + 'systemd-pcrfs@.service', + 'systemd-pcrmachine.service', + 'systemd-pcrphase', 'systemd-pcrphase-initrd.service', 'systemd-pcrphase-sysinit.service'], 'HAVE_GNU_EFI'], diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml index 3012d98624..dde13883f7 100644 --- a/man/systemd-pcrphase.service.xml +++ b/man/systemd-pcrphase.service.xml @@ -20,15 +20,21 @@ systemd-pcrphase.service systemd-pcrphase-sysinit.service systemd-pcrphase-initrd.service + systemd-pcrmachine.service + systemd-pcrfs-root.service + systemd-pcrfs@.service systemd-pcrphase - Measure boot phase into TPM2 PCR 11 + Measure boot phase into TPM2 PCR 11, machine ID and file system identity into PCR 15 systemd-pcrphase.service systemd-pcrphase-sysinit.service systemd-pcrphase-initrd.service - /usr/lib/systemd/system-pcrphase STRING + systemd-pcrmachine.service + systemd-pcrfs-root.service + systemd-pcrfs@.service + /usr/lib/systemd/system-pcrphase STRING @@ -39,13 +45,23 @@ systemd-pcrphase-initrd.service are system services that measure specific strings into TPM2 PCR 11 during boot at various milestones of the boot process. + systemd-pcrmachine.service is a system service that measures the machine ID + (see machine-id5) into + PCR 15. + + 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 + the root file system, systemd-pcrfs@.service is a template unit that measures the + file system indicated by its instance identifier instead. + These services require systemd-stub7 to be used in a unified kernel image (UKI). They execute no operation 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 TPM2 PCR 11 with certain - literal strings indicating phases of the boot process. During a regular boot process the following - strings are used: + literal strings indicating phases of the boot process. During a regular boot process PCR 11 is extended + with the following strings: enter-initrd — early when the initrd initializes, before activating @@ -102,6 +118,14 @@ systemd-measure1 to pre-calculate expected PCR 11 values for specific boot phases (via the switch). + + systemd-pcrfs-root.service and systemd-pcrfs@.service are + automatically pulled into the initial transaction by + systemd-gpt-generator8 + for the root and /var/ file + systems. systemd-fstab-generator8 + will do this for all mounts with the mount option in + /etc/fstab. @@ -137,6 +161,21 @@ TPM2 device will cause the invocation to fail. + + + + Instead of measuring a word specified on the command line into PCR 11, measure the + host's machine ID into PCR 15. + + + + + + Instead of measuring a word specified on the command line into PCR 11, measure + identity information of the specified file system into PCR 15. The parameter must be the path to the + established mount point of the file system to measure. + + @@ -148,7 +187,9 @@ systemd1, systemd-stub7, - systemd-measure1 + systemd-measure1, + systemd-gpt-generator8, + systemd-fstab-generator8 diff --git a/man/systemd.mount.xml b/man/systemd.mount.xml index da6ade86c8..890128646d 100644 --- a/man/systemd.mount.xml +++ b/man/systemd.mount.xml @@ -366,6 +366,20 @@ Options= setting in a unit file. + + + + Measures file system identity information (mount point, type, label, UUID, partition + label, partition UUID) into PCR 15 after the file system has been mounted. This ensures the + systemd-pcrfs@.service8 + or systemd-pcrfs-root.service services are pulled in by the mount unit. + + Note that this option can only be used in /etc/fstab, and will be ignored + when part of the Options= setting in a unit file. It is also implied for the root + and /usr/ partitions dicovered by + systemd-gpt-auto-generator8. + + From f44ed151c6c203f01a9fe8623b282ecd4ef2e0a9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 16 Dec 2022 16:25:34 +0100 Subject: [PATCH 14/15] test: add simple integration test for checking PCR extension works as it should --- test/TEST-70-TPM2/test.sh | 1 + test/units/testsuite-70.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/test/TEST-70-TPM2/test.sh b/test/TEST-70-TPM2/test.sh index 7c19821ad2..f448a4a5f1 100755 --- a/test/TEST-70-TPM2/test.sh +++ b/test/TEST-70-TPM2/test.sh @@ -20,6 +20,7 @@ test_append_files() { install_dmevent generate_module_dependencies inst_binary tpm2_pcrextend + inst_binary tpm2_pcrread inst_binary openssl } diff --git a/test/units/testsuite-70.sh b/test/units/testsuite-70.sh index d64674461f..2ab8855ddc 100755 --- a/test/units/testsuite-70.sh +++ b/test/units/testsuite-70.sh @@ -172,6 +172,36 @@ else echo "/usr/lib/systemd/systemd-measure or PCR sysfs files not found, skipping signed PCR policy test case" fi +if [ -e /usr/lib/systemd/systemd-pcrphase ] && \ + [ -f /sys/class/tpm/tpm0/pcr-sha256/11 ]; then + + # Let's measure the machine ID + tpm2_pcrread sha256:15 -Q -o /tmp/oldpcr15 + mv /etc/machine-id /etc/machine-id.save + echo 994013bf23864ee7992eab39a96dd3bb >/etc/machine-id + SYSTEMD_FORCE_MEASURE=1 /usr/lib/systemd/systemd-pcrphase --machine-id + mv /etc/machine-id.save /etc/machine-id + tpm2_pcrread sha256:15 -Q -o /tmp/newpcr15 + + # And check it matches expectations + ( cat /tmp/oldpcr15 ; + echo -n "machine-id:994013bf23864ee7992eab39a96dd3bb" | openssl dgst -binary -sha256 ) | openssl dgst -binary -sha256 | cmp - /tmp/newpcr15 + + rm /tmp/oldpcr15 /tmp/newpcr15 + + # And similar for the boot phase measurement into PCR 11 + tpm2_pcrread sha256:11 -Q -o /tmp/oldpcr11 + SYSTEMD_FORCE_MEASURE=1 /usr/lib/systemd/systemd-pcrphase foobar + tpm2_pcrread sha256:11 -Q -o /tmp/newpcr11 + + ( cat /tmp/oldpcr11 ; + echo -n "foobar" | openssl dgst -binary -sha256 ) | openssl dgst -binary -sha256 | cmp - /tmp/newpcr11 + + rm /tmp/oldpcr11 /tmp/newpcr11 +else + echo "/usr/lib/systemd/systemd-pcrphase or PCR sysfs files not found, skipping PCR extension test case" +fi + echo OK >/testok exit 0 From a67a50e8f4a3d19713fe9b84653616fcba5ae14c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Oct 2022 21:21:46 +0200 Subject: [PATCH 15/15] update TODO --- TODO | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/TODO b/TODO index d95cbfaeb7..e78cfa1596 100644 --- a/TODO +++ b/TODO @@ -451,9 +451,8 @@ Features: and via the time window TPM logic invalidated if node doesn't keep itself updated, or becomes corrupted in some way. -* Always measure the LUKS rootfs volume key into PCR 15, and derive the machine - ID from it securely. This would then allow us to bind secrets a specific - system securely. +* in the initrd, once the rootfs encryption key has been measured to PCR 15, + derive default machine ID to use from it, and pass it to host PID 1. * tree-wide: convert as much as possible over to use sd_event_set_signal_exit(), instead of manually hooking into SIGINT/SIGTERM