diff --git a/Makefile-libostree-defines.am b/Makefile-libostree-defines.am index b01ec2a112..dc56e7060d 100644 --- a/Makefile-libostree-defines.am +++ b/Makefile-libostree-defines.am @@ -48,6 +48,7 @@ libostree_public_headers = \ src/libostree/ostree-kernel-args.h \ src/libostree/ostree-sign.h \ src/libostree/ostree-sign-ed25519.h \ + src/libostree/ostree-blob-reader.h \ $(NULL) # This one is generated via configure.ac, and the gtk-doc diff --git a/Makefile-libostree.am b/Makefile-libostree.am index 11a7bbedd3..e6c1e5537d 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -175,9 +175,9 @@ endif # USE_GPGME symbol_files = $(top_srcdir)/src/libostree/libostree-released.sym # Uncomment this include when adding new development symbols. -#if BUILDOPT_IS_DEVEL_BUILD -#symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym -#endif +if BUILDOPT_IS_DEVEL_BUILD +symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym +endif # http://blog.jgc.org/2007/06/escaping-comma-and-space-in-gnu-make.html wl_versionscript_arg = -Wl,--version-script= @@ -261,7 +261,18 @@ 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-spki.c \ + src/libostree/ostree-sign-spki.h \ src/libostree/ostree-sign-private.h \ + src/libostree/ostree-blob-reader.c \ + src/libostree/ostree-blob-reader.h \ + src/libostree/ostree-blob-reader-base64.c \ + src/libostree/ostree-blob-reader-base64.h \ + src/libostree/ostree-blob-reader-raw.c \ + src/libostree/ostree-blob-reader-raw.h \ + src/libostree/ostree-blob-reader-pem.c \ + src/libostree/ostree-blob-reader-pem.h \ + src/libostree/ostree-blob-reader-private.h \ $(NULL) if USE_COMPOSEFS diff --git a/Makefile-otcore.am b/Makefile-otcore.am index 8252ead00e..ec34578506 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-spki-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 abf61af2d0..46ca848051 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_SPKI +_installed_or_uninstalled_test_scripts += \ + tests/test-signed-commit-spki.sh \ + $(NULL) +endif + if USE_GPGME _installed_or_uninstalled_test_scripts += \ tests/test-remote-gpg-import.sh \ @@ -270,7 +282,7 @@ _installed_or_uninstalled_test_programs = tests/test-varint tests/test-ot-unix-u tests/test-keyfile-utils tests/test-ot-opt-utils tests/test-ot-tool-util \ tests/test-checksum tests/test-lzma tests/test-rollsum \ tests/test-basic-c tests/test-sysroot-c tests/test-pull-c tests/test-repo tests/test-include-ostree-h tests/test-kargs \ - tests/test-rfc2616-dates + tests/test-rfc2616-dates tests/test-pem if USE_GPGME _installed_or_uninstalled_test_programs += \ @@ -403,6 +415,12 @@ tests_test_rfc2616_dates_SOURCES = \ tests_test_rfc2616_dates_CFLAGS = $(TESTS_CFLAGS) tests_test_rfc2616_dates_LDADD = $(TESTS_LDADD) +tests_test_pem_SOURCES = \ + src/libostree/ostree-blob-reader-pem.c \ + tests/test-pem.c +tests_test_pem_CFLAGS = $(TESTS_CFLAGS) +tests_test_pem_LDADD = $(TESTS_LDADD) + noinst_PROGRAMS += tests/test-commit-sign-sh-ext tests_test_commit_sign_sh_ext_CFLAGS = $(TESTS_CFLAGS) tests_test_commit_sign_sh_ext_LDADD = $(TESTS_LDADD) diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt index b46e606c6a..c515d4c441 100644 --- a/apidoc/ostree-sections.txt +++ b/apidoc/ostree-sections.txt @@ -767,6 +767,15 @@ ostree_sign_metadata_key ostree_sign_set_pk ostree_sign_set_sk ostree_sign_summary +ostree_sign_read_pk +ostree_sign_read_sk ostree_sign_get_type + +
+ostree-blob-reader +ostree_blob_reader_read_blob + +ostree_blob_reader_get_type +
diff --git a/configure.ac b/configure.ac index 538ff3456e..c2fdbf6732 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_SPKI], [test x$with_openssl != xno]) + +AM_COND_IF([HAVE_SPKI], [ + AC_DEFINE([HAVE_SPKI], 1, [Define if spki is supported ]) + OSTREE_FEATURES="$OSTREE_FEATURES sign-spki" +]) 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 spki 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..013ec3aadb 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, spki, 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 spki 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 eb63f7ff83..bae07473cb 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 `spki`, 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/rvokeded.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 `spki` 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, spki, 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 spki 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..bf6a2e0a29 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_SPKI); 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/libostree-devel.sym b/src/libostree/libostree-devel.sym index 6640e11c78..9e35a6eeae 100644 --- a/src/libostree/libostree-devel.sym +++ b/src/libostree/libostree-devel.sym @@ -20,6 +20,14 @@ - uncomment the include in Makefile-libostree.am */ +LIBOSTREE_2024.8 { +global: + ostree_sign_read_pk; + ostree_sign_read_sk; + ostree_blob_reader_get_type; + ostree_blob_reader_read_blob; +} LIBOSTREE_2024.7; + /* Stub section for the stable release *after* this development one; don't * edit this other than to update the year. This is just a copy/paste * source. Replace $LASTSTABLE with the last stable version, and $NEWVERSION diff --git a/src/libostree/ostree-blob-reader-base64.c b/src/libostree/ostree-blob-reader-base64.c new file mode 100644 index 0000000000..6faa482027 --- /dev/null +++ b/src/libostree/ostree-blob-reader-base64.c @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.0+ + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "config.h" + +#include "ostree-blob-reader-base64.h" + +struct _OstreeBlobReaderBase64 +{ + GDataInputStream parent_instance; +}; + +static void ostree_blob_reader_base64_iface_init (OstreeBlobReaderInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (OstreeBlobReaderBase64, _ostree_blob_reader_base64, + G_TYPE_DATA_INPUT_STREAM, + G_IMPLEMENT_INTERFACE (OSTREE_TYPE_BLOB_READER, + ostree_blob_reader_base64_iface_init)); + +static void +ostree_blob_reader_base64_iface_init (OstreeBlobReaderInterface *iface) +{ + iface->read_blob = ostree_blob_reader_base64_read_blob; +} + +static void +_ostree_blob_reader_base64_class_init (OstreeBlobReaderBase64Class *klass) +{ +} + +static void +_ostree_blob_reader_base64_init (OstreeBlobReaderBase64 *self) +{ +} + +OstreeBlobReaderBase64 * +_ostree_blob_reader_base64_new (GInputStream *stream) +{ + return g_object_new (OSTREE_TYPE_BLOB_READER_BASE64, "base-stream", stream, NULL); +} + +GBytes * +ostree_blob_reader_base64_read_blob (OstreeBlobReader *self, GCancellable *cancellable, + GError **error) +{ + gsize len = 0; + g_autoptr (GError) local_error = NULL; + g_autofree char *line + = g_data_input_stream_read_line (G_DATA_INPUT_STREAM (self), &len, cancellable, &local_error); + if (local_error != NULL) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return NULL; + } + + if (line == NULL) + return NULL; + + gsize n_elements; + g_base64_decode_inplace (line, &n_elements); + explicit_bzero (line + n_elements, len - n_elements); + + return g_bytes_new_take (g_steal_pointer (&line), n_elements); +} diff --git a/src/libostree/ostree-blob-reader-base64.h b/src/libostree/ostree-blob-reader-base64.h new file mode 100644 index 0000000000..b1e78ba2f3 --- /dev/null +++ b/src/libostree/ostree-blob-reader-base64.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.0+ + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#include "ostree-blob-reader.h" + +G_BEGIN_DECLS + +#define OSTREE_TYPE_BLOB_READER_BASE64 (_ostree_blob_reader_base64_get_type ()) + +_OSTREE_PUBLIC +G_DECLARE_FINAL_TYPE (OstreeBlobReaderBase64, _ostree_blob_reader_base64, OSTREE, + BLOB_READER_BASE64, GDataInputStream); + +_OSTREE_PUBLIC +OstreeBlobReaderBase64 *_ostree_blob_reader_base64_new (GInputStream *stream); + +_OSTREE_PUBLIC +GBytes *ostree_blob_reader_base64_read_blob (OstreeBlobReader *self, GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/src/libostree/ostree-blob-reader-pem.c b/src/libostree/ostree-blob-reader-pem.c new file mode 100644 index 0000000000..85c544f55a --- /dev/null +++ b/src/libostree/ostree-blob-reader-pem.c @@ -0,0 +1,225 @@ +/* + * 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 . + */ + +/* This implements a simple parser of the PEM format defined in RFC + * 7468, which doesn't allow headers to be encoded alongside the data + * (unlike the legacy RFC 1421). + */ + +#include "config.h" + +#include "ostree-blob-reader-pem.h" +#include "ostree-blob-reader-private.h" +#include + +enum +{ + PROP_0, + PROP_LABEL +}; + +struct _OstreeBlobReaderPem +{ + GDataInputStream parent_instance; + + gchar *label; +}; + +static void ostree_blob_reader_pem_iface_init (OstreeBlobReaderInterface *iface); +G_DEFINE_TYPE_WITH_CODE (OstreeBlobReaderPem, _ostree_blob_reader_pem, G_TYPE_DATA_INPUT_STREAM, + G_IMPLEMENT_INTERFACE (OSTREE_TYPE_BLOB_READER, + ostree_blob_reader_pem_iface_init)); + +static void +ostree_blob_reader_pem_iface_init (OstreeBlobReaderInterface *iface) +{ + iface->read_blob = ostree_blob_reader_pem_read_blob; +} + +static void +ostree_blob_reader_pem_set_property (GObject *object, guint prop_id, const GValue *value, + GParamSpec *pspec) +{ + OstreeBlobReaderPem *self = OSTREE_BLOB_READER_PEM (object); + + switch (prop_id) + { + case PROP_LABEL: + self->label = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +ostree_blob_reader_pem_get_property (GObject *object, guint prop_id, GValue *value, + GParamSpec *pspec) +{ + OstreeBlobReaderPem *self = OSTREE_BLOB_READER_PEM (object); + + switch (prop_id) + { + case PROP_LABEL: + g_value_set_string (value, self->label); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +ostree_blob_reader_pem_finalize (GObject *object) +{ + OstreeBlobReaderPem *self = OSTREE_BLOB_READER_PEM (object); + + g_free (self->label); + + G_OBJECT_CLASS (_ostree_blob_reader_pem_parent_class)->finalize (object); +} + +static void +_ostree_blob_reader_pem_class_init (OstreeBlobReaderPemClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->set_property = ostree_blob_reader_pem_set_property; + gobject_class->get_property = ostree_blob_reader_pem_get_property; + gobject_class->finalize = ostree_blob_reader_pem_finalize; + + /* + * OstreeBlobReaderPem:label: + * + * The label to filter the PEM blocks. + */ + g_object_class_install_property ( + gobject_class, PROP_LABEL, + g_param_spec_string ("label", "", "", "", G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} + +static void +_ostree_blob_reader_pem_init (OstreeBlobReaderPem *self) +{ +} + +OstreeBlobReaderPem * +_ostree_blob_reader_pem_new (GInputStream *base, const gchar *label) +{ + g_assert (G_IS_INPUT_STREAM (base)); + + return g_object_new (OSTREE_TYPE_BLOB_READER_PEM, "base-stream", base, "label", label, NULL); +} + +enum PemInputState +{ + PEM_INPUT_STATE_OUTER, + PEM_INPUT_STATE_INNER +}; + +#define PEM_SUFFIX "-----" +#define PEM_PREFIX_BEGIN "-----BEGIN " +#define PEM_PREFIX_END "-----END " + +GBytes * +_ostree_read_pem_block (GDataInputStream *stream, gchar **label, GCancellable *cancellable, + GError **error) +{ + enum PemInputState state = PEM_INPUT_STATE_OUTER; + g_autofree gchar *tmp_label = NULL; + g_autoptr (GString) buf = g_string_new (""); + + while (TRUE) + { + gsize length; + g_autofree gchar *line = g_data_input_stream_read_line (stream, &length, cancellable, error); + if (!line) + break; + + line = g_strstrip (line); + if (*line == '\0') + continue; + + switch (state) + { + case PEM_INPUT_STATE_OUTER: + if (g_str_has_prefix (line, PEM_PREFIX_BEGIN) && g_str_has_suffix (line, PEM_SUFFIX)) + { + const gchar *start = line + sizeof (PEM_PREFIX_BEGIN) - 1; + const gchar *end = g_strrstr (start + 1, PEM_SUFFIX); + + tmp_label = g_strndup (start, end - start); + state = PEM_INPUT_STATE_INNER; + } + break; + + case PEM_INPUT_STATE_INNER: + if (g_str_has_prefix (line, PEM_PREFIX_END) && g_str_has_suffix (line, PEM_SUFFIX)) + { + const gchar *start = line + sizeof (PEM_PREFIX_END) - 1; + const gchar *end = g_strrstr (start + 1, PEM_SUFFIX); + + if (strncmp (tmp_label, start, end - start) != 0) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unmatched PEM header"); + return NULL; + } + + g_base64_decode_inplace (buf->str, &buf->len); + GBytes *result = g_bytes_new_take (buf->str, buf->len); + + /* Don't leak the trailing encoded bytes */ + explicit_bzero (buf->str + buf->len, buf->allocated_len - buf->len); + g_string_free (buf, FALSE); + buf = NULL; + + if (label) + *label = g_steal_pointer (&tmp_label); + + return result; + } + else + g_string_append (buf, line); + break; + + default: + g_assert_not_reached (); + } + } + + if (state != PEM_INPUT_STATE_OUTER) + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "PEM trailer not found"); + return NULL; +} + +GBytes * +ostree_blob_reader_pem_read_blob (OstreeBlobReader *self, GCancellable *cancellable, GError **error) +{ + OstreeBlobReaderPem *pself = OSTREE_BLOB_READER_PEM (self); + + g_autofree gchar *label = NULL; + g_autoptr (GBytes) blob + = _ostree_read_pem_block (G_DATA_INPUT_STREAM (self), &label, cancellable, error); + if (blob != NULL && !g_str_equal (label, pself->label)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Unexpected label \"%s\"", label); + g_clear_pointer (&blob, g_bytes_unref); + } + return g_steal_pointer (&blob); +} diff --git a/src/libostree/ostree-blob-reader-pem.h b/src/libostree/ostree-blob-reader-pem.h new file mode 100644 index 0000000000..04efb99d5d --- /dev/null +++ b/src/libostree/ostree-blob-reader-pem.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.0+ + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#include "ostree-blob-reader.h" + +G_BEGIN_DECLS + +#define OSTREE_TYPE_BLOB_READER_PEM (_ostree_blob_reader_pem_get_type ()) + +_OSTREE_PUBLIC +G_DECLARE_FINAL_TYPE (OstreeBlobReaderPem, _ostree_blob_reader_pem, OSTREE, BLOB_READER_PEM, + GDataInputStream); + +_OSTREE_PUBLIC +OstreeBlobReaderPem *_ostree_blob_reader_pem_new (GInputStream *base, const gchar *label); + +_OSTREE_PUBLIC +GBytes *ostree_blob_reader_pem_read_blob (OstreeBlobReader *self, GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/src/libostree/ostree-blob-reader-private.h b/src/libostree/ostree-blob-reader-private.h new file mode 100644 index 0000000000..52b589be65 --- /dev/null +++ b/src/libostree/ostree-blob-reader-private.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.0+ + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#ifndef __GI_SCANNER__ + +#include + +G_BEGIN_DECLS + +GBytes *_ostree_read_pem_block (GDataInputStream *stream, gchar **label, GCancellable *cancellable, + GError **error); + +G_END_DECLS + +#endif diff --git a/src/libostree/ostree-blob-reader-raw.c b/src/libostree/ostree-blob-reader-raw.c new file mode 100644 index 0000000000..8535525436 --- /dev/null +++ b/src/libostree/ostree-blob-reader-raw.c @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.0+ + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "config.h" + +#include "ostree-blob-reader-raw.h" + +struct _OstreeBlobReaderRaw +{ + GDataInputStream parent_instance; +}; + +static void ostree_blob_reader_raw_iface_init (OstreeBlobReaderInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (OstreeBlobReaderRaw, _ostree_blob_reader_raw, G_TYPE_DATA_INPUT_STREAM, + G_IMPLEMENT_INTERFACE (OSTREE_TYPE_BLOB_READER, + ostree_blob_reader_raw_iface_init)); + +static void +ostree_blob_reader_raw_iface_init (OstreeBlobReaderInterface *iface) +{ + iface->read_blob = ostree_blob_reader_raw_read_blob; +} + +static void +_ostree_blob_reader_raw_class_init (OstreeBlobReaderRawClass *klass) +{ +} + +static void +_ostree_blob_reader_raw_init (OstreeBlobReaderRaw *self) +{ +} + +OstreeBlobReaderRaw * +_ostree_blob_reader_raw_new (GInputStream *stream) +{ + return g_object_new (OSTREE_TYPE_BLOB_READER_RAW, "base-stream", stream, NULL); +} + +GBytes * +ostree_blob_reader_raw_read_blob (OstreeBlobReader *self, GCancellable *cancellable, GError **error) +{ + gsize len = 0; + g_autoptr (GError) local_error = NULL; + g_autofree char *line + = g_data_input_stream_read_line (G_DATA_INPUT_STREAM (self), &len, cancellable, &local_error); + if (local_error != NULL) + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return NULL; + } + + if (line == NULL) + return NULL; + + return g_bytes_new_take (g_steal_pointer (&line), len); +} diff --git a/src/libostree/ostree-blob-reader-raw.h b/src/libostree/ostree-blob-reader-raw.h new file mode 100644 index 0000000000..614e26baf5 --- /dev/null +++ b/src/libostree/ostree-blob-reader-raw.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.0+ + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#include "ostree-blob-reader.h" + +G_BEGIN_DECLS + +#define OSTREE_TYPE_BLOB_READER_RAW (_ostree_blob_reader_raw_get_type ()) + +_OSTREE_PUBLIC +G_DECLARE_FINAL_TYPE (OstreeBlobReaderRaw, _ostree_blob_reader_raw, OSTREE, BLOB_READER_RAW, + GDataInputStream); + +_OSTREE_PUBLIC +OstreeBlobReaderRaw *_ostree_blob_reader_raw_new (GInputStream *stream); + +_OSTREE_PUBLIC +GBytes *ostree_blob_reader_raw_read_blob (OstreeBlobReader *self, GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/src/libostree/ostree-blob-reader.c b/src/libostree/ostree-blob-reader.c new file mode 100644 index 0000000000..4f1314b844 --- /dev/null +++ b/src/libostree/ostree-blob-reader.c @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.0+ + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "config.h" + +#include "ostree-blob-reader.h" + +G_DEFINE_INTERFACE (OstreeBlobReader, ostree_blob_reader, G_TYPE_OBJECT); + +static void +ostree_blob_reader_default_init (OstreeBlobReaderInterface *iface) +{ + g_debug ("OstreeBlobReader initialization"); +} + +GBytes * +ostree_blob_reader_read_blob (OstreeBlobReader *self, GCancellable *cancellable, GError **error) +{ + g_assert (OSTREE_IS_BLOB_READER (self)); + return OSTREE_BLOB_READER_GET_IFACE (self)->read_blob (self, cancellable, error); +} diff --git a/src/libostree/ostree-blob-reader.h b/src/libostree/ostree-blob-reader.h new file mode 100644 index 0000000000..caf8fb24f2 --- /dev/null +++ b/src/libostree/ostree-blob-reader.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.0+ + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#include "ostree-types.h" +#include + +G_BEGIN_DECLS + +#define OSTREE_TYPE_BLOB_READER (ostree_blob_reader_get_type ()) +_OSTREE_PUBLIC +G_DECLARE_INTERFACE (OstreeBlobReader, ostree_blob_reader, OSTREE, BLOB_READER, GObject) + +struct _OstreeBlobReaderInterface +{ + GTypeInterface g_iface; + + GBytes *(*read_blob) (OstreeBlobReader *self, GCancellable *cancellable, GError **error); +}; + +_OSTREE_PUBLIC +GBytes *ostree_blob_reader_read_blob (OstreeBlobReader *self, GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/src/libostree/ostree-sign-ed25519.c b/src/libostree/ostree-sign-ed25519.c index 3dcff2ebb5..b7718880e5 100644 --- a/src/libostree/ostree-sign-ed25519.c +++ b/src/libostree/ostree-sign-ed25519.c @@ -27,6 +27,7 @@ #include "otcore.h" #include #include +#include #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "OSTreeSign" @@ -103,7 +104,7 @@ validate_length (gsize found, gsize expected, GError **error) return TRUE; return glnx_throw ( error, "Ill-formed input: expected %" G_GSIZE_FORMAT " bytes, got %" G_GSIZE_FORMAT " bytes", - found, expected); + expected, found); } static gboolean @@ -152,7 +153,7 @@ ostree_sign_ed25519_data (OstreeSign *self, GBytes *data, GBytes **signature, if (!pkey) { EVP_MD_CTX_free (ctx); - return glnx_throw (error, "openssl: Failed to initialize ed5519 key"); + return glnx_throw (error, "openssl: Failed to initialize ed25519 key"); } size_t len; @@ -320,7 +321,7 @@ ostree_sign_ed25519_clear_keys (OstreeSign *self, GError **error) /* Clear secret key */ if (sign->secret_key != NULL) { - memset (sign->secret_key, 0, OSTREE_SIGN_ED25519_SECKEY_SIZE); + explicit_bzero (sign->secret_key, OSTREE_SIGN_ED25519_SECKEY_SIZE); g_free (sign->secret_key); sign->secret_key = NULL; } @@ -451,14 +452,25 @@ _ed25519_add_revoked (OstreeSign *self, GVariant *revoked_key, GError **error) { g_assert (OSTREE_IS_SIGN (self)); - if (!g_variant_is_of_type (revoked_key, G_VARIANT_TYPE_STRING)) - return glnx_throw (error, "Unknown ed25519 revoked key type"); - OstreeSignEd25519 *sign = _ostree_sign_ed25519_get_instance_private (OSTREE_SIGN_ED25519 (self)); - const gchar *rk_ascii = g_variant_get_string (revoked_key, NULL); + g_autofree guint8 *key_owned = NULL; + const guint8 *key = NULL; gsize n_elements = 0; - g_autofree guint8 *key = g_base64_decode (rk_ascii, &n_elements); + + 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 ed25519 revoked key type"); + } if (!validate_length (n_elements, OSTREE_SIGN_ED25519_PUBKEY_SIZE, error)) return glnx_prefix_error (error, "Incorrect ed25519 revoked key"); @@ -477,22 +489,24 @@ _ed25519_add_revoked (OstreeSign *self, GVariant *revoked_key, GError **error) } static gboolean -_load_pk_from_stream (OstreeSign *self, GDataInputStream *key_data_in, gboolean trusted, +_load_pk_from_stream (OstreeSign *self, GInputStream *key_stream_in, gboolean trusted, GError **error) { - if (key_data_in == NULL) + if (key_stream_in == NULL) return glnx_throw (error, "ed25519: 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) { - gsize len = 0; g_autoptr (GVariant) pk = NULL; gboolean added = FALSE; g_autoptr (GError) local_error = NULL; - g_autofree char *line = g_data_input_stream_read_line (key_data_in, &len, NULL, &local_error); + g_autoptr (GBytes) blob = ostree_blob_reader_read_blob (blob_reader, NULL, &local_error); if (local_error != NULL) { @@ -500,19 +514,20 @@ _load_pk_from_stream (OstreeSign *self, GDataInputStream *key_data_in, gboolean return FALSE; } - if (line == NULL) + if (blob == NULL) return ret; /* Read the key itself */ - /* base64 encoded key */ - pk = g_variant_new_string (line); + pk = g_variant_new_from_bytes (G_VARIANT_TYPE_BYTESTRING, blob, FALSE); if (trusted) added = ostree_sign_ed25519_add_pk (self, pk, error); else added = _ed25519_add_revoked (self, pk, error); - g_debug ("%s %s key: %s", added ? "Added" : "Invalid", trusted ? "public" : "revoked", line); + 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) @@ -529,7 +544,6 @@ _load_pk_from_file (OstreeSign *self, const gchar *filename, gboolean trusted, G g_autoptr (GFile) keyfile = NULL; g_autoptr (GFileInputStream) key_stream_in = NULL; - g_autoptr (GDataInputStream) key_data_in = NULL; if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR)) { @@ -542,10 +556,7 @@ _load_pk_from_file (OstreeSign *self, const gchar *filename, gboolean trusted, G if (key_stream_in == NULL) return FALSE; - key_data_in = g_data_input_stream_new (G_INPUT_STREAM (key_stream_in)); - g_assert (key_data_in != NULL); - - if (!_load_pk_from_stream (self, key_data_in, trusted, error)) + if (!_load_pk_from_stream (self, G_INPUT_STREAM (key_stream_in), trusted, error)) { if (error == NULL || *error == NULL) return glnx_throw (error, "signature: ed25519: no valid keys in file '%s'", filename); diff --git a/src/libostree/ostree-sign-spki.c b/src/libostree/ostree-sign-spki.c new file mode 100644 index 0000000000..5ad81da399 --- /dev/null +++ b/src/libostree/ostree-sign-spki.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-spki.h" +#include "otcore.h" +#include +#include +#include + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "OSTreeSign" + +#define OSTREE_SIGN_SPKI_NAME "spki" + +typedef enum +{ + SPKI_OK, + SPKI_NOT_SUPPORTED, + SPKI_FAILED_INITIALIZATION +} spki_state; + +struct _OstreeSignSpki +{ + GObject parent; + spki_state state; + GBytes *secret_key; + GList *public_keys; /* GBytes */ + GList *revoked_keys; /* GBytes */ +}; + +static void ostree_sign_spki_iface_init (OstreeSignInterface *self); + +G_DEFINE_TYPE_WITH_CODE (OstreeSignSpki, _ostree_sign_spki, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (OSTREE_TYPE_SIGN, ostree_sign_spki_iface_init)); + +static void +ostree_sign_spki_iface_init (OstreeSignInterface *self) +{ + + self->data = ostree_sign_spki_data; + self->data_verify = ostree_sign_spki_data_verify; + self->get_name = ostree_sign_spki_get_name; + self->metadata_key = ostree_sign_spki_metadata_key; + self->metadata_format = ostree_sign_spki_metadata_format; + self->clear_keys = ostree_sign_spki_clear_keys; + self->set_sk = ostree_sign_spki_set_sk; + self->set_pk = ostree_sign_spki_set_pk; + self->add_pk = ostree_sign_spki_add_pk; + self->load_pk = ostree_sign_spki_load_pk; +} + +static void +_ostree_sign_spki_class_init (OstreeSignSpkiClass *self) +{ +} + +static void +_ostree_sign_spki_init (OstreeSignSpki *self) +{ + + self->state = SPKI_OK; + self->secret_key = NULL; + self->public_keys = NULL; + self->revoked_keys = NULL; + +#if !defined(USE_OPENSSL) + self->state = SPKI_NOT_SUPPORTED; +#else + if (!otcore_spki_init ()) + self->state = SPKI_FAILED_INITIALIZATION; +#endif +} + +static gboolean +_ostree_sign_spki_is_initialized (OstreeSignSpki *self, GError **error) +{ + switch (self->state) + { + case SPKI_OK: + break; + case SPKI_NOT_SUPPORTED: + return glnx_throw (error, "spki: engine is not supported"); + case SPKI_FAILED_INITIALIZATION: + return glnx_throw (error, "spki: crypto library isn't initialized properly"); + } + + return TRUE; +} + +gboolean +ostree_sign_spki_data (OstreeSign *self, GBytes *data, GBytes **signature, + GCancellable *cancellable, GError **error) +{ +#if defined(USE_OPENSSL) + g_assert (OSTREE_IS_SIGN (self)); + OstreeSignSpki *sign = _ostree_sign_spki_get_instance_private (OSTREE_SIGN_SPKI (self)); + + if (!_ostree_sign_spki_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 spki 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, "spki signature validation requested, but support not compiled in"); +#endif +} + +gboolean +ostree_sign_spki_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, "spki: unable to verify NULL data"); + + OstreeSignSpki *sign = _ostree_sign_spki_get_instance_private (OSTREE_SIGN_SPKI (self)); + + if (!_ostree_sign_spki_is_initialized (sign, error)) + return FALSE; + + if (signatures == NULL) + return glnx_throw (error, "spki: commit have no signatures of my type"); + + if (!g_variant_is_of_type (signatures, (GVariantType *)OSTREE_SIGN_METADATA_SPKI_TYPE)) + return glnx_throw (error, "spki: 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_spki_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_spki_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 ( + "spki: 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, "spki: Signature couldn't be verified; tried %u keys", + n_invalid_signatures); + return glnx_throw (error, "spki: Signature couldn't be verified with: %s", + invalid_signatures->str); + } + return glnx_throw (error, "spki: no signatures found"); +} + +const gchar * +ostree_sign_spki_get_name (OstreeSign *self) +{ + g_assert (OSTREE_IS_SIGN (self)); + + return OSTREE_SIGN_SPKI_NAME; +} + +const gchar * +ostree_sign_spki_metadata_key (OstreeSign *self) +{ + + return OSTREE_SIGN_METADATA_SPKI_KEY; +} + +const gchar * +ostree_sign_spki_metadata_format (OstreeSign *self) +{ + + return OSTREE_SIGN_METADATA_SPKI_TYPE; +} + +gboolean +ostree_sign_spki_clear_keys (OstreeSign *self, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + OstreeSignSpki *sign = _ostree_sign_spki_get_instance_private (OSTREE_SIGN_SPKI (self)); + + if (!_ostree_sign_spki_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_spki_set_sk (OstreeSign *self, GVariant *secret_key, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + if (!ostree_sign_spki_clear_keys (self, error)) + return FALSE; + + OstreeSignSpki *sign = _ostree_sign_spki_get_instance_private (OSTREE_SIGN_SPKI (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 spki 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_spki_set_pk (OstreeSign *self, GVariant *public_key, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + if (!ostree_sign_spki_clear_keys (self, error)) + return FALSE; + + return ostree_sign_spki_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_spki_add_pk (OstreeSign *self, GVariant *public_key, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + OstreeSignSpki *sign = _ostree_sign_spki_get_instance_private (OSTREE_SIGN_SPKI (self)); + + if (!_ostree_sign_spki_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 spki public key type"); + } + + g_autofree char *hex = g_malloc0 (n_elements * 2 + 1); + ot_bin2hex (hex, key, n_elements); + g_debug ("Read spki 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 +_spki_add_revoked (OstreeSign *self, GVariant *revoked_key, GError **error) +{ + g_assert (OSTREE_IS_SIGN (self)); + + OstreeSignSpki *sign = _ostree_sign_spki_get_instance_private (OSTREE_SIGN_SPKI (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 spki revoked key type"); + } + + g_autofree char *hex = g_malloc0 (n_elements * 2 + 1); + ot_bin2hex (hex, key, n_elements); + g_debug ("Read spki 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, "spki: 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_spki_add_pk (self, pk, error); + else + added = _spki_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: spki: no valid keys in file '%s'", filename); + else + return FALSE; + } + + return TRUE; +} + +static gboolean +_spki_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) spki_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.spki" : "revoked.spki", NULL); + + g_debug ("Check spki keys from file: %s", base_name); + g_autofree gchar *base_dir = g_strconcat (base_name, ".d", NULL); + g_ptr_array_add (spki_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 spki keys from file: %s", filename); + g_ptr_array_add (spki_files, filename); + } + } + + /* Scan all well-known files */ + for (gint i = 0; i < spki_files->len; i++) + { + if (!_load_pk_from_file (self, (gchar *)g_ptr_array_index (spki_files, i), trusted, error)) + { + g_debug ("Problem with loading spki %s keys from `%s`", trusted ? "public" : "revoked", + (gchar *)g_ptr_array_index (spki_files, i)); + g_clear_error (error); + } + else + ret = TRUE; + } + + if (!ret && (error == NULL || *error == NULL)) + return glnx_throw (error, "signature: spki: 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.spki.d' and 'revoked.spki.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_spki_load_pk (OstreeSign *self, GVariant *options, GError **error) +{ + + const gchar *filename = NULL; + + OstreeSignSpki *sign = _ostree_sign_spki_get_instance_private (OSTREE_SIGN_SPKI (self)); + if (!_ostree_sign_spki_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 (!_spki_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 (!_spki_load_pk (self, options, FALSE, error)) + g_clear_error (error); + + return TRUE; +} diff --git a/src/libostree/ostree-sign-spki.h b/src/libostree/ostree-sign-spki.h new file mode 100644 index 0000000000..089bd27235 --- /dev/null +++ b/src/libostree/ostree-sign-spki.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_SPKI (_ostree_sign_spki_get_type ()) + +_OSTREE_PUBLIC +G_DECLARE_FINAL_TYPE (OstreeSignSpki, _ostree_sign_spki, OSTREE, SIGN_SPKI, GObject) + +gboolean ostree_sign_spki_data (OstreeSign *self, GBytes *data, GBytes **signature, + GCancellable *cancellable, GError **error); + +gboolean ostree_sign_spki_data_verify (OstreeSign *self, GBytes *data, GVariant *signatures, + char **out_success_message, GError **error); + +const gchar *ostree_sign_spki_get_name (OstreeSign *self); +const gchar *ostree_sign_spki_metadata_key (OstreeSign *self); +const gchar *ostree_sign_spki_metadata_format (OstreeSign *self); + +gboolean ostree_sign_spki_clear_keys (OstreeSign *self, GError **error); + +gboolean ostree_sign_spki_set_sk (OstreeSign *self, GVariant *secret_key, GError **error); + +gboolean ostree_sign_spki_set_pk (OstreeSign *self, GVariant *public_key, GError **error); + +gboolean ostree_sign_spki_add_pk (OstreeSign *self, GVariant *public_key, GError **error); + +gboolean ostree_sign_spki_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 d76271bca7..2d9f2ba622 100644 --- a/src/libostree/ostree-sign.c +++ b/src/libostree/ostree-sign.c @@ -38,10 +38,14 @@ #include #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-spki.h" #include "ostree-sign.h" #include "ostree-autocleanups.h" @@ -59,6 +63,9 @@ typedef struct _sign_type sign_types[] = { #if defined(HAVE_ED25519) { OSTREE_SIGN_NAME_ED25519, 0 }, +#endif +#if defined(HAVE_SPKI) + { OSTREE_SIGN_NAME_SPKI, 0 }, #endif { "dummy", 0 } }; @@ -67,6 +74,9 @@ enum { #if defined(HAVE_ED25519) SIGN_ED25519, +#endif +#if defined(HAVE_SPKI) + SIGN_SPKI, #endif SIGN_DUMMY }; @@ -536,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_SPKI) + if (sign_types[SIGN_SPKI].type == 0) + sign_types[SIGN_SPKI].type = OSTREE_TYPE_SIGN_SPKI; #endif if (sign_types[SIGN_DUMMY].type == 0) sign_types[SIGN_DUMMY].type = OSTREE_TYPE_SIGN_DUMMY; @@ -641,3 +655,57 @@ ostree_sign_summary (OstreeSign *self, OstreeRepo *repo, GVariant *keys, GCancel { return _ostree_sign_summary_at (self, repo, repo->repo_dir_fd, keys, cancellable, error); } + +/** + * ostree_sign_read_pk: + * @self: Self + * @stream: a #GInputStream + * + * Start reading public keys from a stream. + * + * Returns: (transfer full): a #OstreamBlobReader or %NULL on error + * + * Since: 2024.8 + */ +OstreeBlobReader * +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_SPKI) + if (OSTREE_IS_SIGN_SPKI (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)); + return NULL; +} + +/** + * ostree_sign_read_sk: + * @self: Self + * @stream: a #GInputStream + * + * Start reading secret keys from a stream. + * + * Returns: (transfer full): a #OstreamBlobReader or %NULL on error + * + * Since: 2024.8 + */ +OstreeBlobReader * +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_SPKI) + if (OSTREE_IS_SIGN_SPKI (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)); + return NULL; +} diff --git a/src/libostree/ostree-sign.h b/src/libostree/ostree-sign.h index 9424b258f8..a817f32ada 100644 --- a/src/libostree/ostree-sign.h +++ b/src/libostree/ostree-sign.h @@ -27,6 +27,7 @@ #include #include +#include "ostree-blob-reader.h" #include "ostree-ref.h" #include "ostree-remote.h" #include "ostree-types.h" @@ -43,6 +44,14 @@ G_BEGIN_DECLS */ #define OSTREE_SIGN_NAME_ED25519 "ed25519" +/** + * OSTREE_SIGN_NAME_SPKI: + * The name of the spki signing type. + * + * Since: 2024.7 + */ +#define OSTREE_SIGN_NAME_SPKI "spki" + _OSTREE_PUBLIC G_DECLARE_INTERFACE (OstreeSign, ostree_sign, OSTREE, SIGN, GObject) @@ -113,4 +122,11 @@ OstreeSign *ostree_sign_get_by_name (const gchar *name, GError **error); _OSTREE_PUBLIC gboolean ostree_sign_summary (OstreeSign *self, OstreeRepo *repo, GVariant *keys, GCancellable *cancellable, GError **error); + +_OSTREE_PUBLIC +OstreeBlobReader *ostree_sign_read_pk (OstreeSign *self, GInputStream *stream); + +_OSTREE_PUBLIC +OstreeBlobReader *ostree_sign_read_sk (OstreeSign *self, GInputStream *stream); + G_END_DECLS diff --git a/src/libotcore/otcore-ed25519-verify.c b/src/libotcore/otcore-ed25519-verify.c index 1c0ec2b83e..24f173b445 100644 --- a/src/libotcore/otcore-ed25519-verify.c +++ b/src/libotcore/otcore-ed25519-verify.c @@ -97,7 +97,7 @@ otcore_validate_ed25519_signature (GBytes *data, GBytes *public_key, GBytes *sig if (!pkey) { EVP_MD_CTX_free (ctx); - return glnx_throw (error, "openssl: Failed to initialize ed5519 key"); + return glnx_throw (error, "openssl: Failed to initialize ed25519 key"); } if (EVP_DigestVerifyInit (ctx, NULL, NULL, NULL, pkey) != 0 && EVP_DigestVerify (ctx, signature_buf, OSTREE_SIGN_ED25519_SIG_SIZE, diff --git a/src/libotcore/otcore-spki-verify.c b/src/libotcore/otcore-spki-verify.c new file mode 100644 index 0000000000..3cf22f659e --- /dev/null +++ b/src/libotcore/otcore-spki-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_spki_init (void) +{ + return true; +} + +/* Validate a single spki 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_spki_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 spki 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, "spki signature validation requested, but support not compiled in"); +#endif +} diff --git a/src/libotcore/otcore.h b/src/libotcore/otcore.h index 6e1d510329..12f32b6e30 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_SPKI_KEY "ostree.sign.spki" +// The variant type +#define OSTREE_SIGN_METADATA_SPKI_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_spki_init (void); +gboolean otcore_validate_spki_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/src/ostree/ot-builtin-commit.c b/src/ostree/ot-builtin-commit.c index 7c6d63e4df..b3d42c0ab6 100644 --- a/src/ostree/ot-builtin-commit.c +++ b/src/ostree/ot-builtin-commit.c @@ -961,19 +961,43 @@ ostree_builtin_commit (int argc, char **argv, OstreeCommandInvocation *invocatio goto out; } - // Load each base64 encoded private key in a file and sign with it. + // Load each encoded private key in a file and sign with it. for (char **iter = opt_key_files; iter && *iter; iter++) { const char *path = *iter; - g_autofree char *b64key - = glnx_file_get_contents_utf8_at (AT_FDCWD, path, NULL, NULL, error); - if (!b64key) + g_autoptr (GFile) keyfile = NULL; + g_autoptr (GFileInputStream) key_stream_in = NULL; + g_autoptr (OstreeBlobReader) blob_reader = NULL; + g_autoptr (GBytes) blob = NULL; + g_autoptr (GError) local_error = NULL; + g_autoptr (GVariant) secret_key = NULL; + + keyfile = g_file_new_for_path (path); + key_stream_in = g_file_read (keyfile, NULL, error); + if (key_stream_in == NULL) + goto out; + + g_assert (sign); + blob_reader = ostree_sign_read_sk (sign, G_INPUT_STREAM (key_stream_in)); + if (blob_reader == NULL) + goto out; + + blob = ostree_blob_reader_read_blob (blob_reader, cancellable, &local_error); + if (local_error != NULL) + { + g_propagate_prefixed_error (error, g_steal_pointer (&local_error), "Reading %s", + path); + goto out; + } + + if (blob == NULL) { g_prefix_error (error, "Reading %s", path); goto out; } - g_autoptr (GVariant) secret_key = g_variant_new_string (b64key); - g_assert (sign); + + // Pass the key as a bytestring + secret_key = g_variant_new_from_bytes (G_VARIANT_TYPE_BYTESTRING, blob, FALSE); if (!ostree_sign_set_sk (sign, secret_key, error)) goto out; diff --git a/src/ostree/ot-builtin-sign.c b/src/ostree/ot-builtin-sign.c index 059df5b41d..5016794d18 100644 --- a/src/ostree/ot-builtin-sign.c +++ b/src/ostree/ot-builtin-sign.c @@ -188,7 +188,7 @@ ostree_builtin_sign (int argc, char **argv, OstreeCommandInvocation *invocation, { g_autoptr (GFile) keyfile = NULL; g_autoptr (GFileInputStream) key_stream_in = NULL; - g_autoptr (GDataInputStream) key_data_in = NULL; + g_autoptr (OstreeBlobReader) blob_reader = NULL; if (!g_file_test (opt_filename, G_FILE_TEST_IS_REGULAR)) { @@ -203,25 +203,24 @@ ostree_builtin_sign (int argc, char **argv, OstreeCommandInvocation *invocation, if (key_stream_in == NULL) goto out; - key_data_in = g_data_input_stream_new (G_INPUT_STREAM (key_stream_in)); - g_assert (key_data_in != NULL); + blob_reader = ostree_sign_read_sk (sign, G_INPUT_STREAM (key_stream_in)); + g_assert (blob_reader != NULL); /* Use simple file format with just a list of base64 public keys per line */ while (TRUE) { - gsize len = 0; - g_autofree char *line - = g_data_input_stream_read_line (key_data_in, &len, NULL, error); + g_autoptr (GBytes) blob + = ostree_blob_reader_read_blob (blob_reader, cancellable, error); g_autoptr (GVariant) sk = NULL; if (*error != NULL) goto out; - if (line == NULL) + if (blob == NULL) break; // Pass the key as a string - sk = g_variant_new_string (line); + sk = g_variant_new_from_bytes (G_VARIANT_TYPE_BYTESTRING, blob, FALSE); if (!ostree_sign_set_sk (sign, sk, error)) { ret = FALSE; diff --git a/tests/libtest.sh b/tests/libtest.sh index 2c2a33f0d9..f8a7dc74e4 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 spki signing tests +SPKIPUBLICPEM= +SPKISECRETPEM= + +SPKIPUBLIC= +SPKISECRET= + +gen_spki_keys () +{ + # Generate private key in PEM format + SPKISECRETPEM="$(mktemp -p ${test_tmpdir} ed448_sk_XXXXXX.pem)" + openssl genpkey -algorithm ed448 -outform PEM -out "${SPKISECRETPEM}" + SPKIPUBLICPEM="$(mktemp -p ${test_tmpdir} ed448_pk_XXXXXX.pem)" + openssl pkey -outform PEM -pubout -in "${SPKISECRETPEM}" -out "${SPKIPUBLICPEM}" + + SPKIPUBLIC="$(openssl pkey -inform PEM -outform DER -pubin -pubout -in ${SPKIPUBLICPEM} | base64 -w 0)" + SPKISECRET="$(openssl pkey -inform PEM -outform DER -in ${SPKISECRETPEM} | base64 -w 0)" + + echo "Generated ed448 keys:" + echo "public: ${SPKIPUBLIC}" + echo "secret: ${SPKISECRET}" +} + +gen_spki_random_public() +{ + openssl genpkey -algorithm ed448 | openssl pkey -pubout -outform DER | base64 -w 0 + echo +} + +gen_spki_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-pem.c b/tests/test-pem.c new file mode 100644 index 0000000000..ed0f82d037 --- /dev/null +++ b/tests/test-pem.c @@ -0,0 +1,124 @@ +/* + * 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 + +#include "ostree-blob-reader-private.h" + +static const guint8 pubkey_ed25519[] + = { 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, 0x36, 0x09, 0x06, + 0x69, 0xf3, 0x52, 0xb1, 0xe3, 0x7e, 0xd4, 0xb5, 0xe3, 0x4c, 0x52, 0x6b, 0x7d, 0xdb, 0xba, + 0x37, 0x6a, 0xac, 0xe6, 0xb9, 0x5f, 0xf5, 0xdd, 0xf1, 0x95, 0xa5, 0x5c, 0x96, 0x09 }; + +static const gchar pem_pubkey_ed25519[] + = "-----BEGIN PUBLIC KEY-----\n" + "MCowBQYDK2VwAyEANgkGafNSseN+1LXjTFJrfdu6N2qs5rlf9d3xlaVclgk=\n" + "-----END PUBLIC KEY-----\n"; + +static const gchar pem_pubkey_ed25519_whitespace[] + = "-----BEGIN PUBLIC KEY-----\n" + " \n" + "MCowBQYDK2VwAyEANgkGafNSseN+1LXjTFJrfdu6N2qs5rlf9d3xlaVclgk=\n" + "-----END PUBLIC KEY-----\n"; + +static const gchar pem_pubkey_empty[] = ""; + +static const gchar pem_pubkey_ed25519_no_trailer[] + = "-----BEGIN PUBLIC KEY-----\n" + "MCowBQYDK2VwAyEANgkGafNSseN+1LXjTFJrfdu6N2qs5rlf9d3xlaVclgk=\n"; + +static const gchar pem_pubkey_ed25519_label_mismatch[] + = "-----BEGIN PUBLIC KEY X-----\n" + "MCowBQYDK2VwAyEANgkGafNSseN+1LXjTFJrfdu6N2qs5rlf9d3xlaVclgk=\n" + "-----END PUBLIC KEY Y-----\n"; + +static void +test_ostree_read_pem_block_valid (void) +{ + static const struct + { + const gchar *pem_data; + gsize pem_size; + const gchar *label; + const guint8 *data; + gsize size; + } tests[] = { + { pem_pubkey_ed25519, sizeof (pem_pubkey_ed25519), "PUBLIC KEY", pubkey_ed25519, + sizeof (pubkey_ed25519) }, + { pem_pubkey_ed25519_whitespace, sizeof (pem_pubkey_ed25519_whitespace), "PUBLIC KEY", + pubkey_ed25519, sizeof (pubkey_ed25519) }, + { pem_pubkey_empty, sizeof (pem_pubkey_empty), NULL, NULL, 0 }, + }; + + for (gsize i = 0; i < G_N_ELEMENTS (tests); i++) + { + g_autoptr (GInputStream) stream + = g_memory_input_stream_new_from_data (tests[i].pem_data, tests[i].pem_size, NULL); + g_autoptr (GDataInputStream) data_stream = g_data_input_stream_new (stream); + + g_autofree char *label = NULL; + g_autoptr (GBytes) bytes = NULL; + g_autoptr (GError) error = NULL; + bytes = _ostree_read_pem_block (data_stream, &label, NULL, &error); + g_assert_no_error (error); + g_assert_cmpstr (label, ==, tests[i].label); + if (tests[i].data) + { + g_autoptr (GBytes) expected_bytes = g_bytes_new_static (tests[i].data, tests[i].size); + g_assert (g_bytes_equal (bytes, expected_bytes)); + } + } +} + +static void +test_ostree_read_pem_block_invalid (void) +{ + static const struct + { + const gchar *pem_data; + gsize pem_size; + } tests[] = { + { pem_pubkey_ed25519_no_trailer, sizeof (pem_pubkey_ed25519_no_trailer) }, + { pem_pubkey_ed25519_label_mismatch, sizeof (pem_pubkey_ed25519_label_mismatch) }, + }; + + for (gsize i = 0; i < G_N_ELEMENTS (tests); i++) + { + g_autoptr (GInputStream) stream + = g_memory_input_stream_new_from_data (tests[i].pem_data, tests[i].pem_size, NULL); + g_autoptr (GDataInputStream) data_stream = g_data_input_stream_new (stream); + + g_autofree char *label = NULL; + g_autoptr (GBytes) bytes = NULL; + g_autoptr (GError) error = NULL; + bytes = _ostree_read_pem_block (data_stream, &label, NULL, &error); + g_assert_null (bytes); + g_assert_null (label); + g_assert_nonnull (error); + } +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/ostree_read_pem_block/valid", test_ostree_read_pem_block_valid); + g_test_add_func ("/ostree_read_pem_block/invalid", test_ostree_read_pem_block_invalid); + return g_test_run (); +} 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 72% rename from tests/test-signed-commit.sh rename to tests/test-signed-commit-ed25519.sh index cf1cd1c852..5408ca957a 100755 --- a/tests/test-signed-commit.sh +++ b/tests/test-signed-commit-ed25519.sh @@ -21,56 +21,14 @@ set -euo pipefail . $(dirname $0)/libtest.sh -echo "1..11" - # 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 +# 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}" -echo "ok Detached dummy signature added" - -# Verify vith sign mechanism -${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} -echo "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} -echo "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' -echo "ok dummy sig requires env" - -# tests below require libsodium support -if ! has_ostree_feature sign-ed25519; then - echo "ok Detached ed25519 signature # SKIP due libsodium unavailability" - echo "ok ed25519 signature verified # SKIP due libsodium unavailability" - echo "ok multiple signing # SKIP due libsodium unavailability" - echo "ok verify ed25519 keys file # SKIP due libsodium unavailability" - echo "ok sign with ed25519 keys file # SKIP due libsodium unavailability" - echo "ok verify ed25519 system-wide configuration # SKIP due libsodium unavailability" - echo "ok verify ed25519 revoking keys mechanism # SKIP due libsodium unavailability" - exit 0 -fi # Test ostree sign with 'ed25519' module gen_ed25519_keys @@ -87,7 +45,7 @@ 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.ed25519 &>/dev/null -echo "ok Detached ed25519 signature added" +tap_ok "Detached ed25519 signature added" # Verify vith sign mechanism if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${WRONG_PUBLIC}; then @@ -99,9 +57,9 @@ ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed255 ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} $(gen_ed25519_random_public) $(gen_ed25519_random_public) ${PUBLIC} ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${PUBLIC} $(gen_ed25519_random_public) $(gen_ed25519_random_public) ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} $(gen_ed25519_random_public) $(gen_ed25519_random_public) ${PUBLIC} $(gen_ed25519_random_public) $(gen_ed25519_random_public) -echo "ok ed25519 signature verified" +tap_ok "ed25519 signature verified" -# Check if we able to use all available modules to sign the same commit +# 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)" @@ -121,7 +79,7 @@ ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed2551 assert_file_has_content out.txt "ed25519: 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" -echo "ok multiple signing " +tap_ok "multiple signing " # Prepare files with public ed25519 signatures PUBKEYS="$(mktemp -p ${test_tmpdir} ed25519_XXXXXX.ed25519)" @@ -163,7 +121,7 @@ ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed2551 echo ${PUBLIC} >> ${PUBKEYS} ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-file=${PUBKEYS} ${COMMIT} -echo "ok verify ed25519 keys file" +tap_ok "verify ed25519 keys file" # Check ed25519 signing with secret file echo "Unsigned commit for secret file usage" >> file.txt @@ -176,7 +134,7 @@ echo "${SECRET}" > ${KEYFILE} ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=ed25519 --keys-file=${KEYFILE} ${COMMIT} # Verify ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-file=${PUBKEYS} ${COMMIT} -echo "ok sign with ed25519 keys file" +tap_ok "sign with ed25519 keys file" # Check the well-known places mechanism mkdir -p ${test_tmpdir}/{trusted,revoked}.ed25519.d @@ -192,7 +150,7 @@ echo ${PUBLIC} > ${test_tmpdir}/trusted.ed25519.d/correct # Verify with correct key ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-dir=${test_tmpdir} ${COMMIT} -echo "ok verify ed25519 system-wide configuration" +tap_ok "verify ed25519 system-wide configuration" # Add the public key into revoked list echo ${PUBLIC} > ${test_tmpdir}/revoked.ed25519.d/correct @@ -201,4 +159,6 @@ if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed2 exit 1 fi rm -rf ${test_tmpdir}/{trusted,revoked}.ed25519.d -echo "ok verify ed25519 revoking keys mechanism" +tap_ok "verify ed25519 revoking keys mechanism" + +tap_end diff --git a/tests/test-signed-commit-spki.sh b/tests/test-signed-commit-spki.sh new file mode 100755 index 0000000000..68b84f18f5 --- /dev/null +++ b/tests/test-signed-commit-spki.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 'spki' module +gen_spki_keys +PUBLIC=${SPKIPUBLIC} +SECRET=${SPKISECRET} + +WRONG_PUBLIC="$(gen_spki_random_public)" + +echo "PUBLIC = $PUBLIC" + +echo "Signed commit with spki: ${SECRET}" >> file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s "Signed with spki module" --sign="${SECRET}" --sign-type=spki +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.spki &>/dev/null +tap_ok "Detached spki signature added" + +# Verify vith sign mechanism +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki ${COMMIT} ${WRONG_PUBLIC}; then + exit 1 +fi +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki ${COMMIT} ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki ${COMMIT} ${PUBLIC} ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki ${COMMIT} $(gen_spki_random_public) ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki ${COMMIT} $(gen_spki_random_public) $(gen_spki_random_public) ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki ${COMMIT} ${PUBLIC} $(gen_spki_random_public) $(gen_spki_random_public) +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki ${COMMIT} $(gen_spki_random_public) $(gen_spki_random_public) ${PUBLIC} $(gen_spki_random_public) $(gen_spki_random_public) +tap_ok "spki 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" "spki"; 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=spki ${COMMIT} ${SECRET} +# and verify +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki ${COMMIT} ${PUBLIC} >out.txt +assert_file_has_content out.txt "spki: 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 spki signatures +PUBKEYS="$(mktemp -p ${test_tmpdir} spki_XXXXXX.spki)" + +# Test if file contain no keys +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki --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=spki --keys-file=${test_tmpdir} ${COMMIT}; then + exit 1 +fi + +# Test with single key in list +cat ${SPKIPUBLICPEM} > ${PUBKEYS} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki --keys-file=${PUBKEYS} ${COMMIT} >out.txt +assert_file_has_content out.txt 'spki: 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_spki_random_public_pem +done > ${PUBKEYS} +# Check if file contain no valid signatures +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki --keys-file=${PUBKEYS} ${COMMIT} 2>err.txt; then + fatal "validated with no signatures" +fi +assert_file_has_content err.txt 'error:.* spki: 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=spki --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=spki --keys-file=${PUBKEYS} ${COMMIT} ${PUBLIC} + +# Add correct key into the list +cat "${SPKIPUBLICPEM}" >> ${PUBKEYS} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki --keys-file=${PUBKEYS} ${COMMIT} + +tap_ok "verify spki keys file" + +# Check spki 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.spki)" +cat "${SPKISECRETPEM}" > ${KEYFILE} +# Sign +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=spki --keys-file=${KEYFILE} ${COMMIT} +# Verify +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki --keys-file=${PUBKEYS} ${COMMIT} +tap_ok "sign with spki keys file" + +# Check the well-known places mechanism +mkdir -p ${test_tmpdir}/{trusted,revoked}.spki.d +for((i=0;i<100;i++)); do + # Generate some key files with random public signatures + gen_spki_random_public_pem > ${test_tmpdir}/trusted.spki.d/signature_$i +done +# Check no valid public keys are available +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki --keys-dir=${test_tmpdir} ${COMMIT}; then + exit 1 +fi +cat "${SPKIPUBLICPEM}" > ${test_tmpdir}/trusted.spki.d/correct +# Verify with correct key +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki --keys-dir=${test_tmpdir} ${COMMIT} + +tap_ok "verify spki system-wide configuration" + +# Add the public key into revoked list +cat "${SPKIPUBLICPEM}" > ${test_tmpdir}/revoked.spki.d/correct +# Check if public key is not valid anymore +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=spki --keys-dir=${test_tmpdir} ${COMMIT}; then + exit 1 +fi +rm -rf ${test_tmpdir}/{trusted,revoked}.spki.d +tap_ok "verify spki revoking keys mechanism" + +tap_end