X-Git-Url: https://git.distorted.org.uk/~mdw/tripe/blobdiff_plain/e6b06b6b61b4b877937d4a56ba704d3f18154dc2..786989941b7b4504f0234c4a318f929802e981ad:/keys/tripe-keys.in diff --git a/keys/tripe-keys.in b/keys/tripe-keys.in new file mode 100644 index 00000000..6da6eb4a --- /dev/null +++ b/keys/tripe-keys.in @@ -0,0 +1,391 @@ +#! @PYTHON@ +# -*-python-*- + +### External dependencies + +import catacomb as C +import os as OS +import sys as SYS +import sre as RX +import getopt as O +import shutil as SH +import filecmp as FC +from cStringIO import StringIO +from errno import * +from stat import * + +### Useful regular expressions + +rx_comment = RX.compile(r'^\s*(#|$)') +rx_keyval = RX.compile(r'^\s*([-\w]+)(?:\s+(?!=)|\s*=\s*)(|\S|\S.*\S)\s*$') +rx_dollarsubst = RX.compile(r'\$\{([-\w]+)\}') +rx_atsubst = RX.compile(r'@([-\w]+)@') +rx_nonalpha = RX.compile(r'\W') +rx_seq = RX.compile(r'\') + +### Utility functions + +class SubprocessError (Exception): pass +class VerifyError (Exception): pass + +quis = OS.path.basename(SYS.argv[0]) +PACKAGE = "@PACKAGE@" +VERSION = "@VERSION@" + +def moan(msg): + SYS.stderr.write('%s: %s\n' % (quis, msg)) + +def die(msg, rc = 1): + moan(msg) + SYS.exit(rc) + +def subst(s, rx, map): + out = StringIO() + i = 0 + for m in rx.finditer(s): + out.write(s[i:m.start()] + map[m.group(1)]) + i = m.end() + out.write(s[i:]) + return out.getvalue() + +def rmtree(path): + try: + st = OS.lstat(path) + except OSError, err: + if err.errno == ENOENT: + return + raise + if not S_ISDIR(st.st_mode): + OS.unlink(path) + else: + cwd = OS.getcwd() + try: + OS.chdir(path) + for i in OS.listdir('.'): + rmtree(i) + finally: + OS.chdir(cwd) + OS.rmdir(path) + +def zap(file): + try: + OS.unlink(file) + except OSError, err: + if err.errno == ENOENT: return + raise + +def run(args): + args = map(conf_subst, args.split()) + nargs = [] + for a in args: + if len(a) > 0 and a[0] != '!': + nargs += [a] + else: + nargs += a[1:].split() + args = nargs + print '+ %s' % ' '.join(args) + rc = OS.spawnvp(OS.P_WAIT, args[0], args) + if rc != 0: + raise SubprocessError, rc + +def hexhyphens(bytes): + out = StringIO() + for i in xrange(0, len(bytes)): + if i > 0 and i % 4 == 0: out.write('-') + out.write('%02x' % ord(bytes[i])) + return out.getvalue() + +def fingerprint(kf, ktag): + h = C.gchashes[conf['fingerprint-hash']]() + k = C.KeyFile(kf)[ktag].fingerprint(h, '-secret') + return h.done() + +### Read configuration + +class ConfigFileError (Exception): pass +conf = {} + +def conf_subst(s): return subst(s, rx_dollarsubst, conf) + +## Read the file +def conf_read(f): + lno = 0 + for line in file(f): + lno += 1 + if rx_comment.match(line): continue + if line[-1] == '\n': line = line[:-1] + match = rx_keyval.match(line) + if not match: + raise ConfigFileError, "%s:%d: bad line `%s'" % (f, lno, line) + k, v = match.groups() + conf[k] = conf_subst(v) + +## Sift the wreckage +def conf_defaults(): + for k, v in [('repos-base', 'tripe-keys.tar.gz'), + ('sig-base', 'tripe-keys.sig-'), + ('repos-url', '${base-url}${repos-base}'), + ('sig-url', '${base-url}${sig-base}'), + ('sig-file', '${base-dir}${sig-base}'), + ('repos-file', '${base-dir}${repos-base}'), + ('conf-file', '${base-dir}tripe-keys.conf'), + ('kx', 'dh'), + ('kx-param', lambda: {'dh': '-LS -b2048 -B256', + 'ec': '-Cnist-p256'}[conf['kx']]), + ('kx-expire', 'now + 1 year'), + ('cipher', 'blowfish-cbc'), + ('hash', 'sha256'), + ('mgf', '${hash}-mgf'), + ('mac', lambda: '%s-hmac/%d' % + (conf['hash'], + C.gchashes[conf['hash']].hashsz * 4)), + ('sig', lambda: {'dh': 'dsa', 'ec': 'ecdsa'}[conf['kx']]), + ('sig-fresh', 'always'), + ('sig-genalg', lambda: {'kcdsa': 'dh', + 'dsa': 'dsa', + 'rsapkcs1': 'rsa', + 'rsapss': 'rsa', + 'ecdsa': 'ec', + 'eckcdsa': 'ec'}[conf['sig']]), + ('sig-param', lambda: {'dh': '-LS -b2048 -B256', + 'dsa': '-b2048 -B256', + 'ec': '-Cnist-p256', + 'rsa': '-b2048'}[conf['sig-genalg']]), + ('sig-hash', '${hash}'), + ('sig-expire', 'forever'), + ('fingerprint-hash', '${hash}')]: + try: + if k in conf: continue + if type(v) == str: + conf[k] = conf_subst(v) + else: + conf[k] = v() + except KeyError, exc: + if len(exc.args) == 0: raise + conf[k] = '' % exc.args[0] + +### Commands + +def version(fp = SYS.stdout): + fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION)) + +def usage(fp): + fp.write('Usage: %s SUBCOMMAND [ARGS...]\n' % quis) + +def cmd_help(args): + if len(args) == 0: + version(SYS.stdout) + print + usage(SYS.stdout) + print """ +Key management utility for TrIPE. + +Options supported: + +-h, --help Show this help message. +-v, --version Show the version number. +-u, --usage Show pointlessly short usage string. + +Subcommands available: +""" + args = commands.keys() + args.sort() + for c in args: + func, min, max, help = commands[c] + print '%s %s' % (c, help) + +def master_keys(): + if not OS.path.exists('master'): + return + for k in C.KeyFile('master'): + if (k.type != 'tripe-keys-master' or + k.expiredp or + not k.tag.startswith('master-')): + continue #?? + yield k +def master_sequence(k): + return int(k.tag[7:]) +def max_master_sequence(): + seq = -1 + for k in master_keys(): + q = master_sequence(k) + if q > seq: seq = q + return seq +def seqsubst(x, q): + return rx_seq.sub(str(q), conf[x]) + +def cmd_newmaster(args): + seq = max_master_sequence() + 1 + run('''key -kmaster add + -a${sig-genalg} !${sig-param} + -e${sig-expire} -l -tmaster-%d tripe-keys-master + sig=${sig} hash=${sig-hash}''' % seq) + run('key -kmaster extract -f-secret repos/master.pub') + +def cmd_setup(args): + OS.mkdir('repos') + run('''key -krepos/param add + -a${kx}-param !${kx-param} + -eforever -tparam tripe-${kx}-param + cipher=${cipher} hash=${hash} mac=${mac} mgf=${mgf}''') + cmd_newmaster(args) + +def cmd_upload(args): + + ## Sanitize the repository directory + umask = OS.umask(0); OS.umask(umask) + mode = 0666 & ~umask + for f in OS.listdir('repos'): + ff = OS.path.join('repos', f) + if (f.startswith('master') or f.startswith('peer-')) \ + and f.endswith('.old'): + OS.unlink(ff) + continue + OS.chmod(ff, mode) + + rmtree('tmp') + OS.mkdir('tmp') + OS.symlink('../repos', 'tmp/repos') + cwd = OS.getcwd() + try: + + ## Build the configuration file + seq = max_master_sequence() + v = {'MASTER-SEQUENCE': str(seq), + 'HK-MASTER': hexhyphens(fingerprint('repos/master.pub', + 'master-%d' % seq))} + fin = file('tripe-keys.master') + fout = file('tmp/tripe-keys.conf', 'w') + for line in fin: + fout.write(subst(line, rx_atsubst, v)) + fin.close(); fout.close() + SH.copyfile('tmp/tripe-keys.conf', conf_subst('${conf-file}.new')) + commit = [conf['repos-file'], conf['conf-file']] + + ## Make and sign the repository archive + OS.chdir('tmp') + run('tar chozf ${repos-file}.new .') + OS.chdir(cwd) + for k in master_keys(): + seq = master_sequence(k) + sigfile = seqsubst('sig-file', seq) + run('''catsign -kmaster sign -abdC -kmaster-%d + -o%s.new ${repos-file}.new''' % (seq, sigfile)) + commit.append(sigfile) + + ## Commit the changes + for base in commit: + new = '%s.new' % base + OS.rename(new, base) + finally: + OS.chdir(cwd) + rmtree('tmp') + +def cmd_update(args): + cwd = OS.getcwd() + rmtree('tmp') + try: + + ## Fetch a new distribution + OS.mkdir('tmp') + OS.chdir('tmp') + seq = int(conf['master-sequence']) + run('curl -s -o tripe-keys.tar.gz ${repos-url}') + run('curl -s -o tripe-keys.sig %s' % seqsubst('sig-url', seq)) + run('tar xfz tripe-keys.tar.gz') + + ## Verify the signature + want = C.bytes(rx_nonalpha.sub('', conf['hk-master'])) + got = fingerprint('repos/master.pub', 'master-%d' % seq) + if want != got: raise VerifyError + run('''catsign -krepos/master.pub verify -avC -kmaster-%d + -t${sig-fresh} tripe-keys.sig tripe-keys.tar.gz''' % seq) + + ## OK: update our copy + OS.chdir(cwd) + if OS.path.exists('repos'): OS.rename('repos', 'repos.old') + OS.rename('tmp/repos', 'repos') + if not FC.cmp('tmp/tripe-keys.conf', 'tripe-keys.conf'): + moan('configuration file changed: recommend running another update') + OS.rename('tmp/tripe-keys.conf', 'tripe-keys.conf') + rmtree('repos.old') + + finally: + OS.chdir(cwd) + rmtree('tmp') + cmd_rebuild(args) + +def cmd_rebuild(args): + zap('keyring.pub') + for i in OS.listdir('repos'): + if i.startswith('peer-') and i.endswith('.pub'): + run('key -kkeyring.pub merge %s' % OS.path.join('repos', i)) + +def cmd_generate(args): + tag, = args + keyring_pub = 'peer-%s.pub' % tag + zap('keyring'); zap(keyring_pub) + run('key -kkeyring merge repos/param') + run('key -kkeyring add -a${kx} -pparam -e${kx-expire} -t%s tripe-${kx}' % + tag) + run('key -kkeyring extract -f-secret %s %s' % (keyring_pub, tag)) + +def cmd_clean(args): + rmtree('repos') + rmtree('tmp') + for i in OS.listdir('.'): + r = i + if r.endswith('.old'): r = r[:-4] + if (r == 'master' or r == 'param' or + r == 'keyring' or r == 'keyring.pub' or r.startswith('peer-')): + zap(i) + +### Main driver + +class UsageError (Exception): pass + +commands = {'help': (cmd_help, 0, 1, ''), + 'newmaster': (cmd_newmaster, 0, 0, ''), + 'setup': (cmd_setup, 0, 0, ''), + 'upload': (cmd_upload, 0, 0, ''), + 'update': (cmd_update, 0, 0, ''), + 'clean': (cmd_clean, 0, 0, ''), + 'generate': (cmd_generate, 1, 1, 'TAG'), + 'rebuild': (cmd_rebuild, 0, 0, '')} + +def init(): + for f in ['tripe-keys.master', 'tripe-keys.conf']: + if OS.path.exists(f): + conf_read(f) + break + conf_defaults() +def main(argv): + try: + opts, args = O.getopt(argv[1:], 'hvu', + ['help', 'version', 'usage']) + except O.GetoptError, exc: + moan(exc) + usage(SYS.stderr) + SYS.exit(1) + for o, v in opts: + if o in ('-h', '--help'): + cmd_help([]) + SYS.exit(0) + elif o in ('-v', '--version'): + version(SYS.stdout) + SYS.exit(0) + elif o in ('-u', '--usage'): + usage(SYS.stdout) + SYS.exit(0) + if len(argv) < 2: + cmd_help([]) + else: + c = argv[1] + func, min, max, help = commands[c] + args = argv[2:] + if len(args) < min or (max > 0 and len(args) > max): + raise UsageError, (c, help) + func(args) + +init() +main(SYS.argv)