lib: Rework composefs metadata, drop custom signatures

We will be switching to handling signature verification of
the target ostree commit.
This commit is contained in:
Colin Walters 2023-06-16 15:35:50 -04:00
parent 1fe2bb9f5a
commit a6f2d053c8
13 changed files with 111 additions and 377 deletions

View File

@ -174,9 +174,9 @@ endif # USE_GPGME
symbol_files = $(top_srcdir)/src/libostree/libostree-released.sym
# Uncomment this include when adding new development symbols.
#if BUILDOPT_IS_DEVEL_BUILD
#symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym
#endif
if BUILDOPT_IS_DEVEL_BUILD
symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym
endif
# http://blog.jgc.org/2007/06/escaping-comma-and-space-in-gnu-make.html
wl_versionscript_arg = -Wl,--version-script=

View File

@ -72,6 +72,7 @@ _installed_or_uninstalled_test_scripts = \
tests/test-remote-add.sh \
tests/test-remote-headers.sh \
tests/test-remote-refs.sh \
tests/test-composefs.sh \
tests/test-commit-sign.sh \
tests/test-commit-timestamp.sh \
tests/test-export.sh \

View File

@ -426,6 +426,7 @@ ostree_repo_write_commit
ostree_repo_write_commit_with_time
ostree_repo_read_commit_detached_metadata
ostree_repo_write_commit_detached_metadata
ostree_repo_commit_add_composefs_metadata
OstreeRepoCheckoutAtOptions
ostree_repo_checkout_at_options_set_devino
OstreeRepoCheckoutMode

View File

