From b570204a97bccfbfce8fc4ffa65306f8a06fe16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 15 Nov 2022 15:00:57 +0100 Subject: [PATCH 01/12] bootctl: make --json output normal json We would output a sequence of concatenated JSON strings. 'jq' accepts such output without fuss, and can even automatically build an array with --slurp/-s. Nevertheless, parsing this format is more effort for the reader, since it's not "standard JSON". E.g. Python's json module cannot do this out-of-the-box, but needs some loop with json.JSONDecoder.raw_decode() and then collecting the objects into an array. Such streaming output make sense in case of logs, where we stream the output and it has no predefined length. But here we expect at most a few dozen entries, so it's nicer to write normal JSON that is trivial to parse. I'm treating this is a bugfix and not attempting to provide compatibility backwards. I don't think the previous format was seeing much use, and it's trivial to adapt to the new one. --- src/shared/bootspec.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shared/bootspec.c b/src/shared/bootspec.c index 789b89ea93..435fa4c1bb 100644 --- a/src/shared/bootspec.c +++ b/src/shared/bootspec.c @@ -1402,6 +1402,8 @@ int show_boot_entries(const BootConfig *config, JsonFormatFlags json_format) { assert(config); if (!FLAGS_SET(json_format, JSON_FORMAT_OFF)) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + for (size_t i = 0; i < config->n_entries; i++) { _cleanup_free_ char *opts = NULL; const BootEntry *e = config->entries + i; @@ -1441,9 +1443,13 @@ int show_boot_entries(const BootConfig *config, JsonFormatFlags json_format) { if (r < 0) return log_oom(); - json_variant_dump(v, json_format, stdout, NULL); + r = json_variant_append_array(&array, v); + if (r < 0) + return log_oom(); } + json_variant_dump(array, json_format, NULL, NULL); + } else { for (size_t n = 0; n < config->n_entries; n++) { r = show_boot_entry( From 50f2c32cd5477bb42bf9a26e4aefd7230c2574c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 16 Nov 2022 09:49:30 +0100 Subject: [PATCH 02/12] tests: add a simple test that bootctl output is valid json python's json.tool module is used because it does validation. jq is more forgiving. Also, json is in the stdlib, so it should be always available. --- meson.build | 10 +++++++++- test/meson.build | 1 + test/test-bootctl-json.sh | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100755 test/test-bootctl-json.sh diff --git a/meson.build b/meson.build index 37d46dfae2..2fb5102971 100644 --- a/meson.build +++ b/meson.build @@ -2583,7 +2583,7 @@ if conf.get('HAVE_BLKID') == 1 and conf.get('HAVE_GNU_EFI') == 1 boot_link_with = [libsystemd_static, libshared_static] endif - public_programs += executable( + exe = executable( 'bootctl', 'src/boot/bootctl.c', include_directories : includes, @@ -2592,6 +2592,14 @@ if conf.get('HAVE_BLKID') == 1 and conf.get('HAVE_GNU_EFI') == 1 versiondep], install_rpath : rootpkglibdir, install : true) + public_programs += exe + + if want_tests != 'false' + test('test-bootctl-json', + test_bootctl_json_sh, + args : exe.full_path(), + depends : exe) + endif public_programs += executable( 'systemd-bless-boot', diff --git a/test/meson.build b/test/meson.build index f2080b5a9a..93f0e679fe 100644 --- a/test/meson.build +++ b/test/meson.build @@ -89,6 +89,7 @@ if install_tests install_dir : testdata_dir) endif +test_bootctl_json_sh = find_program('test-bootctl-json.sh') test_fstab_generator_sh = find_program('test-fstab-generator.sh') test_network_generator_conversion_sh = find_program('test-network-generator-conversion.sh') test_systemctl_enable_sh = find_program('test-systemctl-enable.sh') diff --git a/test/test-bootctl-json.sh b/test/test-bootctl-json.sh new file mode 100755 index 0000000000..7a660a8ea7 --- /dev/null +++ b/test/test-bootctl-json.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -e +set -o pipefail + +bootctl="${1:?}" + +"$bootctl" --no-pager list >/dev/null || { + echo "$bootctl list failed, skipping tests" 1>&2 + exit 77 +} + +set -x + +"$bootctl" list --json=pretty | python3 -m json.tool >/dev/null +"$bootctl" list --json=short | python3 -m json.tool >/dev/null + +command -v jq >/dev/null || { + echo "jq is not available, skipping jq tests" 1>&2 + exit 0 +} + +"$bootctl" list --json=pretty | jq . >/dev/null +"$bootctl" list --json=short | jq . >/dev/null From b0eeb945881b86f6740b84c055f0ad2be7a09ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 17 Nov 2022 14:15:48 +0100 Subject: [PATCH 03/12] shared/json: make it possible to specify source name for strings too, add tests The source would be set implicitly when parsing from a named file. But it's useful to specify the source also for cases where we're parsing a ready string. I noticed the lack of this API when trying to write tests, but it seems generally useful to be specify a source name when parsing things. --- src/shared/json.c | 58 ++++++++++++++++++++++++++++++---------- src/shared/json.h | 12 +++++++-- src/test/test-json.c | 63 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 16 deletions(-) diff --git a/src/shared/json.c b/src/shared/json.c index 94d7b31557..98bae0f194 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -3180,16 +3180,53 @@ finish: return r; } -int json_parse(const char *input, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { - return json_parse_internal(&input, NULL, flags, ret, ret_line, ret_column, false); +int json_parse_with_source( + const char *input, + const char *source, + JsonParseFlags flags, + JsonVariant **ret, + unsigned *ret_line, + unsigned *ret_column) { + + _cleanup_(json_source_unrefp) JsonSource *s = NULL; + + if (source) { + s = json_source_new(source); + if (!s) + return -ENOMEM; + } + + return json_parse_internal(&input, s, flags, ret, ret_line, ret_column, false); } -int json_parse_continue(const char **p, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { - return json_parse_internal(p, NULL, flags, ret, ret_line, ret_column, true); +int json_parse_with_source_continue( + const char **p, + const char *source, + JsonParseFlags flags, + JsonVariant **ret, + unsigned *ret_line, + unsigned *ret_column) { + + _cleanup_(json_source_unrefp) JsonSource *s = NULL; + + if (source) { + s = json_source_new(source); + if (!s) + return -ENOMEM; + } + + return json_parse_internal(p, s, flags, ret, ret_line, ret_column, true); } -int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { - _cleanup_(json_source_unrefp) JsonSource *source = NULL; +int json_parse_file_at( + FILE *f, + int dir_fd, + const char *path, + JsonParseFlags flags, + JsonVariant **ret, + unsigned *ret_line, + unsigned *ret_column) { + _cleanup_free_ char *text = NULL; int r; @@ -3205,14 +3242,7 @@ int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonParseFlags fla if (isempty(text)) return -ENODATA; - if (path) { - source = json_source_new(path); - if (!source) - return -ENOMEM; - } - - const char *p = text; - return json_parse_internal(&p, source, flags, ret, ret_line, ret_column, false); + return json_parse_with_source(text, path, flags, ret, ret_line, ret_column); } int json_buildv(JsonVariant **ret, va_list ap) { diff --git a/src/shared/json.h b/src/shared/json.h index c5f052a9d5..b1658b5f4b 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -222,8 +222,16 @@ typedef enum JsonParseFlags { JSON_PARSE_SENSITIVE = 1 << 0, /* mark variant as "sensitive", i.e. something containing secret key material or such */ } JsonParseFlags; -int json_parse(const char *string, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); -int json_parse_continue(const char **p, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); +int json_parse_with_source(const char *string, const char *source, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); +int json_parse_with_source_continue(const char **p, const char *source, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); + +static inline int json_parse(const char *string, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { + return json_parse_with_source(string, NULL, flags, ret, ret_line, ret_column); +} +static inline int json_parse_continue(const char **p, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { + return json_parse_with_source_continue(p, NULL, flags, ret, ret_line, ret_column); +} + int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); static inline int json_parse_file(FILE *f, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { diff --git a/src/test/test-json.c b/src/test/test-json.c index 17ad2017f8..7ff9c560dd 100644 --- a/src/test/test-json.c +++ b/src/test/test-json.c @@ -663,4 +663,67 @@ TEST(json_append) { assert_se(json_variant_equal(v, w)); } +static inline void json_array_append_with_source_one(bool source) { + _cleanup_(json_variant_unrefp) JsonVariant *a, *b; + + /* Parse two sources, each with a different name and line/column numbers */ + + assert_se(json_parse_with_source(" [41]", source ? "string 1" : NULL, 0, + &a, NULL, NULL) >= 0); + assert_se(json_parse_with_source("\n\n [42]", source ? "string 2" : NULL, 0, + &b, NULL, NULL) >= 0); + + assert_se(json_variant_is_array(a)); + assert_se(json_variant_elements(a) == 1); + assert_se(json_variant_is_array(b)); + assert_se(json_variant_elements(b) == 1); + + /* Verify source information */ + + const char *s1, *s2; + unsigned line1, col1, line2, col2; + assert_se(json_variant_get_source(a, &s1, &line1, &col1) >= 0); + assert_se(json_variant_get_source(b, &s2, &line2, &col2) >= 0); + + assert_se(streq_ptr(s1, source ? "string 1" : NULL)); + assert_se(streq_ptr(s2, source ? "string 2" : NULL)); + assert_se(line1 == 1); + assert_se(col1 == 2); + assert_se(line2 == 3); + assert_se(col2 == 4); + + /* Append one elem from the second array (and source) to the first. */ + + JsonVariant *elem; + assert_se(elem = json_variant_by_index(b, 0)); + assert_se(json_variant_is_integer(elem)); + assert_se(json_variant_elements(elem) == 0); + + assert_se(json_variant_append_array(&a, elem) >= 0); + + assert_se(json_variant_is_array(a)); + assert_se(json_variant_elements(a) == 2); + + /* Verify that source information was propagated correctly */ + + assert_se(json_variant_get_source(elem, &s1, &line1, &col1) >= 0); + assert_se(elem = json_variant_by_index(a, 1)); + assert_se(json_variant_get_source(elem, &s2, &line2, &col2) >= 0); + + assert_se(streq_ptr(s1, source ? "string 2" : NULL)); + assert_se(streq_ptr(s2, source ? "string 2" : NULL)); + assert_se(line1 == 3); + assert_se(col1 == 5); + assert_se(line2 == 3); + assert_se(col2 == 5); +} + +TEST(json_array_append_with_source) { + json_array_append_with_source_one(true); +} + +TEST(json_array_append_without_source) { + json_array_append_with_source_one(false); +} + DEFINE_TEST_MAIN(LOG_DEBUG); From 8525bb369a09f488ec77f94e1557ecc2343eb4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 17 Nov 2022 14:32:46 +0100 Subject: [PATCH 04/12] shared/json: optimize appending objects to arrays When repeatedly appending an object to a growing array, we would create a new array larger by one slot, insert all the old entries and the new element with ref count bumps into the new array, and then unref the old array. This would cause problems when building an array with more than a few thousand elements. If userdbctl is modified to construct an array, 'userdbctl --json=pretty group >/dev/null' with 31k groups: 0.74s (existing code) 102.17s (returning an array) 0.79s (with this patch) We append arrays in various places, so it seems nice to make this generally fast. --- src/shared/json.c | 112 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 33 deletions(-) diff --git a/src/shared/json.c b/src/shared/json.c index 98bae0f194..fe03f23046 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -553,9 +553,34 @@ static void json_variant_copy_source(JsonVariant *v, JsonVariant *from) { v->source = json_source_ref(from->source); } +static int _json_variant_array_put_element(JsonVariant *array, JsonVariant *element) { + assert(array); + JsonVariant *w = array + 1 + array->n_elements; + + uint16_t d = json_variant_depth(element); + if (d >= DEPTH_MAX) /* Refuse too deep nesting */ + return -ELNRNG; + if (d >= array->depth) + array->depth = d + 1; + array->n_elements ++; + + *w = (JsonVariant) { + .is_embedded = true, + .parent = array, + }; + + json_variant_set(w, element); + json_variant_copy_source(w, element); + + if (!json_variant_is_normalized(element)) + array->normalized = false; + + return 0; +} + int json_variant_new_array(JsonVariant **ret, JsonVariant **array, size_t n) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; - bool normalized = true; + int r; assert_return(ret, -EINVAL); if (n == 0) { @@ -571,33 +596,15 @@ int json_variant_new_array(JsonVariant **ret, JsonVariant **array, size_t n) { *v = (JsonVariant) { .n_ref = 1, .type = JSON_VARIANT_ARRAY, + .normalized = true, }; - for (v->n_elements = 0; v->n_elements < n; v->n_elements++) { - JsonVariant *w = v + 1 + v->n_elements, - *c = array[v->n_elements]; - uint16_t d; - - d = json_variant_depth(c); - if (d >= DEPTH_MAX) /* Refuse too deep nesting */ - return -ELNRNG; - if (d >= v->depth) - v->depth = d + 1; - - *w = (JsonVariant) { - .is_embedded = true, - .parent = v, - }; - - json_variant_set(w, c); - json_variant_copy_source(w, c); - - if (!json_variant_is_normalized(c)) - normalized = false; + while (v->n_elements < n) { + r = _json_variant_array_put_element(v, array[v->n_elements]); + if (r < 0) + return r; } - v->normalized = normalized; - *ret = TAKE_PTR(v); return 0; } @@ -823,6 +830,19 @@ static void json_variant_free_inner(JsonVariant *v, bool force_sensitive) { explicit_bzero_safe(v, json_variant_size(v)); } +static unsigned json_variant_n_ref(const JsonVariant *v) { + /* Return the number of references to v. + * 0 => NULL or not a regular object or embedded. + * >0 => number of references + */ + + if (!v || !json_variant_is_regular(v) || v->is_embedded) + return 0; + + assert(v->n_ref > 0); + return v->n_ref; +} + JsonVariant *json_variant_ref(JsonVariant *v) { if (!v) return NULL; @@ -2072,28 +2092,54 @@ int json_variant_append_array(JsonVariant **v, JsonVariant *element) { if (!*v || json_variant_is_null(*v)) blank = true; - else if (!json_variant_is_array(*v)) - return -EINVAL; - else + else if (json_variant_is_array(*v)) blank = json_variant_elements(*v) == 0; + else + return -EINVAL; - if (blank) + if (blank) { r = json_variant_new_array(&nv, (JsonVariant*[]) { element }, 1); - else { - _cleanup_free_ JsonVariant **array = new(JsonVariant*, json_variant_elements(*v) + 1); + if (r < 0) + return r; + } else if (json_variant_n_ref(*v) == 1) { + /* Let's bump the reference count on element. We can't do the realloc if we're appending *v + * to itself, or one of the objects embedded in *v to *v. If the reference count grows, we + * need to fall back to the other method below. */ + + _unused_ _cleanup_(json_variant_unrefp) JsonVariant *dummy = json_variant_ref(element); + if (json_variant_n_ref(*v) == 1) { + /* We hold the only reference. Let's mutate the object. */ + size_t size = json_variant_elements(*v); + void *old = *v; + + if (!GREEDY_REALLOC(*v, size + 1 + 1)) + return -ENOMEM; + + if (old != *v) + /* Readjust the parent pointers to the new address */ + for (size_t i = 1; i < size; i++) + (*v)[1 + i].parent = *v; + + return _json_variant_array_put_element(*v, element); + } + } + + if (!blank) { + size_t size = json_variant_elements(*v); + + _cleanup_free_ JsonVariant **array = new(JsonVariant*, size + 1); if (!array) return -ENOMEM; - size_t size = json_variant_elements(*v); for (size_t i = 0; i < size; i++) array[i] = json_variant_by_index(*v, i); array[size] = element; r = json_variant_new_array(&nv, array, size + 1); + if (r < 0) + return r; } - if (r < 0) - return r; json_variant_propagate_sensitive(*v, nv); JSON_VARIANT_REPLACE(*v, TAKE_PTR(nv)); From c3f0bff9456dcb79f5c929f3d9b15a1a884735df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 17 Nov 2022 15:40:54 +0100 Subject: [PATCH 05/12] bootctl: use output mode where "[]" is written instead for empty output It's easier for the caller if output is always a list, even if there are no entries. --- src/shared/bootspec.c | 2 +- src/shared/json.c | 8 ++++++-- src/shared/json.h | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/shared/bootspec.c b/src/shared/bootspec.c index 435fa4c1bb..8ae891e8a0 100644 --- a/src/shared/bootspec.c +++ b/src/shared/bootspec.c @@ -1448,7 +1448,7 @@ int show_boot_entries(const BootConfig *config, JsonFormatFlags json_format) { return log_oom(); } - json_variant_dump(array, json_format, NULL, NULL); + json_variant_dump(array, json_format | JSON_FORMAT_EMPTY_ARRAY, NULL, NULL); } else { for (size_t n = 0; n < config->n_entries; n++) { diff --git a/src/shared/json.c b/src/shared/json.c index fe03f23046..b1ef0ed349 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1810,8 +1810,12 @@ int json_variant_format(JsonVariant *v, JsonFormatFlags flags, char **ret) { } int json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const char *prefix) { - if (!v) - return 0; + if (!v) { + if (flags & JSON_FORMAT_EMPTY_ARRAY) + v = JSON_VARIANT_MAGIC_EMPTY_ARRAY; + else + return 0; + } if (!f) f = stdout; diff --git a/src/shared/json.h b/src/shared/json.h index b1658b5f4b..8d060e7877 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -194,7 +194,8 @@ typedef enum JsonFormatFlags { JSON_FORMAT_SSE = 1 << 6, /* prefix/suffix with W3C server-sent events */ JSON_FORMAT_SEQ = 1 << 7, /* prefix/suffix with RFC 7464 application/json-seq */ JSON_FORMAT_FLUSH = 1 << 8, /* call fflush() after dumping JSON */ - JSON_FORMAT_OFF = 1 << 9, /* make json_variant_format() fail with -ENOEXEC */ + JSON_FORMAT_EMPTY_ARRAY = 1 << 9, /* output "[]" for empty input */ + JSON_FORMAT_OFF = 1 << 10, /* make json_variant_format() fail with -ENOEXEC */ } JsonFormatFlags; int json_variant_format(JsonVariant *v, JsonFormatFlags flags, char **ret); From f4780cbe303c266c3b25a107eb0eb0296c6156da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 29 Oct 2022 20:07:46 +0200 Subject: [PATCH 06/12] ukify: add helper to create UKIs Features: - adds sections .linux, .initrd, .uname, .osrel, .pcrpkey, .pcrsig, .cmdline, .splash - multiple initrds can be concatenated - section flags are set properly (READONLY, DATA or CODE) - uses systemd-measure to precalculate pcr measurements and create a signed json policy - the inner linux image will be signed automatically with sbsign if unsigned - uses sbsign to sign the output image - offsets are calculated so that sections are placed adjacent, with .linux last - custom sections are possible - multiple pcr signing keys can be specified and different boot phase paths can be signed with different keys - most things can be overriden (path to tools, stub file, signing keys, pcr banks, boot phase paths, whether to sign things) - superficial verification of slash bmp is done - kernel uname "scraping" from the kernel if not specified (in a later patch) TODO: - change systemd-measure to not require a functional TPM2. W/o this, we'd need to support all banks in the build machine, which is hard to guarantee. - load signing keys from /etc/kernel/ - supress exceptions, so if something external fails, the user will not see a traceback - conversion to BMP from other formats $ sudo /usr/lib/systemd/ukify \ --tools=build/ \ --measure \ /lib/modules/6.0.5-300.fc37.x86_64/vmlinuz \ /boot/08a5690a2eed47cf92ac0a5d2e3cf6b0/6.0.5-300.fc37.x86_64/initrd \ --secureboot-private-key=server.key --secureboot-certificate=server.crt \ --pcr-private-key=tpm2-pcr-private.pem --pcr-public-key=tpm2-pcr-public.pem \ --cmdline='rw quiet' \ --section test:TESTTESTTEST \ --section test2:TESTTESTTEST2 \ --pcr-banks=sha1 \ --uname="$(uname -rv)" Host arch 'x86_64', efi arch 'x64' + sbverify --list /lib/modules/6.0.5-300.fc37.x86_64/vmlinuz + build/systemd-measure calculate --linux=/lib/modules/6.0.5-300.fc37.x86_64/vmlinuz --osrel=/etc/os-release --cmdline=/tmp/tmpcmdline_5aufjir --pcrpkey=tpm2-pcr-public.pem --initrd=/boot/08a5690a2eed47cf92ac0a5d2e3cf6b0/6.0.5-300.fc37.x86_64/initrd --bank=sha1 11:sha1=03df5e5243bc002b959d52359fe04e266d0b5ebf 11:sha1=54949b82bae32e80343ff0f01eeeeb75f4c07d3f 11:sha1=0fc62be88aa9c5ad7282aa8adb504f451bcec9df 11:sha1=b71155e7fcd467f7c1696f675e37887032e2eafa + build/systemd-measure sign --linux=/lib/modules/6.0.5-300.fc37.x86_64/vmlinuz --osrel=/etc/os-release --cmdline=/tmp/tmpcmdline_5aufjir --pcrpkey=tpm2-pcr-public.pem --initrd=/boot/08a5690a2eed47cf92ac0a5d2e3cf6b0/6.0.5-300.fc37.x86_64/initrd --bank=sha1 --private-key=tpm2-pcr-private.pem --public-key=tpm2-pcr-public.pem + objcopy /usr/lib/systemd/boot/efi/linuxx64.efi.stub --add-section .osrel=/etc/os-release --change-section-vma .osrel=0x22000 --add-section .cmdline=/tmp/tmpcmdline_5aufjir --change-section-vma .cmdline=0x23000 --add-section .pcrpkey=tpm2-pcr-public.pem --change-section-vma .pcrpkey=0x24000 --add-section .initrd=/boot/08a5690a2eed47cf92ac0a5d2e3cf6b0/6.0.5-300.fc37.x86_64/initrd --change-section-vma .initrd=0x25000 --add-section .uname=/tmp/tmpuname0v3uzh5r --change-section-vma .uname=0x4009000 --add-section .test=/tmp/tmptestuxve59c8 --change-section-vma .test=0x400a000 --add-section .test2=/tmp/tmptest2_i143p9i --change-section-vma .test2=0x400b000 --add-section .pcrsig=/tmp/tmppcrsigdtcqxz_w --change-section-vma .pcrsig=0x400c000 --add-section .linux=/lib/modules/6.0.5-300.fc37.x86_64/vmlinuz --change-section-vma .linux=0x400d000 /tmp/uki4vsbf7y8 + sbsign --key server.key --cert server.crt /tmp/uki4vsbf7y8 --output vmlinuz.efi warning: data remaining[79849520 vs 79866644]: gaps between PE/COFF sections? warning: data remaining[79849520 vs 79866648]: gaps between PE/COFF sections? Signing Unsigned original image Wrote signed vmlinuz.efi --- src/ukify/ukify.py | 576 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100755 src/ukify/ukify.py diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py new file mode 100755 index 0000000000..4041cefc9d --- /dev/null +++ b/src/ukify/ukify.py @@ -0,0 +1,576 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: LGPL-2.1+ + +# pylint: disable=missing-docstring,invalid-name,import-outside-toplevel +# pylint: disable=consider-using-with,unspecified-encoding,line-too-long +# pylint: disable=too-many-locals,too-many-statements,too-many-return-statements +# pylint: disable=too-many-branches + +import argparse +import collections +import dataclasses +import fnmatch +import itertools +import json +import os +import pathlib +import re +import shlex +import subprocess +import tempfile +import typing + +import pefile + +EFI_ARCH_MAP = { + # host_arch glob : [efi_arch, 32_bit_efi_arch if mixed mode is supported] + 'x86_64' : ['x64', 'ia32'], + 'i[3456]86' : ['ia32'], + 'aarch64' : ['aa64'], + 'arm[45678]*l' : ['arm'], + 'riscv64' : ['riscv64'], +} +EFI_ARCHES: list[str] = sum(EFI_ARCH_MAP.values(), []) + +def guess_efi_arch(): + arch = os.uname().machine + + for glob, mapping in EFI_ARCH_MAP.items(): + if fnmatch.fnmatch(arch, glob): + efi_arch, *fallback = mapping + break + else: + raise ValueError(f'Unsupported architecture {arch}') + + # This makes sense only on some architectures, but it also probably doesn't + # hurt on others, so let's just apply the check everywhere. + if fallback: + fw_platform_size = pathlib.Path('/sys/firmware/efi/fw_platform_size') + try: + size = fw_platform_size.read_text().strip() + except FileNotFoundError: + pass + else: + if int(size) == 32: + efi_arch = fallback[0] + + print(f'Host arch {arch!r}, EFI arch {efi_arch!r}') + return efi_arch + + +def shell_join(cmd): + # TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path. + return ' '.join(shlex.quote(str(x)) for x in cmd) + + +def pe_executable_size(filename): + pe = pefile.PE(filename) + section = pe.sections[-1] + return section.VirtualAddress + section.Misc_VirtualSize + + +def round_up(x, blocksize=4096): + return (x + blocksize - 1) // blocksize * blocksize + + +@dataclasses.dataclass +class Section: + name: str + content: pathlib.Path + tmpfile: typing.IO | None = None + flags: list[str] | None = dataclasses.field(default=None) + offset: int | None = None + measure: bool = False + + @classmethod + def create(cls, name, contents, flags=None, measure=False): + if isinstance(contents, str): + tmp = tempfile.NamedTemporaryFile(mode='wt', prefix=f'tmp{name}') + tmp.write(contents) + tmp.flush() + contents = pathlib.Path(tmp.name) + else: + tmp = None + + return cls(name, contents, tmpfile=tmp, flags=flags, measure=measure) + + @classmethod + def parse_arg(cls, s): + try: + name, contents, *rest = s.split(':') + except ValueError as e: + raise ValueError(f'Cannot parse section spec (name or contents missing): {s!r}') from e + if rest: + raise ValueError(f'Cannot parse section spec (extraneous parameters): {s!r}') + + if contents.startswith('@'): + contents = pathlib.Path(contents[1:]) + + return cls.create(name, contents) + + def size(self): + return self.content.stat().st_size + + def check_name(self): + # PE section names with more than 8 characters are legal, but our stub does + # not support them. + if not self.name.isascii() or not self.name.isprintable(): + raise ValueError(f'Bad section name: {self.name!r}') + if len(self.name) > 8: + raise ValueError(f'Section name too long: {self.name!r}') + + +@dataclasses.dataclass +class UKI: + executable: list[pathlib.Path|str] + sections: list[Section] = dataclasses.field(default_factory=list, init=False) + offset: int | None = dataclasses.field(default=None, init=False) + + def __post_init__(self): + self.offset = round_up(pe_executable_size(self.executable)) + + def add_section(self, section): + assert self.offset + assert section.offset is None + + if section.name in [s.name for s in self.sections]: + raise ValueError(f'Duplicate section {section.name}') + + section.offset = self.offset + self.offset += round_up(section.size()) + self.sections += [section] + + +def parse_banks(s): + banks = re.split(r',|\s+', s) + # TODO: do some sanity checking here + return banks + + +KNOWN_PHASES = ( + 'enter-initrd', + 'leave-initrd', + 'sysinit', + 'ready', + 'shutdown', + 'final', +) + +def parse_phase_paths(s): + # Split on commas or whitespace here. Commas might be hard to parse visually. + paths = re.split(r',|\s+', s) + + for path in paths: + for phase in path.split(':'): + if phase not in KNOWN_PHASES: + raise argparse.ArgumentTypeError(f'Unknown boot phase {phase!r} ({path=})') + + return paths + + +def check_splash(filename): + if filename is None: + return + + # import is delayed, to avoid import when the splash image is not used + try: + from PIL import Image + except ImportError: + return + + img = Image.open(filename, formats=['BMP']) + print(f'Splash image {filename} is {img.width}×{img.height} pixels') + + +def check_inputs(opts): + for name, value in vars(opts).items(): + if name in {'output', 'tools'}: + continue + + if not isinstance(value, pathlib.Path): + continue + + # Open file to check that we can read it, or generate an exception + value.open().close() + + check_splash(opts.splash) + + +def find_tool(name, fallback=None, opts=None): + if opts and opts.tools: + tool = opts.tools / name + if tool.exists(): + return tool + + return fallback or name + + +def combine_signatures(pcrsigs): + combined = collections.defaultdict(list) + for pcrsig in pcrsigs: + for bank, sigs in pcrsig.items(): + for sig in sigs: + if sig not in combined[bank]: + combined[bank] += [sig] + return json.dumps(combined) + + +def call_systemd_measure(uki, linux, opts): + measure_tool = find_tool('systemd-measure', + '/usr/lib/systemd/systemd-measure', + opts=opts) + + banks = opts.pcr_banks or () + + # PCR measurement + + if opts.measure: + pp_groups = opts.phase_path_groups or [] + + cmd = [ + measure_tool, + 'calculate', + f'--linux={linux}', + *(f"--{s.name.removeprefix('.')}={s.content}" + for s in uki.sections + if s.measure), + *(f'--bank={bank}' + for bank in banks), + # For measurement, the keys are not relevant, so we can lump all the phase paths + # into one call to systemd-measure calculate. + *(f'--phase={phase_path}' + for phase_path in itertools.chain.from_iterable(pp_groups)), + ] + + print('+', shell_join(cmd)) + subprocess.check_call(cmd) + + # PCR signing + + if opts.pcr_private_keys: + n_priv = len(opts.pcr_private_keys or ()) + pp_groups = opts.phase_path_groups or [None] * n_priv + pub_keys = opts.pcr_public_keys or [None] * n_priv + + pcrsigs = [] + + cmd = [ + measure_tool, + 'sign', + f'--linux={linux}', + *(f"--{s.name.removeprefix('.')}={s.content}" + for s in uki.sections + if s.measure), + *(f'--bank={bank}' + for bank in banks), + ] + + for priv_key, pub_key, group in zip(opts.pcr_private_keys, + pub_keys, + pp_groups): + extra = [f'--private-key={priv_key}'] + if pub_key: + extra += [f'--public-key={pub_key}'] + extra += [f'--phase={phase_path}' for phase_path in group or ()] + + print('+', shell_join(cmd + extra)) + pcrsig = subprocess.check_output(cmd + extra, text=True) + pcrsig = json.loads(pcrsig) + pcrsigs += [pcrsig] + + combined = combine_signatures(pcrsigs) + uki.add_section(Section.create('.pcrsig', combined)) + + +def make_uki(opts): + # kernel payload signing + + sbsign_tool = find_tool('sbsign', opts=opts) + sbsign_invocation = [ + sbsign_tool, + '--key', opts.sb_key, + '--cert', opts.sb_cert, + ] + + if opts.signing_engine is not None: + sbsign_invocation += ['--engine', opts.signing_engine] + + sign_kernel = opts.sign_kernel + if sign_kernel is None and opts.sb_key: + # figure out if we should sign the kernel + sbverify_tool = find_tool('sbverify', opts=opts) + + cmd = [ + sbverify_tool, + '--list', + opts.linux, + ] + + print('+', shell_join(cmd)) + info = subprocess.check_output(cmd, text=True) + + # sbverify has wonderful API + if 'No signature table present' in info: + sign_kernel = True + + if sign_kernel: + linux_signed = tempfile.NamedTemporaryFile(prefix='linux-signed') + linux = linux_signed.name + + cmd = [ + *sbsign_invocation, + opts.linux, + '--output', linux, + ] + + print('+', shell_join(cmd)) + subprocess.check_call(cmd) + else: + linux = opts.linux + + uki = UKI(opts.stub) + + # TODO: derive public key from from opts.pcr_private_keys? + pcrpkey = opts.pcrpkey + if pcrpkey is None: + if opts.pcr_public_keys and len(opts.pcr_public_keys) == 1: + pcrpkey = opts.pcr_public_keys[0] + + sections = [ + # name, content, measure? + ('.osrel', opts.os_release, True ), + ('.cmdline', opts.cmdline, True ), + ('.dtb', opts.devicetree, True ), + ('.splash', opts.splash, True ), + ('.pcrpkey', pcrpkey, True ), + ('.initrd', opts.initrd, True ), + ('.uname', opts.uname, False), + + # linux shall be last to leave breathing room for decompression. + # We'll add it later. + ] + + for name, content, measure in sections: + if content: + uki.add_section(Section.create(name, content, measure=measure)) + + # systemd-measure doesn't know about those extra sections + for section in opts.sections: + uki.add_section(section) + + # PCR measurement and signing + + call_systemd_measure(uki, linux, opts=opts) + + # UKI creation + + uki.add_section( + Section.create('.linux', linux, measure=True, + flags=['code', 'readonly'])) + + if opts.sb_key: + unsigned = tempfile.NamedTemporaryFile(prefix='uki') + output = unsigned.name + else: + output = opts.output + + objcopy_tool = find_tool('objcopy', opts=opts) + + cmd = [ + objcopy_tool, + opts.stub, + *itertools.chain.from_iterable( + ('--add-section', f'{s.name}={s.content}', + '--change-section-vma', f'{s.name}=0x{s.offset:x}') + for s in uki.sections), + *itertools.chain.from_iterable( + ('--set-section-flags', f"{s.name}={','.join(s.flags)}") + for s in uki.sections + if s.flags is not None), + output, + ] + print('+', shell_join(cmd)) + subprocess.check_call(cmd) + + # UKI signing + + if opts.sb_key: + cmd = [ + *sbsign_invocation, + unsigned.name, + '--output', opts.output, + ] + print('+', shell_join(cmd)) + subprocess.check_call(cmd) + + # We end up with no executable bits, let's reapply them + os.umask(umask := os.umask(0)) + os.chmod(opts.output, 0o777 & ~umask) + + print(f"Wrote {'signed' if opts.sb_key else 'unsigned'} {opts.output}") + + +def parse_args(args=None): + p = argparse.ArgumentParser( + description='Build and sign Unified Kernel Images', + allow_abbrev=False, + usage='''\ +usage: ukify [options…] linux initrd + ukify -h | --help +''') + + # Suppress printing of usage synopsis on errors + p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n') + + p.add_argument('linux', + type=pathlib.Path, + help='vmlinuz file [.linux section]') + p.add_argument('initrd', + type=pathlib.Path, + help='initrd file [.initrd section]') + + p.add_argument('--cmdline', + metavar='TEXT|@PATH', + help='kernel command line [.cmdline section]') + + p.add_argument('--os-release', + metavar='TEXT|@PATH', + help='path to os-release file [.osrel section]') + + p.add_argument('--devicetree', + metavar='PATH', + type=pathlib.Path, + help='Device Tree file [.dtb section]') + p.add_argument('--splash', + metavar='BMP', + type=pathlib.Path, + help='splash image bitmap file [.splash section]') + p.add_argument('--pcrpkey', + metavar='KEY', + type=pathlib.Path, + help='embedded public key to seal secrets to [.pcrpkey section]') + p.add_argument('--uname', + metavar='VERSION', + help='"uname -r" information [.uname section]') + + p.add_argument('--efi-arch', + metavar='ARCH', + choices=('ia32', 'x64', 'arm', 'aa64', 'riscv64'), + help='target EFI architecture') + + p.add_argument('--stub', + type=pathlib.Path, + help='path the the sd-stub file [.text,.data,… sections]') + + p.add_argument('--section', + dest='sections', + metavar='NAME:TEXT|@PATH', + type=Section.parse_arg, + action='append', + default=[], + help='additional section as name and contents [NAME section]') + + p.add_argument('--pcr-private-key', + dest='pcr_private_keys', + metavar='PATH', + type=pathlib.Path, + action='append', + help='private part of the keypair for signing PCR signatures') + p.add_argument('--pcr-public-key', + dest='pcr_public_keys', + metavar='PATH', + type=pathlib.Path, + action='append', + help='public part of the keypair for signing PCR signatures') + p.add_argument('--phases', + dest='phase_path_groups', + metavar='PHASE-PATH…', + type=parse_phase_paths, + action='append', + help='phase-paths to create signatures for') + + p.add_argument('--pcr-banks', + metavar='BANK…', + type=parse_banks) + + p.add_argument('--signing-engine', + metavar='ENGINE', + help='OpenSSL engine to use for signing') + p.add_argument('--secureboot-private-key', + dest='sb_key', + help='path to key file or engine-specific designation for SB signing') + p.add_argument('--secureboot-certificate', + dest='sb_cert', + help='path to certificate file or engine-specific designation for SB signing') + + p.add_argument('--sign-kernel', + action=argparse.BooleanOptionalAction, + help='Sign the embedded kernel') + + p.add_argument('--tools', + type=pathlib.Path, + help='a directory with systemd-measure and other tools') + + p.add_argument('--output', '-o', + type=pathlib.Path, + help='output file path') + + p.add_argument('--measure', + action=argparse.BooleanOptionalAction, + help='print systemd-measure output for the UKI') + + opts = p.parse_args(args) + + if opts.cmdline and opts.cmdline.startswith('@'): + opts.cmdline = pathlib.Path(opts.cmdline[1:]) + + if opts.os_release is not None and opts.os_release.startswith('@'): + opts.os_release = pathlib.Path(opts.os_release[1:]) + elif opts.os_release is None: + p = pathlib.Path('/etc/os-release') + if not p.exists(): + p = pathlib.Path('/usr/lib/os-release') + opts.os_release = p + + if opts.efi_arch is None: + opts.efi_arch = guess_efi_arch() + + if opts.stub is None: + opts.stub = f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub' + + if opts.signing_engine is None: + opts.sb_key = pathlib.Path(opts.sb_key) if opts.sb_key else None + opts.sb_cert = pathlib.Path(opts.sb_cert) if opts.sb_cert else None + + if bool(opts.sb_key) ^ bool(opts.sb_cert): + raise ValueError('--secureboot-private-key= and --secureboot-certificate= must be specified together') + + if opts.sign_kernel and not opts.sb_key: + raise ValueError('--sign-kernel requires --secureboot-private-key= and --secureboot-certificate= to be specified') + + n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys) + n_pcr_priv = None if opts.pcr_private_keys is None else len(opts.pcr_private_keys) + n_phase_path_groups = None if opts.phase_path_groups is None else len(opts.phase_path_groups) + if n_pcr_pub is not None and n_pcr_pub != n_pcr_priv: + raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=') + if n_phase_path_groups is not None and n_phase_path_groups != n_pcr_priv: + raise ValueError('--phases= specifications must match --pcr-private-key=') + + if opts.output is None: + suffix = '.efi' if opts.sb_key else '.unsigned.efi' + opts.output = opts.linux.name + suffix + + for section in opts.sections: + section.check_name() + + return opts + + +def main(): + opts = parse_args() + check_inputs(opts) + make_uki(opts) + + +if __name__ == '__main__': + main() From 30ec2eaef5f1e3c6639304316a12bf01a5cd7150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 16 Nov 2022 15:52:47 +0100 Subject: [PATCH 07/12] meson,ukify: hook up ukify, add --version option The option is added because we have a similar one for kernel-install. This program requires python, and some people might want to skip it because of this. The tool is installed in /usr/lib/systemd for now, since the interface might change. A template file is used, but there is no .in suffix. The problem is that we'll later want to import the file as a module for tests, but recent Python versions make it annoyingly hard to import a module from a file without a .py suffix. imp.load_sources() works, but it is deprecated and throws warnings. importlib.machinery.SourceFileLoader().load_module() works, but is also deprecated. And the documented replacements are a maze of twisted little callbacks that result in an empty module. So let's take the easy way out, and skip the suffix which makes it easy to import the template as a module after adding the directory to sys.path. --- meson.build | 23 +++++++++++++++++++++++ meson_options.txt | 2 ++ src/ukify/ukify.py | 6 ++++++ 3 files changed, 31 insertions(+) diff --git a/meson.build b/meson.build index 2fb5102971..84b2a22ebe 100644 --- a/meson.build +++ b/meson.build @@ -712,6 +712,17 @@ if run_command(python, '-c', 'import jinja2', check : false).returncode() != 0 error('python3 jinja2 missing') endif +python_310 = run_command(python, '-c', + 'import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)', + check : false).returncode() == 0 +if get_option('ukify') == 'auto' + want_ukify = python_310 +elif get_option('ukify') == 'true' and not python310 + error('ukify requires Python >= 3.10') +else + want_ukify = get_option('ukify') == 'true' +endif + ############################################################ gperf = find_program('gperf') @@ -3991,6 +4002,18 @@ if want_tests != 'false' and want_kernel_install args : [exe.full_path(), loaderentry_install]) endif +if want_ukify + exe = custom_target( + 'ukify', + input : 'src/ukify/ukify.py', + output : 'ukify', + command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'], + install : true, + install_mode : 'rwxr-xr-x', + install_dir : rootlibexecdir) + public_programs += exe +endif + ############################################################ runtest_env = custom_target( diff --git a/meson_options.txt b/meson_options.txt index 19e9c2d990..6a95955dd9 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -501,6 +501,8 @@ option('llvm-fuzz', type : 'boolean', value : false, description : 'build against LLVM libFuzzer') option('kernel-install', type: 'boolean', value: true, description : 'install kernel-install and associated files') +option('ukify', type : 'combo', choices : ['auto', 'true', 'false'], + description : 'install ukify') option('analyze', type: 'boolean', value: true, description : 'install systemd-analyze') diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 4041cefc9d..d4afd88290 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -22,6 +22,8 @@ import typing import pefile +__version__ = '{{GIT_VERSION}}' + EFI_ARCH_MAP = { # host_arch glob : [efi_arch, 32_bit_efi_arch if mixed mode is supported] 'x86_64' : ['x64', 'ia32'], @@ -519,6 +521,10 @@ usage: ukify [options…] linux initrd action=argparse.BooleanOptionalAction, help='print systemd-measure output for the UKI') + p.add_argument('--version', + action='version', + version=f'ukify {__version__}') + opts = p.parse_args(args) if opts.cmdline and opts.cmdline.startswith('@'): From a1d6dbb1c94685d7972f63ed2762fe4ba0251287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 25 Nov 2022 17:21:40 +0100 Subject: [PATCH 08/12] tests: add pytest tests for ukify Some gymnastics were needed to import ukify as a module. Before the file was templated, this was trivial: insert the directory in sys.path, call import. But it's a real pain to import the unsuffixed file after processing. Instead, the untemplated file is imported, which works well enough for tests and is very simple. The tests can be called via pytest: PATH=build/:$PATH pytest -v src/ukify/test/test_ukify.py or directly: PATH=build/:$PATH src/ukify/test/test_ukify.py or via the meson test machinery output: meson test -C build test-ukify -v or without verbose output: meson test -C build test-ukify Zekret files are obfuscated using base64. --- meson.build | 1 + src/ukify/test/example.signing.crt.base64 | 23 ++ src/ukify/test/example.signing.key.base64 | 30 ++ .../test/example.tpm2-pcr-private.pem.base64 | 30 ++ .../test/example.tpm2-pcr-private2.pem.base64 | 30 ++ .../test/example.tpm2-pcr-public.pem.base64 | 8 + .../test/example.tpm2-pcr-public2.pem.base64 | 8 + src/ukify/test/meson.build | 7 + src/ukify/test/setup.cfg | 2 + src/ukify/test/test_ukify.py | 372 ++++++++++++++++++ 10 files changed, 511 insertions(+) create mode 100644 src/ukify/test/example.signing.crt.base64 create mode 100644 src/ukify/test/example.signing.key.base64 create mode 100644 src/ukify/test/example.tpm2-pcr-private.pem.base64 create mode 100644 src/ukify/test/example.tpm2-pcr-private2.pem.base64 create mode 100644 src/ukify/test/example.tpm2-pcr-public.pem.base64 create mode 100644 src/ukify/test/example.tpm2-pcr-public2.pem.base64 create mode 100644 src/ukify/test/meson.build create mode 100644 src/ukify/test/setup.cfg create mode 100755 src/ukify/test/test_ukify.py diff --git a/meson.build b/meson.build index 84b2a22ebe..9f8556ff96 100644 --- a/meson.build +++ b/meson.build @@ -2198,6 +2198,7 @@ subdir('src/test') subdir('src/fuzz') subdir('rules.d') subdir('test') +subdir('src/ukify/test') # needs to be last for test_env variable ############################################################ diff --git a/src/ukify/test/example.signing.crt.base64 b/src/ukify/test/example.signing.crt.base64 new file mode 100644 index 0000000000..694d13b5a6 --- /dev/null +++ b/src/ukify/test/example.signing.crt.base64 @@ -0,0 +1,23 @@ +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURsVENDQW4yZ0F3SUJBZ0lVTzlqUWhhblhj +b3ViOERzdXlMMWdZbksrR1lvd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1dURUxNQWtHQTFVRUJoTUNX +Rmd4RlRBVEJnTlZCQWNNREVSbFptRjFiSFFnUTJsMGVURWNNQm9HQTFVRQpDZ3dUUkdWbVlYVnNk +Q0JEYjIxd1lXNTVJRXgwWkRFVk1CTUdBMVVFQXd3TWEyVjVJSE5wWjI1cGJtbG5NQ0FYCkRUSXlN +VEF5T1RFM01qY3dNVm9ZRHpNd01qSXdNekF4TVRjeU56QXhXakJaTVFzd0NRWURWUVFHRXdKWVdE +RVYKTUJNR0ExVUVCd3dNUkdWbVlYVnNkQ0JEYVhSNU1Sd3dHZ1lEVlFRS0RCTkVaV1poZFd4MElF +TnZiWEJoYm5rZwpUSFJrTVJVd0V3WURWUVFEREF4clpYa2djMmxuYm1sdWFXY3dnZ0VpTUEwR0NT +cUdTSWIzRFFFQkFRVUFBNElCCkR3QXdnZ0VLQW9JQkFRREtVeHR4Y0d1aGYvdUp1SXRjWEhvdW0v +RE9RL1RJM3BzUWlaR0ZWRkJzbHBicU5wZDUKa2JDaUFMNmgrY1FYaGRjUmlOT1dBR0wyMFZ1T2Rv +VTZrYzlkdklGQnFzKzc2NHhvWGY1UGd2SlhvQUxSUGxDZAp4YVdPQzFsOFFIRHpxZ09SdnREMWNI +WFoveTkvZ1YxVU1GK1FlYm12aUhRN0U4eGw1T2h5MG1TQVZYRDhBTitsCjdpMUR6N0NuTzhrMVph +alhqYXlpNWV1WEV0TnFSZXNuVktRRElTQ0t2STFueUxySWxHRU1GZmFuUmRLQWthZ3MKalJnTmVh +T3N3aklHNjV6UzFVdjJTZXcxVFpIaFhtUmd5TzRVT0JySHZlSml2T2hObzU3UlRKd0M2K2lGY0FG +aApSSnorVmM2QUlSSkI1ZWtJUmdCN3VDNEI5ZmwydXdZKytMODNBZ01CQUFHalV6QlJNQjBHQTFV +ZERnUVdCQlFqCllIMnpzVFlPQU51MkcweXk1QkxlOHBvbWZUQWZCZ05WSFNNRUdEQVdnQlFqWUgy +enNUWU9BTnUyRzB5eTVCTGUKOHBvbWZUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEdDU3FHU0li +M0RRRUJDd1VBQTRJQkFRQ2dxcmFXaE51dQptUmZPUjVxcURVcC83RkpIL1N6Zk1vaDBHL2lWRkhv +OUpSS0tqMUZ2Q0VZc1NmeThYTmdaUDI5eS81Z0h4cmcrCjhwZWx6bWJLczdhUTRPK01TcmIzTm11 +V1IzT0M0alBoNENrM09ZbDlhQy9iYlJqSWFvMDJ6K29XQWNZZS9xYTEKK2ZsemZWVEUwMHJ5V1RM +K0FJdDFEZEVqaG01WXNtYlgvbWtacUV1TjBtSVhhRXhSVE9walczUWRNeVRQaURTdApvanQvQWMv +R2RUWDd0QkhPTk44Z3djaC91V293aVNORERMUm1wM2VScnlOZ3RPKzBISUd5Qm16ZWNsM0VlVEo2 +CnJzOGRWUFhqR1Z4dlZDb2tqQllrOWdxbkNGZEJCMGx4VXVNZldWdVkyRUgwSjI3aGh4SXNFc3ls +VTNIR1EyK2MKN1JicVY4VTNSRzA4Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K diff --git a/src/ukify/test/example.signing.key.base64 b/src/ukify/test/example.signing.key.base64 new file mode 100644 index 0000000000..88baedbcb6 --- /dev/null +++ b/src/ukify/test/example.signing.key.base64 @@ -0,0 +1,30 @@ +LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZB +QVNDQktnd2dnU2tBZ0VBQW9JQkFRREtVeHR4Y0d1aGYvdUoKdUl0Y1hIb3VtL0RPUS9USTNwc1Fp +WkdGVkZCc2xwYnFOcGQ1a2JDaUFMNmgrY1FYaGRjUmlOT1dBR0wyMFZ1Twpkb1U2a2M5ZHZJRkJx +cys3NjR4b1hmNVBndkpYb0FMUlBsQ2R4YVdPQzFsOFFIRHpxZ09SdnREMWNIWFoveTkvCmdWMVVN +RitRZWJtdmlIUTdFOHhsNU9oeTBtU0FWWEQ4QU4rbDdpMUR6N0NuTzhrMVphalhqYXlpNWV1WEV0 +TnEKUmVzblZLUURJU0NLdkkxbnlMcklsR0VNRmZhblJkS0FrYWdzalJnTmVhT3N3aklHNjV6UzFV +djJTZXcxVFpIaApYbVJneU80VU9Cckh2ZUppdk9oTm81N1JUSndDNitpRmNBRmhSSnorVmM2QUlS +SkI1ZWtJUmdCN3VDNEI5ZmwyCnV3WSsrTDgzQWdNQkFBRUNnZ0VBQkhZQ28rU3JxdHJzaStQU3hz +MlBNQm5tSEZZcFBvaVIrTEpmMEFYRTVEQUoKMGM0MFZzemNqU1hoRGljNHFLQWQxdGdpZWlzMkEy +VW9WS0xPV3pVOTBqNUd4MURoMWEzaTRhWTQ1ajNuNUFDMgpMekRsakNVQWVucExsYzdCN3MxdjJM +WFJXNmdJSVM5Y043NTlkVTYvdktyQ2FsbGkzcTZZRWlNUzhQMHNsQnZFCkZtdEc1elFsOVJjV0gr +cHBqdzlIMTJSZ3BldUVJVEQ2cE0vd2xwcXZHRlUwcmZjM0NjMHhzaWdNTnh1Z1FJNGgKbnpjWDVs +OEs0SHdvbmhOTG9TYkh6OU5BK3p3QkpuUlZVSWFaaEVjSThtaEVPWHRaRkpYc01aRnhjS2l3SHFS +dApqUUVHOHJRa3lPLytXMmR5Z2czV1lNYXE1OWpUWVdIOUsrQmFyeEMzRVFLQmdRRFBNSFMycjgz +ZUpRTTlreXpkCndDdnlmWGhQVlVtbVJnOGwyWng0aC9tci9mNUdDeW5SdzRzT2JuZGVQd29tZ1Iz +cFBleFFGWlFFSExoZ1RGY3UKVk5uYXcrTzBFL1VnL01pRGswZDNXU0hVZXZPZnM1cEM2b3hYNjNT +VENwNkVLT2VEZlpVMW9OeHRsZ0YyRVhjcgpmVlZpSzFKRGk3N2dtaENLcFNGcjBLK3gyUUtCZ1FE +NS9VUC9hNU52clExdUhnKzR0SzJZSFhSK1lUOFREZG00Ck8xZmh5TU5lOHRYSkd5UUJjTktVTWg2 +M2VyR1MwWlRWdGdkNHlGS3RuOGtLU2U4TmlacUl1aitVUVIyZ3pEQVAKQ2VXcXl2Y2pRNmovU1Yw +WjVvKzlTNytiOStpWWx5RTg2bGZobHh5Z21aNnptYisxUUNteUtNVUdBNis5VmUvMgo1MHhDMXBB +L2p3S0JnUUNEOHA4UnpVcDFZK3I1WnVaVzN0RGVJSXZqTWpTeVFNSGE0QWhuTm1tSjREcjBUcDIy +CmFpci82TmY2WEhsUlpqOHZVSEZUMnpvbG1FalBneTZ1WWZsUCtocmtqeVU0ZWVRVTcxRy9Mek45 +UjBRcCs4Nk4KT1NSaHhhQzdHRE0xaFh0VFlVSUtJa1RmUVgzeXZGTEJqcE0yN3RINEZHSmVWWitk +UEdiWmE5REltUUtCZ1FENQpHTU5qeExiQnhhY25QYThldG5KdnE1SUR5RFRJY0xtc1dQMkZ6cjNX +WTVSZzhybGE4aWZ5WVVxNE92cXNPRWZjCjk2ZlVVNUFHejd2TWs4VXZNUmtaK3JRVnJ4aXR2Q2g3 +STdxRkIvOWdWVEFWU080TE8vR29oczBqeGRBd0ZBK2IKbWtyOVQ4ekh2cXNqZlNWSW51bXRTL0Nl +d0plaHl2cjBoSjg1em9Fbnd3S0JnR1h6UXVDSjJDb3NHVVhEdnlHKwpyRzVBd3pUZGd0bHg4YTBK +NTg1OWtZbVd0cW5WYUJmbFdrRmNUcHNEaGZ2ZWVDUkswc29VRlNPWkcranpsbWJrCkpRL09aVkZJ +dG9MSVZCeE9qeWVXNlhUSkJXUzFRSkVHckkwY0tTbXNKcENtUXVPdUxMVnZYczV0U21CVmc5RXQK +MjZzUkZwcjVWWmsrZlNRa3RhbkM4NGV1Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K diff --git a/src/ukify/test/example.tpm2-pcr-private.pem.base64 b/src/ukify/test/example.tpm2-pcr-private.pem.base64 new file mode 100644 index 0000000000..586b28ef9b --- /dev/null +++ b/src/ukify/test/example.tpm2-pcr-private.pem.base64 @@ -0,0 +1,30 @@ +LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZB +QVNDQktnd2dnU2tBZ0VBQW9JQkFRQzVuOHFhbzVNZ1BJUVcKc0F5Y2R3dnB1bjdNNHlRSW9FL3I3 +ekFGTG1hZlBXclo3d2JaaUIyTkY1MVdHOEo4bnlDQkI3M0RLcmZaeWs5cwphQXdXVW5RR2t0dGFv +RXpXRzZSRTM3dXdQOUpVM09YdklTNTBhcy9KSHVHNlJPYmE2V0NOOFp2TTdkZGpvTDFKCkZlYnBS +SXI1Vi82VStMTFhrUnRNYVczUnZ6T0xYeU1NT2QzOEcxZ0d0VlRHcm90ejVldFgrTUNVU2lOVGFE +OVUKN1dEZXVsZXVpMlRnK1I3TGRoSXg3ZTQ5cEhRM3d6a1NxeFQ4SGpoU3ZURWpITWVSNjIwaUhF +ZW9uYzdsMXVnagpzY1pwTktHdk13bXUvU2ptWFp6UkpOdjVOU0txcEVnQll2RnFkS3dUdlc4MWl6 +SUFvN3paMkx6NDJYb25zSWJ2CjNrbGZqTG1mQWdNQkFBRUNnZ0VBQXozYm8yeTAzb3kvLzhkdVNQ +TTVSWWtvdXJwQ3dGWFFYMzNyV0VQUnJmazgKR3ZjMkp1bGVIcjhwVTc0alhOcklqZ2hORTVIMDZQ +eEQrOUFyV2Q1eHdVV2lTQWhobnlHWGNrNTM4Q0dGTWs4egpRc1JSRTk1anA0Ny9BU28vMzlYUWhs +b1FUdmxlV0JLUUM2MHl2YU1oVEM1eHR6ZEtwRUlYK0hNazVGTlMrcDJVCmxtL3AzVE1YWDl1bmc5 +Mk9pTzUzV1VreFpQN2cwTVJHbGJrNzhqc1dkdjFYY0tLRjhuVmU5WC9NR1lTYlVLNy8KM2NYazFR +WTRUdVZaQlBFSE12RFRpWWwxbmdDd1ZuL2MyY3JQU3hJRFdFWlhEdm90SFUwQkNQZURVckxGa0F5 +cQpDaloza3MzdEh4am42STkraEVNcUJDMzY1MHFjdDNkZ0RVV2loc2MzdVFLQmdRRG1mVTNKc29K +QWFOdmxCbXgyClhzRDRqbXlXV1F2Z244cVNVNG03a2JKdmprcUJ6VnB0T0ZsYmk2ejZHOXR6ZHNX +a0dJSjh3T0ZRb1hlM0dKOFIKSlVpeEFXTWZOM1JURGo5VjVXbzZJdE5EbzM1N3dNbVVYOW1qeThF +YXp0RE1BckdSNGJva0Q5RjY3clhqSGdSMQpaZVcvSDlUWHFUV1l4VHl6UDB3ZDBQeUZ4d0tCZ1FE +T0swWHVQS0o0WG00WmFCemN0OTdETXdDcFBSVmVvUWU3CmkzQjRJQ3orWFZ4cVM2amFTY2xNeEVm +Nk5tM2tLNERDR1dwVkpXcm9qNjlMck1KWnQzTlI2VUJ5NzNqUVBSamsKRXk5N3YrR04yVGwwNjFw +ZUxUM0dRS2RhT2VxWldpdElOcFc1dUxHL1poMGhoRUY5c1lSVTRtUFYwUWpla2kvdgp1bnVmcWx0 +TmFRS0JnQTl6TE1pdFg0L0R0NkcxZVlYQnVqdXZDRlpYcDdVcDRPRklHajVwZU1XRGl6a0NNK0tJ +CldXMEtndERORnp1NUpXeG5mQyt5bWlmV2V2alovS2Vna1N2VVJQbXR0TzF3VWd5RzhVVHVXcXo1 +QTV4MkFzMGcKVTYxb0ZneWUrbDRDZkRha0k5OFE5R0RDS1kwTTBRMnhnK0g0MTBLUmhCYzJlV2dt +Z1FxcW5KSzNBb0dCQU1rZgpnOWZXQlBVQndjdzlPYkxFR0tjNkVSSUlTZG1IbytCOE5kcXFJTnAv +djFEZXdEazZ0QXFVakZiMlZCdTdxSjh4ClpmN3NRcS9ldzdaQ01WS09XUXgyVEc0VFdUdGo3dTFJ +SGhGTjdiNlFRN0hnaXNiR3diV3VpdFBGSGl3OXYyMXgKK253MFJnb2VscHFFeDlMVG92R2Y3SjdB +ampONlR4TkJTNnBGNlUzSkFvR0JBT0tnbHlRWDJpVG5oMXd4RG1TVQo4RXhoQVN3S09iNS8yRmx4 +aUhtUHVjNTZpS0tHY0lkV1cxMUdtbzdJakNzSTNvRm9iRkFjKzBXZkMvQTdMNWlmCjNBYVNWcmh0 +cThRRklRaUtyYUQ0YlRtRk9Famg5QVVtUHMrWnd1OE9lSXJBSWtwZDV3YmlhTEJLd0pRbVdtSFAK +dUNBRTA3cXlSWXJ0c3QvcnVSSG5IdFA1Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K diff --git a/src/ukify/test/example.tpm2-pcr-private2.pem.base64 b/src/ukify/test/example.tpm2-pcr-private2.pem.base64 new file mode 100644 index 0000000000..d21a3d6043 --- /dev/null +++ b/src/ukify/test/example.tpm2-pcr-private2.pem.base64 @@ -0,0 +1,30 @@ +LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZB +QVNDQktZd2dnU2lBZ0VBQW9JQkFRQzJ2Nk1oZHg3a3VjUHIKbmtFNFIrY3FnV2Y5T3B1c2h2M2o3 +SG50K08wdi84d2l2T1BFNTlLMHYvRWJOOG94TDZEWUNXU0JCRU4vREJ5MgpMUTYwbldSdHBZN2Ju +bEcrcEtVeTRvSDRNZXZCR2JqZUhrak9LU3dNYVVWNGs4UmVSSjg4cVZ1U1MxSnVORW1NCmd5SERF +NGFPNG5ndG5UUFZZdzUydVBIcG1rN0E4VFdXN2lLZE5JWWZWOCtuR1pENXIzRWllekRsUUNORG54 +UkcKdm5uSFZ6VFhZR3RwY2xaeWlJclpVekpBNFFPZnRueXB5UDVrQS94NVM1MU9QeGFxWlA3eGtP +S0NicUUvZmZvMApFTi9rTno0N0ZoUGUxbVBHUkZZWldHZXg0aWFPdHlLdHhnU1FYYkdlNEVoeVR4 +SjJlT3U4QUVoVklTdjh6UU9nClNtbWx2UGQvQWdNQkFBRUNnZ0VBUUFpRERRRlR3bG96QTVhMmpK +VnBNdlFYNzF0L1c2TUxTRGMrZS90cWhKU1IKUHlUSGZHR3NhMmdMLy9qNjhHUWJiRWRTUDRDeWM4 +eFhMU0E1bEdESDVVR0svbm9KYzQ3MlVZK2JjYzl3SjMrdgpUcWoyNHNIN2JMZmdQMEVybjhwVXIy +azZMRmNYSVlWUnRobm1sUmQ4NFFrS2loVVlxZTdsRFFWOXdsZ3V1eHpRCnBmVEtDTWk1bXJlYjIx +OExHS0QrMUxjVmVYZjExamc3Z2JnMllLZ1dOQ2R3VmIyUzJ5V0hTTjBlT3hPd21kWXIKSUVCekpG +eEc2MFJxSlJ1RzVIam9iemc2cy9ycUo1THFta3JhUWh6bHFPQVZLblpGOHppbG9vcDhXUXBQY3RN +cwp0cHBjczhtYkFkWHpoSTVjN0U1VVpDM2NJcEd6SE4raDZLK0F3R3ZEeVFLQmdRRDRBOTdQM29v +dGhoMHZHQmFWCnZWOXhHTm1YbW5TeUg0b29HcmJvaG1JWkkwVlhZdms5dWViSUJjbDZRMUx4WnN3 +UFpRMVh5TUhpTjY1Z0E1emgKai9HZGcrdDlvcU5CZml0TUFrUTl1aWxvaXlKVWhYblk5REMvRitl +ZksycEpNbHdkci9qWEttRHpkQUZBVDgyWQpWRmJ3MlpLVi9GNEJNMUtCdDBZN0RPTmlad0tCZ1FD +OG9kZk0waytqL25VSzQ4TEV2MGtGbVNMdWdnTVlkM3hVCmZibmx0cUhFTVpJZU45OFVHK2hBWEdw +dU1Ya0JPM2Mwcm5ZRDVXZkNBRzFxT1V2ZTZzdHd6N0VuK3hWdlkvcWEKU3ZTaDRzMzhnZlBIeXhR +aGJvNWRwQTZUT3pwT0MyVi9rVXBVRUdJSmVVVllhQ05uWXNpUjRWUGVWL1lvR1htSwpQV29KbnAw +REtRS0JnQlk3cXBheDJXczVVWlp1TDJBZkNOWkhwd0hySzdqb0VPZUZkWTRrdGRpUkM5OUlsUlZP +CmUvekVZQXBnektldFVtK3kzRjVaTmVCRW81SWg0TWRyc3ZvdTRFWno5UFNqRGRpVGYzQ1ZKcThq +Z2VGWDBkTjgKR0g2WTh2K1cwY0ZjRFZ2djhYdkFaYzZOUUt0Mk8vVUM0b1JXek1nN1JtWVBKcjlR +SWJDYmVDclRBb0dBTjdZbApJbDFMSUVoYkVTaExzZ2c4N09aWnBzL0hVa2FYOWV4Y0p6aFZkcmlk +UzBkOUgxZE90Uk9XYTQwNUMrQWdTUEx0CjhDQ2xFR3RINVlPZW9Pdi93Z1hWY05WN2N6YTRJVEhh +SnFYeDZJNEpEZzB3bU44cU5RWHJPQmphRTRyU0kyY3AKNk1JZDhtWmEwTTJSQjB2cHFRdy8xUDl0 +dUZJdHoySnNHd001cEdFQ2dZQVVnQVV3WENBcEtZVkZFRmxHNlBhYwpvdTBhdzdGNm1aMi9NNUcv +ek9tMHFDYnNXdGFNU09TdUEvNmlVOXB0NDBaWUFONFUvd2ZxbncyVkVoRnA3dzFNCnpZWmJCRDBx +ZVlkcDRmc1NuWXFMZmJBVmxQLzB6dmEzdkwwMlJFa25WalBVSnAvaGpKVWhBK21WN252VDZ5VjQK +cTg4SWVvOEx3Q1c1c2Jtd2lyU3Btdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K diff --git a/src/ukify/test/example.tpm2-pcr-public.pem.base64 b/src/ukify/test/example.tpm2-pcr-public.pem.base64 new file mode 100644 index 0000000000..728a0f5362 --- /dev/null +++ b/src/ukify/test/example.tpm2-pcr-public.pem.base64 @@ -0,0 +1,8 @@ +LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FR +OEFNSUlCQ2dLQ0FRRUF1Wi9LbXFPVElEeUVGckFNbkhjTAo2YnArek9Na0NLQlA2Kzh3QlM1bW56 +MXEyZThHMllnZGpSZWRWaHZDZko4Z2dRZTl3eXEzMmNwUGJHZ01GbEowCkJwTGJXcUJNMWh1a1JO +KzdzRC9TVk56bDd5RXVkR3JQeVI3aHVrVG0ydWxnamZHYnpPM1hZNkM5U1JYbTZVU0sKK1ZmK2xQ +aXkxNUViVEdsdDBiOHppMThqRERuZC9CdFlCclZVeHE2TGMrWHJWL2pBbEVvalUyZy9WTzFnM3Jw +WApyb3RrNFBrZXkzWVNNZTN1UGFSME44TTVFcXNVL0I0NFVyMHhJeHpIa2V0dEloeEhxSjNPNWRi +b0k3SEdhVFNoCnJ6TUpydjBvNWwyYzBTVGIrVFVpcXFSSUFXTHhhblNzRTcxdk5Zc3lBS084MmRp +OCtObDZKN0NHNzk1Slg0eTUKbndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg== diff --git a/src/ukify/test/example.tpm2-pcr-public2.pem.base64 b/src/ukify/test/example.tpm2-pcr-public2.pem.base64 new file mode 100644 index 0000000000..44bb3ee9ac --- /dev/null +++ b/src/ukify/test/example.tpm2-pcr-public2.pem.base64 @@ -0,0 +1,8 @@ +LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FR +OEFNSUlCQ2dLQ0FRRUF0citqSVhjZTVMbkQ2NTVCT0VmbgpLb0ZuL1RxYnJJYjk0K3g1N2ZqdEwv +L01JcnpqeE9mU3RML3hHemZLTVMrZzJBbGtnUVJEZnd3Y3RpME90SjFrCmJhV08yNTVSdnFTbE11 +S0IrREhyd1JtNDNoNUl6aWtzREdsRmVKUEVYa1NmUEtsYmtrdFNialJKaklNaHd4T0cKanVKNExa +MHoxV01PZHJqeDZacE93UEUxbHU0aW5UU0dIMWZQcHhtUSthOXhJbnN3NVVBalE1OFVScjU1eDFj +MAoxMkJyYVhKV2NvaUsyVk15UU9FRG43WjhxY2orWkFQOGVVdWRUajhXcW1UKzhaRGlnbTZoUDMz +Nk5CRGY1RGMrCk94WVQzdFpqeGtSV0dWaG5zZUltanJjaXJjWUVrRjJ4bnVCSWNrOFNkbmpydkFC +SVZTRXIvTTBEb0VwcHBiejMKZndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg== diff --git a/src/ukify/test/meson.build b/src/ukify/test/meson.build new file mode 100644 index 0000000000..e39178f892 --- /dev/null +++ b/src/ukify/test/meson.build @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +if want_ukify and want_tests != 'false' + test('test-ukify', + files('test_ukify.py'), + env : test_env) +endif diff --git a/src/ukify/test/setup.cfg b/src/ukify/test/setup.cfg new file mode 100644 index 0000000000..1f655da834 --- /dev/null +++ b/src/ukify/test/setup.cfg @@ -0,0 +1,2 @@ +[tool:pytest] +addopts = --flakes diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py new file mode 100755 index 0000000000..292540bbde --- /dev/null +++ b/src/ukify/test/test_ukify.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: LGPL-2.1+ + +# pylint: disable=missing-docstring,redefined-outer-name,invalid-name +# pylint: disable=unused-import,import-outside-toplevel,useless-else-on-loop +# pylint: disable=consider-using-with,wrong-import-position,unspecified-encoding + +import base64 +import json +import os +import pathlib +import re +import shutil +import subprocess +import sys +import tempfile + +try: + import pytest +except ImportError: + sys.exit(77) + +# We import ukify.py, which is a template file. But only __version__ is +# substituted, which we don't care about here. Having the .py suffix makes it +# easier to import the file. +sys.path.append(os.path.dirname(__file__) + '/..') +import ukify + + +def test_guess_efi_arch(): + arch = ukify.guess_efi_arch() + assert arch in ukify.EFI_ARCHES + +def test_shell_join(): + assert ukify.shell_join(['a', 'b', ' ']) == "a b ' '" + +def test_round_up(): + assert ukify.round_up(0) == 0 + assert ukify.round_up(4095) == 4096 + assert ukify.round_up(4096) == 4096 + assert ukify.round_up(4097) == 8192 + +def test_parse_args_minimal(): + opts = ukify.parse_args('arg1 arg2'.split()) + assert opts.linux == pathlib.Path('arg1') + assert opts.initrd == pathlib.Path('arg2') + assert opts.os_release in (pathlib.Path('/etc/os-release'), + pathlib.Path('/usr/lib/os-release')) + +def test_parse_args_many(): + opts = ukify.parse_args( + ['/ARG1', '///ARG2', + '--cmdline=a b c', + '--os-release=K1=V1\nK2=V2', + '--devicetree=DDDDTTTT', + '--splash=splash', + '--pcrpkey=PATH', + '--uname=1.2.3', + '--stub=STUBPATH', + '--pcr-private-key=PKEY1', + '--pcr-public-key=PKEY2', + '--pcr-banks=SHA1,SHA256', + '--signing-engine=ENGINE', + '--secureboot-private-key=SBKEY', + '--secureboot-certificate=SBCERT', + '--sign-kernel', + '--no-sign-kernel', + '--tools=TOOLZ///', + '--output=OUTPUT', + '--measure', + '--no-measure', + ]) + assert opts.linux == pathlib.Path('/ARG1') + assert opts.initrd == pathlib.Path('/ARG2') + assert opts.os_release == 'K1=V1\nK2=V2' + assert opts.devicetree == pathlib.Path('DDDDTTTT') + assert opts.splash == pathlib.Path('splash') + assert opts.pcrpkey == pathlib.Path('PATH') + assert opts.uname == '1.2.3' + assert opts.stub == pathlib.Path('STUBPATH') + assert opts.pcr_private_keys == [pathlib.Path('PKEY1')] + assert opts.pcr_public_keys == [pathlib.Path('PKEY2')] + assert opts.pcr_banks == ['SHA1', 'SHA256'] + assert opts.signing_engine == 'ENGINE' + assert opts.sb_key == 'SBKEY' + assert opts.sb_cert == 'SBCERT' + assert opts.sign_kernel is False + assert opts.tools == pathlib.Path('TOOLZ/') + assert opts.output == pathlib.Path('OUTPUT') + assert opts.measure is False + +def test_parse_sections(): + opts = ukify.parse_args( + ['/ARG1', '/ARG2', + '--section=test:TESTTESTTEST', + '--section=test2:@FILE', + ]) + + assert opts.linux == pathlib.Path('/ARG1') + assert opts.initrd == pathlib.Path('/ARG2') + assert len(opts.sections) == 2 + + assert opts.sections[0].name == 'test' + assert isinstance(opts.sections[0].content, pathlib.Path) + assert opts.sections[0].tmpfile + assert opts.sections[0].offset is None + assert opts.sections[0].measure is False + + assert opts.sections[1].name == 'test2' + assert opts.sections[1].content == pathlib.Path('FILE') + assert opts.sections[1].tmpfile is None + assert opts.sections[1].offset is None + assert opts.sections[1].measure is False + +def test_help(capsys): + with pytest.raises(SystemExit): + ukify.parse_args(['--help']) + out = capsys.readouterr() + assert '--section' in out.out + assert not out.err + +def test_help_error(capsys): + with pytest.raises(SystemExit): + ukify.parse_args(['a', 'b', '--no-such-option']) + out = capsys.readouterr() + assert not out.out + assert '--no-such-option' in out.err + assert len(out.err.splitlines()) == 1 + +@pytest.fixture(scope='session') +def kernel_initrd(): + try: + text = subprocess.check_output(['bootctl', 'list', '--json=short'], + text=True) + except subprocess.CalledProcessError: + return None + + items = json.loads(text) + + for item in items: + try: + linux = f"{item['root']}{item['linux']}" + initrd = f"{item['root']}{item['initrd'][0]}" + except (KeyError, IndexError): + pass + return [linux, initrd] + else: + return None + +def test_check_splash(): + try: + # pyflakes: noqa + import PIL # noqa + except ImportError: + pytest.skip('PIL not available') + + with pytest.raises(OSError): + ukify.check_splash(os.devnull) + +def test_basic_operation(kernel_initrd, tmpdir): + if kernel_initrd is None: + pytest.skip('linux+initrd not found') + + output = f'{tmpdir}/basic.efi' + opts = ukify.parse_args(kernel_initrd + [f'--output={output}']) + try: + ukify.check_inputs(opts) + except OSError as e: + pytest.skip(str(e)) + + ukify.make_uki(opts) + + # let's check that objdump likes the resulting file + subprocess.check_output(['objdump', '-h', output]) + +def test_sections(kernel_initrd, tmpdir): + if kernel_initrd is None: + pytest.skip('linux+initrd not found') + + output = f'{tmpdir}/basic.efi' + opts = ukify.parse_args([ + *kernel_initrd, + f'--output={output}', + '--uname=1.2.3', + '--cmdline=ARG1 ARG2 ARG3', + '--os-release=K1=V1\nK2=V2\n', + '--section=.test:CONTENTZ', + ]) + + try: + ukify.check_inputs(opts) + except OSError as e: + pytest.skip(str(e)) + + ukify.make_uki(opts) + + # let's check that objdump likes the resulting file + dump = subprocess.check_output(['objdump', '-h', output], text=True) + + for sect in 'text osrel cmdline linux initrd uname test'.split(): + assert re.search(fr'^\s*\d+\s+.{sect}\s+0', dump, re.MULTILINE) + + +def unbase64(filename): + tmp = tempfile.NamedTemporaryFile() + base64.decode(filename.open('rb'), tmp) + tmp.flush() + return tmp + + +def test_efi_signing(kernel_initrd, tmpdir): + if kernel_initrd is None: + pytest.skip('linux+initrd not found') + if not shutil.which('sbsign'): + pytest.skip('sbsign not found') + + ourdir = pathlib.Path(__file__).parent + cert = unbase64(ourdir / 'example.signing.crt.base64') + key = unbase64(ourdir / 'example.signing.key.base64') + + output = f'{tmpdir}/signed.efi' + opts = ukify.parse_args([ + *kernel_initrd, + f'--output={output}', + '--uname=1.2.3', + '--cmdline=ARG1 ARG2 ARG3', + f'--secureboot-certificate={cert.name}', + f'--secureboot-private-key={key.name}', + ]) + + try: + ukify.check_inputs(opts) + except OSError as e: + pytest.skip(str(e)) + + ukify.make_uki(opts) + + if shutil.which('sbverify'): + # let's check that sbverify likes the resulting file + dump = subprocess.check_output([ + 'sbverify', + '--cert', cert.name, + output, + ], text=True) + + assert 'Signature verification OK' in dump + +def test_pcr_signing(kernel_initrd, tmpdir): + if kernel_initrd is None: + pytest.skip('linux+initrd not found') + if os.getuid() != 0: + pytest.skip('must be root to access tpm2') + if subprocess.call(['systemd-creds', 'has-tpm2', '-q']) != 0: + pytest.skip('tpm2 is not available') + + ourdir = pathlib.Path(__file__).parent + pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64') + priv = unbase64(ourdir / 'example.tpm2-pcr-private.pem.base64') + + output = f'{tmpdir}/signed.efi' + opts = ukify.parse_args([ + *kernel_initrd, + f'--output={output}', + '--uname=1.2.3', + '--cmdline=ARG1 ARG2 ARG3', + '--os-release=ID=foobar\n', + '--pcr-banks=sha1', # use sha1 as that is most likely to be supported + f'--pcrpkey={pub.name}', + f'--pcr-public-key={pub.name}', + f'--pcr-private-key={priv.name}', + ]) + + try: + ukify.check_inputs(opts) + except OSError as e: + pytest.skip(str(e)) + + ukify.make_uki(opts) + + # let's check that objdump likes the resulting file + dump = subprocess.check_output(['objdump', '-h', output], text=True) + + for sect in 'text osrel cmdline linux initrd uname pcrsig'.split(): + assert re.search(fr'^\s*\d+\s+.{sect}\s+0', dump, re.MULTILINE) + + # objcopy fails when called without an output argument (EPERM). + # It also fails when called with /dev/null (file truncated). + # It also fails when called with /dev/zero (because it reads the + # output file, infinitely in this case.) + # So let's just call it with a dummy output argument. + subprocess.check_call([ + 'objcopy', + *(f'--dump-section=.{n}={tmpdir}/out.{n}' for n in ( + 'pcrpkey', 'pcrsig', 'osrel', 'uname', 'cmdline')), + output, + tmpdir / 'dummy', + ], + text=True) + + assert open(tmpdir / 'out.pcrpkey').read() == open(pub.name).read() + assert open(tmpdir / 'out.osrel').read() == 'ID=foobar\n' + assert open(tmpdir / 'out.uname').read() == '1.2.3' + assert open(tmpdir / 'out.cmdline').read() == 'ARG1 ARG2 ARG3' + sig = open(tmpdir / 'out.pcrsig').read() + sig = json.loads(sig) + assert list(sig.keys()) == ['sha1'] + assert len(sig['sha1']) == 4 # four items for four phases + +def test_pcr_signing2(kernel_initrd, tmpdir): + if kernel_initrd is None: + pytest.skip('linux+initrd not found') + if os.getuid() != 0: + pytest.skip('must be root to access tpm2') + if subprocess.call(['systemd-creds', 'has-tpm2', '-q']) != 0: + pytest.skip('tpm2 is not available') + + ourdir = pathlib.Path(__file__).parent + pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64') + priv = unbase64(ourdir / 'example.tpm2-pcr-private.pem.base64') + pub2 = unbase64(ourdir / 'example.tpm2-pcr-public2.pem.base64') + priv2 = unbase64(ourdir / 'example.tpm2-pcr-private2.pem.base64') + + output = f'{tmpdir}/signed.efi' + opts = ukify.parse_args([ + *kernel_initrd, + f'--output={output}', + '--uname=1.2.3', + '--cmdline=ARG1 ARG2 ARG3', + '--os-release=ID=foobar\n', + '--pcr-banks=sha1', # use sha1 as that is most likely to be supported + f'--pcrpkey={pub2.name}', + f'--pcr-public-key={pub.name}', + f'--pcr-private-key={priv.name}', + '--phases=enter-initrd enter-initrd:leave-initrd', + f'--pcr-public-key={pub2.name}', + f'--pcr-private-key={priv2.name}', + '--phases=sysinit ready shutdown final', # yes, those phase paths are not reachable + ]) + + try: + ukify.check_inputs(opts) + except OSError as e: + pytest.skip(str(e)) + + ukify.make_uki(opts) + + # let's check that objdump likes the resulting file + dump = subprocess.check_output(['objdump', '-h', output], text=True) + + for sect in 'text osrel cmdline linux initrd uname pcrsig'.split(): + assert re.search(fr'^\s*\d+\s+.{sect}\s+0', dump, re.MULTILINE) + + subprocess.check_call([ + 'objcopy', + *(f'--dump-section=.{n}={tmpdir}/out.{n}' for n in ( + 'pcrpkey', 'pcrsig', 'osrel', 'uname', 'cmdline')), + output, + tmpdir / 'dummy', + ], + text=True) + + assert open(tmpdir / 'out.pcrpkey').read() == open(pub2.name).read() + assert open(tmpdir / 'out.osrel').read() == 'ID=foobar\n' + assert open(tmpdir / 'out.uname').read() == '1.2.3' + assert open(tmpdir / 'out.cmdline').read() == 'ARG1 ARG2 ARG3' + sig = open(tmpdir / 'out.pcrsig').read() + sig = json.loads(sig) + assert list(sig.keys()) == ['sha1'] + assert len(sig['sha1']) == 6 # six items for six phases paths + +if __name__ == '__main__': + pytest.main([__file__, '-v']) From 483c9c1b8a559e2fe2a6bd1e544b4d6c50566071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 16 Nov 2022 17:44:28 +0100 Subject: [PATCH 09/12] ukify: try to find the uname string in the linux image if not specified The approach is based on mkinicpio's autodetection. This is hacky as hell. Some cases are actually fairly nice: ppc64el images have a note that contains 'uname -r'. (The note is not uniquely labeled at all, and only contains the release part instead of the full version-hostname-release string, and we don't actually care about ppc, and it's very hard to read the note from Python, but in general that'd be the approach I'd like.) I opted to simply read and decompress the full linux binary in some cases. Python doesn't make it easy to do streaming decompression with regexp matching, and it doesn't seem to matter much: the image decompresses in a fraction of a second. --- src/ukify/test/test_ukify.py | 8 +++ src/ukify/ukify.py | 123 +++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py index 292540bbde..a4afa56ad9 100755 --- a/src/ukify/test/test_ukify.py +++ b/src/ukify/test/test_ukify.py @@ -208,6 +208,14 @@ def unbase64(filename): return tmp +def test_uname_scraping(kernel_initrd): + if kernel_initrd is None: + pytest.skip('linux+initrd not found') + + uname = ukify.Uname.scrape(kernel_initrd[0]) + assert re.match(r'\d+\.\d+\.\d+', uname) + + def test_efi_signing(kernel_initrd, tmpdir): if kernel_initrd is None: pytest.skip('linux+initrd not found') diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index d4afd88290..d4772d2bae 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -75,6 +75,125 @@ def round_up(x, blocksize=4096): return (x + blocksize - 1) // blocksize * blocksize +def maybe_decompress(filename): + """Decompress file if compressed. Return contents.""" + f = open(filename, 'rb') + start = f.read(4) + f.seek(0) + + if start.startswith(b'\x7fELF'): + # not compressed + return f.read() + + if start.startswith(b'\x1f\x8b'): + import gzip + return gzip.open(f).read() + + if start.startswith(b'\x28\xb5\x2f\xfd'): + import zstd + return zstd.uncompress(f.read()) + + if start.startswith(b'\x02\x21\x4c\x18'): + import lz4.frame + return lz4.frame.decompress(f.read()) + + if start.startswith(b'\x04\x22\x4d\x18'): + print('Newer lz4 stream format detected! This may not boot!') + import lz4.frame + return lz4.frame.decompress(f.read()) + + if start.startswith(b'\x89LZO'): + # python3-lzo is not packaged for Fedora + raise NotImplementedError('lzo decompression not implemented') + + if start.startswith(b'BZh'): + import bz2 + return bz2.open(f).read() + + if start.startswith(b'\x5d\x00\x00'): + import lzma + return lzma.open(f).read() + + raise NotImplementedError(f'unknown file format (starts with {start})') + + +class Uname: + # This class is here purely as a namespace for the functions + + VERSION_PATTERN = r'(?P[a-z0-9._-]+) \([^ )]+\) (?:#.*)' + + NOTES_PATTERN = r'^\s+Linux\s+0x[0-9a-f]+\s+OPEN\n\s+description data: (?P[0-9a-f ]+)\s*$' + + # Linux version 6.0.8-300.fc37.ppc64le (mockbuild@buildvm-ppc64le-03.iad2.fedoraproject.org) + # (gcc (GCC) 12.2.1 20220819 (Red Hat 12.2.1-2), GNU ld version 2.38-24.fc37) + # #1 SMP Fri Nov 11 14:39:11 UTC 2022 + TEXT_PATTERN = rb'Linux version (?P\d\.\S+) \(' + + @classmethod + def scrape_x86(cls, filename, opts=None): + # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L136 + # and https://www.kernel.org/doc/html/latest/x86/boot.html#the-real-mode-kernel-header + with open(filename, 'rb') as f: + f.seek(0x202) + magic = f.read(4) + if magic != b'HdrS': + raise ValueError('Real-Mode Kernel Header magic not found') + f.seek(0x20E) + offset = f.read(1)[0] + f.read(1)[0]*256 # Pointer to kernel version string + f.seek(0x200 + offset) + text = f.read(128) + text = text.split(b'\0', maxsplit=1)[0] + text = text.decode() + + if not (m := re.match(cls.VERSION_PATTERN, text)): + raise ValueError(f'Cannot parse version-host-release uname string: {text!r}') + return m.group('version') + + @classmethod + def scrape_elf(cls, filename, opts=None): + readelf = find_tool('readelf', opts=opts) + + cmd = [ + readelf, + '--notes', + filename, + ] + + print('+', shell_join(cmd)) + notes = subprocess.check_output(cmd, text=True) + + if not (m := re.search(cls.NOTES_PATTERN, notes, re.MULTILINE)): + raise ValueError('Cannot find Linux version note') + + text = ''.join(chr(int(c, 16)) for c in m.group('version').split()) + return text.rstrip('\0') + + @classmethod + def scrape_generic(cls, filename, opts=None): + # import libarchive + # libarchive-c fails with + # ArchiveError: Unrecognized archive format (errno=84, retcode=-30, archive_p=94705420454656) + + # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L209 + + text = maybe_decompress(filename) + if not (m := re.search(cls.TEXT_PATTERN, text)): + raise ValueError(f'Cannot find {cls.TEXT_PATTERN!r} in {filename}') + + return m.group('version').decode() + + @classmethod + def scrape(cls, filename, opts=None): + for func in (cls.scrape_x86, cls.scrape_elf, cls.scrape_generic): + try: + version = func(filename, opts=opts) + print(f'Found uname version: {version}') + return version + except ValueError as e: + print(str(e)) + return None + + @dataclasses.dataclass class Section: name: str @@ -330,6 +449,10 @@ def make_uki(opts): else: linux = opts.linux + if opts.uname is None: + print('Kernel version not specified, starting autodetection 😖.') + opts.uname = Uname.scrape(opts.linux, opts=opts) + uki = UKI(opts.stub) # TODO: derive public key from from opts.pcr_private_keys? From 0fdf4e181025b98df2e52ff3c861dc35d0412b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 25 Nov 2022 16:25:23 +0100 Subject: [PATCH 10/12] man: add man page for ukify --- man/rules/meson.build | 1 + man/ukify.xml | 307 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 man/ukify.xml diff --git a/man/rules/meson.build b/man/rules/meson.build index bb7799036d..442640a4dc 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1189,6 +1189,7 @@ manpages = [ ''], ['udev_new', '3', ['udev_ref', 'udev_unref'], ''], ['udevadm', '8', [], ''], + ['ukify', '1', [], 'HAVE_GNU_EFI'], ['user@.service', '5', ['systemd-user-runtime-dir', 'user-runtime-dir@.service'], diff --git a/man/ukify.xml b/man/ukify.xml new file mode 100644 index 0000000000..3cc13a4cba --- /dev/null +++ b/man/ukify.xml @@ -0,0 +1,307 @@ + + + + + + + + ukify + systemd + + + + ukify + 1 + + + + ukify + Combine kernel and initrd into a signed Unified Kernel Image + + + + + /usr/lib/systemd/ukify + LINUX + INITRD + OPTIONS + + + + + Description + + Note: this command is experimental for now. While it is intended to become a regular component of + systemd, it might still change in behaviour and interface. + + ukify is a tool that combines a kernel and an initrd with + a UEFI boot stub to create a + Unified Kernel Image (UKI) + — a PE binary that can be executed by the firmware to start the embedded linux kernel. + See systemd-stub7 + for details about the stub. + + Additional sections will be inserted into the UKI, either automatically or only if a specific + option is provided. See the discussions of + , + , + , + , + , + , + and + below. + + If PCR signing keys are provided via the and + options, PCR values that will be seen after booting with the given + kernel, initrd, and other sections, will be calculated, signed, and embedded in the UKI. + systemd-measure1 is + used to perform this calculation and signing. + + The calculation of PCR values is done for specific boot phase paths. Those can be specified with + option. If not specified, the default provided by + systemd-measure is used. It is also possible to specify the + , , and + arguments more than once. Signatures will be then performed with each of the specified keys. When both + and are used, they must be specified the + same number of times, and then the n-th boot phase path set will be signed by the n-th key. This can be + used to build different trust policies for different phases of the boot. + + If a SecureBoot signing key is provided via the option, + the resulting PE binary will be signed as a whole, allowing the resulting UKI to be trusted by + SecureBoot. Also see the discussion of automatic enrollment in + systemd-boot7. + + + + + Options + + Note that the LINUX and INITRD positional + arguments are mandatory. + + The following options are understood: + + + + + + Specify the kernel command line (the .cmdline section). The + argument may be a literal string, or @ followed by a path name. If not specified, + no command line will be embedded. + + + + + + Specify the os-release description (the .osrel section). The + argument may be a literal string, or @ followed by a path name. If not specified, + the os-release5 + file will be picked up from the host system. + + + + + + Specify the devicetree description (the .dtb section). The + argument is a path to a compiled binary DeviceTree file. If not specified, the section will not be + present. + + + + + + Specify a picture to display during boot (the .splash section). + The argument is a path to a BMP file. If not specified, the section will not be present. + + + + + + + Specify a path to a public key to embed in the .pcrpkey section. + If not specified, and there's exactly one argument, that key will + be used. Otherwise, the section will not be present. + + + + + + Specify the kernel version (as in uname -r, the + .uname section). If not specified, an attempt will be made to extract the version + string from the kernel image. It is recommended to pass this explicitly if known, because the + extraction is based on heuristics and not very reliable. If not specified and extraction fails, the + section will not be present. + + + + + + Specify an arbitrary additional section + NAME. Note that the name is used as-is, and if the + section name should start with a dot, it must be included in NAME. The + argument may be a literal string, or @ followed by a path name. This option may be + specified more than once. Any sections specified in this fashion will be inserted (in order) before + the .linux section which is always last. + + + + + + Specify a private key to use for signing PCR policies. This option may be specified + more than once, in which case multiple signatures will be made. + + + + + + Specify a public key to use for signing PCR policies. This option may be specified + more than once, similarly to the option. If not present, the + public keys will be extracted from the private keys. If present, the this option must be specified + the same number of times as the option. + + + + + + A comma or space-separated list of colon-separated phase paths to sign a policy for. + If not present, the default of + systemd-measure1 + will be used. When this argument is present, it must appear the same number of times as the + option. Each set of boot phase paths will be signed with the + corresponding private key. + + + + + + A comma or space-separated list of PCR banks to sign a policy for. If not present, + all known banks will be used (sha1, sha256, + sha384, sha512), which will fail if not supported by the + system. + + + + + + A path to a private key to use for signing of the resulting binary. If the + option is used, this may also be an engine-specific + designation. + + + + + + A path to a certificate to use for signing of the resulting binary. If the + option is used, this may also be an engine-specific + designation. + + + + + + An "engine" to for signing of the resulting binary. This option is currently passed + verbatim to the option of + sbsign1. + + + + + + + + Override the detection of whether to sign the Linux binary itself before it is + embedded in the combined image. If not specified, it will be signed if a SecureBoot signing key is + provided via the option and the binary has not already + been signed. If is specified, and the binary has already been signed, + the signature will be appended anyway. + + + + + + Specify a directory with helper tools. ukify will look for helper + tools in that directory first, and if not found, try to load them from $PATH in + the usual fashion. + + + + + + + Enable or disable a call to systmed-measure to print + pre-calculated PCR values. Defaults to false. + + + + + + The output filename. If not specified, the name of the + LINUX argument, with the suffix .unsigned.efi or + .signed.efi will be used, depending on whether signing for SecureBoot was + performed. + + + + + + + + + Examples + + + Minimal invocation + + ukify \ + /lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \ + /some/path/initramfs-6.0.9-300.fc37.x86_64.img \ + --cmdline='quiet rw' + + + This creates an unsigned UKI ./vmlinuz.unsigned.efi. + + + + All the bells and whistles + + /usr/lib/systemd/ukify \ + /lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \ + /some/path/initramfs-6.0.9-300.fc37.x86_64.img \ + --pcr-private-key=pcr-private-initrd-key.pem \ + --pcr-public-key=pcr-public-initrd-key.pem \ + --phases='enter-initrd' \ + --pcr-private-key=pcr-private-system-key.pem \ + --pcr-public-key=pcr-public-system-key.pem \ + --phases='enter-initrd:leave-initrd enter-initrd:leave-initrd:sysinit \ + enter-initrd:leave-initrd:sysinit:ready' \ + --pcr-banks=sha384,sha512 \ + --secureboot-private-key=sb.key \ + --secureboot-certificate=sb.cert \ + --sign-kernel \ + --cmdline='quiet rw rhgb' + + + This creates a signed UKI ./vmlinuz.signed.efi. + The policy embedded in the .pcrsig section will be signed for the initrd (the + enter-initrd phase) with the key + pcr-private-initrd-key.pem, and for the main system (phases + leave-initrd, sysinit, ready) with the + key pcr-private-system-key.pem. The Linux binary and the resulting + combined image will be signed with the SecureBoot key sb.key. + + + + + See Also + + systemd1, + systemd-stub7, + systemd-boot7, + objcopy1, + systemd-pcrphase.service1 + + + + From 1f6da5d902ef192c7fe24ffd4ebb3b9e1f2ecda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 25 Nov 2022 19:14:42 +0100 Subject: [PATCH 11/12] ci: install pefile --- .github/workflows/build_test.sh | 1 + .github/workflows/unit_tests.sh | 1 + README | 1 + src/ukify/test/test_ukify.py | 6 ++++++ src/ukify/ukify.py | 3 ++- 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_test.sh b/.github/workflows/build_test.sh index 1eed81600c..0d18c6c18b 100755 --- a/.github/workflows/build_test.sh +++ b/.github/workflows/build_test.sh @@ -53,6 +53,7 @@ PACKAGES=( python3-evdev python3-jinja2 python3-lxml + python3-pefile python3-pip python3-pyparsing python3-setuptools diff --git a/.github/workflows/unit_tests.sh b/.github/workflows/unit_tests.sh index 9a9fbb36ab..70ba090eb8 100755 --- a/.github/workflows/unit_tests.sh +++ b/.github/workflows/unit_tests.sh @@ -21,6 +21,7 @@ ADDITIONAL_DEPS=( libzstd-dev perl python3-libevdev + python3-pefile python3-pyparsing rpm zstd diff --git a/README b/README index d8c279f9fa..b12ad3f236 100644 --- a/README +++ b/README @@ -207,6 +207,7 @@ REQUIREMENTS: docbook-xsl (optional, required for documentation) xsltproc (optional, required for documentation) python-jinja2 + python-pefile python-lxml (optional, required to build the indices) python >= 3.5 meson >= 0.53.2 diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py index a4afa56ad9..48ffc6d495 100755 --- a/src/ukify/test/test_ukify.py +++ b/src/ukify/test/test_ukify.py @@ -20,6 +20,12 @@ try: except ImportError: sys.exit(77) +try: + # pyflakes: noqa + import pefile # noqa +except ImportError: + sys.exit(77) + # We import ukify.py, which is a template file. But only __version__ is # substituted, which we don't care about here. Having the .py suffix makes it # easier to import the file. diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index d4772d2bae..83423fc720 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -20,7 +20,6 @@ import subprocess import tempfile import typing -import pefile __version__ = '{{GIT_VERSION}}' @@ -66,6 +65,8 @@ def shell_join(cmd): def pe_executable_size(filename): + import pefile + pe = pefile.PE(filename) section = pe.sections[-1] return section.VirtualAddress + section.Misc_VirtualSize From 54c84c8a7a95f73af3a1cd5f53e49abc79244b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 26 Nov 2022 14:31:57 +0100 Subject: [PATCH 12/12] ukify: allow multiple initrds If given, multiple initrds are concatenated into a temporary file which then becomes the .initrd section. It is also possible to give no initrd. After all, some machines boot without an initrd, and it should be possible to use the stub without requiring an initrd. (The stub might not like this, but this is something to fix there.) --- man/ukify.xml | 11 ++++++++--- src/ukify/test/test_ukify.py | 18 ++++++++++++------ src/ukify/ukify.py | 31 ++++++++++++++++++++++++++----- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/man/ukify.xml b/man/ukify.xml index 3cc13a4cba..17546d543d 100644 --- a/man/ukify.xml +++ b/man/ukify.xml @@ -24,7 +24,7 @@ /usr/lib/systemd/ukify LINUX - INITRD + INITRD OPTIONS @@ -78,8 +78,10 @@ Options - Note that the LINUX and INITRD positional - arguments are mandatory. + Note that the LINUX positional argument is mandatory. The + INITRD positional arguments are optional. If more than one is specified, they + will all be combined into a single PE section. This is useful to for example prepend microcode before the + actual initrd. The following options are understood: @@ -268,6 +270,7 @@ /usr/lib/systemd/ukify \ /lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \ + early_cpio \ /some/path/initramfs-6.0.9-300.fc37.x86_64.img \ --pcr-private-key=pcr-private-initrd-key.pem \ --pcr-public-key=pcr-public-initrd-key.pem \ @@ -284,6 +287,8 @@ This creates a signed UKI ./vmlinuz.signed.efi. + The initrd section contains two concatenated parts, early_cpio + and initramfs-6.0.9-300.fc37.x86_64.img. The policy embedded in the .pcrsig section will be signed for the initrd (the enter-initrd phase) with the key pcr-private-initrd-key.pem, and for the main system (phases diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py index 48ffc6d495..34701402e5 100755 --- a/src/ukify/test/test_ukify.py +++ b/src/ukify/test/test_ukify.py @@ -49,13 +49,13 @@ def test_round_up(): def test_parse_args_minimal(): opts = ukify.parse_args('arg1 arg2'.split()) assert opts.linux == pathlib.Path('arg1') - assert opts.initrd == pathlib.Path('arg2') + assert opts.initrd == [pathlib.Path('arg2')] assert opts.os_release in (pathlib.Path('/etc/os-release'), pathlib.Path('/usr/lib/os-release')) def test_parse_args_many(): opts = ukify.parse_args( - ['/ARG1', '///ARG2', + ['/ARG1', '///ARG2', '/ARG3 WITH SPACE', '--cmdline=a b c', '--os-release=K1=V1\nK2=V2', '--devicetree=DDDDTTTT', @@ -77,7 +77,7 @@ def test_parse_args_many(): '--no-measure', ]) assert opts.linux == pathlib.Path('/ARG1') - assert opts.initrd == pathlib.Path('/ARG2') + assert opts.initrd == [pathlib.Path('/ARG2'), pathlib.Path('/ARG3 WITH SPACE')] assert opts.os_release == 'K1=V1\nK2=V2' assert opts.devicetree == pathlib.Path('DDDDTTTT') assert opts.splash == pathlib.Path('splash') @@ -103,7 +103,7 @@ def test_parse_sections(): ]) assert opts.linux == pathlib.Path('/ARG1') - assert opts.initrd == pathlib.Path('/ARG2') + assert opts.initrd == [pathlib.Path('/ARG2')] assert len(opts.sections) == 2 assert opts.sections[0].name == 'test' @@ -334,9 +334,13 @@ def test_pcr_signing2(kernel_initrd, tmpdir): pub2 = unbase64(ourdir / 'example.tpm2-pcr-public2.pem.base64') priv2 = unbase64(ourdir / 'example.tpm2-pcr-private2.pem.base64') + # simulate a microcode file + with open(f'{tmpdir}/microcode', 'wb') as microcode: + microcode.write(b'1234567890') + output = f'{tmpdir}/signed.efi' opts = ukify.parse_args([ - *kernel_initrd, + kernel_initrd[0], microcode.name, kernel_initrd[1], f'--output={output}', '--uname=1.2.3', '--cmdline=ARG1 ARG2 ARG3', @@ -367,7 +371,7 @@ def test_pcr_signing2(kernel_initrd, tmpdir): subprocess.check_call([ 'objcopy', *(f'--dump-section=.{n}={tmpdir}/out.{n}' for n in ( - 'pcrpkey', 'pcrsig', 'osrel', 'uname', 'cmdline')), + 'pcrpkey', 'pcrsig', 'osrel', 'uname', 'cmdline', 'initrd')), output, tmpdir / 'dummy', ], @@ -377,6 +381,8 @@ def test_pcr_signing2(kernel_initrd, tmpdir): assert open(tmpdir / 'out.osrel').read() == 'ID=foobar\n' assert open(tmpdir / 'out.uname').read() == '1.2.3' assert open(tmpdir / 'out.cmdline').read() == 'ARG1 ARG2 ARG3' + assert open(tmpdir / 'out.initrd', 'rb').read(10) == b'1234567890' + sig = open(tmpdir / 'out.pcrsig').read() sig = json.loads(sig) assert list(sig.keys()) == ['sha1'] diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 83423fc720..e9e5d13d13 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -206,8 +206,9 @@ class Section: @classmethod def create(cls, name, contents, flags=None, measure=False): - if isinstance(contents, str): - tmp = tempfile.NamedTemporaryFile(mode='wt', prefix=f'tmp{name}') + if isinstance(contents, str | bytes): + mode = 'wt' if isinstance(contents, str) else 'wb' + tmp = tempfile.NamedTemporaryFile(mode=mode, prefix=f'tmp{name}') tmp.write(contents) tmp.flush() contents = pathlib.Path(tmp.name) @@ -404,6 +405,24 @@ def call_systemd_measure(uki, linux, opts): uki.add_section(Section.create('.pcrsig', combined)) +def join_initrds(initrds): + match initrds: + case []: + return None + case [initrd]: + return initrd + case multiple: + seq = [] + for file in multiple: + initrd = file.read_bytes() + padding = b'\0' * round_up(len(initrd), 4) # pad to 32 bit alignment + seq += [initrd, padding] + + return b''.join(seq) + + assert False + + def make_uki(opts): # kernel payload signing @@ -455,6 +474,7 @@ def make_uki(opts): opts.uname = Uname.scrape(opts.linux, opts=opts) uki = UKI(opts.stub) + initrd = join_initrds(opts.initrd) # TODO: derive public key from from opts.pcr_private_keys? pcrpkey = opts.pcrpkey @@ -469,7 +489,7 @@ def make_uki(opts): ('.dtb', opts.devicetree, True ), ('.splash', opts.splash, True ), ('.pcrpkey', pcrpkey, True ), - ('.initrd', opts.initrd, True ), + ('.initrd', initrd, True ), ('.uname', opts.uname, False), # linux shall be last to leave breathing room for decompression. @@ -541,7 +561,7 @@ def parse_args(args=None): description='Build and sign Unified Kernel Images', allow_abbrev=False, usage='''\ -usage: ukify [options…] linux initrd +usage: ukify [options…] linux initrd… ukify -h | --help ''') @@ -553,7 +573,8 @@ usage: ukify [options…] linux initrd help='vmlinuz file [.linux section]') p.add_argument('initrd', type=pathlib.Path, - help='initrd file [.initrd section]') + nargs='*', + help='initrd files [.initrd section]') p.add_argument('--cmdline', metavar='TEXT|@PATH',