From 2027653ad7bb8c2c7ade65bc5198315fd9328639 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 19 Jun 2025 07:42:48 -0400 Subject: [PATCH] prepare-root: Factor out composefs handling into otcore I'm thinking about creating ostree-prepare-soft-reboot.c. Prepare for this by factoring out shared helper functions. --- Makefile-otcore.am | 8 +- src/libotcore/otcore-prepare-root.c | 242 +++++++++++++++++++++++++++ src/libotcore/otcore.h | 21 +++ src/switchroot/ostree-prepare-root.c | 239 +------------------------- 4 files changed, 273 insertions(+), 237 deletions(-) diff --git a/Makefile-otcore.am b/Makefile-otcore.am index ec345785..07beb1fe 100644 --- a/Makefile-otcore.am +++ b/Makefile-otcore.am @@ -22,5 +22,11 @@ libotcore_la_SOURCES = \ src/libotcore/otcore-spki-verify.c \ $(NULL) -libotcore_la_CFLAGS = $(AM_CFLAGS) -I$(srcdir)/libglnx -I$(srcdir)/src/libotutil -DLOCALEDIR=\"$(datadir)/locale\" $(OT_INTERNAL_GIO_UNIX_CFLAGS) $(OT_INTERNAL_GPGME_CFLAGS) $(OT_DEP_CRYPTO_LIBS) $(LIBSYSTEMD_CFLAGS) +# Note that this also uses *includes* from libostree, so there's a partial +# circular dependency. +libotcore_la_CFLAGS = $(AM_CFLAGS) -I$(srcdir)/libglnx -I$(srcdir)/src/libotutil -I$(srcdir)/src/libostree -DLOCALEDIR=\"$(datadir)/locale\" $(OT_INTERNAL_GIO_UNIX_CFLAGS) $(OT_INTERNAL_GPGME_CFLAGS) $(OT_DEP_CRYPTO_LIBS) $(LIBSYSTEMD_CFLAGS) libotcore_la_LIBADD = $(OT_INTERNAL_GIO_UNIX_LIBS) $(OT_INTERNAL_GPGME_LIBS) $(LIBSYSTEMD_LIBS) $(OT_DEP_CRYPTO_LIBS) + +if USE_COMPOSEFS +libotcore_la_LIBADD += $(OT_DEP_COMPOSEFS_LIBS) +endif \ No newline at end of file diff --git a/src/libotcore/otcore-prepare-root.c b/src/libotcore/otcore-prepare-root.c index 18bdf43e..01acf5cf 100644 --- a/src/libotcore/otcore-prepare-root.c +++ b/src/libotcore/otcore-prepare-root.c @@ -18,6 +18,14 @@ #include "config.h" #include "otcore.h" +#include +#include +#include + +#ifdef HAVE_COMPOSEFS +#include +#include +#endif // This key is used by default if present in the initramfs to verify // the signature on the target commit object. When composefs is @@ -247,3 +255,237 @@ otcore_load_composefs_config (const char *cmdline, GKeyFile *config, gboolean lo return g_steal_pointer (&ret); } + +#ifdef HAVE_COMPOSEFS +static GVariant * +load_variant (const char *root_mountpoint, const char *digest, const char *extension, + const GVariantType *type, GError **error) +{ + g_autofree char *path = g_strdup_printf ("%s/ostree/repo/objects/%.2s/%s.%s", root_mountpoint, + digest, digest + 2, extension); + + char *data = NULL; + gsize data_size; + if (!g_file_get_contents (path, &data, &data_size, error)) + return NULL; + + return g_variant_ref_sink (g_variant_new_from_data (type, data, data_size, FALSE, g_free, data)); +} + +// Given a mount point, directly load the .commit object. At the current time this tool +// doesn't link to libostree. +static gboolean +load_commit_for_deploy (const char *root_mountpoint, const char *deploy_path, GVariant **commit_out, + GVariant **commitmeta_out, GError **error) +{ + g_autoptr (GError) local_error = NULL; + g_autofree char *digest = g_path_get_basename (deploy_path); + char *dot = strchr (digest, '.'); + if (dot != NULL) + *dot = 0; + + g_autoptr (GVariant) commit_v + = load_variant (root_mountpoint, digest, "commit", OSTREE_COMMIT_GVARIANT_FORMAT, error); + if (commit_v == NULL) + return FALSE; + + g_autoptr (GVariant) commitmeta_v = load_variant (root_mountpoint, digest, "commitmeta", + G_VARIANT_TYPE ("a{sv}"), &local_error); + if (commitmeta_v == NULL) + { + if (g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) + glnx_throw (error, "No commitmeta for commit %s", digest); + else + g_propagate_error (error, g_steal_pointer (&local_error)); + return FALSE; + } + + *commit_out = g_steal_pointer (&commit_v); + *commitmeta_out = g_steal_pointer (&commitmeta_v); + + return TRUE; +} + +/** + * validate_signature: + * @data: The raw data whose signature must be validated + * @signatures: A variant of type "ay" (byte array) containing signatures + * @pubkeys: an array of type GBytes* + * + * Verify that @data is signed using @signatures and @pubkeys. + */ +static gboolean +validate_signature (GBytes *data, GVariant *signatures, GPtrArray *pubkeys, GError **error) +{ + g_assert (data); + g_assert (signatures); + g_assert (pubkeys); + + for (gsize j = 0; j < pubkeys->len; j++) + { + GBytes *pubkey = pubkeys->pdata[j]; + g_assert (pubkey); + + for (gsize i = 0; i < g_variant_n_children (signatures); i++) + { + g_autoptr (GVariant) child = g_variant_get_child_value (signatures, i); + g_autoptr (GBytes) signature = g_variant_get_data_as_bytes (child); + bool valid = false; + + if (!otcore_validate_ed25519_signature (data, pubkey, signature, &valid, error)) + return glnx_prefix_error (error, "signature verification failed"); + // At least one valid signature is enough. + if (valid) + return TRUE; + } + } + + return FALSE; +} + +// Output a friendly message based on an errno for common cases +static const char * +composefs_error_message (int errsv) +{ + switch (errsv) + { + case ENOVERITY: + return "fsverity not enabled on composefs image"; + case EWRONGVERITY: + return "Wrong fsverity digest in composefs image"; + case ENOSIGNATURE: + return "Missing signature for fsverity in composefs image"; + default: + return strerror (errsv); + } +} + +#endif + +gboolean +otcore_mount_composefs (ComposefsConfig *composefs_config, GVariantBuilder *metadata_builder, + gboolean root_transient, const char *root_mountpoint, + const char *deploy_path, const char *mount_target, + bool *out_using_composefs, GError **error) +{ + bool using_composefs = FALSE; +#ifdef HAVE_COMPOSEFS + /* We construct the new sysroot in /sysroot.tmp, which is either the composefs + mount or a bind mount of the deploy-dir */ + if (composefs_config->enabled == OT_TRISTATE_NO) + return TRUE; + + const char *objdirs[] = { "/sysroot/ostree/repo/objects" }; + struct lcfs_mount_options_s cfs_options = { + objdirs, + 1, + }; + + cfs_options.flags = 0; + cfs_options.image_mountdir = OSTREE_COMPOSEFS_LOWERMNT; + if (mkdirat (AT_FDCWD, OSTREE_COMPOSEFS_LOWERMNT, 0700) < 0 && errno != EEXIST) + return glnx_throw (error, "Failed to create %s", OSTREE_COMPOSEFS_LOWERMNT); + + g_autofree char *expected_digest = NULL; + + // For now we just stick the transient root on the default /run tmpfs; + // however, see + // https://github.com/systemd/systemd/blob/604b2001081adcbd64ee1fbe7de7a6d77c5209fe/src/basic/mountpoint-util.h#L36 + // which bumps up these defaults for the rootfs a bit. + g_autofree char *root_upperdir + = root_transient ? g_build_filename (OTCORE_RUN_OSTREE_PRIVATE, "root/upper", NULL) : NULL; + g_autofree char *root_workdir + = root_transient ? g_build_filename (OTCORE_RUN_OSTREE_PRIVATE, "root/work", NULL) : NULL; + + // Propagate these options for transient root, if provided + if (root_transient) + { + if (!glnx_shutil_mkdir_p_at (AT_FDCWD, root_upperdir, 0755, NULL, error)) + return glnx_prefix_error (error, "Failed to create %s", root_upperdir); + if (!glnx_shutil_mkdir_p_at (AT_FDCWD, root_workdir, 0700, NULL, error)) + return glnx_prefix_error (error, "Failed to create %s", root_workdir); + + cfs_options.workdir = root_workdir; + cfs_options.upperdir = root_upperdir; + } + else + { + cfs_options.flags = LCFS_MOUNT_FLAGS_READONLY; + } + + if (composefs_config->is_signed) + { + const char *composefs_pubkey = composefs_config->signature_pubkey; + g_autoptr (GVariant) commit = NULL; + g_autoptr (GVariant) commitmeta = NULL; + + if (!load_commit_for_deploy (root_mountpoint, deploy_path, &commit, &commitmeta, error)) + return glnx_prefix_error (error, "Error loading signatures from repo"); + + g_autoptr (GVariant) signatures = g_variant_lookup_value ( + commitmeta, OSTREE_SIGN_METADATA_ED25519_KEY, G_VARIANT_TYPE ("aay")); + if (signatures == NULL) + return glnx_throw (error, "Signature validation requested, but no signatures in commit"); + + g_autoptr (GBytes) commit_data = g_variant_get_data_as_bytes (commit); + if (!validate_signature (commit_data, signatures, composefs_config->pubkeys, error)) + return glnx_throw (error, "No valid signatures found for public key"); + + g_print ("composefs+ostree: Validated commit signature using '%s'\n", composefs_pubkey); + g_variant_builder_add (metadata_builder, "{sv}", OTCORE_RUN_BOOTED_KEY_COMPOSEFS_SIGNATURE, + g_variant_new_string (composefs_pubkey)); + + g_autoptr (GVariant) metadata = g_variant_get_child_value (commit, 0); + g_autoptr (GVariant) cfs_digest_v = g_variant_lookup_value ( + metadata, OSTREE_COMPOSEFS_DIGEST_KEY_V0, G_VARIANT_TYPE_BYTESTRING); + if (cfs_digest_v == NULL || g_variant_get_size (cfs_digest_v) != OSTREE_SHA256_DIGEST_LEN) + return glnx_throw (error, "Signature validation requested, but no valid digest in commit"); + const guint8 *cfs_digest_buf = ot_variant_get_data (cfs_digest_v, error); + if (!cfs_digest_buf) + return glnx_prefix_error (error, "Failed to query digest"); + + expected_digest = g_malloc (OSTREE_SHA256_STRING_LEN + 1); + ot_bin2hex (expected_digest, cfs_digest_buf, g_variant_get_size (cfs_digest_v)); + + g_assert (composefs_config->require_verity); + cfs_options.flags |= LCFS_MOUNT_FLAGS_REQUIRE_VERITY; + g_print ("composefs: Verifying digest: %s\n", expected_digest); + cfs_options.expected_fsverity_digest = expected_digest; + } + else if (composefs_config->require_verity) + { + cfs_options.flags |= LCFS_MOUNT_FLAGS_REQUIRE_VERITY; + } + + if (lcfs_mount_image (OSTREE_COMPOSEFS_NAME, mount_target, &cfs_options) == 0) + { + using_composefs = true; + bool using_verity = (cfs_options.flags & LCFS_MOUNT_FLAGS_REQUIRE_VERITY) > 0; + g_variant_builder_add (metadata_builder, "{sv}", OTCORE_RUN_BOOTED_KEY_COMPOSEFS, + g_variant_new_boolean (true)); + g_variant_builder_add (metadata_builder, "{sv}", OTCORE_RUN_BOOTED_KEY_COMPOSEFS_VERITY, + g_variant_new_boolean (using_verity)); + g_print ("composefs: mounted successfully (verity=%s)\n", using_verity ? "true" : "false"); + } + else + { + int errsv = errno; + g_assert (composefs_config->enabled != OT_TRISTATE_NO); + if (composefs_config->enabled == OT_TRISTATE_MAYBE && errsv == ENOENT) + { + g_print ("composefs: No image present\n"); + } + else + { + const char *errmsg = composefs_error_message (errsv); + return glnx_throw (error, "composefs: failed to mount: %s", errmsg); + } + } +#else + /* if composefs is configured as "maybe", we should continue */ + if (composefs_config->enabled == OT_TRISTATE_YES) + return glnx_throw (error, "composefs: enabled at runtime, but support is not compiled in"); +#endif + *out_using_composefs = using_composefs; + return TRUE; +} diff --git a/src/libotcore/otcore.h b/src/libotcore/otcore.h index 351c195e..7a9978de 100644 --- a/src/libotcore/otcore.h +++ b/src/libotcore/otcore.h @@ -78,6 +78,27 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (ComposefsConfig, otcore_free_composefs_config) ComposefsConfig *otcore_load_composefs_config (const char *cmdline, GKeyFile *config, gboolean load_keys, GError **error); +/** + * otcore_mount_composefs: + * @composefs_config: Configuration for composefs. + * @metadata_builder: (transfer none): GVariantBuilder to add metadata to. + * @root_transient: Whether the root filesystem is transient. + * @root_mountpoint: The mount point of the physical root filesystem. + * @deploy_path: The path to the deployment. + * @mount_target: The target path to mount the composefs image. + * @out_using_composefs: (out): Whether composefs was successfully used. + * @error: (out): Return location for a GError, or %NULL. + * + * Mounts a composefs image based on the provided configuration. + * + * Returns: %TRUE on success, %FALSE on error. + */ +gboolean otcore_mount_composefs (ComposefsConfig *composefs_config, + GVariantBuilder *metadata_builder, gboolean root_transient, + const char *root_mountpoint, const char *deploy_path, + const char *mount_target, bool *out_using_composefs, + GError **error); + // Our directory with transient state (eventually /run/ostree-booted should be a link to // /run/ostree/booted) #define OTCORE_RUN_OSTREE "/run/ostree" diff --git a/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c index 1e77809f..d3dee902 100644 --- a/src/switchroot/ostree-prepare-root.c +++ b/src/switchroot/ostree-prepare-root.c @@ -90,11 +90,6 @@ // A temporary mount point #define TMP_SYSROOT "/sysroot.tmp" -#ifdef HAVE_COMPOSEFS -#include -#include -#endif - #include "ostree-mount-util.h" static GOptionEntry options[] = { { NULL } }; @@ -149,113 +144,6 @@ resolve_deploy_path (const char *kernel_cmdline, const char *root_mountpoint) return deploy_path; } -#ifdef HAVE_COMPOSEFS -static GVariant * -load_variant (const char *root_mountpoint, const char *digest, const char *extension, - const GVariantType *type, GError **error) -{ - g_autofree char *path = g_strdup_printf ("%s/ostree/repo/objects/%.2s/%s.%s", root_mountpoint, - digest, digest + 2, extension); - - char *data = NULL; - gsize data_size; - if (!g_file_get_contents (path, &data, &data_size, error)) - return NULL; - - return g_variant_ref_sink (g_variant_new_from_data (type, data, data_size, FALSE, g_free, data)); -} - -// Given a mount point, directly load the .commit object. At the current time this tool -// doesn't link to libostree. -static gboolean -load_commit_for_deploy (const char *root_mountpoint, const char *deploy_path, GVariant **commit_out, - GVariant **commitmeta_out, GError **error) -{ - g_autoptr (GError) local_error = NULL; - g_autofree char *digest = g_path_get_basename (deploy_path); - char *dot = strchr (digest, '.'); - if (dot != NULL) - *dot = 0; - - g_autoptr (GVariant) commit_v - = load_variant (root_mountpoint, digest, "commit", OSTREE_COMMIT_GVARIANT_FORMAT, error); - if (commit_v == NULL) - return FALSE; - - g_autoptr (GVariant) commitmeta_v = load_variant (root_mountpoint, digest, "commitmeta", - G_VARIANT_TYPE ("a{sv}"), &local_error); - if (commitmeta_v == NULL) - { - if (g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) - glnx_throw (error, "No commitmeta for commit %s", digest); - else - g_propagate_error (error, g_steal_pointer (&local_error)); - return FALSE; - } - - *commit_out = g_steal_pointer (&commit_v); - *commitmeta_out = g_steal_pointer (&commitmeta_v); - - return TRUE; -} - -/** - * validate_signature: - * @data: The raw data whose signature must be validated - * @signatures: A variant of type "ay" (byte array) containing signatures - * @pubkeys: an array of type GBytes* - * - * Verify that @data is signed using @signatures and @pubkeys. - */ -static gboolean -validate_signature (GBytes *data, GVariant *signatures, GPtrArray *pubkeys) -{ - g_assert (data); - g_assert (signatures); - g_assert (pubkeys); - - for (gsize j = 0; j < pubkeys->len; j++) - { - GBytes *pubkey = pubkeys->pdata[j]; - g_assert (pubkey); - - for (gsize i = 0; i < g_variant_n_children (signatures); i++) - { - g_autoptr (GError) local_error = NULL; - g_autoptr (GVariant) child = g_variant_get_child_value (signatures, i); - g_autoptr (GBytes) signature = g_variant_get_data_as_bytes (child); - bool valid = false; - - if (!otcore_validate_ed25519_signature (data, pubkey, signature, &valid, &local_error)) - errx (EXIT_FAILURE, "signature verification failed: %s", local_error->message); - // At least one valid signature is enough. - if (valid) - return TRUE; - } - } - - return FALSE; -} - -// Output a friendly message based on an errno for common cases -static const char * -composefs_error_message (int errsv) -{ - switch (errsv) - { - case ENOVERITY: - return "fsverity not enabled on composefs image"; - case EWRONGVERITY: - return "Wrong fsverity digest in composefs image"; - case ENOSIGNATURE: - return "Missing signature for fsverity in composefs image"; - default: - return strerror (errsv); - } -} - -#endif - int main (int argc, char *argv[]) { @@ -386,130 +274,9 @@ main (int argc, char *argv[]) // Tracks if we did successfully enable it at runtime bool using_composefs = false; - -#ifdef HAVE_COMPOSEFS - /* We construct the new sysroot in /sysroot.tmp, which is either the composefs - mount or a bind mount of the deploy-dir */ - if (composefs_config->enabled != OT_TRISTATE_NO) - { - const char *objdirs[] = { "/sysroot/ostree/repo/objects" }; - g_autofree char *cfs_digest = NULL; - struct lcfs_mount_options_s cfs_options = { - objdirs, - 1, - }; - - cfs_options.flags = 0; - cfs_options.image_mountdir = OSTREE_COMPOSEFS_LOWERMNT; - if (mkdirat (AT_FDCWD, OSTREE_COMPOSEFS_LOWERMNT, 0700) < 0 && errno != EEXIST) - err (EXIT_FAILURE, "Failed to create %s", OSTREE_COMPOSEFS_LOWERMNT); - - g_autofree char *expected_digest = NULL; - - // For now we just stick the transient root on the default /run tmpfs; - // however, see - // https://github.com/systemd/systemd/blob/604b2001081adcbd64ee1fbe7de7a6d77c5209fe/src/basic/mountpoint-util.h#L36 - // which bumps up these defaults for the rootfs a bit. - g_autofree char *root_upperdir - = root_transient ? g_build_filename (OTCORE_RUN_OSTREE_PRIVATE, "root/upper", NULL) - : NULL; - g_autofree char *root_workdir - = root_transient ? g_build_filename (OTCORE_RUN_OSTREE_PRIVATE, "root/work", NULL) : NULL; - - // Propagate these options for transient root, if provided - if (root_transient) - { - if (!glnx_shutil_mkdir_p_at (AT_FDCWD, root_upperdir, 0755, NULL, &error)) - errx (EXIT_FAILURE, "Failed to create %s: %s", root_upperdir, error->message); - if (!glnx_shutil_mkdir_p_at (AT_FDCWD, root_workdir, 0700, NULL, &error)) - errx (EXIT_FAILURE, "Failed to create %s: %s", root_workdir, error->message); - - cfs_options.workdir = root_workdir; - cfs_options.upperdir = root_upperdir; - } - else - { - cfs_options.flags = LCFS_MOUNT_FLAGS_READONLY; - } - - if (composefs_config->is_signed) - { - const char *composefs_pubkey = composefs_config->signature_pubkey; - g_autoptr (GError) local_error = NULL; - g_autoptr (GVariant) commit = NULL; - g_autoptr (GVariant) commitmeta = NULL; - - if (!load_commit_for_deploy (root_mountpoint, deploy_path, &commit, &commitmeta, - &local_error)) - errx (EXIT_FAILURE, "Error loading signatures from repo: %s", local_error->message); - - g_autoptr (GVariant) signatures = g_variant_lookup_value ( - commitmeta, OSTREE_SIGN_METADATA_ED25519_KEY, G_VARIANT_TYPE ("aay")); - if (signatures == NULL) - errx (EXIT_FAILURE, "Signature validation requested, but no signatures in commit"); - - g_autoptr (GBytes) commit_data = g_variant_get_data_as_bytes (commit); - if (!validate_signature (commit_data, signatures, composefs_config->pubkeys)) - errx (EXIT_FAILURE, "No valid signatures found for public key"); - - g_print ("composefs+ostree: Validated commit signature using '%s'\n", composefs_pubkey); - g_variant_builder_add (&metadata_builder, "{sv}", - OTCORE_RUN_BOOTED_KEY_COMPOSEFS_SIGNATURE, - g_variant_new_string (composefs_pubkey)); - - g_autoptr (GVariant) metadata = g_variant_get_child_value (commit, 0); - g_autoptr (GVariant) cfs_digest_v = g_variant_lookup_value ( - metadata, OSTREE_COMPOSEFS_DIGEST_KEY_V0, G_VARIANT_TYPE_BYTESTRING); - if (cfs_digest_v == NULL || g_variant_get_size (cfs_digest_v) != OSTREE_SHA256_DIGEST_LEN) - errx (EXIT_FAILURE, "Signature validation requested, but no valid digest in commit"); - const guint8 *cfs_digest_buf = ot_variant_get_data (cfs_digest_v, &error); - if (!cfs_digest_buf) - errx (EXIT_FAILURE, "Failed to query digest: %s", error->message); - - expected_digest = g_malloc (OSTREE_SHA256_STRING_LEN + 1); - ot_bin2hex (expected_digest, cfs_digest_buf, g_variant_get_size (cfs_digest_v)); - - g_assert (composefs_config->require_verity); - cfs_options.flags |= LCFS_MOUNT_FLAGS_REQUIRE_VERITY; - g_print ("composefs: Verifying digest: %s\n", expected_digest); - cfs_options.expected_fsverity_digest = expected_digest; - } - else if (composefs_config->require_verity) - { - cfs_options.flags |= LCFS_MOUNT_FLAGS_REQUIRE_VERITY; - } - - if (lcfs_mount_image (OSTREE_COMPOSEFS_NAME, TMP_SYSROOT, &cfs_options) == 0) - { - using_composefs = true; - bool using_verity = (cfs_options.flags & LCFS_MOUNT_FLAGS_REQUIRE_VERITY) > 0; - g_variant_builder_add (&metadata_builder, "{sv}", OTCORE_RUN_BOOTED_KEY_COMPOSEFS, - g_variant_new_boolean (true)); - g_variant_builder_add (&metadata_builder, "{sv}", OTCORE_RUN_BOOTED_KEY_COMPOSEFS_VERITY, - g_variant_new_boolean (using_verity)); - g_print ("composefs: mounted successfully (verity=%s)\n", - using_verity ? "true" : "false"); - } - else - { - int errsv = errno; - g_assert (composefs_config->enabled != OT_TRISTATE_NO); - if (composefs_config->enabled == OT_TRISTATE_MAYBE && errsv == ENOENT) - { - g_print ("composefs: No image present\n"); - } - else - { - const char *errmsg = composefs_error_message (errsv); - errx (EXIT_FAILURE, "composefs: failed to mount: %s", errmsg); - } - } - } -#else - /* if composefs is configured as "maybe", we should continue */ - if (composefs_config->enabled == OT_TRISTATE_YES) - errx (EXIT_FAILURE, "composefs: enabled at runtime, but support is not compiled in"); -#endif + if (!otcore_mount_composefs (composefs_config, &metadata_builder, root_transient, root_mountpoint, + deploy_path, TMP_SYSROOT, &using_composefs, &error)) + errx (EXIT_FAILURE, "Failed to mount composefs: %s", error->message); if (!using_composefs) {