chiark / gitweb /
One that got away.
[tripe] / tripe-keys.in
1 #! @PYTHON@
2 # -*-python-*-
3
4 ### External dependencies
5
6 import catacomb as C
7 import os as OS
8 import sys as SYS
9 import sre as RX
10 import getopt as O
11 from cStringIO import StringIO
12 from errno import *
13 from stat import *
14
15 ### Useful regular expressions
16
17 r_comment = RX.compile(r'^\s*(#|$)')
18 r_keyval = RX.compile(r'^\s*([-\w]+)(?:\s+(?!=)|\s*=\s*)(|\S|\S.*\S)\s*$')
19 r_dollarsubst = RX.compile(r'\$\{([-\w]+)\}')
20 r_atsubst = RX.compile(r'@([-\w]+)@')
21 r_nonalpha = RX.compile(r'\W')
22
23 ### Utility functions
24
25 class SubprocessError (Exception): pass
26 class VerifyError (Exception): pass
27
28 quis = OS.path.basename(SYS.argv[0])
29 PACKAGE = "@PACKAGE@"
30 VERSION = "@VERSION@"
31
32 def moan(msg):
33   SYS.stderr.write('%s: %s\n' % (quis, msg))
34
35 def die(msg, rc = 1):
36   moan(msg)
37   SYS.exit(rc)
38
39 def subst(s, rx, map):
40   out = StringIO()
41   i = 0
42   for m in rx.finditer(s):
43     out.write(s[i:m.start()] + map[m.group(1)])
44     i = m.end()
45   out.write(s[i:])
46   return out.getvalue()
47
48 def rmtree(path):
49   try:
50     st = OS.stat(path)
51   except OSError, err:
52     if err.errno == ENOENT:
53       return
54     raise
55   if not S_ISDIR(st.st_mode):
56     OS.unlink(path)
57   else:
58     cwd = OS.getcwd()
59     try:
60       OS.chdir(path)
61       for i in OS.listdir('.'):
62         rmtree(i)
63     finally:
64       OS.chdir(cwd)
65     OS.rmdir(path)
66
67 def zap(file):
68   try:
69     OS.unlink(file)
70   except OSError, err:
71     if err.errno == ENOENT: return
72     raise
73
74 def run(args):
75   args = map(conf_subst, args.split())
76   nargs = []
77   for a in args:
78     if len(a) > 0 and a[0] != '!':
79       nargs += [a]
80     else:
81       nargs += a[1:].split()
82   args = nargs
83   print '+ %s' % ' '.join(args)
84   rc = OS.spawnvp(OS.P_WAIT, args[0], args)
85   if rc != 0:
86     raise SubprocessError, rc
87
88 def hexhyphens(bytes):
89   out = StringIO()
90   for i in xrange(0, len(bytes)):
91     if i > 0 and i % 4 == 0: out.write('-')
92     out.write('%02x' % ord(bytes[i]))
93   return out.getvalue()
94
95 def fingerprint(kf, ktag):
96   h = C.gchashes[conf['fingerprint-hash']]()
97   k = C.KeyFile(kf)[ktag].fingerprint(h, '-secret')
98   return h.done()
99
100 ### Read configuration
101
102 class ConfigFileError (Exception): pass
103 conf = {}
104
105 def conf_subst(s): return subst(s, r_dollarsubst, conf)
106
107 ## Read the file
108 def conf_read(f):
109   lno = 0
110   for line in file(f):
111     lno += 1
112     if r_comment.match(line): continue
113     if line[-1] == '\n': line = line[:-1]
114     match = r_keyval.match(line)
115     if not match:
116       raise ConfigFileError, "%s:%d: bad line `%s'" % (f, lno, line)
117     k, v = match.groups()
118     conf[k] = conf_subst(v)
119
120 ## Sift the wreckage
121 def conf_defaults():
122   for k, v in [('sig-url', '${base-url}tripe-keys.sig'),
123                ('repos-url', '${base-url}tripe-keys.tar.gz'),
124                ('sig-file', '${base-dir}tripe-keys.sig'),
125                ('repos-file', '${base-dir}tripe-keys.tar.gz'),
126                ('conf-file', '${base-dir}tripe-keys.conf'),
127                ('kx', 'dh'),
128                ('kx-param', lambda: {'dh': '-LS -b2048 -B256',
129                                      'ec': '-Cnist-p256'}[conf['kx']]),
130                ('kx-expire', 'now + 1 year'),
131                ('cipher', 'blowfish-cbc'),
132                ('hash', 'sha256'),
133                ('mgf', '${hash}-mgf'),
134                ('mac', lambda: '%s-hmac/%d' %
135                          (conf['hash'],
136                           C.gchashes[conf['hash']].hashsz * 4)),
137                ('sig', lambda: {'dh': 'dsa', 'ec': 'ecdsa'}[conf['kx']]),
138                ('sig-fresh', 'always'),
139                ('sig-genalg', lambda: {'kcdsa': 'dh',
140                                        'dsa': 'dsa',
141                                        'rsapkcs1': 'rsa',
142                                        'rsapss': 'rsa',
143                                        'ecdsa': 'ec',
144                                        'eckcdsa': 'ec'}[conf['sig']]),
145                ('sig-param', lambda: {'dh': '-LS -b2048 -B256',
146                                       'dsa': '-b2048 -B256',
147                                       'ec': '-Cnist-p256',
148                                       'rsa': '-b2048'}[conf['sig-genalg']]),
149                ('sig-hash', '${hash}'),
150                ('sig-expire', 'forever'),
151                ('fingerprint-hash', '${hash}')]:
152     try:
153       if k in conf: continue
154       if type(v) == str:
155         conf[k] = conf_subst(v)
156       else:
157         conf[k] = v()
158     except KeyError, exc:
159       if len(exc.args) == 0: raise
160       conf[k] = '<missing-var %s>' % exc.args[0]
161
162 ### Commands
163
164 def version(fp = SYS.stdout):
165   fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
166
167 def usage(fp):
168   fp.write('Usage: %s SUBCOMMAND [ARGS...]\n' % quis)
169
170 def cmd_help(args):
171   if len(args) == 0:
172     version(SYS.stdout)
173     print
174     usage(SYS.stdout)
175     print """
176 Key management utility for TrIPE.
177
178 Options supported:
179
180 -h, --help              Show this help message.
181 -v, --version           Show the version number.
182 -u, --usage             Show pointlessly short usage string.
183
184 Subcommands available:
185 """
186     args = commands.keys()
187     args.sort()
188   for c in args:
189     func, min, max, help = commands[c]
190     print '%s %s' % (c, help)
191
192 def cmd_setup(args):
193   OS.mkdir('repos')
194
195   ## Generate the master key
196   run('''key -kmaster add
197     -a${sig-genalg} !${sig-param}
198     -e${sig-expire} -l -ttripe-keys-master ccsig
199     sig=${sig} hash=${sig-hash}''')
200   run('key -kmaster extract -f-secret repos/master.pub tripe-keys-master')
201
202   ## Generate the parameters key
203   run('''key -krepos/param add
204     -a${kx}-param !${kx-param}
205     -eforever -tparam tripe-${kx}-param
206     cipher=${cipher} hash=${hash} mac=${mac} mgf=${mgf}''')
207
208   ## Get fingerprints
209   print 'Setup OK: master key = %s' % \
210         hexhyphens(fingerprint('repos/master.pub', 'tripe-keys-master'))
211
212 def cmd_upload(args):
213
214   ## Sanitize the repository directory
215   umask = OS.umask(0); OS.umask(umask)
216   mode = 0666 & ~umask
217   for f in OS.listdir('repos'):
218     ff = OS.path.join('repos', f)
219     if f.endswith('.old'):
220       OS.unlink(ff)
221       continue
222     OS.chmod(OS.path.join('repos', f), mode)
223
224   ## Build the configuration file
225   v = {'HK-MASTER': hexhyphens(fingerprint('repos/master.pub',
226                                            'tripe-keys-master'))}
227   fin = file('tripe-keys.master')
228   fout = file(conf_subst('${conf-file}.new'), 'w')
229   for line in fin:
230     fout.write(subst(line, r_atsubst, v))
231   fin.close(); fout.close()
232
233   ## Make and sign the repository archive
234   run('tar chozf ${repos-file}.new repos')
235   run('''catsign -kmaster sign -abdC -ktripe-keys-master
236     -o${sig-file}.new ${repos-file}.new''')
237
238   ## Commit the changes
239   for i in ['conf-file', 'repos-file', 'sig-file']:
240     base = conf[i]
241     new = '%s.new' % base
242     OS.rename(new, base)
243
244 def cmd_update(args):
245   cwd = OS.getcwd()
246   rmtree('tmp')
247   try:
248
249     ## Fetch a new distribution
250     OS.mkdir('tmp')
251     OS.chdir('tmp')
252     run('wget -q -O tripe-keys.tar.gz ${repos-url}')
253     run('wget -q -O tripe-keys.sig ${sig-url}')
254     run('tar xfz tripe-keys.tar.gz')
255
256     ## Verify the signature
257     want = C.bytes(r_nonalpha.sub('', conf['hk-master']))
258     got = fingerprint('repos/master.pub', 'tripe-keys-master')
259     if want != got: raise VerifyError
260     run('''catsign -krepos/master.pub verify -avC -ktripe-keys-master
261       -t${sig-fresh} tripe-keys.sig tripe-keys.tar.gz''')
262
263     ## OK: update our copy
264     OS.chdir(cwd)
265     if OS.path.exists('repos'): OS.rename('repos', 'repos.old')
266     OS.rename('tmp/repos', 'repos')
267     rmtree('repos.old')
268
269   finally:
270     OS.chdir(cwd)
271     rmtree('tmp')
272   cmd_rebuild(args)
273
274 def cmd_rebuild(args):
275   zap('keyring.pub')
276   for i in OS.listdir('repos'):
277     if i.startswith('peer-') and i.endswith('.pub'):
278       run('key -kkeyring.pub merge %s' % OS.path.join('repos', i))
279
280 def cmd_generate(args):
281   tag, = args
282   keyring_pub = 'peer-%s.pub' % tag
283   zap('keyring'); zap(keyring_pub)
284   run('key -kkeyring merge repos/param')
285   run('key -kkeyring add -a${kx} -pparam -e${kx-expire} -t%s tripe-${kx}' %
286       (tag,))
287   run('key -kkeyring extract -f-secret %s %s' % (keyring_pub, tag))
288   print 'Generated %s key = %s' % \
289         (tag,
290          hexhyphens(fingerprint('repos/master.pub', 'tripe-keys-master')))
291   
292
293 def cmd_clean(args):
294   rmtree('repos')
295   rmtree('tmp')
296   for i in 'master', 'keyring.pub':
297     zap(i)
298     zap('%s.old' % i)
299
300 ### Main driver
301
302 class UsageError (Exception): pass
303   
304 commands = {'help': (cmd_help, 0, 1, ''),
305             'setup': (cmd_setup, 0, 0, ''),
306             'upload': (cmd_upload, 0, 0, ''),
307             'update': (cmd_update, 0, 0, ''),
308             'clean': (cmd_clean, 0, 0, ''),
309             'generate': (cmd_generate, 1, 1, 'TAG'),
310             'rebuild': (cmd_rebuild, 0, 0, '')}
311
312 def init():
313   for f in ['tripe-keys.master', 'tripe-keys.conf']:
314     if OS.path.exists(f):
315       conf_read(f)
316       break
317   conf_defaults()
318 def main(argv):
319   try:
320     opts, args = O.getopt(argv[1:], 'hvu',
321                           ['help', 'version', 'usage'])
322   except O.GetoptError, exc:
323     moan(exc)
324     usage(SYS.stderr)
325     SYS.exit(1)
326   for o, v in opts:
327     if o in ('-h', '--help'):
328       cmd_help([])
329       SYS.exit(0)
330     elif o in ('-v', '--version'):
331       version(SYS.stdout)
332       SYS.exit(0)
333     elif o in ('-u', '--usage'):
334       usage(SYS.stdout)
335       SYS.exit(0)
336   if len(argv) < 2:
337     cmd_help([])
338   else:
339     c = argv[1]
340     func, min, max, help = commands[c]
341     args = argv[2:]
342     if len(args) < min or (max > 0 and len(args) > max):
343       raise UsageError, (c, help)
344     func(args)
345
346 init()
347 main(SYS.argv)