From 265cf7d786ffa3972d1698aeeb8bb4e5b50385b5 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 8 Jul 2023 15:42:12 -0400 Subject: [PATCH 1/3] build-sys: Add libsodium to OT_DEP_CRYPTO There's no reason to have these distinct really. If we're using libsodium, we want it in the same places we're using openssl. Prep for further refactoring. --- Makefile-libostree.am | 5 ----- Makefile-ostree.am | 5 ----- configure.ac | 4 ++++ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Makefile-libostree.am b/Makefile-libostree.am index 8edd7f4de1..e616b0ae23 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -262,11 +262,6 @@ libostree_1_la_SOURCES += \ src/libostree/ostree-sign-private.h \ $(NULL) -if USE_LIBSODIUM -libostree_1_la_CFLAGS += $(OT_DEP_LIBSODIUM_CFLAGS) -libostree_1_la_LIBADD += $(OT_DEP_LIBSODIUM_LIBS) -endif # USE_LIBSODIUM - if USE_COMPOSEFS libostree_1_la_LIBADD += libcomposefs.la endif # USE_COMPOSEFS diff --git a/Makefile-ostree.am b/Makefile-ostree.am index db3f7a54be..118db281c7 100644 --- a/Makefile-ostree.am +++ b/Makefile-ostree.am @@ -153,8 +153,3 @@ if USE_LIBARCHIVE ostree_CFLAGS += $(OT_DEP_LIBARCHIVE_CFLAGS) ostree_LDADD += $(OT_DEP_LIBARCHIVE_LIBS) endif - -if USE_LIBSODIUM -ostree_CFLAGS += $(OT_DEP_LIBSODIUM_CFLAGS) -ostree_LDADD += $(OT_DEP_LIBSODIUM_LIBS) -endif # USE_LIBSODIUM diff --git a/configure.ac b/configure.ac index 5eb7367e3c..02164858bf 100644 --- a/configure.ac +++ b/configure.ac @@ -465,6 +465,10 @@ AS_IF([ test $with_crypto = gnutls ], [ AM_CONDITIONAL(USE_GNUTLS, test $with_crypto = gnutls) dnl end gnutls +dnl we always inject libsodium into our crypto deps in addition to openssl/gnutls +OT_DEP_CRYPTO_CFLAGS="${OT_DEP_CRYPTO_CFLAGS} ${OT_DEP_LIBSODIUM_CFLAGS}" +OT_DEP_CRYPTO_LIBS="${OT_DEP_CRYPTO_LIBS} ${OT_DEP_LIBSODIUM_LIBS}" + dnl Avahi dependency for finding repos AVAHI_DEPENDENCY="avahi-client >= 0.6.31 avahi-glib >= 0.6.31" From b8ff2109413a123111eba30f0ae776956d56e081 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 7 Jul 2023 16:31:58 -0400 Subject: [PATCH 2/3] Factor out a libotcore This will contain logic shared between ostree-prepare-root and libostree-1.so. It will just link to libgio.so, so as to avoid pulling in e.g. libcurl and other things. In other words, `ostree-prepare-root` will not link to `libostree-1.so`, but will pull in just what it needs from this library. --- Makefile-libostree.am | 4 +- Makefile-otcore.am | 24 ++++++ Makefile.am | 1 + src/libostree/ostree-sign-ed25519.c | 61 +++----------- src/libotcore/README.md | 1 + src/libotcore/otcore-ed25519-verify.c | 115 ++++++++++++++++++++++++++ src/libotcore/otcore.h | 44 ++++++++++ 7 files changed, 198 insertions(+), 52 deletions(-) create mode 100644 Makefile-otcore.am create mode 100644 src/libotcore/README.md create mode 100644 src/libotcore/otcore-ed25519-verify.c create mode 100644 src/libotcore/otcore.h diff --git a/Makefile-libostree.am b/Makefile-libostree.am index e616b0ae23..1cb7de5477 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -185,12 +185,12 @@ EXTRA_DIST += \ $(top_srcdir)/src/libostree/libostree-released.sym \ $(NULL) -libostree_1_la_CFLAGS = $(AM_CFLAGS) -I$(srcdir)/bsdiff -I$(srcdir)/libglnx -I$(srcdir)/composefs -I$(srcdir)/src/libotutil -I$(srcdir)/src/libostree -I$(builddir)/src/libostree \ +libostree_1_la_CFLAGS = $(AM_CFLAGS) -I$(srcdir)/bsdiff -I$(srcdir)/libglnx -I$(srcdir)/composefs -I$(srcdir)/src/libotutil -I$(srcdir)/src/libotcore -I$(srcdir)/src/libostree -I$(builddir)/src/libostree \ $(OT_INTERNAL_GIO_UNIX_CFLAGS) $(OT_INTERNAL_GPGME_CFLAGS) $(OT_DEP_LZMA_CFLAGS) $(OT_DEP_ZLIB_CFLAGS) $(OT_DEP_CRYPTO_CFLAGS) \ -fvisibility=hidden '-D_OSTREE_PUBLIC=__attribute__((visibility("default"))) extern' \ -DPKGLIBEXECDIR=\"$(pkglibexecdir)\" 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) \ +libostree_1_la_LIBADD = libotutil.la libotcore.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) # Some change between rust-1.21.0-1.fc27 and rust-1.22.1-1.fc27.x86_64 libostree_1_la_LIBADD += $(bupsplitpath) diff --git a/Makefile-otcore.am b/Makefile-otcore.am new file mode 100644 index 0000000000..8accf85844 --- /dev/null +++ b/Makefile-otcore.am @@ -0,0 +1,24 @@ +# 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 . + +noinst_LTLIBRARIES += libotcore.la + +libotcore_la_SOURCES = \ + src/libotcore/otcore.h \ + src/libotcore/otcore-ed25519-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) +libotcore_la_LIBADD = $(OT_INTERNAL_GIO_UNIX_LIBS) $(OT_INTERNAL_GPGME_LIBS) $(LIBSYSTEMD_LIBS) $(OT_DEP_CRYPTO_LIBS) diff --git a/Makefile.am b/Makefile.am index ca7dec9e4f..19abc0c1e1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -127,6 +127,7 @@ noinst_LTLIBRARIES += libcomposefs.la endif include Makefile-otutil.am +include Makefile-otcore.am include Makefile-libostree.am include Makefile-ostree.am include Makefile-switchroot.am diff --git a/src/libostree/ostree-sign-ed25519.c b/src/libostree/ostree-sign-ed25519.c index 6ff132600b..21f380aa48 100644 --- a/src/libostree/ostree-sign-ed25519.c +++ b/src/libostree/ostree-sign-ed25519.c @@ -24,31 +24,15 @@ #include "config.h" #include "ostree-sign-ed25519.h" +#include "otcore.h" #include #include -#ifdef HAVE_LIBSODIUM -#include -#define USE_LIBSODIUM -#else - -#if defined(HAVE_OPENSSL) -#include -#define USE_OPENSSL -#endif - -#endif - #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "OSTreeSign" #define OSTREE_SIGN_ED25519_NAME "ed25519" -#define OSTREE_SIGN_METADATA_ED25519_KEY "ostree.sign.ed25519" -#define OSTREE_SIGN_METADATA_ED25519_TYPE "aay" - -#define OSTREE_SIGN_ED25519_SIG_SIZE 64U -#define OSTREE_SIGN_ED25519_PUBKEY_SIZE 32U #define OSTREE_SIGN_ED25519_SEED_SIZE 32U #define OSTREE_SIGN_ED25519_SECKEY_SIZE \ (OSTREE_SIGN_ED25519_SEED_SIZE + OSTREE_SIGN_ED25519_PUBKEY_SIZE) @@ -108,12 +92,11 @@ _ostree_sign_ed25519_init (OstreeSignEd25519 *self) self->public_keys = NULL; self->revoked_keys = NULL; -#if defined(USE_LIBSODIUM) - if (sodium_init () < 0) - self->state = ED25519_FAILED_INITIALIZATION; -#elif defined(USE_OPENSSL) -#else +#if !(defined(USE_OPENSSL) || defined(USE_LIBSODIUM)) self->state = ED25519_NOT_SUPPORTED; +#else + if (!otcore_ed25519_init ()) + self->state = ED25519_FAILED_INITIALIZATION; #endif } @@ -232,7 +215,6 @@ ostree_sign_ed25519_data_verify (OstreeSign *self, GBytes *data, GVariant *signa { g_autoptr (GVariant) child = g_variant_get_child_value (signatures, i); g_autoptr (GBytes) signature = g_variant_get_data_as_bytes (child); - gboolean valid = FALSE; if (g_bytes_get_size (signature) != OSTREE_SIGN_ED25519_SIG_SIZE) return glnx_throw ( @@ -246,7 +228,6 @@ ostree_sign_ed25519_data_verify (OstreeSign *self, GBytes *data, GVariant *signa for (GList *public_key = sign->public_keys; public_key != NULL; public_key = public_key->next) { - /* TODO: use non-list for tons of revoked keys? */ if (g_list_find_custom (sign->revoked_keys, public_key->data, _compare_ed25519_keys) != NULL) @@ -256,32 +237,12 @@ ostree_sign_ed25519_data_verify (OstreeSign *self, GBytes *data, GVariant *signa continue; } -#if defined(USE_LIBSODIUM) - valid = crypto_sign_verify_detached ((guchar *)g_variant_get_data (child), - g_bytes_get_data (data, NULL), - g_bytes_get_size (data), public_key->data) - == 0; -#elif defined(USE_OPENSSL) - EVP_MD_CTX *ctx = EVP_MD_CTX_new (); - if (!ctx) - return glnx_throw (error, "openssl: failed to allocate context"); - EVP_PKEY *pkey = EVP_PKEY_new_raw_public_key (EVP_PKEY_ED25519, NULL, public_key->data, - OSTREE_SIGN_ED25519_PUBKEY_SIZE); - if (!pkey) - { - EVP_MD_CTX_free (ctx); - return glnx_throw (error, "openssl: Failed to initialize ed5519 key"); - } - - valid = EVP_DigestVerifyInit (ctx, NULL, NULL, NULL, pkey) != 0 - && EVP_DigestVerify (ctx, g_bytes_get_data (signature, NULL), - g_bytes_get_size (signature), g_bytes_get_data (data, NULL), - g_bytes_get_size (data)) - != 0; - - EVP_PKEY_free (pkey); - EVP_MD_CTX_free (ctx); -#endif + bool valid = false; + // Wrap the pubkey in a GBytes as that's what this API wants + g_autoptr (GBytes) public_key_bytes + = g_bytes_new_static (public_key->data, OSTREE_SIGN_ED25519_PUBKEY_SIZE); + if (!otcore_validate_ed25519_signature (data, public_key_bytes, signature, &valid, error)) + return FALSE; if (!valid) { /* Incorrect signature! */ diff --git a/src/libotcore/README.md b/src/libotcore/README.md new file mode 100644 index 0000000000..967a5d9f63 --- /dev/null +++ b/src/libotcore/README.md @@ -0,0 +1 @@ +This library is (will be) shared between `libostree-1.so` and `ostree-prepare-root`. diff --git a/src/libotcore/otcore-ed25519-verify.c b/src/libotcore/otcore-ed25519-verify.c new file mode 100644 index 0000000000..1c0ec2b83e --- /dev/null +++ b/src/libotcore/otcore-ed25519-verify.c @@ -0,0 +1,115 @@ +/* + * 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 . + */ + +#include "config.h" + +#include "otcore.h" + +/* Initialize global state; may be called multiple times and is idempotent. */ +bool +otcore_ed25519_init (void) +{ +#if defined(HAVE_LIBSODIUM) + static gssize initstate; + if (g_once_init_enter (&initstate)) + { + int val = sodium_init () >= 0 ? 1 : -1; + g_once_init_leave (&initstate, val); + } + switch (initstate) + { + case 1: + return true; + case -1: + return false; + default: + g_assert_not_reached (); + } +#else + return true; +#endif +} + +/* Validate a single ed25519 signature. If there is an unexpected state, such + * as an ill-forumed public key or signature, a hard error will be returned. + * + * If the signature is not correct, this function will return successfully, but + * `out_valid` will be set to `false`. + * + * If the signature is correct, `out_valid` will be `true`. + * */ +gboolean +otcore_validate_ed25519_signature (GBytes *data, GBytes *public_key, GBytes *signature, + bool *out_valid, GError **error) +{ + // Since this is signature verification code, let's verify preconditions. + g_assert (data); + g_assert (public_key); + g_assert (signature); + g_assert (out_valid); + // It is OK for error to be NULL, though according to GError rules. + +#if defined(HAVE_LIBSODIUM) || defined(HAVE_OPENSSL) + // And strictly verify pubkey and signature lengths + if (g_bytes_get_size (public_key) != OSTREE_SIGN_ED25519_PUBKEY_SIZE) + return glnx_throw (error, "Invalid public key of %" G_GSIZE_FORMAT " expected %" G_GSIZE_FORMAT, + (gsize)g_bytes_get_size (public_key), + (gsize)OSTREE_SIGN_ED25519_PUBKEY_SIZE); + const guint8 *public_key_buf = g_bytes_get_data (public_key, NULL); + if (g_bytes_get_size (signature) != OSTREE_SIGN_ED25519_SIG_SIZE) + return glnx_throw ( + error, "Invalid signature length of %" G_GSIZE_FORMAT " bytes, expected %" G_GSIZE_FORMAT, + (gsize)g_bytes_get_size (signature), (gsize)OSTREE_SIGN_ED25519_SIG_SIZE); + const guint8 *signature_buf = g_bytes_get_data (signature, NULL); + +#endif + +#if defined(HAVE_LIBSODIUM) + // Note that libsodium assumes the passed byte arrays for the signature and public key + // have at least the expected length, but we checked that above. + if (crypto_sign_verify_detached (signature_buf, g_bytes_get_data (data, NULL), + g_bytes_get_size (data), public_key_buf) + == 0) + { + *out_valid = true; + } + return TRUE; +#elif defined(HAVE_OPENSSL) + EVP_MD_CTX *ctx = EVP_MD_CTX_new (); + if (!ctx) + return glnx_throw (error, "openssl: failed to allocate context"); + EVP_PKEY *pkey = EVP_PKEY_new_raw_public_key (EVP_PKEY_ED25519, NULL, public_key_buf, + OSTREE_SIGN_ED25519_PUBKEY_SIZE); + if (!pkey) + { + EVP_MD_CTX_free (ctx); + return glnx_throw (error, "openssl: Failed to initialize ed5519 key"); + } + if (EVP_DigestVerifyInit (ctx, NULL, NULL, NULL, pkey) != 0 + && EVP_DigestVerify (ctx, signature_buf, OSTREE_SIGN_ED25519_SIG_SIZE, + g_bytes_get_data (data, NULL), g_bytes_get_size (data)) + != 0) + { + *out_valid = true; + } + EVP_PKEY_free (pkey); + EVP_MD_CTX_free (ctx); + return TRUE; +#else + return glnx_throw (error, "ed25519 signature validation requested, but support not compiled in"); +#endif +} diff --git a/src/libotcore/otcore.h b/src/libotcore/otcore.h new file mode 100644 index 0000000000..fdbca4932c --- /dev/null +++ b/src/libotcore/otcore.h @@ -0,0 +1,44 @@ +/* + * 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 . + */ + +#pragma once + +#include "config.h" + +#include "otutil.h" +#include + +#ifdef HAVE_LIBSODIUM +#include +#define USE_LIBSODIUM +#elif defined(HAVE_OPENSSL) +#include +#define USE_OPENSSL +#endif + +// Length of a signature in bytes +#define OSTREE_SIGN_ED25519_SIG_SIZE 64U +// Length of a public key in bytes +#define OSTREE_SIGN_ED25519_PUBKEY_SIZE 32U +// This key is stored inside commit metadata. +#define OSTREE_SIGN_METADATA_ED25519_KEY "ostree.sign.ed25519" +// The variant type +#define OSTREE_SIGN_METADATA_ED25519_TYPE "aay" + +bool otcore_ed25519_init (void); +gboolean otcore_validate_ed25519_signature (GBytes *data, GBytes *pubkey, GBytes *signature, + bool *out_valid, GError **error); From c29f4193cd7e431f0f49a3af98df9830e5fea8c5 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Fri, 7 Jul 2023 11:29:31 +0200 Subject: [PATCH 3/3] ostree-prepare-root: Validate ed25519 signatures when requested If requested, by specifying ot-composefs=signed=/path/to/pub.key then the commit object is validated against the specified ed25519 public key, and if valid, the composefs digest from the commit object is used to ensure we boot the right digest. --- Makefile-switchroot.am | 6 +- docs/composefs.md | 38 ++++++-- src/switchroot/ostree-prepare-root.c | 134 ++++++++++++++++++++++++++- 3 files changed, 162 insertions(+), 16 deletions(-) diff --git a/Makefile-switchroot.am b/Makefile-switchroot.am index 09f954f381..6b7cca08de 100644 --- a/Makefile-switchroot.am +++ b/Makefile-switchroot.am @@ -49,10 +49,10 @@ ostree-prepare-root : $(ostree_prepare_root_SOURCES) CLEANFILES += ostree-prepare-root else ostree_boot_PROGRAMS += ostree-prepare-root -ostree_prepare_root_CFLAGS += $(AM_CFLAGS) -Isrc/switchroot -I$(srcdir)/composefs +ostree_prepare_root_CFLAGS += $(AM_CFLAGS) -Isrc/switchroot -I$(srcdir)/composefs -I$(srcdir)/src/libostree -I$(srcdir)/src/libotcore -I$(srcdir)/src/libotutil ostree_prepare_root_SOURCES += src/switchroot/ostree-prepare-root.c -ostree_prepare_root_CPPFLAGS += $(OT_INTERNAL_GIO_UNIX_CFLAGS) -I $(srcdir)/libglnx -ostree_prepare_root_LDADD += $(AM_LDFLAGS) $(OT_INTERNAL_GIO_UNIX_LIBS) libglnx.la +ostree_prepare_root_CPPFLAGS += $(OT_INTERNAL_GIO_UNIX_CFLAGS) $(OT_DEP_CRYPTO_CFLAGS) -I $(srcdir)/libglnx +ostree_prepare_root_LDADD += $(AM_LDFLAGS) $(OT_INTERNAL_GIO_UNIX_LIBS) $(OT_DEP_CRYPTO_LIBS) libotcore.la libotutil.la libglnx.la endif # BUILDOPT_USE_STATIC_COMPILER diff --git a/docs/composefs.md b/docs/composefs.md index 8f63c6e239..8f2c425e96 100644 --- a/docs/composefs.md +++ b/docs/composefs.md @@ -43,7 +43,8 @@ The possible values are: - `maybe`: Use composefs if supported and there is a composefs image in the deployment directory - `on`: Require composefs - `digest=`: Require the mounted composefs image to have a particular digest -- `signed`: This option will be documented in the future; don't use it right now +- `signed=`: Require that the commit is signed as validated by the ed25519 public key specified + by `path` (the path is resolved in the initrd). ### Injecting composefs digests @@ -51,16 +52,37 @@ When generating an OSTree commit, there is a CLI switch `--generate-composefs-me and a corresponding C API `ostree_repo_commit_add_composefs_metadata`. This will inject the composefs digest as metadata into the ostree commit under a metadata key `ostree.composefs.v0`. Because an OSTree commit can be signed, this allows -covering the composefs fsverity digest with a signature. - -At the current time, ostree does not directly support verifying the signature on -the commit object before mounting, but that is in progress. +covering the composefs fsverity digest with a signature. + +### Signatures + +If a commit is signed with a ed25519 private key (see `ostree +--sign`), and `signed=/path/to/public.key` is specified on the +commandline, then the initrd will find the commit being booted in the +system repo and validate its signature against the public key. It will +then ensure that the composefs digest being booted has an fs-verity +digest matching the one in the commit. This allows a fully trusted +read-only /usr. + +The exact usage of the signature is up to the user, but a common way +to use it with transien keys. This is done like this: + * Generate a new keypair before each build + * Embed the public key in the initrd that is part of the commit. + * Ensure the kernel commandline has `ot-signed=/path/to/key` + * After commiting, run `ostree --sign` with the private key. + * Throw away the private key. + +When a transient key is used this way, that ties the initrd with the +userspace part from the commit. This means each initrd can only boot +the very same userspace it was made for. For example, if an older +version of the OS has a security flaw, you can't boot a new fixed +(signed) initrd and have it boot the older userspace with the flaw. ## Requirements -The current default composefs integration in ostree does not have any requirements -from the underlying kernel and filesystem other than having the following -kernel options set: +The current default composefs integration in ostree does not have any +requirements from the underlying kernel and filesystem other than +having the following kernel options set: - `CONFIG_OVERLAY_FS` - `CONFIG_BLK_DEV_LOOP` diff --git a/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c index ab8bb14227..ea07331bfc 100644 --- a/src/switchroot/ostree-prepare-root.c +++ b/src/switchroot/ostree-prepare-root.c @@ -70,6 +70,11 @@ #include #include +#include +#include + +#include "otcore.h" + /* We can't include both linux/fs.h and sys/mount.h, so define these directly */ #define FS_VERITY_FL 0x00100000 /* Verity protected inode */ #define FS_IOC_GETFLAGS _IOR ('f', 1, long) @@ -160,6 +165,80 @@ pivot_root (const char *new_root, const char *put_old) return syscall (__NR_pivot_root, new_root, put_old); } +#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 = NULL; + char *data = NULL; + gsize data_size; + + path = g_strdup_printf ("%s/ostree/repo/objects/%.2s/%s.%s", root_mountpoint, digest, digest + 2, + extension); + + 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)); +} + +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; + + 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; +} + +static gboolean +validate_signature (GBytes *data, GVariant *signatures, const guchar *pubkey, size_t pubkey_size) +{ + g_autoptr (GBytes) pubkey_buf = g_bytes_new_static (pubkey, pubkey_size); + + 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_buf, signature, &valid, &local_error)) + errx (EXIT_FAILURE, "signature verification failed: %s", local_error->message); + if (valid) + return TRUE; + } + + return FALSE; +} +#endif + int main (int argc, char *argv[]) { @@ -167,6 +246,8 @@ main (int argc, char *argv[]) const char *root_arg = NULL; bool we_mounted_proc = false; + g_autoptr (GError) error = NULL; + if (argc < 2) err (EXIT_FAILURE, "usage: ostree-prepare-root SYSROOT"); root_arg = argv[1]; @@ -206,6 +287,7 @@ main (int argc, char *argv[]) OstreeComposefsMode composefs_mode = OSTREE_COMPOSEFS_MODE_MAYBE; autofree char *ot_composefs = read_proc_cmdline_key ("ot-composefs"); char *composefs_digest = NULL; + char *composefs_pubkey = NULL; if (ot_composefs) { if (strcmp (ot_composefs, "off") == 0) @@ -214,8 +296,11 @@ main (int argc, char *argv[]) composefs_mode = OSTREE_COMPOSEFS_MODE_MAYBE; else if (strcmp (ot_composefs, "on") == 0) composefs_mode = OSTREE_COMPOSEFS_MODE_ON; - else if (strcmp (ot_composefs, "signed") == 0) - composefs_mode = OSTREE_COMPOSEFS_MODE_SIGNED; + else if (strncmp (ot_composefs, "signed=", strlen ("signed=")) == 0) + { + composefs_mode = OSTREE_COMPOSEFS_MODE_SIGNED; + composefs_pubkey = ot_composefs + strlen ("signed="); + } else if (strncmp (ot_composefs, "digest=", strlen ("digest=")) == 0) { composefs_mode = OSTREE_COMPOSEFS_MODE_DIGEST; @@ -229,6 +314,7 @@ main (int argc, char *argv[]) if (composefs_mode == OSTREE_COMPOSEFS_MODE_MAYBE) composefs_mode = OSTREE_COMPOSEFS_MODE_OFF; (void)composefs_digest; + (void)composefs_pubkey; #endif /* Query the repository configuration - this is an operating system builder @@ -266,13 +352,51 @@ main (int argc, char *argv[]) { #ifdef HAVE_COMPOSEFS const char *objdirs[] = { "/sysroot/ostree/repo/objects" }; + g_autofree char *cfs_digest = NULL; struct lcfs_mount_options_s cfs_options = { objdirs, 1, }; if (composefs_mode == OSTREE_COMPOSEFS_MODE_SIGNED) - errx (EXIT_FAILURE, "composefs signature not supported"); + { + g_autoptr (GError) local_error = NULL; + g_autofree char *pubkey = NULL; + gsize pubkey_size; + g_autoptr (GVariant) commit = NULL; + g_autoptr (GVariant) commitmeta = NULL; + + if (!g_file_get_contents (composefs_pubkey, &pubkey, &pubkey_size, &local_error)) + errx (EXIT_FAILURE, "Failed to load public key '%s': %s", composefs_pubkey, + local_error->message); + + 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.ed25519", 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, (guchar *)pubkey, pubkey_size)) + errx (EXIT_FAILURE, "No valid signatures found for public key"); + +#ifdef USE_LIBSYSTEMD + sd_journal_send ("MESSAGE=Validated commit signature using '%s'", composefs_pubkey, NULL); +#endif + + 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"); + + composefs_digest = g_malloc (OSTREE_SHA256_STRING_LEN + 1); + ot_bin2hex (composefs_digest, g_variant_get_data (cfs_digest_v), + g_variant_get_size (cfs_digest_v)); + } cfs_options.flags = LCFS_MOUNT_FLAGS_READONLY; @@ -280,7 +404,7 @@ main (int argc, char *argv[]) err (EXIT_FAILURE, "failed to assemble /boot/loader path"); cfs_options.image_mountdir = srcpath; - if (composefs_mode == OSTREE_COMPOSEFS_MODE_DIGEST) + if (composefs_digest != NULL) { cfs_options.flags |= LCFS_MOUNT_FLAGS_REQUIRE_VERITY; cfs_options.expected_fsverity_digest = composefs_digest; @@ -289,7 +413,7 @@ main (int argc, char *argv[]) #ifdef USE_LIBSYSTEMD if (composefs_mode == OSTREE_COMPOSEFS_MODE_MAYBE) sd_journal_send ("MESSAGE=Trying to mount composefs rootfs", NULL); - else if (composefs_mode == OSTREE_COMPOSEFS_MODE_DIGEST) + else if (composefs_digest != NULL) sd_journal_send ("MESSAGE=Mounting composefs rootfs with expected digest '%s'", composefs_digest, NULL); else