diff --git a/Makefile-tests.am b/Makefile-tests.am index e1cea7fe08..eae9f3182c 100644 --- a/Makefile-tests.am +++ b/Makefile-tests.am @@ -104,6 +104,7 @@ _installed_or_uninstalled_test_scripts = \ tests/test-admin-upgrade-systemd-update.sh \ tests/test-admin-deploy-syslinux.sh \ tests/test-admin-deploy-bootprefix.sh \ + tests/test-admin-deploy-composefs.sh \ tests/test-admin-deploy-2.sh \ tests/test-admin-deploy-karg.sh \ tests/test-admin-deploy-switch.sh \ diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 3a537fcb58..49c3a6abbd 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -661,8 +661,34 @@ checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploy cancellable, error)) return FALSE; + glnx_autofd int ret_deployment_dfd = -1; + if (!glnx_opendirat (osdeploy_dfd, checkout_target_name, TRUE, &ret_deployment_dfd, error)) + return FALSE; + #ifdef HAVE_COMPOSEFS - if (repo->composefs_wanted != OT_TRISTATE_NO) + /* TODO: Consider changing things in the future to parse the deployment config from memory, and + * if composefs is enabled, then we can check out in "user mode" (i.e. only have suid binaries + * enabled in composefs, etc.) + * + * However in practice we should get this for free by going to composefs-native backing + * storage. + */ + g_autoptr (GKeyFile) prepare_root_config + = otcore_load_config (ret_deployment_dfd, PREPARE_ROOT_CONFIG_PATH, error); + if (!prepare_root_config) + return glnx_prefix_error (error, "Parsing prepare-root config"); + // We always parse the composefs config, because we want to detect and error + // out if it's enabled, but not supported at compile time. + g_autoptr (ComposefsConfig) composefs_config + = otcore_load_composefs_config (prepare_root_config, error); + if (!composefs_config) + return glnx_prefix_error (error, "Reading composefs config"); + + OtTristate composefs_enabled = composefs_config->enabled; + g_debug ("composefs enabled by config: %d repo: %d", composefs_enabled, repo->composefs_wanted); + if (repo->composefs_wanted == OT_TRISTATE_YES) + composefs_enabled = repo->composefs_wanted; + if (composefs_enabled == OT_TRISTATE_YES) { g_autofree guchar *fsverity_digest = NULL; g_auto (GLnxTmpfile) tmpf = { @@ -691,6 +717,8 @@ checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploy g_autofree char *composefs_cfs_path = g_strdup_printf ("%s/" OSTREE_COMPOSEFS_NAME, checkout_target_name); + g_debug ("writing %s", composefs_cfs_path); + if (!glnx_open_tmpfile_linkable_at (osdeploy_dfd, checkout_target_name, O_WRONLY | O_CLOEXEC, &tmpf, error)) return FALSE; @@ -712,9 +740,13 @@ checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploy error)) return FALSE; } + else + g_debug ("not using composefs"); #endif - return glnx_opendirat (osdeploy_dfd, checkout_target_name, TRUE, out_deployment_dfd, error); + if (out_deployment_dfd) + *out_deployment_dfd = glnx_steal_fd (&ret_deployment_dfd); + return TRUE; } static char * diff --git a/src/libotcore/otcore-prepare-root.c b/src/libotcore/otcore-prepare-root.c index f7523ef65e..42f92c9124 100644 --- a/src/libotcore/otcore-prepare-root.c +++ b/src/libotcore/otcore-prepare-root.c @@ -19,6 +19,12 @@ #include "otcore.h" +// This key is used by default if present in the initramfs to verify +// the signature on the target commit object. When composefs is +// in use, the ostree commit metadata will contain the composefs image digest, +// which can be used to fully verify the target filesystem tree. +#define BINDING_KEYPATH "/etc/ostree/initramfs-root-binding.key" + static bool proc_cmdline_has_key_starting_with (const char *cmdline, const char *key) { @@ -137,3 +143,69 @@ otcore_load_config (int rootfs_fd, const char *filename, GError **error) return g_steal_pointer (&ret); } + +void +otcore_free_composefs_config (ComposefsConfig *config) +{ + g_clear_pointer (&config->pubkeys, g_ptr_array_unref); + g_free (config->signature_pubkey); + g_free (config); +} + +// Parse the [composefs] section of the prepare-root.conf. +ComposefsConfig * +otcore_load_composefs_config (GKeyFile *config, GError **error) +{ + GLNX_AUTO_PREFIX_ERROR ("Loading composefs config", error); + + g_autoptr (ComposefsConfig) ret = g_new0 (ComposefsConfig, 1); + + g_autofree char *enabled = g_key_file_get_value (config, OTCORE_PREPARE_ROOT_COMPOSEFS_KEY, + OTCORE_PREPARE_ROOT_ENABLED_KEY, NULL); + if (g_strcmp0 (enabled, "signed") == 0) + { + ret->enabled = OT_TRISTATE_YES; + ret->is_signed = true; + } + else if (!ot_keyfile_get_tristate_with_default (config, OTCORE_PREPARE_ROOT_COMPOSEFS_KEY, + OTCORE_PREPARE_ROOT_ENABLED_KEY, + OT_TRISTATE_MAYBE, &ret->enabled, error)) + return NULL; + + // Look for a key - we default to the initramfs binding path. + if (!ot_keyfile_get_value_with_default (config, OTCORE_PREPARE_ROOT_COMPOSEFS_KEY, + OTCORE_PREPARE_ROOT_KEYPATH_KEY, BINDING_KEYPATH, + &ret->signature_pubkey, error)) + return NULL; + + if (ret->is_signed) + { + ret->pubkeys = g_ptr_array_new_with_free_func ((GDestroyNotify)g_bytes_unref); + + g_autofree char *pubkeys = NULL; + gsize pubkeys_size; + + /* Load keys */ + + if (!g_file_get_contents (ret->signature_pubkey, &pubkeys, &pubkeys_size, error)) + return glnx_prefix_error_null (error, "Reading public key file '%s'", + ret->signature_pubkey); + + g_auto (GStrv) lines = g_strsplit (pubkeys, "\n", -1); + for (char **iter = lines; *iter; iter++) + { + const char *line = *iter; + if (!*line) + continue; + + gsize pubkey_size; + g_autofree guchar *pubkey = g_base64_decode (line, &pubkey_size); + g_ptr_array_add (ret->pubkeys, g_bytes_new_take (g_steal_pointer (&pubkey), pubkey_size)); + } + + if (ret->pubkeys->len == 0) + return glnx_null_throw (error, "public key file specified, but no public keys found"); + } + + return g_steal_pointer (&ret); +} diff --git a/src/libotcore/otcore.h b/src/libotcore/otcore.h index 752c0758e4..5fd24ec9d3 100644 --- a/src/libotcore/otcore.h +++ b/src/libotcore/otcore.h @@ -48,6 +48,18 @@ gboolean otcore_get_ostree_target (const char *cmdline, char **out_target, GErro GKeyFile *otcore_load_config (int rootfs, const char *filename, GError **error); +typedef struct +{ + OtTristate enabled; + gboolean is_signed; + char *signature_pubkey; + GPtrArray *pubkeys; +} ComposefsConfig; +void otcore_free_composefs_config (ComposefsConfig *config); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ComposefsConfig, otcore_free_composefs_config) + +ComposefsConfig *otcore_load_composefs_config (GKeyFile *config, 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" @@ -55,6 +67,8 @@ GKeyFile *otcore_load_config (int rootfs, const char *filename, GError **error); // we make it with mode 0 (which requires CAP_DAC_OVERRIDE to pass through). #define OTCORE_RUN_OSTREE_PRIVATE "/run/ostree/.private" +#define PREPARE_ROOT_CONFIG_PATH "ostree/prepare-root.conf" + // The directory holding extra/backing data for a deployment, such as overlayfs workdirs #define OSTREE_DEPLOYMENT_BACKING_DIR "backing" // The directory holding the root overlayfs @@ -70,6 +84,10 @@ GKeyFile *otcore_load_config (int rootfs, const char *filename, GError **error); // EROFS mount if we somehow leaked it (but it *should* be unmounted always). #define OSTREE_COMPOSEFS_LOWERMNT OTCORE_RUN_OSTREE_PRIVATE "/cfsroot-lower" +#define OTCORE_PREPARE_ROOT_COMPOSEFS_KEY "composefs" +#define OTCORE_PREPARE_ROOT_ENABLED_KEY "enabled" +#define OTCORE_PREPARE_ROOT_KEYPATH_KEY "keypath" + // The file written in the initramfs which contains an a{sv} of metadata // from ostree-prepare-root. #define OTCORE_RUN_BOOTED "/run/ostree-booted" diff --git a/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c index 28e422f430..2d01482c75 100644 --- a/src/switchroot/ostree-prepare-root.c +++ b/src/switchroot/ostree-prepare-root.c @@ -76,14 +76,6 @@ #include "ot-keyfile-utils.h" #include "otcore.h" -#define PREPARE_ROOT_CONFIG_PATH "ostree/prepare-root.conf" - -// This key is used by default if present in the initramfs to verify -// the signature on the target commit object. When composefs is -// in use, the ostree commit metadata will contain the composefs image digest, -// which can be used to fully verify the target filesystem tree. -#define BINDING_KEYPATH "/etc/ostree/initramfs-root-binding.key" - #define SYSROOT_KEY "sysroot" #define READONLY_KEY "readonly" @@ -92,10 +84,6 @@ #define ETC_KEY "etc" #define TRANSIENT_KEY "transient" -#define COMPOSEFS_KEY "composefs" -#define ENABLED_KEY "enabled" -#define KEYPATH_KEY "keypath" - #define OSTREE_PREPARE_ROOT_DEPLOYMENT_MSG \ SD_ID128_MAKE (71, 70, 33, 6a, 73, ba, 46, 01, ba, d3, 1a, f8, 88, aa, 0d, f7) @@ -258,79 +246,6 @@ composefs_error_message (int errsv) #endif -typedef struct -{ - OtTristate enabled; - gboolean is_signed; - char *signature_pubkey; - GPtrArray *pubkeys; -} ComposefsConfig; - -static void -free_composefs_config (ComposefsConfig *config) -{ - g_ptr_array_unref (config->pubkeys); - g_free (config->signature_pubkey); - g_free (config); -} - -G_DEFINE_AUTOPTR_CLEANUP_FUNC (ComposefsConfig, free_composefs_config) - -// Parse the [composefs] section of the prepare-root.conf. -static ComposefsConfig * -load_composefs_config (GKeyFile *config, GError **error) -{ - GLNX_AUTO_PREFIX_ERROR ("Loading composefs config", error); - - g_autoptr (ComposefsConfig) ret = g_new0 (ComposefsConfig, 1); - - g_autofree char *enabled = g_key_file_get_value (config, COMPOSEFS_KEY, ENABLED_KEY, NULL); - if (g_strcmp0 (enabled, "signed") == 0) - { - ret->enabled = OT_TRISTATE_YES; - ret->is_signed = true; - } - else if (!ot_keyfile_get_tristate_with_default (config, COMPOSEFS_KEY, ENABLED_KEY, - OT_TRISTATE_MAYBE, &ret->enabled, error)) - return NULL; - - // Look for a key - we default to the initramfs binding path. - if (!ot_keyfile_get_value_with_default (config, COMPOSEFS_KEY, KEYPATH_KEY, BINDING_KEYPATH, - &ret->signature_pubkey, error)) - return NULL; - - if (ret->is_signed) - { - ret->pubkeys = g_ptr_array_new_with_free_func ((GDestroyNotify)g_bytes_unref); - - g_autofree char *pubkeys = NULL; - gsize pubkeys_size; - - /* Load keys */ - - if (!g_file_get_contents (ret->signature_pubkey, &pubkeys, &pubkeys_size, error)) - return glnx_prefix_error_null (error, "Reading public key file '%s'", - ret->signature_pubkey); - - g_auto (GStrv) lines = g_strsplit (pubkeys, "\n", -1); - for (char **iter = lines; *iter; iter++) - { - const char *line = *iter; - if (!*line) - continue; - - gsize pubkey_size; - g_autofree guchar *pubkey = g_base64_decode (line, &pubkey_size); - g_ptr_array_add (ret->pubkeys, g_bytes_new_take (g_steal_pointer (&pubkey), pubkey_size)); - } - - if (ret->pubkeys->len == 0) - return glnx_null_throw (error, "public key file specified, but no public keys found"); - } - - return g_steal_pointer (&ret); -} - int main (int argc, char *argv[]) { @@ -362,7 +277,7 @@ main (int argc, char *argv[]) // We always parse the composefs config, because we want to detect and error // out if it's enabled, but not supported at compile time. - g_autoptr (ComposefsConfig) composefs_config = load_composefs_config (config, &error); + g_autoptr (ComposefsConfig) composefs_config = otcore_load_composefs_config (config, &error); if (!composefs_config) errx (EXIT_FAILURE, "%s", error->message); diff --git a/tests/admin-test.sh b/tests/admin-test.sh index 17dbc6111c..0179067593 100644 --- a/tests/admin-test.sh +++ b/tests/admin-test.sh @@ -63,6 +63,11 @@ assert_not_file_has_content status.txt "pending" assert_not_file_has_content status.txt "rollback" validate_bootloader +# Someday probably soon we'll turn this on by default, but for now +if test -f sysroot/ostree/deploy/testos/deploy/*.0/.ostree.cfs; then + fatal "found composefs unexpectedly" +fi + # Test the bootable and linux keys ${CMD_PREFIX} ostree --repo=sysroot/ostree/repo --print-metadata-key=ostree.linux show testos:testos/buildmain/x86_64-runtime >out.txt assert_file_has_content_literal out.txt 3.6.0 diff --git a/tests/test-admin-deploy-composefs.sh b/tests/test-admin-deploy-composefs.sh new file mode 100755 index 0000000000..a4565a7416 --- /dev/null +++ b/tests/test-admin-deploy-composefs.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# +# Copyright (C) 2024 Red Hat, Inc. +# +# 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 . + +set -euox pipefail + +. $(dirname $0)/libtest.sh + +# Exports OSTREE_SYSROOT so --sysroot not needed. +setup_os_repository "archive" "syslinux" + +cd osdata +mkdir -p usr/lib/ostree +cat > usr/lib/ostree/prepare-root.conf << 'EOF' +[composefs] +enabled=true +EOF +${CMD_PREFIX} ostree --repo=${test_tmpdir}/testos-repo commit --add-metadata-string version=1.composefs -b testos/buildmain/x86_64-runtime +cd - +${CMD_PREFIX} ostree --repo=sysroot/ostree/repo pull-local --remote=testos testos-repo testos/buildmain/x86_64-runtime + +${CMD_PREFIX} ostree admin deploy --os=testos --karg=root=LABEL=foo --karg=testkarg=1 testos:testos/buildmain/x86_64-runtime +ls sysroot/ostree/deploy/testos/deploy/*.0/.ostree.cfs + +tap_ok composefs + +tap_end