RPKI Engine 1.0
|
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()