diff --git a/docs/TPM2_PCR_MEASUREMENTS.md b/docs/TPM2_PCR_MEASUREMENTS.md index 571a4c1077..5d0801a907 100644 --- a/docs/TPM2_PCR_MEASUREMENTS.md +++ b/docs/TPM2_PCR_MEASUREMENTS.md @@ -58,7 +58,22 @@ away there's a naming concept, so that nvindexes are referenced by name string rather than number. NvPCRs are defined in little JSON snippets in `/usr/lib/nvpcr/*.nvpcr`, that -match up index number and name, as well as pick a hash algorithm. +match up index number and name, as well as pick a hash algorithm. The recognized +fields are: + +* `name` — the NvPCR name (string), which must match the file name (without the + `.nvpcr` suffix). Mandatory. +* `nvIndex` — the fixed TPM2 NV index handle (number) to allocate for this NvPCR. + Mandatory. +* `algorithm` — the hash algorithm to use (string), e.g. `sha256` (the default). +* `priority` — an unsigned integer allocation priority, defaulting to `1000`. + Lower values are considered more important and are allocated first. This only + affects the order in which `systemd-tpm2-setup.service` attempts allocation at + boot: if the TPM's NV index space is too small to fit all NvPCRs, the most + important ones (lowest `priority` value) win the available space, and the + least important ones are skipped gracefully rather than the allocation failing + arbitrarily. Ties are broken by name. Priority does not affect the NV index, + the algorithm, or anything measured into the NvPCR. There's one complication: these NV indexes (like any NV indexes) can be deleted by anyone with access to the TPM, and then be recreated. This could be used to diff --git a/src/analyze/analyze-nvpcrs.c b/src/analyze/analyze-nvpcrs.c index 56b5c9a204..dde31be073 100644 --- a/src/analyze/analyze-nvpcrs.c +++ b/src/analyze/analyze-nvpcrs.c @@ -16,6 +16,7 @@ static int add_nvpcr_to_table(Tpm2Context **c, Table *t, const char *name) { _cleanup_free_ char *h = NULL; uint32_t nv_index = 0; + uint64_t priority = 0; if (c) { if (!*c) { r = tpm2_context_new_or_warn(/* device= */ NULL, c); @@ -24,7 +25,7 @@ static int add_nvpcr_to_table(Tpm2Context **c, Table *t, const char *name) { } _cleanup_(iovec_done) struct iovec digest = {}; - r = tpm2_nvpcr_read(*c, /* session= */ NULL, name, &digest, &nv_index); + r = tpm2_nvpcr_read(*c, /* session= */ NULL, name, &digest, &nv_index, &priority); if (r < 0) return log_error_errno(r, "Failed to read NvPCR '%s': %m", name); if (r > 0) { /* set? */ @@ -33,7 +34,7 @@ static int add_nvpcr_to_table(Tpm2Context **c, Table *t, const char *name) { return log_oom(); } } else { - r = tpm2_nvpcr_get_index(name, &nv_index); + r = tpm2_nvpcr_get_index(name, &nv_index, &priority); if (r < 0) return log_error_errno(r, "Failed to get NV index of NvPCR '%s': %m", name); } @@ -42,6 +43,7 @@ static int add_nvpcr_to_table(Tpm2Context **c, Table *t, const char *name) { t, TABLE_STRING, name, TABLE_UINT32_HEX_0x, nv_index, + TABLE_UINT64, priority, TABLE_STRING, h); if (r < 0) return table_log_add_error(r); @@ -61,16 +63,17 @@ int verb_nvpcrs(int argc, char *argv[], uintptr_t _data, void *userdata) { if (!have_tpm2) log_notice("System lacks full TPM2 support, not showing NvPCR state."); - table = table_new("name", "nvindex", "value"); + table = table_new("name", "nvindex", "priority", "value"); if (!table) return log_oom(); (void) table_set_align_percent(table, table_get_cell(table, 0, 1), 100); + (void) table_set_align_percent(table, table_get_cell(table, 0, 2), 100); table_set_ersatz_string(table, TABLE_ERSATZ_DASH); - (void) table_set_sort(table, (size_t) 0); + (void) table_set_sort(table, (size_t) 2, (size_t) 0); if (!have_tpm2) - (void) table_hide_column_from_display(table, (size_t) 2); + (void) table_hide_column_from_display(table, (size_t) 3); if (strv_isempty(strv_skip(argv, 1))) { _cleanup_strv_free_ char **l = NULL; diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 533cdd6306..26ce8d7246 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6991,6 +6991,7 @@ typedef struct NvPCRData { char *name; uint16_t algorithm; uint32_t nv_index; + uint64_t priority; } NvPCRData; static void nvpcr_data_done(NvPCRData *d) { @@ -7030,11 +7031,13 @@ static int nvpcr_data_load(const char *name, NvPCRData *ret) { { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(NvPCRData, name), SD_JSON_MANDATORY }, { "algorithm", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_tpm2_algorithm, offsetof(NvPCRData, algorithm), 0 }, { "nvIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(NvPCRData, nv_index), SD_JSON_MANDATORY }, + { "priority", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(NvPCRData, priority), 0 }, {}, }; _cleanup_(nvpcr_data_done) NvPCRData p = { .algorithm = TPM2_ALG_SHA256, + .priority = TPM2_NVPCR_PRIORITY_DEFAULT, }; r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) @@ -7047,7 +7050,7 @@ static int nvpcr_data_load(const char *name, NvPCRData *ret) { return 0; } -int tpm2_nvpcr_get_index(const char *name, uint32_t *ret) { +int tpm2_nvpcr_get_index(const char *name, uint32_t *ret_nv_index, uint64_t *ret_priority) { int r; _cleanup_(nvpcr_data_done) NvPCRData p = {}; @@ -7055,8 +7058,10 @@ int tpm2_nvpcr_get_index(const char *name, uint32_t *ret) { if (r < 0) return r; - if (ret) - *ret = p.nv_index; + if (ret_nv_index) + *ret_nv_index = p.nv_index; + if (ret_priority) + *ret_priority = p.priority; return 0; } @@ -7748,7 +7753,8 @@ int tpm2_nvpcr_read( const Tpm2Handle *session, const char *name, struct iovec *ret_value, - uint32_t *ret_nv_index) { + uint32_t *ret_nv_index, + uint64_t *ret_priority) { #if HAVE_OPENSSL int r; @@ -7775,6 +7781,8 @@ int tpm2_nvpcr_read( *ret_value = (struct iovec) {}; if (ret_nv_index) *ret_nv_index = p.nv_index; + if (ret_priority) + *ret_priority = p.priority; return 0; } @@ -7811,6 +7819,8 @@ int tpm2_nvpcr_read( if (ret_nv_index) *ret_nv_index = p.nv_index; + if (ret_priority) + *ret_priority = p.priority; return r; #else /* HAVE_OPENSSL */ diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index f2f63a94a4..bf2321a514 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -158,11 +158,16 @@ typedef enum Tpm2UserspaceEventType { DECLARE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType); int tpm2_pcr_extend_bytes(Tpm2Context *c, char **banks, unsigned pcr_index, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event_type, const char *description); -int tpm2_nvpcr_get_index(const char *name, uint32_t *ret); + +/* Default allocation priority for NvPCRs that do not specify one explicitly. Lower values are more + * important and are allocated first when the TPM's NV index space is constrained. */ +#define TPM2_NVPCR_PRIORITY_DEFAULT UINT64_C(1000) + +int tpm2_nvpcr_get_index(const char *name, uint32_t *ret_nv_index, uint64_t *ret_priority); int tpm2_nvpcr_extend_bytes(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event_type, const char *description); int tpm2_nvpcr_acquire_anchor_secret(struct iovec *ret, bool sync_secondary); int tpm2_nvpcr_initialize(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *anchor_secret); -int tpm2_nvpcr_read(Tpm2Context *c, const Tpm2Handle *session, const char *name, struct iovec *ret, uint32_t *ret_nv_index); +int tpm2_nvpcr_read(Tpm2Context *c, const Tpm2Handle *session, const char *name, struct iovec *ret, uint32_t *ret_nv_index, uint64_t *ret_priority); uint32_t tpm2_tpms_pcr_selection_to_mask(const TPMS_PCR_SELECTION *s); void tpm2_tpms_pcr_selection_from_mask(uint32_t mask, TPMI_ALG_HASH hash, TPMS_PCR_SELECTION *ret); diff --git a/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in b/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in index acec95206f..3f01ef34eb 100644 --- a/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in +++ b/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in @@ -1,5 +1,6 @@ { "name" : "cryptsetup", "algorithm" : "sha256", - "nvIndex" : {{TPM2_NVPCR_BASE + 1}} + "nvIndex" : {{TPM2_NVPCR_BASE + 1}}, + "priority" : 700 } diff --git a/src/tpm2-setup/nvpcr/hardware.nvpcr.in b/src/tpm2-setup/nvpcr/hardware.nvpcr.in index ddeb7f99e5..bb4a3afa69 100644 --- a/src/tpm2-setup/nvpcr/hardware.nvpcr.in +++ b/src/tpm2-setup/nvpcr/hardware.nvpcr.in @@ -1,5 +1,6 @@ { "name" : "hardware", "algorithm" : "sha256", - "nvIndex" : {{TPM2_NVPCR_BASE + 0}} + "nvIndex" : {{TPM2_NVPCR_BASE + 0}}, + "priority" : 500 } diff --git a/src/tpm2-setup/nvpcr/verity.nvpcr.in b/src/tpm2-setup/nvpcr/verity.nvpcr.in index b4fb62bd76..7d1630d6cd 100644 --- a/src/tpm2-setup/nvpcr/verity.nvpcr.in +++ b/src/tpm2-setup/nvpcr/verity.nvpcr.in @@ -1,5 +1,6 @@ { "name" : "verity", "algorithm" : "sha256", - "nvIndex" : {{TPM2_NVPCR_BASE + 2}} + "nvIndex" : {{TPM2_NVPCR_BASE + 2}}, + "priority" : 300 } diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index bb08e31a81..eb25d7fd7f 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -23,6 +23,7 @@ #include "parse-util.h" #include "pretty-print.h" #include "set.h" +#include "sort-util.h" #include "string-util.h" #include "strv.h" #include "tmpfile-util.h" @@ -369,7 +370,8 @@ static int setup_srk(void) { typedef struct SetupNvPCRContext { Tpm2Context *tpm2_context; struct iovec anchor_secret; - size_t n_already, n_anchored, n_failed; + size_t n_already, n_anchored, n_failed, n_skipped; + bool nv_space_exhausted; /* Set once the TPM ran out of NV index space, so we skip the rest. */ Set *done; } SetupNvPCRContext; @@ -393,6 +395,15 @@ static int setup_nvpcr_one( if (set_contains(c->done, name)) return 0; + /* If the TPM already ran out of NV index space, don't even try to allocate further NvPCRs. As + * NvPCRs are processed in order of priority (most important first), everything still pending is + * less important than what already didn't fit. */ + if (c->nv_space_exhausted) { + log_debug("TPM NV index space already exhausted, skipping allocation of NvPCR '%s'.", name); + c->n_skipped++; + return 0; + } + r = tpm2_nvpcr_initialize(c->tpm2_context, /* session= */ NULL, name, &c->anchor_secret); if (r == -EUNATCH) { assert(!iovec_is_set(&c->anchor_secret)); @@ -413,9 +424,13 @@ static int setup_nvpcr_one( LOG_MESSAGE_ID(SD_MESSAGE_TPM_NVPCR_UNSUPPORTED_STR)); } if (r == -ENOSPC) { - c->n_failed++; - return log_struct_errno(LOG_ERR, r, - LOG_MESSAGE("The TPM's NV index space is exhausted, unable to allocate NvPCR '%s': %m", name), + /* The TPM's NV index space is exhausted. Remember this so we skip the remaining (less + * important) NvPCRs, and report it gracefully at the end rather than failing the boot. + * Logged at notice level, not error. */ + c->nv_space_exhausted = true; + c->n_skipped++; + return log_struct_errno(LOG_NOTICE, r, + LOG_MESSAGE("The TPM's NV index space is exhausted, skipping allocation of NvPCR '%s' and any less important ones: %m", name), LOG_MESSAGE_ID(SD_MESSAGE_TPM_NVINDEX_EXHAUSTED_STR)); } if (r < 0) { @@ -434,6 +449,26 @@ static int setup_nvpcr_one( return 0; } +typedef struct NvPCREntry { + const char *name; /* points into the strv 'l' in setup_nvpcr() */ + uint64_t priority; +} NvPCREntry; + +static int nvpcr_entry_compare(const NvPCREntry *a, const NvPCREntry *b) { + int r; + + assert(a); + assert(b); + + /* Lower priority value means more important, hence allocate it first. Break ties by name + * (ascending) so the resulting order is fully deterministic. */ + r = CMP(a->priority, b->priority); + if (r != 0) + return r; + + return strcmp(a->name, b->name); +} + static int setup_nvpcr(void) { _cleanup_(setup_nvpcr_context_done) SetupNvPCRContext c = {}; int r; @@ -448,8 +483,33 @@ static int setup_nvpcr(void) { if (r < 0) return log_error_errno(r, "Failed to find .nvpcr files: %m"); + /* Pair each NvPCR name with its allocation priority, then sort, so that we allocate the most + * important NvPCRs first. This matters when the TPM's NV index space is too small to fit all of + * them: we then degrade gracefully, skipping the least important NvPCRs. */ + size_t n = strv_length(l); + _cleanup_free_ NvPCREntry *entries = new(NvPCREntry, n); + if (!entries) + return log_oom(); + + for (size_t i = 0; i < n; i++) { + uint64_t priority = TPM2_NVPCR_PRIORITY_DEFAULT; + + r = tpm2_nvpcr_get_index(l[i], /* ret_nv_index= */ NULL, &priority); + if (r < 0) + /* If we can't read the definition, assume the default priority and let the + * per-NvPCR error path in setup_nvpcr_one() report the actual problem. */ + log_warning_errno(r, "Failed to read priority of NvPCR '%s', assuming default priority %" PRIu64 ": %m", l[i], priority); + + entries[i] = (NvPCREntry) { + .name = l[i], + .priority = priority, + }; + } + + typesafe_qsort(entries, n, nvpcr_entry_compare); + int ret = 0; - STRV_FOREACH(i, l) { + FOREACH_ARRAY(e, entries, n) { if (!c.tpm2_context) { /* Inability to contact the TPM shall be fatal for us */ r = tpm2_context_new_or_warn(arg_tpm2_device, &c.tpm2_context); @@ -457,8 +517,10 @@ static int setup_nvpcr(void) { return r; } + log_debug("Setting up NvPCR '%s' (priority %" PRIu64 ").", e->name, e->priority); + /* But if we fail to initialize some NvPCR, we go on */ - RET_GATHER(ret, setup_nvpcr_one(&c, *i)); + RET_GATHER(ret, setup_nvpcr_one(&c, e->name)); } if (c.n_already > 0 && c.n_anchored == 0 && !arg_early) @@ -467,6 +529,9 @@ static int setup_nvpcr(void) { * yet. Hence, let's explicitly do so now, to catch up. */ RET_GATHER(ret, tpm2_nvpcr_acquire_anchor_secret(/* ret= */ NULL, /* sync_secondary= */ true)); + if (c.nv_space_exhausted) + log_notice("Skipped %zu lowest-priority NvPCR(s) because the TPM's NV index space is exhausted, proceeding anyway.", c.n_skipped); + if (c.n_failed > 0) log_warning("%zu NvPCRs failed to initialize, proceeding anyway.", c.n_failed); @@ -477,7 +542,7 @@ static int setup_nvpcr(void) { log_info("%zu NvPCRs initialized. (%zu NvPCRs were already initialized.)", c.n_anchored, c.n_already); } else if (c.n_already > 0) log_info("%zu NvPCRs already initialized.", c.n_already); - else if (c.n_failed == 0) + else if (c.n_failed == 0 && c.n_skipped == 0) log_debug("No NvPCRs defined, nothing initialized."); /* Turn some errors into recognizable ones, which we can catch with diff --git a/test/units/TEST-70-TPM2.nvpcr.sh b/test/units/TEST-70-TPM2.nvpcr.sh index 05ae378d84..d84bb1feb3 100755 --- a/test/units/TEST-70-TPM2.nvpcr.sh +++ b/test/units/TEST-70-TPM2.nvpcr.sh @@ -21,7 +21,8 @@ at_exit() { fi rm -rf /run/nvpcr /tmp/nvpcr - rm -f /var/tmp/nvpcr.raw /run/verity.d/test-70-nvpcr.crt /run/systemd/nvpcr/test.anchor + rm -f /var/tmp/nvpcr.raw /run/verity.d/test-70-nvpcr.crt + rm -f /run/systemd/nvpcr/test.anchor /run/systemd/nvpcr/test2.anchor /run/systemd/nvpcr/aaa.anchor /run/systemd/nvpcr/zzz.anchor } trap at_exit EXIT @@ -54,6 +55,33 @@ DIGEST_MEASURED2="$(echo -n "schnurz" | openssl dgst -sha256 -hex -r | cut -d' ' DIGEST_EXPECTED2="$(echo "$DIGEST_EXPECTED$DIGEST_MEASURED2" | tr '[:lower:]' '[:upper:]' | basenc --base16 -d | openssl dgst -sha256 -hex -r | cut -d' ' -f1)" test "$DIGEST_ACTUAL2" = "$DIGEST_EXPECTED2" +# Verify the 'priority' field round-trips through the JSON definition. The 'test' NvPCR above sets no +# priority, so it must report the default (1000). +PRIORITY_DEFAULT="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .priority')" +test "$PRIORITY_DEFAULT" = "1000" + +# A definition with an explicit priority must report exactly that value. +cat >/run/nvpcr/test2.nvpcr </run/nvpcr/aaa.nvpcr </run/nvpcr/zzz.nvpcr <