diff --git a/Makefile-libostree.am b/Makefile-libostree.am index 7925a26d..badbb92d 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -99,3 +99,8 @@ CLEANFILES += $(gir_DATA) $(typelib_DATA) endif pkgconfig_DATA += src/libostree/ostree-1.pc + +if USE_GPGME +libostree_1_la_LIBADD += $(GPGME_LIBS) +endif + diff --git a/Makefile-tests.am b/Makefile-tests.am index 09ca24d0..d6b3e448 100644 --- a/Makefile-tests.am +++ b/Makefile-tests.am @@ -28,6 +28,7 @@ testfiles = test-basic \ test-pull-archive-z \ test-pull-corruption \ test-pull-resume \ + test-gpg-signed-commit \ test-admin-deploy-1 \ test-admin-deploy-2 \ test-admin-deploy-uboot \ @@ -41,6 +42,11 @@ insttest_DATA = tests/archive-test.sh \ tests/libtest.sh \ $(NULL) +gpginsttestdir = $(pkglibexecdir)/installed-tests/gpghome +gpginsttest_DATA = tests/gpghome/secring.gpg \ + tests/gpghome/pubring.gpg \ + tests/gpghome/trustdb.gpg + %.test: tests/%.sh Makefile $(AM_V_GEN) (echo '[Test]' > $@.tmp; \ echo 'Exec=$(pkglibexecdir)/installed-tests/$(notdir $<)' >> $@.tmp; \ diff --git a/configure.ac b/configure.ac index d2898761..20bc7d9a 100644 --- a/configure.ac +++ b/configure.ac @@ -81,6 +81,31 @@ m4_ifdef([GOBJECT_INTROSPECTION_CHECK], [ ]) AM_CONDITIONAL(BUILDOPT_INTROSPECTION, test x$found_introspection = xyes) +LIBGPGME_DEPENDENCY="1.1.8" + +AC_ARG_WITH(gpgme, + AS_HELP_STRING([--without-gpgme], [Do not use gpgme]), + :, with_gpgme=maybe) + +AS_IF([ test x$with_gpgme != xno ], [ + AC_MSG_CHECKING([for $LIBGPGME_DEPENDENCY]) + m4_ifdef([AM_PATH_GPGME], [ + AM_PATH_GPGME($LIBGPGME_DEPENDENCY, have_gpgme=yes, have_gpgme=no) + ],[ + AM_CONDITIONAL([have_gpgme],[false]) + ]) + AC_MSG_RESULT([$have_gpgme]) + AS_IF([ test x$have_gpgme = xno && test x$with_gpgme != xmaybe ], [ + AC_MSG_ERROR([gpgme is enabled but could not be found]) + ]) + AS_IF([ test x$have_gpgme = xyes], [ + AC_DEFINE(HAVE_GPGME, 1, [Define if we have gpgme]) + with_gpgme=yes + ], [ with_gpgme=no ]) +], [ with_gpgme=no ]) +if test x$with_gpgme != xno; then OSTREE_FEATURES="$OSTREE_FEATURES +gpgme"; fi +AM_CONDITIONAL(USE_GPGME, test $with_gpgme != no) + LIBARCHIVE_DEPENDENCY="libarchive >= 2.8.0" GTK_DOC_CHECK([1.15], [--flavour no-tmpl]) @@ -154,6 +179,7 @@ echo " introspection: $found_introspection libsoup (retrieve remote HTTP repositories): $with_soup libarchive (parse tar files directly): $with_libarchive + gpgme (sign commits): $with_gpgme documentation: $enable_gtk_doc gjs-based tests: $have_gjs dracut: $with_dracut" diff --git a/src/libostree/ostree-repo.c b/src/libostree/ostree-repo.c index 28ec7ff6..189740f0 100644 --- a/src/libostree/ostree-repo.c +++ b/src/libostree/ostree-repo.c @@ -24,6 +24,7 @@ #include #include +#include #include "otutil.h" #include "libgsystem.h" @@ -31,6 +32,12 @@ #include "ostree-repo-private.h" #include "ostree-repo-file.h" +#ifdef HAVE_GPGME +#include +#include +#include +#endif + /** * SECTION:libostree-repo * @title: Content-addressed object store @@ -1463,3 +1470,180 @@ ostree_repo_pull (OstreeRepo *self, return FALSE; } #endif + +#ifdef HAVE_GPGME +gboolean +ostree_repo_sign_commit (OstreeRepo *self, + const gchar *commit_checksum, + const gchar *key_id, + const gchar *homedir, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + gs_unref_object GFile *commit_path = NULL; + gs_unref_variant GVariant *metadata = NULL; + gs_free gchar *commit_filename = NULL; + gs_unref_object GFile *tmp_signature_file = NULL; + gs_unref_object GOutputStream *tmp_signature_output = NULL; + gs_unref_variant_builder GVariantBuilder *builder = NULL; + gs_unref_variant_builder GVariantBuilder *signature_builder = NULL; + gs_unref_variant GVariant *commit_variant = NULL; + gs_unref_variant GVariant *signaturedata = NULL; + gs_unref_bytes GBytes *signature_bytes = NULL; + gpgme_ctx_t context; + gpgme_engine_info_t info; + gpgme_error_t err; + gpgme_key_t key = NULL; + gpgme_data_t commit_buffer = NULL; + gpgme_data_t signature_buffer = NULL; + int signature_fd = -1; + gpgme_sign_result_t result; + GMappedFile *signature_file = NULL; + + if (!ostree_repo_load_variant (self, OSTREE_OBJECT_TYPE_COMMIT, + commit_checksum, &commit_variant, error)) + goto out; + + if (!ostree_repo_read_commit_detached_metadata (self, + commit_checksum, + &metadata, + cancellable, + error)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to read existing detached metadata"); + goto out; + } + + if (!gs_file_open_in_tmpdir (self->tmp_dir, 0644, + &tmp_signature_file, &tmp_signature_output, + cancellable, error)) + goto out; + + gpgme_check_version (NULL); + gpgme_set_locale (NULL, LC_CTYPE, setlocale (LC_CTYPE, NULL)); + + if ((err = gpgme_new (&context)) != GPG_ERR_NO_ERROR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to create gpg context"); + goto out; + } + + info = gpgme_ctx_get_engine_info (context); + + if (homedir != NULL) + { + if ((err = gpgme_ctx_set_engine_info (context, info->protocol, info->file_name, homedir)) + != GPG_ERR_NO_ERROR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to set gpg homedir"); + goto out; + } + } + + /* Get the secret keys with the given key id */ + if ((err = gpgme_get_key (context, key_id, &key, 1)) != GPG_ERR_NO_ERROR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "No gpg key found with the given key-id"); + goto out; + } + + /* Add the key to the context as a signer */ + if ((err = gpgme_signers_add (context, key)) != GPG_ERR_NO_ERROR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Error signing commit"); + goto out; + } + + if ((err = gpgme_data_new_from_mem (&commit_buffer, g_variant_get_data (commit_variant), + g_variant_get_size (commit_variant), FALSE)) != GPG_ERR_NO_ERROR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create buffer from commit file"); + goto out; + } + + signature_fd = g_file_descriptor_based_get_fd ((GFileDescriptorBased*)tmp_signature_output); + if (signature_fd < 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to open signature file"); + goto out; + } + + if ((err = gpgme_data_new_from_fd (&signature_buffer, signature_fd)) != GPG_ERR_NO_ERROR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create buffer for signature file"); + goto out; + } + + if ((err = gpgme_op_sign (context, commit_buffer, signature_buffer, GPGME_SIG_MODE_DETACH)) + != GPG_ERR_NO_ERROR) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failure signing commit file"); + goto out; + } + + result = gpgme_op_sign_result (context); + + if (!g_output_stream_close (tmp_signature_output, cancellable, error)) + goto out; + + signature_file = gs_file_map_noatime (tmp_signature_file, cancellable, error); + if (!signature_file) + goto out; + signature_bytes = g_mapped_file_get_bytes (signature_file); + + // Now read the file and put its contents into the result GVariant + if (metadata) + { + builder = ot_util_variant_builder_from_variant (metadata, G_VARIANT_TYPE ("a{sv}")); + signaturedata = g_variant_lookup_value (metadata, "ostree.gpgsigs", G_VARIANT_TYPE ("aay")); + if (signaturedata) + signature_builder = ot_util_variant_builder_from_variant (signaturedata, G_VARIANT_TYPE ("aay")); + } + if (!builder) + builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + if (!signature_builder) + signature_builder = g_variant_builder_new (G_VARIANT_TYPE ("aay")); + + g_variant_builder_add (signature_builder, "@ay", ot_gvariant_new_ay_bytes (signature_bytes)); + + g_variant_builder_add (builder, "{sv}", "ostree.gpgsigs", g_variant_builder_end (signature_builder)); + + metadata = g_variant_builder_end (builder); + + if (!ostree_repo_write_commit_detached_metadata (self, + commit_checksum, + metadata, + cancellable, + error)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to read existing detached metadata"); + goto out; + } + + ret = TRUE; +out: + if (commit_buffer) + gpgme_data_release (commit_buffer); + if (signature_buffer) + gpgme_data_release (signature_buffer); + if (key) + gpgme_key_release (key); + if (context) + gpgme_release (context); + if (signature_file) + g_mapped_file_unref (signature_file); + return ret; +} + +#endif diff --git a/src/libostree/ostree-repo.h b/src/libostree/ostree-repo.h index d55874f8..4f40b9f9 100644 --- a/src/libostree/ostree-repo.h +++ b/src/libostree/ostree-repo.h @@ -22,6 +22,7 @@ #pragma once +#include "config.h" #include "ostree-core.h" #include "ostree-types.h" @@ -462,5 +463,14 @@ gboolean ostree_repo_pull (OstreeRepo *self, GCancellable *cancellable, GError **error); +#ifdef HAVE_GPGME +gboolean ostree_repo_sign_commit (OstreeRepo *self, + const gchar *commit_checksum, + const gchar *key_id, + const gchar *homedir, + GCancellable *cancellable, + GError **error); +#endif + G_END_DECLS diff --git a/src/libotutil/ot-variant-utils.c b/src/libotutil/ot-variant-utils.c index 417975f6..291f7466 100644 --- a/src/libotutil/ot-variant-utils.c +++ b/src/libotutil/ot-variant-utils.c @@ -199,3 +199,23 @@ ot_variant_read (GVariant *variant) return (GInputStream*)ret; } +GVariantBuilder * +ot_util_variant_builder_from_variant (GVariant *variant, + const GVariantType *type) +{ + GVariantBuilder *builder = NULL; + gint i, n; + + builder = g_variant_builder_new (type); + + n = g_variant_n_children (variant); + for (i = 0; i < n; i++) + { + GVariant *child = g_variant_get_child_value (variant, i); + g_variant_builder_add_value (builder, child); + g_variant_unref (child); + } + + return builder; +} + diff --git a/src/libotutil/ot-variant-utils.h b/src/libotutil/ot-variant-utils.h index 83a3f540..92746a2d 100644 --- a/src/libotutil/ot-variant-utils.h +++ b/src/libotutil/ot-variant-utils.h @@ -55,5 +55,8 @@ gboolean ot_util_variant_from_stream (GInputStream *src, GInputStream *ot_variant_read (GVariant *variant); +GVariantBuilder *ot_util_variant_builder_from_variant (GVariant *variant, + const GVariantType *type); + G_END_DECLS diff --git a/src/ostree/ot-builtin-commit.c b/src/ostree/ot-builtin-commit.c index 2cbe22f1..e9c030d7 100644 --- a/src/ostree/ot-builtin-commit.c +++ b/src/ostree/ot-builtin-commit.c @@ -41,6 +41,10 @@ static char **opt_trees; static gint opt_owner_uid = -1; static gint opt_owner_gid = -1; static gboolean opt_table_output; +#ifdef HAVE_GPGME +static char **opt_key_ids; +static char *opt_gpg_homedir; +#endif static GOptionEntry options[] = { { "subject", 's', 0, G_OPTION_ARG_STRING, &opt_subject, "One line subject", "subject" }, @@ -57,6 +61,10 @@ static GOptionEntry options[] = { { "skip-if-unchanged", 0, 0, G_OPTION_ARG_NONE, &opt_skip_if_unchanged, "If the contents are unchanged from previous commit, do nothing", NULL }, { "statoverride", 0, 0, G_OPTION_ARG_FILENAME, &opt_statoverride_file, "File containing list of modifications to make to permissions", "path" }, { "table-output", 0, 0, G_OPTION_ARG_NONE, &opt_table_output, "Output more information in a KEY: VALUE format", NULL }, +#ifdef HAVE_GPGME + { "gpg-sign", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_key_ids, "GPG Key ID to sign the commit with", "key-id"}, + { "gpg-homedir", 0, 0, G_OPTION_ARG_STRING, &opt_gpg_homedir, "GPG Homedir to use when looking for keyrings", "homedir"}, +#endif { NULL } }; @@ -462,6 +470,26 @@ ostree_builtin_commit (int argc, char **argv, OstreeRepo *repo, GCancellable *ca goto out; } +#ifdef HAVE_GPGME + if (opt_key_ids) + { + char **iter; + + for (iter = opt_key_ids; iter && *iter; iter++) + { + const char *keyid = *iter; + + if (!ostree_repo_sign_commit (repo, + commit_checksum, + keyid, + opt_gpg_homedir, + cancellable, + error)) + goto out; + } + } +#endif + ostree_repo_transaction_set_ref (repo, NULL, opt_branch, commit_checksum); if (!ostree_repo_commit_transaction (repo, &stats, cancellable, error)) diff --git a/tests/gpghome/pubring.gpg b/tests/gpghome/pubring.gpg new file mode 100644 index 00000000..502a1a37 Binary files /dev/null and b/tests/gpghome/pubring.gpg differ diff --git a/tests/gpghome/secring.gpg b/tests/gpghome/secring.gpg new file mode 100644 index 00000000..635e20c5 Binary files /dev/null and b/tests/gpghome/secring.gpg differ diff --git a/tests/gpghome/trustdb.gpg b/tests/gpghome/trustdb.gpg new file mode 100644 index 00000000..aeb46cbd Binary files /dev/null and b/tests/gpghome/trustdb.gpg differ diff --git a/tests/libtest.sh b/tests/libtest.sh index c421b452..84fd88f5 100644 --- a/tests/libtest.sh +++ b/tests/libtest.sh @@ -22,6 +22,9 @@ test_tmpdir=$(pwd) export G_DEBUG=fatal-warnings +export TEST_GPG_KEYID="472CDAFA" +export TEST_GPG_HOME=${SRCDIR}/gpghome + if test -n "${OT_TESTS_DEBUG}"; then set -x fi diff --git a/tests/test-gpg-signed-commit.sh b/tests/test-gpg-signed-commit.sh new file mode 100644 index 00000000..1166f866 --- /dev/null +++ b/tests/test-gpg-signed-commit.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# +# Copyright (C) 2013 Jeremy Whiting +# +# 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, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +set -e + +if ! ostree --version | grep -q -e '\+gpgme'; then + exit 77 +fi + +. $(dirname $0)/libtest.sh + +setup_test_repository "archive-z2" + +cd ${test_tmpdir} +${OSTREE} commit -b test2 -s "A GPG signed commit" -m "Signed commit body" --gpg-sign=${TEST_GPG_KEYID} --gpg-homedir=${TEST_GPG_HOME} --tree=dir=files +$OSTREE show --print-detached-metadata-key=ostree.gpgsigs test2 > test2-gpgsigs +# We at least got some content here and ran through the code; later +# tests will actually do verification +assert_file_has_content test2-gpgsigs 'byte ' + +# Now sign a commit 3 times (with the same key) +cd ${test_tmpdir} +${OSTREE} commit -b test2 -s "A GPG signed commit" -m "Signed commit body" --gpg-sign=${TEST_GPG_KEYID} --gpg-sign=${TEST_GPG_KEYID} --gpg-sign=${TEST_GPG_KEYID} --gpg-homedir=${TEST_GPG_HOME} --tree=dir=files +$OSTREE show --print-detached-metadata-key=ostree.gpgsigs test2 > test2-gpgsigs +assert_file_has_content test2-gpgsigs 'byte '