@ -20,6 +20,11 @@
- uncomment the include in Makefile-libostree.am
*/
LIBOSTREE_2023.4 {
global:
ostree_repo_commit_add_composefs_metadata;
} LIBOSTREE_2023.1;
/* Stub section for the stable release *after* this development one; don't
* edit this other than to update the year. This is just a copy/paste
* source. Replace $LASTSTABLE with the last stable version, and $NEWVERSION

View File

@ -2916,9 +2916,6 @@ add_auto_metadata (OstreeRepo *self, GVariant *original_metadata, OstreeRepoFile
add_size_index_to_metadata (self, builder);
if (!ostree_repo_commit_add_composefs_metadata (self, builder, repo_root, cancellable, error))
return NULL;
return g_variant_ref_sink (g_variant_builder_end (builder));
}

View File

@ -566,89 +566,46 @@ ostree_repo_checkout_composefs (OstreeRepo *self, OstreeComposefsTarget *target,
#endif
}
#ifdef HAVE_COMPOSEFS
static gboolean
ostree_repo_commit_add_composefs_sig (OstreeRepo *self, GVariantBuilder *builder,
guchar *fsverity_digest, GCancellable *cancellable,
GError **error)
{
g_autofree char *certfile = NULL;
g_autofree char *keyfile = NULL;
g_autoptr (GBytes) sig = NULL;
guchar digest_digest[LCFS_DIGEST_SIZE];
certfile
= g_key_file_get_string (self->config, _OSTREE_INTEGRITY_SECTION, "composefs-certfile", NULL);
keyfile
= g_key_file_get_string (self->config, _OSTREE_INTEGRITY_SECTION, "composefs-keyfile", NULL);
if (certfile == NULL && keyfile == NULL)
return TRUE;
if (certfile == NULL)
return glnx_throw (error, "Error signing compoosefs: keyfile specified but certfile is not");
if (keyfile == NULL)
return glnx_throw (error, "Error signing compoosefs: certfile specified but keyfile is not");
/* We sign not the fs-verity of the image file itself, but rather we sign a file containing
* the fs-verity digest. This may seem weird, but disconnecting the signature from the
* actual image itself has two major advantages:
* * We can read/mount the image (non-verified) even without the public key in
* the keyring.
* * We can apply fs-verity to the image during deploy without the public key in
* the keyring.
*
* This is important because during an update we don't have the public key loaded until
* we boot into the new initrd.
*/
if (lcfs_compute_fsverity_from_data (digest_digest, fsverity_digest, LCFS_DIGEST_SIZE) < 0)
return glnx_throw_errno (error);
if (!_ostree_fsverity_sign (certfile, keyfile, digest_digest, &sig, cancellable, error))
return FALSE;
g_variant_builder_add (builder, "{sv}", "ostree.composefs-sig", ot_gvariant_new_ay_bytes (sig));
return TRUE;
}
#endif
/**
* ostree_repo_commit_add_composefs_metadata:
* @self: Repo
* @format_version: Must be zero
* @dict: A GVariant builder of type a{sv}
* @repo_root: the target filesystem tree
* @cancellable: Cancellable
* @error: Error
*
* Compute the composefs digest for a filesystem tree
* and insert it into metadata for a commit object. The composefs
* digest covers the entire filesystem tree and can be verified by
* the composefs mount tooling.
*/
_OSTREE_PUBLIC
gboolean
ostree_repo_commit_add_composefs_metadata (OstreeRepo *self, GVariantBuilder *builder,
OstreeRepoFile *repo_root, GCancellable *cancellable,
GError **error)
ostree_repo_commit_add_composefs_metadata (OstreeRepo *self, guint format_version,
GVariantDict *dict, OstreeRepoFile *repo_root,
GCancellable *cancellable, GError **error)
{
gboolean add_metadata;
#ifdef HAVE_COMPOSEFS
/* For now */
g_assert (format_version == 0);
if (!ot_keyfile_get_boolean_with_default (self->config, _OSTREE_INTEGRITY_SECTION,
"composefs-add-metadata", FALSE, &add_metadata, error))
/* Create a composefs image and put in deploy dir as .ostree.cfs */
g_autoptr (OstreeComposefsTarget) target = ostree_composefs_target_new ();
if (!ostree_repo_checkout_composefs (self, target, repo_root, cancellable, error))
return FALSE;
if (add_metadata)
{
#ifdef HAVE_COMPOSEFS
/* Create a composefs image and put in deploy dir as .ostree.cfs */
g_autoptr (OstreeComposefsTarget) target = ostree_composefs_target_new ();
g_autofree guchar *fsverity_digest = NULL;
if (!ostree_composefs_target_write (target, -1, &fsverity_digest, cancellable, error))
return FALSE;
if (!ostree_repo_checkout_composefs (self, target, repo_root, cancellable, error))
return FALSE;
g_autofree guchar *fsverity_digest = NULL;
if (!ostree_composefs_target_write (target, -1, &fsverity_digest, cancellable, error))
return FALSE;
g_variant_builder_add (builder, "{sv}", "ostree.composefs",
ot_gvariant_new_bytearray (fsverity_digest, OSTREE_SHA256_DIGEST_LEN));
if (!ostree_repo_commit_add_composefs_sig (self, builder, fsverity_digest, cancellable,
error))
return FALSE;
#else
return composefs_not_supported (error);
#endif
}
g_variant_dict_insert_value (
dict, OSTREE_COMPOSEFS_DIGEST_KEY_V0,
ot_gvariant_new_bytearray (fsverity_digest, OSTREE_SHA256_DIGEST_LEN));
return TRUE;
#else
return composefs_not_supported (error);
#endif
}

View File

@ -65,6 +65,11 @@ G_BEGIN_DECLS
#define OSTREE_COMMIT_TIMESTAMP "ostree.commit.timestamp"
#define OSTREE_COMMIT_VERSION "ostree.commit.version"
// The metadata key for composefs
#define OSTREE_COMPOSEFS_META_PREFIX "ostree.composefs"
// The fs-verity digest of the composefs, version 0
#define OSTREE_COMPOSEFS_DIGEST_KEY_V0 OSTREE_COMPOSEFS_META_PREFIX ".digest.v0"
#define _OSTREE_INTEGRITY_SECTION "ex-integrity"
typedef enum
@ -399,9 +404,6 @@ gboolean _ostree_tmpf_fsverity_core (GLnxTmpfile *tmpf, _OstreeFeatureSupport fs
gboolean _ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GBytes *signature,
GError **error);
gboolean _ostree_fsverity_sign (const char *certfile, const char *keyfile,
const guchar *fsverity_digest, GBytes **data_out,
GCancellable *cancellable, GError **error);
gboolean _ostree_repo_verify_bindings (const char *collection_id, const char *ref_name,
GVariant *commit, GError **error);
@ -465,10 +467,6 @@ gboolean ostree_repo_checkout_composefs (OstreeRepo *self, OstreeComposefsTarget
OstreeRepoFile *source, GCancellable *cancellable,
GError **error);
gboolean ostree_repo_commit_add_composefs_metadata (OstreeRepo *self, GVariantBuilder *builder,
OstreeRepoFile *repo_root,
GCancellable *cancellable, GError **error);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeComposefsTarget, ostree_composefs_target_unref)
G_END_DECLS

