summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gemato/cli.py8
-rw-r--r--gemato/openpgp.py93
2 files changed, 97 insertions, 4 deletions
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 @@ class VerifyingOpenPGPMixin(BaseOpenPGPMixin):
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 @@ class VerifyingOpenPGPMixin(BaseOpenPGPMixin):
# 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 @@ class OpenPGPSystemEnvironment(object):
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 @@ disable-scdaemon
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