1
0
mirror of https://github.com/ostreedev/ostree.git synced 2026-02-05 09:44:55 +01:00

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.
This commit is contained in:
Colin Walters
2025-06-19 07:42:48 -04:00
parent e4590bc130
commit 2027653ad7
4 changed files with 273 additions and 237 deletions

View File

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

View File

@@ -18,6 +18,14 @@
#include "config.h"
#include "otcore.h"
#include <errno.h>
#include <ostree-core.h>
#include <ostree-repo-private.h>
#ifdef HAVE_COMPOSEFS
#include <libcomposefs/lcfs-mount.h>
#include <libcomposefs/lcfs-writer.h>
#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;
}

View File

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

View File

@@ -90,11 +90,6 @@
// A temporary mount point
#define TMP_SYSROOT "/sysroot.tmp"
#ifdef HAVE_COMPOSEFS
#include <libcomposefs/lcfs-mount.h>
#include <libcomposefs/lcfs-writer.h>
#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)
{