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