summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichał Górny <mgorny@gentoo.org>2018-07-23 23:51:07 +0200
committerMichał Górny <mgorny@gentoo.org>2018-07-24 08:41:52 +0200
commit909390c25a0ab589a4ae10d20cb9e321a51163b2 (patch)
tree31872883f0995ba0afd9e6bba9ce8e12365fc72a
parent83c7d51ba45d362f344d0ec0d680604bdfa71121 (diff)
downloadgemato-909390c25a0ab589a4ae10d20cb9e321a51163b2.tar.gz
openpgp: Support key refresh via WKD
Add support for using WKD to refetch keys instead of keyservers. This is experimental but should be more reliable and provide similar level of security (provided that we require that all keys can be fetched via WKD).
-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