RPKI Engine 1.0

myrpki.py (3793)

Go to the documentation of this file.
00001 """
00002 This (oversized) module used to be an (oversized) program.
00003 Refactoring in progress, some doc still needs updating.
00004 
00005 
00006 This program is now the merger of three different tools: the old
00007 myrpki.py script, the old myirbe.py script, and the newer setup.py CLI
00008 tool.  As such, it is still in need of some cleanup, but the need to
00009 provide a saner user interface is more urgent than internal code
00010 prettiness at the moment.  In the long run, 90% of the code in this
00011 file probably ought to move to well-designed library modules.
00012 
00013 Overall goal here is to build up the configuration necessary to run
00014 rpkid and friends, by reading a config file, a collection of .CSV
00015 files, and the results of a few out-of-band XML setup messages
00016 exchanged with one's parents, children, and so forth.
00017 
00018 The config file is in an OpenSSL-compatible format, the CSV files are
00019 simple tab-delimited text.  The XML files are all generated by this
00020 program, either the local instance or an instance being run by another
00021 player in the system; the mechanism used to exchange these setup
00022 messages is outside the scope of this program, feel free to use
00023 PGP-signed mail, a web interface (not provided), USB stick, carrier
00024 pigeons, whatever works.
00025 
00026 With one exception, the commands in this program avoid using any
00027 third-party Python code other than the rpki libraries themselves; with
00028 the same one exception, all OpenSSL work is done with the OpenSSL
00029 command line tool (the one built as a side effect of building rcynic
00030 will do, if your platform has no system copy or the system copy is too
00031 old).  This is all done in an attempt to make the code more portable,
00032 so one can run most of the RPKI back end software on a laptop or
00033 whatever.  The one exception is the configure_daemons command, which
00034 must, of necessity, use the same communication libraries as the
00035 daemons with which it is conversing.  So that one command will not
00036 work if the correct Python modules are not available.
00037 
00038 
00039 $Id: myrpki.py 3793 2011-04-27 04:34:52Z sra $
00040 
00041 Copyright (C) 2009--2011  Internet Systems Consortium ("ISC")
00042 
00043 Permission to use, copy, modify, and distribute this software for any
00044 purpose with or without fee is hereby granted, provided that the above
00045 copyright notice and this permission notice appear in all copies.
00046 
00047 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
00048 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00049 AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
00050 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00051 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00052 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00053 PERFORMANCE OF THIS SOFTWARE.
00054 """
00055 
00056 from __future__ import with_statement
00057 
00058 import subprocess, csv, re, os, getopt, sys, base64, time, glob, copy, warnings
00059 import rpki.config, rpki.cli, rpki.sundial, rpki.log, rpki.oids
00060 
00061 try:
00062   from lxml.etree            import (Element, SubElement, ElementTree,
00063                                      fromstring as ElementFromString,
00064                                      tostring   as ElementToString)
00065 except ImportError:
00066   from xml.etree.ElementTree import (Element, SubElement, ElementTree,
00067                                      fromstring as ElementFromString,
00068                                      tostring   as ElementToString)
00069 
00070 
00071 
00072 # Our XML namespace and protocol version.
00073 
00074 namespace      = "http://www.hactrn.net/uris/rpki/myrpki/"
00075 version        = "2"
00076 namespaceQName = "{" + namespace + "}"
00077 
00078 # Whether to include incomplete entries when rendering to XML.
00079 
00080 allow_incomplete = False
00081 
00082 # Whether to whine about incomplete entries while rendering to XML.
00083 
00084 whine = True
00085 
00086 class comma_set(set):
00087   """
00088   Minor customization of set(), to provide a print syntax.
00089   """
00090 
00091   def __str__(self):
00092     return ",".join(self)
00093 
00094 class EntityDB(object):
00095   """
00096   Wrapper for entitydb path lookups and iterations.
00097   """
00098 
00099   def __init__(self, cfg):
00100     self.dir = cfg.get("entitydb_dir", "entitydb")
00101     self.identity = os.path.join(self.dir, "identity.xml")
00102 
00103   def __call__(self, dirname, filebase = None):
00104     if filebase is None:
00105       return os.path.join(self.dir, dirname)
00106     else:
00107       return os.path.join(self.dir, dirname, filebase + ".xml")
00108 
00109   def iterate(self, dir, base = "*"):
00110     return glob.iglob(os.path.join(self.dir, dir, base + ".xml"))
00111 
00112 class roa_request(object):
00113   """
00114   Representation of a ROA request.
00115   """
00116 
00117   v4re = re.compile("^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]+(-[0-9]+)?$", re.I)
00118   v6re = re.compile("^([0-9a-f]{0,4}:){0,15}[0-9a-f]{0,4}/[0-9]+(-[0-9]+)?$", re.I)
00119 
00120   def __init__(self, asn, group):
00121     self.asn = asn
00122     self.group = group
00123     self.v4 = comma_set()
00124     self.v6 = comma_set()
00125 
00126   def __repr__(self):
00127     s = "<%s asn %s group %s" % (self.__class__.__name__, self.asn, self.group)
00128     if self.v4:
00129       s += " v4 %s" % self.v4
00130     if self.v6:
00131       s += " v6 %s" % self.v6
00132     return s + ">"
00133 
00134   def add(self, prefix):
00135     """
00136     Add one prefix to this ROA request.
00137     """
00138     if self.v4re.match(prefix):
00139       self.v4.add(prefix)
00140     elif self.v6re.match(prefix):
00141       self.v6.add(prefix)
00142     else:
00143       raise RuntimeError, "Bad prefix syntax: %r" % (prefix,)
00144 
00145   def xml(self, e):
00146     """
00147     Generate XML element represeting representing this ROA request.
00148     """
00149     e = SubElement(e, "roa_request",
00150                    asn = self.asn,
00151                    v4 = str(self.v4),
00152                    v6 = str(self.v6))
00153     e.tail = "\n"
00154 
00155 class roa_requests(dict):
00156   """
00157   Database of ROA requests.
00158   """
00159 
00160   def add(self, asn, group, prefix):
00161     """
00162     Add one <ASN, group, prefix> set to ROA request database.
00163     """
00164     key = (asn, group)
00165     if key not in self:
00166       self[key] = roa_request(asn, group)
00167     self[key].add(prefix)
00168 
00169   def xml(self, e):
00170     """
00171     Render ROA requests as XML elements.
00172     """
00173     for r in self.itervalues():
00174       r.xml(e)
00175 
00176   @classmethod
00177   def from_csv(cls, roa_csv_file):
00178     """
00179     Parse ROA requests from CSV file.
00180     """
00181     self = cls()
00182     # format:  p/n-m asn group
00183     for pnm, asn, group in csv_reader(roa_csv_file, columns = 3):
00184       self.add(asn = asn, group = group, prefix = pnm)
00185     return self
00186 
00187 class child(object):
00188   """
00189   Representation of one child entity.
00190   """
00191 
00192   v4re = re.compile("^(([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]+)|(([0-9]{1,3}\.){3}[0-9]{1,3}-([0-9]{1,3}\.){3}[0-9]{1,3})$", re.I)
00193   v6re = re.compile("^(([0-9a-f]{0,4}:){0,15}[0-9a-f]{0,4}/[0-9]+)|(([0-9a-f]{0,4}:){0,15}[0-9a-f]{0,4}-([0-9a-f]{0,4}:){0,15}[0-9a-f]{0,4})$", re.I)
00194 
00195   def __init__(self, handle):
00196     self.handle = handle
00197     self.asns = comma_set()
00198     self.v4 = comma_set()
00199     self.v6 = comma_set()
00200     self.validity = None
00201     self.bpki_certificate = None
00202 
00203   def __repr__(self):
00204     s = "<%s %s" % (self.__class__.__name__, self.handle)
00205     if self.asns:
00206       s += " asn %s" % self.asns
00207     if self.v4:
00208       s += " v4 %s" % self.v4
00209     if self.v6:
00210       s += " v6 %s" % self.v6
00211     if self.validity:
00212       s += " valid %s" % self.validity
00213     if self.bpki_certificate:
00214       s += " cert %s" % self.bpki_certificate
00215     return s + ">"
00216 
00217   def add(self, prefix = None, asn = None, validity = None, bpki_certificate = None):
00218     """
00219     Add prefix, autonomous system number, validity date, or BPKI
00220     certificate for this child.
00221     """
00222     if prefix is not None:
00223       if self.v4re.match(prefix):
00224         self.v4.add(prefix)
00225       elif self.v6re.match(prefix):
00226         self.v6.add(prefix)
00227       else:
00228         raise RuntimeError, "Bad prefix syntax: %r" % (prefix,)
00229     if asn is not None:
00230       self.asns.add(asn)
00231     if validity is not None:
00232       self.validity = validity
00233     if bpki_certificate is not None:
00234       self.bpki_certificate = bpki_certificate
00235 
00236   def xml(self, e):
00237     """
00238     Render this child as an XML element.
00239     """
00240     complete = self.bpki_certificate and self.validity
00241     if whine and not complete:
00242       print "Incomplete child entry %s" % self
00243     if complete or allow_incomplete:
00244       e = SubElement(e, "child",
00245                      handle = self.handle,
00246                      valid_until = self.validity,
00247                      asns = str(self.asns),
00248                      v4 = str(self.v4),
00249                      v6 = str(self.v6))
00250       e.tail = "\n"
00251       if self.bpki_certificate:
00252         PEMElement(e, "bpki_certificate", self.bpki_certificate)
00253 
00254 class children(dict):
00255   """
00256   Database of children.
00257   """
00258 
00259   def add(self, handle, prefix = None, asn = None, validity = None, bpki_certificate = None):
00260     """
00261     Add resources to a child, creating the child object if necessary.
00262     """
00263     if handle not in self:
00264       self[handle] = child(handle)
00265     self[handle].add(prefix = prefix, asn = asn, validity = validity, bpki_certificate = bpki_certificate)
00266 
00267   def xml(self, e):
00268     """
00269     Render children database to XML.
00270     """
00271     for c in self.itervalues():
00272       c.xml(e)
00273 
00274   @classmethod
00275   def from_entitydb(cls, prefix_csv_file, asn_csv_file, fxcert, entitydb):
00276     """
00277     Parse child data from entitydb.
00278     """
00279     self = cls()
00280     for f in entitydb.iterate("children"):
00281       c = etree_read(f)
00282       self.add(handle = os.path.splitext(os.path.split(f)[-1])[0],
00283                validity = c.get("valid_until"),
00284                bpki_certificate = fxcert(c.findtext("bpki_child_ta")))
00285     # childname p/n
00286     for handle, pn in csv_reader(prefix_csv_file, columns = 2):
00287       self.add(handle = handle, prefix = pn)
00288     # childname asn
00289     for handle, asn in csv_reader(asn_csv_file, columns = 2):
00290       self.add(handle = handle, asn = asn)
00291     return self
00292 
00293 class parent(object):
00294   """
00295   Representation of one parent entity.
00296   """
00297 
00298   def __init__(self, handle):
00299     self.handle = handle
00300     self.service_uri = None
00301     self.bpki_cms_certificate = None
00302     self.myhandle = None
00303     self.sia_base = None
00304 
00305   def __repr__(self):
00306     s = "<%s %s" % (self.__class__.__name__, self.handle)
00307     if self.myhandle:
00308       s += " myhandle %s" % self.myhandle
00309     if self.service_uri:
00310       s += " uri %s" % self.service_uri
00311     if self.sia_base:
00312       s += " sia %s" % self.sia_base
00313     if self.bpki_cms_certificate:
00314       s += " cms %s" % self.bpki_cms_certificate
00315     return s + ">"
00316 
00317   def add(self, service_uri = None,
00318           bpki_cms_certificate = None,
00319           myhandle = None,
00320           sia_base = None):
00321     """
00322     Add service URI or BPKI certificates to this parent object.
00323     """
00324     if service_uri is not None:
00325       self.service_uri = service_uri
00326     if bpki_cms_certificate is not None:
00327       self.bpki_cms_certificate = bpki_cms_certificate
00328     if myhandle is not None:
00329       self.myhandle = myhandle
00330     if sia_base is not None:
00331       self.sia_base = sia_base
00332 
00333   def xml(self, e):
00334     """
00335     Render this parent object to XML.
00336     """
00337     complete = self.bpki_cms_certificate and self.myhandle and self.service_uri and self.sia_base
00338     if whine and not complete:
00339       print "Incomplete parent entry %s" % self
00340     if complete or allow_incomplete:
00341       e = SubElement(e, "parent",
00342                      handle = self.handle,
00343                      myhandle = self.myhandle,
00344                      service_uri = self.service_uri,
00345                      sia_base = self.sia_base)
00346       e.tail = "\n"
00347       if self.bpki_cms_certificate:
00348         PEMElement(e, "bpki_cms_certificate", self.bpki_cms_certificate)
00349 
00350 class parents(dict):
00351   """
00352   Database of parent objects.
00353   """
00354 
00355   def add(self, handle,
00356           service_uri = None,
00357           bpki_cms_certificate = None,
00358           myhandle = None,
00359           sia_base = None):
00360     """
00361     Add service URI or certificates to parent object, creating it if necessary.
00362     """
00363     if handle not in self:
00364       self[handle] = parent(handle)
00365     self[handle].add(service_uri = service_uri,
00366                      bpki_cms_certificate = bpki_cms_certificate,
00367                      myhandle = myhandle,
00368                      sia_base = sia_base)
00369 
00370   def xml(self, e):
00371     for c in self.itervalues():
00372       c.xml(e)
00373 
00374   @classmethod
00375   def from_entitydb(cls, fxcert, entitydb):
00376     """
00377     Parse parent data from entitydb.
00378     """
00379     self = cls()
00380     for f in entitydb.iterate("parents"):
00381       h = os.path.splitext(os.path.split(f)[-1])[0]
00382       p = etree_read(f)
00383       r = etree_read(f.replace(os.path.sep + "parents"      + os.path.sep,
00384                                os.path.sep + "repositories" + os.path.sep))
00385       if r.get("type") == "confirmed":
00386         self.add(handle = h,
00387                  service_uri = p.get("service_uri"),
00388                  bpki_cms_certificate = fxcert(p.findtext("bpki_resource_ta")),
00389                  myhandle = p.get("child_handle"),
00390                  sia_base = r.get("sia_base"))
00391       elif whine:
00392         print "Parent %s's repository entry in state %s, skipping this parent" % (h, r.get("type"))
00393     return self
00394 
00395 class repository(object):
00396   """
00397   Representation of one repository entity.
00398   """
00399 
00400   def __init__(self, handle):
00401     self.handle = handle
00402     self.service_uri = None
00403     self.bpki_certificate = None
00404 
00405   def __repr__(self):
00406     s = "<%s %s" % (self.__class__.__name__, self.handle)
00407     if self.service_uri:
00408       s += " uri %s" % self.service_uri
00409     if self.bpki_certificate:
00410       s += " cert %s" % self.bpki_certificate
00411     return s + ">"
00412 
00413   def add(self, service_uri = None, bpki_certificate = None):
00414     """
00415     Add service URI or BPKI certificates to this repository object.
00416     """
00417     if service_uri is not None:
00418       self.service_uri = service_uri
00419     if bpki_certificate is not None:
00420       self.bpki_certificate = bpki_certificate
00421 
00422   def xml(self, e):
00423     """
00424     Render this repository object to XML.
00425     """
00426     complete = self.bpki_certificate and self.service_uri
00427     if whine and not complete:
00428       print "Incomplete repository entry %s" % self
00429     if complete or allow_incomplete:
00430       e = SubElement(e, "repository",
00431                      handle = self.handle,
00432                      service_uri = self.service_uri)
00433       e.tail = "\n"
00434       if self.bpki_certificate:
00435         PEMElement(e, "bpki_certificate", self.bpki_certificate)
00436 
00437 class repositories(dict):
00438   """
00439   Database of repository objects.
00440   """
00441 
00442   def add(self, handle,
00443           service_uri = None,
00444           bpki_certificate = None):
00445     """
00446     Add service URI or certificate to repository object, creating it if necessary.
00447     """
00448     if handle not in self:
00449       self[handle] = repository(handle)
00450     self[handle].add(service_uri = service_uri,
00451                      bpki_certificate = bpki_certificate)
00452 
00453   def xml(self, e):
00454     for c in self.itervalues():
00455       c.xml(e)
00456 
00457   @classmethod
00458   def from_entitydb(cls, fxcert, entitydb):
00459     """
00460     Parse repository data from entitydb.
00461     """
00462     self = cls()
00463     for f in entitydb.iterate("repositories"):
00464       h = os.path.splitext(os.path.split(f)[-1])[0]
00465       r = etree_read(f)
00466       if r.get("type") == "confirmed":
00467         self.add(handle = h,
00468                  service_uri = r.get("service_uri"),
00469                  bpki_certificate = fxcert(r.findtext("bpki_server_ta")))
00470       elif whine:
00471         print "Repository %s in state %s, skipping this repository" % (h, r.get("type"))
00472 
00473     return self
00474 
00475 class csv_reader(object):
00476   """
00477   Reader for tab-delimited text that's (slightly) friendlier than the
00478   stock Python csv module (which isn't intended for direct use by
00479   humans anyway, and neither was this package originally, but that
00480   seems to be the way that it has evolved...).
00481 
00482   Columns parameter specifies how many columns users of the reader
00483   expect to see; lines with fewer columns will be padded with None
00484   values.
00485 
00486   Original API design for this class courtesy of Warren Kumari, but
00487   don't blame him if you don't like what I did with his ideas.
00488   """
00489 
00490   def __init__(self, filename, columns = None, min_columns = None, comment_characters = "#;"):
00491     assert columns is None or isinstance(columns, int)
00492     assert min_columns is None or isinstance(min_columns, int)
00493     if columns is not None and min_columns is None:
00494       min_columns = columns
00495     self.filename = filename
00496     self.columns = columns
00497     self.min_columns = min_columns
00498     self.comment_characters = comment_characters 
00499     self.file = open(filename, "r")
00500 
00501   def __iter__(self):
00502     line_number = 0
00503     for line in self.file:
00504       line_number += 1
00505       line = line.strip()
00506       if not line or line[0] in self.comment_characters:
00507         continue
00508       fields = line.split()
00509       if self.min_columns is not None and len(fields) < self.min_columns:
00510         raise RuntimeError, "%s:%d: Not enough columns in line %r" % (self.filename, line_number, line)
00511       if self.columns is not None and len(fields) > self.columns:
00512         raise RuntimeError, "%s:%d: Too many  columns in line %r" % (self.filename, line_number, line)
00513       if self.columns is not None and len(fields) < self.columns:
00514         fields += tuple(None for i in xrange(self.columns - len(fields)))
00515       yield fields
00516 
00517 class csv_writer(object):
00518   """
00519   Writer object for tab delimited text.  We just use the stock CSV
00520   module in excel-tab mode for this.
00521 
00522   If "renmwo" is set (default), the file will be written to
00523   a temporary name and renamed to the real filename after closing.
00524   """
00525 
00526   def __init__(self, filename, renmwo = True):
00527     self.filename = filename
00528     self.renmwo = "%s.~renmwo%d~" % (filename, os.getpid()) if renmwo else filename
00529     self.file = open(self.renmwo, "w")
00530     self.writer = csv.writer(self.file, dialect = csv.get_dialect("excel-tab"))
00531 
00532   def close(self):
00533     """
00534     Close this writer.
00535     """
00536     if self.file is not None:
00537       self.file.close()
00538       self.file = None
00539       if self.filename != self.renmwo:
00540         os.rename(self.renmwo, self.filename)
00541 
00542   def __getattr__(self, attr):
00543     """
00544     Fake inheritance from whatever object csv.writer deigns to give us.
00545     """
00546     return getattr(self.writer, attr)
00547 
00548 def PEMBase64(filename):
00549   """
00550   Extract Base64 encoded data from a PEM file.
00551   """
00552   lines = open(filename).readlines()
00553   while lines:
00554     if lines.pop(0).startswith("-----BEGIN "):
00555       break
00556   while lines:
00557     if lines.pop(-1).startswith("-----END "):
00558       break
00559   return "".join(lines)
00560 
00561 def PEMElement(e, tag, filename, **kwargs):
00562   """
00563   Create an XML element containing Base64 encoded data taken from a
00564   PEM file.
00565   """
00566   if e.text is None:
00567     e.text = "\n"
00568   se = SubElement(e, tag, **kwargs)
00569   se.text = "\n" + PEMBase64(filename)
00570   se.tail = "\n"
00571   return se
00572 
00573 class CA(object):
00574   """
00575   Representation of one certification authority.
00576   """
00577 
00578   # Mapping of path restriction values we use to OpenSSL config file
00579   # section names.
00580 
00581   path_restriction = { 0 : "ca_x509_ext_xcert0",
00582                        1 : "ca_x509_ext_xcert1" }
00583 
00584   def __init__(self, cfg_file, dir):
00585     self.cfg    = cfg_file
00586     self.dir    = dir
00587     self.cer    = dir + "/ca.cer"
00588     self.key    = dir + "/ca.key"
00589     self.req    = dir + "/ca.req"
00590     self.crl    = dir + "/ca.crl"
00591     self.index  = dir + "/index"
00592     self.serial = dir + "/serial"
00593     self.crlnum = dir + "/crl_number"
00594 
00595     cfg = rpki.config.parser(cfg_file, "myrpki")
00596     self.openssl = cfg.get("openssl", "openssl")
00597 
00598     self.env = { "PATH" : os.environ["PATH"],
00599                  "BPKI_DIRECTORY" : dir,
00600                  "RANDFILE" : ".OpenSSL.whines.unless.I.set.this",
00601                  "OPENSSL_CONF" : cfg_file }
00602 
00603   def run_openssl(self, *cmd, **kwargs):
00604     """
00605     Run an OpenSSL command, suppresses stderr unless OpenSSL returns
00606     failure, and returns stdout.
00607     """
00608     stdin = kwargs.pop("stdin", None)
00609     env = self.env.copy()
00610     env.update(kwargs)
00611     cmd = (self.openssl,) + cmd
00612     p = subprocess.Popen(cmd, env = env, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
00613     stdout, stderr = p.communicate(stdin)
00614     if p.wait() != 0:
00615       sys.stderr.write("OpenSSL command failed: " + stderr + "\n")
00616       raise subprocess.CalledProcessError(returncode = p.returncode, cmd = cmd)
00617     return stdout
00618 
00619   def run_ca(self, *args):
00620     """
00621     Run OpenSSL "ca" command with common initial arguments.
00622     """
00623     self.run_openssl("ca", "-batch", "-config",  self.cfg, *args)
00624 
00625   def run_req(self, key_file, req_file, log_key = sys.stdout):
00626     """
00627     Run OpenSSL "genrsa" and  "req" commands.
00628     """
00629     if not os.path.exists(key_file):
00630       if log_key:
00631         log_key.write("Generating 2048-bit RSA key %s\n" % os.path.realpath(key_file))
00632       self.run_openssl("genrsa", "-out", key_file, "2048")
00633     if not os.path.exists(req_file):
00634       self.run_openssl("req", "-new", "-sha256", "-config", self.cfg, "-key", key_file, "-out", req_file)
00635 
00636   def run_dgst(self, input, algorithm = "md5"):
00637     """
00638     Run OpenSSL "dgst" command, return cleaned-up result.
00639     """
00640     hash = self.run_openssl("dgst", "-" + algorithm, stdin = input)
00641     #
00642     # Twits just couldn't leave well enough alone, grr.
00643     hash = "".join(hash.split())
00644     if hash.startswith("(stdin)="):
00645       hash =  hash[len("(stdin)="):]
00646     return hash
00647 
00648   @staticmethod
00649   def touch_file(filename, content = None):
00650     """
00651     Create dumb little text files expected by OpenSSL "ca" utility.
00652     """
00653     if not os.path.exists(filename):
00654       f = open(filename, "w")
00655       if content is not None:
00656         f.write(content)
00657       f.close()
00658 
00659   def setup(self, ca_name):
00660     """
00661     Set up this CA.  ca_name is an X.509 distinguished name in
00662     /tag=val/tag=val format.
00663     """
00664 
00665     modified = False
00666 
00667     if not os.path.exists(self.dir):
00668       os.makedirs(self.dir)
00669       self.touch_file(self.index)
00670       self.touch_file(self.serial, "01\n")
00671       self.touch_file(self.crlnum, "01\n")
00672 
00673     self.run_req(key_file = self.key, req_file = self.req)
00674 
00675     if not os.path.exists(self.cer):
00676       modified = True
00677       self.run_ca("-selfsign", "-extensions", "ca_x509_ext_ca", "-subj", ca_name, "-in", self.req, "-out", self.cer)
00678 
00679     if not os.path.exists(self.crl):
00680       modified = True
00681       self.run_ca("-gencrl", "-out", self.crl)
00682 
00683     return modified
00684 
00685   def ee(self, ee_name, base_name):
00686     """
00687     Issue an end-enity certificate.
00688     """
00689     key_file = "%s/%s.key" % (self.dir, base_name)
00690     req_file = "%s/%s.req" % (self.dir, base_name)
00691     cer_file = "%s/%s.cer" % (self.dir, base_name)
00692     self.run_req(key_file = key_file, req_file = req_file)
00693     if not os.path.exists(cer_file):
00694       self.run_ca("-extensions", "ca_x509_ext_ee", "-subj", ee_name, "-in", req_file, "-out", cer_file)
00695       return True
00696     else:
00697       return False
00698 
00699   def cms_xml_sign(self, ee_name, base_name, elt):
00700     """
00701     Sign an XML object with CMS, return Base64 text.
00702     """
00703     self.ee(ee_name, base_name)
00704     return base64.b64encode(self.run_openssl(
00705       "cms", "-sign", "-binary", "-outform", "DER",
00706       "-keyid", "-md", "sha256", "-nodetach", "-nosmimecap",
00707       "-econtent_type", ".".join(str(i) for i in rpki.oids.name2oid["id-ct-xml"]),
00708       "-inkey",  "%s/%s.key" % (self.dir, base_name),
00709       "-signer", "%s/%s.cer" % (self.dir, base_name),
00710       stdin = ElementToString(etree_pre_write(elt))))
00711 
00712   def cms_xml_verify(self, b64, ca):
00713     """
00714     Attempt to verify and extract XML from a Base64-encoded signed CMS
00715     object.  CA is the filename of a certificate that we expect to be
00716     the issuer of the EE certificate bundled with the CMS, and must
00717     previously have been cross-certified under our trust anchor.
00718     """
00719     # In theory, we should be able to use the -certfile parameter to
00720     # pass in the CA certificate, but in practice, I have never gotten
00721     # this to work, either with the command line tool or in the
00722     # OpenSSL C API.  Dunno why.  Passing both TA and CA via -CAfile
00723     # does work, so we do that, using a temporary file, sigh.
00724     CAfile = os.path.join(self.dir, "temp.%s.pem" % os.getpid())
00725     try:
00726       f = open(CAfile, "w")
00727       f.write(open(self.cer).read())
00728       f.write(open(ca).read())
00729       f.close()
00730       return etree_post_read(ElementFromString(self.run_openssl(
00731         "cms", "-verify", "-inform", "DER", "-CAfile", CAfile,
00732         stdin = base64.b64decode(b64))))
00733     finally:
00734       if os.path.exists(CAfile):
00735         os.unlink(CAfile)
00736 
00737   def bsc(self, pkcs10):
00738     """
00739     Issue BSC certificate, if we have a PKCS #10 request for it.
00740     """
00741 
00742     if pkcs10 is None:
00743       return None, None
00744 
00745     pkcs10 = base64.b64decode(pkcs10)
00746 
00747     hash = self.run_dgst(pkcs10)
00748 
00749     req_file = "%s/bsc.%s.req" % (self.dir, hash)
00750     cer_file = "%s/bsc.%s.cer" % (self.dir, hash)
00751 
00752     if not os.path.exists(cer_file):
00753       self.run_openssl("req", "-inform", "DER", "-out", req_file, stdin = pkcs10)
00754       self.run_ca("-extensions", "ca_x509_ext_ee", "-in", req_file, "-out", cer_file)
00755 
00756     return req_file, cer_file
00757 
00758   def fxcert(self, b64, filename = None, path_restriction = 0):
00759     """
00760     Write PEM certificate to file, then cross-certify.
00761     """
00762     fn = os.path.join(self.dir, filename or "temp.%s.cer" % os.getpid())
00763     try:
00764       self.run_openssl("x509", "-inform", "DER", "-out", fn,
00765                        stdin = base64.b64decode(b64))
00766       return self.xcert(fn, path_restriction)
00767     finally:
00768       if not filename and os.path.exists(fn):
00769         os.unlink(fn)
00770 
00771   def xcert_filename(self, cert):
00772     """
00773     Generate filename for a cross-certification.
00774 
00775     Extracts public key and subject name from PEM file and hash it so
00776     we can use the result as a tag for cross-certifying this cert.
00777     """
00778 
00779     if cert and os.path.exists(cert):
00780       return "%s/xcert.%s.cer" % (self.dir, self.run_dgst(self.run_openssl(
00781         "x509", "-noout", "-pubkey", "-subject", "-in", cert)).strip())
00782     else:
00783       return None
00784 
00785   def xcert(self, cert, path_restriction = 0):
00786     """
00787     Cross-certify a certificate represented as a PEM file, if we
00788     haven't already.  This only works for self-signed certs, due to
00789     limitations of the OpenSSL command line tool, but that suffices
00790     for our purposes.
00791     """
00792 
00793     xcert = self.xcert_filename(cert)
00794     if not os.path.exists(xcert):
00795       self.run_ca("-ss_cert", cert, "-out", xcert, "-extensions", self.path_restriction[path_restriction])
00796     return xcert
00797 
00798   def xcert_revoke(self, cert):
00799     """
00800     Revoke a cross-certification and regenerate CRL.
00801     """
00802 
00803     xcert = self.xcert_filename(cert)
00804     if xcert:
00805       self.run_ca("-revoke", xcert)
00806       self.run_ca("-gencrl", "-out", self.crl)
00807 
00808 def etree_validate(e):
00809   # This is a kludge, schema should be loaded as module or configured
00810   # in .conf, but it will do as a temporary debugging hack.
00811   schema = os.getenv("MYRPKI_RNG")
00812   if schema:
00813     try:
00814       import lxml.etree
00815     except ImportError:
00816       return
00817     try:
00818       lxml.etree.RelaxNG(file = schema).assertValid(e)
00819     except lxml.etree.RelaxNGParseError:
00820       return
00821     except lxml.etree.DocumentInvalid:
00822       print lxml.etree.tostring(e, pretty_print = True)
00823       raise
00824 
00825 def etree_write(e, filename, verbose = False, validate = True, msg = None):
00826   """
00827   Write out an etree to a file, safely.
00828 
00829   I still miss SYSCAL(RENMWO).
00830   """
00831   filename = os.path.realpath(filename)
00832   tempname = filename
00833   if not filename.startswith("/dev/"):
00834     tempname += ".tmp"
00835   if verbose or msg:
00836     print "Writing", filename
00837   if msg:
00838     print msg
00839   e = etree_pre_write(e, validate)
00840   ElementTree(e).write(tempname)
00841   if tempname != filename:
00842     os.rename(tempname, filename)
00843 
00844 def etree_pre_write(e, validate = True):
00845   """
00846   Do the namespace frobbing needed on write; broken out of
00847   etree_write() because also needed with ElementToString().
00848   """
00849   e = copy.deepcopy(e)
00850   e.set("version", version)
00851   for i in e.getiterator():
00852     if i.tag[0] != "{":
00853       i.tag = namespaceQName + i.tag
00854     assert i.tag.startswith(namespaceQName)
00855   if validate:
00856     etree_validate(e)
00857   return e
00858 
00859 def etree_read(filename, verbose = False, validate = True):
00860   """
00861   Read an etree from a file, verifying then stripping XML namespace
00862   cruft.
00863   """
00864   if verbose:
00865     print "Reading", filename
00866   e = ElementTree(file = filename).getroot()
00867   return etree_post_read(e, validate)
00868 
00869 def etree_post_read(e, validate = True):
00870   """
00871   Do the namespace frobbing needed on read; broken out of etree_read()
00872   beause also needed by ElementFromString().
00873   """
00874   if validate:
00875     etree_validate(e)
00876   for i in e.getiterator():
00877     if i.tag.startswith(namespaceQName):
00878       i.tag = i.tag[len(namespaceQName):]
00879     else:
00880       raise RuntimeError, "XML tag %r is not in namespace %r" % (i.tag, namespace)
00881   return e
00882 
00883 def b64_equal(thing1, thing2):
00884   """
00885   Compare two Base64-encoded values for equality.
00886   """
00887   return "".join(thing1.split()) == "".join(thing2.split())
00888 
00889 
00890 
00891 class IRDB(object):
00892   """
00893   Front-end to the IRDB.  This is broken out from class main so
00894   that other applications (namely, the portal-gui) can reuse it.
00895   """
00896   def __init__(self, cfg):
00897     """
00898     Opens a new connection to the IRDB, using the configuration
00899     information from a rpki.config.parser object.
00900     """
00901 
00902     if hasattr(warnings, "catch_warnings"):
00903       with warnings.catch_warnings():
00904         warnings.simplefilter("ignore", DeprecationWarning)
00905         import MySQLdb
00906     else:
00907       import MySQLdb
00908 
00909     irdbd_cfg = rpki.config.parser(cfg.get("irdbd_conf", cfg.filename), "irdbd")
00910 
00911     self.db = MySQLdb.connect(user   = irdbd_cfg.get("sql-username"),
00912                               db     = irdbd_cfg.get("sql-database"),
00913                               passwd = irdbd_cfg.get("sql-password"))
00914 
00915   def update(self, handle, roa_requests, children, ghostbusters=None):
00916     """
00917     Update the IRDB for a given resource handle.  Removes all
00918     existing data and replaces it with that specified in the
00919     argument list.
00920 
00921     The "roa_requests" argument is a sequence of tuples of the form
00922     (asID, v4_addresses, v6_addresses), where "v*_addresses" are
00923     instances of rpki.resource_set.roa_prefix_set_ipv*.
00924 
00925     The "children" argument is a sequence of tuples of the form
00926     (child_handle, asns, v4addrs, v6addrs, valid_until),
00927     where "asns" is an instance of rpki.resource_set.resource_set_asn,
00928     "v*addrs" are instances of rpki.resource_set.resource_set_ipv*,
00929     and "valid_until" is an instance of rpki.sundial.datetime.
00930 
00931     The "ghostbusters" argument is a sequence of tuples of the form
00932     (parent_handle, vcard_string).  "parent_handle" may be value None,
00933     in which case the specified vcard object will be used for all
00934     parents.
00935     """
00936 
00937     cur = self.db.cursor()
00938 
00939     cur.execute(
00940       """
00941       DELETE
00942       FROM  roa_request_prefix
00943       USING roa_request, roa_request_prefix
00944       WHERE roa_request.roa_request_id = roa_request_prefix.roa_request_id AND roa_request.roa_request_handle = %s
00945       """, (handle,))
00946 
00947     cur.execute("DELETE FROM roa_request WHERE roa_request.roa_request_handle = %s", (handle,))
00948 
00949     for asID, v4addrs, v6addrs in roa_requests:
00950       assert isinstance(v4addrs, rpki.resource_set.roa_prefix_set_ipv4)
00951       assert isinstance(v6addrs, rpki.resource_set.roa_prefix_set_ipv6)
00952       cur.execute("INSERT roa_request (roa_request_handle, asn) VALUES (%s, %s)", (handle, asID))
00953       roa_request_id = cur.lastrowid
00954       for version, prefix_set in ((4, v4addrs), (6, v6addrs)):
00955         if prefix_set:
00956           cur.executemany("INSERT roa_request_prefix (roa_request_id, prefix, prefixlen, max_prefixlen, version) VALUES (%s, %s, %s, %s, %s)",
00957                           ((roa_request_id, p.prefix, p.prefixlen, p.max_prefixlen, version) for p in prefix_set))
00958 
00959     cur.execute(
00960       """
00961       DELETE
00962       FROM   registrant_asn
00963       USING registrant, registrant_asn
00964       WHERE registrant.registrant_id = registrant_asn.registrant_id AND registrant.registry_handle = %s
00965       """ , (handle,))
00966 
00967     cur.execute(
00968       """
00969       DELETE FROM registrant_net USING registrant, registrant_net
00970       WHERE registrant.registrant_id = registrant_net.registrant_id AND registrant.registry_handle = %s
00971       """ , (handle,))
00972 
00973     cur.execute("DELETE FROM registrant WHERE registrant.registry_handle = %s" , (handle,))
00974 
00975     for child_handle, asns, ipv4, ipv6, valid_until in children:
00976       cur.execute("INSERT registrant (registrant_handle, registry_handle, registrant_name, valid_until) VALUES (%s, %s, %s, %s)",
00977                   (child_handle, handle, child_handle, valid_until.to_sql()))
00978       child_id = cur.lastrowid
00979       if asns:
00980         cur.executemany("INSERT registrant_asn (start_as, end_as, registrant_id) VALUES (%s, %s, %s)",
00981                         ((a.min, a.max, child_id) for a in asns))
00982       if ipv4:
00983         cur.executemany("INSERT registrant_net (start_ip, end_ip, version, registrant_id) VALUES (%s, %s, 4, %s)",
00984                         ((a.min, a.max, child_id) for a in ipv4))
00985       if ipv6:
00986         cur.executemany("INSERT registrant_net (start_ip, end_ip, version, registrant_id) VALUES (%s, %s, 6, %s)",
00987                         ((a.min, a.max, child_id) for a in ipv6))
00988 
00989     # don't munge the ghostbuster_request table when the arg is None.
00990     # this allows the cli to safely run configure_resources without
00991     # stomping on GBRs created by the portal gui.
00992     if ghostbusters is not None:
00993       cur.execute("DELETE FROM ghostbuster_request WHERE self_handle = %s", (handle,))
00994       if ghostbusters:
00995         cur.executemany("INSERT INTO ghostbuster_request (self_handle, parent_handle, vcard) VALUES (%s, %s, %s)",
00996                         ((handle, parent_handle, vcard) for parent_handle, vcard in ghostbusters))
00997 
00998     self.db.commit()
00999 
01000   def close(self):
01001     """
01002     Close the connection to the IRDB.
01003     """
01004     self.db.close()
01005     
01006 
01007 
01008 class main(rpki.cli.Cmd):
01009 
01010   prompt = "myrpki> "
01011 
01012   completedefault = rpki.cli.Cmd.filename_complete
01013 
01014   show_xml = False
01015 
01016   def __init__(self):
01017     os.environ["TZ"] = "UTC"
01018     time.tzset()
01019 
01020     rpki.log.use_syslog = False
01021 
01022     self.cfg_file = None
01023 
01024     opts, argv = getopt.getopt(sys.argv[1:], "c:h?", ["config=", "help"])
01025     for o, a in opts:
01026       if o in ("-c", "--config"):
01027         self.cfg_file = a
01028       elif o in ("-h", "--help", "-?"):
01029         argv = ["help"]
01030 
01031     if not argv or argv[0] != "help":
01032       rpki.log.init("myrpki")
01033       self.read_config()
01034 
01035     rpki.cli.Cmd.__init__(self, argv)
01036 
01037 
01038   def help_overview(self):
01039     """
01040     Show program __doc__ string.  Perhaps there's some clever way to
01041     do this using the textwrap module, but for now something simple
01042     and crude will suffice.
01043     """
01044     for line in __doc__.splitlines(True):
01045       self.stdout.write(" " * 4 + line)
01046     self.stdout.write("\n")
01047 
01048   def entitydb_complete(self, prefix, text, line, begidx, endidx):
01049     """
01050     Completion helper for entitydb filenames.
01051     """
01052     names = []
01053     for name in self.entitydb.iterate(prefix):
01054       name = os.path.splitext(os.path.basename(name))[0]
01055       if name.startswith(text):
01056         names.append(name)
01057     return names
01058 
01059   def read_config(self):
01060 
01061     self.cfg = rpki.config.parser(self.cfg_file, "myrpki")
01062 
01063     self.histfile  = self.cfg.get("history_file", ".myrpki_history")
01064     self.handle    = self.cfg.get("handle")
01065     self.run_rpkid = self.cfg.getboolean("run_rpkid")
01066     self.run_pubd  = self.cfg.getboolean("run_pubd")
01067     self.run_rootd = self.cfg.getboolean("run_rootd")
01068     self.entitydb  = EntityDB(self.cfg)
01069 
01070     if self.run_rootd and (not self.run_pubd or not self.run_rpkid):
01071       raise RuntimeError, "Can't run rootd unless also running rpkid and pubd"
01072 
01073     self.bpki_resources = CA(self.cfg.filename, self.cfg.get("bpki_resources_directory"))
01074     if self.run_rpkid or self.run_pubd or self.run_rootd:
01075       self.bpki_servers = CA(self.cfg.filename, self.cfg.get("bpki_servers_directory"))
01076 
01077     self.default_repository = self.cfg.get("default_repository", "")
01078     self.pubd_contact_info = self.cfg.get("pubd_contact_info", "")
01079 
01080     self.rsync_module = self.cfg.get("publication_rsync_module")
01081     self.rsync_server = self.cfg.get("publication_rsync_server")
01082 
01083 
01084   def do_initialize(self, arg):
01085     """
01086     Initialize an RPKI installation.  This command reads the
01087     configuration file, creates the BPKI and EntityDB directories,
01088     generates the initial BPKI certificates, and creates an XML file
01089     describing the resource-holding aspect of this RPKI installation.
01090     """
01091 
01092     if arg:
01093       raise RuntimeError, "This command takes no arguments"
01094 
01095     self.bpki_resources.setup(self.cfg.get("bpki_resources_ta_dn",
01096                                            "/CN=%s BPKI Resource Trust Anchor" % self.handle))
01097     if self.run_rpkid or self.run_pubd or self.run_rootd:
01098       self.bpki_servers.setup(self.cfg.get("bpki_servers_ta_dn",
01099                                            "/CN=%s BPKI Server Trust Anchor" % self.handle))
01100 
01101     # Create entitydb directories.
01102 
01103     for i in ("parents", "children", "repositories", "pubclients"):
01104       d = self.entitydb(i)
01105       if not os.path.exists(d):
01106         os.makedirs(d)
01107 
01108     if self.run_rpkid or self.run_pubd or self.run_rootd:
01109 
01110       if self.run_rpkid:
01111         self.bpki_servers.ee(self.cfg.get("bpki_rpkid_ee_dn",
01112                                           "/CN=%s rpkid server certificate" % self.handle), "rpkid")
01113         self.bpki_servers.ee(self.cfg.get("bpki_irdbd_ee_dn",
01114                                           "/CN=%s irdbd server certificate" % self.handle), "irdbd")
01115       if self.run_pubd:
01116         self.bpki_servers.ee(self.cfg.get("bpki_pubd_ee_dn",
01117                                           "/CN=%s pubd server certificate" % self.handle), "pubd")
01118       if self.run_rpkid or self.run_pubd:
01119         self.bpki_servers.ee(self.cfg.get("bpki_irbe_ee_dn",
01120                                           "/CN=%s irbe client certificate" % self.handle), "irbe")
01121       if self.run_rootd:
01122         self.bpki_servers.ee(self.cfg.get("bpki_rootd_ee_dn",
01123                                           "/CN=%s rootd server certificate" % self.handle), "rootd")
01124 
01125     # Build the identity.xml file.  Need to check for existing file so we don't
01126     # overwrite?  Worry about that later.
01127 
01128     e = Element("identity", handle = self.handle)
01129     PEMElement(e, "bpki_ta", self.bpki_resources.cer)
01130     etree_write(e, self.entitydb.identity,
01131                 msg = None if self.run_rootd else 'This is the "identity" file you will need to send to your parent')
01132 
01133     # If we're running rootd, construct a fake parent to go with it,
01134     # and cross-certify in both directions so we can talk to rootd.
01135 
01136     if self.run_rootd:
01137 
01138       e = Element("parent", parent_handle = self.handle, child_handle = self.handle,
01139                   service_uri = "http://localhost:%s/" % self.cfg.get("rootd_server_port"),
01140                   valid_until = str(rpki.sundial.now() + rpki.sundial.timedelta(days = 365)))
01141       PEMElement(e, "bpki_resource_ta", self.bpki_servers.cer)
01142       PEMElement(e, "bpki_child_ta", self.bpki_resources.cer)
01143       SubElement(e, "repository", type = "offer")
01144       etree_write(e, self.entitydb("parents", self.handle))
01145 
01146       self.bpki_resources.xcert(self.bpki_servers.cer)
01147 
01148       rootd_child_fn = self.cfg.get("child-bpki-cert", None, "rootd")
01149       if not os.path.exists(rootd_child_fn):
01150         os.link(self.bpki_servers.xcert(self.bpki_resources.cer), rootd_child_fn)
01151 
01152       repo_file_name = self.entitydb("repositories", self.handle)
01153 
01154       try:
01155         want_offer = etree_read(repo_file_name).get("type") != "confirmed"
01156       except IOError:
01157         want_offer = True
01158 
01159       if want_offer:
01160         e = Element("repository", type = "offer", handle = self.handle, parent_handle = self.handle)
01161         PEMElement(e, "bpki_client_ta", self.bpki_resources.cer)
01162         etree_write(e, repo_file_name,
01163                     msg = 'This is the "repository offer" file for you to use if you want to publish in your own repository')
01164 
01165 
01166   def do_update_bpki(self, arg):
01167     """
01168     Update BPKI certificates.  Assumes an existing RPKI installation.
01169 
01170     Basic plan here is to reissue all BPKI certificates we can, right
01171     now.  In the long run we might want to be more clever about only
01172     touching ones that need maintenance, but this will do for a start.
01173 
01174     Most likely this should be run under cron.
01175     """
01176 
01177     if self.bpki_servers:
01178       bpkis = (self.bpki_resources, self.bpki_servers)
01179     else:
01180       bpkis = (self.bpki_resources,)
01181 
01182     for bpki in bpkis:
01183       for cer in glob.iglob("%s/*.cer" % bpki.dir):
01184         key = cer[0:-4] + ".key"
01185         req = cer[0:-4] + ".req"
01186         if os.path.exists(key):
01187           print "Regenerating BPKI PKCS #10", req
01188           bpki.run_openssl("x509", "-x509toreq", "-in", cer, "-out", req, "-signkey", key)
01189         print "Clearing BPKI certificate", cer
01190         os.unlink(cer)
01191         if cer == bpki.cer:
01192           assert req == bpki.req
01193           print "Regenerating certificate", cer
01194           bpki.run_ca("-selfsign", "-extensions", "ca_x509_ext_ca", "-in", req, "-out", cer)
01195 
01196     print "Regenerating CRLs"
01197     for bpki in bpkis:
01198       bpki.run_ca("-gencrl", "-out", bpki.crl)
01199 
01200     self.do_initialize(None)
01201     if self.run_rpkid or self.run_pubd or self.run_rootd:
01202       self.do_configure_daemons(arg)
01203     else:
01204       self.do_configure_resources(None)
01205 
01206 
01207   def do_configure_child(self, arg):
01208     """
01209     Configure a new child of this RPKI entity, given the child's XML
01210     identity file as an input.  This command extracts the child's data
01211     from the XML, cross-certifies the child's resource-holding BPKI
01212     certificate, and generates an XML file describing the relationship
01213     between the child and this parent, including this parent's BPKI
01214     data and up-down protocol service URI.
01215     """
01216 
01217     child_handle = None
01218 
01219     opts, argv = getopt.getopt(arg.split(), "", ["child_handle="])
01220     for o, a in opts:
01221       if o == "--child_handle":
01222         child_handle = a
01223     
01224     if len(argv) != 1:
01225       raise RuntimeError, "Need to specify filename for child.xml"
01226 
01227     c = etree_read(argv[0])
01228 
01229     if child_handle is None:
01230       child_handle = c.get("handle")
01231 
01232     try:
01233       e = etree_read(self.cfg.get("xml_filename"))
01234       service_uri_base = e.get("service_uri")
01235 
01236     except IOError:
01237       if self.run_rpkid:
01238         service_uri_base = "http://%s:%s/up-down/%s" % (self.cfg.get("rpkid_server_host"),
01239                                                         self.cfg.get("rpkid_server_port"),
01240                                                         self.handle)
01241       else:
01242         service_uri_base = None      
01243 
01244     if not service_uri_base:
01245       print "Sorry, you can't set up children of a hosted config that itself has not yet been set up"
01246       return
01247 
01248     print "Child calls itself %r, we call it %r" % (c.get("handle"), child_handle)
01249 
01250     if self.run_rpkid or self.run_pubd or self.run_rootd:
01251       self.bpki_servers.fxcert(c.findtext("bpki_ta"))
01252 
01253     e = Element("parent", parent_handle = self.handle, child_handle = child_handle,
01254                 service_uri = "%s/%s" % (service_uri_base, child_handle),
01255                 valid_until = str(rpki.sundial.now() + rpki.sundial.timedelta(days = 365)))
01256 
01257     PEMElement(e, "bpki_resource_ta", self.bpki_resources.cer)
01258     SubElement(e, "bpki_child_ta").text = c.findtext("bpki_ta")
01259 
01260     repo = None
01261     for f in self.entitydb.iterate("repositories"):
01262       r = etree_read(f)
01263       if r.get("type") == "confirmed":
01264         h = os.path.splitext(os.path.split(f)[-1])[0]
01265         if repo is None or h == self.default_repository:
01266           repo_handle = h
01267           repo = r
01268 
01269     if repo is None:
01270       print "Couldn't find any usable repositories, not giving referral"
01271 
01272     elif repo_handle == self.handle:
01273       SubElement(e, "repository", type = "offer")
01274 
01275     else:
01276       proposed_sia_base = repo.get("sia_base") + child_handle + "/"
01277       r = Element("referral", authorized_sia_base = proposed_sia_base)
01278       r.text = c.findtext("bpki_ta")
01279       auth = self.bpki_resources.cms_xml_sign(
01280         "/CN=%s Publication Referral" % self.handle, "referral", r)
01281 
01282       r = SubElement(e, "repository", type = "referral")
01283       SubElement(r, "authorization", referrer = repo.get("client_handle")).text = auth
01284       SubElement(r, "contact_info").text = repo.findtext("contact_info")
01285 
01286     etree_write(e, self.entitydb("children", child_handle),
01287                 msg = "Send this file back to the child you just configured")
01288 
01289 
01290   def do_delete_child(self, arg):
01291     """
01292     Delete a child of this RPKI entity.
01293 
01294     This should check that the XML file it's deleting really is a
01295     child, but doesn't, yet.
01296     """
01297 
01298     try:
01299       os.unlink(self.entitydb("children", arg))
01300     except OSError:
01301       print "No such child \"%s\"" % arg
01302 
01303   def complete_delete_child(self, *args):
01304     return self.entitydb_complete("children", *args)
01305 
01306 
01307   def do_configure_parent(self, arg):
01308     """
01309     Configure a new parent of this RPKI entity, given the output of
01310     the parent's configure_child command as input.  This command reads
01311     the parent's response XML, extracts the parent's BPKI and service
01312     URI information, cross-certifies the parent's BPKI data into this
01313     entity's BPKI, and checks for offers or referrals of publication
01314     service.  If a publication offer or referral is present, we
01315     generate a request-for-service message to that repository, in case
01316     the user wants to avail herself of the referral or offer.
01317     """
01318 
01319     parent_handle = None
01320 
01321     opts, argv = getopt.getopt(arg.split(), "", ["parent_handle="])
01322     for o, a in opts:
01323       if o == "--parent_handle":
01324         parent_handle = a
01325 
01326     if len(argv) != 1:
01327       raise RuntimeError, "Need to specify filename for parent.xml on command line"
01328 
01329     p = etree_read(argv[0])
01330 
01331     if parent_handle is None:
01332       parent_handle = p.get("parent_handle")
01333 
01334     print "Parent calls itself %r, we call it %r" % (p.get("parent_handle"), parent_handle)
01335     print "Parent calls us %r" % p.get("child_handle")
01336 
01337     self.bpki_resources.fxcert(p.findtext("bpki_resource_ta"))
01338 
01339     etree_write(p, self.entitydb("parents", parent_handle))
01340 
01341     r = p.find("repository")
01342 
01343     if r is None or r.get("type") not in ("offer", "referral"):
01344       r = Element("repository", type = "none")
01345 
01346     r.set("handle", self.handle)
01347     r.set("parent_handle", parent_handle)
01348     PEMElement(r, "bpki_client_ta", self.bpki_resources.cer)
01349     etree_write(r, self.entitydb("repositories", parent_handle),
01350                 msg = "This is the file to send to the repository operator")
01351 
01352 
01353   def do_delete_parent(self, arg):
01354     """
01355     Delete a parent of this RPKI entity.
01356 
01357     This should check that the XML file it's deleting really is a
01358     parent, but doesn't, yet.
01359     """
01360 
01361     try:
01362       os.unlink(self.entitydb("parents", arg))
01363     except OSError:
01364       print "No such parent \"%s\"" % arg
01365 
01366   def complete_delete_parent(self, *args):
01367     return self.entitydb_complete("parents", *args)
01368 
01369 
01370   def do_configure_publication_client(self, arg):
01371     """
01372     Configure publication server to know about a new client, given the
01373     client's request-for-service message as input.  This command reads
01374     the client's request for service, cross-certifies the client's
01375     BPKI data, and generates a response message containing the
01376     repository's BPKI data and service URI.
01377     """
01378 
01379     sia_base = None
01380 
01381     opts, argv = getopt.getopt(arg.split(), "", ["sia_base="])
01382     for o, a in opts:
01383       if o == "--sia_base":
01384         sia_base = a
01385     
01386     if len(argv) != 1:
01387       raise RuntimeError, "Need to specify filename for client.xml"
01388 
01389     client = etree_read(argv[0])
01390 
01391     if sia_base is None and client.get("handle") == self.handle and b64_equal(PEMBase64(self.bpki_resources.cer), client.findtext("bpki_client_ta")):
01392       print "This looks like self-hosted publication"
01393       sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, self.handle)
01394 
01395     if sia_base is None and client.get("type") == "referral":
01396       print "This looks like a referral, checking"
01397       try:
01398         auth = client.find("authorization")
01399         if auth is None:
01400           raise RuntimeError, "Malformed referral, couldn't find <auth/> element"
01401         referrer = etree_read(self.entitydb("pubclients", auth.get("referrer").replace("/",".")))
01402         referrer = self.bpki_servers.fxcert(referrer.findtext("bpki_client_ta"))
01403         referral = self.bpki_servers.cms_xml_verify(auth.text, referrer)
01404         if not b64_equal(referral.text, client.findtext("bpki_client_ta")):
01405           raise RuntimeError, "Referral trust anchor does not match"
01406         sia_base = referral.get("authorized_sia_base")
01407       except IOError:
01408         print "We have no record of client (%s) alleged to have made this referral" % auth.get("referrer")
01409 
01410     if sia_base is None and client.get("type") == "offer" and client.get("parent_handle") == self.handle:
01411       print "This looks like an offer, client claims to be our child, checking"
01412       client_ta = client.findtext("bpki_client_ta")
01413       if not client_ta:
01414         raise RuntimeError, "Malformed offer, couldn't find <bpki_client_ta/> element"
01415       for child in self.entitydb.iterate("children"):
01416         c = etree_read(child)
01417         if b64_equal(c.findtext("bpki_child_ta"), client_ta):
01418           sia_base = "rsync://%s/%s/%s/%s/" % (self.rsync_server, self.rsync_module,
01419                                                self.handle, client.get("handle"))
01420           break
01421 
01422     # If we still haven't figured out what to do with this client, it
01423     # gets a top-level tree of its own, no attempt at nesting.
01424 
01425     if sia_base is None:
01426       print "Don't know where to nest this client, defaulting to top-level"
01427       sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, client.get("handle"))
01428       
01429     if not sia_base.startswith("rsync://"):
01430       raise RuntimeError, "Malformed sia_base parameter %r, should start with 'rsync://'" % sia_base
01431 
01432     client_handle = "/".join(sia_base.rstrip("/").split("/")[4:])
01433 
01434     parent_handle = client.get("parent_handle")
01435 
01436     print "Client calls itself %r, we call it %r" % (client.get("handle"), client_handle)
01437     print "Client says its parent handle is %r" % parent_handle
01438 
01439     self.bpki_servers.fxcert(client.findtext("bpki_client_ta"))
01440 
01441     e = Element("repository", type = "confirmed",
01442                 client_handle = client_handle,
01443                 parent_handle = parent_handle,
01444                 sia_base = sia_base,
01445                 service_uri = "http://%s:%s/client/%s" % (self.cfg.get("pubd_server_host"),
01446                                                           self.cfg.get("pubd_server_port"),
01447                                                           client_handle))
01448 
01449     PEMElement(e, "bpki_server_ta", self.bpki_servers.cer)
01450     SubElement(e, "bpki_client_ta").text = client.findtext("bpki_client_ta")
01451     SubElement(e, "contact_info").text = self.pubd_contact_info
01452     etree_write(e, self.entitydb("pubclients", client_handle.replace("/", ".")),
01453                 msg = "Send this file back to the publication client you just configured")
01454 
01455 
01456   def do_delete_publication_client(self, arg):
01457     """
01458     Delete a publication client of this RPKI entity.
01459 
01460     This should check that the XML file it's deleting really is a
01461     client, but doesn't, yet.
01462     """
01463 
01464     try:
01465       os.unlink(self.entitydb("pubclients", arg))
01466     except OSError:
01467       print "No such client \"%s\"" % arg
01468 
01469   def complete_delete_publication_client(self, *args):
01470     return self.entitydb_complete("pubclients", *args)
01471 
01472 
01473   def do_configure_repository(self, arg):
01474     """
01475     Configure a publication repository for this RPKI entity, given the
01476     repository's response to our request-for-service message as input.
01477     This command reads the repository's response, extracts and
01478     cross-certifies the BPKI data and service URI, and links the
01479     repository data with the corresponding parent data in our local
01480     database.
01481     """
01482 
01483     parent_handle = None
01484 
01485     opts, argv = getopt.getopt(arg.split(), "", ["parent_handle="])
01486     for o, a in opts:
01487       if o == "--parent_handle":
01488         parent_handle = a
01489 
01490     if len(argv) != 1:
01491       raise RuntimeError, "Need to specify filename for repository.xml on command line"
01492 
01493     r = etree_read(argv[0])
01494 
01495     if parent_handle is None:
01496       parent_handle = r.get("parent_handle")
01497 
01498     print "Repository calls us %r" % (r.get("client_handle"))
01499     print "Repository response associated with parent_handle %r" % parent_handle
01500 
01501     etree_write(r, self.entitydb("repositories", parent_handle))
01502 
01503 
01504   def do_delete_repository(self, arg):
01505     """
01506     Delete a repository of this RPKI entity.
01507 
01508     This should check that the XML file it's deleting really is a
01509     repository, but doesn't, yet.
01510     """
01511 
01512     try:
01513       os.unlink(self.entitydb("repositories", arg))
01514     except OSError:
01515       print "No such repository \"%s\"" % arg
01516 
01517   def complete_delete_repository(self, *args):
01518     return self.entitydb_complete("repositories", *args)
01519 
01520 
01521   def renew_children_common(self, arg, plural):
01522     """
01523     Common code for renew_child and renew_all_children commands.
01524     """
01525 
01526     valid_until = None
01527 
01528     opts, argv = getopt.getopt(arg.split(), "", ["valid_until"])
01529     for o, a in opts:
01530       if o == "--valid_until":
01531         valid_until = a
01532 
01533     if plural:
01534       if len(argv) != 0:
01535         raise RuntimeError, "Unexpected arguments"
01536       children = "*"
01537     else:
01538       if len(argv) != 1:
01539         raise RuntimeError, "Need to specify child handle"
01540       children = argv[0]
01541 
01542     if valid_until is None:
01543       valid_until = rpki.sundial.now() + rpki.sundial.timedelta(days = 365)
01544     else:
01545       valid_until = rpki.sundial.fromXMLtime(valid_until)
01546       if valid_until < rpki.sundial.now():
01547         raise RuntimeError, "Specified new expiration time %s has passed" % valid_until
01548 
01549     print "New validity date", valid_until
01550 
01551     for f in self.entitydb.iterate("children", children):
01552       c = etree_read(f)
01553       c.set("valid_until", str(valid_until))
01554       etree_write(c, f)
01555 
01556   def do_renew_child(self, arg):
01557     """
01558     Update validity period for one child entity.
01559     """
01560     return self.renew_children_common(arg, False)
01561 
01562   def complete_renew_child(self, *args):
01563     return self.entitydb_complete("children", *args)
01564 
01565   def do_renew_all_children(self, arg):
01566     """
01567     Update validity period for all child entities.
01568     """
01569     return self.renew_children_common(arg, True)
01570 
01571 
01572 
01573 
01574   def configure_resources_main(self, msg = None):
01575     """
01576     Main program of old myrpki.py script.  This remains separate
01577     because it's called from more than one place.
01578     """
01579 
01580     roa_csv_file                  = self.cfg.get("roa_csv")
01581     prefix_csv_file               = self.cfg.get("prefix_csv")
01582     asn_csv_file                  = self.cfg.get("asn_csv")
01583 
01584     # This probably should become an argument instead of (or in
01585     # addition to a default from?) a config file option.
01586     xml_filename                  = self.cfg.get("xml_filename")
01587 
01588     try:
01589       e = etree_read(xml_filename)
01590       bsc_req, bsc_cer = self.bpki_resources.bsc(e.findtext("bpki_bsc_pkcs10"))
01591       service_uri = e.get("service_uri")
01592     except IOError:
01593       bsc_req, bsc_cer = None, None
01594       service_uri = None
01595 
01596     e = Element("myrpki", handle = self.handle)
01597 
01598     if service_uri:
01599       e.set("service_uri", service_uri)
01600 
01601     roa_requests.from_csv(roa_csv_file).xml(e)
01602 
01603     children.from_entitydb(
01604       prefix_csv_file = prefix_csv_file,
01605       asn_csv_file = asn_csv_file,
01606       fxcert = self.bpki_resources.fxcert,
01607       entitydb = self.entitydb).xml(e)
01608 
01609     parents.from_entitydb(
01610       fxcert = self.bpki_resources.fxcert,
01611       entitydb = self.entitydb).xml(e)
01612 
01613     repositories.from_entitydb(
01614       fxcert = self.bpki_resources.fxcert,
01615       entitydb = self.entitydb).xml(e)
01616 
01617     PEMElement(e, "bpki_ca_certificate", self.bpki_resources.cer)
01618     PEMElement(e, "bpki_crl",            self.bpki_resources.crl)
01619 
01620     if bsc_cer:
01621       PEMElement(e, "bpki_bsc_certificate", bsc_cer)
01622 
01623     if bsc_req:
01624       PEMElement(e, "bpki_bsc_pkcs10", bsc_req)
01625 
01626     etree_write(e, xml_filename, msg = msg)
01627 
01628 
01629   def do_configure_resources(self, arg):
01630     """
01631     Read CSV files and all the descriptions of parents and children
01632     that we've built up, package the result up as a single XML file to
01633     be shipped to a hosting rpkid.
01634     """
01635 
01636     if arg:
01637       raise RuntimeError, "Unexpected argument %r" % arg
01638     self.configure_resources_main(msg = "Send this file to the rpkid operator who is hosting you")
01639 
01640 
01641 
01642   def do_configure_daemons(self, arg):
01643     """
01644     Configure RPKI daemons with the data built up by the other
01645     commands in this program.
01646 
01647     The basic model here is that each entity with resources to certify
01648     runs the myrpki tool, but not all of them necessarily run their
01649     own RPKI engines.  The entities that do run RPKI engines get data
01650     from the entities they host via the XML files output by the
01651     configure_resources command.  Those XML files are the input to
01652     this command, which uses them to do all the work of configuring
01653     daemons, populating SQL databases, and so forth.  A few operations
01654     (eg, BSC construction) generate data which has to be shipped back
01655     to the resource holder, which we do by updating the same XML file.
01656 
01657     In essence, the XML files are a sneakernet (or email, or carrier
01658     pigeon) communication channel between the resource holders and the
01659     RPKI engine operators.
01660 
01661     As a convenience, for the normal case where the RPKI engine
01662     operator is itself a resource holder, this command in effect runs
01663     the configure_resources command automatically to process the RPKI
01664     engine operator's own resources.
01665 
01666     Note that, due to the back and forth nature of some of these
01667     operations, it may take several cycles for data structures to stablize
01668     and everything to reach a steady state.  This is normal.
01669     """
01670 
01671     argv = arg.split()
01672 
01673     try:
01674       import rpki.http, rpki.resource_set, rpki.relaxng, rpki.exceptions
01675       import rpki.left_right, rpki.x509, rpki.async
01676 
01677     except ImportError, e:
01678       print "Sorry, you appear to be missing some of the Python modules needed to run this command"
01679       print "[Error: %r]" % e
01680 
01681     def findbase64(tree, name, b64type = rpki.x509.X509):
01682       x = tree.findtext(name)
01683       return b64type(Base64 = x) if x else None
01684 
01685     # We can use a single BSC for everything -- except BSC key
01686     # rollovers.  Drive off that bridge when we get to it.
01687 
01688     bsc_handle = "bsc"
01689 
01690     self.cfg.set_global_flags()
01691 
01692     # Default values for CRL parameters are low, for testing.  Not
01693     # quite as low as they once were, too much expired CRL whining.
01694 
01695     self_crl_interval = self.cfg.getint("self_crl_interval", 2 * 60 * 60)
01696     self_regen_margin = self.cfg.getint("self_regen_margin", self_crl_interval / 4)
01697     pubd_base         = "http://%s:%s/" % (self.cfg.get("pubd_server_host"), self.cfg.get("pubd_server_port"))
01698     rpkid_base        = "http://%s:%s/" % (self.cfg.get("rpkid_server_host"), self.cfg.get("rpkid_server_port"))
01699 
01700     # Wrappers to simplify calling rpkid and pubd.
01701 
01702     call_rpkid = rpki.async.sync_wrapper(rpki.http.caller(
01703       proto       = rpki.left_right,
01704       client_key  = rpki.x509.RSA( PEM_file = self.bpki_servers.dir + "/irbe.key"),
01705       client_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/irbe.cer"),
01706       server_ta   = rpki.x509.X509(PEM_file = self.bpki_servers.cer),
01707       server_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/rpkid.cer"),
01708       url         = rpkid_base + "left-right",
01709       debug       = self.show_xml))
01710 
01711     if self.run_pubd:
01712 
01713       call_pubd = rpki.async.sync_wrapper(rpki.http.caller(
01714         proto       = rpki.publication,
01715         client_key  = rpki.x509.RSA( PEM_file = self.bpki_servers.dir + "/irbe.key"),
01716         client_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/irbe.cer"),
01717         server_ta   = rpki.x509.X509(PEM_file = self.bpki_servers.cer),
01718         server_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/pubd.cer"),
01719         url         = pubd_base + "control",
01720         debug       = self.show_xml))
01721 
01722       # Make sure that pubd's BPKI CRL is up to date.
01723 
01724       call_pubd(rpki.publication.config_elt.make_pdu(
01725         action = "set",
01726         bpki_crl = rpki.x509.CRL(PEM_file = self.bpki_servers.crl)))
01727 
01728     irdb = IRDB(self.cfg)
01729 
01730     xmlfiles = []
01731 
01732     # If [myrpki] section includes an "xml_filename" setting, run
01733     # myrpki.py internally, as a convenience, and include its output at
01734     # the head of our list of XML files to process.
01735 
01736     my_xmlfile = self.cfg.get("xml_filename", "")
01737     if my_xmlfile:
01738       self.configure_resources_main()
01739       xmlfiles.append(my_xmlfile)
01740     else:
01741       my_xmlfile = None
01742 
01743     # Add any other XML files specified on the command line
01744 
01745     xmlfiles.extend(argv)
01746 
01747     for xmlfile in xmlfiles:
01748 
01749       # Parse XML file and validate it against our scheme
01750 
01751       tree = etree_read(xmlfile, validate = True)
01752 
01753       handle = tree.get("handle")
01754 
01755       # Update IRDB with parsed resource and roa-request data.
01756 
01757       roa_requests = [(
01758         x.get('asn'),
01759         rpki.resource_set.roa_prefix_set_ipv4(x.get("v4")),
01760         rpki.resource_set.roa_prefix_set_ipv6(x.get("v6"))) for x in tree.getiterator("roa_request")]
01761 
01762       children = [(
01763         x.get("handle"),
01764         rpki.resource_set.resource_set_as(x.get("asns")),
01765         rpki.resource_set.resource_set_ipv4(x.get("v4")),
01766         rpki.resource_set.resource_set_ipv6(x.get("v6")),
01767         rpki.sundial.datetime.fromXMLtime(x.get("valid_until"))) for x in tree.getiterator("child")]
01768 
01769       # ghostbusters are ignored for now
01770       irdb.update(handle, roa_requests, children)
01771 
01772       # Check for certificates before attempting anything else
01773 
01774       hosted_cacert = findbase64(tree, "bpki_ca_certificate")
01775       if not hosted_cacert:
01776         print "Nothing else I can do without a trust anchor for the entity I'm hosting."
01777         continue
01778 
01779       rpkid_xcert = rpki.x509.X509(PEM_file = self.bpki_servers.fxcert(b64 = hosted_cacert.get_Base64(),
01780                                                                        #filename = handle + ".cacert.cer",
01781                                                                        path_restriction = 1))
01782 
01783       # See what rpkid and pubd already have on file for this entity.
01784 
01785       if self.run_pubd:
01786         client_pdus = dict((x.client_handle, x)
01787                            for x in call_pubd(rpki.publication.client_elt.make_pdu(action = "list"))
01788                            if isinstance(x, rpki.publication.client_elt))
01789 
01790       rpkid_reply = call_rpkid(
01791         rpki.left_right.self_elt.make_pdu(      action = "get",  tag = "self",       self_handle = handle),
01792         rpki.left_right.bsc_elt.make_pdu(       action = "list", tag = "bsc",        self_handle = handle),
01793         rpki.left_right.repository_elt.make_pdu(action = "list", tag = "repository", self_handle = handle),
01794         rpki.left_right.parent_elt.make_pdu(    action = "list", tag = "parent",     self_handle = handle),
01795         rpki.left_right.child_elt.make_pdu(     action = "list", tag = "child",      self_handle = handle))
01796 
01797       self_pdu        = rpkid_reply[0]
01798       bsc_pdus        = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt))
01799       repository_pdus = dict((x.repository_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.repository_elt))
01800       parent_pdus     = dict((x.parent_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.parent_elt))
01801       child_pdus      = dict((x.child_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.child_elt))
01802 
01803       pubd_query = []
01804       rpkid_query = []
01805 
01806       # There should be exactly one <self/> object per hosted entity, by definition
01807 
01808       if (isinstance(self_pdu, rpki.left_right.report_error_elt) or
01809           self_pdu.crl_interval != self_crl_interval or
01810           self_pdu.regen_margin != self_regen_margin or
01811           self_pdu.bpki_cert != rpkid_xcert):
01812         rpkid_query.append(rpki.left_right.self_elt.make_pdu(
01813           action = "create" if isinstance(self_pdu, rpki.left_right.report_error_elt) else "set",
01814           tag = "self",
01815           self_handle = handle,
01816           bpki_cert = rpkid_xcert,
01817           crl_interval = self_crl_interval,
01818           regen_margin = self_regen_margin))
01819 
01820       # In general we only need one <bsc/> per <self/>.  BSC objects are a
01821       # little unusual in that the PKCS #10 subelement is generated by rpkid
01822       # in response to generate_keypair, so there's more of a separation
01823       # between create and set than with other objects.
01824 
01825       bsc_cert = findbase64(tree, "bpki_bsc_certificate")
01826       bsc_crl  = findbase64(tree, "bpki_crl", rpki.x509.CRL)
01827 
01828       bsc_pdu = bsc_pdus.pop(bsc_handle, None)
01829 
01830       if bsc_pdu is None:
01831         rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
01832           action = "create",
01833           tag = "bsc",
01834           self_handle = handle,
01835           bsc_handle = bsc_handle,
01836           generate_keypair = "yes"))
01837       elif bsc_pdu.signing_cert != bsc_cert or bsc_pdu.signing_cert_crl != bsc_crl:
01838         rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
01839           action = "set",
01840           tag = "bsc",
01841           self_handle = handle,
01842           bsc_handle = bsc_handle,
01843           signing_cert = bsc_cert,
01844           signing_cert_crl = bsc_crl))
01845 
01846       rpkid_query.extend(rpki.left_right.bsc_elt.make_pdu(
01847         action = "destroy", self_handle = handle, bsc_handle = b) for b in bsc_pdus)
01848 
01849       bsc_req = None
01850 
01851       if bsc_pdu and bsc_pdu.pkcs10_request:
01852         bsc_req = bsc_pdu.pkcs10_request
01853 
01854       # At present we need one <repository/> per <parent/>, not because
01855       # rpkid requires that, but because pubd does.  pubd probably should
01856       # be fixed to support a single client allowed to update multiple
01857       # trees, but for the moment the easiest way forward is just to
01858       # enforce a 1:1 mapping between <parent/> and <repository/> objects
01859 
01860       for repository in tree.getiterator("repository"):
01861 
01862         repository_handle = repository.get("handle")
01863         repository_pdu = repository_pdus.pop(repository_handle, None)
01864         repository_uri = repository.get("service_uri")
01865         repository_cert = findbase64(repository, "bpki_certificate")
01866 
01867         if (repository_pdu is None or
01868             repository_pdu.bsc_handle != bsc_handle or
01869             repository_pdu.peer_contact_uri != repository_uri or
01870             repository_pdu.bpki_cert != repository_cert):
01871           rpkid_query.append(rpki.left_right.repository_elt.make_pdu(
01872             action = "create" if repository_pdu is None else "set",
01873             tag = repository_handle,
01874             self_handle = handle,
01875             repository_handle = repository_handle,
01876             bsc_handle = bsc_handle,
01877             peer_contact_uri = repository_uri,
01878             bpki_cert = repository_cert))
01879 
01880       rpkid_query.extend(rpki.left_right.repository_elt.make_pdu(
01881         action = "destroy", self_handle = handle, repository_handle = r) for r in repository_pdus)
01882 
01883       # <parent/> setup code currently assumes 1:1 mapping between
01884       # <repository/> and <parent/>, and further assumes that the handles
01885       # for an associated pair are the identical (that is:
01886       # parent.repository_handle == parent.parent_handle).
01887 
01888       for parent in tree.getiterator("parent"):
01889 
01890         parent_handle = parent.get("handle")
01891         parent_pdu = parent_pdus.pop(parent_handle, None)
01892         parent_uri = parent.get("service_uri")
01893         parent_myhandle = parent.get("myhandle")
01894         parent_sia_base = parent.get("sia_base")
01895         parent_cms_cert = findbase64(parent, "bpki_cms_certificate")
01896 
01897         if (parent_pdu is None or
01898             parent_pdu.bsc_handle != bsc_handle or
01899             parent_pdu.repository_handle != parent_handle or
01900             parent_pdu.peer_contact_uri != parent_uri or
01901             parent_pdu.sia_base != parent_sia_base or
01902             parent_pdu.sender_name != parent_myhandle or
01903             parent_pdu.recipient_name != parent_handle or
01904             parent_pdu.bpki_cms_cert != parent_cms_cert):
01905           rpkid_query.append(rpki.left_right.parent_elt.make_pdu(
01906             action = "create" if parent_pdu is None else "set",
01907             tag = parent_handle,
01908             self_handle = handle,
01909             parent_handle = parent_handle,
01910             bsc_handle = bsc_handle,
01911             repository_handle = parent_handle,
01912             peer_contact_uri = parent_uri,
01913             sia_base = parent_sia_base,
01914             sender_name = parent_myhandle,
01915             recipient_name = parent_handle,
01916             bpki_cms_cert = parent_cms_cert))
01917 
01918       rpkid_query.extend(rpki.left_right.parent_elt.make_pdu(
01919         action = "destroy", self_handle = handle, parent_handle = p) for p in parent_pdus)
01920 
01921       # Children are simpler than parents, because they call us, so no URL
01922       # to construct and figuring out what certificate to use is their
01923       # problem, not ours.
01924 
01925       for child in tree.getiterator("child"):
01926 
01927         child_handle = child.get("handle")
01928         child_pdu = child_pdus.pop(child_handle, None)
01929         child_cert = findbase64(child, "bpki_certificate")
01930 
01931         if (child_pdu is None or
01932             child_pdu.bsc_handle != bsc_handle or
01933             child_pdu.bpki_cert != child_cert):
01934           rpkid_query.append(rpki.left_right.child_elt.make_pdu(
01935             action = "create" if child_pdu is None else "set",
01936             tag = child_handle,
01937             self_handle = handle,
01938             child_handle = child_handle,
01939             bsc_handle = bsc_handle,
01940             bpki_cert = child_cert))
01941 
01942       rpkid_query.extend(rpki.left_right.child_elt.make_pdu(
01943         action = "destroy", self_handle = handle, child_handle = c) for c in child_pdus)
01944 
01945       # Publication setup.
01946 
01947       if self.run_pubd:
01948 
01949         for f in self.entitydb.iterate("pubclients"):
01950           c = etree_read(f)
01951 
01952           client_handle = c.get("client_handle")
01953           client_base_uri = c.get("sia_base")
01954           client_bpki_cert = rpki.x509.X509(PEM_file = self.bpki_servers.fxcert(c.findtext("bpki_client_ta")))
01955           client_pdu = client_pdus.pop(client_handle, None)
01956 
01957           if (client_pdu is None or
01958               client_pdu.base_uri != client_base_uri or
01959               client_pdu.bpki_cert != client_bpki_cert):
01960             pubd_query.append(rpki.publication.client_elt.make_pdu(
01961               action = "create" if client_pdu is None else "set",
01962               client_handle = client_handle,
01963               bpki_cert = client_bpki_cert,
01964               base_uri = client_base_uri))
01965 
01966         pubd_query.extend(rpki.publication.client_elt.make_pdu(
01967             action = "destroy", client_handle = p) for p in client_pdus)
01968 
01969       # If we changed anything, ship updates off to daemons
01970 
01971       failed = False
01972 
01973       if rpkid_query:
01974         rpkid_reply = call_rpkid(*rpkid_query)
01975         bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt))
01976         if bsc_handle in bsc_pdus and bsc_pdus[bsc_handle].pkcs10_request:
01977           bsc_req = bsc_pdus[bsc_handle].pkcs10_request
01978         for r in rpkid_reply:
01979           if isinstance(r, rpki.left_right.report_error_elt):
01980             failed = True
01981             print "rpkid reported failure:", r.error_code
01982             if r.error_text:
01983               print r.error_text
01984 
01985       if failed:
01986         raise RuntimeError
01987 
01988       if pubd_query:
01989         assert self.run_pubd
01990         pubd_reply = call_pubd(*pubd_query)
01991         for r in pubd_reply:
01992           if isinstance(r, rpki.publication.report_error_elt):
01993             failed = True
01994             print "pubd reported failure:", r.error_code
01995             if r.error_text:
01996               print r.error_text
01997             
01998       if failed:
01999         raise RuntimeError
02000 
02001       # Rewrite XML.
02002 
02003       e = tree.find("bpki_bsc_pkcs10")
02004       if e is not None:
02005         tree.remove(e)
02006       if bsc_req is not None:
02007         SubElement(tree, "bpki_bsc_pkcs10").text = bsc_req.get_Base64()
02008 
02009       tree.set("service_uri", rpkid_base + "up-down/" + handle)
02010 
02011       etree_write(tree, xmlfile, validate = True,
02012                   msg = None if xmlfile is my_xmlfile else 'Send this file back to the hosted entity ("%s")' % handle)
02013 
02014     irdb.close()
02015 
02016     # We used to run event loop again to give TLS connections a chance to shut down cleanly.
02017     # Seems not to be needed (and sometimes hangs forever, which is odd) with TLS out of the picture.
02018     #rpki.async.event_loop()
 All Classes Namespaces Files Functions Variables