diff --git a/Makefile-libostree.am b/Makefile-libostree.am index 7f9d7e671b..37318e7ff8 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -101,7 +101,6 @@ libostree_1_la_SOURCES = \ src/libostree/ostree-repo-libarchive.c \ src/libostree/ostree-repo-prune.c \ src/libostree/ostree-repo-refs.c \ - src/libostree/ostree-repo-verity.c \ src/libostree/ostree-repo-traverse.c \ src/libostree/ostree-repo-private.h \ src/libostree/ostree-repo-file.c \ @@ -163,6 +162,9 @@ else # if ENABLE_EXPERIMENTAL_API libostree_1_la_SOURCES += \ $(NULL) endif +if BUILDOPT_FSVERITY +libostree_1_la_SOURCES += src/libostree/ostree-repo-verity.c +endif if USE_AVAHI libostree_1_la_SOURCES += \ @@ -203,7 +205,7 @@ libostree_1_la_CFLAGS = $(AM_CFLAGS) -I$(srcdir)/bsdiff -I$(srcdir)/libglnx -I$( -fvisibility=hidden '-D_OSTREE_PUBLIC=__attribute__((visibility("default"))) extern' libostree_1_la_LDFLAGS = -version-number 1:0:0 -Bsymbolic-functions $(addprefix $(wl_versionscript_arg),$(symbol_files)) libostree_1_la_LIBADD = libotutil.la libglnx.la libbsdiff.la $(OT_INTERNAL_GIO_UNIX_LIBS) $(OT_INTERNAL_GPGME_LIBS) \ - $(OT_DEP_LZMA_LIBS) $(OT_DEP_ZLIB_LIBS) $(OT_DEP_CRYPTO_LIBS) + $(OT_DEP_LZMA_LIBS) $(OT_DEP_ZLIB_LIBS) $(OT_DEP_CRYPTO_LIBS) $(FSVERITY_LIBS) # Some change between rust-1.21.0-1.fc27 and rust-1.22.1-1.fc27.x86_64 if ENABLE_RUST libostree_1_la_LIBADD += -ldl diff --git a/configure.ac b/configure.ac index 8ffdf64b77..1a6ea9383f 100644 --- a/configure.ac +++ b/configure.ac @@ -252,14 +252,31 @@ AS_IF([test x$with_ed25519_libsodium != xno], [ ], with_ed25519_libsodium=no ) AM_CONDITIONAL(USE_LIBSODIUM, test "x$have_libsodium" = xyes) +AC_ARG_WITH(fsverity, + AS_HELP_STRING([--with-fsverity], [Support fsverity @<:@default=maybe@:>@]), + [], [with_fsverity=maybe]) +AS_IF([test x$with_fsverity != xno], [ + AC_CHECK_HEADERS([libfsverity.h]) + AM_CONDITIONAL(BUILDOPT_FSVERITY, [test x$ac_cv_header_libfsverity_h = xyes]) + AM_COND_IF([BUILDOPT_FSVERITY], + [ + AC_DEFINE([BUILDOPT_FSVERITY], 1, [Define if using fsverity]) + OSTREE_FEATURES="$OSTREE_FEATURES ex-fsverity" + buildopt_fsverity=yes + FSVERITY_LIBS="-lfsverity" + AC_SUBST(FSVERITY_LIBS) + ], + [ + AS_IF([test x$with_fsverity != xyes], [AC_MSG_ERROR([libfsverity requested but not found])]) + buildopt_fsverity=no + ] + ) +]) + LIBARCHIVE_DEPENDENCY="libarchive >= 2.8.0" # What's in RHEL7.2. FUSE_DEPENDENCY="fuse >= 2.9.2" -AC_CHECK_HEADERS([linux/fsverity.h]) -AS_IF([test x$ac_cv_header_linux_fsverity_h = xyes ], - [OSTREE_FEATURES="$OSTREE_FEATURES ex-fsverity"]) - # check for gtk-doc m4_ifdef([GTK_DOC_CHECK], [ GTK_DOC_CHECK([1.15], [--flavour no-tmpl]) @@ -632,7 +649,7 @@ echo " HTTP backend: $fetcher_backend \"ostree trivial-httpd\": $enable_trivial_httpd_cmdline SELinux: $with_selinux - fs-verity: $ac_cv_header_linux_fsverity_h + fs-verity: $buildopt_fsverity cryptographic checksums: $with_crypto systemd: $with_libsystemd libmount: $with_libmount diff --git a/src/libostree/ostree-repo-private.h b/src/libostree/ostree-repo-private.h index 6c01bc6bc4..4e0c46bdcc 100644 --- a/src/libostree/ostree-repo-private.h +++ b/src/libostree/ostree-repo-private.h @@ -162,8 +162,11 @@ struct OstreeRepo { GMutex txn_lock; OstreeRepoTxn txn; gboolean txn_locked; - _OstreeFeatureSupport fs_verity_wanted; - _OstreeFeatureSupport fs_verity_supported; + gboolean fs_verity_wanted; +#ifdef BUILDOPT_FSVERITY + char *fsverity_cert; + char *fsverity_key; +#endif GMutex cache_lock; guint dirmeta_cache_refcount; @@ -521,18 +524,21 @@ OstreeRepoAutoLock * _ostree_repo_auto_lock_push (OstreeRepo *self, void _ostree_repo_auto_lock_cleanup (OstreeRepoAutoLock *lock); G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoAutoLock, _ostree_repo_auto_lock_cleanup) -gboolean _ostree_repo_parse_fsverity_config (OstreeRepo *self, GError **error); - -gboolean -_ostree_tmpf_fsverity_core (GLnxTmpfile *tmpf, - _OstreeFeatureSupport fsverity_requested, - gboolean *supported, - GError **error); - +#define _OSTREE_FSVERITY_CONFIG_KEY "ex-fsverity" +#ifdef BUILDOPT_FSVERITY +gboolean +_ostree_repo_parse_fsverity_config (OstreeRepo *self, GError **error); gboolean _ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GError **error); +#else +static inline gboolean +_ostree_repo_parse_fsverity_config (OstreeRepo *self, GError **error) { return TRUE;} +static inline gboolean +_ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GError **error) { return TRUE; } +#endif + gboolean _ostree_repo_verify_bindings (const char *collection_id, diff --git a/src/libostree/ostree-repo-verity.c b/src/libostree/ostree-repo-verity.c index 92b026eeb4..0ccd491219 100644 --- a/src/libostree/ostree-repo-verity.c +++ b/src/libostree/ostree-repo-verity.c @@ -2,7 +2,7 @@ * Copyright (C) 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 @@ -27,94 +27,98 @@ #include "ostree-repo-private.h" #include "otutil.h" #include "ot-fs-utils.h" -#ifdef HAVE_LINUX_FSVERITY_H -#include -#endif +#include -gboolean -_ostree_repo_parse_fsverity_config (OstreeRepo *self, GError **error) -{ - /* Currently experimental */ - static const char fsverity_key[] = "ex-fsverity"; - self->fs_verity_wanted = _OSTREE_FEATURE_NO; -#ifdef HAVE_LINUX_FSVERITY_H - self->fs_verity_supported = _OSTREE_FEATURE_MAYBE; -#else - self->fs_verity_supported = _OSTREE_FEATURE_NO; -#endif - gboolean fsverity_required = FALSE; - if (!ot_keyfile_get_boolean_with_default (self->config, fsverity_key, "required", - FALSE, &fsverity_required, error)) - return FALSE; - if (fsverity_required) - { - self->fs_verity_wanted = _OSTREE_FEATURE_YES; - if (self->fs_verity_supported == _OSTREE_FEATURE_NO) - return glnx_throw (error, "fsverity required, but libostree compiled without support"); - } - else - { - gboolean fsverity_opportunistic = FALSE; - if (!ot_keyfile_get_boolean_with_default (self->config, fsverity_key, "opportunistic", - FALSE, &fsverity_opportunistic, error)) - return FALSE; - if (fsverity_opportunistic) - self->fs_verity_wanted = _OSTREE_FEATURE_MAYBE; - } +/* libfsverity has a thread-unsafe process global error callback */ +G_LOCK_DEFINE_STATIC(ot_fsverity_err_lock); +static char *ot_last_fsverity_error; - return TRUE; +static void +ot_on_libfsverity_error(const char *msg) +{ + G_LOCK (ot_fsverity_err_lock); + g_free (ot_last_fsverity_error); + ot_last_fsverity_error = g_strdup (msg); + G_UNLOCK (ot_fsverity_err_lock); } +static gboolean +throw_fsverity_error (GError **error, const char *prefix) +{ + g_autofree char *msg = NULL; + G_LOCK (ot_fsverity_err_lock); + msg = g_steal_pointer (&ot_last_fsverity_error); + G_UNLOCK (ot_fsverity_err_lock); + return glnx_throw (error, "%s: %s", prefix, msg); +} -/* Wrapper around the fsverity ioctl, compressing the result to - * "success, unsupported or error". This is used for /boot where - * we enable verity if supported. - * */ -gboolean -_ostree_tmpf_fsverity_core (GLnxTmpfile *tmpf, - _OstreeFeatureSupport fsverity_requested, - gboolean *supported, - GError **error) +static char * +maybe_make_repo_relative (OstreeRepo *repo, const char *path) { - /* Set this by default to simplify the code below */ - if (supported) - *supported = FALSE; + if (path[0] == '/') + return g_strdup (path); + return g_strdup_printf ("/proc/self/fd/%d/%s", repo->repo_dir_fd, glnx_basename (path)); +} - if (fsverity_requested == _OSTREE_FEATURE_NO) +gboolean +_ostree_repo_parse_fsverity_config (OstreeRepo *self, GError **error) +{ + struct stat stbuf; + g_autofree char *keypath = NULL; + if (!ot_keyfile_get_value_with_default (self->config, _OSTREE_FSVERITY_CONFIG_KEY, "key", + NULL, &keypath, error)) + return FALSE; + /* If no key is set, we're done */ + if (keypath == NULL) return TRUE; -#ifdef HAVE_LINUX_FSVERITY_H - GLNX_AUTO_PREFIX_ERROR ("fsverity", error); + self->fsverity_key = maybe_make_repo_relative (self, keypath); + if (!glnx_fstatat (self->repo_dir_fd, self->fsverity_key, &stbuf, 0, error)) + return glnx_prefix_error (error, "Couldn't access fsverity key"); + /* Enforce not-world-readable for the same reason as ssh */ + if (stbuf.st_mode & S_IROTH) + return glnx_throw (error, "fsverity key must not be world-readable"); - /* fs-verity requires a read-only file descriptor */ - if (!glnx_tmpfile_reopen_rdonly (tmpf, error)) + g_autofree char *certpath = NULL; + if (!ot_keyfile_get_value_with_default (self->config, _OSTREE_FSVERITY_CONFIG_KEY, "cert", NULL, &certpath, error)) return FALSE; + if (!certpath) + return glnx_throw (error, "fsverity key specified, but no certificate"); + self->fsverity_cert = maybe_make_repo_relative (self, certpath); + if (!glnx_fstatat (self->repo_dir_fd, self->fsverity_cert, &stbuf, 0, error)) + return glnx_prefix_error (error, "Couldn't access fsverity certificate"); + + /* Process global state is bad. We want to support multiple ostree repos per process. + * At some point we should try patching + * libfsverity to have something GError like that gives us a string too. + */ + static gsize initialized = 0; + if (g_once_init_enter (&initialized)) + { + libfsverity_set_error_callback (ot_on_libfsverity_error); + g_once_init_leave (&initialized, 1); + } - struct fsverity_enable_arg arg = { 0, }; - arg.version = 1; - arg.hash_algorithm = FS_VERITY_HASH_ALG_SHA256; /* TODO configurable? */ - arg.block_size = 4096; /* FIXME query */ - arg.salt_size = 0; /* TODO store salt in ostree repo config */ - arg.salt_ptr = 0; - arg.sig_size = 0; /* We don't currently expect use of in-kernel signature verification */ - arg.sig_ptr = 0; + return TRUE; +} - if (ioctl (tmpf->fd, FS_IOC_ENABLE_VERITY, &arg) < 0) +static int +ot_fsverity_read_callback (void *file, void *bufp, size_t count) +{ + errno = 0; + int fd = GPOINTER_TO_INT (file); + guint8* buf = bufp; + while (count > 0) { - switch (errno) - { - case ENOTTY: - case EOPNOTSUPP: - return TRUE; - default: - return glnx_throw_errno_prefix (error, "ioctl(FS_IOC_ENABLE_VERITY)"); - } + ssize_t n = read (fd, buf, MIN (count, INT_MAX)); + if (n < 0) + return -errno; + if (n == 0) + return -EIO; + buf += n; + count -= n; } - - if (supported) - *supported = TRUE; -#endif - return TRUE; + return 0; } /* Enable verity on a file, respecting the "wanted" and "supported" states. @@ -127,48 +131,42 @@ _ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GError **error) { -#ifdef HAVE_LINUX_FSVERITY_H - g_mutex_lock (&self->txn_lock); - _OstreeFeatureSupport fsverity_wanted = self->fs_verity_wanted; - _OstreeFeatureSupport fsverity_supported = self->fs_verity_supported; - g_mutex_unlock (&self->txn_lock); + GLNX_AUTO_PREFIX_ERROR ("ostree/fsverity", error); - switch (fsverity_wanted) - { - case _OSTREE_FEATURE_YES: - { - if (fsverity_supported == _OSTREE_FEATURE_NO) - return glnx_throw (error, "fsverity required but filesystem does not support it"); - } - break; - case _OSTREE_FEATURE_MAYBE: - break; - case _OSTREE_FEATURE_NO: - return TRUE; - } + if (!self->fs_verity_wanted) + return TRUE; + + struct libfsverity_signature_params sig_params = { + .keyfile = self->fsverity_key, + .certfile = self->fsverity_cert, + }; - gboolean supported = FALSE; - if (!_ostree_tmpf_fsverity_core (tmpf, fsverity_wanted, &supported, error)) + /* fs-verity requires a read-only file descriptor */ + if (!glnx_tmpfile_reopen_rdonly (tmpf, error)) return FALSE; - if (!supported) - { - if (G_UNLIKELY (fsverity_wanted == _OSTREE_FEATURE_YES)) - return glnx_throw (error, "fsverity required but filesystem does not support it"); - - /* If we got here, we must be trying "opportunistic" use of fs-verity */ - g_assert_cmpint (fsverity_wanted, ==, _OSTREE_FEATURE_MAYBE); - g_mutex_lock (&self->txn_lock); - self->fs_verity_supported = _OSTREE_FEATURE_NO; - g_mutex_unlock (&self->txn_lock); - return TRUE; - } - - g_mutex_lock (&self->txn_lock); - self->fs_verity_supported = _OSTREE_FEATURE_YES; - g_mutex_unlock (&self->txn_lock); -#else - g_assert_cmpint (self->fs_verity_wanted, !=, _OSTREE_FEATURE_YES); -#endif + struct stat stbuf; + if (!glnx_fstat (tmpf->fd, &stbuf, error)) + return FALSE; + + struct libfsverity_merkle_tree_params tree_params = { + .version = 1, + .hash_algorithm = FS_VERITY_HASH_ALG_SHA256, + .file_size = stbuf.st_size, + .block_size = 4096, + /* TODO: salt? */ + }; + g_autofree struct libfsverity_digest *digest = NULL; + if (libfsverity_compute_digest (GINT_TO_POINTER (tmpf->fd), ot_fsverity_read_callback, &tree_params, &digest) < 0) + return throw_fsverity_error (error, "failed to compute digest"); + + guint8 *sig = NULL; + size_t sig_size; + if (libfsverity_sign_digest (digest, &sig_params, &sig, &sig_size) < 0) + return throw_fsverity_error (error, "failed to generate signature"); + + if (libfsverity_enable_with_sig (tmpf->fd, &tree_params, sig, sig_size) < 0) + return throw_fsverity_error (error, "failed to enable fsverity for file"); + return TRUE; } diff --git a/src/libostree/ostree-repo.c b/src/libostree/ostree-repo.c index 0c045c41f1..1028aa2413 100644 --- a/src/libostree/ostree-repo.c +++ b/src/libostree/ostree-repo.c @@ -1049,6 +1049,11 @@ ostree_repo_finalize (GObject *object) g_free (self->collection_id); g_strfreev (self->repo_finders); +#ifdef BUILDOPT_FSVERITY + g_free (self->fsverity_cert); + g_free (self->fsverity_key); +#endif + g_clear_pointer (&self->remotes, g_hash_table_destroy); g_mutex_clear (&self->remotes_lock); @@ -3017,9 +3022,20 @@ reload_core_config (OstreeRepo *self, } } - if (!_ostree_repo_parse_fsverity_config (self, error)) + /* Currently experimental */ + if (!ot_keyfile_get_boolean_with_default (self->config, _OSTREE_FSVERITY_CONFIG_KEY, "required", + FALSE, &self->fs_verity_wanted, error)) return FALSE; - + if (self->fs_verity_wanted) + { + #ifndef BUILDOPT_FSVERITY + return glnx_throw (error, "fsverity required, but libostree compiled without support"); + #else + if (!_ostree_repo_parse_fsverity_config (self, error)) + return FALSE; + #endif + } + { g_clear_pointer (&self->collection_id, g_free); if (!ot_keyfile_get_value_with_default (self->config, "core", "collection-id", diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 0fff820be5..f7b37a39cf 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -158,17 +158,6 @@ install_into_boot (OstreeRepo *repo, if (fdatasync (tmp_dest.fd) < 0) return glnx_throw_errno_prefix (error, "fdatasync"); - /* Today we don't have a config flag to *require* verity on /boot, - * and at least for Fedora CoreOS we're not likely to do fsverity on - * /boot soon due to wanting to support mounting it from old Linux - * kernels. So change "required" to "maybe". - */ - _OstreeFeatureSupport boot_verity = _OSTREE_FEATURE_NO; - if (repo->fs_verity_wanted != _OSTREE_FEATURE_NO) - boot_verity = _OSTREE_FEATURE_MAYBE; - if (!_ostree_tmpf_fsverity_core (&tmp_dest, boot_verity, NULL, error)) - return FALSE; - if (!glnx_link_tmpfile_at (&tmp_dest, GLNX_LINK_TMPFILE_NOREPLACE, dest_dfd, dest_subpath, error)) return FALSE; diff --git a/tests/inst/Cargo.toml b/tests/inst/Cargo.toml index 31b43b4e70..53fbf99232 100644 --- a/tests/inst/Cargo.toml +++ b/tests/inst/Cargo.toml @@ -14,6 +14,7 @@ structopt = "0.3" serde = "1.0.111" serde_derive = "1.0.111" serde_json = "1.0" +serde_yaml = "0.8.14" sh-inline = "0.1.0" anyhow = "1.0" tempfile = "3.1.0" diff --git a/tests/inst/src/fsverity.rs b/tests/inst/src/fsverity.rs new file mode 100644 index 0000000000..51f9c3c428 --- /dev/null +++ b/tests/inst/src/fsverity.rs @@ -0,0 +1,63 @@ +//! Test fsverity + +use anyhow::Result; +use sh_inline::bash; + +use crate::test::*; + +// Not *really* destructive, but must run as root +// and also leaks loopback devices on failure +#[itest(destructive = true)] +fn fsverity() -> Result<()> { + if !check_ostree_feature("ex-fsverity")? { + return Ok(()); + } + + // Create tempdir and sparse disk file in it + let td = tempfile::tempdir_in("/var/tmp")?; + let tmp_disk = td.path().join("disk"); + let tmp_disk = tmp_disk.as_path(); + let mnt = td.path().join("mnt"); + let mnt = mnt.as_path(); + std::fs::create_dir(&mnt)?; + // Create filesystem on it, loopback mount and create a repo + let repopath = mnt.join("repo"); + let repopath = repopath.as_path(); + bash!( + "truncate -s 500M {tmp_disk} + mkfs.ext4 -b $(getconf PAGE_SIZE) -O verity {tmp_disk} + mount -o loop {tmp_disk} {mnt} + echo foo > {mnt}/foo + echo bar > {mnt}/bar + ", + tmp_disk = tmp_disk, + mnt = mnt + )?; + bash!( + r#" +set -x +ostree --repo={repopath} init --mode=bare +k=fsverity-key.pem +c=fsverity-cert.pem +openssl req -batch -newkey rsa:4096 -nodes -keyout {repopath}/$k -x509 -out {repopath}/$c +cat >>{repopath}/config << EOF +[ex-fsverity] +required=true +key=$k +cert=$c +EOF +mkdir {mnt}/testtree +cp -a /usr/bin/ostree {mnt}/testtree +ostree --repo={repopath} commit -b testverity --tree=dir={mnt}/testtree +find {repopath}/objects -type f | while read f; do + fsverity measure $f + if echo somedata >> $f; then + echo 'modified fsverity file!' 1>&2; exit 1 + fi +done + "#, + repopath = repopath, + mnt = mnt, + )?; + Ok(()) +} diff --git a/tests/inst/src/insttestmain.rs b/tests/inst/src/insttestmain.rs index 3fdc1be1a8..d35538aa93 100644 --- a/tests/inst/src/insttestmain.rs +++ b/tests/inst/src/insttestmain.rs @@ -2,6 +2,7 @@ use anyhow::{bail, Result}; use structopt::StructOpt; mod destructive; +mod fsverity; mod repobin; mod rpmostree; mod sysroot; diff --git a/tests/inst/src/test.rs b/tests/inst/src/test.rs index 9d8e156c47..8205ad4f28 100644 --- a/tests/inst/src/test.rs +++ b/tests/inst/src/test.rs @@ -8,6 +8,7 @@ use std::time; use anyhow::{bail, Context, Result}; use linkme::distributed_slice; use rand::Rng; +use serde_derive::Deserialize; pub use itest_macro::itest; pub use with_procspawn_tempdir::with_procspawn_tempdir; @@ -209,6 +210,29 @@ pub(crate) fn prepare_reboot>(mark: M) -> Result<()> { Ok(()) } +#[derive(Deserialize, Debug)] +struct OstreeVersionOutputData { + #[serde(rename = "Features")] + features: Vec, +} + +#[derive(Deserialize, Debug)] +struct OstreeVersionOutput { + libostree: OstreeVersionOutputData, +} + +pub(crate) fn check_ostree_feature>(feature: S) -> Result { + let feature = feature.as_ref(); + let out = std::process::Command::new("ostree") + .arg("--version") + .output()?; + if !out.status.success() { + anyhow::bail!("{:?}", out); + } + let v: OstreeVersionOutput = serde_yaml::from_reader(&*out.stdout)?; + Ok(v.libostree.features.iter().any(|f| f.as_str() == feature)) +} + // I put tests in your tests so you can test while you test #[cfg(test)] mod tests {