diff options
author | Lennart Poettering <lennart@poettering.net> | 2022-12-08 15:11:18 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-08 15:11:18 +0100 |
commit | a5799902771dd8601dd933afae6cb6b5307a5d0e (patch) | |
tree | ab3d9adedec3dffddb368984eac6010d5330d25d /src | |
parent | 0254e4d66af7aa893b31b2326335ded5dde48b51 (diff) | |
parent | 54c84c8a7a95f73af3a1cd5f53e49abc79244b3f (diff) | |
download | systemd-a5799902771dd8601dd933afae6cb6b5307a5d0e.tar.gz |
Merge pull request #25180 from keszybz/ukify
ukify: add helper to create UKIs
Diffstat (limited to 'src')
-rw-r--r-- | src/shared/bootspec.c | 8 | ||||
-rw-r--r-- | src/shared/json.c | 178 | ||||
-rw-r--r-- | src/shared/json.h | 15 | ||||
-rw-r--r-- | src/test/test-json.c | 63 | ||||
-rw-r--r-- | src/ukify/test/example.signing.crt.base64 | 23 | ||||
-rw-r--r-- | src/ukify/test/example.signing.key.base64 | 30 | ||||
-rw-r--r-- | src/ukify/test/example.tpm2-pcr-private.pem.base64 | 30 | ||||
-rw-r--r-- | src/ukify/test/example.tpm2-pcr-private2.pem.base64 | 30 | ||||
-rw-r--r-- | src/ukify/test/example.tpm2-pcr-public.pem.base64 | 8 | ||||
-rw-r--r-- | src/ukify/test/example.tpm2-pcr-public2.pem.base64 | 8 | ||||
-rw-r--r-- | src/ukify/test/meson.build | 7 | ||||
-rw-r--r-- | src/ukify/test/setup.cfg | 2 | ||||
-rwxr-xr-x | src/ukify/test/test_ukify.py | 392 | ||||
-rwxr-xr-x | src/ukify/ukify.py | 727 |
14 files changed, 1468 insertions, 53 deletions
diff --git a/src/shared/bootspec.c b/src/shared/bootspec.c index 83960b99d3..4cced23adc 100644 --- a/src/shared/bootspec.c +++ b/src/shared/bootspec.c @@ -1404,6 +1404,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; @@ -1443,9 +1445,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 | JSON_FORMAT_EMPTY_ARRAY, NULL, NULL); + } else { for (size_t n = 0; n < config->n_entries; n++) { r = show_boot_entry( diff --git a/src/shared/json.c b/src/shared/json.c index 94d7b31557..b1ef0ed349 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; @@ -1790,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; @@ -2072,28 +2096,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)); @@ -3180,16 +3230,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 +3292,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..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); @@ -222,8 +223,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); 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..34701402e5 --- /dev/null +++ b/src/ukify/test/test_ukify.py @@ -0,0 +1,392 @@ +#!/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) + +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. +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', '/ARG3 WITH SPACE', + '--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'), 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') + 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_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') + 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') + + # 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[0], microcode.name, kernel_initrd[1], + 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', 'initrd')), + 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' + 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'] + assert len(sig['sha1']) == 6 # six items for six phases paths + +if __name__ == '__main__': + pytest.main([__file__, '-v']) diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py new file mode 100755 index 0000000000..e9e5d13d13 --- /dev/null +++ b/src/ukify/ukify.py @@ -0,0 +1,727 @@ +#!/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 + + +__version__ = '{{GIT_VERSION}}' + +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): + import pefile + + 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 + + +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<version>[a-z0-9._-]+) \([^ )]+\) (?:#.*)' + + NOTES_PATTERN = r'^\s+Linux\s+0x[0-9a-f]+\s+OPEN\n\s+description data: (?P<version>[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<version>\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 + 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 | 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) + 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 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 + + 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 + + if opts.uname is None: + print('Kernel version not specified, starting autodetection 😖.') + 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 + 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', 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, + nargs='*', + help='initrd files [.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') + + p.add_argument('--version', + action='version', + version=f'ukify {__version__}') + + 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() |