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