View File

@ -206,131 +206,3 @@ _ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GBytes *signature, G
#endif
return TRUE;
}
#if defined(HAVE_OPENSSL)
static gboolean
read_pem_x509_certificate (const char *certfile, X509 **cert_ret, GError **error)
{
g_autoptr (BIO) bio = NULL;
X509 *cert;
errno = 0;
bio = BIO_new_file (certfile, "r");
if (!bio)
return glnx_throw_errno_prefix (error, "Error loading composefs certfile '%s'", certfile);
cert = PEM_read_bio_X509 (bio, NULL, NULL, NULL);
if (!cert)
return glnx_throw (error, "Error parsing composefs certfile '%s'", certfile);
*cert_ret = cert;
return TRUE;
}
static gboolean
read_pem_pkcs8_private_key (const char *keyfile, EVP_PKEY **pkey_ret, GError **error)
{
g_autoptr (BIO) bio;
EVP_PKEY *pkey;
errno = 0;
bio = BIO_new_file (keyfile, "r");
if (!bio)
return glnx_throw_errno_prefix (error, "Error loading composefs keyfile '%s'", keyfile);
pkey = PEM_read_bio_PrivateKey (bio, NULL, NULL, NULL);
if (!pkey)
return glnx_throw (error, "Error parsing composefs keyfile '%s'", keyfile);
*pkey_ret = pkey;
return TRUE;
}
static gboolean
sign_pkcs7 (const void *data_to_sign, size_t data_size, EVP_PKEY *pkey, X509 *cert,
const EVP_MD *md, BIO **res, GError **error)
{
int pkcs7_flags = PKCS7_BINARY | PKCS7_DETACHED | PKCS7_NOATTR | PKCS7_NOCERTS | PKCS7_PARTIAL;
g_autoptr (BIO) bio = NULL;
g_autoptr (BIO) bio_res = NULL;
g_autoptr (PKCS7) p7 = NULL;
bio = BIO_new_mem_buf ((void *)data_to_sign, data_size);
if (!bio)
return glnx_throw (error, "Can't allocate buffer");
p7 = PKCS7_sign (NULL, NULL, NULL, bio, pkcs7_flags);
if (!p7)
return glnx_throw (error, "Can't initialize PKCS#7");
if (!PKCS7_sign_add_signer (p7, cert, pkey, md, pkcs7_flags))
return glnx_throw (error, "Can't add signer to PKCS#7");
if (PKCS7_final (p7, bio, pkcs7_flags) != 1)
return glnx_throw (error, "Can't finalize PKCS#7");
bio_res = BIO_new (BIO_s_mem ());
if (!bio_res)
return glnx_throw (error, "Can't allocate buffer");
if (i2d_PKCS7_bio (bio_res, p7) != 1)
return glnx_throw (error, "Can't DER-encode PKCS#7 signature object");
*res = g_steal_pointer (&bio_res);
return TRUE;
}
gboolean
_ostree_fsverity_sign (const char *certfile, const char *keyfile, const guchar *fsverity_digest,
GBytes **data_out, GCancellable *cancellable, GError **error)
{
g_autofree struct fsverity_formatted_digest *d = NULL;
gsize d_size;
g_autoptr (X509) cert = NULL;
g_autoptr (EVP_PKEY) pkey = NULL;
g_autoptr (BIO) bio_sig = NULL;
const EVP_MD *md;
guchar *sig;
long sig_size;
if (certfile == NULL)
return glnx_throw (error, "certfile not specified");
if (keyfile == NULL)
return glnx_throw (error, "keyfile not specified");
if (!read_pem_x509_certificate (certfile, &cert, error))
return FALSE;
if (!read_pem_pkcs8_private_key (keyfile, &pkey, error))
return FALSE;
md = EVP_sha256 ();
if (md == NULL)
return glnx_throw (error, "No sha256 support in openssl");
d_size = sizeof (struct fsverity_formatted_digest) + OSTREE_SHA256_DIGEST_LEN;
d = g_malloc0 (d_size);
memcpy (d->magic, "FSVerity", 8);
d->digest_algorithm = GUINT16_TO_LE (FS_VERITY_HASH_ALG_SHA256);
d->digest_size = GUINT16_TO_LE (OSTREE_SHA256_DIGEST_LEN);
memcpy (d->digest, fsverity_digest, OSTREE_SHA256_DIGEST_LEN);
if (!sign_pkcs7 (d, d_size, pkey, cert, md, &bio_sig, error))
return FALSE;
sig_size = BIO_get_mem_data (bio_sig, &sig);
*data_out = g_bytes_new (sig, sig_size);
return TRUE;
}
#else
gboolean
_ostree_fsverity_sign (const char *certfile, const char *keyfile, const guchar *fsverity_digest,
GBytes **data_out, GCancellable *cancellable, GError **error)
{
return glnx_throw (error, "fsverity signature support not built");
}
#endif

