Skip to content

Commit

Permalink
openpgp: Support key refresh via WKD
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
mgorny committed Jul 24, 2018
1 parent 83c7d51 commit 909390c
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 4 deletions.
8 changes: 6 additions & 2 deletions gemato/cli.py
Expand Up @@ -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)
Expand All @@ -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.')


Expand Down
93 changes: 91 additions & 2 deletions gemato/openpgp.py
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 909390c

Please sign in to comment.