From af338b8f9a755eae7b6a3290d790d3e9c9c42944 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Fri, 12 Jul 2024 12:54:13 +0900 Subject: [PATCH] sign: Support x509 signature type The current "ed25519" signing type assumes raw Ed25519 key format for both public and private keys. That requires custom processing of keys after generated with openssl tools, and also lacks cryptographic agility[1]; when Ed25519 becomes vulnerable, it would not be straightforward to migrate to other algorithms, such as post-quantum signature algorithms. This patch adds a new signature type "x509" to use the key formats natively supported by OpenSSL (PKCS#8 and SubjectPublicKeyInfo) and capable of embedding algorithm identifier in an X.509 format. The "x509" signature type prefers keys to be encoded in the PEM format on disk, while it still accepts base64 encoded keys when given through the command-line. 1. https://en.wikipedia.org/wiki/Cryptographic_agility Signed-off-by: Daiki Ueno --- Makefile-libostree.am | 2 + Makefile-otcore.am | 1 + Makefile-tests.am | 14 +- configure.ac | 15 +- man/ostree-commit.xml | 7 +- man/ostree-sign.xml | 28 +- rust-bindings/sys/tests/constant.c | 1 + src/libostree/ostree-sign-x509.c | 636 ++++++++++++++++++ src/libostree/ostree-sign-x509.h | 54 ++ src/libostree/ostree-sign.c | 20 + src/libostree/ostree-sign.h | 8 + src/libotcore/otcore-x509-verify.c | 89 +++ src/libotcore/otcore.h | 13 + tests/libtest.sh | 34 + tests/test-signed-commit-dummy.sh | 61 ++ ...ommit.sh => test-signed-commit-ed25519.sh} | 42 +- tests/test-signed-commit-x509.sh | 164 +++++ 17 files changed, 1128 insertions(+), 61 deletions(-) create mode 100644 src/libostree/ostree-sign-x509.c create mode 100644 src/libostree/ostree-sign-x509.h create mode 100644 src/libotcore/otcore-x509-verify.c create mode 100755 tests/test-signed-commit-dummy.sh rename tests/{test-signed-commit.sh => test-signed-commit-ed25519.sh} (76%) create mode 100755 tests/test-signed-commit-x509.sh diff --git a/Makefile-libostree.am b/Makefile-libostree.am index 701653ed1c..411fafccb2 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -261,6 +261,8 @@ libostree_1_la_SOURCES += \ src/libostree/ostree-sign-dummy.h \ src/libostree/ostree-sign-ed25519.c \ src/libostree/ostree-sign-ed25519.h \ + src/libostree/ostree-sign-x509.c \ + src/libostree/ostree-sign-x509.h \ src/libostree/ostree-sign-private.h \ src/libostree/ostree-blob-reader.c \ src/libostree/ostree-blob-reader.h \ diff --git a/Makefile-otcore.am b/Makefile-otcore.am index 8252ead00e..dbb25e8d93 100644 --- a/Makefile-otcore.am +++ b/Makefile-otcore.am @@ -19,6 +19,7 @@ libotcore_la_SOURCES = \ src/libotcore/otcore.h \ src/libotcore/otcore-ed25519-verify.c \ src/libotcore/otcore-prepare-root.c \ + src/libotcore/otcore-x509-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) diff --git a/Makefile-tests.am b/Makefile-tests.am index 415637a5d1..51505f1982 100644 --- a/Makefile-tests.am +++ b/Makefile-tests.am @@ -156,12 +156,24 @@ _installed_or_uninstalled_test_scripts = \ tests/test-summary-collections.sh \ tests/test-pull-collections.sh \ tests/test-config.sh \ - tests/test-signed-commit.sh \ + tests/test-signed-commit-dummy.sh \ tests/test-signed-pull.sh \ tests/test-pre-signed-pull.sh \ tests/test-signed-pull-summary.sh \ $(NULL) +if HAVE_ED25519 +_installed_or_uninstalled_test_scripts += \ + tests/test-signed-commit-ed25519.sh \ + $(NULL) +endif + +if HAVE_X509 +_installed_or_uninstalled_test_scripts += \ + tests/test-signed-commit-x509.sh \ + $(NULL) +endif + if USE_GPGME _installed_or_uninstalled_test_scripts += \ tests/test-remote-gpg-import.sh \ diff --git a/configure.ac b/configure.ac index 538ff3456e..d29cfd059a 100644 --- a/configure.ac +++ b/configure.ac @@ -452,10 +452,19 @@ if test x$with_openssl != xno; then OSTREE_FEATURES="$OSTREE_FEATURES openssl"; AM_CONDITIONAL(USE_OPENSSL, test $with_openssl != no) dnl end openssl -if test x$with_openssl != xno || test x$with_ed25519_libsodium != xno; then +AM_CONDITIONAL([HAVE_ED25519], [test x$with_openssl != xno || test x$with_ed25519_libsodium != xno]) + +AM_COND_IF([HAVE_ED25519], [ AC_DEFINE([HAVE_ED25519], 1, [Define if ed25519 is supported ]) OSTREE_FEATURES="$OSTREE_FEATURES sign-ed25519" -fi +]) + +AM_CONDITIONAL([HAVE_X509], [test x$with_openssl != xno]) + +AM_COND_IF([HAVE_X509], [ + AC_DEFINE([HAVE_X509], 1, [Define if x509 is supported ]) + OSTREE_FEATURES="$OSTREE_FEATURES sign-x509" +]) dnl begin gnutls; in contrast to openssl this one only dnl supports --with-crypto=gnutls @@ -697,7 +706,7 @@ echo " systemd: $with_libsystemd libmount: $with_libmount libsodium (ed25519 signatures): $with_ed25519_libsodium - openssl (ed25519 signatures): $with_openssl + openssl (ed25519 and x509 signatures): $with_openssl libarchive (parse tar files directly): $with_libarchive static deltas: yes (always enabled now) O_TMPFILE: $enable_otmpfile diff --git a/man/ostree-commit.xml b/man/ostree-commit.xml index 12f4fd10fa..b842c53f6d 100644 --- a/man/ostree-commit.xml +++ b/man/ostree-commit.xml @@ -312,7 +312,7 @@ License along with this library. If not, see . Use particular signature engine. Currently - available ed25519 and dummy + available ed25519, x509, and dummy signature types. The default is ed25519. @@ -323,7 +323,8 @@ License along with this library. If not, see . ="PATH" - This will read a key (corresponding to the provided --sign-type from the provided path. The key should be base64 encoded. + This will read a key (corresponding to the provided --sign-type from the provided path. The encoding of the key depends on + signature engine. For ed25519 the key should be base64 encoded, for x509 it should be in PEM format, and for dummy it should be an ASCII-string. @@ -337,7 +338,7 @@ License along with this library. If not, see . The KEY-ID is: - + base64-encoded secret key for commit signing. diff --git a/man/ostree-sign.xml b/man/ostree-sign.xml index 508a6ca22b..c5542e0339 100644 --- a/man/ostree-sign.xml +++ b/man/ostree-sign.xml @@ -64,26 +64,28 @@ License along with this library. If not, see . - There are several "well-known" system places for `ed25519` trusted and revoked public keys -- expected single base64-encoded key per line. + For `ed25519` and `x509`, there are several "well-known" system places for trusted and revoked public keys as listed below. Files: - /etc/ostree/trusted.ed25519 - /etc/ostree/revoked.ed25519 - /usr/share/ostree/trusted.ed25519 - /usr/share/ostree/revoked.ed25519 + /etc/ostree/trusted.SIGN-TYPE + /etc/ostree/revoked.SIGN-TYPE + /usr/share/ostree/trusted.SIGN-TYPE + /usr/share/ostree/revoked.SIGN-TYPE Directories containing files with keys: - /etc/ostree/trusted.ed25519.d - /etc/ostree/revoked.ed25519.d - /usr/share/ostree/trusted.ed25519.d - /usr/share/ostree/revoked.ed25519.d + /etc/ostree/trusted.SIGN-TYPE.d + /etc/ostree/revoked.SIGN-TYPE.d + /usr/share/ostree/trusted.SIGN-TYPE.d + /usr/share/ostree/revoked.SIGN-TYPE.d + + The format of those files depends on the signature mechanism; for `ed25519`, keys are stored in the base64 encoding per line, while for `x509` they are stored in the PEM "PUBLIC KEY" encoding. @@ -95,7 +97,7 @@ License along with this library. If not, see . - + base64-encoded secret (for signing) or public key (for verifying). @@ -120,7 +122,7 @@ License along with this library. If not, see . Use particular signature mechanism. Currently - available ed25519 and dummy + available ed25519, x509, and dummy signature types. The default is ed25519. @@ -133,8 +135,8 @@ License along with this library. If not, see . - Valid for ed25519 signature type. - For ed25519 this file must contain base64-encoded + Valid for ed25519 and x509 signature types. + This file must contain base64-encoded secret key(s) (for signing) or public key(s) (for verifying) per line. diff --git a/rust-bindings/sys/tests/constant.c b/rust-bindings/sys/tests/constant.c index 7b3a9d7c57..ba1d600d32 100644 --- a/rust-bindings/sys/tests/constant.c +++ b/rust-bindings/sys/tests/constant.c @@ -157,6 +157,7 @@ main () PRINT_CONSTANT (OSTREE_SHA256_DIGEST_LEN); PRINT_CONSTANT (OSTREE_SHA256_STRING_LEN); PRINT_CONSTANT (OSTREE_SIGN_NAME_ED25519); + PRINT_CONSTANT (OSTREE_SIGN_NAME_X509); PRINT_CONSTANT ((gint)OSTREE_STATIC_DELTA_GENERATE_OPT_LOWLATENCY); PRINT_CONSTANT ((gint)OSTREE_STATIC_DELTA_GENERATE_OPT_MAJOR); PRINT_CONSTANT ((gint)OSTREE_STATIC_DELTA_INDEX_FLAGS_NONE); diff --git a/src/libostree/ostree-sign-x509.c b/src/libostree/ostree-sign-x509.c new file mode 100644 index 0000000000..2d32c89b04 --- /dev/null +++ b/src/libostree/ostree-sign-x509.c @@ -0,0 +1,636 @@ +/* vim:set et sw=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e2s: */ +/* + * Copyright © 2019 Collabora Ltd. + * Copyright © 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 . + */ + +#include "config.h" + +#include "ostree-sign-x509.h" +#include "otcore.h" +#include +#include +#include + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "OSTreeSign" + +#define OSTREE_SIGN_X509_NAME "x509" + +typedef enum +{ + X509_OK, + X509_NOT_SUPPORTED, + X509_FAILED_INITIALIZATION +} x509_state; + +struct _OstreeSignX509 +{ + GObject parent; + x509_state state; + GBytes *secret_key; + GList *public_keys; /* GBytes */ + GList *revoked_keys; /* GBytes */ +}; + +static void ostree_sign_x509_iface_init (OstreeSignInterface *self); + +G_DEFINE_TYPE_WITH_CODE (OstreeSignX509, _ostree_sign_x509, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (OSTREE_TYPE_SIGN, ostree_sign_x509_iface_init)); + +static void +ostree_sign_x509_iface_init (OstreeSignInterface *self) +{ + + self->data = ostree_sign_x509_data; + self->data_verify = ostree_sign_x509_data_verify; + self->get_name = ostree_sign_x509_get_name; + self->metadata_key = ostree_sign_x509_metadata_key; + self->metadata_format = ostree_sign_x509_metadata_format; + self->clear_keys = ostree_sign_x509_clear_keys; + self->set_sk = ostree_sign_x509_set_sk; + self->set_pk = ostree_sign_x509_set_pk; + self->add_pk = ostree_sign_x509_add_pk; + self->load_pk = ostree_sign_x509_load_pk; +} + +static void +_ostree_sign_x509_class_init (OstreeSignX509Class *self) +{ +} + +static void +_ostree_sign_x509_init (OstreeSignX509 *self) +{ + + self->state = X509_OK; + self->secret_key = NULL; + self->public_keys = NULL; + self->revoked_keys = NULL; + +#if !defined(USE_OPENSSL) + self->state = X509_NOT_SUPPORTED; +#else + if (!otcore_x509_init ()) + self->state = X509_FAILED_INITIALIZATION; +#endif +} + +static gboolean +_ostree_sign_x509_is_initialized (OstreeSignX509 *self, GError **error) +{ + switch (self->state) + { + case X509_OK: + break; + case X509_NOT_SUPPORTED: + return glnx_throw (error, "x509: engine is not supported"); + case X509_FAILED_INITIALIZATION: + return glnx_throw (error, "x509: crypto library isn't initialized properly"); + } + + return TRUE; +} + +gboolean +ostree_sign_x509_data (OstreeSign *self, GBytes *data, GBytes **signature, + GCancellable *cancellable, GError **error) +{ +#if defined(USE_OPENSSL) + g_assert (OSTREE_IS_SIGN (self)); + OstreeSignX509 *sign = _ostree_sign_x509_get_instance_private (OSTREE_SIGN_X509 (self)); + + if (!_ostree_sign_x509_is_initialized (sign, error)) + return FALSE; + + if (sign->secret_key == NULL) + return glnx_throw (error, "Not able to sign: secret key is not set"); + + gsize secret_key_size; + const guint8 *secret_key_buf = g_bytes_get_data (sign->secret_key, &secret_key_size); + + EVP_MD_CTX *ctx = EVP_MD_CTX_new (); + if (!ctx) + return glnx_throw (error, "openssl: failed to allocate context"); + + const unsigned char *p = secret_key_buf; + EVP_PKEY *pkey = d2i_AutoPrivateKey (NULL, &p, secret_key_size); + if (!pkey) + { + EVP_MD_CTX_free (ctx); + return glnx_throw (error, "openssl: Failed to initialize x509 key"); + } + + unsigned long long sig_size = 0; + g_autofree guchar *sig = NULL; + + size_t len; + if (EVP_DigestSignInit (ctx, NULL, NULL, NULL, pkey) + && EVP_DigestSign (ctx, NULL, &len, g_bytes_get_data (data, NULL), g_bytes_get_size (data))) + { + sig = g_malloc0 (len); + if (EVP_DigestSign (ctx, sig, &len, g_bytes_get_data (data, NULL), g_bytes_get_size (data))) + sig_size = len; + } + + EVP_PKEY_free (pkey); + EVP_MD_CTX_free (ctx); + + if (sig_size == 0) + return glnx_throw (error, "Failed to sign"); + + *signature = g_bytes_new_take (g_steal_pointer (&sig), sig_size); + return TRUE; +#else + return glnx_throw (error, "x509 signature validation requested, but support not compiled in"); +#endif +} + +gboolean +ostree_sign_x509_data_verify (OstreeSign *self, GBytes *data, GVariant *signatures, + char **out_success_message, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + if (data == NULL) + return glnx_throw (error, "x509: unable to verify NULL data"); + + OstreeSignX509 *sign = _ostree_sign_x509_get_instance_private (OSTREE_SIGN_X509 (self)); + + if (!_ostree_sign_x509_is_initialized (sign, error)) + return FALSE; + + if (signatures == NULL) + return glnx_throw (error, "x509: commit have no signatures of my type"); + + if (!g_variant_is_of_type (signatures, (GVariantType *)OSTREE_SIGN_METADATA_X509_TYPE)) + return glnx_throw (error, "x509: wrong type passed for verification"); + + /* If no keys pre-loaded then, + * try to load public keys from storage(s) */ + if (sign->public_keys == NULL) + { + g_autoptr (GVariantBuilder) builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + g_autoptr (GVariant) options = g_variant_builder_end (builder); + + if (!ostree_sign_x509_load_pk (self, options, error)) + return FALSE; + } + + g_debug ("verify: data hash = 0x%x", g_bytes_hash (data)); + + g_autoptr (GString) invalid_signatures = NULL; + guint n_invalid_signatures = 0; + + 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); + + g_debug ("Read signature %d: %s", (gint)i, g_variant_print (child, TRUE)); + + for (GList *l = sign->public_keys; l != NULL; l = l->next) + { + GBytes *public_key = l->data; + /* TODO: use non-list for tons of revoked keys? */ + if (g_list_find_custom (sign->revoked_keys, public_key, g_bytes_compare) != NULL) + { + g_autofree char *hex = g_malloc0 (g_bytes_get_size (public_key) * 2 + 1); + ot_bin2hex (hex, g_bytes_get_data (public_key, NULL), g_bytes_get_size (public_key)); + g_debug ("Skip revoked key '%s'", hex); + continue; + } + + bool valid = false; + if (!otcore_validate_x509_signature (data, public_key, signature, &valid, error)) + return FALSE; + if (!valid) + { + /* Incorrect signature! */ + if (invalid_signatures == NULL) + invalid_signatures = g_string_new (""); + else + g_string_append (invalid_signatures, "; "); + n_invalid_signatures++; + g_autofree char *hex = g_malloc0 (g_bytes_get_size (public_key) * 2 + 1); + ot_bin2hex (hex, g_bytes_get_data (public_key, NULL), g_bytes_get_size (public_key)); + g_string_append_printf (invalid_signatures, "key '%s'", hex); + } + else + { + if (out_success_message) + { + g_autofree char *hex = g_malloc0 (g_bytes_get_size (public_key) * 2 + 1); + ot_bin2hex (hex, g_bytes_get_data (public_key, NULL), + g_bytes_get_size (public_key)); + *out_success_message = g_strdup_printf ( + "x509: Signature verified successfully with key '%s'", hex); + } + return TRUE; + } + } + } + + if (invalid_signatures) + { + g_assert_cmpuint (n_invalid_signatures, >, 0); + /* The test suite has a key ring with 100 keys. This seems insane, let's + * cap a reasonable error message at 3. + */ + if (n_invalid_signatures > 3) + return glnx_throw (error, "x509: Signature couldn't be verified; tried %u keys", + n_invalid_signatures); + return glnx_throw (error, "x509: Signature couldn't be verified with: %s", + invalid_signatures->str); + } + return glnx_throw (error, "x509: no signatures found"); +} + +const gchar * +ostree_sign_x509_get_name (OstreeSign *self) +{ + g_assert (OSTREE_IS_SIGN (self)); + + return OSTREE_SIGN_X509_NAME; +} + +const gchar * +ostree_sign_x509_metadata_key (OstreeSign *self) +{ + + return OSTREE_SIGN_METADATA_X509_KEY; +} + +const gchar * +ostree_sign_x509_metadata_format (OstreeSign *self) +{ + + return OSTREE_SIGN_METADATA_X509_TYPE; +} + +gboolean +ostree_sign_x509_clear_keys (OstreeSign *self, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + OstreeSignX509 *sign = _ostree_sign_x509_get_instance_private (OSTREE_SIGN_X509 (self)); + + if (!_ostree_sign_x509_is_initialized (sign, error)) + return FALSE; + + /* Clear secret key */ + if (sign->secret_key != NULL) + { + gsize size; + gpointer data = g_bytes_unref_to_data (sign->secret_key, &size); + explicit_bzero (data, size); + sign->secret_key = NULL; + } + + /* Clear already loaded trusted keys */ + if (sign->public_keys != NULL) + { + g_list_free_full (sign->public_keys, (GDestroyNotify)g_bytes_unref); + sign->public_keys = NULL; + } + + /* Clear already loaded revoked keys */ + if (sign->revoked_keys != NULL) + { + g_list_free_full (sign->revoked_keys, (GDestroyNotify)g_bytes_unref); + sign->revoked_keys = NULL; + } + + return TRUE; +} + +/* Support 2 representations: + * base64 ascii -- secret key is passed as string + * raw key -- key is passed as bytes array + * */ +gboolean +ostree_sign_x509_set_sk (OstreeSign *self, GVariant *secret_key, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + if (!ostree_sign_x509_clear_keys (self, error)) + return FALSE; + + OstreeSignX509 *sign = _ostree_sign_x509_get_instance_private (OSTREE_SIGN_X509 (self)); + + gsize n_elements = 0; + + g_autofree guchar *secret_key_buf = NULL; + if (g_variant_is_of_type (secret_key, G_VARIANT_TYPE_STRING)) + { + const gchar *sk_ascii = g_variant_get_string (secret_key, NULL); + secret_key_buf = g_base64_decode (sk_ascii, &n_elements); + } + else if (g_variant_is_of_type (secret_key, G_VARIANT_TYPE_BYTESTRING)) + { + secret_key_buf + = (guchar *)g_variant_get_fixed_array (secret_key, &n_elements, sizeof (guchar)); + } + else + { + return glnx_throw (error, "Unknown x509 secret key type"); + } + + sign->secret_key = g_bytes_new_take (g_steal_pointer (&secret_key_buf), n_elements); + + return TRUE; +} + +/* Support 2 representations: + * base64 ascii -- public key is passed as string + * raw key -- key is passed as bytes array + * */ +gboolean +ostree_sign_x509_set_pk (OstreeSign *self, GVariant *public_key, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + if (!ostree_sign_x509_clear_keys (self, error)) + return FALSE; + + return ostree_sign_x509_add_pk (self, public_key, error); +} + +/* Support 2 representations: + * base64 ascii -- public key is passed as string + * raw key -- key is passed as bytes array + * */ +gboolean +ostree_sign_x509_add_pk (OstreeSign *self, GVariant *public_key, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + OstreeSignX509 *sign = _ostree_sign_x509_get_instance_private (OSTREE_SIGN_X509 (self)); + + if (!_ostree_sign_x509_is_initialized (sign, error)) + return FALSE; + + g_autofree guint8 *key_owned = NULL; + const guint8 *key = NULL; + gsize n_elements = 0; + + if (g_variant_is_of_type (public_key, G_VARIANT_TYPE_STRING)) + { + const gchar *pk_ascii = g_variant_get_string (public_key, NULL); + key = key_owned = g_base64_decode (pk_ascii, &n_elements); + } + else if (g_variant_is_of_type (public_key, G_VARIANT_TYPE_BYTESTRING)) + { + key = g_variant_get_fixed_array (public_key, &n_elements, sizeof (guchar)); + } + else + { + return glnx_throw (error, "Unknown x509 public key type"); + } + + g_autofree char *hex = g_malloc0 (n_elements * 2 + 1); + ot_bin2hex (hex, key, n_elements); + g_debug ("Read x509 public key = %s", hex); + + g_autoptr (GBytes) key_bytes = g_bytes_new_static (key, n_elements); + if (g_list_find_custom (sign->public_keys, key_bytes, g_bytes_compare) == NULL) + { + GBytes *new_key_bytes = g_bytes_new (key, n_elements); + sign->public_keys = g_list_prepend (sign->public_keys, new_key_bytes); + } + + return TRUE; +} + +/* Add revoked public key */ +static gboolean +_x509_add_revoked (OstreeSign *self, GVariant *revoked_key, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + OstreeSignX509 *sign = _ostree_sign_x509_get_instance_private (OSTREE_SIGN_X509 (self)); + + g_autofree guint8 *key_owned = NULL; + const guint8 *key = NULL; + gsize n_elements = 0; + + if (g_variant_is_of_type (revoked_key, G_VARIANT_TYPE_STRING)) + { + const gchar *rk_ascii = g_variant_get_string (revoked_key, NULL); + key = key_owned = g_base64_decode (rk_ascii, &n_elements); + } + else if (g_variant_is_of_type (revoked_key, G_VARIANT_TYPE_BYTESTRING)) + { + key = g_variant_get_fixed_array (revoked_key, &n_elements, sizeof (guchar)); + } + else + { + return glnx_throw (error, "Unknown x509 revoked key type"); + } + + g_autofree char *hex = g_malloc0 (n_elements * 2 + 1); + ot_bin2hex (hex, key, n_elements); + g_debug ("Read x509 revoked key = %s", hex); + + g_autoptr (GBytes) key_bytes = g_bytes_new_static (key, n_elements); + if (g_list_find_custom (sign->revoked_keys, key, g_bytes_compare) == NULL) + { + GBytes *new_key_bytes = g_bytes_new (key, n_elements); + sign->revoked_keys = g_list_prepend (sign->revoked_keys, new_key_bytes); + } + + return TRUE; +} + +static gboolean +_load_pk_from_stream (OstreeSign *self, GInputStream *key_stream_in, gboolean trusted, + GError **error) +{ + if (key_stream_in == NULL) + return glnx_throw (error, "x509: unable to read from NULL key-data input stream"); + + gboolean ret = FALSE; + + g_autoptr (OstreeBlobReader) blob_reader = ostree_sign_read_pk (self, key_stream_in); + g_assert (blob_reader); + + /* Use simple file format with just a list of base64 public keys per line */ + while (TRUE) + { + gboolean added = FALSE; + g_autoptr (GError) local_error = NULL; + g_autoptr (GBytes) blob = ostree_blob_reader_read_blob (blob_reader, NULL, &local_error); + + if (local_error != NULL) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return FALSE; + } + + if (blob == NULL) + return ret; + + /* Read the key itself */ + g_autoptr (GVariant) pk = g_variant_new_from_bytes (G_VARIANT_TYPE_BYTESTRING, blob, FALSE); + + if (trusted) + added = ostree_sign_x509_add_pk (self, pk, error); + else + added = _x509_add_revoked (self, pk, error); + + g_autofree gchar *pk_printable = g_variant_print (pk, FALSE); + g_debug ("%s %s key: %s", added ? "Added" : "Invalid", trusted ? "public" : "revoked", + pk_printable); + + /* Mark what we load at least one key */ + if (added) + ret = TRUE; + } + + return ret; +} + +static gboolean +_load_pk_from_file (OstreeSign *self, const gchar *filename, gboolean trusted, GError **error) +{ + g_debug ("Processing file '%s'", filename); + + if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR)) + { + g_debug ("Can't open file '%s' with public keys", filename); + return glnx_throw (error, "File object '%s' is not a regular file", filename); + } + + g_autoptr (GFile) keyfile = keyfile = g_file_new_for_path (filename); + g_autoptr (GFileInputStream) key_stream_in = g_file_read (keyfile, NULL, error); + if (key_stream_in == NULL) + return FALSE; + + if (!_load_pk_from_stream (self, G_INPUT_STREAM (key_stream_in), trusted, error)) + { + if (error == NULL || *error == NULL) + return glnx_throw (error, "signature: x509: no valid keys in file '%s'", filename); + else + return FALSE; + } + + return TRUE; +} + +static gboolean +_x509_load_pk (OstreeSign *self, GVariant *options, gboolean trusted, GError **error) +{ + + gboolean ret = FALSE; + const gchar *custom_dir = NULL; + + g_autoptr (GPtrArray) base_dirs = g_ptr_array_new_with_free_func (g_free); + g_autoptr (GPtrArray) x509_files = g_ptr_array_new_with_free_func (g_free); + + if (g_variant_lookup (options, "basedir", "&s", &custom_dir)) + { + /* Add custom directory */ + g_ptr_array_add (base_dirs, g_strdup (custom_dir)); + } + else + { + /* Default paths where to find files with public keys */ + g_ptr_array_add (base_dirs, g_strdup ("/etc/ostree")); + g_ptr_array_add (base_dirs, g_strdup (DATADIR "/ostree")); + } + + /* Scan all well-known directories and construct the list with file names to scan keys */ + for (gint i = 0; i < base_dirs->len; i++) + { + g_autofree gchar *base_name + = g_build_filename ((gchar *)g_ptr_array_index (base_dirs, i), + trusted ? "trusted.x509" : "revoked.x509", NULL); + + g_debug ("Check x509 keys from file: %s", base_name); + g_autofree gchar *base_dir = g_strconcat (base_name, ".d", NULL); + g_ptr_array_add (x509_files, g_steal_pointer (&base_name)); + + g_autoptr (GDir) dir = g_dir_open (base_dir, 0, error); + if (dir == NULL) + { + g_clear_error (error); + continue; + } + const gchar *entry = NULL; + while ((entry = g_dir_read_name (dir)) != NULL) + { + gchar *filename = g_build_filename (base_dir, entry, NULL); + g_debug ("Check x509 keys from file: %s", filename); + g_ptr_array_add (x509_files, filename); + } + } + + /* Scan all well-known files */ + for (gint i = 0; i < x509_files->len; i++) + { + if (!_load_pk_from_file (self, (gchar *)g_ptr_array_index (x509_files, i), trusted, error)) + { + g_debug ("Problem with loading x509 %s keys from `%s`", trusted ? "public" : "revoked", + (gchar *)g_ptr_array_index (x509_files, i)); + g_clear_error (error); + } + else + ret = TRUE; + } + + if (!ret && (error == NULL || *error == NULL)) + return glnx_throw (error, "signature: x509: no keys loaded"); + + return ret; +} + +/* + * options argument should be a{sv}: + * - filename -- single file to use to load keys from; + * - basedir -- directory containing subdirectories + * 'trusted.x509.d' and 'revoked.x509.d' with appropriate + * public keys. Used for testing and re-definition of system-wide + * directories if defaults are not suitable for any reason. + */ +gboolean +ostree_sign_x509_load_pk (OstreeSign *self, GVariant *options, GError **error) +{ + + const gchar *filename = NULL; + + OstreeSignX509 *sign = _ostree_sign_x509_get_instance_private (OSTREE_SIGN_X509 (self)); + if (!_ostree_sign_x509_is_initialized (sign, error)) + return FALSE; + + /* Read keys only from single file provided */ + if (g_variant_lookup (options, "filename", "&s", &filename)) + return _load_pk_from_file (self, filename, TRUE, error); + + /* Load public keys from well-known directories and files */ + if (!_x509_load_pk (self, options, TRUE, error)) + return FALSE; + + /* Load untrusted keys from well-known directories and files + * Ignore the failure from this function -- it is expected to have + * empty list of revoked keys. + * */ + if (!_x509_load_pk (self, options, FALSE, error)) + g_clear_error (error); + + return TRUE; +} diff --git a/src/libostree/ostree-sign-x509.h b/src/libostree/ostree-sign-x509.h new file mode 100644 index 0000000000..bd8232f188 --- /dev/null +++ b/src/libostree/ostree-sign-x509.h @@ -0,0 +1,54 @@ +/* vim:set et sw=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e2s: */ + +/* + * Copyright © 2019 Collabora Ltd. + * Copyright © 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 . + */ + +#pragma once + +#include "ostree-sign.h" + +G_BEGIN_DECLS + +#define OSTREE_TYPE_SIGN_X509 (_ostree_sign_x509_get_type ()) + +_OSTREE_PUBLIC +G_DECLARE_FINAL_TYPE (OstreeSignX509, _ostree_sign_x509, OSTREE, SIGN_X509, GObject) + +gboolean ostree_sign_x509_data (OstreeSign *self, GBytes *data, GBytes **signature, + GCancellable *cancellable, GError **error); + +gboolean ostree_sign_x509_data_verify (OstreeSign *self, GBytes *data, GVariant *signatures, + char **out_success_message, GError **error); + +const gchar *ostree_sign_x509_get_name (OstreeSign *self); +const gchar *ostree_sign_x509_metadata_key (OstreeSign *self); +const gchar *ostree_sign_x509_metadata_format (OstreeSign *self); + +gboolean ostree_sign_x509_clear_keys (OstreeSign *self, GError **error); + +gboolean ostree_sign_x509_set_sk (OstreeSign *self, GVariant *secret_key, GError **error); + +gboolean ostree_sign_x509_set_pk (OstreeSign *self, GVariant *public_key, GError **error); + +gboolean ostree_sign_x509_add_pk (OstreeSign *self, GVariant *public_key, GError **error); + +gboolean ostree_sign_x509_load_pk (OstreeSign *self, GVariant *options, GError **error); + +G_END_DECLS diff --git a/src/libostree/ostree-sign.c b/src/libostree/ostree-sign.c index ad2358de68..c5d625aaa8 100644 --- a/src/libostree/ostree-sign.c +++ b/src/libostree/ostree-sign.c @@ -39,11 +39,13 @@ #include "ostree-autocleanups.h" #include "ostree-blob-reader-base64.h" +#include "ostree-blob-reader-pem.h" #include "ostree-blob-reader-raw.h" #include "ostree-core.h" #include "ostree-sign-dummy.h" #include "ostree-sign-ed25519.h" #include "ostree-sign-private.h" +#include "ostree-sign-x509.h" #include "ostree-sign.h" #include "ostree-autocleanups.h" @@ -61,6 +63,9 @@ typedef struct _sign_type sign_types[] = { #if defined(HAVE_ED25519) { OSTREE_SIGN_NAME_ED25519, 0 }, +#endif +#if defined(HAVE_X509) + { OSTREE_SIGN_NAME_X509, 0 }, #endif { "dummy", 0 } }; @@ -69,6 +74,9 @@ enum { #if defined(HAVE_ED25519) SIGN_ED25519, +#endif +#if defined(HAVE_X509) + SIGN_X509, #endif SIGN_DUMMY }; @@ -538,6 +546,10 @@ ostree_sign_get_by_name (const gchar *name, GError **error) #if defined(HAVE_ED25519) if (sign_types[SIGN_ED25519].type == 0) sign_types[SIGN_ED25519].type = OSTREE_TYPE_SIGN_ED25519; +#endif +#if defined(HAVE_X509) + if (sign_types[SIGN_X509].type == 0) + sign_types[SIGN_X509].type = OSTREE_TYPE_SIGN_X509; #endif if (sign_types[SIGN_DUMMY].type == 0) sign_types[SIGN_DUMMY].type = OSTREE_TYPE_SIGN_DUMMY; @@ -661,6 +673,10 @@ ostree_sign_read_pk (OstreeSign *self, GInputStream *stream) #if defined(HAVE_ED25519) if (OSTREE_IS_SIGN_ED25519 (self)) return OSTREE_BLOB_READER (_ostree_blob_reader_base64_new (stream)); +#endif +#if defined(HAVE_X509) + if (OSTREE_IS_SIGN_X509 (self)) + return OSTREE_BLOB_READER (_ostree_blob_reader_pem_new (stream, "PUBLIC KEY")); #endif if (OSTREE_IS_SIGN_DUMMY (self)) return OSTREE_BLOB_READER (_ostree_blob_reader_raw_new (stream)); @@ -684,6 +700,10 @@ ostree_sign_read_sk (OstreeSign *self, GInputStream *stream) #if defined(HAVE_ED25519) if (OSTREE_IS_SIGN_ED25519 (self)) return OSTREE_BLOB_READER (_ostree_blob_reader_base64_new (stream)); +#endif +#if defined(HAVE_X509) + if (OSTREE_IS_SIGN_X509 (self)) + return OSTREE_BLOB_READER (_ostree_blob_reader_pem_new (stream, "PRIVATE KEY")); #endif if (OSTREE_IS_SIGN_DUMMY (self)) return OSTREE_BLOB_READER (_ostree_blob_reader_raw_new (stream)); diff --git a/src/libostree/ostree-sign.h b/src/libostree/ostree-sign.h index 81495c3900..b5b7397c6f 100644 --- a/src/libostree/ostree-sign.h +++ b/src/libostree/ostree-sign.h @@ -44,6 +44,14 @@ G_BEGIN_DECLS */ #define OSTREE_SIGN_NAME_ED25519 "ed25519" +/** + * OSTREE_SIGN_NAME_X509: + * The name of the x509 signing type. + * + * Since: 2024.7 + */ +#define OSTREE_SIGN_NAME_X509 "x509" + _OSTREE_PUBLIC G_DECLARE_INTERFACE (OstreeSign, ostree_sign, OSTREE, SIGN, GObject) diff --git a/src/libotcore/otcore-x509-verify.c b/src/libotcore/otcore-x509-verify.c new file mode 100644 index 0000000000..c8fe7611f1 --- /dev/null +++ b/src/libotcore/otcore-x509-verify.c @@ -0,0 +1,89 @@ +/* + * 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_x509_init (void) +{ + return true; +} + +/* Validate a single x509 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_x509_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_OPENSSL) + gsize public_key_size; + const guint8 *public_key_buf = g_bytes_get_data (public_key, &public_key_size); + + gsize signature_size; + const guint8 *signature_buf = g_bytes_get_data (signature, &signature_size); + + if (public_key_size > OSTREE_SIGN_MAX_METADATA_SIZE) + return glnx_throw ( + error, "Invalid public key of %" G_GSIZE_FORMAT " bytes, expected <= %" G_GSIZE_FORMAT, + public_key_size, (gsize)OSTREE_SIGN_MAX_METADATA_SIZE); + + if (signature_size > OSTREE_SIGN_MAX_METADATA_SIZE) + return glnx_throw ( + error, "Invalid signature of %" G_GSIZE_FORMAT " bytes, expected <= %" G_GSIZE_FORMAT, + signature_size, (gsize)OSTREE_SIGN_MAX_METADATA_SIZE); + + EVP_MD_CTX *ctx = EVP_MD_CTX_new (); + if (!ctx) + return glnx_throw (error, "openssl: failed to allocate context"); + + const unsigned char *p = public_key_buf; + EVP_PKEY *pkey = d2i_PUBKEY (NULL, &p, public_key_size); + if (!pkey) + { + EVP_MD_CTX_free (ctx); + return glnx_throw (error, "openssl: Failed to initialize x509 key"); + } + if (EVP_DigestVerifyInit (ctx, NULL, NULL, NULL, pkey) != 0 + && EVP_DigestVerify (ctx, signature_buf, signature_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, "x509 signature validation requested, but support not compiled in"); +#endif +} diff --git a/src/libotcore/otcore.h b/src/libotcore/otcore.h index 6e1d510329..7296aee391 100644 --- a/src/libotcore/otcore.h +++ b/src/libotcore/otcore.h @@ -27,6 +27,7 @@ #define USE_LIBSODIUM #elif defined(HAVE_OPENSSL) #include +#include #define USE_OPENSSL #endif @@ -39,10 +40,22 @@ // The variant type #define OSTREE_SIGN_METADATA_ED25519_TYPE "aay" +// This key is stored inside commit metadata. +#define OSTREE_SIGN_METADATA_X509_KEY "ostree.sign.x509" +// The variant type +#define OSTREE_SIGN_METADATA_X509_TYPE "aay" + +// Maximum size of metadata in bytes, in sync with OSTREE_MAX_METADATA_SIZE +#define OSTREE_SIGN_MAX_METADATA_SIZE (128 * 1024 * 1024) + bool otcore_ed25519_init (void); gboolean otcore_validate_ed25519_signature (GBytes *data, GBytes *pubkey, GBytes *signature, bool *out_valid, GError **error); +bool otcore_x509_init (void); +gboolean otcore_validate_x509_signature (GBytes *data, GBytes *public_key, GBytes *signature, + bool *out_valid, GError **error); + char *otcore_find_proc_cmdline_key (const char *cmdline, const char *key); gboolean otcore_get_ostree_target (const char *cmdline, gboolean *is_aboot, char **out_target, GError **error); diff --git a/tests/libtest.sh b/tests/libtest.sh index 2c2a33f0d9..da556ea912 100755 --- a/tests/libtest.sh +++ b/tests/libtest.sh @@ -780,6 +780,40 @@ gen_ed25519_random_public() openssl genpkey -algorithm ED25519 | openssl pkey -outform DER | tail -c 32 | base64 } +# Keys for x509 signing tests +X509PUBLICPEM= +X509SECRETPEM= + +X509PUBLIC= +X509SECRET= + +gen_x509_keys () +{ + # Generate private key in PEM format + X509SECRETPEM="$(mktemp -p ${test_tmpdir} ed448_sk_XXXXXX.pem)" + openssl genpkey -algorithm ed448 -outform PEM -out "${X509SECRETPEM}" + X509PUBLICPEM="$(mktemp -p ${test_tmpdir} ed448_pk_XXXXXX.pem)" + openssl pkey -outform PEM -pubout -in "${X509SECRETPEM}" -out "${X509PUBLICPEM}" + + X509PUBLIC="$(openssl pkey -inform PEM -outform DER -pubin -pubout -in ${X509PUBLICPEM} | base64 -w 0)" + X509SECRET="$(openssl pkey -inform PEM -outform DER -in ${X509SECRETPEM} | base64 -w 0)" + + echo "Generated ed448 keys:" + echo "public: ${X509PUBLIC}" + echo "secret: ${X509SECRET}" +} + +gen_x509_random_public() +{ + openssl genpkey -algorithm ed448 | openssl pkey -pubout -outform DER | base64 -w 0 + echo +} + +gen_x509_random_public_pem() +{ + openssl genpkey -algorithm ed448 | openssl pkey -pubout -outform PEM +} + is_bare_user_only_repo () { grep -q 'mode=bare-user-only' $1/config } diff --git a/tests/test-signed-commit-dummy.sh b/tests/test-signed-commit-dummy.sh new file mode 100755 index 0000000000..305af38bf5 --- /dev/null +++ b/tests/test-signed-commit-dummy.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# +# Copyright (C) 2019 Collabora Ltd. +# +# 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 -euo pipefail + +. $(dirname $0)/libtest.sh + +# This is explicitly opt in for testing +export OSTREE_DUMMY_SIGN_ENABLED=1 + +mkdir ${test_tmpdir}/repo +ostree_repo_init repo --mode="archive" + +echo "Unsigned commit" > file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s 'Unsigned commit' +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" + +# Test `ostree sign` with dummy module first +DUMMYSIGN="dummysign" +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy ${COMMIT} ${DUMMYSIGN} + +# Ensure that detached metadata really contain expected string +EXPECTEDSIGN="$(echo $DUMMYSIGN | hexdump -n 9 -e '8/1 "0x%.2x, " 1/1 " 0x%.2x"')" +${CMD_PREFIX} ostree --repo=repo show ${COMMIT} --print-detached-metadata-key=ostree.sign.dummy | grep -q -e "${EXPECTEDSIGN}" +tap_ok "Detached dummy signature added" + +# Verify vith sign mechanism +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} +tap_ok "dummy signature verified" + +echo "Signed commit with dummy key: ${DUMMYSIGN}" >> file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s 'Signed with dummy module' --sign=${DUMMYSIGN} --sign-type=dummy +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} +tap_ok "commit with dummy signing" + +if ${CMD_PREFIX} env -u OSTREE_DUMMY_SIGN_ENABLED ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} 2>err.txt; then + fatal "verified dummy signature without env" +fi +# FIXME the error message here is broken +#assert_file_has_content_literal err.txt 'dummy signature type is only for ostree testing' +assert_file_has_content_literal err.txt ' No valid signatures found' +tap_ok "dummy sig requires env" + +tap_end diff --git a/tests/test-signed-commit.sh b/tests/test-signed-commit-ed25519.sh similarity index 76% rename from tests/test-signed-commit.sh rename to tests/test-signed-commit-ed25519.sh index 3fac605e8c..5408ca957a 100755 --- a/tests/test-signed-commit.sh +++ b/tests/test-signed-commit-ed25519.sh @@ -27,48 +27,8 @@ export OSTREE_DUMMY_SIGN_ENABLED=1 mkdir ${test_tmpdir}/repo ostree_repo_init repo --mode="archive" -echo "Unsigned commit" > file.txt -${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s 'Unsigned commit' -COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" - -# Test `ostree sign` with dummy module first +# For multi-sign test DUMMYSIGN="dummysign" -${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy ${COMMIT} ${DUMMYSIGN} - -# Ensure that detached metadata really contain expected string -EXPECTEDSIGN="$(echo $DUMMYSIGN | hexdump -n 9 -e '8/1 "0x%.2x, " 1/1 " 0x%.2x"')" -${CMD_PREFIX} ostree --repo=repo show ${COMMIT} --print-detached-metadata-key=ostree.sign.dummy | grep -q -e "${EXPECTEDSIGN}" -tap_ok "Detached dummy signature added" - -# Verify vith sign mechanism -${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} -tap_ok "dummy signature verified" - -echo "Signed commit with dummy key: ${DUMMYSIGN}" >> file.txt -${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s 'Signed with dummy module' --sign=${DUMMYSIGN} --sign-type=dummy -COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" -${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} -tap_ok "commit with dummy signing" - -if ${CMD_PREFIX} env -u OSTREE_DUMMY_SIGN_ENABLED ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} 2>err.txt; then - fatal "verified dummy signature without env" -fi -# FIXME the error message here is broken -#assert_file_has_content_literal err.txt 'dummy signature type is only for ostree testing' -assert_file_has_content_literal err.txt ' No valid signatures found' -tap_ok "dummy sig requires env" - -# tests below require libsodium support -if ! has_ostree_feature sign-ed25519; then - tap_ok "Detached ed25519 signature # SKIP due libsodium unavailability" - tap_ok "ed25519 signature verified # SKIP due libsodium unavailability" - tap_ok "multiple signing # SKIP due libsodium unavailability" - tap_ok "verify ed25519 keys file # SKIP due libsodium unavailability" - tap_ok "sign with ed25519 keys file # SKIP due libsodium unavailability" - tap_ok "verify ed25519 system-wide configuration # SKIP due libsodium unavailability" - tap_ok "verify ed25519 revoking keys mechanism # SKIP due libsodium unavailability" - exit 0 -fi # Test ostree sign with 'ed25519' module gen_ed25519_keys diff --git a/tests/test-signed-commit-x509.sh b/tests/test-signed-commit-x509.sh new file mode 100755 index 0000000000..83e4cf8abc --- /dev/null +++ b/tests/test-signed-commit-x509.sh @@ -0,0 +1,164 @@ +#!/bin/bash +# +# Copyright (C) 2019 Collabora Ltd. +# +# 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 -euo pipefail + +. $(dirname $0)/libtest.sh + +# This is explicitly opt in for testing +export OSTREE_DUMMY_SIGN_ENABLED=1 + +mkdir ${test_tmpdir}/repo +ostree_repo_init repo --mode="archive" + +# For multi-sign test +DUMMYSIGN="dummysign" + +# Test ostree sign with 'x509' module +gen_x509_keys +PUBLIC=${X509PUBLIC} +SECRET=${X509SECRET} + +WRONG_PUBLIC="$(gen_x509_random_public)" + +echo "PUBLIC = $PUBLIC" + +echo "Signed commit with x509: ${SECRET}" >> file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s "Signed with x509 module" --sign="${SECRET}" --sign-type=x509 +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" + +# Ensure that detached metadata contain signature +${CMD_PREFIX} ostree --repo=repo show ${COMMIT} --print-detached-metadata-key=ostree.sign.x509 &>/dev/null +tap_ok "Detached x509 signature added" + +# Verify vith sign mechanism +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 ${COMMIT} ${WRONG_PUBLIC}; then + exit 1 +fi +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 ${COMMIT} ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 ${COMMIT} ${PUBLIC} ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 ${COMMIT} $(gen_x509_random_public) ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 ${COMMIT} $(gen_x509_random_public) $(gen_x509_random_public) ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 ${COMMIT} ${PUBLIC} $(gen_x509_random_public) $(gen_x509_random_public) +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 ${COMMIT} $(gen_x509_random_public) $(gen_x509_random_public) ${PUBLIC} $(gen_x509_random_public) $(gen_x509_random_public) +tap_ok "x509 signature verified" + +# Check if we are able to use all available modules to sign the same commit +echo "Unsigned commit for multi-sign" >> file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s 'Unsigned commit' +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" +# Check if we have no signatures +for mod in "dummy" "x509"; do + if ostree --repo=repo show ${COMMIT} --print-detached-metadata-key=ostree.sign.${mod}; then + echo "Unexpected signature for ${mod} found" + exit 1 + fi +done + +# Sign with all available modules +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy ${COMMIT} ${DUMMYSIGN} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=x509 ${COMMIT} ${SECRET} +# and verify +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 ${COMMIT} ${PUBLIC} >out.txt +assert_file_has_content out.txt "x509: Signature verified successfully with key" +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} >out.txt +assert_file_has_content out.txt "dummy: Signature verified" +tap_ok "multiple signing " + +# Prepare files with public x509 signatures +PUBKEYS="$(mktemp -p ${test_tmpdir} x509_XXXXXX.x509)" + +# Test if file contain no keys +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-file=${PUBKEYS} ${COMMIT}; then + exit 1 +fi + +# Test if have a problem with file object +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-file=${test_tmpdir} ${COMMIT}; then + exit 1 +fi + +# Test with single key in list +cat ${X509PUBLICPEM} > ${PUBKEYS} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-file=${PUBKEYS} ${COMMIT} >out.txt +assert_file_has_content out.txt 'x509: Signature verified successfully' + +# Test the file with multiple keys without a valid public key +for((i=0;i<100;i++)); do + # Generate a list with some public signatures + gen_x509_random_public_pem +done > ${PUBKEYS} +# Check if file contain no valid signatures +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-file=${PUBKEYS} ${COMMIT} 2>err.txt; then + fatal "validated with no signatures" +fi +assert_file_has_content err.txt 'error:.* x509: Signature couldn.t be verified; tried 100 keys' +# Check if no valid signatures provided via args&file +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-file=${PUBKEYS} ${COMMIT} ${WRONG_PUBLIC}; then + exit 1 +fi + +#Test keys file and public key +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-file=${PUBKEYS} ${COMMIT} ${PUBLIC} + +# Add correct key into the list +cat "${X509PUBLICPEM}" >> ${PUBKEYS} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-file=${PUBKEYS} ${COMMIT} + +tap_ok "verify x509 keys file" + +# Check x509 signing with secret file +echo "Unsigned commit for secret file usage" >> file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s 'Unsigned commit' +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" + +KEYFILE="$(mktemp -p ${test_tmpdir} secret_XXXXXX.x509)" +cat "${X509SECRETPEM}" > ${KEYFILE} +# Sign +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=x509 --keys-file=${KEYFILE} ${COMMIT} +# Verify +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-file=${PUBKEYS} ${COMMIT} +tap_ok "sign with x509 keys file" + +# Check the well-known places mechanism +mkdir -p ${test_tmpdir}/{trusted,revoked}.x509.d +for((i=0;i<100;i++)); do + # Generate some key files with random public signatures + gen_x509_random_public_pem > ${test_tmpdir}/trusted.x509.d/signature_$i +done +# Check no valid public keys are available +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-dir=${test_tmpdir} ${COMMIT}; then + exit 1 +fi +cat "${X509PUBLICPEM}" > ${test_tmpdir}/trusted.x509.d/correct +# Verify with correct key +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-dir=${test_tmpdir} ${COMMIT} + +tap_ok "verify x509 system-wide configuration" + +# Add the public key into revoked list +cat "${X509PUBLICPEM}" > ${test_tmpdir}/revoked.x509.d/correct +# Check if public key is not valid anymore +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=x509 --keys-dir=${test_tmpdir} ${COMMIT}; then + exit 1 +fi +rm -rf ${test_tmpdir}/{trusted,revoked}.x509.d +tap_ok "verify x509 revoking keys mechanism" + +tap_end