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"