diff --git a/gemato/cli.py b/gemato/cli.py index fdd1a6c..a33e593 100644 --- a/gemato/cli.py +++ b/gemato/cli.py @@ -122,6 +122,10 @@ def add_options(self, subp): dest='refresh_keys', help='Disable refreshing OpenPGP key (prevents network access, ' +'applicable when using -K only)') + subp.add_argument('-W', '--no-wkd', action='store_false', + dest='allow_wkd', + help='Do not attempt to use WKD to refetch keys (use ' + +'keyservers only)') def parse_args(self, args, argp): super(VerifyingOpenPGPMixin, self).parse_args(args, argp) @@ -130,8 +134,8 @@ def parse_args(self, args, argp): # always refresh keys to check for revocation # (unless user specifically asked us not to) if args.refresh_keys: - logging.info('Refreshing keys from keyserver...') - self.openpgp_env.refresh_keys() + logging.info('Refreshing keys...') + self.openpgp_env.refresh_keys(allow_wkd=args.allow_wkd) logging.info('Keys refreshed.') diff --git a/gemato/openpgp.py b/gemato/openpgp.py index 177ff43..d3cb13d 100644 --- a/gemato/openpgp.py +++ b/gemato/openpgp.py @@ -4,6 +4,7 @@ # Licensed under the terms of 2-clause BSD license import datetime +import email.utils import errno import os import os.path @@ -55,11 +56,15 @@ def import_key(self, keyfile): raise NotImplementedError('import_key() is not implemented by this OpenPGP provider') - def refresh_keys(self): + def refresh_keys(self, allow_wkd=True): """ Update the keys from their assigned keyservers. This should be called at start of every execution in order to ensure that revocations are respected. This action requires network access. + + @allow_wkd specifies whether WKD can be used to fetch keys. This is + experimental but usually is more reliable than keyservers. If WKD + fails to fetch *all* keys, gemato falls back to keyservers. """ raise NotImplementedError('refresh_keys() is not implemented by this OpenPGP provider') @@ -225,11 +230,95 @@ def import_key(self, keyfile): if exitst != 0: raise gemato.exceptions.OpenPGPKeyImportError(err.decode('utf8')) - def refresh_keys(self): + def refresh_keys_wkd(self): + """ + Attempt to fetch updated keys using WKD. Returns true if *all* + keys were successfully found. Otherwise, returns false. + """ + # list all keys in the keyring + exitst, out, err = self._spawn_gpg(['--with-colons', '--list-keys'], '') + if exitst != 0: + raise gemato.exceptions.OpenPGPKeyRefreshError(err.decode('utf8')) + + # find keys and UIDs + addrs = set() + addrs_key = set() + keys = set() + prev_pub = None + for l in out.splitlines(): + # were we expecting a fingerprint? + if prev_pub is not None: + if l.startswith(b'fpr:'): + fpr = l.split(b':')[9].decode('ASCII') + assert fpr.endswith(prev_pub) + keys.add(fpr) + prev_pub = None + else: + # old GnuPG doesn't give fingerprints by default + # (but it doesn't support WKD either) + return False + elif l.startswith(b'pub:'): + if keys: + # every key must have at least one UID + if not addrs_key: + return False + addrs.update(addrs_key) + addrs_key = set() + + # wait for the fingerprint + prev_pub = l.split(b':')[4].decode('ASCII') + elif l.startswith(b'uid:'): + uid = l.split(b':')[9] + name, addr = email.utils.parseaddr(uid.decode('utf8')) + if '@' in addr: + addrs_key.add(addr) + + # grab the final set (also aborts when there are no keys) + if not addrs_key: + return False + addrs.update(addrs_key) + + # create another isolated environment to fetch keys cleanly + with OpenPGPEnvironment() as subenv: + # use --locate-keys to fetch keys via WKD + exitst, out, err = subenv._spawn_gpg(['--locate-keys'] + + list(addrs), '') + # if at least one fetch failed, gpg returns unsuccessfully + if exitst != 0: + return False + + # otherwise, xfer the keys + exitst, out, err = subenv._spawn_gpg(['--status-fd', '2', + '--export'] + list(keys), '') + if exitst != 0: + return False + + # we need to explicitly ensure all keys were fetched + for l in err.splitlines(): + if l.startswith(b'[GNUPG:] EXPORTED'): + fpr = l.split(b' ')[2].decode('ASCII') + keys.remove(fpr) + if keys: + return False + + exitst, out2, err = self._spawn_gpg(['--import'], out) + if exitst != 0: + # there's no valid reason for import to fail here + raise gemato.exceptions.OpenPGPKeyRefreshError(err.decode('utf8')) + + return True + + def refresh_keys_keyserver(self): exitst, out, err = self._spawn_gpg(['--refresh-keys'], '') if exitst != 0: raise gemato.exceptions.OpenPGPKeyRefreshError(err.decode('utf8')) + def refresh_keys(self, allow_wkd=True): + if allow_wkd and self.refresh_keys_wkd(): + return + + self.refresh_keys_keyserver() + @property def home(self): assert self._home is not None