diff --git a/TODO b/TODO index 7d2c88bc45..c254d15f95 100644 --- a/TODO +++ b/TODO @@ -251,19 +251,6 @@ Features: kernel. So far we only did this for the various --image= switches, but not for the root fs or /usr/. -* extend systemd-measure with an --append= mode when signing expected PCR - measurements. In this mode the tool should read an existing signature JSON - object (which primarily contains an array with the actual signature data), - and then append the new signature to it instead of writing out an entirely - JSON object. Usecase: it might make sense to to sign a UKI's expected PCRs - with different keys for different boot phases. i.e. use keypair X for signing - the expected PCR in the initrd boot phase and keypair Y for signing the - expected PCR in the main boot phase. Via the --append logic we could merge - these signatures into one object, and then include the result in the UKI. - Then, if you bind a LUKS volume to public key X it really only can be - unlocked during early boot, and you bind a LUKS volume to public key Y it - really only can be unlocked during later boot, and so on. - * dissection policy should enforce that unlocking can only take place by certain means, i.e. only via pw, only via tpm2, or only via fido, or a combination thereof. diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml index f3b2834b2e..998ae33d99 100644 --- a/man/systemd-measure.xml +++ b/man/systemd-measure.xml @@ -182,6 +182,19 @@ systemd-pcrphase.service8. + + PATH + + When generating a PCR JSON signature (via the sign command), + combine it with a previously generated PCR JSON signature, and output it as one. The specified path + must refer to a regular file that contains a valid JSON PCR signature object. The specified file is + not modified. It will be read first, then the newly generated signature appended to it, and the + resulting object is written to standard output. Use this to generate a single JSON object consisting + from signatures made with a number of signing keys (for example, to have one key per boot phase). The + command will suppress duplicates: if a specific signature is already included in a JSON signature + object it is not added a second time. + + diff --git a/src/boot/measure.c b/src/boot/measure.c index 913cf18ee6..4ee5b1de3b 100644 --- a/src/boot/measure.c +++ b/src/boot/measure.c @@ -33,12 +33,14 @@ static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORM static PagerFlags arg_pager_flags = 0; static bool arg_current = false; static char **arg_phase = NULL; +static char *arg_append = 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_DESTRUCTOR_REGISTER(arg_append, freep); static inline void free_sections(char*(*sections)[_UNIFIED_SECTION_MAX]) { for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++) @@ -73,6 +75,7 @@ static int help(int argc, char *argv[], void *userdata) { " --public-key=KEY Public key (PEM) to validate against\n" " --json=MODE Output as JSON\n" " -j Same as --json=pretty on tty, --json=short otherwise\n" + " --append=PATH Load specified JSON signature, and append new signature to it\n" "\n%3$sUKI PE Section Options:%4$s %3$sUKI PE Section%4$s\n" " --linux=PATH Path to Linux kernel image file %7$s .linux\n" " --osrel=PATH Path to os-release file %7$s .osrel\n" @@ -128,6 +131,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_TPM2_DEVICE, ARG_JSON, ARG_PHASE, + ARG_APPEND, }; static const struct option options[] = { @@ -148,6 +152,7 @@ static int parse_argv(int argc, char *argv[]) { { "public-key", required_argument, NULL, ARG_PUBLIC_KEY }, { "json", required_argument, NULL, ARG_JSON }, { "phase", required_argument, NULL, ARG_PHASE }, + { "append", required_argument, NULL, ARG_APPEND }, {} }; @@ -254,6 +259,13 @@ static int parse_argv(int argc, char *argv[]) { break; } + case ARG_APPEND: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_append); + if (r < 0) + return r; + + break; + case '?': return -EINVAL; @@ -623,6 +635,8 @@ static int verb_calculate(int argc, char *argv[], void *userdata) { if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing."); + if (arg_append) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --append= switch is only supported for 'sign', not 'calculate'."); assert(!strv_isempty(arg_banks)); assert(!strv_isempty(arg_phase)); @@ -728,6 +742,15 @@ static int verb_sign(int argc, char *argv[], void *userdata) { assert(!strv_isempty(arg_banks)); assert(!strv_isempty(arg_phase)); + if (arg_append) { + r = json_parse_file(NULL, arg_append, 0, &v, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse '%s': %m", arg_append); + + if (!json_variant_is_object(v)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File '%s' is not a valid JSON object, refusing.", arg_append); + } + /* When signing we only support JSON output */ arg_json_format_flags &= ~JSON_FORMAT_OFF; @@ -936,7 +959,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { _cleanup_(json_variant_unrefp) JsonVariant *av = NULL; av = json_variant_ref(json_variant_by_key(v, p->bank)); - r = json_variant_append_array(&av, bv); + r = json_variant_append_array_nodup(&av, bv); if (r < 0) { log_error_errno(r, "Failed to append JSON object: %m"); goto finish; diff --git a/src/shared/json.c b/src/shared/json.c index b1ef0ed349..fd54835efa 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -2093,7 +2093,6 @@ int json_variant_append_array(JsonVariant **v, JsonVariant *element) { assert(v); assert(element); - if (!*v || json_variant_is_null(*v)) blank = true; else if (json_variant_is_array(*v)) @@ -2151,6 +2150,27 @@ int json_variant_append_array(JsonVariant **v, JsonVariant *element) { return 0; } +JsonVariant *json_variant_find(JsonVariant *haystack, JsonVariant *needle) { + JsonVariant *i; + + /* Find a json object in an array. Returns NULL if not found, or if the array is not actually an array. */ + + JSON_VARIANT_ARRAY_FOREACH(i, haystack) + if (json_variant_equal(i, needle)) + return i; + + return NULL; +} + +int json_variant_append_array_nodup(JsonVariant **v, JsonVariant *element) { + assert(v); + + if (json_variant_find(*v, element)) + return 0; + + return json_variant_append_array(v, element); +} + int json_variant_strv(JsonVariant *v, char ***ret) { char **l = NULL; bool sensitive; diff --git a/src/shared/json.h b/src/shared/json.h index 8d060e7877..5d79472351 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -210,7 +210,10 @@ int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uint64_t int json_variant_set_field_boolean(JsonVariant **v, const char *field, bool b); int json_variant_set_field_strv(JsonVariant **v, const char *field, char **l); +JsonVariant *json_variant_find(JsonVariant *haystack, JsonVariant *needle); + int json_variant_append_array(JsonVariant **v, JsonVariant *element); +int json_variant_append_array_nodup(JsonVariant **v, JsonVariant *element); int json_variant_merge(JsonVariant **v, JsonVariant *m); diff --git a/src/test/test-json.c b/src/test/test-json.c index 7ff9c560dd..0f5c5b1a6e 100644 --- a/src/test/test-json.c +++ b/src/test/test-json.c @@ -726,4 +726,29 @@ TEST(json_array_append_without_source) { json_array_append_with_source_one(false); } +TEST(json_array_append_nodup) { + _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *s = NULL, *wd = NULL, *nd = NULL; + + assert_se(json_build(&l, JSON_BUILD_STRV(STRV_MAKE("foo", "bar", "baz", "bar", "baz", "foo", "qux", "baz"))) >= 0); + assert_se(json_build(&s, JSON_BUILD_STRV(STRV_MAKE("foo", "bar", "baz", "qux"))) >= 0); + + assert_se(!json_variant_equal(l, s)); + assert_se(json_variant_elements(l) == 8); + assert_se(json_variant_elements(s) == 4); + + JsonVariant *i; + JSON_VARIANT_ARRAY_FOREACH(i, l) { + assert_se(json_variant_append_array(&wd, i) >= 0); + assert_se(json_variant_append_array_nodup(&nd, i) >= 0); + } + + assert_se(json_variant_elements(wd) == 8); + assert_se(json_variant_equal(l, wd)); + assert_se(!json_variant_equal(s, wd)); + + assert_se(json_variant_elements(nd) == 4); + assert_se(!json_variant_equal(l, nd)); + assert_se(json_variant_equal(s, nd)); +} + DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/test/units/testsuite-70.sh b/test/units/testsuite-70.sh index 89cd2a3f82..84c366036b 100755 --- a/test/units/testsuite-70.sh +++ b/test/units/testsuite-70.sh @@ -150,6 +150,23 @@ 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.sig3",headless=1 /usr/lib/systemd/systemd-cryptsetup detach test-volume2 + # Test --append mode and de-duplication. With the same parameters signing should not add a new entry + /usr/lib/systemd/systemd-measure sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: --append="/tmp/pcrsign.sig3" > "/tmp/pcrsign.sig4" + cmp "/tmp/pcrsign.sig3" "/tmp/pcrsign.sig4" + + # Sign one more phase, this should + /usr/lib/systemd/systemd-measure sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=quux:waldo --append="/tmp/pcrsign.sig4" > "/tmp/pcrsign.sig5" + ( ! cmp "/tmp/pcrsign.sig4" "/tmp/pcrsign.sig5" ) + + # Should still be good to unlock, given the old entry still exists + SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE=0 /usr/lib/systemd/systemd-cryptsetup attach test-volume2 $img - tpm2-device=auto,tpm2-signature="/tmp/pcrsign.sig5",headless=1 + /usr/lib/systemd/systemd-cryptsetup detach test-volume2 + + # Adding both signatures once more shoud not change anything, due to the deduplication + /usr/lib/systemd/systemd-measure sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: --append="/tmp/pcrsign.sig5" > "/tmp/pcrsign.sig6" + /usr/lib/systemd/systemd-measure sign --current "${MEASURE_BANKS[@]}" --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=quux:waldo --append="/tmp/pcrsign.sig6" > "/tmp/pcrsign.sig7" + cmp "/tmp/pcrsign.sig5" "/tmp/pcrsign.sig7" + rm $img else echo "/usr/lib/systemd/systemd-measure or PCR sysfs files not found, skipping signed PCR policy test case"