From 6ca0016398d97c1f51644088708f9ce865692e01 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 17 Sep 2022 15:22:54 +0200 Subject: [PATCH] measure: allow pre-calculating PCR values for multiple boot phases --- man/systemd-measure.xml | 22 ++++ src/boot/measure.c | 214 +++++++++++++++++++++++++++++++++---- test/units/testsuite-70.sh | 25 +++-- 3 files changed, 233 insertions(+), 28 deletions(-) diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml index ab5c9787d64..4e37d000e20 100644 --- a/man/systemd-measure.xml +++ b/man/systemd-measure.xml @@ -156,6 +156,28 @@ all suitable TPM2 devices currently discovered. + + PHASE + + Controls which boot phase(s) to calculate expected PCR 11 values for. This takes a + series of colon-separated strings that encode boot "paths" for entering a specific phase of the boot + process. Each of the specified strings is measured by the + systemd-pcrphase-initrd.service and + systemd-pcrphase.service8 + into PCR 11 during different milestones of the boot process. This switch may be specified multiple + times to calculate PCR values for multiple boot phases at once. If not used defaults to + enter-initrd, enter-initrd:leave-initrd, + enter-initrd:leave-initrd:ready, i.e. calculates expected PCR values for the boot + phase in the initrd, during early boot, and during system runtime, but excluding the phases before + the initrd or when shutting down. This setting is honoured both by calculate and + sign. When used with the latter it's particularly useful for generating PCR + signatures that can only be used for unlocking resources during specific parts of the boot + process. + + For further details about PCR boot phases, see + systemd-pcrphase.service8. + + diff --git a/src/boot/measure.c b/src/boot/measure.c index ec67e0cdb67..fd83cd8ba50 100644 --- a/src/boot/measure.c +++ b/src/boot/measure.c @@ -31,11 +31,13 @@ static char *arg_public_key = NULL; static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_OFF; static PagerFlags arg_pager_flags = 0; static bool arg_current = false; +static char **arg_phase = NULL; STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_public_key, freep); +STATIC_DESTRUCTOR_REGISTER(arg_phase, strv_freep); static inline void free_sections(char*(*sections)[_UNIFIED_SECTION_MAX]) { for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++) @@ -63,6 +65,7 @@ static int help(int argc, char *argv[], void *userdata) { " --version Print version\n" " --no-pager Do not pipe output into a pager\n" " -c --current Use current PCR values\n" + " --phase=PHASE Specify a boot phase to sign for\n" " --bank=DIGEST Select TPM bank (SHA1, SHA256)\n" " --tpm2-device=PATH Use specified TPM2 device\n" " --private-key=KEY Private key (PEM) to sign with\n" @@ -89,6 +92,21 @@ static int help(int argc, char *argv[], void *userdata) { return 0; } +static char *normalize_phase(const char *s) { + _cleanup_strv_free_ char **l = NULL; + + /* Let's normalize phase expressions. We split the series of colon-separated words up, then remove + * all empty ones, and glue them back together again. In other words we remove duplicate ":", as well + * as leading and trailing ones. */ + + l = strv_split(s, ":"); /* Split series of words */ + if (!l) + return NULL; + + /* Remove all empty words and glue things back together */ + return strv_join(strv_remove(l, ""), ":"); +} + static int parse_argv(int argc, char *argv[]) { enum { ARG_VERSION = 0x100, @@ -108,6 +126,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_PUBLIC_KEY, ARG_TPM2_DEVICE, ARG_JSON, + ARG_PHASE, }; static const struct option options[] = { @@ -127,6 +146,7 @@ static int parse_argv(int argc, char *argv[]) { { "private-key", required_argument, NULL, ARG_PRIVATE_KEY }, { "public-key", required_argument, NULL, ARG_PUBLIC_KEY }, { "json", required_argument, NULL, ARG_JSON }, + { "phase", required_argument, NULL, ARG_PHASE }, {} }; @@ -219,6 +239,20 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_PHASE: { + char *n; + + n = normalize_phase(optarg); + if (!n) + return log_oom(); + + r = strv_consume(&arg_phase, TAKE_PTR(n)); + if (r < 0) + return r; + + break; + } + case '?': return -EINVAL; @@ -241,14 +275,36 @@ static int parse_argv(int argc, char *argv[]) { if (arg_sections[us]) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --current switch cannot be used in combination with --linux= and related switches."); + if (strv_isempty(arg_phase)) { + /* If no phases are specifically selected, pick everything from the beginning of the initrd + * to the beginning of shutdown. */ + if (strv_extend_strv(&arg_phase, + STRV_MAKE("enter-initrd", + "enter-initrd:leave-initrd", + "enter-initrd:leave-initrd:ready"), + /* filter_duplicates= */ false) < 0) + return log_oom(); + } else { + strv_sort(arg_phase); + strv_uniq(arg_phase); + } + + _cleanup_free_ char *j = NULL; + j = strv_join(arg_phase, ", "); + if (!j) + return log_oom(); + + log_debug("Measuring boot phases: %s", j); return 1; } +/* The PCR 11 state for one specific bank */ typedef struct PcrState { char *bank; const EVP_MD *md; void *value; size_t value_size; + void *saved_value; /* A copy of the original value we calculated, used by pcr_states_save()/pcr_states_restore() to come later back to */ } PcrState; static void pcr_state_free_all(PcrState **pcr_state) { @@ -260,6 +316,7 @@ static void pcr_state_free_all(PcrState **pcr_state) { for (size_t i = 0; (*pcr_state)[i].value; i++) { free((*pcr_state)[i].bank); free((*pcr_state)[i].value); + free((*pcr_state)[i].saved_value); } *pcr_state = mfree(*pcr_state); @@ -320,6 +377,8 @@ static int measure_kernel(PcrState *pcr_states, size_t n) { assert(n > 0); assert(pcr_states); + /* Virtually measures the components of a unified kernel image into PCR 11 */ + if (arg_current) { /* Shortcut things, if we should just use the current PCR value */ @@ -432,6 +491,54 @@ static int measure_kernel(PcrState *pcr_states, size_t n) { return 0; } +static int measure_phase(PcrState *pcr_states, size_t n, const char *phase) { + _cleanup_strv_free_ char **l = NULL; + int r; + + assert(pcr_states); + assert(n > 0); + + /* Measure a phase string into PCR 11. This splits up the "phase" expression at colons, and then + * virtually extends each specified word into PCR 11, to model how during boot we measure a series of + * words into PCR 11, one for each phase. */ + + l = strv_split(phase, ":"); + if (!l) + return log_oom(); + + STRV_FOREACH(word, l) { + size_t wl; + + if (isempty(*word)) + continue; + + wl = strlen(*word); + + for (size_t i = 0; i < n; i++) { /* For each bank */ + _cleanup_free_ void *b = NULL; + int bsz; + + bsz = EVP_MD_size(pcr_states[i].md); + assert(bsz > 0); + + b = malloc(bsz); + if (!b) + return log_oom(); + + /* First hash the word itself */ + if (EVP_Digest(*word, wl, b, NULL, pcr_states[i].md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash word '%s'.", *word); + + /* And then extend the PCR with the resulting hash */ + r = pcr_state_extend(pcr_states + i, b, bsz); + if (r < 0) + return r; + } + } + + return 0; +} + static int pcr_states_allocate(PcrState **ret) { _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; size_t n = 0; @@ -473,6 +580,39 @@ static int pcr_states_allocate(PcrState **ret) { return (int) n; } +static int pcr_states_save(PcrState *pcr_states, size_t n) { + assert(pcr_states); + assert(n > 0); + + for (size_t i = 0; i < n; i++) { + _cleanup_free_ void *saved = NULL; + + if (!pcr_states[i].value) + continue; + + saved = memdup(pcr_states[i].value, pcr_states[i].value_size); + if (!saved) + return log_oom(); + + free_and_replace(pcr_states[i].saved_value, saved); + } + + return 0; +} + +static void pcr_states_restore(PcrState *pcr_states, size_t n) { + assert(pcr_states); + assert(n > 0); + + for (size_t i = 0; i < n; i++) { + + assert(pcr_states[i].value); + assert(pcr_states[i].saved_value); + + memcpy(pcr_states[i].value, pcr_states[i].saved_value, pcr_states[i].value_size); + } +} + static int verb_calculate(int argc, char *argv[], void *userdata) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; @@ -492,34 +632,64 @@ static int verb_calculate(int argc, char *argv[], void *userdata) { if (r < 0) return r; - for (size_t i = 0; i < n; i++) { - if (arg_json_format_flags & JSON_FORMAT_OFF) { - _cleanup_free_ char *hd = NULL; + /* Save the current state, so that we later can restore to it. This way we can measure the PCR values + * for multiple different boot phases without heaving to start from zero each time */ + r = pcr_states_save(pcr_states, n); + if (r < 0) + return r; - hd = hexmem(pcr_states[i].value, pcr_states[i].value_size); - if (!hd) - return log_oom(); + STRV_FOREACH(phase, arg_phase) { - printf("%" PRIu32 ":%s=%s\n", TPM_PCR_INDEX_KERNEL_IMAGE, pcr_states[i].bank, hd); - } else { - _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL; + r = measure_phase(pcr_states, n, *phase); + if (r < 0) + return r; - r = json_build(&bv, - JSON_BUILD_ARRAY( - JSON_BUILD_OBJECT( - JSON_BUILD_PAIR("pcr", JSON_BUILD_INTEGER(TPM_PCR_INDEX_KERNEL_IMAGE)), - JSON_BUILD_PAIR("hash", JSON_BUILD_HEX(pcr_states[i].value, pcr_states[i].value_size)) - ) - ) - ); - if (r < 0) - return log_error_errno(r, "Failed to build JSON object: %m"); + for (size_t i = 0; i < n; i++) { + if (arg_json_format_flags & JSON_FORMAT_OFF) { + _cleanup_free_ char *hd = NULL; - r = json_variant_set_field(&w, pcr_states[i].bank, bv); - if (r < 0) - return log_error_errno(r, "Failed to add bank info to object: %m"); + if (i == 0) { + fflush(stdout); + fprintf(stderr, "%s# PCR[%" PRIu32 "] Phase <%s>%s\n", + ansi_grey(), + TPM_PCR_INDEX_KERNEL_IMAGE, + isempty(*phase) ? ":" : *phase, + ansi_normal()); + fflush(stderr); + } + hd = hexmem(pcr_states[i].value, pcr_states[i].value_size); + if (!hd) + return log_oom(); + + printf("%" PRIu32 ":%s=%s\n", TPM_PCR_INDEX_KERNEL_IMAGE, pcr_states[i].bank, hd); + } else { + _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL, *array = NULL; + + array = json_variant_ref(json_variant_by_key(w, pcr_states[i].bank)); + + r = json_build(&bv, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(!isempty(*phase), "phase", JSON_BUILD_STRING(*phase)), + JSON_BUILD_PAIR("pcr", JSON_BUILD_INTEGER(TPM_PCR_INDEX_KERNEL_IMAGE)), + JSON_BUILD_PAIR("hash", JSON_BUILD_HEX(pcr_states[i].value, pcr_states[i].value_size)) + ) + ); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + r = json_variant_append_array(&array, bv); + if (r < 0) + return log_error_errno(r, "Failed to append JSON object to array: %m"); + + r = json_variant_set_field(&w, pcr_states[i].bank, array); + if (r < 0) + return log_error_errno(r, "Failed to add bank info to object: %m"); + } } + + /* Return to the original kernel measurement for the next phase calculation */ + pcr_states_restore(pcr_states, n); } if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { diff --git a/test/units/testsuite-70.sh b/test/units/testsuite-70.sh index e2219e89d35..97536c812cc 100755 --- a/test/units/testsuite-70.sh +++ b/test/units/testsuite-70.sh @@ -69,14 +69,27 @@ if [[ -e /usr/lib/systemd/systemd-measure ]]; then 11:sha384=5573f9b2caf55b1d0a6a701f890662d682af961899f0419cf1e2d5ea4a6a68c1f25bd4f5b8a0865eeee82af90f5cb087 11:sha512=961305d7e9981d6606d1ce97b3a9a1f92610cac033e9c39064895f0e306abc1680463d55767bd98e751eae115bdef3675a9ee1d29ed37da7885b1db45bb2555b EOF - - /usr/lib/systemd/systemd-measure calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 | cmp - /tmp/result + /usr/lib/systemd/systemd-measure calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 --phase=: | cmp - /tmp/result cat >/tmp/result.json </tmp/result </tmp/result.json < /dev/null && { echo 'unexpected success'; exit 1; } # Sign new PCR state, decrypting should work now. - /usr/lib/systemd/systemd-measure sign --current --bank=sha1 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" > "/tmp/pcrsign.sig2" + /usr/lib/systemd/systemd-measure sign --current --bank=sha1 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: > "/tmp/pcrsign.sig2" systemd-creds decrypt /tmp/pcrtestdata.encrypted - --tpm2-signature="/tmp/pcrsign.sig2" | cmp - /tmp/pcrtestdata # Now, do the same, but with a cryptsetup binding @@ -121,7 +134,7 @@ if [ -e /usr/lib/systemd/systemd-measure ] && \ SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=1 /usr/lib/systemd/systemd-cryptsetup attach test-volume2 $img - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig2",headless=1 && { echo 'unexpected success'; exit 1; } # But once we sign the current PCRs, we should be able to unlock again - /usr/lib/systemd/systemd-measure sign --current --bank=sha1 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" > "/tmp/pcrsign.sig3" + /usr/lib/systemd/systemd-measure sign --current --bank=sha1 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: > "/tmp/pcrsign.sig3" SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=0 /usr/lib/systemd/systemd-cryptsetup attach test-volume2 $img - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig3",headless=1 /usr/lib/systemd/systemd-cryptsetup detach test-volume2 SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=1 /usr/lib/systemd/systemd-cryptsetup attach test-volume2 $img - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig3",headless=1