commit: bbf8790c330358a618bdac44f4a488d99a4b5007
Author: Michał Górny <mgorny <AT> gentoo <DOT> org>
AuthorDate: Tue Oct 21 11:36:52 2025 +0000
Commit: Michał Górny <mgorny <AT> gentoo <DOT> org>
CommitDate: Tue Oct 21 11:36:52 2025 +0000
URL: https://gitweb.gentoo.org/proj/gemato.git/commit/?id=bbf8790c
openpgp: Support list_keys() in all envs
Signed-off-by: Michał Górny <mgorny <AT> gentoo.org>
gemato/openpgp.py | 110 +++++++++++++++++++++++++-------------------------
tests/test_openpgp.py | 23 +++++++++--
2 files changed, 76 insertions(+), 57 deletions(-)
diff --git a/gemato/openpgp.py b/gemato/openpgp.py
index fede3e5..f148765 100644
--- a/gemato/openpgp.py
+++ b/gemato/openpgp.py
@@ -2,6 +2,8 @@
# (c) 2017-2024 Michał Górny
# SPDX-License-Identifier: GPL-2.0-or-later
+from __future__ import annotations
+
import base64
import dataclasses
import datetime
@@ -138,6 +140,60 @@ class SystemGPGEnvironment:
raise NotImplementedError(
'import_key() is not implemented by this OpenPGP provider')
+ def list_keys(self, key_ids: list[str] = []) -> dict[str, list[str]]:
+ """
+ List fingerprints and UIDs of specified keys or all keys in keyring
+
+ Returns a mapping from fingerprint (as a string) to an iterable
+ of UIDs.
+ """
+
+ exitst, out, err = self._spawn_gpg(
+ [GNUPG, '--batch', '--with-colons', '--list-keys', *key_ids],
+ raise_on_error=OpenPGPKeyListingError)
+
+ prev_pub = None
+ fpr = None
+ ret = {}
+
+ for line in out.splitlines():
+ # were we expecting a fingerprint?
+ if prev_pub is not None:
+ if line.startswith(b'fpr:'):
+ fpr = line.split(b':')[9].decode('ASCII')
+ if not fpr.endswith(prev_pub):
+ raise OpenPGPKeyListingError(
+ f'Incorrect fingerprint {fpr} for key '
+ f'{prev_pub}')
+ LOGGER.debug(
+ f'list_keys(): fingerprint: {fpr}')
+ ret[fpr] = []
+ prev_pub = None
+ else:
+ raise OpenPGPKeyListingError(
+ f'No fingerprint in GPG output, instead got: '
+ f'{line}')
+ elif line.startswith(b'pub:'):
+ # wait for the fingerprint
+ prev_pub = line.split(b':')[4].decode('ASCII')
+ LOGGER.debug(f'list_keys(): keyid: {prev_pub}')
+ elif line.startswith(b'uid:'):
+ if fpr is None:
+ raise OpenPGPKeyListingError(
+ f'UID without key in GPG output: {line}')
+ uid = line.split(b':')[9]
+ _, addr = email.utils.parseaddr(
+ uid.decode('utf8', errors='replace'))
+ if '@' in addr:
+ LOGGER.debug(f'list_keys(): UID: {addr}')
+ ret[fpr].append(addr)
+ else:
+ LOGGER.debug(
+ f'list_keys(): ignoring UID without mail: '
+ f'{uid!r}')
+
+ return ret
+
def refresh_keys(self, allow_wkd=True, keyserver=None):
"""
Update the keys from their assigned keyservers. This should be called
@@ -502,60 +558,6 @@ debug-level guru
ownertrust,
raise_on_error=OpenPGPKeyImportError)
- def list_keys(self):
- """
- List fingerprints and UIDs of all keys in keyring
-
- Returns a mapping from fingerprint (as a string) to an iterable
- of UIDs.
- """
-
- exitst, out, err = self._spawn_gpg(
- [GNUPG, '--batch', '--with-colons', '--list-keys'],
- raise_on_error=OpenPGPKeyListingError)
-
- prev_pub = None
- fpr = None
- ret = {}
-
- for line in out.splitlines():
- # were we expecting a fingerprint?
- if prev_pub is not None:
- if line.startswith(b'fpr:'):
- fpr = line.split(b':')[9].decode('ASCII')
- if not fpr.endswith(prev_pub):
- raise OpenPGPKeyListingError(
- f'Incorrect fingerprint {fpr} for key '
- f'{prev_pub}')
- LOGGER.debug(
- f'list_keys(): fingerprint: {fpr}')
- ret[fpr] = []
- prev_pub = None
- else:
- raise OpenPGPKeyListingError(
- f'No fingerprint in GPG output, instead got: '
- f'{line}')
- elif line.startswith(b'pub:'):
- # wait for the fingerprint
- prev_pub = line.split(b':')[4].decode('ASCII')
- LOGGER.debug(f'list_keys(): keyid: {prev_pub}')
- elif line.startswith(b'uid:'):
- if fpr is None:
- raise OpenPGPKeyListingError(
- f'UID without key in GPG output: {line}')
- uid = line.split(b':')[9]
- _, addr = email.utils.parseaddr(
- uid.decode('utf8', errors='replace'))
- if '@' in addr:
- LOGGER.debug(f'list_keys(): UID: {addr}')
- ret[fpr].append(addr)
- else:
- LOGGER.debug(
- f'list_keys(): ignoring UID without mail: '
- f'{uid!r}')
-
- return ret
-
def refresh_keys_wkd(self):
"""
Attempt to fetch updated keys using WKD. Returns true if *all*
diff --git a/tests/test_openpgp.py b/tests/test_openpgp.py
index 357cad4..6019019 100644
--- a/tests/test_openpgp.py
+++ b/tests/test_openpgp.py
@@ -24,6 +24,7 @@ from gemato.exceptions import (
OpenPGPExpiredKeyFailure,
OpenPGPRevokedKeyFailure,
OpenPGPKeyImportError,
+ OpenPGPKeyListingError,
OpenPGPKeyRefreshError,
OpenPGPRuntimeError,
OpenPGPUntrustedSigFailure,
@@ -808,12 +809,28 @@ def
test_recursive_manifest_loader_save_submanifest(tmp_path, privkey_env):
('VALID_KEY_NOEMAIL', {KEY_FINGERPRINT: []}),
('VALID_KEY_NONUTF', {KEY_FINGERPRINT: ['[email protected]']}),
])
-def test_list_keys(openpgp_env_with_refresh, key_var, expected):
+def test_list_keys(openpgp_env, key_var, expected):
try:
- openpgp_env_with_refresh.import_key(io.BytesIO(globals()[key_var]))
+ openpgp_env.import_key(io.BytesIO(globals()[key_var]))
+ except OpenPGPNoImplementation as e:
+ pytest.skip(str(e))
+ assert openpgp_env.list_keys() == expected
+ assert openpgp_env.list_keys(list(expected.keys())) == expected
+
+
+def test_list_keys_empty(openpgp_env):
+ try:
+ assert openpgp_env.list_keys() == {}
+ except OpenPGPNoImplementation as e:
+ pytest.skip(str(e))
+
+
+def test_list_keys_error(openpgp_env):
+ try:
+ with pytest.raises(OpenPGPKeyListingError):
+ openpgp_env.list_keys(["zzzzz"])
except OpenPGPNoImplementation as e:
pytest.skip(str(e))
- assert openpgp_env_with_refresh.list_keys() == expected
@pytest.fixture(scope='module')