chiark / gitweb /
ec-field-test.c: Make the field-element type use internal format.
[secnet.git] / make-secnet-sites
1 #! /usr/bin/env python3
2 #
3 # This file is part of secnet.
4 # See README for full list of copyright holders.
5 #
6 # secnet is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10
11 # secnet is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # version 3 along with secnet; if not, see
18 # https://www.gnu.org/licenses/gpl.html.
19
20 """VPN sites file manipulation.
21
22 This program enables VPN site descriptions to be submitted for
23 inclusion in a central database, and allows the resulting database to
24 be turned into a secnet configuration file.
25
26 A database file can be turned into a secnet configuration file simply:
27 make-secnet-sites.py [infile [outfile]]
28
29 It would be wise to run secnet with the "--just-check-config" option
30 before installing the output on a live system.
31
32 The program expects to be invoked via userv to manage the database; it
33 relies on the USERV_USER and USERV_GROUP environment variables. The
34 command line arguments for this invocation are:
35
36 make-secnet-sites.py -u header-filename groupfiles-directory output-file \
37   group
38
39 All but the last argument are expected to be set by userv; the 'group'
40 argument is provided by the user. A suitable userv configuration file
41 fragment is:
42
43 reset
44 no-disconnect-hup
45 no-suppress-args
46 cd ~/secnet/sites-test/
47 execute ~/secnet/make-secnet-sites.py -u vpnheader groupfiles sites
48
49 This program is part of secnet.
50
51 """
52
53 from __future__ import print_function
54 from __future__ import unicode_literals
55 from builtins import int
56
57 import string
58 import time
59 import sys
60 import os
61 import getopt
62 import re
63 import argparse
64 import math
65
66 import ipaddress
67
68 # entry 0 is "near the executable", or maybe from PYTHONPATH=.,
69 # which we don't want to preempt
70 sys.path.insert(1,"/usr/local/share/secnet")
71 sys.path.insert(1,"/usr/share/secnet")
72 import ipaddrset
73 import base91
74
75 from argparseactionnoyes import ActionNoYes
76
77 VERSION="0.1.18"
78
79 max_version = 2
80
81 from sys import version_info
82 if version_info.major == 2:  # for python2
83     import codecs
84     sys.stdin = codecs.getreader('utf-8')(sys.stdin)
85     sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
86     import io
87     open=lambda f,m='r': io.open(f,m,encoding='utf-8')
88
89 max={'rsa_bits':8200,'name':33,'dh_bits':8200,'algname':127}
90
91 def debugrepr(*args):
92         if debug_level > 0:
93                 print(repr(args), file=sys.stderr)
94
95 def base91s_encode(bindata):
96         return base91.encode(bindata).replace('"',"-")
97
98 def base91s_decode(string):
99         return base91.decode(string.replace("-",'"'))
100
101 class Tainted:
102         def __init__(self,s,tline=None,tfile=None):
103                 self._s=s
104                 self._ok=None
105                 self._line=line if tline is None else tline
106                 self._file=file if tfile is None else tfile
107         def __eq__(self,e):
108                 return self._s==e
109         def __ne__(self,e):
110                 # for Python2
111                 return not self.__eq__(e)
112         def __str__(self):
113                 raise RuntimeError('direct use of Tainted value')
114         def __repr__(self):
115                 return 'Tainted(%s)' % repr(self._s)
116
117         def _bad(self,what,why):
118                 assert(self._ok is not True)
119                 self._ok=False
120                 complain('bad parameter: %s: %s' % (what, why))
121                 return False
122
123         def _max_ok(self,what,maxlen):
124                 if len(self._s) > maxlen:
125                         return self._bad(what,'too long (max %d)' % maxlen)
126                 return True
127
128         def _re_ok(self,bad,what,maxlen=None):
129                 if maxlen is None: maxlen=max[what]
130                 self._max_ok(what,maxlen)
131                 if self._ok is False: return False
132                 if bad.search(self._s):
133                         #print(repr(self), file=sys.stderr)
134                         return self._bad(what,'bad syntax')
135                 return True
136
137         def _rtnval(self, is_ok, ifgood, ifbad=''):
138                 if is_ok:
139                         assert(self._ok is not False)
140                         self._ok=True
141                         return ifgood
142                 else:
143                         assert(self._ok is not True)
144                         self._ok=False
145                         return ifbad
146
147         def _rtn(self, is_ok, ifbad=''):
148                 return self._rtnval(is_ok, self._s, ifbad)
149
150         def raw(self):
151                 return self._s
152         def raw_mark_ok(self):
153                 # caller promises to throw if syntax was dangeorus
154                 return self._rtn(True)
155
156         def output(self):
157                 if self._ok is False: return ''
158                 if self._ok is True: return self._s
159                 print('%s:%d: unchecked/unknown additional data "%s"' %
160                       (self._file,self._line,self._s),
161                       file=sys.stderr)
162                 sys.exit(1)
163
164         bad_name=re.compile(r'^[^a-zA-Z]|[^-_0-9a-zA-Z]')
165         # secnet accepts _ at start of names, but we reserve that
166         bad_name_counter=0
167         def name(self,what='name'):
168                 ok=self._re_ok(Tainted.bad_name,what)
169                 return self._rtn(ok,
170                                  '_line%d_%s' % (self._line, id(self)))
171
172         def keyword(self):
173                 ok=self._s in keywords or self._s in levels
174                 if not ok:
175                         complain('unknown keyword %s' % self._s)
176                 return self._rtn(ok)
177
178         bad_hex=re.compile(r'[^0-9a-fA-F]')
179         def bignum_16(self,kind,what):
180                 maxlen=(max[kind+'_bits']+3)/4
181                 ok=self._re_ok(Tainted.bad_hex,what,maxlen)
182                 return self._rtn(ok)
183
184         bad_num=re.compile(r'[^0-9]')
185         def bignum_10(self,kind,what):
186                 maxlen=math.ceil(max[kind+'_bits'] / math.log10(2))
187                 ok=self._re_ok(Tainted.bad_num,what,maxlen)
188                 return self._rtn(ok)
189
190         def number(self,minn,maxx,what='number'):
191                 # not for bignums
192                 ok=self._re_ok(Tainted.bad_num,what,10)
193                 if ok:
194                         v=int(self._s)
195                         if v<minn or v>maxx:
196                                 ok=self._bad(what,'out of range %d..%d'
197                                              % (minn,maxx))
198                 return self._rtnval(ok,v,minn)
199
200         def hexid(self,byteslen,what):
201                 ok=self._re_ok(Tainted.bad_hex,what,byteslen*2)
202                 if ok:
203                         if len(self._s) < byteslen*2:
204                                 ok=self._bad(what,'too short')
205                 return self._rtn(ok,ifbad='00'*byteslen)
206
207         bad_host=re.compile(r'[^-\][_.:0-9a-zA-Z]')
208         # We permit _ so we can refer to special non-host domains
209         # which have A and AAAA RRs.  This is a crude check and we may
210         # still produce config files with syntactically invalid
211         # domains or addresses, but that is OK.
212         def host(self):
213                 ok=self._re_ok(Tainted.bad_host,'host/address',255)
214                 return self._rtn(ok)
215
216         bad_email=re.compile(r'[^-._0-9a-z@!$%^&*=+~/]')
217         # ^ This does not accept all valid email addresses.  That's
218         # not really possible with this input syntax.  It accepts
219         # all ones that don't require quoting anywhere in email
220         # protocols (and also accepts some invalid ones).
221         def email(self):
222                 ok=self._re_ok(Tainted.bad_email,'email address',1023)
223                 return self._rtn(ok)
224
225         bad_groupname=re.compile(r'^[^_A-Za-z]|[^-+_0-9A-Za-z]')
226         def groupname(self):
227                 ok=self._re_ok(Tainted.bad_groupname,'group name',64)
228                 return self._rtn(ok)
229
230         bad_base91=re.compile(r'[^!-~]|[\'\"\\]')
231         def base91(self,what='base91'):
232                 ok=self._re_ok(Tainted.bad_base91,what,4096)
233                 return self._rtn(ok)
234
235 class ArgActionLambda(argparse.Action):
236         def __init__(self, fn, **kwargs):
237                 self.fn=fn
238                 argparse.Action.__init__(self,**kwargs)
239         def __call__(self,ap,ns,values,option_string):
240                 self.fn(values,ns,ap,option_string)
241
242 class PkmBase():
243         def site_start(self,pubkeys_path):
244                 self._pa=pubkeys_path
245                 self._fs = FilterState()
246         def site_serial(self,serial): pass
247         def write_key(self,k): pass
248         def site_finish(self,confw): pass
249
250 class PkmSingle(PkmBase):
251         opt = 'single'
252         help = 'write one public key per site to sites.conf'
253         def site_start(self,pubkeys_path):
254                 PkmBase.site_start(self,pubkeys_path)
255                 self._outk = []
256         def write_key(self,k):
257                 if k.okforonlykey(output_version,self._fs):
258                         self._outk.append(k)
259         def site_finish(self,confw):
260                 if len(self._outk) == 0:
261                         complain("site with no public key");
262                 elif len(self._outk) != 1:
263                         debugrepr('outk ', self._outk)
264                         complain(
265  "site with multiple public keys, without --pubkeys-install (maybe --output-version=1 would help"
266                         )
267                 else:
268                         confw.write("key %s;\n"%str(self._outk[0]))
269
270 class PkmInstall(PkmBase):
271         opt = 'install'
272         help = 'install public keys in public key directory'
273         def site_start(self,pubkeys_path):
274                 PkmBase.site_start(self,pubkeys_path)
275                 self._pw=open(self._pa+'~tmp','w')
276         def site_serial(self,serial):
277                 self._pw.write('serial %s\n' % serial)
278         def write_key(self,k):
279                 wout=k.forpub(output_version,self._fs)
280                 self._pw.write(' '.join(wout))
281                 self._pw.write('\n')
282         def site_finish(self,confw):
283                 self._pw.close()
284                 os.rename(self._pa+'~tmp',self._pa+'~update')
285                 PkmElide.site_finish(self,confw)
286
287 class PkmElide(PkmBase):
288         opt = 'elide'
289         help = 'no public keys in sites.conf output nor in directory'
290         def site_finish(self,confw):
291                 confw.write("peer-keys \"%s\";\n"%self._pa);
292
293 class OpBase():
294         # Base case is reading a sites file from self.inputfilee.
295         # And writing a sites file to self.sitesfile.
296         def positional_args(self, av):
297                 if len(av.arg)>3:
298                         print("Too many arguments")
299                         sys.exit(1)
300                 (self.inputfile, self.outputfile) = (av.arg + [None]*2)[0:2]
301         def read_in(self):
302                 if self.inputfile is None:
303                         self.inputlines = pfile("stdin",sys.stdin.readlines())
304                 else:
305                         self.inputlines = pfilepath(self.inputfile)
306         def write_out(self):
307                 if self.outputfile is None:
308                         f=sys.stdout
309                 else:
310                         f=open(self.outputfile+"-tmp",'w')
311                 f.write("# sites file autogenerated by make-secnet-sites\n")
312                 self.write_out_heading(f)
313                 f.write("# use make-secnet-sites to turn this file into a\n")
314                 f.write("# valid /etc/secnet/sites.conf file\n\n")
315                 self.write_out_contents(f)
316                 f.write("# end of sites file\n")
317                 if self.outputfile is not None:
318                         f.close()
319                         os.rename(self.outputfile+"-tmp",self.outputfile)
320
321 class OpConf(OpBase):
322         opts = ['--conf']
323         help = 'sites.conf generation mode (default)'
324         def check_group(self,group,w): pass
325         def write_out(self):
326                 if self.outputfile is None:
327                         of=sys.stdout
328                 else:
329                         tmp_outputfile=self.outputfile+'~tmp~'
330                         of=open(tmp_outputfile,'w')
331                 outputsites(of)
332                 if self.outputfile is not None:
333                         os.rename(tmp_outputfile,self.outputfile)
334
335 class OpFilter(OpBase):
336         opts = ['--filter']
337         help = 'sites file filtering mode'
338         def positional_arXgs(self, av):
339                 if len(av.arg)!=1:
340                         print("Too many arguments")
341                 (self.inputfile,) = (av.arg + [None])[0:1]
342                 self.outputfile = None
343         def write_out_heading(self,f):
344                 f.write("# --filter --output-version=%d\n"%output_version)
345         def write_out_contents(self,f):
346                 for i in self.inputlines: f.write(i)
347
348 class OpUserv(OpBase):
349         opts = ['--userv','-u']
350         help = 'userv service fragment update mode'
351         def positional_args(self, av):
352                 if len(av.arg)!=4:
353                         print("Wrong number of arguments")
354                         sys.exit(1)
355                 (self.header, self.groupfiledir,
356                  self.outputfile, self.group) = av.arg
357                 self.group = Tainted(self.group,0,'command line')
358                 # untrusted argument from caller
359                 if "USERV_USER" not in os.environ:
360                         print("Environment variable USERV_USER not found")
361                         sys.exit(1)
362                 self.user=os.environ["USERV_USER"]
363                 # Check that group is in USERV_GROUP
364                 if "USERV_GROUP" not in os.environ:
365                         print("Environment variable USERV_GROUP not found")
366                         sys.exit(1)
367                 ugs=os.environ["USERV_GROUP"]
368                 ok=0
369                 for i in ugs.split():
370                         if self.group==i: ok=1
371                 if not ok:
372                         print("caller not in group %s"%group)
373                         sys.exit(1)
374         def check_group(self,group,w):
375                 if group!=self.group: complain("Incorrect group!")
376                 w[2].groupname()
377         def read_in(self):
378                 self.headerinput=pfilepath(self.header,allow_include=True)
379                 self.userinput=sys.stdin.readlines()
380                 pfile("user input",self.userinput)
381         def write_out(self):
382                 # Put the user's input into their group file, and
383                 # rebuild the main sites file
384                 f=open(self.groupfiledir+"/T"+self.group.groupname(),'w')
385                 f.write("# Section submitted by user %s, %s\n"%
386                         (self.user,time.asctime(time.localtime(time.time()))))
387                 f.write("# Checked by make-secnet-sites version %s\n\n"
388                         %VERSION)
389                 for i in self.userinput: f.write(i)
390                 f.write("\n")
391                 f.close()
392                 os.rename(self.groupfiledir+"/T"+self.group.groupname(),
393                           self.groupfiledir+"/R"+self.group.groupname())
394                 OpBase.write_out(self)
395         def write_out_heading(self,f):
396                 f.write("# generated %s, invoked by %s\n"%
397                         (time.asctime(time.localtime(time.time())),
398                          self.user))
399         def write_out_contents(self,f):
400                 for i in self.headerinput: f.write(i)
401                 files=os.listdir(self.groupfiledir)
402                 for i in files:
403                         if i[0]=='R':
404                                 j=open(self.groupfiledir+"/"+i)
405                                 f.write(j.read())
406                                 j.close()
407
408 def parse_args():
409         global opmode
410         global prefix
411         global key_prefix
412         global debug_level
413         global output_version
414         global pubkeys_dir
415         global pubkeys_mode
416
417         ap = argparse.ArgumentParser(description='process secnet sites files')
418         def add_opmode(how):
419                 ap.add_argument(*how().opts, action=ArgActionLambda,
420                         nargs=0,
421                         fn=(lambda v,ns,*x: setattr(ns,'opmode',how)),
422                         help=how().help)
423         add_opmode(OpConf)
424         add_opmode(OpFilter)
425         add_opmode(OpUserv)
426         ap.add_argument('--conf-key-prefix', action=ActionNoYes,
427                         default=True,
428                  help='prefix conf file key names derived from sites data')
429         def add_pkm(how):
430                 ap.add_argument('--pubkeys-'+how().opt, action=ArgActionLambda,
431                         nargs=0,
432                         fn=(lambda v,ns,*x: setattr(ns,'pkm',how)),
433                         help=how().help)
434         add_pkm(PkmInstall)
435         add_pkm(PkmSingle)
436         add_pkm(PkmElide)
437         ap.add_argument('--pubkeys-dir',  nargs=1,
438                         help='public key directory',
439                         default=['/var/lib/secnet/pubkeys'])
440         ap.add_argument('--output-version', nargs=1, type=int,
441                         help='sites file output version',
442                         default=[max_version])
443         ap.add_argument('--prefix', '-P', nargs=1,
444                         help='set prefix')
445         ap.add_argument('--debug', '-D', action='count', default=0)
446         ap.add_argument('arg',nargs=argparse.REMAINDER)
447         av = ap.parse_args()
448         debug_level = av.debug
449         debugrepr('av',av)
450         opmode = getattr(av,'opmode',OpConf)()
451         prefix = '' if av.prefix is None else av.prefix[0]
452         key_prefix = av.conf_key_prefix
453         output_version = av.output_version[0]
454         pubkeys_dir = av.pubkeys_dir[0]
455         pubkeys_mode = getattr(av,'pkm',PkmSingle)
456         opmode.positional_args(av)
457
458 parse_args()
459
460 # Classes describing possible datatypes in the configuration file
461
462 class basetype:
463         "Common protocol for configuration types."
464         def add(self,obj,w):
465                 complain("%s %s already has property %s defined"%
466                         (obj.type,obj.name,w[0].raw()))
467         def forsites(self,version,copy,fs):
468                 return copy
469
470 class conflist:
471         "A list of some kind of configuration type."
472         def __init__(self,subtype,w):
473                 self.subtype=subtype
474                 self.list=[subtype(w)]
475         def add(self,obj,w):
476                 self.list.append(self.subtype(w))
477         def __str__(self):
478                 return ', '.join(map(str, self.list))
479         def forsites(self,version,copy,fs):
480                 most_recent=self.list[len(self.list)-1]
481                 return most_recent.forsites(version,copy,fs)
482 def listof(subtype):
483         return lambda w: conflist(subtype, w)
484
485 class single_ipaddr (basetype):
486         "An IP address"
487         def __init__(self,w):
488                 self.addr=ipaddress.ip_address(w[1].raw_mark_ok())
489         def __str__(self):
490                 return '"%s"'%self.addr
491
492 class networks (basetype):
493         "A set of IP addresses specified as a list of networks"
494         def __init__(self,w):
495                 self.set=ipaddrset.IPAddressSet()
496                 for i in w[1:]:
497                         x=ipaddress.ip_network(i.raw_mark_ok(),strict=True)
498                         self.set.append([x])
499         def __str__(self):
500                 return ",".join(map((lambda n: '"%s"'%n), self.set.networks()))
501
502 class trad_dhgroup (basetype):
503         "A Diffie-Hellman group"
504         def __init__(self,w):
505                 self.mod=w[1].bignum_16('dh','dh mod')
506                 self.gen=w[2].bignum_16('dh','dh gen')
507         def __str__(self):
508                 return 'diffie-hellman("%s","%s")'%(self.mod,self.gen)
509 def dhgroup(w):
510         if w[1] in ('x25519', 'x448'): return w[1]
511         else: return trad_dhgroup(w)
512
513 class hash (basetype):
514         "A choice of hash function"
515         def __init__(self,w):
516                 hname=w[1]
517                 self.ht=hname.raw()
518                 if (self.ht not in ('md5', 'sha1', 'sha512')):
519                         complain("unknown hash type %s"%(self.ht))
520                         self.ht=None
521                 else:
522                         hname.raw_mark_ok()
523         def __str__(self):
524                 return '%s'%(self.ht)
525
526 class email (basetype):
527         "An email address"
528         def __init__(self,w):
529                 self.addr=w[1].email()
530         def __str__(self):
531                 return '<%s>'%(self.addr)
532
533 class boolean (basetype):
534         "A boolean"
535         def __init__(self,w):
536                 v=w[1]
537                 if re.match('[TtYy1]',v.raw()):
538                         self.b=True
539                         v.raw_mark_ok()
540                 elif re.match('[FfNn0]',v.raw()):
541                         self.b=False
542                         v.raw_mark_ok()
543                 else:
544                         complain("invalid boolean value");
545         def __str__(self):
546                 return ['False','True'][self.b]
547
548 class num (basetype):
549         "A decimal number"
550         def __init__(self,w):
551                 self.n=w[1].number(0,0x7fffffff)
552         def __str__(self):
553                 return '%d'%(self.n)
554
555 class serial (basetype):
556         def __init__(self,w):
557                 self.i=w[1].hexid(4,'serial')
558         def __str__(self):
559                 return self.i
560         def forsites(self,version,copy,fs):
561                 if version < 2: return []
562                 return copy
563
564 class address (basetype):
565         "A DNS name and UDP port number"
566         def __init__(self,w):
567                 self.adr=w[1].host()
568                 self.port=w[2].number(1,65536,'port')
569         def __str__(self):
570                 return '"%s"; port %d'%(self.adr,self.port)
571
572 class inpub (basetype):
573         def forsites(self,version,xcopy,fs):
574                 return self.forpub(version,fs)
575
576 class pubkey (inpub):
577         "Some kind of publie key"
578         def __init__(self,w):
579                 self.a=w[1].name('algname')
580                 self.d=w[2].base91();
581         def __str__(self):
582                 return 'make-public("%s","%s")'%(self.a,self.d)
583         def forpub(self,version,fs):
584                 if version < 2: return []
585                 return ['pub', self.a, self.d]
586         def okforonlykey(self,version,fs):
587                 return len(self.forpub(version,fs)) != 0
588
589 class rsakey (pubkey):
590         "An old-style RSA public key"
591         def __init__(self,w):
592                 self.l=w[1].number(0,max['rsa_bits'],'rsa len')
593                 self.e=w[2].bignum_10('rsa','rsa e')
594                 self.n=w[3].bignum_10('rsa','rsa n')
595                 if len(w) >= 5: w[4].email()
596                 self.a='rsa1'
597                 self.d=base91s_encode(b'%d %s %s' %
598                                       (self.l,
599                                        self.e.encode('ascii'),
600                                        self.n.encode('ascii')))
601                 # ^ this allows us to use the pubkey.forsites()
602                 # method for output in versions>=2
603         def __str__(self):
604                 return 'rsa-public("%s","%s")'%(self.e,self.n)
605                 # this specialisation means we can generate files
606                 # compatible with old secnet executables
607         def forpub(self,version,fs):
608                 if version < 2:
609                         if fs.pkg != '00000000': return []
610                         return ['pubkey', str(self.l), self.e, self.n]
611                 return pubkey.forpub(self,version,fs)
612
613 class rsakey_newfmt(rsakey):
614         "An old-style RSA public key in new-style sites format"
615         # This is its own class simply to have its own constructor.
616         def __init__(self,w):
617                 self.a=w[1].name()
618                 assert(self.a == 'rsa1')
619                 self.d=w[2].base91()
620                 try:
621                         w_inner=list(map(Tainted,
622                                         ['X-PUB-RSA1'] +
623                                         base91s_decode(self.d)
624                                         .decode('ascii')
625                                         .split(' ')))
626                 except UnicodeDecodeError:
627                         complain('rsa1 key in new format has bad base91')
628                 #print(repr(w_inner), file=sys.stderr)
629                 rsakey.__init__(self,w_inner)
630
631 class pubkey_group(inpub):
632         "Public key group introducer"
633         # appears in the site's list of keys mixed in with the keys
634         def __init__(self,w,fallback):
635                 self.i=w[1].hexid(4,'pkg-id')
636                 self.fallback=fallback
637         def forpub(self,version,fs):
638                 fs.pkg=self.i
639                 if version < 2: return []
640                 return ['pkgf' if self.fallback else 'pkg', self.i]
641         def okforonlykey(self,version,fs):
642                 self.forpub(version,fs)
643                 return False
644         
645 def somepubkey(w):
646         #print(repr(w), file=sys.stderr)
647         if w[0]=='pubkey':
648                 return rsakey(w)
649         elif w[0]=='pub' and w[1]=='rsa1':
650                 return rsakey_newfmt(w)
651         elif w[0]=='pub':
652                 return pubkey(w)
653         elif w[0]=='pkg':
654                 return pubkey_group(w,False)
655         elif w[0]=='pkgf':
656                 return pubkey_group(w,True)
657         else:
658                 assert(False)
659
660 # Possible properties of configuration nodes
661 keywords={
662  'contact':(email,"Contact address"),
663  'dh':(listof(dhgroup),"Diffie-Hellman group"),
664  'hash':(hash,"Hash function"),
665  'key-lifetime':(num,"Maximum key lifetime (ms)"),
666  'setup-timeout':(num,"Key setup timeout (ms)"),
667  'setup-retries':(num,"Maximum key setup packet retries"),
668  'wait-time':(num,"Time to wait after unsuccessful key setup (ms)"),
669  'renegotiate-time':(num,"Time after key setup to begin renegotiation (ms)"),
670  'restrict-nets':(networks,"Allowable networks"),
671  'networks':(networks,"Claimed networks"),
672  'serial':(serial,"public key set serial"),
673  'pkg':(listof(somepubkey),"start of public key group",'pub'),
674  'pkgf':(listof(somepubkey),"start of fallback public key group",'pub'),
675  'pub':(listof(somepubkey),"new style public site key"),
676  'pubkey':(listof(somepubkey),"Old-style RSA public site key",'pub'),
677  'peer':(single_ipaddr,"Tunnel peer IP address"),
678  'address':(address,"External contact address and port"),
679  'mobile':(boolean,"Site is mobile"),
680 }
681
682 def sp(name,value):
683         "Simply output a property - the default case"
684         return "%s %s;\n"%(name,value)
685
686 # All levels support these properties
687 global_properties={
688         'contact':(lambda name,value:"# Contact email address: %s\n"%(value)),
689         'dh':sp,
690         'hash':sp,
691         'key-lifetime':sp,
692         'setup-timeout':sp,
693         'setup-retries':sp,
694         'wait-time':sp,
695         'renegotiate-time':sp,
696         'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value),
697 }
698
699 class level:
700         "A level in the configuration hierarchy"
701         depth=0
702         leaf=0
703         allow_properties={}
704         require_properties={}
705         def __init__(self,w):
706                 self.type=w[0].keyword()
707                 self.name=w[1].name()
708                 self.properties={}
709                 self.children={}
710         def indent(self,w,t):
711                 w.write("                 "[:t])
712         def prop_out(self,n):
713                 return self.allow_properties[n](n,str(self.properties[n]))
714         def output_props(self,w,ind):
715                 for i in sorted(self.properties.keys()):
716                         if self.allow_properties[i]:
717                                 self.indent(w,ind)
718                                 w.write("%s"%self.prop_out(i))
719         def kname(self):
720                 return ((self.type[0].upper() if key_prefix else '')
721                         + self.name)
722         def output_data(self,w,path):
723                 ind = 2*len(path)
724                 self.indent(w,ind)
725                 w.write("%s {\n"%(self.kname()))
726                 self.output_props(w,ind+2)
727                 if self.depth==1: w.write("\n");
728                 for k in sorted(self.children.keys()):
729                         c=self.children[k]
730                         c.output_data(w,path+(c,))
731                 self.indent(w,ind)
732                 w.write("};\n")
733
734 class vpnlevel(level):
735         "VPN level in the configuration hierarchy"
736         depth=1
737         leaf=0
738         type="vpn"
739         allow_properties=global_properties.copy()
740         require_properties={
741          'contact':"VPN admin contact address"
742         }
743         def __init__(self,w):
744                 level.__init__(self,w)
745         def output_vpnflat(self,w,path):
746                 "Output flattened list of site names for this VPN"
747                 ind=2*(len(path)+1)
748                 self.indent(w,ind)
749                 w.write("%s {\n"%(self.kname()))
750                 for i in self.children.keys():
751                         self.children[i].output_vpnflat(w,path+(self,))
752                 w.write("\n")
753                 self.indent(w,ind+2)
754                 w.write("all-sites %s;\n"%
755                         ','.join(map(lambda i: i.kname(),
756                                      self.children.values())))
757                 self.indent(w,ind)
758                 w.write("};\n")
759
760 class locationlevel(level):
761         "Location level in the configuration hierarchy"
762         depth=2
763         leaf=0
764         type="location"
765         allow_properties=global_properties.copy()
766         require_properties={
767          'contact':"Location admin contact address",
768         }
769         def __init__(self,w):
770                 level.__init__(self,w)
771                 self.group=w[2].groupname()
772         def output_vpnflat(self,w,path):
773                 ind=2*(len(path)+1)
774                 self.indent(w,ind)
775                 # The "path=path,self=self" abomination below exists because
776                 # Python didn't support nested_scopes until version 2.1
777                 #
778                 #"/"+self.name+"/"+i
779                 w.write("%s %s;\n"%(self.kname(),','.join(
780                         map(lambda x,path=path,self=self:
781                             '/'.join([prefix+"vpn-data"] + list(map(
782                                     lambda i: i.kname(),
783                                     path+(self,x)))),
784                             self.children.values()))))
785
786 class sitelevel(level):
787         "Site level (i.e. a leafnode) in the configuration hierarchy"
788         depth=3
789         leaf=1
790         type="site"
791         allow_properties=global_properties.copy()
792         allow_properties.update({
793          'address':sp,
794          'networks':None,
795          'peer':None,
796          'serial':None,
797          'pkg':None,
798          'pkgf':None,
799          'pub':None,
800          'pubkey':None,
801          'mobile':sp,
802         })
803         require_properties={
804          'dh':"Diffie-Hellman group",
805          'contact':"Site admin contact address",
806          'networks':"Networks claimed by the site",
807          'hash':"hash function",
808          'peer':"Gateway address of the site",
809         }
810         def mangle_name(self):
811                 return self.name.replace('/',',')
812         def pubkeys_path(self):
813                 return pubkeys_dir + '/peer.' + self.mangle_name()
814         def __init__(self,w):
815                 level.__init__(self,w)
816         def output_data(self,w,path):
817                 ind=2*len(path)
818                 np='/'.join(map(lambda i: i.name, path))
819                 self.indent(w,ind)
820                 w.write("%s {\n"%(self.kname()))
821                 self.indent(w,ind+2)
822                 w.write("name \"%s\";\n"%(np,))
823                 self.indent(w,ind+2)
824
825                 pkm = pubkeys_mode()
826                 debugrepr('pkm ',pkm)
827                 pkm.site_start(self.pubkeys_path())
828                 if 'serial' in self.properties:
829                         pkm.site_serial(self.properties['serial'])
830
831                 for k in self.properties["pub"].list:
832                         debugrepr('pubkeys ', k)
833                         pkm.write_key(k)
834
835                 pkm.site_finish(w)
836
837                 self.output_props(w,ind+2)
838                 self.indent(w,ind+2)
839                 w.write("link netlink {\n");
840                 self.indent(w,ind+4)
841                 w.write("routes %s;\n"%str(self.properties["networks"]))
842                 self.indent(w,ind+4)
843                 w.write("ptp-address %s;\n"%str(self.properties["peer"]))
844                 self.indent(w,ind+2)
845                 w.write("};\n")
846                 self.indent(w,ind)
847                 w.write("};\n")
848
849 # Levels in the configuration file
850 # (depth,properties)
851 levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
852
853 def complain(msg):
854         "Complain about a particular input line"
855         moan(("%s line %d: "%(file,line))+msg)
856 def moan(msg):
857         "Complain about something in general"
858         global complaints
859         print(msg);
860         if complaints is None: sys.exit(1)
861         complaints=complaints+1
862
863 class UntaintedRoot():
864         def __init__(self,s): self._s=s
865         def name(self): return self._s
866         def keyword(self): return self._s
867
868 root=level([UntaintedRoot(x) for x in ['root','root']])
869 # All vpns are children of this node
870 obstack=[root]
871 allow_defs=0   # Level above which new definitions are permitted
872
873 def set_property(obj,w):
874         "Set a property on a configuration node"
875         prop=w[0]
876         propname=prop.raw_mark_ok()
877         kw=keywords[propname]
878         if len(kw) >= 3: propname=kw[2] # for aliases
879         if propname in obj.properties:
880                 obj.properties[propname].add(obj,w)
881         else:
882                 obj.properties[propname]=kw[0](w)
883         return obj.properties[propname]
884
885 class FilterState:
886         def __init__(self):
887                 self.reset()
888         def reset(self):
889                 # called when we enter a new node,
890                 # in particular, at the start of each site
891                 self.pkg = '00000000'
892
893 def pline(il,filterstate,allow_include=False):
894         "Process a configuration file line"
895         global allow_defs, obstack, root
896         w=il.rstrip('\n').split()
897         if len(w)==0: return ['']
898         w=list([Tainted(x) for x in w])
899         keyword=w[0]
900         current=obstack[len(obstack)-1]
901         copyout_core=lambda: ' '.join([ww.output() for ww in w])
902         indent='    '*len(obstack)
903         copyout=lambda: [indent + copyout_core() + '\n']
904         if keyword=='end-definitions':
905                 keyword.raw_mark_ok()
906                 allow_defs=sitelevel.depth
907                 obstack=[root]
908                 return copyout()
909         if keyword=='include':
910                 if not allow_include:
911                         complain("include not permitted here")
912                         return []
913                 if len(w) != 2:
914                         complain("include requires one argument")
915                         return []
916                 newfile=os.path.join(os.path.dirname(file),w[1].raw_mark_ok())
917                 # ^ user of "include" is trusted so raw_mark_ok is good
918                 return pfilepath(newfile,allow_include=allow_include)
919         if keyword.raw() in levels:
920                 # We may go up any number of levels, but only down by one
921                 newdepth=levels[keyword.raw_mark_ok()].depth
922                 currentdepth=len(obstack) # actually +1...
923                 if newdepth<=currentdepth:
924                         obstack=obstack[:newdepth]
925                 if newdepth>currentdepth:
926                         complain("May not go from level %d to level %d"%
927                                 (currentdepth-1,newdepth))
928                 # See if it's a new one (and whether that's permitted)
929                 # or an existing one
930                 current=obstack[len(obstack)-1]
931                 tname=w[1].name()
932                 if tname in current.children:
933                         # Not new
934                         current=current.children[tname]
935                         if current.depth==2:
936                                 opmode.check_group(current.group, w)
937                 else:
938                         # New
939                         # Ignore depth check for now
940                         nl=levels[keyword.raw()](w)
941                         if nl.depth<allow_defs:
942                                 complain("New definitions not allowed at "
943                                         "level %d"%nl.depth)
944                                 # we risk crashing if we continue
945                                 sys.exit(1)
946                         current.children[tname]=nl
947                         current=nl
948                 filterstate.reset()
949                 obstack.append(current)
950                 return copyout()
951         if keyword.raw() not in current.allow_properties:
952                 complain("Property %s not allowed at %s level"%
953                         (keyword.raw(),current.type))
954                 return []
955         elif current.depth == vpnlevel.depth < allow_defs:
956                 complain("Not allowed to set VPN properties here")
957                 return []
958         else:
959                 prop=set_property(current,w)
960                 out=[copyout_core()]
961                 out=prop.forsites(output_version,out,filterstate)
962                 if len(out)==0: return [indent + '#', copyout_core(), '\n']
963                 return [indent + ' '.join(out) + '\n']
964
965         complain("unknown keyword '%s'"%(keyword.raw()))
966
967 def pfilepath(pathname,allow_include=False):
968         f=open(pathname)
969         outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
970         f.close()
971         return outlines
972
973 def pfile(name,lines,allow_include=False):
974         "Process a file"
975         global file,line
976         file=name
977         line=0
978         outlines=[]
979         filterstate = FilterState()
980         for i in lines:
981                 line=line+1
982                 if (i[0]=='#'): continue
983                 outlines += pline(i,filterstate,allow_include=allow_include)
984         return outlines
985
986 def outputsites(w):
987         "Output include file for secnet configuration"
988         w.write("# secnet sites file autogenerated by make-secnet-sites "
989                 +"version %s\n"%VERSION)
990         w.write("# %s\n"%time.asctime(time.localtime(time.time())))
991         w.write("# Command line: %s\n\n"%' '.join(sys.argv))
992
993         # Raw VPN data section of file
994         w.write(prefix+"vpn-data {\n")
995         for i in root.children.values():
996                 i.output_data(w,(i,))
997         w.write("};\n")
998
999         # Per-VPN flattened lists
1000         w.write(prefix+"vpn {\n")
1001         for i in root.children.values():
1002                 i.output_vpnflat(w,())
1003         w.write("};\n")
1004
1005         # Flattened list of sites
1006         w.write(prefix+"all-sites %s;\n"%",".join(
1007                 map(lambda x:"%svpn/%s/all-sites"%(prefix,x.kname()),
1008                         root.children.values())))
1009
1010 line=0
1011 file=None
1012 complaints=0
1013
1014 # Sanity check section
1015 # Delete nodes where leaf=0 that have no children
1016
1017 def live(n):
1018         "Number of leafnodes below node n"
1019         if n.leaf: return 1
1020         for i in n.children.keys():
1021                 if live(n.children[i]): return 1
1022         return 0
1023 def delempty(n):
1024         "Delete nodes that have no leafnode children"
1025         for i in list(n.children.keys()):
1026                 delempty(n.children[i])
1027                 if not live(n.children[i]):
1028                         del n.children[i]
1029
1030 # Check that all constraints are met (as far as I can tell
1031 # restrict-nets/networks/peer are the only special cases)
1032
1033 def checkconstraints(n,p,ra):
1034         new_p=p.copy()
1035         new_p.update(n.properties)
1036         for i in n.require_properties.keys():
1037                 if i not in new_p:
1038                         moan("%s %s is missing property %s"%
1039                                 (n.type,n.name,i))
1040         for i in new_p.keys():
1041                 if i not in n.allow_properties:
1042                         moan("%s %s has forbidden property %s"%
1043                                 (n.type,n.name,i))
1044         # Check address range restrictions
1045         if "restrict-nets" in n.properties:
1046                 new_ra=ra.intersection(n.properties["restrict-nets"].set)
1047         else:
1048                 new_ra=ra
1049         if "networks" in n.properties:
1050                 if not n.properties["networks"].set <= new_ra:
1051                         moan("%s %s networks out of bounds"%(n.type,n.name))
1052                 if "peer" in n.properties:
1053                         if not n.properties["networks"].set.contains(
1054                                 n.properties["peer"].addr):
1055                                 moan("%s %s peer not in networks"%(n.type,n.name))
1056         for i in n.children.keys():
1057                 checkconstraints(n.children[i],new_p,new_ra)
1058
1059 opmode.read_in()
1060
1061 delempty(root)
1062 checkconstraints(root,{},ipaddrset.complete_set())
1063
1064 if complaints>0:
1065         if complaints==1: print("There was 1 problem.")
1066         else: print("There were %d problems."%(complaints))
1067         sys.exit(1)
1068 complaints=None # arranges to crash if we complain later
1069
1070 opmode.write_out()