core: Use libgpgme to add GPG signatures to detached metadata for commit object

Add an optional dependency on gpgme to add GPG signatures into the
detached metadata, with the key "ostree.gpgsigs", as an "aay", an
array of signatures (treated as binary data).

The commit command gains a --gpg-sign=<key-id> argument.  Also add an
argument --gpg-homedir to set the GPG homedir where we look for
keyrings.
This commit is contained in:
Jeremy Whiting 2013-09-02 19:43:49 -06:00 committed by Colin Walters
parent 0f486105db
commit 7d5aa74dae
13 changed files with 326 additions and 0 deletions

View File

@ -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

View File

@ -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; \

View File

@ -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"

View File

@ -24,6 +24,7 @@
#include <glib-unix.h>
#include <gio/gunixinputstream.h>
#include <gio/gfiledescriptorbased.h>
#include "otutil.h"
#include "libgsystem.h"
@ -31,6 +32,12 @@
#include "ostree-repo-private.h"
#include "ostree-repo-file.h"
#ifdef HAVE_GPGME
#include <locale.h>
#include <gpgme.h>
#include <glib/gstdio.h>
#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

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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))

BIN
tests/gpghome/pubring.gpg Normal file

Binary file not shown.

BIN
tests/gpghome/secring.gpg Normal file

Binary file not shown.

BIN
tests/gpghome/trustdb.gpg Normal file

Binary file not shown.

View File

@ -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

View File

@ -0,0 +1,41 @@
#!/bin/bash
#
# Copyright (C) 2013 Jeremy Whiting <jeremy.whiting@collabora.com>
#
# 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 '