From 0dfec99ce973a1ef82fba87351726c7c91270441 Mon Sep 17 00:00:00 2001 From: Mohan Boddu Date: Oct 31 2019 18:30:44 +0000 Subject: Adding a script to generate the fedora.gpg Signed-off-by: Mohan Boddu --- diff --git a/sites/getfedora.org/scripts/update-gpg-keys b/sites/getfedora.org/scripts/update-gpg-keys new file mode 100755 index 0000000..a77143d --- /dev/null +++ b/sites/getfedora.org/scripts/update-gpg-keys @@ -0,0 +1,298 @@ +#!/usr/bin/python3 + +"""Update gpg keys used on getfedora.org. This script helps automate the +process of maintaining the fedora key files used on the website. It manages +both the individual key files, stored as static/$KEYID.txt, and the full Fedora +keyblock, stored as static/fedora.gpg. To add or update individual keyfiles, +specify the path to a key, e.g. /etc/pki/rpm-gpg/RPM-GPG-KEY-fedora. Removing +keys may be done using the --remove (-r) option, specifying a path or keyid. +""" + +# TODO +# +# Allow removal of keys based on userid or release version (i.e. 10, to remove +# all Fedora 10 keys). +# +# Automatically download fedora-release packages and/or RPM-GPG-KEY-* files for +# supported releases and extract the keys from them, prompting to verify the +# keys, of course. + +import sys +assert sys.version_info >= (3,), "This script requires Python 3" + +import os +import re +import glob +import shutil +import optparse +import tempfile +from stat import * +from subprocess import Popen, PIPE, STDOUT + +# Obsolete keys +obsolete_keys = [ + '4F2A6FD2', # Fedora Project (original key, retired after 'the incident') + '30C9ECF8', # Fedora Project (Test Software) + '6DF2196F', # Fedora (8 and 9) + 'DF9B0AE9', # Fedora (8 and 9 testing) + '4EBFC273', # Fedora 10 + '0B86274E', # Fedora 10 (testing) + 'D22E77F2', # Fedora 11 + '57BBCCBA', # Fedora 12 + 'E8E40FDE', # Fedora 13 + '97A1071F', # Fedora 14 + 'FDB36B03', # Fedora 14 (s390x) + '069C8460', # Fedora 15 + '3AD31D0B', # Fedora SPARC + 'A82BA4B7', # Fedora 16 primary + '10D90A9E', # Fedora 16 secondary + '1ACA3465', # Fedora 17 primary + 'F8DF67E6', # Fedora 17 secondary + 'DE7F38BD', # Fedora 18 primary + 'A4D647E9', # Fedora 18 secondary + 'FB4B18E6', # Fedora 19 primary + 'BA094068', # Fedora 19 secondary + '246110C1', # Fedora 20 primary + 'EFE550F5', # Fedora 20 secondary + '95A43F54', # Fedora 21 primary + 'A0A7BADB', # Fedora 21 secondary + '8E1431D5', # Fedora 22 primary + 'A29CB19C', # Fedora 22 secondary + '34EC9CBA', # Fedora 23 primary + '873529B8', # Fedora 23 secondary + '81B46521', # Fedora 24 primary + '030D5AED', # Fedora 24 secondary + 'FDB19C98', # Fedora 25 primary + 'E372E838', # Fedora 25 secondary + '64DAB85D', # Fedora 26 primary + '3B921D09', # Fedora 26 secondary + 'F5282EE4', # Fedora 27 + '9DB62FB1', # Fedora 28 + '1AC70CE6', # Fedora Extras + '731002FA', # Fedora Legacy + '217521F6', # EPEL +] + +secondary_arches = ('secondary', 'arm', 'ia64', 'mips', 'parisc', 'ppc', 's390', 'sparc') + +# Match keyids +keyid_re = re.compile('^[0-9a-z]{8}$', re.IGNORECASE) + +# Match keyblocks +keyblock_begin = '-----BEGIN PGP PUBLIC KEY BLOCK-----' +keyblock_end = '-----END PGP PUBLIC KEY BLOCK-----' +keyblock_re = re.compile('.*(^%s\n.*%s)' % (keyblock_begin, keyblock_end), re.S|re.M) + +# Match '10 testing' in Fedora (10 testing) +version_re = re.compile('[^(]*\(([^)]*)\).*') + +class GpgError(EnvironmentError): + pass + +def get_keyinfo(path): + """Return some basic information about a gpg key.""" + cmd = ['gpg', '--with-colons', path] + p = Popen(cmd, stdout=PIPE, stderr=PIPE) + stdout, stderr = p.communicate() + stdout = stdout.decode('utf-8') + if p.returncode: + raise GpgError(-1, stderr) + for line in stdout.split('\n'): + if line.startswith('pub:'): + info = line.split(':') + keyid = info[4][8:] + if line.startswith('uid:'): + info = line.split(':') + userid = info[9] + return { 'keyid': keyid, 'userid': userid } + +def natsorted(l): + """ Sort the given list in the way that humans expect.""" + # Taken from http://nedbatchelder.com/blog/200712/human_sorting.html + convert = lambda text: int(text) if text.isdigit() else text + alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] + return sorted(l, key=alphanum_key) + +usage = '%prog [options] keyfile [keyfile...]' +parser = optparse.OptionParser(usage=usage, epilog=__doc__) +parser.add_option('-d', '--keydir', dest='keydir', + metavar='dir', default='../getfedora.org/static', + help='Location of fedora keyblock and key files [%default/]') +parser.add_option('-k', '--keyblock', dest='keyblock', + metavar='file', default='fedora.gpg', + help='Name of fedora keyblock [%default]') +parser.add_option('-f', '--fingerprint', dest='fingerprint', + action='store_true', default=False, + help='Add fingerprints to key files and output [%default]') +parser.add_option('--no-fingerprint', dest='fingerprint', + action='store_false', + help='Do not add fingerprints to key files and output') +parser.add_option('-r', '--remove', dest='rmkeys', action='append', + metavar='file|keyid', default=[], + help='Remove key (may be used more than once)') +parser.add_option('-v', '--verbose', dest='verbose', action='count', default=0, + help='Increase verbosity (may be used more than once)') +opts, args = parser.parse_args() + +# Ensure keydir exists and has reasonable perms +if os.path.isdir(opts.keydir): + if not os.access(opts.keydir, os.R_OK | os.W_OK | os.X_OK): + msg = 'read, write, and search perms are required for %s' % opts.keydir + raise SystemExit('Error: %s' % msg) +else: + if opts.verbose: + print('Creating keydir: %s' % opts.keydir) + try: + os.makedirs(opts.keydir) + except Exception as e: + raise SystemExit('Failed to create %s: %s' % (opts.keydir, str(e))) + +# Handle removal requests +for k in opts.rmkeys: + path = '' + if os.path.exists(k): + path = k + elif keyid_re.match(k): + f = os.path.join(opts.keydir, k.upper() + '.txt') + if os.path.exists(f): + path = f + else: + print('%s appears to be keyid, but %s not found' % (k, f)) + continue + else: + f = os.path.join(opts.keydir, k) + if os.path.exists(f): + path = f + else: + print('No matches found for %s' % k) + continue + if path: + try: + os.remove(path) + except Exception as e: + print('Failed to remove %s: %s' % (path, str(e))) + continue + print('Removed %s' % path) + +# Process any key files passed in as arguments +for f in args: + if not os.path.isfile(f): + print('%s is not a file, skipping...' % f) + continue + keydata = open(f).read() + m = keyblock_re.match(keydata) + if not m: + print('%s does not appear to be a gpg key file, skipping...' % f) + continue + keydata = m.group(1) + '\n' + try: + keyinfo = get_keyinfo(f) + except Exception as e: + print('Failed to get keyinfo for %s: %s' % (f, str(e))) + continue + keyfile = os.path.join(opts.keydir, '%s.txt' % keyinfo['keyid']) + cmd = ['gpg'] + if opts.fingerprint: + cmd.append('--with-fingerprint') + try: + p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + output, stderr = p.communicate(input=keydata.encode('utf-8')) + output = output.decode('utf-8') + if p.returncode: + raise GpgError(-1, stderr) + except Exception as e: + print('Failed to read %s: %s' % (f, str(e))) + continue + keydata = output + '\n' + keydata + if opts.verbose: + print('Adding key data to %s:\n%s' % (keyfile, output)) + kf = open(keyfile, 'w') + kf.write(keydata) + kf.close() + +# Update the fedora.gpg keyblock +keys = {} +for keyfile in glob.glob('%s/*.txt' % opts.keydir): + basename = os.path.splitext(os.path.basename(keyfile))[0] + if not keyid_re.match(basename): + if opts.verbose > 2: + print('Skipping %s: Does not match keyid regex\n' % keyfile) + continue + if opts.verbose: + print('Processing %s' % keyfile) + if keyblock_begin not in open(keyfile).read(): + if opts.verbose: + print(" %s doesn't start with %s\n" % (keyfile, keyblock_begin)) + continue + try: + keyinfo = get_keyinfo(keyfile) + except GpgError as e: + print('Skipping %s: %s' % (keyfile, str(e))) + if keyinfo['keyid'] in obsolete_keys: + if opts.verbose: + print(' Skipping: obsolete key\n') + continue + if 'EPEL' in keyinfo['userid']: + try: + version = 'epel-%s' % version_re.findall(keyinfo['userid'])[0] + except: + version = 'epel' + else: + try: + version = version_re.findall(keyinfo['userid'])[0] + except: + raise SystemExit('Unable to find version string for %s' % keyfile) + for arch in secondary_arches: + uid = keyinfo['userid'].lower() + if arch in uid and arch not in version.lower(): + if opts.verbose > 1: + print(' Adding %s to version' % arch) + version += '-%s' % arch + break + if opts.verbose > 1: + print(' userid = %s' % keyinfo['userid']) + print(' version = %s' % version) + keys[version] = (keyinfo['keyid'], keyfile) + if opts.verbose: + print() + +if not keys: + raise SystemExit('No keys were found') + +# Create a temporary homedir for gpg +gpgdir = tempfile.mkdtemp(prefix='gpg', dir='.') + +# Import key(s) to tmp keyring +cmd = ['gpg', '--homedir', gpgdir, '--quiet', '--import'] +cmd.extend([keys[k][1] for k in natsorted(list(keys.keys()))]) +try: + p = Popen(cmd, stdout=PIPE, stderr=PIPE) + stdout, stderr = p.communicate() + if p.returncode: + raise GpgError(-1, stderr) +except Exception as e: + shutil.rmtree(gpgdir) + raise SystemExit('Failed to import key(s): %s' % str(e)) + +# Export key(s) from tmp keyring +cmd = ['gpg', '--homedir', gpgdir, '--armor', '--export'] +try: + p = Popen(cmd, stdout=PIPE, stderr=PIPE) + stdout, stderr = p.communicate() + if p.returncode: + raise GpgError(-1, stderr) +except Exception as e: + shutil.rmtree(gpgdir) + raise SystemExit('Failed to export key(s): %s' % str(e)) + +# Remove tmp gpgdir +shutil.rmtree(gpgdir) + +# Write fedora keyblock +keyblock = os.path.join(opts.keydir, opts.keyblock) +try: + f = open(keyblock, 'wb') + f.write(stdout) + f.close() +except Exception as e: + print('Failed to write %s keyblock: %s' % (keyblock, str(e)))