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