measure: allow pre-calculating PCR values for multiple boot phases

This commit is contained in:
Lennart Poettering 2022-09-17 15:22:54 +02:00
parent 40f1856791
commit 6ca0016398
3 changed files with 233 additions and 28 deletions

View file

@ -156,6 +156,28 @@
all suitable TPM2 devices currently discovered.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--phase=</option><replaceable>PHASE</replaceable></term>
<listitem><para>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
<filename>systemd-pcrphase-initrd.service</filename> and
<citerefentry><refentrytitle>systemd-pcrphase.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
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
<literal>enter-initrd</literal>, <literal>enter-initrd:leave-initrd</literal>,
<literal>enter-initrd:leave-initrd:ready</literal>, 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 <command>calculate</command> and
<command>sign</command>. 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.</para>
<para>For further details about PCR boot phases, see
<citerefentry><refentrytitle>systemd-pcrphase.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="json" />
<xi:include href="standard-options.xml" xpointer="no-pager" />
<xi:include href="standard-options.xml" xpointer="help" />

View file

@ -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)) {

View file

@ -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 <<EOF
{"sha1":[{"pcr":11,"hash":"5177e4ad69db92192c10e5f80402bf81bfec8a81"}],"sha256":[{"pcr":11,"hash":"37b48bd0b222394dbe3cceff2fca4660c4b0a90ae9369ec90b42f14489989c13"}],"sha384":[{"pcr":11,"hash":"5573f9b2caf55b1d0a6a701f890662d682af961899f0419cf1e2d5ea4a6a68c1f25bd4f5b8a0865eeee82af90f5cb087"}],"sha512":[{"pcr":11,"hash":"961305d7e9981d6606d1ce97b3a9a1f92610cac033e9c39064895f0e306abc1680463d55767bd98e751eae115bdef3675a9ee1d29ed37da7885b1db45bb2555b"}]}
EOF
/usr/lib/systemd/systemd-measure calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 --phase=: -j | diff -u - /tmp/result.json
/usr/lib/systemd/systemd-measure calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 -j | diff -u - /tmp/result.json
cat >/tmp/result <<EOF
11:sha1=6765ee305db063040c454d32697d922b3d4f232b
11:sha256=21c49c1242042649e09c156546fd7d425ccc3c67359f840507b30be4e0f6f699
11:sha384=08d0b003a134878eee552070d51d58abe942f457ca85704131dd36f73728e7327ca837594bc9d5ac7de818d02a3d5dd2
11:sha512=65120f6ebc04b156421c6f3d543b2fad545363d9ca61c514205459e9c0e0b22e09c23605eae5853e38458ef3ca54e087168af8d8a882a98d220d9391e48be6d0
EOF
/usr/lib/systemd/systemd-measure calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 --phase=foo | cmp - /tmp/result
cat >/tmp/result.json <<EOF
{"sha1":[{"phase":"foo","pcr":11,"hash":"6765ee305db063040c454d32697d922b3d4f232b"}],"sha256":[{"phase":"foo","pcr":11,"hash":"21c49c1242042649e09c156546fd7d425ccc3c67359f840507b30be4e0f6f699"}],"sha384":[{"phase":"foo","pcr":11,"hash":"08d0b003a134878eee552070d51d58abe942f457ca85704131dd36f73728e7327ca837594bc9d5ac7de818d02a3d5dd2"}],"sha512":[{"phase":"foo","pcr":11,"hash":"65120f6ebc04b156421c6f3d543b2fad545363d9ca61c514205459e9c0e0b22e09c23605eae5853e38458ef3ca54e087168af8d8a882a98d220d9391e48be6d0"}]}
EOF
/usr/lib/systemd/systemd-measure calculate --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha1 --bank=sha256 --bank=sha384 --bank=sha512 --phase=foo -j | diff -u - /tmp/result.json
rm /tmp/result /tmp/result.json
else
echo "/usr/lib/systemd/systemd-measure not found, skipping PCR policy test case"
fi
@ -89,7 +102,7 @@ if [ -e /usr/lib/systemd/systemd-measure ] && \
openssl rsa -pubout -in "/tmp/pcrsign-private.pem" -out "/tmp/pcrsign-public.pem"
# Sign current PCR state with it
/usr/lib/systemd/systemd-measure sign --current --bank=sha1 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" | tee "/tmp/pcrsign.sig"
/usr/lib/systemd/systemd-measure sign --current --bank=sha1 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: | tee "/tmp/pcrsign.sig"
dd if=/dev/urandom of=/tmp/pcrtestdata bs=1024 count=64
systemd-creds encrypt /tmp/pcrtestdata /tmp/pcrtestdata.encrypted --with-key=host+tpm2-with-public-key --tpm2-public-key="/tmp/pcrsign-public.pem"
systemd-creds decrypt /tmp/pcrtestdata.encrypted - --tpm2-signature="/tmp/pcrsign.sig" | cmp - /tmp/pcrtestdata
@ -99,7 +112,7 @@ if [ -e /usr/lib/systemd/systemd-measure ] && \
systemd-creds decrypt /tmp/pcrtestdata.encrypted - --tpm2-signature="/tmp/pcrsign.sig" > /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