Merge pull request #25224 from poettering/measure-append

add --append= switch to systemd-measure
This commit is contained in:
Yu Watanabe 2022-12-15 21:47:29 +09:00 committed by GitHub
commit cd07f6e8e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 103 additions and 15 deletions

13
TODO
View file

@ -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.

View file

@ -182,6 +182,19 @@
<citerefentry><refentrytitle>systemd-pcrphase.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--append=</option><replaceable>PATH</replaceable></term>
<listitem><para>When generating a PCR JSON signature (via the <command>sign</command> 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.</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

@ -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;

View file

@ -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;

View file

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

View file

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

View file

@ -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"