diff --git a/Makefile-ostree.am b/Makefile-ostree.am
index 7b53cb1489..0eaed26edc 100644
--- a/Makefile-ostree.am
+++ b/Makefile-ostree.am
@@ -109,6 +109,8 @@ ostree_SOURCES += \
if USE_GPGME
ostree_SOURCES += \
src/ostree/ot-remote-builtin-gpg-import.c \
+ src/ostree/ot-remote-builtin-list-gpg-keys.c \
+ src/ostree/ot-remote-builtin-update-gpg-keys.c \
$(NULL)
endif
diff --git a/Makefile-otutil.am b/Makefile-otutil.am
index e8901b57da..7bc87b6a4f 100644
--- a/Makefile-otutil.am
+++ b/Makefile-otutil.am
@@ -49,6 +49,8 @@ if USE_GPGME
libotutil_la_SOURCES += \
src/libotutil/ot-gpg-utils.c \
src/libotutil/ot-gpg-utils.h \
+ src/libotutil/zbase32.c \
+ src/libotutil/zbase32.h \
$(NULL)
endif
diff --git a/Makefile-tests.am b/Makefile-tests.am
index f5a6527811..401c59690d 100644
--- a/Makefile-tests.am
+++ b/Makefile-tests.am
@@ -140,6 +140,7 @@ _installed_or_uninstalled_test_scripts = \
if USE_GPGME
_installed_or_uninstalled_test_scripts += \
tests/test-remote-gpg-import.sh \
+ tests/test-remote-update-gpg-keys.sh \
tests/test-gpg-signed-commit.sh \
tests/test-admin-gpg.sh \
$(NULL)
diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt
index 252a563acb..398292d0cb 100644
--- a/apidoc/ostree-sections.txt
+++ b/apidoc/ostree-sections.txt
@@ -312,6 +312,8 @@ ostree_repo_remote_get_url
ostree_repo_remote_get_gpg_verify
ostree_repo_remote_get_gpg_verify_summary
ostree_repo_remote_gpg_import
+ostree_repo_remote_get_gpg_keys
+ostree_repo_remote_update_gpg_keys
ostree_repo_remote_fetch_summary
ostree_repo_remote_fetch_summary_with_options
ostree_repo_reload_config
diff --git a/bash/ostree b/bash/ostree
index fc42998376..8b4f9b3083 100644
--- a/bash/ostree
+++ b/bash/ostree
@@ -1232,6 +1232,40 @@ _ostree_remote_list_cookies() {
return 0
}
+_ostree_remote_list_gpg_keys() {
+ local boolean_options="
+ $main_boolean_options
+ "
+
+ local options_with_args="
+ --repo
+ "
+
+ local options_with_args_glob=$( __ostree_to_extglob "$options_with_args" )
+
+ case "$prev" in
+ --repo)
+ __ostree_compreply_dirs_only
+ return 0
+ ;;
+ esac
+
+ case "$cur" in
+ -*)
+ local all_options="$boolean_options $options_with_args"
+ __ostree_compreply_all_options
+ ;;
+ *)
+ local argpos=$( __ostree_pos_first_nonflag $( __ostree_to_alternatives "$options_with_args" ) )
+
+ if [ $cword -eq $argpos ]; then
+ __ostree_compreply_remotes
+ fi
+ esac
+
+ return 0
+}
+
_ostree_remote_refs() {
local boolean_options="
$main_boolean_options
@@ -1346,9 +1380,11 @@ _ostree_remote() {
gpg-import
list
list-cookies
+ list-gpg-keys
refs
show-url
summary
+ update-gpg-keys
"
__ostree_subcommands "$subcommands" && return 0
diff --git a/man/ostree-remote.xml b/man/ostree-remote.xml
index 407f7e3d2c..8d348a8230 100644
--- a/man/ostree-remote.xml
+++ b/man/ostree-remote.xml
@@ -65,6 +65,12 @@ Boston, MA 02111-1307, USA.
ostree remote gpg-import OPTIONS NAME KEY-ID
+
+ ostree remote list-gpg-keys NAME
+
+
+ ostree remote update-gpg-keys NAME
+
ostree remote refs NAME
@@ -106,11 +112,21 @@ Boston, MA 02111-1307, USA.
for more information.
- The gpg-import subcommand can associate GPG keys to a specific remote repository for use when pulling signed commits from that repository (if GPG verification is enabled).
+ The gpg-import subcommand can associate GPG
+ keys to a specific remote repository for use when pulling signed
+ commits from that repository (if GPG verification is enabled). The
+ list-gpg-keys subcommand can be used to see the
+ GPG keys currently associated with a remote repository.
The GPG keys to import may be in binary OpenPGP format or ASCII armored. The optional KEY-ID list can restrict which keys are imported from a keyring file or input stream. All keys are imported if this list is omitted. If neither nor options are given, then keys are imported from the user's personal GPG keyring.
+
+ The update-gpg-keys subcommand will attempt to
+ update the remote's GPG trusted keys using the PGP Web Key Directory
+ protocol. The URLs that will be used for locating keys can be seen in
+ the list-gpg-keys subcommand.
+
The various cookie related command allow management of a remote specific cookie jar.
diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym
index 0b876f3b02..0b8d7a0583 100644
--- a/src/libostree/libostree-devel.sym
+++ b/src/libostree/libostree-devel.sym
@@ -19,6 +19,9 @@
/* Add new symbols here. Release commits should copy this section into -released.sym. */
LIBOSTREE_2019.5 {
+global:
+ ostree_repo_remote_get_gpg_keys;
+ ostree_repo_remote_update_gpg_keys;
} LIBOSTREE_2019.4;
/* Stub section for the stable release *after* this development one; don't
diff --git a/src/libostree/ostree-gpg-verifier.c b/src/libostree/ostree-gpg-verifier.c
index 95ed36eed6..eef0a48ff2 100644
--- a/src/libostree/ostree-gpg-verifier.c
+++ b/src/libostree/ostree-gpg-verifier.c
@@ -91,43 +91,16 @@ verify_result_finalized_cb (gpointer data,
(void) glnx_shutil_rm_rf_at (AT_FDCWD, tmp_dir, NULL, NULL);
}
-OstreeGpgVerifyResult *
-_ostree_gpg_verifier_check_signature (OstreeGpgVerifier *self,
- GBytes *signed_data,
- GBytes *signatures,
- GCancellable *cancellable,
- GError **error)
+static gboolean
+_ostree_gpg_verifier_import_keys (OstreeGpgVerifier *self,
+ gpgme_ctx_t gpgme_ctx,
+ GOutputStream *pubring_stream,
+ GCancellable *cancellable,
+ GError **error)
{
GLNX_AUTO_PREFIX_ERROR("GPG", error);
- gpgme_error_t gpg_error = 0;
- g_auto(gpgme_data_t) data_buffer = NULL;
- g_auto(gpgme_data_t) signature_buffer = NULL;
- g_autofree char *tmp_dir = NULL;
- g_autoptr(GOutputStream) target_stream = NULL;
- OstreeGpgVerifyResult *result = NULL;
- gboolean success = FALSE;
- GList *link;
- int armor;
-
- /* GPGME has no API for using multiple keyrings (aka, gpg --keyring),
- * so we concatenate all the keyring files into one pubring.gpg in a
- * temporary directory, then tell GPGME to use that directory as the
- * home directory. */
-
- if (g_cancellable_set_error_if_cancelled (cancellable, error))
- goto out;
-
- result = g_initable_new (OSTREE_TYPE_GPG_VERIFY_RESULT,
- cancellable, error, NULL);
- if (result == NULL)
- goto out;
-
- if (!ot_gpgme_ctx_tmp_home_dir (result->context,
- &tmp_dir, &target_stream,
- cancellable, error))
- goto out;
- for (link = self->keyrings; link != NULL; link = link->next)
+ for (GList *link = self->keyrings; link != NULL; link = link->next)
{
g_autoptr(GFileInputStream) source_stream = NULL;
GFile *keyring_file = link->data;
@@ -145,15 +118,15 @@ _ostree_gpg_verifier_check_signature (OstreeGpgVerifier *self,
else if (local_error != NULL)
{
g_propagate_error (error, local_error);
- goto out;
+ return FALSE;
}
- bytes_written = g_output_stream_splice (target_stream,
+ bytes_written = g_output_stream_splice (pubring_stream,
G_INPUT_STREAM (source_stream),
G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE,
cancellable, error);
if (bytes_written < 0)
- goto out;
+ return FALSE;
}
for (guint i = 0; i < self->keyring_data->len; i++)
@@ -162,47 +135,174 @@ _ostree_gpg_verifier_check_signature (OstreeGpgVerifier *self,
gsize len;
gsize bytes_written;
const guint8 *buf = g_bytes_get_data (keyringd, &len);
- if (!g_output_stream_write_all (target_stream, buf, len, &bytes_written,
+ if (!g_output_stream_write_all (pubring_stream, buf, len, &bytes_written,
cancellable, error))
- goto out;
+ return FALSE;
}
- if (!g_output_stream_close (target_stream, cancellable, error))
- goto out;
+ if (!g_output_stream_close (pubring_stream, cancellable, error))
+ return FALSE;
/* Save the previous armor value - we need it on for importing ASCII keys */
- armor = gpgme_get_armor (result->context);
- gpgme_set_armor (result->context, 1);
+ int armor = gpgme_get_armor (gpgme_ctx);
+ gpgme_set_armor (gpgme_ctx, 1);
/* Now, use the API to import ASCII-armored keys */
if (self->key_ascii_files)
{
for (guint i = 0; i < self->key_ascii_files->len; i++)
{
+ gpgme_error_t gpg_error;
const char *path = self->key_ascii_files->pdata[i];
glnx_autofd int fd = -1;
g_auto(gpgme_data_t) kdata = NULL;
if (!glnx_openat_rdonly (AT_FDCWD, path, TRUE, &fd, error))
- goto out;
+ return FALSE;
gpg_error = gpgme_data_new_from_fd (&kdata, fd);
if (gpg_error != GPG_ERR_NO_ERROR)
{
ot_gpgme_throw (gpg_error, error, "Loading data from fd %i", fd);
- goto out;
+ return FALSE;
}
- gpg_error = gpgme_op_import (result->context, kdata);
+ gpg_error = gpgme_op_import (gpgme_ctx, kdata);
if (gpg_error != GPG_ERR_NO_ERROR)
{
ot_gpgme_throw (gpg_error, error, "Failed to import key");
+ return FALSE;
+ }
+ }
+ }
+
+ gpgme_set_armor (gpgme_ctx, armor);
+
+ return TRUE;
+}
+
+gboolean
+_ostree_gpg_verifier_list_keys (OstreeGpgVerifier *self,
+ const char * const *key_ids,
+ GPtrArray **out_keys,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GLNX_AUTO_PREFIX_ERROR("GPG", error);
+ g_auto(gpgme_ctx_t) context = NULL;
+ g_autoptr(GOutputStream) pubring_stream = NULL;
+ g_autofree char *tmp_dir = NULL;
+ g_autoptr(GPtrArray) keys = NULL;
+ gpgme_error_t gpg_error = 0;
+ gboolean ret = FALSE;
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ goto out;
+
+ context = ot_gpgme_new_ctx (NULL, error);
+ if (context == NULL)
+ goto out;
+
+ if (!ot_gpgme_ctx_tmp_home_dir (context, &tmp_dir, &pubring_stream,
+ cancellable, error))
+ goto out;
+
+ if (!_ostree_gpg_verifier_import_keys (self, context, pubring_stream,
+ cancellable, error))
+ goto out;
+
+ keys = g_ptr_array_new_with_free_func ((GDestroyNotify) gpgme_key_unref);
+ if (key_ids != NULL)
+ {
+ for (guint i = 0; key_ids[i] != NULL; i++)
+ {
+ gpgme_key_t key = NULL;
+
+ gpg_error = gpgme_get_key (context, key_ids[i], &key, 0);
+ if (gpg_error != GPG_ERR_NO_ERROR)
+ {
+ ot_gpgme_throw (gpg_error, error, "Unable to find key \"%s\"",
+ key_ids[i]);
goto out;
}
+
+ /* Transfer ownership. */
+ g_ptr_array_add (keys, key);
+ }
+ }
+ else
+ {
+ gpg_error = gpgme_op_keylist_start (context, NULL, 0);
+ while (gpg_error == GPG_ERR_NO_ERROR)
+ {
+ gpgme_key_t key = NULL;
+
+ gpg_error = gpgme_op_keylist_next (context, &key);
+ if (gpg_error != GPG_ERR_NO_ERROR)
+ break;
+
+ /* Transfer ownership. */
+ g_ptr_array_add (keys, key);
+ }
+
+ if (gpgme_err_code (gpg_error) != GPG_ERR_EOF)
+ {
+ ot_gpgme_throw (gpg_error, error, "Unable to list keys");
+ goto out;
}
}
- gpgme_set_armor (result->context, armor);
+ if (out_keys != NULL)
+ *out_keys = g_steal_pointer (&keys);
+
+ ret = TRUE;
+
+ out:
+ if (tmp_dir != NULL) {
+ ot_gpgme_kill_agent (tmp_dir);
+ (void) glnx_shutil_rm_rf_at (AT_FDCWD, tmp_dir, NULL, NULL);
+ }
+
+ return ret;
+}
+
+OstreeGpgVerifyResult *
+_ostree_gpg_verifier_check_signature (OstreeGpgVerifier *self,
+ GBytes *signed_data,
+ GBytes *signatures,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GLNX_AUTO_PREFIX_ERROR("GPG", error);
+ gpgme_error_t gpg_error = 0;
+ g_auto(gpgme_data_t) data_buffer = NULL;
+ g_auto(gpgme_data_t) signature_buffer = NULL;
+ g_autofree char *tmp_dir = NULL;
+ g_autoptr(GOutputStream) target_stream = NULL;
+ OstreeGpgVerifyResult *result = NULL;
+ gboolean success = FALSE;
+
+ /* GPGME has no API for using multiple keyrings (aka, gpg --keyring),
+ * so we concatenate all the keyring files into one pubring.gpg in a
+ * temporary directory, then tell GPGME to use that directory as the
+ * home directory. */
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ goto out;
+
+ result = g_initable_new (OSTREE_TYPE_GPG_VERIFY_RESULT,
+ cancellable, error, NULL);
+ if (result == NULL)
+ goto out;
+
+ if (!ot_gpgme_ctx_tmp_home_dir (result->context,
+ &tmp_dir, &target_stream,
+ cancellable, error))
+ goto out;
+
+ if (!_ostree_gpg_verifier_import_keys (self, result->context, target_stream,
+ cancellable, error))
+ goto out;
/* Both the signed data and signature GBytes instances will outlive the
* gpgme_data_t structs, so we can safely reuse the GBytes memory buffer
diff --git a/src/libostree/ostree-gpg-verifier.h b/src/libostree/ostree-gpg-verifier.h
index 634d33b299..3d803c4953 100644
--- a/src/libostree/ostree-gpg-verifier.h
+++ b/src/libostree/ostree-gpg-verifier.h
@@ -51,6 +51,12 @@ OstreeGpgVerifyResult *_ostree_gpg_verifier_check_signature (OstreeGpgVerifier *
GCancellable *cancellable,
GError **error);
+gboolean _ostree_gpg_verifier_list_keys (OstreeGpgVerifier *self,
+ const char * const *key_ids,
+ GPtrArray **out_keys,
+ GCancellable *cancellable,
+ GError **error);
+
gboolean _ostree_gpg_verifier_add_keyring_dir (OstreeGpgVerifier *self,
GFile *path,
GCancellable *cancellable,
diff --git a/src/libostree/ostree-repo.c b/src/libostree/ostree-repo.c
index 584037c428..55dc90ccdf 100644
--- a/src/libostree/ostree-repo.c
+++ b/src/libostree/ostree-repo.c
@@ -40,6 +40,7 @@
#include "ostree-repo-file-enumerator.h"
#include "ostree-gpg-verifier.h"
#include "ostree-repo-static-delta-private.h"
+#include "ostree-fetcher-util.h"
#include "ot-fs-utils.h"
#include "ostree-autocleanups.h"
@@ -2349,6 +2350,342 @@ ostree_repo_remote_gpg_import (OstreeRepo *self,
#endif /* OSTREE_DISABLE_GPGME */
}
+static gboolean
+_ostree_repo_gpg_prepare_verifier (OstreeRepo *self,
+ const gchar *remote_name,
+ GFile *keyringdir,
+ GFile *extra_keyring,
+ gboolean add_global_keyrings,
+ OstreeGpgVerifier **out_verifier,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * ostree_repo_remote_get_gpg_keys:
+ * @self: an #OstreeRepo
+ * @name: name of the remote
+ * @key_ids: (array zero-terminated=1) (element-type utf8) (nullable):
+ * a %NULL-terminated array of GPG key IDs to include, or %NULL
+ * @out_keys: (out) (optional) (element-type GVariant) (transfer container):
+ * return location for a #GPtrArray of the remote's trusted GPG keys, or
+ * %NULL
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Enumerate the trusted GPG keys for the remote @name. The keys will be
+ * returned in the @out_keys #GPtrArray. Each element in the array is a
+ * #GVariant of format %OSTREE_GPG_KEY_GVARIANT_FORMAT. The @key_ids array
+ * can be used to limit which keys are included. If @key_ids is %NULL, then
+ * all keys are included.
+ *
+ * Returns: %TRUE if the GPG keys could be enumerated, %FALSE otherwise
+ *
+ * Since: 2019.5
+ */
+gboolean
+ostree_repo_remote_get_gpg_keys (OstreeRepo *self,
+ const char *name,
+ const char * const *key_ids,
+ GPtrArray **out_keys,
+ GCancellable *cancellable,
+ GError **error)
+{
+#ifndef OSTREE_DISABLE_GPGME
+ g_autoptr(OstreeGpgVerifier) verifier = NULL;
+ if (!_ostree_repo_gpg_prepare_verifier (self, name, NULL, NULL, (name == NULL),
+ &verifier, cancellable, error))
+ return FALSE;
+
+ g_autoptr(GPtrArray) gpg_keys = NULL;
+ if (!_ostree_gpg_verifier_list_keys (verifier, key_ids, &gpg_keys,
+ cancellable, error))
+ return FALSE;
+
+ g_autoptr(GPtrArray) keys =
+ g_ptr_array_new_with_free_func ((GDestroyNotify) g_variant_unref);
+ for (guint i = 0; i < gpg_keys->len; i++)
+ {
+ gpgme_key_t key = gpg_keys->pdata[i];
+
+ g_autoptr(GVariantBuilder) subkeys_builder = g_variant_builder_new (G_VARIANT_TYPE ("a(a{sv})"));
+ g_autoptr(GVariantBuilder) uids_builder = g_variant_builder_new (G_VARIANT_TYPE ("a(a{sv})"));
+
+ for (gpgme_subkey_t subkey = key->subkeys; subkey != NULL;
+ subkey = subkey->next)
+ {
+ g_auto(GVariantDict) subkey_dict = OT_VARIANT_BUILDER_INITIALIZER;
+ g_variant_dict_init (&subkey_dict, NULL);
+ g_variant_dict_insert_value (&subkey_dict, "fingerprint",
+ g_variant_new_string (subkey->fpr));
+ g_variant_dict_insert_value (&subkey_dict, "created",
+ g_variant_new_int64 (GINT64_TO_BE (subkey->timestamp)));
+ g_variant_dict_insert_value (&subkey_dict, "expires",
+ g_variant_new_int64 (GINT64_TO_BE (subkey->expires)));
+ g_variant_dict_insert_value (&subkey_dict, "revoked",
+ g_variant_new_boolean (subkey->revoked));
+ g_variant_dict_insert_value (&subkey_dict, "expired",
+ g_variant_new_boolean (subkey->expired));
+ g_variant_dict_insert_value (&subkey_dict, "invalid",
+ g_variant_new_boolean (subkey->invalid));
+ g_variant_builder_add (subkeys_builder, "(@a{sv})",
+ g_variant_dict_end (&subkey_dict));
+ }
+
+ for (gpgme_user_id_t uid = key->uids; uid != NULL; uid = uid->next)
+ {
+ /* Get WKD update URLs if address set */
+ g_autofree char *advanced_url = NULL;
+ g_autofree char *direct_url = NULL;
+ if (uid->address != NULL)
+ {
+ if (!ot_gpg_wkd_urls (uid->address, &advanced_url, &direct_url,
+ error))
+ return FALSE;
+ }
+
+ g_auto(GVariantDict) uid_dict = OT_VARIANT_BUILDER_INITIALIZER;
+ g_variant_dict_init (&uid_dict, NULL);
+ g_variant_dict_insert_value (&uid_dict, "uid",
+ g_variant_new_string (uid->uid));
+ g_variant_dict_insert_value (&uid_dict, "name",
+ g_variant_new_string (uid->name));
+ g_variant_dict_insert_value (&uid_dict, "comment",
+ g_variant_new_string (uid->comment));
+ g_variant_dict_insert_value (&uid_dict, "email",
+ g_variant_new_string (uid->email));
+ g_variant_dict_insert_value (&uid_dict, "revoked",
+ g_variant_new_boolean (uid->revoked));
+ g_variant_dict_insert_value (&uid_dict, "invalid",
+ g_variant_new_boolean (uid->invalid));
+ g_variant_dict_insert_value (&uid_dict, "advanced_url",
+ g_variant_new ("ms", advanced_url));
+ g_variant_dict_insert_value (&uid_dict, "direct_url",
+ g_variant_new ("ms", direct_url));
+ g_variant_builder_add (uids_builder, "(@a{sv})",
+ g_variant_dict_end (&uid_dict));
+ }
+
+ /* Currently empty */
+ g_autoptr(GVariantDict) metadata_dict = g_variant_dict_new (NULL);
+
+ g_autoptr(GVariant) key_variant =
+ g_variant_ref_sink (g_variant_new ("(@a(a{sv})@a(a{sv})@a{sv})",
+ g_variant_builder_end (subkeys_builder),
+ g_variant_builder_end (uids_builder),
+ g_variant_dict_end (metadata_dict)));
+ g_ptr_array_add (keys, g_steal_pointer (&key_variant));
+ }
+
+ if (out_keys)
+ *out_keys = g_steal_pointer (&keys);
+
+ return TRUE;
+#else /* OSTREE_DISABLE_GPGME */
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+ "'%s': GPG feature is disabled in a build time",
+ __FUNCTION__);
+ return FALSE;
+#endif /* OSTREE_DISABLE_GPGME */
+}
+
+/* Arbitrary limits for fetched GPG keys */
+#define GPG_UPDATE_MAX_SIZE (1024 * 1024)
+#define GPG_UPDATE_N_RETRIES 1
+
+static gboolean
+fetch_gpg_uid_key (OstreeFetcher *fetcher,
+ const char *address,
+ GBytes **out_key,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (address != NULL, FALSE);
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return FALSE;
+
+ g_autofree char *advanced_url = NULL;
+ g_autofree char *direct_url = NULL;
+ if (!ot_gpg_wkd_urls (address, &advanced_url, &direct_url, error))
+ return FALSE;
+
+ g_autoptr(OstreeFetcherURI) advanced_uri =
+ _ostree_fetcher_uri_parse (advanced_url, error);
+ if (advanced_uri == NULL)
+ return FALSE;
+
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GBytes) key = NULL;
+ if (!_ostree_fetcher_request_uri_to_membuf (fetcher,
+ advanced_uri,
+ 0,
+ GPG_UPDATE_N_RETRIES,
+ &key,
+ GPG_UPDATE_MAX_SIZE,
+ cancellable,
+ &local_error))
+ {
+ if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ {
+ g_propagate_error (error, g_steal_pointer (&local_error));
+ return FALSE;
+ }
+
+ /* Key at advanced URL not found, try direct URL */
+ g_autoptr(OstreeFetcherURI) direct_uri =
+ _ostree_fetcher_uri_parse (direct_url, error);
+ if (direct_uri == NULL)
+ return FALSE;
+ if (!_ostree_fetcher_request_uri_to_membuf (fetcher,
+ direct_uri,
+ 0,
+ GPG_UPDATE_N_RETRIES,
+ &key,
+ GPG_UPDATE_MAX_SIZE,
+ cancellable,
+ error))
+ return FALSE;
+ }
+
+ if (out_key != NULL)
+ *out_key = g_steal_pointer (&key);
+
+ return TRUE;
+}
+
+/**
+ * ostree_repo_remote_update_gpg_keys:
+ * @self: an #OstreeRepo
+ * @name: name of the remote
+ * @out_keys: (out) (optional) (element-type GVariant) (transfer container):
+ * return location for a #GPtrArray of the remote's trusted GPG keys, or
+ * %NULL
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Update the trusted GPG keys for the remote @name. The updated keys will be
+ * returned in the @out_keys #GPtrArray. Each element in the array is a
+ * #GVariant of format %OSTREE_GPG_KEY_GVARIANT_FORMAT.
+ *
+ * Returns: %TRUE if the GPG keys could be updated, %FALSE otherwise
+ *
+ * Since: 2019.5
+ */
+gboolean
+ostree_repo_remote_update_gpg_keys (OstreeRepo *self,
+ const char *name,
+ GPtrArray **out_keys,
+ GCancellable *cancellable,
+ GError **error)
+{
+#ifndef OSTREE_DISABLE_GPGME
+ g_autoptr(OstreeGpgVerifier) verifier = NULL;
+ if (!_ostree_repo_gpg_prepare_verifier (self, name, NULL, NULL, (name == NULL),
+ &verifier, cancellable, error))
+ return FALSE;
+
+ g_autoptr(GPtrArray) gpg_keys = NULL;
+ if (!_ostree_gpg_verifier_list_keys (verifier, NULL, &gpg_keys, cancellable,
+ error))
+ return FALSE;
+
+ /* Use a temporary file for the updated keys */
+ g_auto(GLnxTmpfile) updated_keys_tmpf = { 0, };
+ if (!glnx_open_anonymous_tmpfile (O_RDWR | O_CLOEXEC, &updated_keys_tmpf,
+ error))
+ return FALSE;
+
+ /* GPGME buffer for updated keys */
+ g_autoptr(GOutputStream) updated_keys_ostream =
+ g_unix_output_stream_new (updated_keys_tmpf.fd, FALSE);
+ g_auto(gpgme_data_t) updated_keys_data = ot_gpgme_data_output (updated_keys_ostream);
+
+ g_autoptr(GPtrArray) updated_fingerprints = g_ptr_array_new_with_free_func (g_free);
+ g_autoptr(OstreeFetcher) fetcher = _ostree_fetcher_new (self->tmp_dir_fd,
+ name, 0);
+ for (guint i = 0; i < gpg_keys->len; i++)
+ {
+ gpgme_key_t key = gpg_keys->pdata[i];
+
+ for (gpgme_user_id_t uid = key->uids; uid != NULL; uid = uid->next)
+ {
+ if (uid->address == NULL)
+ continue;
+
+ g_autoptr(GBytes) fetched_key = NULL;
+ g_autoptr(GError) temp_error = NULL;
+ if (!fetch_gpg_uid_key (fetcher, uid->address, &fetched_key,
+ cancellable, &temp_error))
+ {
+ if (g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ {
+ /* No key found for this uid */
+ g_debug ("No GPG key update found for UID %s", uid->uid);
+ g_clear_error (&temp_error);
+ continue;
+ } else {
+ g_propagate_error (error, g_steal_pointer (&temp_error));
+ return FALSE;
+ }
+ }
+
+ /* Find the keys matching this email */
+ if (!ot_gpgme_filter_keyring_by_email (fetched_key,
+ uid->address,
+ updated_keys_data,
+ updated_fingerprints,
+ cancellable,
+ error))
+ return FALSE;
+ }
+ }
+
+ /* Writing to the new keyring is finished */
+ gpgme_data_release (updated_keys_data);
+ updated_keys_data = NULL;
+ g_clear_object (&updated_keys_ostream);
+
+ /* Import the updated keys if any were found */
+ g_autoptr(GPtrArray) ret_keys = NULL;
+ if (updated_fingerprints->len > 0)
+ {
+ /* NULL terminate the fingerprint array for use as a key ID array */
+ g_ptr_array_add (updated_fingerprints, NULL);
+ const char * const *key_ids = (const char * const *)updated_fingerprints->pdata;
+
+ /* Seek back to the beginning of the tmp file and open an input
+ * stream for importing.
+ */
+ if (lseek (updated_keys_tmpf.fd, 0, SEEK_SET) < 0)
+ return glnx_throw_errno_prefix (error, "lseek");
+ g_autoptr(GInputStream) updated_keys_istream =
+ g_unix_input_stream_new (updated_keys_tmpf.fd, FALSE);
+ if (!ostree_repo_remote_gpg_import (self, name, updated_keys_istream,
+ key_ids, NULL, cancellable, error))
+ return FALSE;
+
+ if (!ostree_repo_remote_get_gpg_keys (self, name, key_ids, &ret_keys,
+ cancellable, error))
+ return FALSE;
+ }
+ else
+ {
+ /* Empty key array */
+ ret_keys = g_ptr_array_new ();
+ }
+
+ if (out_keys != NULL)
+ *out_keys = g_steal_pointer (&ret_keys);
+
+ return TRUE;
+#else /* OSTREE_DISABLE_GPGME */
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+ "'%s': GPG feature is disabled in a build time",
+ __FUNCTION__);
+ return FALSE;
+#endif /* OSTREE_DISABLE_GPGME */
+}
+
/**
* ostree_repo_remote_fetch_summary:
* @self: Self
@@ -5268,20 +5605,17 @@ find_keyring (OstreeRepo *self,
return TRUE;
}
-static OstreeGpgVerifyResult *
-_ostree_repo_gpg_verify_data_internal (OstreeRepo *self,
- const gchar *remote_name,
- GBytes *data,
- GBytes *signatures,
- GFile *keyringdir,
- GFile *extra_keyring,
- GCancellable *cancellable,
- GError **error)
+static gboolean
+_ostree_repo_gpg_prepare_verifier (OstreeRepo *self,
+ const gchar *remote_name,
+ GFile *keyringdir,
+ GFile *extra_keyring,
+ gboolean add_global_keyrings,
+ OstreeGpgVerifier **out_verifier,
+ GCancellable *cancellable,
+ GError **error)
{
- g_autoptr(OstreeGpgVerifier) verifier = NULL;
- gboolean add_global_keyring_dir = TRUE;
-
- verifier = _ostree_gpg_verifier_new ();
+ g_autoptr(OstreeGpgVerifier) verifier = _ostree_gpg_verifier_new ();
if (remote_name == OSTREE_ALL_REMOTES)
{
@@ -5289,7 +5623,7 @@ _ostree_repo_gpg_verify_data_internal (OstreeRepo *self,
if (!_ostree_gpg_verifier_add_keyring_dir_at (verifier, self->repo_dir_fd, ".",
cancellable, error))
- return NULL;
+ return FALSE;
}
else if (remote_name != NULL)
{
@@ -5299,16 +5633,16 @@ _ostree_repo_gpg_verify_data_internal (OstreeRepo *self,
remote = _ostree_repo_get_remote_inherited (self, remote_name, error);
if (remote == NULL)
- return NULL;
+ return FALSE;
g_autoptr(GBytes) keyring_data = NULL;
if (!find_keyring (self, remote, &keyring_data, cancellable, error))
- return NULL;
+ return FALSE;
if (keyring_data != NULL)
{
_ostree_gpg_verifier_add_keyring_data (verifier, keyring_data, remote->keyring);
- add_global_keyring_dir = FALSE;
+ add_global_keyrings = FALSE;
}
g_auto(GStrv) gpgkeypath_list = NULL;
@@ -5319,35 +5653,62 @@ _ostree_repo_gpg_verify_data_internal (OstreeRepo *self,
";,",
&gpgkeypath_list,
error))
- return NULL;
+ return FALSE;
if (gpgkeypath_list)
{
for (char **iter = gpgkeypath_list; *iter != NULL; ++iter)
if (!_ostree_gpg_verifier_add_keyfile_path (verifier, *iter,
cancellable, error))
- return NULL;
+ return FALSE;
}
}
- if (add_global_keyring_dir)
+ if (add_global_keyrings)
{
/* Use the deprecated global keyring directory. */
if (!_ostree_gpg_verifier_add_global_keyring_dir (verifier, cancellable, error))
- return NULL;
+ return FALSE;
}
if (keyringdir)
{
if (!_ostree_gpg_verifier_add_keyring_dir (verifier, keyringdir,
cancellable, error))
- return NULL;
+ return FALSE;
}
if (extra_keyring != NULL)
{
_ostree_gpg_verifier_add_keyring_file (verifier, extra_keyring);
}
+ if (out_verifier != NULL)
+ *out_verifier = g_steal_pointer (&verifier);
+
+ return TRUE;
+}
+
+static OstreeGpgVerifyResult *
+_ostree_repo_gpg_verify_data_internal (OstreeRepo *self,
+ const gchar *remote_name,
+ GBytes *data,
+ GBytes *signatures,
+ GFile *keyringdir,
+ GFile *extra_keyring,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(OstreeGpgVerifier) verifier = NULL;
+ if (!_ostree_repo_gpg_prepare_verifier (self,
+ remote_name,
+ keyringdir,
+ extra_keyring,
+ TRUE,
+ &verifier,
+ cancellable,
+ error))
+ return NULL;
+
return _ostree_gpg_verifier_check_signature (verifier,
data,
signatures,
diff --git a/src/libostree/ostree-repo.h b/src/libostree/ostree-repo.h
index 40d3f77379..cf8c8d99c4 100644
--- a/src/libostree/ostree-repo.h
+++ b/src/libostree/ostree-repo.h
@@ -1347,6 +1347,55 @@ gboolean ostree_repo_remote_get_gpg_verify_summary (OstreeRepo *self,
const char *name,
gboolean *out_gpg_verify_summary,
GError **error);
+
+/**
+ * OSTREE_GPG_KEY_GVARIANT_FORMAT:
+ *
+ * - a(a{sv}) - Array of subkeys. Each a{sv} dictionary represents a
+ * subkey. The primary key is the first subkey. The following keys are
+ * currently recognized:
+ * - key: `fingerprint`, value: `s`, key fingerprint hexadecimal string
+ * - key: `created`, value: `x`, key creation timestamp (seconds since
+ * the Unix epoch in UTC, big-endian)
+ * - key: `expires`, value: `x`, key expiration timestamp (seconds since
+ * the Unix epoch in UTC, big-endian). If this value is 0, the key does
+ * not expire.
+ * - key: `revoked`, value: `b`, whether key is revoked
+ * - key: `expired`, value: `b`, whether key is expired
+ * - key: `invalid`, value: `b`, whether key is invalid
+ * - a(a{sv}) - Array of user IDs. Each a{sv} dictionary represents a
+ * user ID. The following keys are currently recognized:
+ * - key: `uid`, value: `s`, full user ID (name, email and comment)
+ * - key: `name`, value: `s`, user ID name component
+ * - key: `comment`, value: `s`, user ID comment component
+ * - key: `email`, value: `s`, user ID email component
+ * - key: `revoked`, value: `b`, whether user ID is revoked
+ * - key: `invalid`, value: `b`, whether user ID is invalid
+ * - key: `advanced_url`, value: `ms`, advanced WKD update URL
+ * - key: `direct_url`, value: `ms`, direct WKD update URL
+ * - a{sv} - Additional metadata dictionary. There are currently no
+ * additional metadata keys defined.
+ *
+ * Since: 2019.5
+ */
+#define OSTREE_GPG_KEY_GVARIANT_STRING "(a(a{sv})a(a{sv})a{sv})"
+#define OSTREE_GPG_KEY_GVARIANT_FORMAT G_VARIANT_TYPE (OSTREE_GPG_KEY_GVARIANT_STRING)
+
+_OSTREE_PUBLIC
+gboolean ostree_repo_remote_get_gpg_keys (OstreeRepo *self,
+ const char *name,
+ const char * const *key_ids,
+ GPtrArray **out_keys,
+ GCancellable *cancellable,
+ GError **error);
+
+_OSTREE_PUBLIC
+gboolean ostree_repo_remote_update_gpg_keys (OstreeRepo *self,
+ const char *name,
+ GPtrArray **out_keys,
+ GCancellable *cancellable,
+ GError **error);
+
_OSTREE_PUBLIC
gboolean ostree_repo_remote_gpg_import (OstreeRepo *self,
const char *name,
diff --git a/src/libotutil/ot-gpg-utils.c b/src/libotutil/ot-gpg-utils.c
index 743d941e37..d5d45a5108 100644
--- a/src/libotutil/ot-gpg-utils.c
+++ b/src/libotutil/ot-gpg-utils.c
@@ -27,6 +27,7 @@
#include
#include "libglnx.h"
+#include "zbase32.h"
/* Like glnx_throw_errno_prefix, but takes @gpg_error */
gboolean
@@ -538,3 +539,233 @@ ot_gpgme_kill_agent (const char *homedir)
return;
}
}
+
+gboolean
+ot_gpgme_filter_keyring_by_email (GBytes *keyring_data,
+ const char *email,
+ gpgme_data_t export_data,
+ GPtrArray *export_fingerprints,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean ret = FALSE;
+ g_autofree char *tmp_dir = NULL;
+ g_autoptr(GPtrArray) export_keys = NULL;
+
+ g_return_val_if_fail (keyring_data != NULL, FALSE);
+ g_return_val_if_fail (email != NULL, FALSE);
+ g_return_val_if_fail (export_data != NULL, FALSE);
+
+ /* Setup a temporary context and homedir to import the keyring into since
+ * gpgme offers no other method to analyze it.
+ */
+ g_auto(gpgme_ctx_t) ctx = ot_gpgme_new_ctx (NULL, error);
+ if (ctx == NULL)
+ goto out;
+ if (!ot_gpgme_ctx_tmp_home_dir (ctx, &tmp_dir, NULL, cancellable, error))
+ goto out;
+
+ /* Import the keyring data */
+ gpgme_error_t gpg_error = 0;
+ g_auto(gpgme_data_t) input_buffer = NULL;
+ gpg_error = gpgme_data_new_from_mem (&input_buffer,
+ g_bytes_get_data (keyring_data, NULL),
+ g_bytes_get_size (keyring_data),
+ 0 /* do not copy */);
+ if (gpg_error != GPG_ERR_NO_ERROR)
+ {
+ ot_gpgme_throw (gpg_error, error, "Unable to load keyring data");
+ goto out;
+ }
+ gpg_error = gpgme_op_import (ctx, input_buffer);
+ if (gpg_error != GPG_ERR_NO_ERROR)
+ {
+ ot_gpgme_throw (gpg_error, error, "Unable to import keyring data");
+ goto out;
+ }
+
+ /* Fail if any of the keys couldn't be imported */
+ gpgme_import_result_t import_result;
+ gpgme_import_status_t import_status;
+ import_result = gpgme_op_import_result (ctx);
+ g_debug ("Read %d keys for %s", import_result->imported, email);
+ for (import_status = import_result->imports;
+ import_status != NULL;
+ import_status = import_status->next)
+ {
+ if (import_status->result != GPG_ERR_NO_ERROR)
+ {
+ ot_gpgme_throw (import_status->result, error,
+ "Unable to import key \"%s\"",
+ import_status->fpr);
+ goto out;
+ }
+ }
+
+ /* Iterate through the imported keys looking for any that match email */
+ export_keys = g_ptr_array_new_with_free_func ((GDestroyNotify) gpgme_key_unref);
+ gpg_error = gpgme_op_keylist_start (ctx, NULL, 0);
+ while (gpg_error == GPG_ERR_NO_ERROR)
+ {
+ g_auto(gpgme_key_t) key = NULL;
+
+ gpg_error = gpgme_op_keylist_next (ctx, &key);
+ if (gpg_error != GPG_ERR_NO_ERROR)
+ break;
+
+ for (gpgme_user_id_t uid = key->uids; uid != NULL; uid = uid->next)
+ {
+ if (g_strcmp0 (uid->address, email) == 0)
+ {
+ g_debug ("Found key %s matching %s", key->fpr, email);
+ gpgme_key_ref (key);
+ g_ptr_array_add (export_keys, key);
+ g_ptr_array_add (export_fingerprints, g_strdup (key->fpr));
+ break;
+ }
+ }
+ }
+
+ /* Export the matching keys */
+ if (export_keys->len > 0)
+ {
+ /* NULL terminate key array */
+ g_ptr_array_add (export_keys, NULL);
+
+ gpg_error = gpgme_op_export_keys (ctx, (gpgme_key_t *) export_keys->pdata,
+ 0, export_data);
+ if (gpg_error != GPG_ERR_NO_ERROR)
+ {
+ ot_gpgme_throw (gpg_error, error, "Unable to export keys");
+ goto out;
+ }
+ }
+
+ ret = TRUE;
+
+ out:
+ if (tmp_dir != NULL) {
+ ot_gpgme_kill_agent (tmp_dir);
+ (void) glnx_shutil_rm_rf_at (AT_FDCWD, tmp_dir, NULL, NULL);
+ }
+
+ return ret;
+}
+
+static char *
+ascii_lower (const char *in)
+{
+ GString *tmp;
+
+ g_return_val_if_fail (in != NULL, NULL);
+ tmp = g_string_new (in);
+ return g_string_free (g_string_ascii_down (tmp), FALSE);
+}
+
+/* Takes the SHA1 checksum of the local component of an email address and
+ * returns the zbase32 encoding.
+ */
+static char *
+encode_wkd_local (const char *local)
+{
+ g_autoptr(GChecksum) checksum = NULL;
+ guint8 digest[20] = { 0 };
+ gsize len = sizeof(digest);
+ char *encoded;
+
+ g_return_val_if_fail (local != NULL, NULL);
+
+ checksum = g_checksum_new (G_CHECKSUM_SHA1);
+ g_checksum_update (checksum, (const guchar *)local, -1);
+ g_checksum_get_digest (checksum, digest, &len);
+
+ encoded = zbase32_encode (digest, len);
+
+ /* If the returned string is NULL, then there must have been a memory
+ * allocation problem. Just exit immediately like g_malloc.
+ */
+ if (encoded == NULL)
+ g_error ("%s: %s", G_STRLOC, g_strerror (errno));
+
+ return encoded;
+}
+
+/* Implementation of OpenPGP Web Key Directory URLs as defined in
+ * https://tools.ietf.org/html/draft-koch-openpgp-webkey-service-08#section-3.1.
+ */
+gboolean
+ot_gpg_wkd_urls (const char *email,
+ char **out_advanced_url,
+ char **out_direct_url,
+ GError **error)
+{
+ g_auto(GStrv) email_parts = NULL;
+ g_autofree char *local_lowered = NULL;
+ g_autofree char *domain_lowered = NULL;
+ g_autofree char *local_encoded = NULL;
+ g_autofree char *local_escaped = NULL;
+ g_autofree char *advanced_server = NULL;
+ g_autofree char *direct_server = NULL;
+ g_autofree char *advanced_url = NULL;
+ g_autofree char *direct_url = NULL;
+
+ g_return_val_if_fail (email != NULL, FALSE);
+
+ email_parts = g_strsplit (email, "@", -1);
+ if (g_strv_length (email_parts) != 2)
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ "Invalid email address \"%s\"", email);
+ return FALSE;
+ }
+
+ local_lowered = ascii_lower (email_parts[0]);
+ domain_lowered = ascii_lower (email_parts[1]);
+ local_encoded = encode_wkd_local (local_lowered);
+ local_escaped = g_uri_escape_string (email_parts[0], NULL, FALSE);
+
+ /* Allow URLs to point to a local server for testing. */
+ const char *local_port = g_getenv ("_OSTREE_GPG_UPDATE_LOCAL_PORT");
+ if (local_port != NULL)
+ {
+ for (const char *cur = local_port; *cur != '\0'; cur++)
+ {
+ if (!g_ascii_isdigit (*cur))
+ {
+ g_debug ("Ignoring non-digit environment variable "
+ "_OSTREE_GPG_UPDATE_LOCAL_PORT");
+ local_port = NULL;
+ break;
+ }
+ }
+ }
+
+ if (local_port != NULL)
+ {
+ advanced_server = g_strdup_printf ("http://127.0.0.1:%s", local_port);
+ direct_server = g_strdup (advanced_server);
+ }
+ else
+ {
+ advanced_server = g_strdup_printf ("https://openpgpkey.%s",
+ email_parts[1]);
+ direct_server = g_strdup_printf ("https://%s", email_parts[1]);
+ }
+
+ advanced_url = g_strdup_printf ("%s/.well-known/openpgpkey/"
+ "%s/hu/%s?l=%s",
+ advanced_server, domain_lowered,
+ local_encoded, local_escaped);
+ g_debug ("Advanced WKD URL: %s", advanced_url);
+
+ direct_url = g_strdup_printf ("%s/.well-known/openpgpkey/hu/%s?l=%s",
+ direct_server, local_encoded, local_escaped);
+ g_debug ("Direct WKD URL: %s", direct_url);
+
+ if (out_advanced_url != NULL)
+ *out_advanced_url = g_steal_pointer (&advanced_url);
+ if (out_direct_url != NULL)
+ *out_direct_url = g_steal_pointer (&direct_url);
+
+ return TRUE;
+}
diff --git a/src/libotutil/ot-gpg-utils.h b/src/libotutil/ot-gpg-utils.h
index e8a240b597..6d75bb5dbb 100644
--- a/src/libotutil/ot-gpg-utils.h
+++ b/src/libotutil/ot-gpg-utils.h
@@ -48,4 +48,16 @@ gpgme_ctx_t ot_gpgme_new_ctx (const char *homedir,
void ot_gpgme_kill_agent (const char *homedir);
+gboolean ot_gpgme_filter_keyring_by_email (GBytes *keyring_data,
+ const char *email,
+ gpgme_data_t export_data,
+ GPtrArray *export_fingerprints,
+ GCancellable *cancellable,
+ GError **error);
+
+gboolean ot_gpg_wkd_urls (const char *email,
+ char **out_advanced_url,
+ char **out_direct_url,
+ GError **error);
+
G_END_DECLS
diff --git a/src/libotutil/zbase32.c b/src/libotutil/zbase32.c
new file mode 100644
index 0000000000..39fa97a465
--- /dev/null
+++ b/src/libotutil/zbase32.c
@@ -0,0 +1,141 @@
+/**
+ * copyright 2002, 2003 Bryce "Zooko" Wilcox-O'Hearn
+ * mailto:zooko@zooko.com
+ *
+ * See the end of this file for the free software, open source license (BSD-style).
+ */
+#include "zbase32.h"
+
+#include
+#include
+#include
+#include /* XXX only for debug printfs */
+
+static const char*const chars="ybndrfg8ejkmcpqxot1uwisza345h769";
+
+/* Types from zstr */
+/**
+ * A zstr is simply an unsigned int length and a pointer to a buffer of
+ * unsigned chars.
+ */
+typedef struct {
+ size_t len; /* the length of the string (not counting the null-terminating character) */
+ unsigned char* buf; /* pointer to the first byte */
+} zstr;
+
+/**
+ * A zstr is simply an unsigned int length and a pointer to a buffer of
+ * const unsigned chars.
+ */
+typedef struct {
+ size_t len; /* the length of the string (not counting the null-terminating character) */
+ const unsigned char* buf; /* pointer to the first byte */
+} czstr;
+
+/* Functions from zstr */
+static zstr
+new_z(const size_t len)
+{
+ zstr result;
+ result.buf = (unsigned char *)malloc(len+1);
+ if (result.buf == NULL) {
+ result.len = 0;
+ return result;
+ }
+ result.len = len;
+ result.buf[len] = '\0';
+ return result;
+}
+
+/* Functions from zutil */
+static size_t
+divceil(size_t n, size_t d)
+{
+ return n/d+((n%d)!=0);
+}
+
+static zstr b2a_l_extra_Duffy(const czstr os, const size_t lengthinbits)
+{
+ zstr result = new_z(divceil(os.len*8, 5)); /* if lengthinbits is not a multiple of 8 then this is allocating space for 0, 1, or 2 extra quintets that will be truncated at the end of this function if they are not needed */
+ if (result.buf == NULL)
+ return result;
+
+ unsigned char* resp = result.buf + result.len; /* pointer into the result buffer, initially pointing to the "one-past-the-end" quintet */
+ const unsigned char* osp = os.buf + os.len; /* pointer into the os buffer, initially pointing to the "one-past-the-end" octet */
+
+ /* Now this is a real live Duff's device. You gotta love it. */
+ unsigned long x=0; /* to hold up to 32 bits worth of the input */
+ switch ((osp - os.buf) % 5) {
+ case 0:
+ do {
+ x = *--osp;
+ *--resp = chars[x % 32]; /* The least sig 5 bits go into the final quintet. */
+ x /= 32; /* ... now we have 3 bits worth in x... */
+ case 4:
+ x |= ((unsigned long)(*--osp)) << 3; /* ... now we have 11 bits worth in x... */
+ *--resp = chars[x % 32];
+ x /= 32; /* ... now we have 6 bits worth in x... */
+ *--resp = chars[x % 32];
+ x /= 32; /* ... now we have 1 bits worth in x... */
+ case 3:
+ x |= ((unsigned long)(*--osp)) << 1; /* The 8 bits from the 2-indexed octet. So now we have 9 bits worth in x... */
+ *--resp = chars[x % 32];
+ x /= 32; /* ... now we have 4 bits worth in x... */
+ case 2:
+ x |= ((unsigned long)(*--osp)) << 4; /* The 8 bits from the 1-indexed octet. So now we have 12 bits worth in x... */
+ *--resp = chars[x%32];
+ x /= 32; /* ... now we have 7 bits worth in x... */
+ *--resp = chars[x%32];
+ x /= 32; /* ... now we have 2 bits worth in x... */
+ case 1:
+ x |= ((unsigned long)(*--osp)) << 2; /* The 8 bits from the 0-indexed octet. So now we have 10 bits worth in x... */
+ *--resp = chars[x%32];
+ x /= 32; /* ... now we have 5 bits worth in x... */
+ *--resp = chars[x];
+ } while (osp > os.buf);
+ } /* switch ((osp - os.buf) % 5) */
+
+ /* truncate any unused trailing zero quintets */
+ result.len = divceil(lengthinbits, 5);
+ result.buf[result.len] = '\0';
+ return result;
+}
+
+static zstr b2a_l(const czstr os, const size_t lengthinbits)
+{
+ return b2a_l_extra_Duffy(os, lengthinbits);
+}
+
+static zstr b2a(const czstr os)
+{
+ return b2a_l(os, os.len*8);
+}
+
+char *
+zbase32_encode(const unsigned char *data, size_t length)
+{
+ czstr input = { length, data };
+ zstr output = b2a(input);
+ return (char *)output.buf;
+}
+
+/**
+ * Copyright (c) 2002 Bryce "Zooko" Wilcox-O'Hearn
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software to deal in this software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of this software, and to permit
+ * persons to whom this software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of this software.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THIS SOFTWARE.
+ */
diff --git a/src/libotutil/zbase32.h b/src/libotutil/zbase32.h
new file mode 100644
index 0000000000..bf9cf6832d
--- /dev/null
+++ b/src/libotutil/zbase32.h
@@ -0,0 +1,49 @@
+/**
+ * copyright 2002, 2003 Bryce "Zooko" Wilcox-O'Hearn
+ * mailto:zooko@zooko.com
+ *
+ * See the end of this file for the free software, open source license (BSD-style).
+ */
+#ifndef __INCL_base32_h
+#define __INCL_base32_h
+
+static char const* const base32_h_cvsid = "$Id: base32.h,v 1.11 2003/12/15 01:16:19 zooko Exp $";
+
+static int const base32_vermaj = 0;
+static int const base32_vermin = 9;
+static int const base32_vermicro = 12;
+static char const* const base32_vernum = "0.9.12";
+
+#include
+#include
+
+/**
+ * @param data to be zbase-32 encoded
+ * @param length size of the data buffer
+ *
+ * @return an allocated string containing the zbase-32 encoded representation
+ */
+char *zbase32_encode(const unsigned char *data, size_t length);
+
+#endif /* #ifndef __INCL_base32_h */
+
+/**
+ * Copyright (c) 2002 Bryce "Zooko" Wilcox-O'Hearn
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software to deal in this software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of this software, and to permit
+ * persons to whom this software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of this software.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THIS SOFTWARE.
+ */
diff --git a/src/ostree/ot-builtin-remote.c b/src/ostree/ot-builtin-remote.c
index 6b3f6a268d..d387db6dfe 100644
--- a/src/ostree/ot-builtin-remote.c
+++ b/src/ostree/ot-builtin-remote.c
@@ -44,6 +44,12 @@ static OstreeCommand remote_subcommands[] = {
{ "gpg-import", OSTREE_BUILTIN_FLAG_NONE,
ot_remote_builtin_gpg_import,
"Import GPG keys" },
+ { "list-gpg-keys", OSTREE_BUILTIN_FLAG_NONE,
+ ot_remote_builtin_list_gpg_keys,
+ "Show remote GPG keys" },
+ { "update-gpg-keys", OSTREE_BUILTIN_FLAG_NONE,
+ ot_remote_builtin_update_gpg_keys,
+ "Update remote GPG keys" },
#endif /* OSTREE_DISABLE_GPGME */
#ifdef HAVE_LIBCURL_OR_LIBSOUP
{ "add-cookie", OSTREE_BUILTIN_FLAG_NONE,
diff --git a/src/ostree/ot-dump.c b/src/ostree/ot-dump.c
index 38f3730b84..1027149282 100644
--- a/src/ostree/ot-dump.c
+++ b/src/ostree/ot-dump.c
@@ -53,6 +53,7 @@ ot_dump_variant (GVariant *variant)
static gchar *
format_timestamp (guint64 timestamp,
+ gboolean local_tz,
GError **error)
{
GDateTime *dt;
@@ -66,7 +67,19 @@ format_timestamp (guint64 timestamp,
return NULL;
}
- str = g_date_time_format (dt, "%Y-%m-%d %H:%M:%S +0000");
+ if (local_tz)
+ {
+ /* Convert to local time and display in the locale's preferred
+ * representation.
+ */
+ g_autoptr(GDateTime) dt_local = g_date_time_to_local (dt);
+ str = g_date_time_format (dt_local, "%c");
+ }
+ else
+ {
+ str = g_date_time_format (dt, "%Y-%m-%d %H:%M:%S +0000");
+ }
+
g_date_time_unref (dt);
return str;
@@ -123,7 +136,7 @@ dump_commit (GVariant *variant,
&subject, &body, ×tamp, NULL, NULL);
timestamp = GUINT64_FROM_BE (timestamp);
- str = format_timestamp (timestamp, &local_error);
+ str = format_timestamp (timestamp, FALSE, &local_error);
if (!str)
{
g_assert (local_error); /* Pacify static analysis */
@@ -366,3 +379,106 @@ ot_dump_summary_bytes (GBytes *summary_bytes,
g_print ("%s: %s\n", key, value_str);
}
}
+
+static gboolean
+dump_gpg_subkey (GVariant *subkey,
+ gboolean primary,
+ GError **error)
+{
+ const gchar *fingerprint = NULL;
+ gint64 created = 0;
+ gint64 expires = 0;
+ gboolean revoked = FALSE;
+ gboolean expired = FALSE;
+ gboolean invalid = FALSE;
+ (void) g_variant_lookup (subkey, "fingerprint", "&s", &fingerprint);
+ (void) g_variant_lookup (subkey, "created", "x", &created);
+ (void) g_variant_lookup (subkey, "expires", "x", &expires);
+ (void) g_variant_lookup (subkey, "revoked", "b", &revoked);
+ (void) g_variant_lookup (subkey, "expired", "b", &expired);
+ (void) g_variant_lookup (subkey, "invalid", "b", &invalid);
+
+ /* Convert timestamps from big endian if needed */
+ created = GINT64_FROM_BE (created);
+ expires = GINT64_FROM_BE (expires);
+
+ g_print ("%s: %s%s%s\n",
+ primary ? "Key" : " Subkey",
+ fingerprint,
+ revoked ? " (revoked)" : "",
+ invalid ? " (invalid)" : "");
+
+ g_autofree gchar *created_str = format_timestamp (created, TRUE,
+ error);
+ if (created_str == NULL)
+ return FALSE;
+ g_print ("%sCreated: %s\n",
+ primary ? " " : " ",
+ created_str);
+
+ if (expires > 0)
+ {
+ g_autofree gchar *expires_str = format_timestamp (expires, TRUE,
+ error);
+ if (expires_str == NULL)
+ return FALSE;
+ g_print ("%s%s: %s\n",
+ primary ? " " : " ",
+ expired ? "Expired" : "Expires",
+ expires_str);
+ }
+
+ return TRUE;
+}
+
+gboolean
+ot_dump_gpg_key (GVariant *key,
+ GError **error)
+{
+ if (!g_variant_is_of_type (key, OSTREE_GPG_KEY_GVARIANT_FORMAT))
+ return glnx_throw (error, "GPG key variant type doesn't match '%s'",
+ OSTREE_GPG_KEY_GVARIANT_STRING);
+
+ g_autoptr(GVariant) subkeys_v = g_variant_get_child_value (key, 0);
+ GVariantIter subkeys_iter;
+ g_variant_iter_init (&subkeys_iter, subkeys_v);
+
+ g_autoptr(GVariant) primary_key = NULL;
+ g_variant_iter_next (&subkeys_iter, "(@a{sv})", &primary_key);
+ if (!dump_gpg_subkey (primary_key, TRUE, error))
+ return FALSE;
+
+ g_autoptr(GVariant) uids_v = g_variant_get_child_value (key, 1);
+ GVariantIter uids_iter;
+ g_variant_iter_init (&uids_iter, uids_v);
+ GVariant *uid_v = NULL;
+ while (g_variant_iter_loop (&uids_iter, "(@a{sv})", &uid_v))
+ {
+ const gchar *uid = NULL;
+ gboolean revoked = FALSE;
+ gboolean invalid = FALSE;
+ (void) g_variant_lookup (uid_v, "uid", "&s", &uid);
+ (void) g_variant_lookup (uid_v, "revoked", "b", &revoked);
+ (void) g_variant_lookup (uid_v, "invalid", "b", &invalid);
+ g_print (" UID: %s%s%s\n",
+ uid,
+ revoked ? " (revoked)" : "",
+ invalid ? " (invalid)" : "");
+
+ const char *advanced_url = NULL;
+ const char *direct_url = NULL;
+ (void) g_variant_lookup (uid_v, "advanced_url", "m&s", &advanced_url);
+ (void) g_variant_lookup (uid_v, "direct_url", "m&s", &direct_url);
+ g_print (" Advanced update URL: %s\n", advanced_url ?: "");
+ g_print (" Direct update URL: %s\n", direct_url ?: "");
+ }
+
+ GVariant *subkey = NULL;
+ while (g_variant_iter_loop (&subkeys_iter, "(@a{sv})", &subkey))
+ {
+ if (!dump_gpg_subkey (subkey, FALSE, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/src/ostree/ot-dump.h b/src/ostree/ot-dump.h
index 0e1952af81..02e2f1a65c 100644
--- a/src/ostree/ot-dump.h
+++ b/src/ostree/ot-dump.h
@@ -42,3 +42,6 @@ void ot_dump_object (OstreeObjectType objtype,
void ot_dump_summary_bytes (GBytes *summary_bytes,
OstreeDumpFlags flags);
+
+gboolean ot_dump_gpg_key (GVariant *key,
+ GError **error);
diff --git a/src/ostree/ot-remote-builtin-list-gpg-keys.c b/src/ostree/ot-remote-builtin-list-gpg-keys.c
new file mode 100644
index 0000000000..360d9e32af
--- /dev/null
+++ b/src/ostree/ot-remote-builtin-list-gpg-keys.c
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 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, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include "otutil.h"
+
+#include "ot-main.h"
+#include "ot-dump.h"
+#include "ot-remote-builtins.h"
+
+/* ATTENTION:
+ * Please remember to update the bash-completion script (bash/ostree) and
+ * man page (man/ostree-remote.xml) when changing the option list.
+ */
+
+static GOptionEntry option_entries[] = {
+ { NULL }
+};
+
+gboolean
+ot_remote_builtin_list_gpg_keys (int argc,
+ char **argv,
+ OstreeCommandInvocation *invocation,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GOptionContext) context = g_option_context_new ("NAME");
+ g_autoptr(OstreeRepo) repo = NULL;
+ if (!ostree_option_context_parse (context, option_entries, &argc, &argv,
+ invocation, &repo, cancellable, error))
+ return FALSE;
+
+ /* if (argc < 2) */
+ /* { */
+ /* ot_util_usage_error (context, "NAME must be specified", error); */
+ /* return FALSE; */
+ /* } */
+
+ const char *remote_name = (argc > 1) ? argv[1] : NULL;
+
+ g_autoptr(GPtrArray) keys = NULL;
+ if (!ostree_repo_remote_get_gpg_keys (repo, remote_name, NULL, &keys,
+ cancellable, error))
+ return FALSE;
+
+ for (guint i = 0; i < keys->len; i++)
+ {
+ if (!ot_dump_gpg_key (keys->pdata[i], error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/src/ostree/ot-remote-builtin-update-gpg-keys.c b/src/ostree/ot-remote-builtin-update-gpg-keys.c
new file mode 100644
index 0000000000..4488a75476
--- /dev/null
+++ b/src/ostree/ot-remote-builtin-update-gpg-keys.c
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 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, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include "otutil.h"
+
+#include "ot-main.h"
+#include "ot-dump.h"
+#include "ot-remote-builtins.h"
+
+/* ATTENTION:
+ * Please remember to update the bash-completion script (bash/ostree) and
+ * man page (man/ostree-remote.xml) when changing the option list.
+ */
+
+static GOptionEntry option_entries[] = {
+ { NULL }
+};
+
+gboolean
+ot_remote_builtin_update_gpg_keys (int argc,
+ char **argv,
+ OstreeCommandInvocation *invocation,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GOptionContext) context = g_option_context_new ("NAME");
+ g_autoptr(OstreeRepo) repo = NULL;
+ if (!ostree_option_context_parse (context, option_entries, &argc, &argv,
+ invocation, &repo, cancellable, error))
+ return FALSE;
+
+ if (argc < 2)
+ {
+ ot_util_usage_error (context, "NAME must be specified", error);
+ return FALSE;
+ }
+
+ const char *remote_name = argv[1];
+
+ g_autoptr(GPtrArray) keys = NULL;
+ if (!ostree_repo_remote_update_gpg_keys (repo, remote_name, &keys,
+ cancellable, error))
+ return FALSE;
+
+ for (guint i = 0; i < keys->len; i++)
+ {
+ if (!ot_dump_gpg_key (keys->pdata[i], error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/src/ostree/ot-remote-builtins.h b/src/ostree/ot-remote-builtins.h
index 71b2365a3b..3a04af450c 100644
--- a/src/ostree/ot-remote-builtins.h
+++ b/src/ostree/ot-remote-builtins.h
@@ -32,6 +32,8 @@ G_BEGIN_DECLS
BUILTINPROTO(add);
BUILTINPROTO(delete);
BUILTINPROTO(gpg_import);
+BUILTINPROTO(list_gpg_keys);
+BUILTINPROTO(update_gpg_keys);
BUILTINPROTO(list);
#ifdef HAVE_LIBCURL_OR_LIBSOUP
BUILTINPROTO(add_cookie);
diff --git a/tests/gpghome/key1-subkey-revoked.asc b/tests/gpghome/key1-subkey-revoked.asc
new file mode 100644
index 0000000000..8a7e65b3a2
--- /dev/null
+++ b/tests/gpghome/key1-subkey-revoked.asc
@@ -0,0 +1,36 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFIuhBYBCADTbnocQsJgMfOELkFt3wRrAZShijoBPYZT9BrIuIKZxAbaxZJr
+Tbw8eIGgHZ51NCfdoikul0i82dt4hwtsACNVL5EGRmvTIKHPacb0yJMr1YBjcSwD
+Slo+niLPb/oVtLTbDWFt/msYKREF/lGJT9dJyXkQ5UOwWdipDaHIlwb0IKUvL7cu
+NpNthRFRm1M5d5M9OtqTCrCja6zckQ6OfvoStsbneHzfVWeH7vLcKBxxkfDhusVt
+y1iVaDk1EYT8ZxsrAWw4S7nRK/bjr86IYpFPjG2aKMd9qxyIo7hcX4r8od24jzfM
+v/ysOapnkTJuv8J6v7MakM1HkCz+TKF6gXxVABEBAAG0HU9zdHJlZSBUZXN0ZXIg
+PHRlc3RAdGVzdC5jb20+iQE5BBMBAgAjBQJSLoQWAhsDBwsJCAcDAgEGFQgCCQoL
+BBYCAwECHgECF4AACgkQf8oj2Ecs2vr/9wgAnme6WsWQy8CYeGH4q/5I6XFL6q1m
+S0+qdeGnYRmR0jJAGJ84vqDhnKxjeQzp+8Nq81DHGEJBszCkMW2o22neFi2Mo95h
+Dq3GWNZVldCDshjPs563AY6j7zACUN7Cy5XB3MK/vj5R/SrHBtJmSgPTx9WfmUgn
+n5Udg+fzSsS8z8DUtJFtexgrSnEmwH+nOmIfrsjIYL5EPg+CTTalhygROrERjINr
+pCYiShaFCKbuyt/XvyQ71y0JbB2yS7tDv0mL4SZjSuBQ1PkNE8ZQsymqBOJHA1Y3
+ppgPs1OenmtYgxaR8HQQv7uxHWZz0dmwQN93Qx8zMZwW40Odmdh1zLNQf7kBDQRS
+LoQWAQgA9i9QWg28qmFrPIzn90ZlNlUtFzoZy/8/lIk34awge1uO5aHydYBzkuWU
+jCDyBtQLWZQlwOKq8oHBbjENR2sfsmNkrYKcceQ02hSXqEJkc6jcDMCpB9eWy34K
+sPZmdl76Eo/vIIgRqJ9JPeGoMPaIBg2ouEz6Ft6jcX3EriYIKebCEA9wPk29z40x
+7D8mBZn06WrZ3JyePfbCdNJlQANEnrk7KDMNwPhhE1wcfPkiVtqBR0/FwIoUP0jn
+PishIWOuFObYnXQQ2R8sxrw/V0hGqVTh+k+iNAjzEp4yPsAvB+LdMH9nCY5rU3Vo
+1paEqVM1EHoBPu4NupRN0AjIJPr5UQARAQABiQFABCgBCgAqFiEEXmXedascUBhi
+1HY0f8oj2Ecs2voFAl2uBq0MHQJXZSBsb3N0IGl0AAoJEH/KI9hHLNr6AM8H/1OF
+IGKVihrk0/aqVzeB/qX0UEmy33BxUPQ1fW2Lwh0CorfgrkMfjoHUvWtj75Jmz+YR
+YVmIPwIp7q46OlutFL6PcwT7AYEGlzf+EEqw82khToSarGFRrzcjiZ/XZoUPTXNF
+7DPn6iya6QkU0nnbZATa0I7nPnVT5YPmJiEl1BIOWX23qkOhl8mgQ0wi7nsWi2Vp
+wd2jTzfj5FYDC2qJYZp0kCPrTc6EtpVzx6/bwNerl8g94ViKEjMZnUOG28nqSmne
+kMUh3hBRUJjvz88/xb+gHa73eJI731rcXkCySP0sAN4BHcRWZfJaj1OKAVFgXVx4
+9BC3GJvnjpe+M4uOCiSJAR8EGAECAAkFAlIuhBYCGwwACgkQf8oj2Ecs2vryLggA
+x1z4SABo9kVZlxcUYF+Gc4pAUL+79boK7UmOohiQY7QfFKFJ8GTECuqWnfvDhhUf
+htSS7qNrjbVt8YU0y0x9ePDaZTcdF1oN6c/o4a/aNiZiYW3rOLQllmG+LxkJwBBN
+K1nYyzeHCy0IyIFc+ZgDspb0bOjglBIoJbmFogIZVJaXuSGfQ6SE5NUj27M2vv4u
+FifaJv/KdJowp4jiFny/UcO5jRXUTre8U8YsUFM9qhE+meb3IGdcxaGttX3svp4S
+h7t9q6tLI9wXXUsQULnHygQ2dsf7C0Bc5rJCjeWV34lFr0IkRmXxJN1FT2jY0XAX
+xczwEQ5ae0xLxo2k+ggsLw==
+=53Nf
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/tests/test-remote-gpg-import.sh b/tests/test-remote-gpg-import.sh
index 4d73fa1164..e64f10082c 100755
--- a/tests/test-remote-gpg-import.sh
+++ b/tests/test-remote-gpg-import.sh
@@ -92,6 +92,24 @@ ${OSTREE} remote add R1 $(cat httpd-address)/ostree/gnomerepo
cat ${test_tmpdir}/gpghome/key{1,2,3}.asc | ${OSTREE} remote gpg-import --stdin R1 | grep -o 'Imported [[:digit:]] GPG key' > result
assert_file_has_content result 'Imported 3 GPG key'
+# List out keys
+${OSTREE} remote list-gpg-keys R1 > result
+assert_file_has_content result 'Key: 5E65DE75AB1C501862D476347FCA23D8472CDAFA'
+assert_file_has_content result 'UID: Ostree Tester '
+assert_file_has_content result 'Advanced update URL: https://openpgpkey.test.com/.well-known/openpgpkey/test.com/hu/iffe93qcsgp4c8ncbb378rxjo6cn9q6u?l=test$'
+assert_file_has_content result 'Direct update URL: https://test.com/.well-known/openpgpkey/hu/iffe93qcsgp4c8ncbb378rxjo6cn9q6u?l=test$'
+assert_file_has_content result 'Subkey: CC47B2DFB520AEF231180725DF20F58B408DEA49'
+assert_file_has_content result 'Key: 7B3B1020D74479687FDB2273D8228CFECA950D41'
+assert_file_has_content result 'UID: Ostree Tester II '
+assert_file_has_content result 'Advanced update URL: https://openpgpkey.test.com/.well-known/openpgpkey/test.com/hu/nnxwsxno46ap6hw7fgphp68j76egpfa9?l=test2$'
+assert_file_has_content result 'Direct update URL: https://test.com/.well-known/openpgpkey/hu/nnxwsxno46ap6hw7fgphp68j76egpfa9?l=test2$'
+assert_file_has_content result 'Subkey: 1EFA95C06EB1EB91754575E004B69C2560D53993'
+assert_file_has_content result 'Key: 7D29CF060B8269CDF63BFBDD0D15FAE7DF444D67'
+assert_file_has_content result 'UID: Ostree Tester III '
+assert_file_has_content result 'Advanced update URL: https://openpgpkey.test.com/.well-known/openpgpkey/test.com/hu/8494gyqhmrcs6gn38tn6kgjexet117cj?l=test3$'
+assert_file_has_content result 'Direct update URL: https://test.com/.well-known/openpgpkey/hu/8494gyqhmrcs6gn38tn6kgjexet117cj?l=test3$'
+assert_file_has_content result 'Subkey: 0E45E48CBF7B360C0E04443E0C601A7402416340'
+
${OSTREE} remote delete R1
#------------------------------------------------------------
diff --git a/tests/test-remote-update-gpg-keys.sh b/tests/test-remote-update-gpg-keys.sh
new file mode 100755
index 0000000000..f6f8fb14a5
--- /dev/null
+++ b/tests/test-remote-update-gpg-keys.sh
@@ -0,0 +1,98 @@
+#!/bin/bash
+#
+# Copyright (C) 2015 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, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+set -euo pipefail
+
+. $(dirname $0)/libtest.sh
+
+# We don't want OSTREE_GPG_HOME used for these tests.
+unset OSTREE_GPG_HOME
+
+setup_fake_remote_repo1 "archive"
+
+# Use the local http server for GPG key update tests
+_OSTREE_GPG_UPDATE_LOCAL_PORT=$(cat ${test_tmpdir}/httpd-port)
+export _OSTREE_GPG_UPDATE_LOCAL_PORT
+
+echo "1..6"
+
+cd ${test_tmpdir}
+mkdir repo
+ostree_repo_init repo
+
+# Check that update-gpg-keys works with no existing keys
+${OSTREE} remote add R1 $(cat httpd-address)/ostree/gnomerepo
+${OSTREE} remote update-gpg-keys R1 > result
+assert_file_empty result
+echo "ok remote with no gpg keys"
+
+# Import a GPG key and check that no updates found
+${OSTREE} remote gpg-import --keyring ${test_tmpdir}/gpghome/key1.asc R1
+${OSTREE} remote update-gpg-keys R1 > result
+assert_file_empty result
+echo "ok update no keys found"
+
+# Test advanced update URL
+rm -rf ${test_tmpdir}/httpd/.well-known/openpgpkey/
+mkdir -p ${test_tmpdir}/httpd/.well-known/openpgpkey/test.com/hu/
+cp ${test_tmpdir}/gpghome/trusted/pubring.gpg \
+ ${test_tmpdir}/httpd/.well-known/openpgpkey/test.com/hu/iffe93qcsgp4c8ncbb378rxjo6cn9q6u
+${OSTREE} remote update-gpg-keys R1 > result
+assert_file_has_content result 'Key: 5E65DE75AB1C501862D476347FCA23D8472CDAFA'
+assert_file_has_content result 'UID: Ostree Tester '
+assert_not_file_has_content result 'Key: 7B3B1020D74479687FDB2273D8228CFECA950D41'
+assert_not_file_has_content result 'UID: Ostree Tester II '
+assert_not_file_has_content result 'Key: 7D29CF060B8269CDF63BFBDD0D15FAE7DF444D67'
+assert_not_file_has_content result 'UID: Ostree Tester III '
+echo "ok update advanced URL"
+
+# Test direct update URL
+rm -rf ${test_tmpdir}/httpd/.well-known/openpgpkey/
+mkdir -p ${test_tmpdir}/httpd/.well-known/openpgpkey/hu/
+cp ${test_tmpdir}/gpghome/trusted/pubring.gpg \
+ ${test_tmpdir}/httpd/.well-known/openpgpkey/hu/iffe93qcsgp4c8ncbb378rxjo6cn9q6u
+${OSTREE} remote update-gpg-keys R1 > result
+assert_file_has_content result 'Key: 5E65DE75AB1C501862D476347FCA23D8472CDAFA'
+assert_file_has_content result 'UID: Ostree Tester '
+assert_not_file_has_content result 'Key: 7B3B1020D74479687FDB2273D8228CFECA950D41'
+assert_not_file_has_content result 'UID: Ostree Tester II '
+assert_not_file_has_content result 'Key: 7D29CF060B8269CDF63BFBDD0D15FAE7DF444D67'
+assert_not_file_has_content result 'UID: Ostree Tester III '
+echo "ok update direct URL"
+
+# Test invalid remote GPG key
+rm -rf ${test_tmpdir}/httpd/.well-known/openpgpkey/
+mkdir -p ${test_tmpdir}/httpd/.well-known/openpgpkey/test.com/hu/
+echo invalid > ${test_tmpdir}/httpd/.well-known/openpgpkey/test.com/hu/iffe93qcsgp4c8ncbb378rxjo6cn9q6u
+${OSTREE} --verbose remote update-gpg-keys R1 > result
+assert_file_empty result
+echo "ok ignored invalid remote GPG key"
+
+# Test importing a revoked subkey
+rm -rf ${test_tmpdir}/httpd/.well-known/openpgpkey/
+mkdir -p ${test_tmpdir}/httpd/.well-known/openpgpkey/test.com/hu/
+cp ${test_tmpdir}/gpghome/key1-subkey-revoked.asc \
+ ${test_tmpdir}/httpd/.well-known/openpgpkey/test.com/hu/iffe93qcsgp4c8ncbb378rxjo6cn9q6u
+${OSTREE} --verbose remote update-gpg-keys R1 > result
+assert_file_has_content result 'Key: 5E65DE75AB1C501862D476347FCA23D8472CDAFA'
+assert_file_has_content result 'UID: Ostree Tester '
+assert_file_has_content result 'Subkey: CC47B2DFB520AEF231180725DF20F58B408DEA49 (revoked)'
+echo "ok update revoked subkey"