diff --git a/Makefile-ostree.am b/Makefile-ostree.am index fd5ec9de..a5509f7c 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-tests.am b/Makefile-tests.am index 295c734e..1997bfd8 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/bash/ostree b/bash/ostree index d1de8530..32d5e317 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 407f7e3d..928bf9b5 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/ostree/ot-builtin-remote.c b/src/ostree/ot-builtin-remote.c index 6b3f6a26..7028eacc 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 a8ed54a2..1c0f04a9 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,99 @@ 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)" : ""); + } + + 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 0e1952af..02e2f1a6 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 00000000..84d0f1a3 --- /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 71b2365a..4b46af19 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 00000000..5ad6c9f2 --- /dev/null +++ b/tests/test-remote-list-gpg-keys.sh @@ -0,0 +1,144 @@ +#!/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 + 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 + Subkey: CC47B2DFB520AEF231180725DF20F58B408DEA49 + Created: Tue Sep 10 02:29:42 2013 +Key: 7B3B1020D74479687FDB2273D8228CFECA950D41 + Created: Tue Mar 17 14:00:32 2015 + UID: Ostree Tester II + Subkey: 1EFA95C06EB1EB91754575E004B69C2560D53993 + Created: Tue Mar 17 14:00:32 2015 +Key: 7D29CF060B8269CDF63BFBDD0D15FAE7DF444D67 + Created: Tue Mar 17 14:01:05 2015 + UID: Ostree Tester III + 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