From 334e0b0d2e1fd9bb4359e7a565f4e0a6d67856be Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Fri, 12 Jul 2024 15:29:58 +0900 Subject: [PATCH 1/6] sign: Fix typo in error messages and comments Signed-off-by: Daiki Ueno --- man/ostree-sign.xml | 2 +- src/libostree/ostree-sign-ed25519.c | 4 ++-- src/libotcore/otcore-ed25519-verify.c | 2 +- tests/test-signed-commit.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/man/ostree-sign.xml b/man/ostree-sign.xml index eb63f7ff..508a6ca2 100644 --- a/man/ostree-sign.xml +++ b/man/ostree-sign.xml @@ -81,7 +81,7 @@ License along with this library. If not, see . /etc/ostree/trusted.ed25519.d /etc/ostree/revoked.ed25519.d /usr/share/ostree/trusted.ed25519.d - /usr/share/ostree/rvokeded.ed25519.d + /usr/share/ostree/revoked.ed25519.d diff --git a/src/libostree/ostree-sign-ed25519.c b/src/libostree/ostree-sign-ed25519.c index 3dcff2eb..e0e3ac46 100644 --- a/src/libostree/ostree-sign-ed25519.c +++ b/src/libostree/ostree-sign-ed25519.c @@ -103,7 +103,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 +152,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; diff --git a/src/libotcore/otcore-ed25519-verify.c b/src/libotcore/otcore-ed25519-verify.c index 1c0ec2b8..24f173b4 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/tests/test-signed-commit.sh b/tests/test-signed-commit.sh index cf1cd1c8..73218528 100755 --- a/tests/test-signed-commit.sh +++ b/tests/test-signed-commit.sh @@ -101,7 +101,7 @@ ${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} $(gen_ed25519_random_public) $(gen_ed25519_random_public) echo "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)" From 62e54529e436f70ecb2aabde831ba9c172fa79a2 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Sat, 13 Jul 2024 10:05:00 +0900 Subject: [PATCH 2/6] sign: Use explicit_bzero to clear secret key material Suggested in: https://github.com/ostreedev/ostree/pull/3278#discussion_r1675696052 Signed-off-by: Daiki Ueno --- src/libostree/ostree-sign-ed25519.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libostree/ostree-sign-ed25519.c b/src/libostree/ostree-sign-ed25519.c index e0e3ac46..e3b5b7a3 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" @@ -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; } From ef44e33def4dd8dd9d13f6331b0eeab37960f535 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Sun, 11 Aug 2024 15:08:12 +0900 Subject: [PATCH 3/6] sign: Factor out logic to read key blobs This defines a new interface OstreeBlobReader, which encapsulates the key file parsing logic. This would make it easy to support custom file formats such as PEM. Signed-off-by: Daiki Ueno --- Makefile-libostree-defines.am | 1 + Makefile-libostree.am | 12 +++- apidoc/ostree-sections.txt | 9 +++ src/libostree/libostree-devel.sym | 8 +++ src/libostree/ostree-blob-reader-base64.c | 80 +++++++++++++++++++++++ src/libostree/ostree-blob-reader-base64.h | 39 +++++++++++ src/libostree/ostree-blob-reader-raw.c | 74 +++++++++++++++++++++ src/libostree/ostree-blob-reader-raw.h | 39 +++++++++++ src/libostree/ostree-blob-reader.c | 37 +++++++++++ src/libostree/ostree-blob-reader.h | 42 ++++++++++++ src/libostree/ostree-sign-ed25519.c | 46 ++++++++----- src/libostree/ostree-sign.c | 48 ++++++++++++++ src/libostree/ostree-sign.h | 8 +++ src/ostree/ot-builtin-commit.c | 36 ++++++++-- src/ostree/ot-builtin-sign.c | 15 ++--- 15 files changed, 459 insertions(+), 35 deletions(-) create mode 100644 src/libostree/ostree-blob-reader-base64.c create mode 100644 src/libostree/ostree-blob-reader-base64.h create mode 100644 src/libostree/ostree-blob-reader-raw.c create mode 100644 src/libostree/ostree-blob-reader-raw.h create mode 100644 src/libostree/ostree-blob-reader.c create mode 100644 src/libostree/ostree-blob-reader.h diff --git a/Makefile-libostree-defines.am b/Makefile-libostree-defines.am index b01ec2a1..dc56e706 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 11a7bbed..27c20b5d 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= @@ -262,6 +262,12 @@ libostree_1_la_SOURCES += \ src/libostree/ostree-sign-ed25519.c \ src/libostree/ostree-sign-ed25519.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 \ $(NULL) if USE_COMPOSEFS diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt index b46e606c..c515d4c4 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/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym index 6640e11c..9e35a6ee 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 00000000..6faa4820 --- /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 00000000..b1e78ba2 --- /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-raw.c b/src/libostree/ostree-blob-reader-raw.c new file mode 100644 index 00000000..85355254 --- /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 00000000..614e26ba --- /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 00000000..4f1314b8 --- /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 00000000..caf8fb24 --- /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 e3b5b7a3..b7718880 100644 --- a/src/libostree/ostree-sign-ed25519.c +++ b/src/libostree/ostree-sign-ed25519.c @@ -452,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"); @@ -478,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) { @@ -501,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) @@ -530,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)) { @@ -543,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.c b/src/libostree/ostree-sign.c index d76271bc..ad2358de 100644 --- a/src/libostree/ostree-sign.c +++ b/src/libostree/ostree-sign.c @@ -38,6 +38,8 @@ #include #include "ostree-autocleanups.h" +#include "ostree-blob-reader-base64.h" +#include "ostree-blob-reader-raw.h" #include "ostree-core.h" #include "ostree-sign-dummy.h" #include "ostree-sign-ed25519.h" @@ -641,3 +643,49 @@ 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 (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 (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 9424b258..81495c39 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" @@ -113,4 +114,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/ostree/ot-builtin-commit.c b/src/ostree/ot-builtin-commit.c index 7c6d63e4..b3d42c0a 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 059df5b4..5016794d 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; From 0fa10617790936c2ab8e84112d944da82c313a04 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Fri, 9 Aug 2024 14:54:27 +0900 Subject: [PATCH 4/6] sign: Add PEM reading facility This adds a new class OstreePemReader, which reads PEM blocks from an input stream. This would be useful for the "x509" signing backend, as the keys are typically stored in the PEM format. Signed-off-by: Daiki Ueno --- Makefile-libostree.am | 3 + Makefile-tests.am | 8 +- src/libostree/ostree-blob-reader-pem.c | 225 +++++++++++++++++++++ src/libostree/ostree-blob-reader-pem.h | 39 ++++ src/libostree/ostree-blob-reader-private.h | 33 +++ tests/test-pem.c | 124 ++++++++++++ 6 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 src/libostree/ostree-blob-reader-pem.c create mode 100644 src/libostree/ostree-blob-reader-pem.h create mode 100644 src/libostree/ostree-blob-reader-private.h create mode 100644 tests/test-pem.c diff --git a/Makefile-libostree.am b/Makefile-libostree.am index 27c20b5d..701653ed 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -268,6 +268,9 @@ libostree_1_la_SOURCES += \ 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-tests.am b/Makefile-tests.am index abf61af2..415637a5 100644 --- a/Makefile-tests.am +++ b/Makefile-tests.am @@ -270,7 +270,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 +403,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/src/libostree/ostree-blob-reader-pem.c b/src/libostree/ostree-blob-reader-pem.c new file mode 100644 index 00000000..85c544f5 --- /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 00000000..04efb99d --- /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 00000000..52b589be --- /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/tests/test-pem.c b/tests/test-pem.c new file mode 100644 index 00000000..ed0f82d0 --- /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 (); +} From 5583563b8f5232d86bace72bc0a8fb34b7ed1e78 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Wed, 11 Sep 2024 16:04:34 +0900 Subject: [PATCH 5/6] tests: Use tap_ok/tap_end in test-signed-commit.sh Signed-off-by: Daiki Ueno --- tests/test-signed-commit.sh | 40 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/test-signed-commit.sh b/tests/test-signed-commit.sh index 73218528..3fac605e 100755 --- a/tests/test-signed-commit.sh +++ b/tests/test-signed-commit.sh @@ -21,8 +21,6 @@ set -euo pipefail . $(dirname $0)/libtest.sh -echo "1..11" - # This is explicitly opt in for testing export OSTREE_DUMMY_SIGN_ENABLED=1 @@ -40,17 +38,17 @@ ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy ${COMMIT} # 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" +tap_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" +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} -echo "ok commit with dummy signing" +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" @@ -58,17 +56,17 @@ 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" +tap_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" + 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 @@ -87,7 +85,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,7 +97,7 @@ ${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 are able to use all available modules to sign the same commit echo "Unsigned commit for multi-sign" >> file.txt @@ -121,7 +119,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 +161,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 +174,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 +190,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 +199,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 From e83eda0aecd296d67c8b2a05e214955b85804db2 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Fri, 12 Jul 2024 12:54:13 +0900 Subject: [PATCH 6/6] sign: Support spki 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 "spki" which uses the X.509 SubjectPublicKeyInfo format for public keys. Keys in this format can easily be created with openssl tools and provide crypto agility as the format embeds algorithm identifier. Currently, the corresponding private keys shall be in the PKCS#8 format, while future extensions may support other format such as opaque key handles on a hardware token. The "spki" 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-spki.c | 636 ++++++++++++++++++ src/libostree/ostree-sign-spki.h | 54 ++ src/libostree/ostree-sign.c | 20 + src/libostree/ostree-sign.h | 8 + src/libotcore/otcore-spki-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-spki.sh | 164 +++++ 17 files changed, 1128 insertions(+), 61 deletions(-) create mode 100644 src/libostree/ostree-sign-spki.c create mode 100644 src/libostree/ostree-sign-spki.h create mode 100644 src/libotcore/otcore-spki-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-spki.sh diff --git a/Makefile-libostree.am b/Makefile-libostree.am index 701653ed..e6c1e553 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-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 \ diff --git a/Makefile-otcore.am b/Makefile-otcore.am index 8252ead0..ec345785 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 415637a5..46ca8480 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 \ diff --git a/configure.ac b/configure.ac index 538ff345..c2fdbf67 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 12f4fd10..013ec3aa 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 508a6ca2..bae07473 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/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 `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 7b3a9d7c..bf6a2e0a 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/ostree-sign-spki.c b/src/libostree/ostree-sign-spki.c new file mode 100644 index 00000000..5ad81da3 --- /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 00000000..089bd272 --- /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 ad2358de..2d9f2ba6 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-spki.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_SPKI) + { OSTREE_SIGN_NAME_SPKI, 0 }, #endif { "dummy", 0 } }; @@ -69,6 +74,9 @@ enum { #if defined(HAVE_ED25519) SIGN_ED25519, +#endif +#if defined(HAVE_SPKI) + SIGN_SPKI, #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_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; @@ -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_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)); @@ -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_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)); diff --git a/src/libostree/ostree-sign.h b/src/libostree/ostree-sign.h index 81495c39..a817f32a 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_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) diff --git a/src/libotcore/otcore-spki-verify.c b/src/libotcore/otcore-spki-verify.c new file mode 100644 index 00000000..3cf22f65 --- /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 6e1d5103..12f32b6e 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/tests/libtest.sh b/tests/libtest.sh index 2c2a33f0..f8a7dc74 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-signed-commit-dummy.sh b/tests/test-signed-commit-dummy.sh new file mode 100755 index 00000000..305af38b --- /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 3fac605e..5408ca95 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-spki.sh b/tests/test-signed-commit-spki.sh new file mode 100755 index 00000000..68b84f18 --- /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