diff --git a/Makefile-libostree.am b/Makefile-libostree.am
index dd39697406..d40de48d13 100644
--- a/Makefile-libostree.am
+++ b/Makefile-libostree.am
@@ -173,9 +173,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=
diff --git a/Makefile-ostree.am b/Makefile-ostree.am
index fd5ec9dea6..a5509f7ca0 100644
--- a/Makefile-ostree.am
+++ b/Makefile-ostree.am
@@ -105,6 +105,7 @@ ostree_SOURCES += \
if USE_GPGME
ostree_SOURCES += \
src/ostree/ot-remote-builtin-gpg-import.c \
+ src/ostree/ot-remote-builtin-list-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 295c734ec0..1997bfd842 100644
--- a/Makefile-tests.am
+++ b/Makefile-tests.am
@@ -152,6 +152,7 @@ _installed_or_uninstalled_test_scripts = \
if USE_GPGME
_installed_or_uninstalled_test_scripts += \
tests/test-remote-gpg-import.sh \
+ tests/test-remote-list-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 2da1d74969..4d02755558 100644
--- a/apidoc/ostree-sections.txt
+++ b/apidoc/ostree-sections.txt
@@ -337,6 +337,7 @@ ostree_repo_remote_list_collection_refs
ostree_repo_remote_get_url
ostree_repo_remote_get_gpg_verify
ostree_repo_remote_get_gpg_verify_summary
+ostree_repo_remote_get_gpg_keys
ostree_repo_remote_gpg_import
ostree_repo_remote_fetch_summary
ostree_repo_remote_fetch_summary_with_options
@@ -482,6 +483,8 @@ ostree_repo_regenerate_summary
OSTREE_REPO
OSTREE_IS_REPO
OSTREE_TYPE_REPO
+OSTREE_GPG_KEY_GVARIANT_STRING
+OSTREE_GPG_KEY_GVARIANT_FORMAT
ostree_repo_get_type
ostree_repo_commit_modifier_get_type
ostree_repo_transaction_stats_get_type
diff --git a/bash/ostree b/bash/ostree
index d1de853068..32d5e3174d 100644
--- a/bash/ostree
+++ b/bash/ostree
@@ -1235,6 +1235,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
@@ -1349,6 +1383,7 @@ _ostree_remote() {
gpg-import
list
list-cookies
+ list-gpg-keys
refs
show-url
summary
diff --git a/man/ostree-remote.xml b/man/ostree-remote.xml
index 407f7e3d2c..928bf9b5f8 100644
--- a/man/ostree-remote.xml
+++ b/man/ostree-remote.xml
@@ -65,6 +65,9 @@ Boston, MA 02111-1307, USA.
ostree remote gpg-import OPTIONS NAME KEY-ID
+
+ ostree remote list-gpg-keys NAME
+
ostree remote refs NAME
@@ -106,7 +109,11 @@ 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.
diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym
index e3cd14a4af..75bc464770 100644
--- a/src/libostree/libostree-devel.sym
+++ b/src/libostree/libostree-devel.sym
@@ -22,6 +22,11 @@
- uncomment the include in Makefile-libostree.am
*/
+LIBOSTREE_2021.4 {
+global:
+ ostree_repo_remote_get_gpg_keys;
+} LIBOSTREE_2021.3;
+
/* 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-gpg-verifier.c b/src/libostree/ostree-gpg-verifier.c
index 95ed36eed6..e9f5c5e332 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,23 +135,25 @@ _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);
+ gboolean ret = FALSE;
+ 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;
@@ -193,7 +168,7 @@ _ostree_gpg_verifier_check_signature (OstreeGpgVerifier *self,
goto out;
}
- 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");
@@ -202,7 +177,136 @@ _ostree_gpg_verifier_check_signature (OstreeGpgVerifier *self,
}
}
- gpgme_set_armor (result->context, armor);
+ ret = TRUE;
+
+ out:
+ gpgme_set_armor (gpgme_ctx, armor);
+
+ return ret;
+}
+
+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;
+ }
+ }
+
+ 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 b90e1c1374..0ff2c53f68 100644
--- a/src/libostree/ostree-repo.c
+++ b/src/libostree/ostree-repo.c
@@ -2353,6 +2353,144 @@ 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: (nullable): name of the remote or %NULL
+ * @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. If @name is
+ * %NULL, the global GPG keys will be returned. 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: 2021.4
+ */
+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;
+ gboolean global_keyrings = (name == NULL);
+ if (!_ostree_repo_gpg_prepare_verifier (self, name, NULL, NULL, global_keyrings,
+ &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_auto(GVariantBuilder) subkeys_builder = OT_VARIANT_BUILDER_INITIALIZER;
+ g_variant_builder_init (&subkeys_builder, G_VARIANT_TYPE ("aa{sv}"));
+ g_auto(GVariantBuilder) uids_builder = OT_VARIANT_BUILDER_INITIALIZER;
+ g_variant_builder_init (&uids_builder, G_VARIANT_TYPE ("aa{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_auto(GVariantDict) metadata_dict = OT_VARIANT_BUILDER_INITIALIZER;
+ g_variant_dict_init (&metadata_dict, NULL);
+
+ GVariant *key_variant = g_variant_new ("(@aa{sv}@aa{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_variant_ref_sink (key_variant));
+ }
+
+ if (out_keys)
+ *out_keys = g_steal_pointer (&keys);
+
+ return TRUE;
+#else /* OSTREE_DISABLE_GPGME */
+ return glnx_throw (error, "GPG feature is disabled in a build time");
+#endif /* OSTREE_DISABLE_GPGME */
+}
+
/**
* ostree_repo_remote_fetch_summary:
* @self: Self
@@ -5338,20 +5476,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)
{
@@ -5359,7 +5494,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)
{
@@ -5369,16 +5504,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;
@@ -5389,35 +5524,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 08d3d408be..5f3093dff3 100644
--- a/src/libostree/ostree-repo.h
+++ b/src/libostree/ostree-repo.h
@@ -1425,6 +1425,48 @@ 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:
+ *
+ * - aa{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
+ * - aa{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: 2021.4
+ */
+#define OSTREE_GPG_KEY_GVARIANT_STRING "(aa{sv}aa{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_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..4dbefdbd46 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,77 @@ ot_gpgme_kill_agent (const char *homedir)
return;
}
}
+
+/* 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_return_val_if_fail (local != NULL, NULL);
+
+ guint8 digest[20] = { 0 };
+ gsize len = sizeof (digest);
+ g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_SHA1);
+ g_checksum_update (checksum, (const guchar *)local, -1);
+ g_checksum_get_digest (checksum, digest, &len);
+
+ char *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://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service
+ */
+gboolean
+ot_gpg_wkd_urls (const char *email,
+ char **out_advanced_url,
+ char **out_direct_url,
+ GError **error)
+{
+ g_return_val_if_fail (email != NULL, FALSE);
+
+ g_auto(GStrv) 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;
+ }
+
+ g_autofree char *local_lowered = g_ascii_strdown (email_parts[0], -1);
+ g_autofree char *domain_lowered = g_ascii_strdown (email_parts[1], -1);
+ g_autofree char *local_encoded = encode_wkd_local (local_lowered);
+ g_autofree char *local_escaped = g_uri_escape_string (email_parts[0], NULL, FALSE);
+
+ g_autofree char *advanced_url = g_strdup_printf ("https://openpgpkey.%s"
+ "/.well-known/openpgpkey"
+ "/%s/hu/%s?l=%s",
+ email_parts[1],
+ domain_lowered,
+ local_encoded,
+ local_escaped);
+ g_debug ("GPG UID \"%s\" advanced WKD URL: %s", email, advanced_url);
+
+ g_autofree char *direct_url = g_strdup_printf ("https://%s"
+ "/.well-known/openpgpkey"
+ "/hu/%s?l=%s",
+ email_parts[1],
+ local_encoded,
+ local_escaped);
+ g_debug ("GPG UID \"%s\" direct WKD URL: %s", email, 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..b559b69573 100644
--- a/src/libotutil/ot-gpg-utils.h
+++ b/src/libotutil/ot-gpg-utils.h
@@ -48,4 +48,9 @@ gpgme_ctx_t ot_gpgme_new_ctx (const char *homedir,
void ot_gpgme_kill_agent (const char *homedir);
+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..7028eacc75 100644
--- a/src/ostree/ot-builtin-remote.c
+++ b/src/ostree/ot-builtin-remote.c
@@ -44,6 +44,9 @@ 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" },
#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 a8ed54a2ee..05c0978026 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;
@@ -124,7 +137,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 */
@@ -390,3 +403,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..84d0f1a309
--- /dev/null
+++ b/src/ostree/ot-remote-builtin-list-gpg-keys.c
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+ 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-builtins.h b/src/ostree/ot-remote-builtins.h
index 71b2365a3b..4b46af199f 100644
--- a/src/ostree/ot-remote-builtins.h
+++ b/src/ostree/ot-remote-builtins.h
@@ -32,6 +32,7 @@ G_BEGIN_DECLS
BUILTINPROTO(add);
BUILTINPROTO(delete);
BUILTINPROTO(gpg_import);
+BUILTINPROTO(list_gpg_keys);
BUILTINPROTO(list);
#ifdef HAVE_LIBCURL_OR_LIBSOUP
BUILTINPROTO(add_cookie);
diff --git a/tests/test-remote-list-gpg-keys.sh b/tests/test-remote-list-gpg-keys.sh
new file mode 100755
index 0000000000..81699f14ec
--- /dev/null
+++ b/tests/test-remote-list-gpg-keys.sh
@@ -0,0 +1,152 @@
+#!/bin/bash
+#
+# Copyright © 2021 Endless OS Foundation LLC
+#
+# 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 most of these tests.
+emptydir=${test_tmpdir}/empty
+trusteddir=${OSTREE_GPG_HOME}
+mkdir ${emptydir}
+OSTREE_GPG_HOME=${emptydir}
+
+# Key listings show dates using the local timezone, so specify UTC for
+# consistency.
+export TZ=UTC
+
+# Some tests require an appropriate gpg
+num_non_gpg_tests=5
+num_gpg_tests=2
+num_tests=$((num_non_gpg_tests + num_gpg_tests))
+
+echo "1..${num_tests}"
+
+setup_test_repository "archive"
+
+cd ${test_tmpdir}
+${OSTREE} remote add R1 http://example.com/repo
+
+# No remote keyring should list no keys.
+${OSTREE} remote list-gpg-keys R1 > result
+assert_file_empty result
+
+echo "ok remote no keyring"
+
+# Make the global keyring available and make sure there are still no
+# keys found for a specified remote.
+OSTREE_GPG_HOME=${trusteddir}
+${OSTREE} remote list-gpg-keys R1 > result
+OSTREE_GPG_HOME=${emptydir}
+assert_file_empty result
+
+echo "ok remote with global keyring"
+
+# Import a key and check that it's listed
+${OSTREE} remote gpg-import --keyring ${TEST_GPG_KEYHOME}/key1.asc R1
+${OSTREE} remote list-gpg-keys R1 > result
+cat > expected <<"EOF"
+Key: 5E65DE75AB1C501862D476347FCA23D8472CDAFA
+ Created: Tue Sep 10 02:29:42 2013
+ UID: Ostree Tester
+ Advanced update URL: https://openpgpkey.test.com/.well-known/openpgpkey/test.com/hu/iffe93qcsgp4c8ncbb378rxjo6cn9q6u?l=test
+ Direct update URL: https://test.com/.well-known/openpgpkey/hu/iffe93qcsgp4c8ncbb378rxjo6cn9q6u?l=test
+ Subkey: CC47B2DFB520AEF231180725DF20F58B408DEA49
+ Created: Tue Sep 10 02:29:42 2013
+EOF
+assert_files_equal result expected
+
+echo "ok remote with keyring"
+
+# Check the global keys with no keyring
+OSTREE_GPG_HOME=${emptydir}
+${OSTREE} remote list-gpg-keys > result
+assert_file_empty result
+
+echo "ok global no keyring"
+
+# Now check the global keys with a keyring
+OSTREE_GPG_HOME=${trusteddir}
+${OSTREE} remote list-gpg-keys > result
+OSTREE_GPG_HOME=${emptydir}
+cat > expected <<"EOF"
+Key: 5E65DE75AB1C501862D476347FCA23D8472CDAFA
+ Created: Tue Sep 10 02:29:42 2013
+ UID: Ostree Tester
+ Advanced update URL: https://openpgpkey.test.com/.well-known/openpgpkey/test.com/hu/iffe93qcsgp4c8ncbb378rxjo6cn9q6u?l=test
+ Direct update URL: https://test.com/.well-known/openpgpkey/hu/iffe93qcsgp4c8ncbb378rxjo6cn9q6u?l=test
+ Subkey: CC47B2DFB520AEF231180725DF20F58B408DEA49
+ Created: Tue Sep 10 02:29:42 2013
+Key: 7B3B1020D74479687FDB2273D8228CFECA950D41
+ Created: Tue Mar 17 14:00:32 2015
+ UID: Ostree Tester II
+ Advanced update URL: https://openpgpkey.test.com/.well-known/openpgpkey/test.com/hu/nnxwsxno46ap6hw7fgphp68j76egpfa9?l=test2
+ Direct update URL: https://test.com/.well-known/openpgpkey/hu/nnxwsxno46ap6hw7fgphp68j76egpfa9?l=test2
+ Subkey: 1EFA95C06EB1EB91754575E004B69C2560D53993
+ Created: Tue Mar 17 14:00:32 2015
+Key: 7D29CF060B8269CDF63BFBDD0D15FAE7DF444D67
+ Created: Tue Mar 17 14:01:05 2015
+ UID: Ostree Tester III
+ Advanced update URL: https://openpgpkey.test.com/.well-known/openpgpkey/test.com/hu/8494gyqhmrcs6gn38tn6kgjexet117cj?l=test3
+ Direct update URL: https://test.com/.well-known/openpgpkey/hu/8494gyqhmrcs6gn38tn6kgjexet117cj?l=test3
+ Subkey: 0E45E48CBF7B360C0E04443E0C601A7402416340
+ Created: Tue Mar 17 14:01:05 2015
+EOF
+assert_files_equal result expected
+
+echo "ok global with keyring"
+
+# Tests checking for expiration and revocation listings require gpg.
+GPG=$(which_gpg)
+if [ -z "${GPG}" ]; then
+ # Print a skip message per skipped test
+ for (( i = 0; i < num_gpg_tests; i++ )); do
+ echo "ok # SKIP this test requires gpg"
+ done
+else
+ # The GPG private keyring in gpghome is in the older secring.gpg
+ # format, but we're likely using a newer gpg. Normally it's
+ # implicitly migrated to the newer format, but this test hasn't
+ # signed anything, so the private keys haven't been loaded. Force
+ # the migration by listing the private keys.
+ ${GPG} --homedir=${test_tmpdir}/gpghome -K >/dev/null
+
+ # Expire key1, wait for it to be expired and re-import it.
+ ${GPG} --homedir=${test_tmpdir}/gpghome --quick-set-expire ${TEST_GPG_KEYFPR_1} seconds=1
+ sleep 2
+ ${GPG} --homedir=${test_tmpdir}/gpghome --armor --export ${TEST_GPG_KEYID_1} > ${test_tmpdir}/key1expired.asc
+ ${OSTREE} remote gpg-import --keyring ${test_tmpdir}/key1expired.asc R1
+ ${OSTREE} remote list-gpg-keys R1 > result
+ assert_file_has_content result "^ Expired:"
+
+ echo "ok remote expired key"
+
+ # Revoke key1 and re-import it.
+ ${GPG} --homedir=${TEST_GPG_KEYHOME} --import ${TEST_GPG_KEYHOME}/revocations/key1.rev
+ ${GPG} --homedir=${test_tmpdir}/gpghome --armor --export ${TEST_GPG_KEYID_1} > ${test_tmpdir}/key1revoked.asc
+ ${OSTREE} remote gpg-import --keyring ${test_tmpdir}/key1revoked.asc R1
+ ${OSTREE} remote list-gpg-keys R1 > result
+ assert_file_has_content result "^Key: 5E65DE75AB1C501862D476347FCA23D8472CDAFA (revoked)"
+ assert_file_has_content result "^ UID: Ostree Tester (revoked)"
+ assert_file_has_content result "^ Subkey: CC47B2DFB520AEF231180725DF20F58B408DEA49 (revoked)"
+
+ echo "ok remote revoked key"
+fi