From f4bff983b267584d51585b8b34fea06d21b132b8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 22 Sep 2026 15:07:23 +0200 Subject: [PATCH] repart: optionally write UAPI.16 manifests of populated partitions and of the partition table Modelled after SplitName=/--split=: a new ManifestName= setting (defaulting to "%t", "-" disables) plus --manifest= make systemd-repart write a UAPI.16 manifest for each partition whose file system tree it populates. The manifest is generated right when the tree is populated (be it in the staging directory for mkfs --root style population, or in the child process that copies into the mounted file system), into an O_TMPFILE file in the image's directory, which is linked into place once the partition UUIDs and hence the final name are known, next to the image as ..Uapi16Manifest. The result is what systemd-manifest would generate for the tree, and can hence be verified with it. In addition --manifest-gpt= writes a manifest of the partition table itself as .Uapi16Manifest: one object per partition, carrying the GPT type, UUID, label and flags, the partition size and the SHA256 digest of its contents, and referencing the partition data by its offset in the image in the "contents" array. Partitions are named like their split-out files, whether or not --split= is used. For verity signature partitions the size and digest cover the signature JSON object only, not the NUL padding after it, matching the (trimmed) split-out file. This is the format sysupdate consumes for partition resources. It also carries an object for the image as a whole, named like the image file, with the size and SHA256 digest of the complete disk. The partition table manifest is written to a temporary file first, and linked into place once complete. Incomplete manifests (i.e. where inodes had to be skipped or metadata omitted) are refused, as systemd-manifest does without --graceful. In dry-run mode no manifests are written. The manifest paths show up in the partition table dump, as the split paths do. --manifest-fields= selects the per-file fields to include in the manifests of the partitions' file system trees, like --field= of systemd-manifest does. If --manifest-private-key= and --manifest-certificate= (plus optionally --manifest-private-key-source= and --manifest-certificate-source=) are specified, all generated manifests are signed with a PKCS#7 signature object. These are deliberately separate from --private-key= and --certificate=, which remain in use for verity signature partitions only. All manifest related switches are grouped in a section of their own in --help and the man page. --- man/repart.d.xml | 26 +- man/systemd-manifest.xml | 2 + man/systemd-repart.xml | 99 +++- src/repart/repart.c | 935 +++++++++++++++++++++++++++++++++-- test/units/TEST-58-REPART.sh | 73 +++ 5 files changed, 1084 insertions(+), 51 deletions(-) diff --git a/man/repart.d.xml b/man/repart.d.xml index ceda2dd6ffb..42f5cc89d86 100644 --- a/man/repart.d.xml +++ b/man/repart.d.xml @@ -994,6 +994,25 @@ + + ManifestName= + + Configures the suffix to append to the manifest file written for the partition when + the option of + systemd-repart8 is + used. The manifest describes the file system tree the partition was populated with (via + CopyFiles=, MakeDirectories=, MakeSymlinks=), + in the UAPI.16 File + Manifest format, i.e. what + systemd-manifest1 + would generate for the tree. Simple specifier expansion is supported, see below, the same + specifiers as for SplitName= are understood. Defaults to %t. + To disable manifest generation for a partition, set ManifestName= to + -. + + + + Minimize= @@ -1288,7 +1307,8 @@ Specifiers may be used in the Label=, CopyBlocks=, CopyFiles=, MakeDirectories=, MakeSymlinks=, - SplitName= settings. The following expansions are understood: + SplitName=, ManifestName= settings. The following expansions are + understood: Specifiers available @@ -1323,8 +1343,8 @@
- Additionally, for the SplitName= setting, the following specifiers are also - understood: + Additionally, for the SplitName= and ManifestName= settings, + the following specifiers are also understood: Specifiers available diff --git a/man/systemd-manifest.xml b/man/systemd-manifest.xml index 75ebe499faa..57d0117320b 100644 --- a/man/systemd-manifest.xml +++ b/man/systemd-manifest.xml @@ -274,6 +274,8 @@ The same default set of fields is used by systemd-dissect1 + and + systemd-repart8 when generating manifests. diff --git a/man/systemd-repart.xml b/man/systemd-repart.xml index 8677bdbfad7..53cca388638 100644 --- a/man/systemd-repart.xml +++ b/man/systemd-repart.xml @@ -371,7 +371,8 @@ Takes a file system path or an engine or provider specific designation. Configures the signing key to use when creating verity signature partitions with the - Verity=signature setting in partition files. + Verity=signature setting in partition files. It is not used for signing + manifests, see below for that. @@ -393,7 +394,8 @@ Takes a file system path or a provider specific designation. Configures the PEM encoded X.509 certificate to use when creating verity signature partitions with the - Verity=signature setting in partition files. + Verity=signature setting in partition files. It is not used for signing + manifests, see below for that. @@ -801,6 +803,99 @@ + + + Manifest Generation Options + + The following options control generation of UAPI.16 File Manifest files + describing the generated image: + + + + + + Enables generation of UAPI.16 File Manifest files + for partitions configured with ManifestName=. If enabled, for each partition that + is populated with a file system tree (see CopyFiles= and related settings in + repart.d5) a + manifest of that tree is written, as + systemd-manifest1 + would generate it, which may later be used to verify the contents of the partition. The output file + name consists of the image file name suffixed with the name configured with + ManifestName= and a .Uapi16Manifest extension, replacing a + .raw extension of the image file name if there is one. Partitions that are not + populated in this invocation (for example because they already exist) get no manifest. + + + + + + + + Enables generation of a UAPI.16 manifest describing the partition table of the image. + If enabled, a manifest with one object per partition is written, carrying the partition's GPT type, + UUID, label and flags, its size and the SHA256 digest of its contents, and referencing the + partition's data by its offset in the image file in the object's contents array. + Partitions are named like their split artifacts (see SplitName= in + repart.d5), + regardless of whether is used; partitions with SplitName= + turned off are named after their partition number. For verity signature partitions, the size and + digest cover only the signature JSON object, not the NUL padding following it in the partition, + matching the split artifact (unless the partition holds no signature at all, in which case the whole + partition is covered). Nothing is written in dry-run mode. In addition, an object for the image as a whole is + included, named like the image file, carrying the size and SHA256 digest of the complete disk + (including the partition table). The output file name is the image file name with a + .Uapi16Manifest extension, replacing a .raw extension if there is + one. Such a manifest is suitable for consumption by + systemd-sysupdate8, + see ManifestFormat= in + sysupdate.d5. + + + + + + + + Selects the per-file fields to include in the manifests of populated partitions + generated with . Takes a comma separated list of field names, the special + values , and , and items prefixed + with + or - to add or remove fields from the current set, in + the same way as the switch of + systemd-manifest1, + see there for details. Defaults to the same set as systemd-manifest, see there + for which fields are included by default and which are not (in particular, owning user and group + names and modification times are not). Has no effect + on the partition table manifest generated with , whose fields are + fixed. + + + + + + + + + + + Configure the private key and X.509 certificate to sign the manifests generated with + and with. If + is specified the manifests are signed with a PKCS#7 signature + object as described in the specification, in the same way as + systemd-manifest1 + signs them, otherwise they are not signed. If one of and + is specified the other must be specified too. The options + take the same arguments as , + , and + , but are independent of them: the latter are used for verity + signature partitions only, the former for manifests only. + + + + + diff --git a/src/repart/repart.c b/src/repart/repart.c index 540de6c9e13..7128913d780 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -56,6 +56,7 @@ #include "list.h" #include "loop-util.h" #include "main-func.h" +#include "manifest-util.h" #include "mkdir.h" #include "mkfs-util.h" #include "mount-util.h" @@ -73,6 +74,7 @@ #include "resize-fs.h" #include "rm-rf.h" #include "set.h" +#include "sha256.h" #include "sort-util.h" #include "specifier.h" #include "string-table.h" @@ -84,6 +86,7 @@ #include "tmpfile-util.h" #include "tpm2-pcr.h" #include "tpm2-util.h" +#include "uapi16-manifest.h" #include "utf8.h" #include "varlink-io.systemd.Repart.h" #include "varlink-util.h" @@ -196,6 +199,15 @@ static char *arg_tpm2_public_key_policyref = NULL; static uint32_t arg_tpm2_public_key_pcr_mask = 0; static char *arg_tpm2_pcrlock = NULL; static bool arg_split = false; +static bool arg_manifest = false; +static bool arg_manifest_gpt = false; +static ManifestField arg_manifest_fields = _MANIFEST_FIELD_DEFAULT; +static char *arg_manifest_private_key = NULL; +static KeySourceType arg_manifest_private_key_source_type = OPENSSL_KEY_SOURCE_FILE; +static char *arg_manifest_private_key_source = NULL; +static char *arg_manifest_certificate = NULL; +static CertificateSourceType arg_manifest_certificate_source_type = OPENSSL_CERTIFICATE_SOURCE_FILE; +static char *arg_manifest_certificate_source = NULL; static GptPartitionType *arg_filter_partitions = NULL; static size_t arg_n_filter_partitions = 0; static FilterPartitionsType arg_filter_partitions_type = FILTER_PARTITIONS_NONE; @@ -232,6 +244,10 @@ STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_private_key_source, freep); STATIC_DESTRUCTOR_REGISTER(arg_certificate, freep); STATIC_DESTRUCTOR_REGISTER(arg_certificate_source, freep); +STATIC_DESTRUCTOR_REGISTER(arg_manifest_private_key, freep); +STATIC_DESTRUCTOR_REGISTER(arg_manifest_private_key_source, freep); +STATIC_DESTRUCTOR_REGISTER(arg_manifest_certificate, freep); +STATIC_DESTRUCTOR_REGISTER(arg_manifest_certificate_source, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_hash_pcr_values, freep); @@ -562,6 +578,12 @@ typedef struct Partition { char *split_name_format; char *split_path; + char *manifest_name_format; + char *manifest_path; + bool manifest_linked; /* Whether we created the file at manifest_path, and hence shall remove it on failure */ + int manifest_fd; + char *manifest_tmp_path; /* Only set if O_TMPFILE is not available */ + PartitionMountPoint *mountpoints; size_t n_mountpoints; @@ -605,6 +627,9 @@ struct Context { struct fdisk_context *fdisk_context; int fdisk_context_fd; uint64_t sector_size, grain_size, default_fs_sector_size; + sd_id128_t disk_uuid; + + char *manifest_gpt_path; /* Removed again on failure, unless disarmed */ sd_id128_t seed; @@ -618,9 +643,15 @@ struct Context { bool from_scratch; #if HAVE_OPENSSL + /* For signing verity signature partitions */ X509 *certificate; OpenSSLAskPasswordUI *ui; EVP_PKEY *private_key; + + /* For signing UAPI.16 manifests */ + X509 *manifest_certificate; + OpenSSLAskPasswordUI *manifest_ui; + EVP_PKEY *manifest_private_key; #endif bool defer_partitions_empty; @@ -806,6 +837,7 @@ static Partition *partition_new(Context *c) { .partno = UINT64_MAX, .offset = UINT64_MAX, .copy_blocks_fd = -EBADF, + .manifest_fd = -EBADF, .copy_blocks_offset = UINT64_MAX, .copy_blocks_size = UINT64_MAX, .no_auto = -1, @@ -891,6 +923,14 @@ static Partition* partition_free(Partition *p) { free(p->split_name_format); unlink_and_free(p->split_path); + + free(p->manifest_name_format); + if (p->manifest_linked) + unlink_and_free(p->manifest_path); + else + free(p->manifest_path); + safe_close(p->manifest_fd); + unlink_and_free(p->manifest_tmp_path); partition_mountpoint_free_many(p->mountpoints, p->n_mountpoints); p->mountpoints = NULL; p->n_mountpoints = 0; @@ -1056,12 +1096,22 @@ static Context* context_free(Context *context) { safe_close(context->fdisk_context_fd); safe_close(context->backing_fd); + unlink_and_free(context->manifest_gpt_path); if (context->node_is_our_file) unlink_and_free(context->node); else free(context->node); #if HAVE_OPENSSL + /* NB: the order matters here! Each OpenSSLAskPasswordUI installs itself as the default UI method, + * and openssl_ask_password_ui_free() insists it still is the default when it is freed, and then + * restores the previous default. Hence free them in the reverse order of their creation in + * context_load_keys(), i.e. the manifest key's UI before the verity key's UI. */ + if (context->manifest_certificate) + sym_X509_free(context->manifest_certificate); + openssl_ask_password_ui_free(context->manifest_ui); + if (context->manifest_private_key) + sym_EVP_PKEY_free(context->manifest_private_key); if (context->certificate) sym_X509_free(context->certificate); openssl_ask_password_ui_free(context->ui); @@ -1081,9 +1131,12 @@ static void context_disarm_auto_removal(Context *context) { /* Make sure automatic removal of half-written artifacts is disarmed */ context->node = mfree(context->node); + context->manifest_gpt_path = mfree(context->manifest_gpt_path); - LIST_FOREACH(partitions, p, context->partitions) + LIST_FOREACH(partitions, p, context->partitions) { p->split_path = mfree(p->split_path); + p->manifest_path = mfree(p->manifest_path); + } } static int context_add_free_area( @@ -3174,6 +3227,7 @@ static int partition_read_definition( { "Partition", "NoAuto", config_parse_tristate, 0, &p->no_auto }, { "Partition", "GrowFileSystem", config_parse_tristate, 0, &p->growfs }, { "Partition", "SplitName", config_parse_string, 0, &p->split_name_format }, + { "Partition", "ManifestName", config_parse_string, 0, &p->manifest_name_format }, { "Partition", "Minimize", config_parse_minimize, 0, &p->minimize }, { "Partition", "Subvolumes", config_parse_subvolumes, 0, &p->subvolumes }, { "Partition", "DefaultSubvolume", config_parse_default_subvolume, 0, &p->default_subvolume }, @@ -3423,6 +3477,15 @@ static int partition_read_definition( } else if (streq(p->split_name_format, "-")) p->split_name_format = mfree(p->split_name_format); + if (!p->manifest_name_format) { + char *s = strdup("%t"); + if (!s) + return log_oom(); + + p->manifest_name_format = s; + } else if (streq(p->manifest_name_format, "-")) + p->manifest_name_format = mfree(p->manifest_name_format); + r = partition_finalize_fstype(p, path); if (r < 0) return r; @@ -3674,6 +3737,10 @@ static int context_copy_from_one(Context *context, const char *src) { if (!np->split_name_format) return log_oom(); + np->manifest_name_format = strdup("%t"); + if (!np->manifest_name_format) + return log_oom(); + /* Pass grain size of 1 to disable rounding by grain as we don't know the grain size * of the old image. We'll round paddings to the grain size of the new image later. */ r = determine_current_padding(c, t, p, secsz, /* grainsz= */ 1, &padding, &is_last_partition); @@ -4368,6 +4435,7 @@ add_initial_free_area: context->start = first_lba; context->end = last_lba; context->total = nsectors; + context->disk_uuid = disk_uuid; context->sector_size = secsz; context->default_fs_sector_size = fs_secsz; context->grain_size = grainsz; @@ -4424,6 +4492,7 @@ static void context_unload_partition_table(Context *context) { context->start = UINT64_MAX; context->end = UINT64_MAX; context->total = UINT64_MAX; + context->disk_uuid = SD_ID128_NULL; if (context->fdisk_context) { sym_fdisk_unref_context(context->fdisk_context); @@ -4506,8 +4575,8 @@ static int context_dump_partitions(Context *context) { _cleanup_(table_unrefp) Table *t = NULL; uint64_t sum_padding = 0, sum_size = 0; int r; - const size_t roothash_col = 14, dropin_files_col = 15, split_path_col = 16; - bool has_roothash = false, has_dropin_files = false, has_split_path = false; + const size_t roothash_col = 14, dropin_files_col = 15, split_path_col = 16, manifest_path_col = 17; + bool has_roothash = false, has_dropin_files = false, has_split_path = false, has_manifest_path = false; if (context->n_partitions == 0 && !sd_json_format_enabled(arg_json_format_flags)) { log_info("Empty partition table."); @@ -4530,7 +4599,8 @@ static int context_dump_partitions(Context *context) { "activity", "roothash", "drop-in files", - "split path"); + "split path", + "manifest path"); if (!t) return log_oom(); @@ -4545,12 +4615,12 @@ static int context_dump_partitions(Context *context) { if (!sd_json_format_enabled(arg_json_format_flags)) (void) table_set_display(t, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 4, (size_t) 8, (size_t) 9, (size_t) 12, roothash_col, dropin_files_col, - split_path_col); + split_path_col, manifest_path_col); else (void) table_set_display(t, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 4, (size_t) 5, (size_t) 6, (size_t) 7, (size_t) 8, (size_t) 10, (size_t) 11, (size_t) 13, roothash_col, dropin_files_col, - split_path_col); + split_path_col, manifest_path_col); } (void) table_set_align_percent(t, table_get_cell(t, /* row= */ 0, 3), 100); @@ -4627,13 +4697,15 @@ static int context_dump_partitions(Context *context) { TABLE_STRING, activity ?: "unchanged", TABLE_STRING, rh, TABLE_STRV, p->drop_in_files, - TABLE_STRING, empty_to_null(p->split_path) ?: "-"); + TABLE_STRING, empty_to_null(p->split_path) ?: "-", + TABLE_STRING, empty_to_null(p->manifest_path) ?: "-"); if (r < 0) return table_log_add_error(r); has_roothash = has_roothash || !isempty(rh); has_dropin_files = has_dropin_files || !strv_isempty(p->drop_in_files); has_split_path = has_split_path || !isempty(p->split_path); + has_manifest_path = has_manifest_path || !isempty(p->manifest_path); } if (!sd_json_format_enabled(arg_json_format_flags) && (sum_padding > 0 || sum_size > 0)) { @@ -4660,6 +4732,7 @@ static int context_dump_partitions(Context *context) { TABLE_EMPTY, TABLE_EMPTY, TABLE_EMPTY, + TABLE_EMPTY, TABLE_EMPTY); if (r < 0) return table_log_add_error(r); @@ -4683,6 +4756,12 @@ static int context_dump_partitions(Context *context) { return log_error_errno(r, "Failed to set columns to display: %m"); } + if (!has_manifest_path) { + r = table_hide_column_from_display(t, manifest_path_col); + if (r < 0) + return log_error_errno(r, "Failed to set columns to display: %m"); + } + return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); } @@ -7462,6 +7541,103 @@ static int do_make_validatefs_xattrs(const Partition *p, const char *root) { return 0; } +static bool partition_wants_manifest(const Partition *p) { + assert(p); + + return arg_manifest && p->manifest_name_format; +} + +static int partition_manifest_prepare(Context *context, Partition *p) { + assert(context); + assert(p); + + /* Sets up the temporary file we collect the manifest of the partition's file system tree in. This + * must happen in the parent process, since the tree might be populated in a forked off child (see + * partition_populate_filesystem()), which shall write into the very same file. + * + * The final name of the manifest is only known late (see manifest_name_resolve()), but its + * directory is the one of the image. Hence create the file there right-away, so that we can + * link it into place under its final name once we know it. + * + * The manifest is only generated when the final tree is populated, which happens exactly once per + * partition: for passes that just determine the minimal size (Minimize=guess) no manifest is + * generated. */ + + if (!partition_wants_manifest(p)) + return 0; + + assert(p->manifest_fd < 0); + assert(!p->manifest_tmp_path); + + p->manifest_fd = open_tmpfile_linkable(context->node, O_RDWR|O_CLOEXEC, &p->manifest_tmp_path); + if (p->manifest_fd < 0) + return log_error_errno(p->manifest_fd, "Failed to create temporary file for manifest: %m"); + + return 0; +} + +static int partition_manifest_write(Context *context, Partition *p, const char *root) { + int r; + + assert(context); + assert(p); + assert(root); + + /* Generates a UAPI.16 manifest of the (fully populated) file system tree at 'root' into the temporary + * file set up by partition_manifest_prepare(). Called from whichever process populated the tree. */ + + if (!partition_wants_manifest(p)) + return 0; + + assert(p->manifest_fd >= 0); + + log_info("Generating manifest of %s filesystem.", p->format); + + _cleanup_close_ int dir_fd = open(root, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (dir_fd < 0) + return log_error_errno(errno, "Failed to open populated tree '%s': %m", root); + + _cleanup_close_ int dup_fd = fcntl(p->manifest_fd, F_DUPFD_CLOEXEC, 3); + if (dup_fd < 0) + return log_error_errno(errno, "Failed to duplicate manifest file descriptor: %m"); + + _cleanup_fclose_ FILE *f = take_fdopen(&dup_fd, "w"); + if (!f) + return log_error_errno(errno, "Failed to allocate FILE object for manifest: %m"); + + _cleanup_(manifest_context_done) ManifestContext c = MANIFEST_CONTEXT_NULL; + c.format = MANIFEST_FORMAT_UAPI16; + c.fields = arg_manifest_fields; + c.output = f; +#if HAVE_OPENSSL + c.certificate = context->manifest_certificate; + c.private_key = context->manifest_private_key; +#endif + + r = manifest_make(&c, dir_fd); + if (r < 0) + return log_error_errno(r, "Failed to generate manifest: %m"); + + /* Refuse incomplete manifests, the same way systemd-manifest does (without --graceful) */ + if (!c.top_level_covered) + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Top-level inode not covered by manifest, refusing."); + uint64_t n_failed = c.n_skipped - c.n_skipped_expected; + if (n_failed > 0) + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), + "%" PRIu64 " entries skipped while generating manifest, as they could not be read or were not representable in the manifest, refusing.", + n_failed); + if (c.n_metadata_omitted > 0) + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), + "%" PRIu64 " metadata fields omitted while generating manifest, as they could not be read or were not representable in the manifest, refusing.", + c.n_metadata_omitted); + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to write manifest: %m"); + + return 0; +} + static int partition_populate_directory(Context *context, Partition *p, char **ret) { _cleanup_(rm_rf_physical_and_freep) char *root = NULL; const char *vt; @@ -7514,18 +7690,29 @@ static int partition_populate_directory(Context *context, Partition *p, char **r if (r < 0) return r; + r = partition_manifest_prepare(context, p); + if (r < 0) + return r; + + r = partition_manifest_write(context, p, root); + if (r < 0) + return r; + log_info("Ready to populate %s filesystem.", p->format); *ret = TAKE_PTR(root); return 0; } -static int partition_populate_filesystem(Context *context, Partition *p, const char *node) { +static int partition_populate_filesystem(Context *context, Partition *p, const char *node, bool final) { int r; assert(p); assert(node); + /* If 'final' is false, the file system is only populated to determine its minimal size, and is not + * what ends up in the image. In that case no manifest is generated. */ + log_info("Populating %s filesystem.", p->format); /* We copy in a child process, since we have to mount the fs for that, and we don't want that fs to @@ -7534,6 +7721,12 @@ static int partition_populate_filesystem(Context *context, Partition *p, const c (void) dlopen_libmount(LOG_DEBUG); + if (final) { + r = partition_manifest_prepare(context, p); + if (r < 0) + return r; + } + r = pidref_safe_fork( "(sd-copy)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, @@ -7571,6 +7764,9 @@ static int partition_populate_filesystem(Context *context, Partition *p, const c if (set_default_subvolume(p, fs) < 0) _exit(EXIT_FAILURE); + if (final && partition_manifest_write(context, p, fs) < 0) + _exit(EXIT_FAILURE); + r = syncfs_path(AT_FDCWD, fs); if (r < 0) { log_error_errno(r, "Failed to synchronize written files: %m"); @@ -7914,7 +8110,7 @@ static int context_mkfs(Context *context) { if (partition_needs_populate(p) && !root) { assert(t->loop || t->block_partition); - r = partition_populate_filesystem(context, p, partition_target_path(t)); + r = partition_populate_filesystem(context, p, partition_target_path(t), /* final= */ true); if (r < 0) return r; } @@ -8344,20 +8540,31 @@ static int context_mangle_partitions(Context *context) { return 0; } -static int split_name_printf(Partition *p, char **ret) { +static int partition_name_printf(Partition *p, const char *format, bool placeholder_uuid, char **ret) { + _cleanup_free_ char *placeholder = NULL; + assert(p); + assert(format); + + /* If 'placeholder_uuid' is true and the partition UUID is not known yet (which is the case for verity + * partitions until they have been generated), %U is resolved to a placeholder that is unique to the + * partition, as the final UUID will be. This is useful to check for name collisions early. */ + if (placeholder_uuid && sd_id128_is_null(p->new_uuid) && + asprintf(&placeholder, "unknown-uuid-%" PRIu64, p->partno) < 0) + return -ENOMEM; const Specifier table[] = { { 't', specifier_string, GPT_PARTITION_TYPE_UUID_TO_STRING_HARDER(p->type.uuid) }, { 'T', specifier_id128, &p->type.uuid }, - { 'U', specifier_id128, &p->new_uuid }, + { 'U', placeholder ? specifier_string : specifier_id128, + placeholder ?: (const void*) &p->new_uuid }, { 'n', specifier_uint64, &p->partno }, COMMON_SYSTEM_SPECIFIERS, {} }; - return specifier_printf(p->split_name_format, NAME_MAX, table, arg_root, p, ret); + return specifier_printf(format, NAME_MAX, table, arg_root, p, ret); } static int split_node(const char *node, char **ret_base, char **ret_ext) { @@ -8390,11 +8597,24 @@ static int split_node(const char *node, char **ret_base, char **ret_ext) { return 0; } -static int split_name_resolve(Context *context) { - _cleanup_free_ char *parent = NULL, *base = NULL, *ext = NULL; +static int partition_name_path( + Context *context, + Partition *p, + const char *format, + const char *suffix, + bool placeholder_uuid, + char **ret) { + _cleanup_free_ char *parent = NULL, *base = NULL, *ext = NULL, *resolved = NULL; int r; assert(context); + assert(p); + assert(format); + assert(ret); + + /* Determines the path of an artifact generated for the partition, placed next to the image: named + * like the image (minus a ".raw" suffix), followed by a dot, the resolved format string and the + * suffix. If 'suffix' is NULL, the ".raw" suffix of the image (if it has one) is used instead. */ r = path_extract_directory(context->node, &parent); if (r < 0 && r != -EDESTADDRREQ) @@ -8404,8 +8624,55 @@ static int split_name_resolve(Context *context) { if (r < 0) return r; + r = partition_name_printf(p, format, placeholder_uuid, &resolved); + if (r < 0) + return log_error_errno(r, "Failed to resolve specifiers in %s: %m", format); + + const char *s = suffix ?: strempty(ext); + char *path = parent ? strjoin(parent, "/", base, ".", resolved, s) : strjoin(base, ".", resolved, s); + if (!path) + return log_oom(); + + *ret = path; + return 0; +} + +static int context_refuse_duplicate_paths(Context *context, size_t offset, const char *what) { + assert(context); + assert(what); + + /* Refuses if two partitions have the same path in the char* field at the specified offset of the + * Partition structure */ + LIST_FOREACH(partitions, p, context->partitions) { - _cleanup_free_ char *resolved = NULL; + const char *a = *(char**) ((uint8_t*) p + offset); + if (!a) + continue; + + LIST_FOREACH(partitions, q, context->partitions) { + if (p == q) + continue; + + const char *b = *(char**) ((uint8_t*) q + offset); + if (!streq_ptr(a, b)) + continue; + + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "%s and %s have the same resolved %s name \"%s\", refusing.", + p->definition_path, q->definition_path, what, a); + } + } + + return 0; +} + +static int split_name_resolve(Context *context) { + int r; + + assert(context); + + LIST_FOREACH(partitions, p, context->partitions) { + _cleanup_free_ char *path = NULL; if (p->dropped) continue; @@ -8413,39 +8680,14 @@ static int split_name_resolve(Context *context) { if (!p->split_name_format) continue; - r = split_name_printf(p, &resolved); + r = partition_name_path(context, p, p->split_name_format, /* suffix= */ NULL, /* placeholder_uuid= */ false, &path); if (r < 0) - return log_error_errno(r, "Failed to resolve specifiers in %s: %m", p->split_name_format); + return r; - if (parent) - p->split_path = strjoin(parent, "/", base, ".", resolved, ext); - else - p->split_path = strjoin(base, ".", resolved, ext); - if (!p->split_path) - return log_oom(); + free_and_replace(p->split_path, path); } - LIST_FOREACH(partitions, p, context->partitions) { - if (!p->split_path) - continue; - - LIST_FOREACH(partitions, q, context->partitions) { - if (p == q) - continue; - - if (!q->split_path) - continue; - - if (!streq(p->split_path, q->split_path)) - continue; - - return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), - "%s and %s have the same resolved split name \"%s\", refusing.", - p->definition_path, q->definition_path, p->split_path); - } - } - - return 0; + return context_refuse_duplicate_paths(context, offsetof(Partition, split_path), "split"); } static int context_split(Context *context) { @@ -8525,6 +8767,468 @@ static int context_split(Context *context) { return 0; } +static int manifest_gpt_path(Context *context, char **ret) { + _cleanup_free_ char *parent = NULL, *base = NULL, *ext = NULL; + int r; + + assert(context); + assert(ret); + + /* The manifest of the partition table is placed next to the image, named like it, with a + * ".Uapi16Manifest" suffix (replacing a ".raw" suffix if there is one) */ + + r = path_extract_directory(context->node, &parent); + if (r < 0 && r != -EDESTADDRREQ) + return log_error_errno(r, "Failed to extract directory from %s: %m", context->node); + + r = split_node(context->node, &base, &ext); + if (r < 0) + return r; + + char *path = parent ? strjoin(parent, "/", base, UAPI16_MANIFEST_SUFFIX) : strjoin(base, UAPI16_MANIFEST_SUFFIX); + if (!path) + return log_oom(); + + *ret = path; + return 0; +} + +static int manifest_name_resolve(Context *context) { + int r; + + assert(context); + + /* Resolves the file names of the manifests of the partitions' file system trees, next to the image: + * named like the image, with the resolved ManifestName= and ".Uapi16Manifest" appended. */ + + LIST_FOREACH(partitions, p, context->partitions) { + _cleanup_free_ char *path = NULL; + + if (p->dropped) + continue; + + if (!partition_wants_manifest(p)) + continue; + + if (partition_defer(context, p)) + continue; + + /* No tree was populated for this partition in this run, hence there's nothing to write + * (and nothing that could collide) */ + if (p->manifest_fd < 0) + continue; + + r = partition_name_path(context, p, p->manifest_name_format, UAPI16_MANIFEST_SUFFIX, /* placeholder_uuid= */ false, &path); + if (r < 0) + return r; + + free_and_replace(p->manifest_path, path); + } + + return context_refuse_duplicate_paths(context, offsetof(Partition, manifest_path), "manifest"); +} + +static int hash_range(int fd, uint64_t offset, uint64_t size, uint8_t ret[static SHA256_DIGEST_SIZE]) { + struct sha256_ctx ctx; + + assert(fd >= 0); + assert(ret); + + sha256_init_ctx(&ctx); + + for (uint64_t pos = offset, end = offset + size; pos < end;) { + uint8_t buffer[64U * 1024U]; + ssize_t n; + + n = pread(fd, buffer, MIN(end - pos, sizeof(buffer)), pos); + if (n < 0) { + if (errno == EINTR) + continue; + + return log_error_errno(errno, "Failed to read image contents: %m"); + } + if (n == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Premature EOF while reading image contents."); + + sha256_process_bytes(buffer, n, &ctx); + pos += n; + } + + sha256_finish_ctx(&ctx, ret); + return 0; +} + +static int partition_verity_sig_size(int fd, const Partition *p, uint64_t *ret) { + assert(fd >= 0); + assert(p); + assert(ret); + assert(p->offset != UINT64_MAX); + assert(p->new_size != UINT64_MAX); + + /* Returns the size of the JSON object in a verity signature partition, i.e. without the trailing + * NUL padding, which is trimmed from the split-out file (see context_split()). If the partition + * holds no JSON object at all (i.e. is all zeroes, for example an empty B slot of an A/B layout), + * returns the full partition size, so that the partition is described as it is. */ + + _cleanup_free_ char *buf = malloc(p->new_size); + if (!buf) + return log_oom(); + + ssize_t n = pread(fd, buf, p->new_size, p->offset); + if (n < 0) + return log_error_errno(errno, "Failed to read verity signature partition: %m"); + if ((uint64_t) n != p->new_size) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading verity signature partition."); + + size_t len = strnlen(buf, p->new_size); + *ret = len > 0 ? len : p->new_size; + return 0; +} + +static int partition_manifest_gpt_name(Context *context, Partition *p, bool placeholder_uuid, char **ret) { + _cleanup_free_ char *path = NULL; + int r; + + assert(context); + assert(p); + assert(ret); + + /* Name the partition like its split-out file, whether or not that is actually generated (i.e. + * independently of --split=). If SplitName= is turned off, use the partition number. */ + r = partition_name_path(context, p, p->split_name_format ?: "%n", /* suffix= */ NULL, placeholder_uuid, &path); + if (r < 0) + return r; + + r = path_extract_filename(path, ret); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from %s: %m", path); + + return 0; +} + +static bool partition_manifest_expected(Context *context, const Partition *p) { + assert(context); + assert(p); + + /* Returns true if a file system tree will be populated for this partition in this run, and hence a + * manifest of it is generated, if requested. This mirrors the conditions under which + * context_minimize() and context_mkfs() populate partitions. */ + + return !p->dropped && + partition_wants_manifest(p) && + !PARTITION_EXISTS(p) && + p->format && + !partition_defer(context, p) && + partition_needs_populate(p); +} + +static int context_check_names_early(Context *context) { + _cleanup_strv_free_ char **split_names = NULL, **gpt_names = NULL, **manifest_names = NULL; + int r; + + assert(context); + + /* The names of split artifacts and of the objects in the partition table manifest are resolved late + * (since the UUIDs of verity partitions are only known once they have been generated), i.e. after + * the partition table has been written. Check them for collisions before we make any changes, with + * a placeholder for the UUIDs not known yet. The late resolution checks again. The same applies + * to the names of the manifests of the partitions' file system trees. */ + + LIST_FOREACH(partitions, p, context->partitions) { + if (p->dropped) + continue; + + if (arg_split && p->split_name_format) { + _cleanup_free_ char *path = NULL; + + r = partition_name_path(context, p, p->split_name_format, /* suffix= */ NULL, /* placeholder_uuid= */ true, &path); + if (r < 0) + return r; + + if (strv_contains(split_names, path)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "Multiple partitions resolve to the same split name \"%s\", refusing.", + path); + + r = strv_consume(&split_names, TAKE_PTR(path)); + if (r < 0) + return log_oom(); + } + + if (arg_manifest_gpt && !partition_defer(context, p)) { + _cleanup_free_ char *name = NULL; + + r = partition_manifest_gpt_name(context, p, /* placeholder_uuid= */ true, &name); + if (r < 0) + return r; + + if (strv_contains(gpt_names, name)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "Multiple partitions resolve to the same name \"%s\" in the partition table manifest, refusing.", + name); + + r = strv_consume(&gpt_names, TAKE_PTR(name)); + if (r < 0) + return log_oom(); + } + + if (partition_manifest_expected(context, p)) { + _cleanup_free_ char *path = NULL; + + r = partition_name_path(context, p, p->manifest_name_format, UAPI16_MANIFEST_SUFFIX, /* placeholder_uuid= */ true, &path); + if (r < 0) + return r; + + if (strv_contains(manifest_names, path)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "Multiple partitions resolve to the same manifest name \"%s\", refusing.", + path); + + r = strv_consume(&manifest_names, TAKE_PTR(path)); + if (r < 0) + return log_oom(); + } + } + + return 0; +} + +static int context_manifest_gpt(Context *context) { + _cleanup_free_ char *image_name = NULL, *path = NULL; + _cleanup_(unlink_and_freep) char *tmp_path = NULL; + _cleanup_strv_free_ char **names = NULL; + _cleanup_fclose_ FILE *f = NULL; + int fd, r; + + assert(context); + + if (!arg_manifest_gpt) + return 0; + + /* We didn't write a partition table, hence don't describe it */ + if (context->dry_run) + return 0; + + /* Writes a UAPI.16 manifest describing the partition table: one object for the image as a whole, + * and one object per partition, carrying the GPT metadata, the partition's size and the hash of its + * contents, and referencing the partition's bytes inside the image by their offset as source. */ + + assert_se((fd = sym_fdisk_get_devfd(context->fdisk_context)) >= 0); + assert(context->total != UINT64_MAX); + + r = manifest_gpt_path(context, &path); + if (r < 0) + return r; + + r = path_extract_filename(context->node, &image_name); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from %s: %m", context->node); + + /* Write to a temporary file first, and link it into place only once complete */ + r = fopen_tmpfile_linkable(path, O_WRONLY|O_CLOEXEC, &tmp_path, &f); + if (r < 0) + return log_error_errno(r, "Failed to create temporary file for partition table manifest %s: %m", path); + + _cleanup_(manifest_context_done) ManifestContext c = MANIFEST_CONTEXT_NULL; + c.format = MANIFEST_FORMAT_UAPI16; + c.output = f; +#if HAVE_OPENSSL + c.certificate = context->manifest_certificate; + c.private_key = context->manifest_private_key; +#endif + + r = manifest_uapi16_begin(&c); + if (r < 0) + return r; + + /* The root object carries the fields of the GPT header */ + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = sd_json_buildo( + &v, + SD_JSON_BUILD_PAIR("mediaType", JSON_BUILD_CONST_STRING(UAPI16_MANIFEST_MEDIA_TYPE)), + SD_JSON_BUILD_PAIR_UUID("gptDiskUuid", context->disk_uuid)); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + r = manifest_uapi16_write_object(&c, v); + if (r < 0) + return r; + + /* Then an object for the whole image, i.e. the complete disk including the partition table */ + uint8_t image_hash[SHA256_DIGEST_SIZE]; + r = hash_range(fd, 0, context->total, image_hash); + if (r < 0) + return r; + + v = sd_json_variant_unref(v); + r = sd_json_buildo( + &v, + SD_JSON_BUILD_PAIR_STRING("name", image_name), + SD_JSON_BUILD_PAIR_UNSIGNED("size", context->total), + SD_JSON_BUILD_PAIR_HEX("sha256", image_hash, sizeof(image_hash))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + r = manifest_uapi16_write_object(&c, v); + if (r < 0) + return r; + + LIST_FOREACH(partitions, p, context->partitions) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *contents = NULL, *o = NULL; + _cleanup_free_ char *name = NULL; + uint8_t hash[SHA256_DIGEST_SIZE]; + + if (p->dropped) + continue; + + if (partition_defer(context, p)) + continue; + + assert(p->offset != UINT64_MAX); + assert(p->new_size != UINT64_MAX); + + r = partition_manifest_gpt_name(context, p, /* placeholder_uuid= */ false, &name); + if (r < 0) + return r; + + /* Already checked by context_check_names_early(), but the verity partition UUIDs were + * not known back then, hence check again */ + if (strv_contains(names, name)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "Multiple partitions resolve to the same name \"%s\" in the partition table manifest, refusing.", + name); + + r = strv_extend(&names, name); + if (r < 0) + return log_oom(); + + /* The split-out file of a verity signature partition is trimmed of the NUL padding, hence + * describe the same bytes here. As those are a prefix of the partition, the object then + * matches both the split-out file and the partition data in the image. */ + uint64_t size = p->new_size; + if (partition_designator_is_verity_sig(p->type.designator)) { + r = partition_verity_sig_size(fd, p, &size); + if (r < 0) + return r; + } + + r = hash_range(fd, p->offset, size, hash); + if (r < 0) + return r; + + /* The partition's bytes are found in the image at the partition offset. */ + r = sd_json_build(&contents, SD_JSON_BUILD_ARRAY( + SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_STRING("file", image_name), + SD_JSON_BUILD_PAIR_UNSIGNED("offset", p->offset), + SD_JSON_BUILD_PAIR_UNSIGNED("originalSize", context->total)))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON array: %m"); + + const char *label = p->new_label ?: p->current_label; + sd_id128_t uuid = p->new_uuid_is_set ? p->new_uuid : p->current_uuid; + + /* For existing partitions report the flags on disk (we never change them), for new ones + * the flags we just wrote. The individual flag fields are derived from these, so that they + * are consistent with "gptFlags", i.e. are left out for partition types that do not + * support them. */ + uint64_t flags; + if (PARTITION_EXISTS(p)) { + r = fdisk_partition_get_attrs_as_uint64(p->current_partition, &flags); + if (r < 0) + return log_error_errno(r, "Failed to get partition flags: %m"); + } else + flags = partition_merge_flags(p); + + r = sd_json_buildo( + &o, + SD_JSON_BUILD_PAIR_STRING("name", name), + SD_JSON_BUILD_PAIR_UNSIGNED("size", size), + SD_JSON_BUILD_PAIR_HEX("sha256", hash, sizeof(hash)), + JSON_BUILD_PAIR_STRING_NON_EMPTY("gptLabel", label), + SD_JSON_BUILD_PAIR_UUID("gptUuid", uuid), + SD_JSON_BUILD_PAIR_UUID("gptTypeUuid", p->type.uuid), + SD_JSON_BUILD_PAIR_UNSIGNED("gptFlags", flags), + SD_JSON_BUILD_PAIR_CONDITION(p->no_auto >= 0 && gpt_partition_type_knows_no_auto(p->type), + "gptFlagNoAuto", SD_JSON_BUILD_BOOLEAN(FLAGS_SET(flags, SD_GPT_FLAG_NO_AUTO))), + SD_JSON_BUILD_PAIR_CONDITION(p->growfs >= 0 && gpt_partition_type_knows_growfs(p->type), + "gptFlagGrowFileSystem", SD_JSON_BUILD_BOOLEAN(FLAGS_SET(flags, SD_GPT_FLAG_GROWFS))), + SD_JSON_BUILD_PAIR_CONDITION(p->read_only >= 0 && gpt_partition_type_knows_read_only(p->type), + "readOnly", SD_JSON_BUILD_BOOLEAN(FLAGS_SET(flags, SD_GPT_FLAG_READ_ONLY))), + SD_JSON_BUILD_PAIR_VARIANT("contents", contents)); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + r = manifest_uapi16_write_object(&c, o); + if (r < 0) + return r; + } + + r = manifest_uapi16_end(&c); + if (r < 0) + return r; + + r = fchmod_umask(fileno(f), 0666); + if (r < 0) + return log_error_errno(r, "Failed to set access mode of partition table manifest: %m"); + + r = flink_tmpfile(f, tmp_path, path, /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to link partition table manifest %s into place: %m", path); + + /* If we used a named temporary file, it has been renamed now */ + tmp_path = mfree(tmp_path); + + /* Remove it again if we fail later on, until context_disarm_auto_removal() is called */ + free_and_replace(context->manifest_gpt_path, path); + + return 0; +} + +static int context_manifest_files(Context *context) { + int r; + + assert(context); + + if (!arg_manifest) + return 0; + + /* We didn't write the partitions, hence don't link their manifests into place either (the temporary + * files are removed when the partitions are freed) */ + if (context->dry_run) + return 0; + + /* As with context_split(), resolution must happen late, since partition UUIDs of verity partitions + * are only known once they have been generated. */ + + r = manifest_name_resolve(context); + if (r < 0) + return r; + + LIST_FOREACH(partitions, p, context->partitions) { + if (!p->manifest_path) + continue; + + assert(p->manifest_fd >= 0); + + r = fchmod_umask(p->manifest_fd, 0666); + if (r < 0) + return log_error_errno(r, "Failed to set access mode of manifest file: %m"); + + r = link_tmpfile(p->manifest_fd, p->manifest_tmp_path, p->manifest_path, /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to link manifest file %s into place: %m", p->manifest_path); + + p->manifest_linked = true; + + /* If we used a named temporary file, it has been renamed now */ + p->manifest_tmp_path = mfree(p->manifest_tmp_path); + p->manifest_fd = safe_close(p->manifest_fd); + } + + return 0; +} + static int write_primary_descriptor( int fd, uint32_t root_sector, @@ -10189,7 +10893,7 @@ static int context_minimize(Context *context) { if (!root) { assert(d); - r = partition_populate_filesystem(context, p, d->node); + r = partition_populate_filesystem(context, p, d->node, /* final= */ false); if (r < 0) return r; } @@ -10252,7 +10956,7 @@ static int context_minimize(Context *context) { if (!root) { assert(d); - r = partition_populate_filesystem(context, p, d->node); + r = partition_populate_filesystem(context, p, d->node, /* final= */ true); if (r < 0) return r; } @@ -10385,6 +11089,47 @@ static int context_load_keys(Context *context) { return log_error_errno(r, "Failed to load private key from %s: %m", arg_private_key); } + if (arg_manifest_certificate) { + if (arg_manifest_certificate_source_type == OPENSSL_CERTIFICATE_SOURCE_FILE) { + r = parse_path_argument(arg_manifest_certificate, /* suppress_root= */ false, &arg_manifest_certificate); + if (r < 0) + return r; + } + + r = openssl_load_x509_certificate( + arg_manifest_certificate_source_type, + arg_manifest_certificate_source, + arg_manifest_certificate, + &context->manifest_certificate); + if (r < 0) + return log_error_errno(r, "Failed to load X.509 certificate from %s: %m", arg_manifest_certificate); + } + + if (arg_manifest_private_key) { + if (arg_manifest_private_key_source_type == OPENSSL_KEY_SOURCE_FILE) { + r = parse_path_argument(arg_manifest_private_key, /* suppress_root= */ false, &arg_manifest_private_key); + if (r < 0) + return r; + } + + r = openssl_load_private_key( + arg_manifest_private_key_source_type, + arg_manifest_private_key_source, + arg_manifest_private_key, + &(AskPasswordRequest) { + .tty_fd = -EBADF, + .id = "repart-manifest-private-key-pin", + .keyring = arg_manifest_private_key, + .credential = "repart.manifest-private-key-pin", + .until = USEC_INFINITY, + .hup_fd = -EBADF, + }, + &context->manifest_private_key, + &context->manifest_ui); + if (r < 0) + return log_error_errno(r, "Failed to load private key from %s: %m", arg_manifest_private_key); + } + #endif return 0; } @@ -11017,6 +11762,73 @@ static int parse_argv(int argc, char *argv[]) { break; + OPTION_GROUP("Manifest Generation"): {} + + OPTION_LONG("manifest", "BOOL", + "Whether to generate UAPI.16 manifests of files and directories in populated partitions"): + r = parse_boolean_argument("--manifest=", opts.arg, /* ret= */ NULL); + if (r < 0) + return r; + + arg_manifest = r; + break; + + OPTION_LONG("manifest-gpt", "BOOL", + "Whether to generate a UAPI.16 manifest of the partition table"): + r = parse_boolean_argument("--manifest-gpt=", opts.arg, /* ret= */ NULL); + if (r < 0) + return r; + + arg_manifest_gpt = r; + break; + + OPTION_LONG("manifest-fields", "FIELDS", + "Fields to include in manifests of populated partitions (mode, uid, gid, size, hash, " + "symlink, device, hardlink, mtime, user-xattr, user-name, group-name, all, default, " + "none, or +/- prefixed to adjust)"): + r = manifest_fields_from_string(opts.arg, &arg_manifest_fields); + if (r < 0) + return log_error_errno(r, "Failed to parse field list: %s", opts.arg); + break; + + OPTION_LONG("manifest-private-key", "PATH|URI", + "Private key to sign generated manifests with"): + r = free_and_strdup_warn(&arg_manifest_private_key, opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("manifest-private-key-source", "SOURCE", + "Specify how to use the private key from --manifest-private-key= " + "(file, provider:PROVIDER, engine:ENGINE)"): + r = parse_openssl_key_source_argument( + opts.arg, + &arg_manifest_private_key_source, + &arg_manifest_private_key_source_type); + if (r < 0) + return r; + break; + + OPTION_LONG("manifest-certificate", "PATH|URI", + "PEM certificate to sign generated manifests with, or a provider-specific " + "designation if --manifest-certificate-source= is used"): + r = free_and_strdup_warn(&arg_manifest_certificate, opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("manifest-certificate-source", "SOURCE", + "Specify how to interpret the certificate from --manifest-certificate=. " + "Allows the certificate to be loaded from an OpenSSL provider " + "(file, provider:PROVIDER)"): + r = parse_openssl_certificate_source_argument( + opts.arg, + &arg_manifest_certificate_source, + &arg_manifest_certificate_source_type); + if (r < 0) + return r; + break; + OPTION_COMMON_INTROSPECT_CLI: return introspect_cli(arg_json_format_flags); } @@ -11121,6 +11933,25 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "A path to an image file must be specified when --split is used."); + if ((arg_manifest || arg_manifest_gpt) && !arg_node) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "A path to an image file must be specified when --manifest= or --manifest-gpt= are used."); + + /* Manifests are signed if (and only if) a private key for them is specified */ + if (!!arg_manifest_private_key != !!arg_manifest_certificate) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--manifest-private-key= and --manifest-certificate= must be specified together."); + + if (arg_manifest_private_key && !arg_manifest && !arg_manifest_gpt) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--manifest-private-key= requires --manifest= or --manifest-gpt=."); + +#if !HAVE_OPENSSL + if (arg_manifest_private_key) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Signing manifests is not supported, compiled without OpenSSL support."); +#endif + if (auto_pcrlock) { assert(!arg_tpm2_pcrlock); @@ -12173,6 +13004,10 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; + r = context_check_names_early(context); + if (r < 0) + return r; + (void) context_dump(context, /* late= */ false); r = context_write_partition_table(context); @@ -12187,6 +13022,14 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; + r = context_manifest_files(context); + if (r < 0) + return r; + + r = context_manifest_gpt(context); + if (r < 0) + return r; + r = context_crypttab(context, /* late= */ true); if (r < 0) return r; diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh index 9632edb6e00..792531f4ea0 100755 --- a/test/units/TEST-58-REPART.sh +++ b/test/units/TEST-58-REPART.sh @@ -1498,6 +1498,9 @@ EOF mkdir -p /run/verity.d ln -sf "$defs/verity.crt" /run/verity.d/ok.crt + # Manifests are signed with a key of their own, separate from the verity one + openssl req -x509 -newkey rsa:2048 -nodes -keyout "$defs/manifest.key" -out "$defs/manifest.crt" -subj /CN=manifest -days 2 2>/dev/null + output=$(systemd-repart --offline="$OFFLINE" \ --definitions="$defs" \ --seed="$seed" \ @@ -1505,6 +1508,11 @@ EOF --empty=create \ --size=auto \ --split=yes \ + --manifest=yes \ + --manifest-gpt=yes \ + --manifest-fields=size,hash \ + --manifest-private-key="$defs/manifest.key" \ + --manifest-certificate="$defs/manifest.crt" \ --json=pretty \ --private-key="$defs/verity.key" \ --certificate="$defs/verity.crt" \ @@ -1524,6 +1532,71 @@ EOF assert_neq "$sig_split" "null" jq . "$sig_split" >/dev/null + # The manifest of the root partition's file system tree lists the files copied into it, and the manifest + # of the partition table describes the partitions, with the split artifacts as sources, hence it must + # verify against the directory the split artifacts are in. + root_manifest=$(jq -r ".[] | select(.type == \"root-${architecture}\") | .manifest_path" <<<"$output") + assert_eq "$root_manifest" "$imgs/verity.root-${architecture}.Uapi16Manifest" + # CopyFiles= places the definitions directory at the same path inside the partition + jq --seq -s -e ".[] | select(.name == \"${defs#/}/verity-data.conf\")" <"$root_manifest" >/dev/null + # Only the requested fields are included + jq --seq -s -e ".[] | select(.name == \"${defs#/}/verity-data.conf\") | has(\"size\") and has(\"sha256\") and (has(\"mode\") | not)" <"$root_manifest" >/dev/null + # The manifest is signed with the manifest key, not the verity key + systemd-manifest verify --certificate="$defs/manifest.crt" / "$root_manifest" + (! systemd-manifest verify --certificate="$defs/verity.crt" / "$root_manifest") + assert_eq "$(jq -r ".[] | select(.type == \"root-${architecture}-verity-sig\") | .manifest_path" <<<"$output")" "-" + + gpt_manifest="$imgs/verity.Uapi16Manifest" + jq --seq -s -e '.[] | select(.mediaType == "application/vnd.uapi.16.signature") | .mechanism == "pkcs7"' <"$gpt_manifest" >/dev/null + # The image as a whole is covered too + jq --seq -s -e ".[] | select(.name == \"verity\") | .size == $(stat -c %s "$imgs/verity") and .sha256 == \"$(sha256sum "$imgs/verity" | cut -d' ' -f1)\"" <"$gpt_manifest" >/dev/null + root_split=$(jq -r ".[] | select(.type == \"root-${architecture}\") | .split_path" <<<"$output") + jq --seq -s -e ".[] | select(.name == \"$(basename "$root_split")\") | (.contents | length) == 1 and .contents[0].file == \"$(basename "$imgs/verity")\" and .contents[0].offset > 0 and .gptLabel != null" <"$gpt_manifest" >/dev/null + # The verity signature partition is described like its split artifact, i.e. without the NUL padding + jq --seq -s -e ".[] | select(.name == \"$(basename "$sig_split")\") | (.contents | length) == 1 and .size == $(stat -c %s "$sig_split")" <"$gpt_manifest" >/dev/null + systemd-manifest verify --certificate="$defs/manifest.crt" "$imgs" "$gpt_manifest" + (! systemd-manifest verify --certificate="$defs/verity.crt" "$imgs" "$gpt_manifest") + + # A verity signature partition without any signature in it (e.g. an empty B slot) is described with + # its full size, rather than failing + mkdir "$defs/empty-sig" + tee "$defs/empty-sig/10-sig.conf" </dev/null + + # Colliding names in the partition table manifest (here: an A/B pair of root partitions with the + # default SplitName=) are refused before the partition table is written + mkdir "$defs/collide" + for i in a b; do + tee "$defs/collide/10-root-$i.conf" <