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