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