chiark / gitweb /
maint-utils/keysubst: A monstrously unpleasant sed hack.
[tripe] / keys / tripe-keys.in
... / ...
CommitLineData
1#! @PYTHON@
2### -*-python-*-
3###
4### Key management and distribution
5###
6### (c) 2006 Straylight/Edgeware
7###
8
9###----- Licensing notice ---------------------------------------------------
10###
11### This file is part of Trivial IP Encryption (TrIPE).
12###
13### TrIPE is free software; you can redistribute it and/or modify
14### it under the terms of the GNU General Public License as published by
15### the Free Software Foundation; either version 2 of the License, or
16### (at your option) any later version.
17###
18### TrIPE is distributed in the hope that it will be useful,
19### but WITHOUT ANY WARRANTY; without even the implied warranty of
20### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21### GNU General Public License for more details.
22###
23### You should have received a copy of the GNU General Public License
24### along with TrIPE; if not, write to the Free Software Foundation,
25### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26
27###--------------------------------------------------------------------------
28### External dependencies.
29
30import catacomb as C
31import os as OS
32import sys as SYS
33import re as RX
34import getopt as O
35import shutil as SH
36import filecmp as FC
37from cStringIO import StringIO
38from errno import *
39from stat import *
40
41###--------------------------------------------------------------------------
42### Useful regular expressions
43
44## Match a comment or blank line.
45rx_comment = RX.compile(r'^\s*(#|$)')
46
47## Match a KEY = VALUE assignment.
48rx_keyval = RX.compile(r'^\s*([-\w]+)(?:\s+(?!=)|\s*=\s*)(|\S|\S.*\S)\s*$')
49
50## Match a ${KEY} substitution.
51rx_dollarsubst = RX.compile(r'\$\{([-\w]+)\}')
52
53## Match a @TAG@ substitution.
54rx_atsubst = RX.compile(r'@([-\w]+)@')
55
56## Match a single non-alphanumeric character.
57rx_nonalpha = RX.compile(r'\W')
58
59## Match the literal string "<SEQ>".
60rx_seq = RX.compile(r'\<SEQ\>')
61
62###--------------------------------------------------------------------------
63### Utility functions.
64
65## Exceptions.
66class SubprocessError (Exception): pass
67class VerifyError (Exception): pass
68
69## Program name and identification.
70quis = OS.path.basename(SYS.argv[0])
71PACKAGE = "@PACKAGE@"
72VERSION = "@VERSION@"
73
74def moan(msg):
75 """Report MSG to standard error."""
76 SYS.stderr.write('%s: %s\n' % (quis, msg))
77
78def die(msg, rc = 1):
79 """Report MSG to standard error, and exit with code RC."""
80 moan(msg)
81 SYS.exit(rc)
82
83def subst(s, rx, map):
84 """
85 Substitute values into a string.
86
87 Repeatedly match RX (a compiled regular expression) against the string S.
88 For each match, extract group 1, and use it as a key to index the MAP;
89 replace the match by the result. Finally, return the fully-substituted
90 string.
91 """
92 out = StringIO()
93 i = 0
94 for m in rx.finditer(s):
95 out.write(s[i:m.start()] + map[m.group(1)])
96 i = m.end()
97 out.write(s[i:])
98 return out.getvalue()
99
100def rmtree(path):
101 """Delete the directory tree given by PATH."""
102 try:
103 st = OS.lstat(path)
104 except OSError, err:
105 if err.errno == ENOENT:
106 return
107 raise
108 if not S_ISDIR(st.st_mode):
109 OS.unlink(path)
110 else:
111 cwd = OS.getcwd()
112 try:
113 OS.chdir(path)
114 for i in OS.listdir('.'):
115 rmtree(i)
116 finally:
117 OS.chdir(cwd)
118 OS.rmdir(path)
119
120def zap(file):
121 """Delete the named FILE if it exists; otherwise do nothing."""
122 try:
123 OS.unlink(file)
124 except OSError, err:
125 if err.errno == ENOENT: return
126 raise
127
128def run(args):
129 """
130 Run a subprocess whose arguments are given by the string ARGS.
131
132 The ARGS are split at word boundaries, and then subjected to configuration
133 variable substitution (see conf_subst). Individual argument elements
134 beginning with `!' are split again into multiple arguments at word
135 boundaries.
136 """
137 args = map(conf_subst, args.split())
138 nargs = []
139 for a in args:
140 if len(a) > 0 and a[0] != '!':
141 nargs += [a]
142 else:
143 nargs += a[1:].split()
144 args = nargs
145 print '+ %s' % ' '.join(args)
146 SYS.stdout.flush()
147 rc = OS.spawnvp(OS.P_WAIT, args[0], args)
148 if rc != 0:
149 raise SubprocessError, rc
150
151def hexhyphens(bytes):
152 """
153 Convert a byte string BYTES into hex, with hyphens at each 4-byte boundary.
154 """
155 out = StringIO()
156 for i in xrange(0, len(bytes)):
157 if i > 0 and i % 4 == 0: out.write('-')
158 out.write('%02x' % ord(bytes[i]))
159 return out.getvalue()
160
161def fingerprint(kf, ktag):
162 """
163 Compute the fingerprint of a key, using the user's selected hash.
164
165 KF is the name of a keyfile; KTAG is the tag of the key.
166 """
167 h = C.gchashes[conf['fingerprint-hash']]()
168 k = C.KeyFile(kf)[ktag].fingerprint(h, '-secret')
169 return h.done()
170
171###--------------------------------------------------------------------------
172### The configuration file.
173
174## Exceptions.
175class ConfigFileError (Exception): pass
176
177## The configuration dictionary.
178conf = {}
179
180def conf_subst(s):
181 """
182 Apply configuration substitutions to S.
183
184 That is, for each ${KEY} in S, replace it with the current value of the
185 configuration variable KEY.
186 """
187 return subst(s, rx_dollarsubst, conf)
188
189def conf_read(f):
190 """
191 Read the file F and insert assignments into the configuration dictionary.
192 """
193 lno = 0
194 for line in file(f):
195 lno += 1
196 if rx_comment.match(line): continue
197 if line[-1] == '\n': line = line[:-1]
198 match = rx_keyval.match(line)
199 if not match:
200 raise ConfigFileError, "%s:%d: bad line `%s'" % (f, lno, line)
201 k, v = match.groups()
202 conf[k] = conf_subst(v)
203
204def conf_defaults():
205 """
206 Apply defaults to the configuration dictionary.
207
208 Fill in all the interesting configuration variables based on the existing
209 contents, as described in the manual.
210 """
211 for k, v in [('repos-base', 'tripe-keys.tar.gz'),
212 ('sig-base', 'tripe-keys.sig-<SEQ>'),
213 ('repos-url', '${base-url}${repos-base}'),
214 ('sig-url', '${base-url}${sig-base}'),
215 ('sig-file', '${base-dir}${sig-base}'),
216 ('repos-file', '${base-dir}${repos-base}'),
217 ('conf-file', '${base-dir}tripe-keys.conf'),
218 ('upload-hook', ': run upload hook'),
219 ('kx', 'dh'),
220 ('kx-param', lambda: {'dh': '-LS -b2048 -B256',
221 'ec': '-Cnist-p256'}[conf['kx']]),
222 ('kx-expire', 'now + 1 year'),
223 ('cipher', 'blowfish-cbc'),
224 ('hash', 'sha256'),
225 ('master-keygen-flags', '-l'),
226 ('mgf', '${hash}-mgf'),
227 ('mac', lambda: '%s-hmac/%d' %
228 (conf['hash'],
229 C.gchashes[conf['hash']].hashsz * 4)),
230 ('sig', lambda: {'dh': 'dsa', 'ec': 'ecdsa'}[conf['kx']]),
231 ('sig-fresh', 'always'),
232 ('sig-genalg', lambda: {'kcdsa': 'dh',
233 'dsa': 'dsa',
234 'rsapkcs1': 'rsa',
235 'rsapss': 'rsa',
236 'ecdsa': 'ec',
237 'eckcdsa': 'ec'}[conf['sig']]),
238 ('sig-param', lambda: {'dh': '-LS -b2048 -B256',
239 'dsa': '-b2048 -B256',
240 'ec': '-Cnist-p256',
241 'rsa': '-b2048'}[conf['sig-genalg']]),
242 ('sig-hash', '${hash}'),
243 ('sig-expire', 'forever'),
244 ('fingerprint-hash', '${hash}')]:
245 try:
246 if k in conf: continue
247 if type(v) == str:
248 conf[k] = conf_subst(v)
249 else:
250 conf[k] = v()
251 except KeyError, exc:
252 if len(exc.args) == 0: raise
253 conf[k] = '<missing-var %s>' % exc.args[0]
254
255###--------------------------------------------------------------------------
256### Key-management utilities.
257
258def master_keys():
259 """
260 Iterate over the master keys.
261 """
262 if not OS.path.exists('master'):
263 return
264 for k in C.KeyFile('master').itervalues():
265 if (k.type != 'tripe-keys-master' or
266 k.expiredp or
267 not k.tag.startswith('master-')):
268 continue #??
269 yield k
270
271def master_sequence(k):
272 """
273 Return the sequence number of the given master key as an integer.
274
275 No checking is done that K is really a master key.
276 """
277 return int(k.tag[7:])
278
279def max_master_sequence():
280 """
281 Find the master key with the highest sequence number and return this
282 sequence number.
283 """
284 seq = -1
285 for k in master_keys():
286 q = master_sequence(k)
287 if q > seq: seq = q
288 return seq
289
290def seqsubst(x, q):
291 """
292 Return the value of the configuration variable X, with <SEQ> replaced by
293 the value Q.
294 """
295 return rx_seq.sub(str(q), conf[x])
296
297###--------------------------------------------------------------------------
298### Commands: help [COMMAND...]
299
300def version(fp = SYS.stdout):
301 fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
302
303def usage(fp):
304 fp.write('Usage: %s SUBCOMMAND [ARGS...]\n' % quis)
305
306def cmd_help(args):
307 if len(args) == 0:
308 version(SYS.stdout)
309 print
310 usage(SYS.stdout)
311 print """
312Key management utility for TrIPE.
313
314Options supported:
315
316-h, --help Show this help message.
317-v, --version Show the version number.
318-u, --usage Show pointlessly short usage string.
319
320Subcommands available:
321"""
322 args = commands.keys()
323 args.sort()
324 for c in args:
325 func, min, max, help = commands[c]
326 print '%s %s' % (c, help)
327
328###--------------------------------------------------------------------------
329### Commands: newmaster
330
331def cmd_newmaster(args):
332 seq = max_master_sequence() + 1
333 run('''key -kmaster add
334 -a${sig-genalg} !${sig-param}
335 -e${sig-expire} !${master-keygen-flags} -tmaster-%d tripe-keys-master
336 sig=${sig} hash=${sig-hash}''' % seq)
337 run('key -kmaster extract -f-secret repos/master.pub')
338
339###--------------------------------------------------------------------------
340### Commands: setup
341
342def cmd_setup(args):
343 OS.mkdir('repos')
344 run('''key -krepos/param add
345 -a${kx}-param !${kx-param}
346 -eforever -tparam tripe-${kx}-param
347 cipher=${cipher} hash=${hash} mac=${mac} mgf=${mgf}''')
348 cmd_newmaster(args)
349
350###--------------------------------------------------------------------------
351### Commands: upload
352
353def cmd_upload(args):
354
355 ## Sanitize the repository directory
356 umask = OS.umask(0); OS.umask(umask)
357 mode = 0666 & ~umask
358 for f in OS.listdir('repos'):
359 ff = OS.path.join('repos', f)
360 if (f.startswith('master') or f.startswith('peer-')) \
361 and f.endswith('.old'):
362 OS.unlink(ff)
363 continue
364 OS.chmod(ff, mode)
365
366 rmtree('tmp')
367 OS.mkdir('tmp')
368 OS.symlink('../repos', 'tmp/repos')
369 cwd = OS.getcwd()
370 try:
371
372 ## Build the configuration file
373 seq = max_master_sequence()
374 v = {'MASTER-SEQUENCE': str(seq),
375 'HK-MASTER': hexhyphens(fingerprint('repos/master.pub',
376 'master-%d' % seq))}
377 fin = file('tripe-keys.master')
378 fout = file('tmp/tripe-keys.conf', 'w')
379 for line in fin:
380 fout.write(subst(line, rx_atsubst, v))
381 fin.close(); fout.close()
382 SH.copyfile('tmp/tripe-keys.conf', conf_subst('${conf-file}.new'))
383 commit = [conf['repos-file'], conf['conf-file']]
384
385 ## Make and sign the repository archive
386 OS.chdir('tmp')
387 run('tar chozf ${repos-file}.new .')
388 OS.chdir(cwd)
389 for k in master_keys():
390 seq = master_sequence(k)
391 sigfile = seqsubst('sig-file', seq)
392 run('''catsign -kmaster sign -abdC -kmaster-%d
393 -o%s.new ${repos-file}.new''' % (seq, sigfile))
394 commit.append(sigfile)
395
396 ## Commit the changes
397 for base in commit:
398 new = '%s.new' % base
399 OS.rename(new, base)
400 finally:
401 OS.chdir(cwd)
402 rmtree('tmp')
403 run('sh -c ${upload-hook}')
404
405###--------------------------------------------------------------------------
406### Commands: rebuild
407
408def cmd_rebuild(args):
409 zap('keyring.pub')
410 for i in OS.listdir('repos'):
411 if i.startswith('peer-') and i.endswith('.pub'):
412 run('key -kkeyring.pub merge %s' % OS.path.join('repos', i))
413
414###--------------------------------------------------------------------------
415### Commands: update
416
417def cmd_update(args):
418 cwd = OS.getcwd()
419 rmtree('tmp')
420 try:
421
422 ## Fetch a new distribution
423 OS.mkdir('tmp')
424 OS.chdir('tmp')
425 seq = int(conf['master-sequence'])
426 run('curl -s -o tripe-keys.tar.gz ${repos-url}')
427 run('curl -s -o tripe-keys.sig %s' % seqsubst('sig-url', seq))
428 run('tar xfz tripe-keys.tar.gz')
429
430 ## Verify the signature
431 want = C.bytes(rx_nonalpha.sub('', conf['hk-master']))
432 got = fingerprint('repos/master.pub', 'master-%d' % seq)
433 if want != got: raise VerifyError
434 run('''catsign -krepos/master.pub verify -avC -kmaster-%d
435 -t${sig-fresh} tripe-keys.sig tripe-keys.tar.gz''' % seq)
436
437 ## OK: update our copy
438 OS.chdir(cwd)
439 if OS.path.exists('repos'): OS.rename('repos', 'repos.old')
440 OS.rename('tmp/repos', 'repos')
441 if not FC.cmp('tmp/tripe-keys.conf', 'tripe-keys.conf'):
442 moan('configuration file changed: recommend running another update')
443 OS.rename('tmp/tripe-keys.conf', 'tripe-keys.conf')
444 rmtree('repos.old')
445
446 finally:
447 OS.chdir(cwd)
448 rmtree('tmp')
449 cmd_rebuild(args)
450
451###--------------------------------------------------------------------------
452### Commands: generate TAG
453
454def cmd_generate(args):
455 tag, = args
456 keyring_pub = 'peer-%s.pub' % tag
457 zap('keyring'); zap(keyring_pub)
458 run('key -kkeyring merge repos/param')
459 run('key -kkeyring add -a${kx} -pparam -e${kx-expire} -t%s tripe-${kx}' %
460 tag)
461 run('key -kkeyring extract -f-secret %s %s' % (keyring_pub, tag))
462
463###--------------------------------------------------------------------------
464### Commands: clean
465
466def cmd_clean(args):
467 rmtree('repos')
468 rmtree('tmp')
469 for i in OS.listdir('.'):
470 r = i
471 if r.endswith('.old'): r = r[:-4]
472 if (r == 'master' or r == 'param' or
473 r == 'keyring' or r == 'keyring.pub' or r.startswith('peer-')):
474 zap(i)
475
476###--------------------------------------------------------------------------
477### Commands: mtu
478
479def cmd_mtu(args):
480 mtu, = (lambda mtu = '1500': (mtu,))(*args)
481 mtu = int(mtu)
482
483 blksz = C.gcciphers[conf['cipher']].blksz
484
485 index = conf['mac'].find('/')
486 if index == -1:
487 tagsz = C.gcmacs[conf['mac']].tagsz
488 else:
489 tagsz = int(conf['mac'][index + 1:])/8
490
491 mtu -= 20 # Minimum IP header
492 mtu -= 8 # UDP header
493 mtu -= 1 # TrIPE packet type octet
494 mtu -= tagsz # MAC tag
495 mtu -= 4 # Sequence number
496 mtu -= blksz # Initialization vector
497
498 print mtu
499
500###--------------------------------------------------------------------------
501### Main driver.
502
503## Exceptions.
504class UsageError (Exception): pass
505
506commands = {'help': (cmd_help, 0, 1, ''),
507 'newmaster': (cmd_newmaster, 0, 0, ''),
508 'setup': (cmd_setup, 0, 0, ''),
509 'upload': (cmd_upload, 0, 0, ''),
510 'update': (cmd_update, 0, 0, ''),
511 'clean': (cmd_clean, 0, 0, ''),
512 'mtu': (cmd_mtu, 0, 1, '[PATH-MTU]'),
513 'generate': (cmd_generate, 1, 1, 'TAG'),
514 'rebuild': (cmd_rebuild, 0, 0, '')}
515
516def init():
517 """
518 Load the appropriate configuration file and set up the configuration
519 dictionary.
520 """
521 for f in ['tripe-keys.master', 'tripe-keys.conf']:
522 if OS.path.exists(f):
523 conf_read(f)
524 break
525 conf_defaults()
526
527def main(argv):
528 """
529 Main program: parse options and dispatch to appropriate command handler.
530 """
531 try:
532 opts, args = O.getopt(argv[1:], 'hvu',
533 ['help', 'version', 'usage'])
534 except O.GetoptError, exc:
535 moan(exc)
536 usage(SYS.stderr)
537 SYS.exit(1)
538 for o, v in opts:
539 if o in ('-h', '--help'):
540 cmd_help([])
541 SYS.exit(0)
542 elif o in ('-v', '--version'):
543 version(SYS.stdout)
544 SYS.exit(0)
545 elif o in ('-u', '--usage'):
546 usage(SYS.stdout)
547 SYS.exit(0)
548 if len(argv) < 2:
549 cmd_help([])
550 else:
551 c = argv[1]
552 func, min, max, help = commands[c]
553 args = argv[2:]
554 if len(args) < min or (max > 0 and len(args) > max):
555 raise UsageError, (c, help)
556 func(args)
557
558###----- That's all, folks --------------------------------------------------
559
560if __name__ == '__main__':
561 init()
562 main(SYS.argv)