#! @PYTHON@ # -*-python-*- ### External dependencies import catacomb as C import os as OS import sys as SYS import sre as RX import getopt as O from cStringIO import StringIO from errno import * from stat import * ### Useful regular expressions r_comment = RX.compile(r'^\s*(#|$)') r_keyval = RX.compile(r'^\s*([-\w]+)(?:\s+(?!=)|\s*=\s*)(|\S|\S.*\S)\s*$') r_dollarsubst = RX.compile(r'\$\{([-\w]+)\}') r_atsubst = RX.compile(r'@([-\w]+)@') r_nonalpha = RX.compile(r'\W') ### 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.stat(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, r_dollarsubst, conf) ## Read the file def conf_read(f): lno = 0 for line in file(f): lno += 1 if r_comment.match(line): continue if line[-1] == '\n': line = line[:-1] match = r_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 [('sig-url', '${base-url}tripe-keys.sig'), ('repos-url', '${base-url}tripe-keys.tar.gz'), ('sig-file', '${base-dir}tripe-keys.sig'), ('repos-file', '${base-dir}tripe-keys.tar.gz'), ('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 cmd_setup(args): OS.mkdir('repos') ## Generate the master key run('''key -kmaster add -a${sig-genalg} !${sig-param} -e${sig-expire} -l -ttripe-keys-master ccsig sig=${sig} hash=${sig-hash}''') run('key -kmaster extract -f-secret repos/master.pub tripe-keys-master') ## Generate the parameters key run('''key -krepos/param add -a${kx}-param !${kx-param} -eforever -tparam tripe-${kx}-param cipher=${cipher} hash=${hash} mac=${mac} mgf=${mgf}''') ## Get fingerprints print 'Setup OK: master key = %s' % \ hexhyphens(fingerprint('repos/master.pub', 'tripe-keys-master')) 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.endswith('.old'): OS.unlink(ff) continue OS.chmod(OS.path.join('repos', f), mode) ## Build the configuration file v = {'HK-MASTER': hexhyphens(fingerprint('repos/master.pub', 'tripe-keys-master'))} fin = file('tripe-keys.master') fout = file(conf_subst('${conf-file}.new'), 'w') for line in fin: fout.write(subst(line, r_atsubst, v)) fin.close(); fout.close() ## Make and sign the repository archive run('tar chozf ${repos-file}.new repos') run('''catsign -kmaster sign -abdC -ktripe-keys-master -o${sig-file}.new ${repos-file}.new''') ## Commit the changes for i in ['conf-file', 'repos-file', 'sig-file']: base = conf[i] new = '%s.new' % base OS.rename(new, base) def cmd_update(args): cwd = OS.getcwd() rmtree('tmp') try: ## Fetch a new distribution OS.mkdir('tmp') OS.chdir('tmp') run('wget -q -O tripe-keys.tar.gz ${repos-url}') run('wget -q -O tripe-keys.sig ${sig-url}') run('tar xfz tripe-keys.tar.gz') ## Verify the signature want = C.bytes(r_nonalpha.sub('', conf['hk-master'])) got = fingerprint('repos/master.pub', 'tripe-keys-master') if want != got: raise VerifyError run('''catsign -krepos/master.pub verify -avC -ktripe-keys-master -t${sig-fresh} tripe-keys.sig tripe-keys.tar.gz''') ## OK: update our copy OS.chdir(cwd) if OS.path.exists('repos'): OS.rename('repos', 'repos.old') OS.rename('tmp/repos', 'repos') 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)) print 'Generated %s key = %s' % \ (tag, hexhyphens(fingerprint('repos/master.pub', 'tripe-keys-master'))) def cmd_clean(args): rmtree('repos') rmtree('tmp') for i in 'master', 'keyring.pub': zap(i) zap('%s.old' % i) ### Main driver class UsageError (Exception): pass commands = {'help': (cmd_help, 0, 1, ''), '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)