From 5ee7f04cafa0ab7511c0b407c82dd065d5e002d3 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 9 Apr 2023 21:10:13 +0200 Subject: [PATCH 1/4] feat: Add WKD support --- gnupg.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++ test_gnupg.py | 11 ++++++++++ 2 files changed, 71 insertions(+) diff --git a/gnupg.py b/gnupg.py index d7b65ea..b437d52 100644 --- a/gnupg.py +++ b/gnupg.py @@ -34,6 +34,7 @@ """ import codecs +from datetime import date from io import StringIO import logging import os @@ -1016,6 +1017,37 @@ def handle_status(self, key, value): logger.debug('message ignored: %s, %s', key, value) +class AutoLocateKey(StatusHandler): + """ + This class handles status messages during key auto-locating. + """ + fingerprint: str + type: str + created_at: date + email: str + email_display_name: str + + def __init__(self, gpg): + StatusHandler.__init__(self, gpg) + self.fingerprint = None + self.type = None + self.created_at = None + self.email = None + self.email_display_name = None + + def handle_status(self, key, value): + if key == "IMPORTED": + _, email, display_name = value.split() + + self.email = email + self.email_display_name = display_name[1:-1] + elif key == "KEY_CONSIDERED": + self.fingerprint = value.strip().split()[0] + else: + print(key, value) + + + VERSION_RE = re.compile(r'gpg \(GnuPG(?:/MacGPG2)?\) (\d+(\.\d+)*)'.encode('ascii'), re.I) HEX_DIGITS_RE = re.compile(r'[0-9a-f]+$', re.I) PUBLIC_KEY_RE = re.compile(r'gpg: public key is (\w+)') @@ -1043,6 +1075,7 @@ class GPG(object): 'trust': TrustResult, 'verify': Verify, 'export': ExportResult, + 'auto-locate-key': AutoLocateKey, } "A map of GPG operations to result object types." @@ -1882,6 +1915,33 @@ def search_keys(self, query, keyserver='pgp.mit.edu', extra_args=None): getattr(result, keyword)(L) return result + def auto_locate_key(self, email, mechanisms=None, **kwargs): + """ + Auto locate a public key by `email`. + + Args: + email (str): The email address to search for. + mechanisms (list[str]): A list of mechanisms to use. Valid mechanisms can be found + here https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html + under "--auto-key-locate". Default: ['wkd', 'ntds', 'ldap', 'cert', 'dane', 'local'] + """ + mechanisms = mechanisms or ['wkd', 'ntds', 'ldap', 'cert', 'dane', 'local'] + + args = ['--auto-key-locate', ','.join(mechanisms), '--locate-keys', email] + + result = self.result_map['auto-locate-key'](self) + data = _make_binary_stream('', self.encoding) + + if 'extra_args' in kwargs: + args.extend(kwargs['extra_args']) + + try: + self._handle_io(args, data, result, binary=True) + finally: + data.close() + return result + + def gen_key(self, input): """ Generate a key; you might use `gen_key_input()` to create the input. diff --git a/test_gnupg.py b/test_gnupg.py index 461c3ff..c91927a 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -1547,6 +1547,17 @@ def test_multiple_signatures_one_invalid(self): finally: os.remove(fn) + def test_auto_key_locating(self): + gpg = self.gpg + + # Let's hope ProtonMail doesn't change their key anytime soon + expected_fingerprint = "90E619A84E85330A692F6D81A655882018DBFA9D" + expected_type = "rsa2048" + + actual = self.gpg.auto_locate_key("no-reply@protonmail.com") + + self.assertEqual(actual.fingerprint, expected_fingerprint) + TEST_GROUPS = { 'sign': From 0f10653dbc57c972d5fbf4c5be30e5b225ac26a8 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 14 Apr 2023 17:18:01 +0200 Subject: [PATCH 2/4] feat: Add output parsing --- gnupg.py | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/gnupg.py b/gnupg.py index b437d52..65995c5 100644 --- a/gnupg.py +++ b/gnupg.py @@ -34,7 +34,8 @@ """ import codecs -from datetime import date +from datetime import date, datetime +from email.utils import parseaddr from io import StringIO import logging import os @@ -1022,10 +1023,10 @@ class AutoLocateKey(StatusHandler): This class handles status messages during key auto-locating. """ fingerprint: str - type: str + key_length: int created_at: date email: str - email_display_name: str + email_real_name: str def __init__(self, gpg): StatusHandler.__init__(self, gpg) @@ -1033,19 +1034,37 @@ def __init__(self, gpg): self.type = None self.created_at = None self.email = None - self.email_display_name = None + self.email_real_name = None def handle_status(self, key, value): if key == "IMPORTED": _, email, display_name = value.split() self.email = email - self.email_display_name = display_name[1:-1] + self.email_real_name = display_name[1:-1] elif key == "KEY_CONSIDERED": self.fingerprint = value.strip().split()[0] - else: - print(key, value) + def pub(self, args): + """ + Internal method to handle the 'pub' status message. + `pub` message contains the fingerprint of the public key, its type and its creation date. + """ + pass + + def uid(self, args): + self.created_at = datetime.fromtimestamp(int(args[5])) + raw_email_content = args[9] + email, real_name = parseaddr(raw_email_content) + self.email = email + self.email_real_name = real_name + + def sub(self, args): + self.key_length = int(args[2]) + + def fpr(self, args): + # Only store the first fingerprint + self.fingerprint = self.fingerprint or args[9] VERSION_RE = re.compile(r'gpg \(GnuPG(?:/MacGPG2)?\) (\d+(\.\d+)*)'.encode('ascii'), re.I) @@ -1915,7 +1934,7 @@ def search_keys(self, query, keyserver='pgp.mit.edu', extra_args=None): getattr(result, keyword)(L) return result - def auto_locate_key(self, email, mechanisms=None, **kwargs): + def auto_locate_key(self, email, mechanisms=None, **kwargs) -> AutoLocateKey: """ Auto locate a public key by `email`. @@ -1930,18 +1949,15 @@ def auto_locate_key(self, email, mechanisms=None, **kwargs): args = ['--auto-key-locate', ','.join(mechanisms), '--locate-keys', email] result = self.result_map['auto-locate-key'](self) - data = _make_binary_stream('', self.encoding) if 'extra_args' in kwargs: args.extend(kwargs['extra_args']) - try: - self._handle_io(args, data, result, binary=True) - finally: - data.close() + process = self._open_subprocess(args) + self._collect_output(process, result, stdin=process.stdin) + self._decode_result(result) return result - def gen_key(self, input): """ Generate a key; you might use `gen_key_input()` to create the input. From 043d553775ec17a8bd670d0d18432b03cc82853f Mon Sep 17 00:00:00 2001 From: Vinay Sajip Date: Tue, 23 Jan 2024 10:03:11 +0000 Subject: [PATCH 3/4] Maintain support for Python 2.x (for now). --- gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg.py b/gnupg.py index 3980927..6ec6b62 100644 --- a/gnupg.py +++ b/gnupg.py @@ -1005,12 +1005,12 @@ def handle_status(self, key, value): class AutoLocateKey(StatusHandler): """ This class handles status messages during key auto-locating. - """ fingerprint: str key_length: int created_at: date email: str email_real_name: str + """ def __init__(self, gpg): StatusHandler.__init__(self, gpg) From 23a30a52e0ffea208c3efbdb37e3e62f308aa524 Mon Sep 17 00:00:00 2001 From: Vinay Sajip Date: Fri, 2 Feb 2024 10:44:36 +0000 Subject: [PATCH 4/4] Remove typing annotation. --- gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg.py b/gnupg.py index 6ec6b62..13ef16e 100644 --- a/gnupg.py +++ b/gnupg.py @@ -1921,7 +1921,7 @@ def search_keys(self, query, keyserver='pgp.mit.edu', extra_args=None): getattr(result, keyword)(L) return result - def auto_locate_key(self, email, mechanisms=None, **kwargs) -> AutoLocateKey: + def auto_locate_key(self, email, mechanisms=None, **kwargs): """ Auto locate a public key by `email`.