View File

@ -690,6 +690,11 @@ _OSTREE_PUBLIC
gboolean ostree_repo_write_mtree (OstreeRepo *self, OstreeMutableTree *mtree, GFile **out_file,
GCancellable *cancellable, GError **error);
_OSTREE_PUBLIC
gboolean ostree_repo_commit_add_composefs_metadata (OstreeRepo *self, guint format_version,
GVariantDict *dict, OstreeRepoFile *repo_root,
GCancellable *cancellable, GError **error);
_OSTREE_PUBLIC
gboolean ostree_repo_write_commit (OstreeRepo *self, const char *parent, const char *subject,
const char *body, GVariant *metadata, OstreeRepoFile *root,

View File

@ -659,10 +659,8 @@ checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploy
return FALSE;
g_autoptr (GVariant) metadata = g_variant_get_child_value (commit_variant, 0);
g_autoptr (GVariant) metadata_composefs
= g_variant_lookup_value (metadata, "ostree.composefs", G_VARIANT_TYPE_BYTESTRING);
g_autoptr (GVariant) metadata_composefs_sig
= g_variant_lookup_value (metadata, "ostree.composefs-sig", G_VARIANT_TYPE_BYTESTRING);
g_autoptr (GVariant) metadata_composefs = g_variant_lookup_value (
metadata, OSTREE_COMPOSEFS_DIGEST_KEY_V0, G_VARIANT_TYPE_BYTESTRING);
/* Create a composefs image and put in deploy dir as .ostree.cfs */
g_autoptr (OstreeComposefsTarget) target = ostree_composefs_target_new ();
@ -695,32 +693,6 @@ checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploy
if (!_ostree_tmpf_fsverity (repo, &tmpf, NULL, error))
return FALSE;
if (metadata_composefs && metadata_composefs_sig)
{
g_autofree char *composefs_digest_path
= g_strdup_printf ("%s/.ostree.cfs.digest", checkout_target_name);
g_autofree char *composefs_sig_path
= g_strdup_printf ("%s/.ostree.cfs.sig", checkout_target_name);
g_autoptr (GBytes) digest = g_variant_get_data_as_bytes (metadata_composefs);
g_autoptr (GBytes) sig = g_variant_get_data_as_bytes (metadata_composefs_sig);
if (!glnx_file_replace_contents_at (osdeploy_dfd, composefs_digest_path,
g_bytes_get_data (digest, NULL),
g_bytes_get_size (digest), 0, cancellable, error))
return FALSE;
if (!glnx_file_replace_contents_at (osdeploy_dfd, composefs_sig_path,
g_bytes_get_data (sig, NULL), g_bytes_get_size (sig),
0, cancellable, error))
return FALSE;
/* The signature should be applied as a fs-verity signature to the digest file. However
* we can't do that until boot, because we can't guarantee that the public key is
* loaded into the keyring until we boot the new initrd. So the signature is applied
* in ostree-prepare-root on first boot.
*/
}
if (!glnx_link_tmpfile_at (&tmpf, GLNX_LINK_TMPFILE_REPLACE, osdeploy_dfd, composefs_cfs_path,
error))
return FALSE;

View File

@ -70,6 +70,7 @@ static char *opt_gpg_homedir;
static char **opt_key_ids;
static char *opt_sign_name;
static gboolean opt_generate_sizes;
static gboolean opt_composefs_metadata;
static gboolean opt_disable_fsync;
static char *opt_timestamp;
@ -161,6 +162,8 @@ static GOptionEntry options[] = {
"Signature type to use (defaults to 'ed25519')", "NAME" },
{ "generate-sizes", 0, 0, G_OPTION_ARG_NONE, &opt_generate_sizes,
"Generate size information along with commit metadata", NULL },
{ "generate-composefs-metadata", 0, 0, G_OPTION_ARG_NONE, &opt_composefs_metadata,
"Generate composefs commit metadata", NULL },
{ "disable-fsync", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &opt_disable_fsync,
"Do not invoke fsync()", NULL },
{ "fsync", 0, 0, G_OPTION_ARG_CALLBACK, parse_fsync_cb, "Specify how to invoke fsync()",
@ -872,6 +875,18 @@ ostree_builtin_commit (int argc, char **argv, OstreeCommandInvocation *invocatio
metadata = g_variant_ref_sink (g_variant_dict_end (&bootmeta));
}
if (opt_composefs_metadata)
{
g_autoptr (GVariant) old_metadata = g_steal_pointer (&metadata);
g_auto (GVariantDict) newmeta;
g_variant_dict_init (&newmeta, old_metadata);
if (!ostree_repo_commit_add_composefs_metadata (
repo, 0, &newmeta, OSTREE_REPO_FILE (root), cancellable, error))
goto out;
metadata = g_variant_ref_sink (g_variant_dict_end (&newmeta));
}
if (!opt_timestamp)
{
if (!ostree_repo_write_commit (repo, parent, opt_subject, commit_body, metadata,

View File

@ -181,130 +181,6 @@ resolve_deploy_path (const char *root_mountpoint)
return deploy_path;
}
#ifdef HAVE_COMPOSEFS
static void
apply_digest_signature (const char *digestfile, const char *sigfile)
{
unsigned char *signature;
size_t signature_len;
int digest_is_readonly;
int digest_fd;
signature = read_file (sigfile, &signature_len);
if (signature == NULL)
err (EXIT_FAILURE, "Missing signaure file %s", sigfile);
/* If we're read-only we temporarily make read-write bind mount to sign */
digest_is_readonly = path_is_on_readonly_fs (digestfile);
if (digest_is_readonly)
{
if (mount (digestfile, digestfile, NULL, MS_BIND | MS_SILENT, NULL) < 0)
err (EXIT_FAILURE, "failed to bind mount %s (for signing)", digestfile);
if (mount (digestfile, digestfile, NULL, MS_REMOUNT | MS_SILENT, NULL) < 0)
err (EXIT_FAILURE, "failed to remount %s read-write (for signing)", digestfile);
}
/* Ensure we re-open after any bindmounts */
digest_fd = open (digestfile, O_RDONLY | O_CLOEXEC);
if (digest_fd < 0)
err (EXIT_FAILURE, "failed to open %s", digestfile);
fsverity_sign (digest_fd, signature, signature_len);
close (digest_fd);
if (digest_is_readonly && umount2 (digestfile, MNT_DETACH) < 0)
err (EXIT_FAILURE, "failed to unmount %s (after signing)", digestfile);
free (signature);
#ifdef USE_LIBSYSTEMD
sd_journal_send ("MESSAGE=Applied fsverity signature %s to %s", sigfile, digestfile, NULL);
#endif
}
static void
ensure_digest_fd_is_signed (int digest_fd)
{
struct fsverity_read_metadata_arg read_metadata = { 0 };
char sig_data[1];
int res;
/* We verify there is a signature by reading one byte of it. */
read_metadata.metadata_type = FS_VERITY_METADATA_TYPE_SIGNATURE;
read_metadata.offset = 0;
read_metadata.length = sizeof (sig_data);
read_metadata.buf_ptr = (size_t)&sig_data;
res = ioctl (digest_fd, FS_IOC_READ_VERITY_METADATA, &read_metadata);
if (res == -1)
{
if (errno == ENODATA)
err (EXIT_FAILURE, "Digest file is unexpectedly not signed");
else
err (EXIT_FAILURE, "Failed to get signature from digest file");
}
}
static char *
read_signed_digest (const char *digestfile, const char *sigfile)
{
unsigned fd_flags;
int digest_fd;
unsigned char buf[LCFS_DIGEST_SIZE];
char *digest;
ssize_t bytes_read;
digest_fd = open (digestfile, O_RDONLY | O_CLOEXEC);
if (digest_fd < 0)
err (EXIT_FAILURE, "failed to open %s", digestfile);
/* Check if file is already fsverity */
if (ioctl (digest_fd, FS_IOC_GETFLAGS, &fd_flags) < 0)
err (EXIT_FAILURE, "failed to get fd flags for %s", digestfile);
/* If it is not, apply signature */
if ((fd_flags & FS_VERITY_FL) == 0)
{
close (digest_fd);
apply_digest_signature (digestfile, sigfile);
/* Reopen */
digest_fd = open (digestfile, O_RDONLY | O_CLOEXEC);
if (digest_fd < 0)
err (EXIT_FAILURE, "failed to reopen %s", digestfile);
}
/* By now we know its fs-verify enabled, also ensure it is signed
* with a key in the keyring */
ensure_digest_fd_is_signed (digest_fd);
/* Load the expected digest */
do
bytes_read = read (digest_fd, buf, LCFS_DIGEST_SIZE);
while (bytes_read == -1 && errno == EINTR);
if (bytes_read == -1)
err (EXIT_FAILURE, "Failed to read digest file");
if (bytes_read != LCFS_DIGEST_SIZE)
err (EXIT_FAILURE, "Digest file has wrong size");
digest = malloc (LCFS_DIGEST_SIZE * 2 + 1);
if (digest == NULL)
err (EXIT_FAILURE, "Out of memory");
bin2hex (digest, buf, LCFS_DIGEST_SIZE);
#ifdef USE_LIBSYSTEMD
sd_journal_send ("MESSAGE=Signed digest file found for root", NULL);
#endif
return digest;
}
#endif
static int
pivot_root (const char *new_root, const char *put_old)
{
@ -439,11 +315,7 @@ main (int argc, char *argv[])
};
if (composefs_mode == OSTREE_COMPOSEFS_MODE_SIGNED)
{
composefs_digest
= read_signed_digest (OSTREE_COMPOSEFS_NAME ".digest", OSTREE_COMPOSEFS_NAME ".sig");
composefs_mode = OSTREE_COMPOSEFS_MODE_DIGEST;
}
errx (EXIT_FAILURE, "composefs signature not supported");
cfs_options.flags = LCFS_MOUNT_FLAGS_READONLY;

39
tests/test-composefs.sh Executable file
View File

@ -0,0 +1,39 @@
#!/bin/bash
#
# SPDX-License-Identifier: LGPL-2.0+
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <https://www.gnu.org/licenses/>.
set -euo pipefail
if ! ostree --version | grep -q -e '- composefs'; then
echo "1..0 #SKIP no composefs support compiled in"
exit 0
fi
. $(dirname $0)/libtest.sh
setup_test_repository "bare-user"
cd ${test_tmpdir}
$OSTREE checkout test2 test2-co
$OSTREE commit ${COMMIT_ARGS} -b test-composefs --generate-composefs-metadata test2-co
orig_composefs_digest=$($OSTREE show --print-metadata-key ostree.composefs.v0 test-composefs)
assert_streq "${orig_composefs_digest}" '[byte 0x1f, 0x08, 0xe5, 0x8b, 0x14, 0x3b, 0x75, 0x34, 0x76, 0xb5, 0xef, 0x0c, 0x0c, 0x6e, 0xce, 0xbf, 0xde, 0xbb, 0x6d, 0x40, 0x30, 0x5e, 0x35, 0xbd, 0x6f, 0x8e, 0xc1, 0x9c, 0xd0, 0xd1, 0x5b, 0xae]'
$OSTREE commit ${COMMIT_ARGS} -b test-composefs2 --generate-composefs-metadata test2-co
new_composefs_digest=$($OSTREE show --print-metadata-key ostree.composefs.v0 test-composefs)
assert_streq "${orig_composefs_digest}" "${new_composefs_digest}"
tap_ok "composefs metadata"
tap_end