summaryrefslogtreecommitdiff
path: root/src/libostree
diff options
context:
space:
mode:
authorColin Walters <walters@verbum.org>2021-08-20 15:11:11 -0400
committerGitHub <noreply@github.com>2021-08-20 15:11:11 -0400
commit98f3fe3d8ea85525fc11e8dbe63c591ece79b160 (patch)
tree7a8e4ae5a468cc1220a5ec36c1bba2d8841c1445 /src/libostree
parent3209acbdbe9d4e0ea45c1ed711655c9ddb55a8b3 (diff)
parent81df5c8abae137bad8a08acefeab7b3a8e7205c1 (diff)
downloadostree-98f3fe3d8ea85525fc11e8dbe63c591ece79b160.tar.gz
Merge pull request #2401 from dbnicholson/gpg-key-info
Remote GPG key info
Diffstat (limited to 'src/libostree')
-rw-r--r--src/libostree/libostree-devel.sym5
-rw-r--r--src/libostree/ostree-gpg-verifier.c194
-rw-r--r--src/libostree/ostree-gpg-verifier.h6
-rw-r--r--src/libostree/ostree-repo.c206
-rw-r--r--src/libostree/ostree-repo.h42
5 files changed, 386 insertions, 67 deletions
diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym
index e3cd14a4..75bc4647 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 95ed36ee..e9f5c5e3 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 634d33b2..3d803c49 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 b90e1c13..0ff2c53f 100644
--- a/src/libostree/ostree-repo.c
+++ b/src/libostree/ostree-repo.c
@@ -2353,6 +2353,144 @@ out:
#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 08d3d408..5f3093df 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,