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 3246 2010-05-12 19:07:50Z sra $
00040
00041 Copyright (C) 2009-2010 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
00073
00074 namespace = "http://www.hactrn.net/uris/rpki/myrpki/"
00075 version = "2"
00076 namespaceQName = "{" + namespace + "}"
00077
00078
00079
00080 allow_incomplete = False
00081
00082
00083
00084 whine = False
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. Hmm, maybe some or all of the
00097 entitydb glob stuff should end up here too? Later.
00098 """
00099
00100 def __init__(self, cfg):
00101 self.dir = cfg.get("entitydb_dir", "entitydb")
00102
00103 def __call__(self, *args):
00104 return os.path.join(self.dir, *args)
00105
00106 def iterate(self, *args):
00107 return glob.iglob(os.path.join(self.dir, *args))
00108
00109 class roa_request(object):
00110 """
00111 Representation of a ROA request.
00112 """
00113
00114 v4re = re.compile("^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]+(-[0-9]+)?$", re.I)
00115 v6re = re.compile("^([0-9a-f]{0,4}:){0,15}[0-9a-f]{0,4}/[0-9]+(-[0-9]+)?$", re.I)
00116
00117 def __init__(self, asn, group):
00118 self.asn = asn
00119 self.group = group
00120 self.v4 = comma_set()
00121 self.v6 = comma_set()
00122
00123 def __repr__(self):
00124 s = "<%s asn %s group %s" % (self.__class__.__name__, self.asn, self.group)
00125 if self.v4:
00126 s += " v4 %s" % self.v4
00127 if self.v6:
00128 s += " v6 %s" % self.v6
00129 return s + ">"
00130
00131 def add(self, prefix):
00132 """
00133 Add one prefix to this ROA request.
00134 """
00135 if self.v4re.match(prefix):
00136 self.v4.add(prefix)
00137 elif self.v6re.match(prefix):
00138 self.v6.add(prefix)
00139 else:
00140 raise RuntimeError, "Bad prefix syntax: %r" % (prefix,)
00141
00142 def xml(self, e):
00143 """
00144 Generate XML element represeting representing this ROA request.
00145 """
00146 e = SubElement(e, "roa_request",
00147 asn = self.asn,
00148 v4 = str(self.v4),
00149 v6 = str(self.v6))
00150 e.tail = "\n"
00151
00152 class roa_requests(dict):
00153 """
00154 Database of ROA requests.
00155 """
00156
00157 def add(self, asn, group, prefix):
00158 """
00159 Add one <ASN, group, prefix> set to ROA request database.
00160 """
00161 key = (asn, group)
00162 if key not in self:
00163 self[key] = roa_request(asn, group)
00164 self[key].add(prefix)
00165
00166 def xml(self, e):
00167 """
00168 Render ROA requests as XML elements.
00169 """
00170 for r in self.itervalues():
00171 r.xml(e)
00172
00173 @classmethod
00174 def from_csv(cls, roa_csv_file):
00175 """
00176 Parse ROA requests from CSV file.
00177 """
00178 self = cls()
00179
00180 for pnm, asn, group in csv_reader(roa_csv_file, columns = 3):
00181 self.add(asn = asn, group = group, prefix = pnm)
00182 return self
00183
00184 class child(object):
00185 """
00186 Representation of one child entity.
00187 """
00188
00189 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)
00190 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)
00191
00192 def __init__(self, handle):
00193 self.handle = handle
00194 self.asns = comma_set()
00195 self.v4 = comma_set()
00196 self.v6 = comma_set()
00197 self.validity = None
00198 self.bpki_certificate = None
00199
00200 def __repr__(self):
00201 s = "<%s %s" % (self.__class__.__name__, self.handle)
00202 if self.asns:
00203 s += " asn %s" % self.asns
00204 if self.v4:
00205 s += " v4 %s" % self.v4
00206 if self.v6:
00207 s += " v6 %s" % self.v6
00208 if self.validity:
00209 s += " valid %s" % self.validity
00210 if self.bpki_certificate:
00211 s += " cert %s" % self.bpki_certificate
00212 return s + ">"
00213
00214 def add(self, prefix = None, asn = None, validity = None, bpki_certificate = None):
00215 """
00216 Add prefix, autonomous system number, validity date, or BPKI
00217 certificate for this child.
00218 """
00219 if prefix is not None:
00220 if self.v4re.match(prefix):
00221 self.v4.add(prefix)
00222 elif self.v6re.match(prefix):
00223 self.v6.add(prefix)
00224 else:
00225 raise RuntimeError, "Bad prefix syntax: %r" % (prefix,)
00226 if asn is not None:
00227 self.asns.add(asn)
00228 if validity is not None:
00229 self.validity = validity
00230 if bpki_certificate is not None:
00231 self.bpki_certificate = bpki_certificate
00232
00233 def xml(self, e):
00234 """
00235 Render this child as an XML element.
00236 """
00237 complete = self.bpki_certificate and self.validity
00238 if whine and not complete:
00239 print "Incomplete child entry %s" % self
00240 if complete or allow_incomplete:
00241 e = SubElement(e, "child",
00242 handle = self.handle,
00243 valid_until = self.validity,
00244 asns = str(self.asns),
00245 v4 = str(self.v4),
00246 v6 = str(self.v6))
00247 e.tail = "\n"
00248 if self.bpki_certificate:
00249 PEMElement(e, "bpki_certificate", self.bpki_certificate)
00250
00251 class children(dict):
00252 """
00253 Database of children.
00254 """
00255
00256 def add(self, handle, prefix = None, asn = None, validity = None, bpki_certificate = None):
00257 """
00258 Add resources to a child, creating the child object if necessary.
00259 """
00260 if handle not in self:
00261 self[handle] = child(handle)
00262 self[handle].add(prefix = prefix, asn = asn, validity = validity, bpki_certificate = bpki_certificate)
00263
00264 def xml(self, e):
00265 """
00266 Render children database to XML.
00267 """
00268 for c in self.itervalues():
00269 c.xml(e)
00270
00271 @classmethod
00272 def from_csv(cls, prefix_csv_file, asn_csv_file, fxcert, entitydb):
00273 """
00274 Parse child resources, certificates, and validity dates from CSV files.
00275 """
00276 self = cls()
00277 for f in entitydb.iterate("children", "*.xml"):
00278 c = etree_read(f)
00279 self.add(handle = os.path.splitext(os.path.split(f)[-1])[0],
00280 validity = c.get("valid_until"),
00281 bpki_certificate = fxcert(c.findtext("bpki_child_ta")))
00282
00283 for handle, pn in csv_reader(prefix_csv_file, columns = 2):
00284 self.add(handle = handle, prefix = pn)
00285
00286 for handle, asn in csv_reader(asn_csv_file, columns = 2):
00287 self.add(handle = handle, asn = asn)
00288 return self
00289
00290 class parent(object):
00291 """
00292 Representation of one parent entity.
00293 """
00294
00295 def __init__(self, handle):
00296 self.handle = handle
00297 self.service_uri = None
00298 self.bpki_cms_certificate = None
00299 self.bpki_https_certificate = None
00300 self.myhandle = None
00301 self.sia_base = None
00302
00303 def __repr__(self):
00304 s = "<%s %s" % (self.__class__.__name__, self.handle)
00305 if self.myhandle:
00306 s += " myhandle %s" % self.myhandle
00307 if self.service_uri:
00308 s += " uri %s" % self.service_uri
00309 if self.sia_base:
00310 s += " sia %s" % self.sia_base
00311 if self.bpki_cms_certificate:
00312 s += " cms %s" % self.bpki_cms_certificate
00313 if self.bpki_https_certificate:
00314 s += " https %s" % self.bpki_https_certificate
00315 return s + ">"
00316
00317 def add(self, service_uri = None,
00318 bpki_cms_certificate = None,
00319 bpki_https_certificate = None,
00320 myhandle = None,
00321 sia_base = None):
00322 """
00323 Add service URI or BPKI certificates to this parent object.
00324 """
00325 if service_uri is not None:
00326 self.service_uri = service_uri
00327 if bpki_cms_certificate is not None:
00328 self.bpki_cms_certificate = bpki_cms_certificate
00329 if bpki_https_certificate is not None:
00330 self.bpki_https_certificate = bpki_https_certificate
00331 if myhandle is not None:
00332 self.myhandle = myhandle
00333 if sia_base is not None:
00334 self.sia_base = sia_base
00335
00336 def xml(self, e):
00337 """
00338 Render this parent object to XML.
00339 """
00340 complete = self.bpki_cms_certificate and self.bpki_https_certificate and self.myhandle and self.service_uri and self.sia_base
00341 if whine and not complete:
00342 print "Incomplete parent entry %s" % self
00343 if complete or allow_incomplete:
00344 e = SubElement(e, "parent",
00345 handle = self.handle,
00346 myhandle = self.myhandle,
00347 service_uri = self.service_uri,
00348 sia_base = self.sia_base)
00349 e.tail = "\n"
00350 if self.bpki_cms_certificate:
00351 PEMElement(e, "bpki_cms_certificate", self.bpki_cms_certificate)
00352 if self.bpki_https_certificate:
00353 PEMElement(e, "bpki_https_certificate", self.bpki_https_certificate)
00354
00355 class parents(dict):
00356 """
00357 Database of parent objects.
00358 """
00359
00360 def add(self, handle,
00361 service_uri = None,
00362 bpki_cms_certificate = None,
00363 bpki_https_certificate = None,
00364 myhandle = None,
00365 sia_base = None):
00366 """
00367 Add service URI or certificates to parent object, creating it if necessary.
00368 """
00369 if handle not in self:
00370 self[handle] = parent(handle)
00371 self[handle].add(service_uri = service_uri,
00372 bpki_cms_certificate = bpki_cms_certificate,
00373 bpki_https_certificate = bpki_https_certificate,
00374 myhandle = myhandle,
00375 sia_base = sia_base)
00376
00377 def xml(self, e):
00378 for c in self.itervalues():
00379 c.xml(e)
00380
00381 @classmethod
00382 def from_csv(cls, fxcert, entitydb):
00383 """
00384 Parse parent data from entitydb.
00385 """
00386 self = cls()
00387 for f in entitydb.iterate("parents", "*.xml"):
00388 h = os.path.splitext(os.path.split(f)[-1])[0]
00389 p = etree_read(f)
00390 r = etree_read(f.replace(os.path.sep + "parents" + os.path.sep,
00391 os.path.sep + "repositories" + os.path.sep))
00392 assert r.get("type") == "confirmed"
00393 self.add(handle = h,
00394 service_uri = p.get("service_uri"),
00395 bpki_cms_certificate = fxcert(p.findtext("bpki_resource_ta")),
00396 bpki_https_certificate = fxcert(p.findtext("bpki_server_ta")),
00397 myhandle = p.get("child_handle"),
00398 sia_base = r.get("sia_base"))
00399 return self
00400
00401 class repository(object):
00402 """
00403 Representation of one repository entity.
00404 """
00405
00406 def __init__(self, handle):
00407 self.handle = handle
00408 self.service_uri = None
00409 self.bpki_certificate = None
00410
00411 def __repr__(self):
00412 s = "<%s %s" % (self.__class__.__name__, self.handle)
00413 if self.service_uri:
00414 s += " uri %s" % self.service_uri
00415 if self.bpki_certificate:
00416 s += " cert %s" % self.bpki_certificate
00417 return s + ">"
00418
00419 def add(self, service_uri = None, bpki_certificate = None):
00420 """
00421 Add service URI or BPKI certificates to this repository object.
00422 """
00423 if service_uri is not None:
00424 self.service_uri = service_uri
00425 if bpki_certificate is not None:
00426 self.bpki_certificate = bpki_certificate
00427
00428 def xml(self, e):
00429 """
00430 Render this repository object to XML.
00431 """
00432 complete = self.bpki_certificate and self.service_uri
00433 if whine and not complete:
00434 print "Incomplete repository entry %s" % self
00435 if complete or allow_incomplete:
00436 e = SubElement(e, "repository",
00437 handle = self.handle,
00438 service_uri = self.service_uri)
00439 e.tail = "\n"
00440 if self.bpki_certificate:
00441 PEMElement(e, "bpki_certificate", self.bpki_certificate)
00442
00443 class repositories(dict):
00444 """
00445 Database of repository objects.
00446 """
00447
00448 def add(self, handle,
00449 service_uri = None,
00450 bpki_certificate = None):
00451 """
00452 Add service URI or certificate to repository object, creating it if necessary.
00453 """
00454 if handle not in self:
00455 self[handle] = repository(handle)
00456 self[handle].add(service_uri = service_uri,
00457 bpki_certificate = bpki_certificate)
00458
00459 def xml(self, e):
00460 for c in self.itervalues():
00461 c.xml(e)
00462
00463 @classmethod
00464 def from_csv(cls, fxcert, entitydb):
00465 """
00466 Parse repository data from entitydb.
00467 """
00468 self = cls()
00469 for f in entitydb.iterate("repositories", "*.xml"):
00470 h = os.path.splitext(os.path.split(f)[-1])[0]
00471 r = etree_read(f)
00472 assert r.get("type") == "confirmed"
00473 self.add(handle = h,
00474 service_uri = r.get("service_uri"),
00475 bpki_certificate = fxcert(r.findtext("bpki_server_ta")))
00476 return self
00477
00478 class csv_reader(object):
00479 """
00480 Reader for tab-delimited text that's (slightly) friendlier than the
00481 stock Python csv module (which isn't intended for direct use by
00482 humans anyway, and neither was this package originally, but that
00483 seems to be the way that it has evolved...).
00484
00485 Columns parameter specifies how many columns users of the reader
00486 expect to see; lines with fewer columns will be padded with None
00487 values.
00488
00489 Original API design for this class courtesy of Warren Kumari, but
00490 don't blame him if you don't like what I did with his ideas.
00491 """
00492
00493 def __init__(self, filename, columns = None, min_columns = None, comment_characters = "#;"):
00494 assert columns is None or isinstance(columns, int)
00495 assert min_columns is None or isinstance(min_columns, int)
00496 if columns is not None and min_columns is None:
00497 min_columns = columns
00498 self.filename = filename
00499 self.columns = columns
00500 self.min_columns = min_columns
00501 self.comment_characters = comment_characters
00502 self.file = open(filename, "r")
00503
00504 def __iter__(self):
00505 line_number = 0
00506 for line in self.file:
00507 line_number += 1
00508 line = line.strip()
00509 if not line or line[0] in self.comment_characters:
00510 continue
00511 fields = line.split()
00512 if self.min_columns is not None and len(fields) < self.min_columns:
00513 raise RuntimeError, "%s:%d: Not enough columns in line %r" % (self.filename, line_number, line)
00514 if self.columns is not None and len(fields) > self.columns:
00515 raise RuntimeError, "%s:%d: Too many columns in line %r" % (self.filename, line_number, line)
00516 if self.columns is not None and len(fields) < self.columns:
00517 fields += tuple(None for i in xrange(self.columns - len(fields)))
00518 yield fields
00519
00520 def csv_writer(filename):
00521 """
00522 Writer object for tab delimited text. We just use the stock CSV
00523 module in excel-tab mode for this.
00524 """
00525 return csv.writer(open(filename, "w"), dialect = csv.get_dialect("excel-tab"))
00526
00527
00528 def PEMElement(e, tag, filename, **kwargs):
00529 """
00530 Create an XML element containing Base64 encoded data taken from a
00531 PEM file.
00532 """
00533 lines = open(filename).readlines()
00534 while lines:
00535 if lines.pop(0).startswith("-----BEGIN "):
00536 break
00537 while lines:
00538 if lines.pop(-1).startswith("-----END "):
00539 break
00540 if e.text is None:
00541 e.text = "\n"
00542 se = SubElement(e, tag, **kwargs)
00543 se.text = "\n" + "".join(lines)
00544 se.tail = "\n"
00545 return se
00546
00547 class CA(object):
00548 """
00549 Representation of one certification authority.
00550 """
00551
00552
00553
00554
00555 path_restriction = { 0 : "ca_x509_ext_xcert0",
00556 1 : "ca_x509_ext_xcert1" }
00557
00558 def __init__(self, cfg_file, dir):
00559 self.cfg = cfg_file
00560 self.dir = dir
00561 self.cer = dir + "/ca.cer"
00562 self.key = dir + "/ca.key"
00563 self.req = dir + "/ca.req"
00564 self.crl = dir + "/ca.crl"
00565 self.index = dir + "/index"
00566 self.serial = dir + "/serial"
00567 self.crlnum = dir + "/crl_number"
00568
00569 cfg = rpki.config.parser(cfg_file, "myrpki")
00570 self.openssl = cfg.get("openssl", "openssl")
00571
00572 self.env = { "PATH" : os.environ["PATH"],
00573 "BPKI_DIRECTORY" : dir,
00574 "RANDFILE" : ".OpenSSL.whines.unless.I.set.this",
00575 "OPENSSL_CONF" : cfg_file }
00576
00577 def run_openssl(self, *cmd, **kwargs):
00578 """
00579 Run an OpenSSL command, suppresses stderr unless OpenSSL returns
00580 failure, and returns stdout.
00581 """
00582 stdin = kwargs.pop("stdin", None)
00583 env = self.env.copy()
00584 env.update(kwargs)
00585 cmd = (self.openssl,) + cmd
00586 p = subprocess.Popen(cmd, env = env, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
00587 stdout, stderr = p.communicate(stdin)
00588 if p.wait() != 0:
00589 sys.stderr.write("OpenSSL command failed: " + stderr + "\n")
00590 raise subprocess.CalledProcessError(returncode = p.returncode, cmd = cmd)
00591 return stdout
00592
00593 def run_ca(self, *args):
00594 """
00595 Run OpenSSL "ca" command with common initial arguments.
00596 """
00597 self.run_openssl("ca", "-batch", "-config", self.cfg, *args)
00598
00599 def run_req(self, key_file, req_file, log_key = sys.stdout):
00600 """
00601 Run OpenSSL "genrsa" and "req" commands.
00602 """
00603 if not os.path.exists(key_file):
00604 if log_key:
00605 log_key.write("Generating 2048-bit RSA key %s\n" % os.path.realpath(key_file))
00606 self.run_openssl("genrsa", "-out", key_file, "2048")
00607 if not os.path.exists(req_file):
00608 self.run_openssl("req", "-new", "-sha256", "-config", self.cfg, "-key", key_file, "-out", req_file)
00609
00610 def run_dgst(self, input, algorithm = "md5"):
00611 """
00612 Run OpenSSL "dgst" command, return cleaned-up result.
00613 """
00614 hash = self.run_openssl("dgst", "-" + algorithm, stdin = input)
00615
00616
00617 hash = "".join(hash.split())
00618 if hash.startswith("(stdin)="):
00619 hash = hash[len("(stdin)="):]
00620 return hash
00621
00622 @staticmethod
00623 def touch_file(filename, content = None):
00624 """
00625 Create dumb little text files expected by OpenSSL "ca" utility.
00626 """
00627 if not os.path.exists(filename):
00628 f = open(filename, "w")
00629 if content is not None:
00630 f.write(content)
00631 f.close()
00632
00633 def setup(self, ca_name):
00634 """
00635 Set up this CA. ca_name is an X.509 distinguished name in
00636 /tag=val/tag=val format.
00637 """
00638
00639 modified = False
00640
00641 if not os.path.exists(self.dir):
00642 os.makedirs(self.dir)
00643 self.touch_file(self.index)
00644 self.touch_file(self.serial, "01\n")
00645 self.touch_file(self.crlnum, "01\n")
00646
00647 self.run_req(key_file = self.key, req_file = self.req)
00648
00649 if not os.path.exists(self.cer):
00650 modified = True
00651 self.run_ca("-selfsign", "-extensions", "ca_x509_ext_ca", "-subj", ca_name, "-in", self.req, "-out", self.cer)
00652
00653 if not os.path.exists(self.crl):
00654 modified = True
00655 self.run_ca("-gencrl", "-out", self.crl)
00656
00657 return modified
00658
00659 def ee(self, ee_name, base_name):
00660 """
00661 Issue an end-enity certificate.
00662 """
00663 key_file = "%s/%s.key" % (self.dir, base_name)
00664 req_file = "%s/%s.req" % (self.dir, base_name)
00665 cer_file = "%s/%s.cer" % (self.dir, base_name)
00666 self.run_req(key_file = key_file, req_file = req_file)
00667 if not os.path.exists(cer_file):
00668 self.run_ca("-extensions", "ca_x509_ext_ee", "-subj", ee_name, "-in", req_file, "-out", cer_file)
00669 return True
00670 else:
00671 return False
00672
00673 def cms_xml_sign(self, ee_name, base_name, elt):
00674 """
00675 Sign an XML object with CMS, return Base64 text.
00676 """
00677 self.ee(ee_name, base_name)
00678 return base64.b64encode(self.run_openssl(
00679 "cms", "-sign", "-binary", "-outform", "DER",
00680 "-keyid", "-md", "sha256", "-nodetach", "-nosmimecap",
00681 "-econtent_type", ".".join(str(i) for i in rpki.oids.name2oid["id-ct-xml"]),
00682 "-inkey", "%s/%s.key" % (self.dir, base_name),
00683 "-signer", "%s/%s.cer" % (self.dir, base_name),
00684 stdin = ElementToString(etree_pre_write(elt))))
00685
00686 def cms_xml_verify(self, b64, ca):
00687 """
00688 Attempt to verify and extract XML from a Base64-encoded signed CMS
00689 object. CA is the filename of a certificate that we expect to be
00690 the issuer of the EE certificate bundled with the CMS, and must
00691 previously have been cross-certified under our trust anchor.
00692 """
00693
00694
00695
00696
00697
00698 CAfile = os.path.join(self.dir, "temp.%s.pem" % os.getpid())
00699 try:
00700 f = open(CAfile, "w")
00701 f.write(open(self.cer).read())
00702 f.write(open(ca).read())
00703 f.close()
00704 return etree_post_read(ElementFromString(self.run_openssl(
00705 "cms", "-verify", "-inform", "DER", "-CAfile", CAfile,
00706 stdin = base64.b64decode(b64))))
00707 finally:
00708 if os.path.exists(CAfile):
00709 os.unlink(CAfile)
00710
00711 def bsc(self, pkcs10):
00712 """
00713 Issue BSC certificiate, if we have a PKCS #10 request for it.
00714 """
00715
00716 if pkcs10 is None:
00717 return None, None
00718
00719 pkcs10 = base64.b64decode(pkcs10)
00720
00721 hash = self.run_dgst(pkcs10)
00722
00723 req_file = "%s/bsc.%s.req" % (self.dir, hash)
00724 cer_file = "%s/bsc.%s.cer" % (self.dir, hash)
00725
00726 if not os.path.exists(cer_file):
00727 self.run_openssl("req", "-inform", "DER", "-out", req_file, stdin = pkcs10)
00728 self.run_ca("-extensions", "ca_x509_ext_ee", "-in", req_file, "-out", cer_file)
00729
00730 return req_file, cer_file
00731
00732 def fxcert(self, b64, filename = None, path_restriction = 0):
00733 """
00734 Write PEM certificate to file, then cross-certify.
00735 """
00736 fn = os.path.join(self.dir, filename or "temp.%s.cer" % os.getpid())
00737 try:
00738 self.run_openssl("x509", "-inform", "DER", "-out", fn,
00739 stdin = base64.b64decode(b64))
00740 return self.xcert(fn, path_restriction)
00741 finally:
00742 if not filename and os.path.exists(fn):
00743 os.unlink(fn)
00744
00745 def xcert(self, cert, path_restriction = 0):
00746 """
00747 Cross-certify a certificate represented as a PEM file.
00748 """
00749
00750 if not cert or not os.path.exists(cert):
00751 return None
00752
00753
00754
00755
00756 hash = self.run_dgst(self.run_openssl(
00757 "x509", "-noout", "-pubkey", "-subject", "-in", cert))
00758
00759
00760
00761
00762
00763 xcert = "%s/xcert.%s.cer" % (self.dir, hash.strip())
00764 if not os.path.exists(xcert):
00765 self.run_ca("-ss_cert", cert, "-out", xcert, "-extensions", self.path_restriction[path_restriction])
00766 return xcert
00767
00768 def etree_validate(e):
00769
00770
00771 schema = os.getenv("MYRPKI_RNG")
00772 if schema:
00773 try:
00774 import lxml.etree
00775 except ImportError:
00776 return
00777 try:
00778 lxml.etree.RelaxNG(file = schema).assertValid(e)
00779 except lxml.etree.RelaxNGParseError:
00780 return
00781 except lxml.etree.DocumentInvalid:
00782 print lxml.etree.tostring(e, pretty_print = True)
00783 raise
00784
00785 def etree_write(e, filename, verbose = False, validate = True, msg = None):
00786 """
00787 Write out an etree to a file, safely.
00788
00789 I still miss SYSCAL(RENMWO).
00790 """
00791 filename = os.path.realpath(filename)
00792 tempname = filename
00793 if not filename.startswith("/dev/"):
00794 tempname += ".tmp"
00795 if verbose or msg:
00796 print "Writing", filename
00797 if msg:
00798 print msg
00799 e = etree_pre_write(e, validate)
00800 ElementTree(e).write(tempname)
00801 if tempname != filename:
00802 os.rename(tempname, filename)
00803
00804 def etree_pre_write(e, validate = True):
00805 """
00806 Do the namespace frobbing needed on write; broken out of
00807 etree_write() because also needed with ElementToString().
00808 """
00809 e = copy.deepcopy(e)
00810 e.set("version", version)
00811 for i in e.getiterator():
00812 if i.tag[0] != "{":
00813 i.tag = namespaceQName + i.tag
00814 assert i.tag.startswith(namespaceQName)
00815 if validate:
00816 etree_validate(e)
00817 return e
00818
00819 def etree_read(filename, verbose = False, validate = True):
00820 """
00821 Read an etree from a file, verifying then stripping XML namespace
00822 cruft.
00823 """
00824 if verbose:
00825 print "Reading", filename
00826 e = ElementTree(file = filename).getroot()
00827 return etree_post_read(e, validate)
00828
00829 def etree_post_read(e, validate = True):
00830 """
00831 Do the namespace frobbing needed on read; broken out of etree_read()
00832 beause also needed by ElementFromString().
00833 """
00834 if validate:
00835 etree_validate(e)
00836 for i in e.getiterator():
00837 if i.tag.startswith(namespaceQName):
00838 i.tag = i.tag[len(namespaceQName):]
00839 else:
00840 raise RuntimeError, "XML tag %r is not in namespace %r" % (i.tag, namespace)
00841 return e
00842
00843 def b64_equal(thing1, thing2):
00844 """
00845 Compare two Base64-encoded values for equality.
00846 """
00847 return "".join(thing1.split()) == "".join(thing2.split())
00848
00849
00850
00851 class main(rpki.cli.Cmd):
00852
00853 prompt = "myrpki> "
00854
00855 completedefault = rpki.cli.Cmd.filename_complete
00856
00857 show_xml = False
00858
00859 def __init__(self):
00860 os.environ["TZ"] = "UTC"
00861 time.tzset()
00862
00863 rpki.log.use_syslog = False
00864
00865 self.cfg_file = os.getenv("MYRPKI_CONF", "myrpki.conf")
00866
00867 opts, argv = getopt.getopt(sys.argv[1:], "c:h?", ["config=", "help"])
00868 for o, a in opts:
00869 if o in ("-c", "--config"):
00870 self.cfg_file = a
00871 elif o in ("-h", "--help", "-?"):
00872 argv = ["help"]
00873
00874 if not argv or argv[0] != "help":
00875 rpki.log.init("myrpki")
00876 self.read_config()
00877
00878 rpki.cli.Cmd.__init__(self, argv)
00879
00880
00881 def help_overview(self):
00882 """
00883 Show program __doc__ string. Perhaps there's some clever way to
00884 do this using the textwrap module, but for now something simple
00885 and crude will suffice.
00886 """
00887 for line in __doc__.splitlines(True):
00888 self.stdout.write(" " * 4 + line)
00889 self.stdout.write("\n")
00890
00891 def read_config(self):
00892
00893 self.cfg = rpki.config.parser(self.cfg_file, "myrpki")
00894
00895 self.histfile = self.cfg.get("history_file", ".myrpki_history")
00896 self.handle = self.cfg.get("handle")
00897 self.run_rpkid = self.cfg.getboolean("run_rpkid")
00898 self.run_pubd = self.cfg.getboolean("run_pubd")
00899 self.run_rootd = self.cfg.getboolean("run_rootd")
00900 self.entitydb = EntityDB(self.cfg)
00901
00902 if self.run_rootd and (not self.run_pubd or not self.run_rpkid):
00903 raise RuntimeError, "Can't run rootd unless also running rpkid and pubd"
00904
00905 self.bpki_resources = CA(self.cfg_file, self.cfg.get("bpki_resources_directory"))
00906 if self.run_rpkid or self.run_pubd or self.run_rootd:
00907 self.bpki_servers = CA(self.cfg_file, self.cfg.get("bpki_servers_directory"))
00908
00909 self.pubd_contact_info = self.cfg.get("pubd_contact_info", "")
00910
00911 self.rsync_module = self.cfg.get("publication_rsync_module")
00912 self.rsync_server = self.cfg.get("publication_rsync_server")
00913
00914
00915 def do_initialize(self, arg):
00916 """
00917 Initialize an RPKI installation. This command reads the
00918 configuration file, creates the BPKI and EntityDB directories,
00919 generates the initial BPKI certificates, and creates an XML file
00920 describing the resource-holding aspect of this RPKI installation.
00921 """
00922
00923 if arg:
00924 raise RuntimeError, "This command takes no arguments"
00925
00926 self.bpki_resources.setup(self.cfg.get("bpki_resources_ta_dn",
00927 "/CN=%s BPKI Resource Trust Anchor" % self.handle))
00928 if self.run_rpkid or self.run_pubd or self.run_rootd:
00929 self.bpki_servers.setup(self.cfg.get("bpki_servers_ta_dn",
00930 "/CN=%s BPKI Server Trust Anchor" % self.handle))
00931
00932
00933
00934 for i in ("parents", "children", "repositories", "pubclients"):
00935 d = self.entitydb(i)
00936 if not os.path.exists(d):
00937 os.makedirs(d)
00938
00939 if self.run_rpkid or self.run_pubd or self.run_rootd:
00940
00941 if self.run_rpkid:
00942 self.bpki_servers.ee(self.cfg.get("bpki_rpkid_ee_dn",
00943 "/CN=%s rpkid server certificate" % self.handle), "rpkid")
00944 self.bpki_servers.ee(self.cfg.get("bpki_irdbd_ee_dn",
00945 "/CN=%s irdbd server certificate" % self.handle), "irdbd")
00946 if self.run_pubd:
00947 self.bpki_servers.ee(self.cfg.get("bpki_pubd_ee_dn",
00948 "/CN=%s pubd server certificate" % self.handle), "pubd")
00949 if self.run_rpkid or self.run_pubd:
00950 self.bpki_servers.ee(self.cfg.get("bpki_irbe_ee_dn",
00951 "/CN=%s irbe client certificate" % self.handle), "irbe")
00952 if self.run_rootd:
00953 self.bpki_servers.ee(self.cfg.get("bpki_rootd_ee_dn",
00954 "/CN=%s rootd server certificate" % self.handle), "rootd")
00955
00956
00957
00958
00959 e = Element("identity", handle = self.handle)
00960 PEMElement(e, "bpki_ta", self.bpki_resources.cer)
00961 etree_write(e, self.entitydb("identity.xml"),
00962 msg = None if self.run_rootd else 'This is the "identity" file you will need to send to your parent')
00963
00964
00965
00966
00967 if self.run_rootd:
00968
00969 e = Element("parent", parent_handle = self.handle, child_handle = self.handle,
00970 service_uri = "https://localhost:%s/" % self.cfg.get("rootd_server_port"),
00971 valid_until = str(rpki.sundial.now() + rpki.sundial.timedelta(days = 365)))
00972 PEMElement(e, "bpki_resource_ta", self.bpki_servers.cer)
00973 PEMElement(e, "bpki_server_ta", self.bpki_servers.cer)
00974 PEMElement(e, "bpki_child_ta", self.bpki_resources.cer)
00975 SubElement(e, "repository", type = "offer")
00976 etree_write(e, self.entitydb("parents", "%s.xml" % self.handle))
00977
00978 self.bpki_resources.xcert(self.bpki_servers.cer)
00979
00980 rootd_child_fn = self.cfg.get("child-bpki-cert", None, "rootd")
00981 if not os.path.exists(rootd_child_fn):
00982 os.link(self.bpki_servers.xcert(self.bpki_resources.cer), rootd_child_fn)
00983
00984 repo_file_name = self.entitydb("repositories", "%s.xml" % self.handle)
00985
00986 try:
00987 want_offer = etree_read(repo_file_name).get("type") != "confirmed"
00988 except IOError:
00989 want_offer = True
00990
00991 if want_offer:
00992 e = Element("repository", type = "offer", handle = self.handle, parent_handle = self.handle)
00993 PEMElement(e, "bpki_client_ta", self.bpki_resources.cer)
00994 etree_write(e, repo_file_name,
00995 msg = 'This is the "repository offer" file for you to use if you want to publish in your own repository')
00996
00997 def do_configure_child(self, arg):
00998 """
00999 Configure a new child of this RPKI entity, given the child's XML
01000 identity file as an input. This command extracts the child's data
01001 from the XML, cross-certifies the child's resource-holding BPKI
01002 certificate, and generates an XML file describing the relationship
01003 between the child and this parent, including this parent's BPKI
01004 data and up-down protocol service URI.
01005 """
01006
01007 child_handle = None
01008
01009 opts, argv = getopt.getopt(arg.split(), "", ["child_handle="])
01010 for o, a in opts:
01011 if o == "--child_handle":
01012 child_handle = a
01013
01014 if len(argv) != 1:
01015 raise RuntimeError, "Need to specify filename for child.xml"
01016
01017 c = etree_read(argv[0])
01018
01019 if child_handle is None:
01020 child_handle = c.get("handle")
01021
01022 try:
01023 e = etree_read(self.cfg.get("xml_filename"))
01024 service_uri_base = e.get("service_uri")
01025 except IOError:
01026 service_uri_base = None
01027
01028 if not service_uri_base and self.run_rpkid:
01029 service_uri_base = "https://%s:%s/up-down/%s" % (self.cfg.get("rpkid_server_host"),
01030 self.cfg.get("rpkid_server_port"),
01031 self.handle)
01032 if not service_uri_base:
01033 print "Sorry, you can't set up children of a hosted config that itself has not yet been set up"
01034 return
01035
01036 print "Child calls itself %r, we call it %r" % (c.get("handle"), child_handle)
01037
01038 self.bpki_servers.fxcert(c.findtext("bpki_ta"))
01039
01040 e = Element("parent", parent_handle = self.handle, child_handle = child_handle,
01041 service_uri = "%s/%s" % (service_uri_base, child_handle),
01042 valid_until = str(rpki.sundial.now() + rpki.sundial.timedelta(days = 365)))
01043
01044 PEMElement(e, "bpki_resource_ta", self.bpki_resources.cer)
01045 PEMElement(e, "bpki_server_ta", self.bpki_servers.cer)
01046 SubElement(e, "bpki_child_ta").text = c.findtext("bpki_ta")
01047
01048 try:
01049 repo = None
01050 for f in self.entitydb.iterate("repositories", "*.xml"):
01051 r = etree_read(f)
01052 if r.get("type") == "confirmed":
01053 if repo is not None:
01054 raise RuntimeError, "Too many repositories, I don't know what to do, not giving referral"
01055 repo_handle = os.path.splitext(os.path.split(f)[-1])[0]
01056 repo = r
01057 if repo is None:
01058 raise RuntimeError, "Couldn't find any usable repositories, not giving referral"
01059
01060 if repo_handle == self.handle:
01061 SubElement(e, "repository", type = "offer")
01062 else:
01063 proposed_sia_base = repo.get("sia_base") + child_handle + "/"
01064 r = Element("referral", authorized_sia_base = proposed_sia_base)
01065 r.text = c.findtext("bpki_ta")
01066 auth = self.bpki_resources.cms_xml_sign(
01067 "/CN=%s Publication Referral" % self.handle, "referral", r)
01068 r = SubElement(e, "repository", type = "referral")
01069 SubElement(r, "authorization", referrer = repo.get("client_handle")).text = auth
01070 SubElement(r, "contact_info").text = repo.findtext("contact_info")
01071
01072 except RuntimeError, err:
01073 print err
01074
01075 etree_write(e, self.entitydb("children", "%s.xml" % child_handle),
01076 msg = "Send this file back to the child you just configured")
01077
01078
01079 def do_configure_parent(self, arg):
01080 """
01081 Configure a new parent of this RPKI entity, given the output of
01082 the parent's configure_child command as input. This command reads
01083 the parent's response XML, extracts the parent's BPKI and service
01084 URI information, cross-certifies the parent's BPKI data into this
01085 entity's BPKI, and checks for offers or referrals of publication
01086 service. If a publication offer or referral is present, we
01087 generate a request-for-service message to that repository, in case
01088 the user wants to avail herself of the referral or offer.
01089 """
01090
01091 parent_handle = None
01092
01093 opts, argv = getopt.getopt(arg.split(), "", ["parent_handle="])
01094 for o, a in opts:
01095 if o == "--parent_handle":
01096 parent_handle = a
01097
01098 if len(argv) != 1:
01099 raise RuntimeError, "Need to specify filename for parent.xml on command line"
01100
01101 p = etree_read(argv[0])
01102
01103 if parent_handle is None:
01104 parent_handle = p.get("parent_handle")
01105
01106 print "Parent calls itself %r, we call it %r" % (p.get("parent_handle"), parent_handle)
01107 print "Parent calls us %r" % p.get("child_handle")
01108
01109 self.bpki_resources.fxcert(p.findtext("bpki_resource_ta"))
01110 self.bpki_resources.fxcert(p.findtext("bpki_server_ta"))
01111
01112 etree_write(p, self.entitydb("parents", "%s.xml" % parent_handle))
01113
01114 r = p.find("repository")
01115
01116 if r is not None and r.get("type") in ("offer", "referral"):
01117 r.set("handle", self.handle)
01118 r.set("parent_handle", parent_handle)
01119 PEMElement(r, "bpki_client_ta", self.bpki_resources.cer)
01120 etree_write(r, self.entitydb("repositories", "%s.xml" % parent_handle),
01121 msg = 'This is the "repository %s" file to send to the repository operator' % r.get("type"))
01122 else:
01123 print "Couldn't find repository offer or referral"
01124
01125
01126 def do_configure_publication_client(self, arg):
01127 """
01128 Configure publication server to know about a new client, given the
01129 client's request-for-service message as input. This command reads
01130 the client's request for service, cross-certifies the client's
01131 BPKI data, and generates a response message containing the
01132 repository's BPKI data and service URI.
01133 """
01134
01135 sia_base = None
01136
01137 opts, argv = getopt.getopt(arg.split(), "", ["sia_base="])
01138 for o, a in opts:
01139 if o == "--sia_base":
01140 sia_base = a
01141
01142 if len(argv) != 1:
01143 raise RuntimeError, "Need to specify filename for client.xml"
01144
01145 client = etree_read(argv[0])
01146
01147 if sia_base is None:
01148
01149 auth = client.find("authorization")
01150 if auth is not None:
01151 print "Found <authorization/> element, this looks like a referral"
01152 referrer = etree_read(self.entitydb("pubclients", "%s.xml" % auth.get("referrer").replace("/",".")))
01153 referrer = self.bpki_servers.fxcert(referrer.findtext("bpki_client_ta"))
01154 referral = self.bpki_servers.cms_xml_verify(auth.text, referrer)
01155 if not b64_equal(referral.text, client.findtext("bpki_client_ta")):
01156 raise RuntimeError, "Referral trust anchor does not match"
01157 sia_base = referral.get("authorized_sia_base")
01158
01159 elif client.get("parent_handle") == self.handle:
01160 print "Client claims to be our child, checking"
01161 client_ta = client.findtext("bpki_client_ta")
01162 assert client_ta
01163 for child in self.entitydb.iterate("children", "*.xml"):
01164 c = etree_read(child)
01165 if b64_equal(c.findtext("bpki_child_ta"), client_ta):
01166 sia_base = "rsync://%s/%s/%s/%s/" % (self.rsync_server, self.rsync_module,
01167 self.handle, client.get("handle"))
01168 break
01169
01170
01171
01172
01173 if sia_base is None:
01174 print "Don't know where to nest this client, defaulting to top-level"
01175 sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, client.get("handle"))
01176
01177 assert sia_base.startswith("rsync://")
01178
01179 client_handle = "/".join(sia_base.rstrip("/").split("/")[4:])
01180
01181 parent_handle = client.get("parent_handle")
01182
01183 print "Client calls itself %r, we call it %r" % (client.get("handle"), client_handle)
01184 print "Client says its parent handle is %r" % parent_handle
01185
01186 self.bpki_servers.fxcert(client.findtext("bpki_client_ta"))
01187
01188 e = Element("repository", type = "confirmed",
01189 client_handle = client_handle,
01190 parent_handle = parent_handle,
01191 sia_base = sia_base,
01192 service_uri = "https://%s:%s/client/%s" % (self.cfg.get("pubd_server_host"),
01193 self.cfg.get("pubd_server_port"),
01194 client_handle))
01195
01196 PEMElement(e, "bpki_server_ta", self.bpki_servers.cer)
01197 SubElement(e, "bpki_client_ta").text = client.findtext("bpki_client_ta")
01198 SubElement(e, "contact_info").text = self.pubd_contact_info
01199 etree_write(e, self.entitydb("pubclients", "%s.xml" % client_handle.replace("/", ".")),
01200 msg = "Send this file back to the publication client you just configured")
01201
01202
01203 def do_configure_repository(self, arg):
01204 """
01205 Configure a publication repository for this RPKI entity, given the
01206 repository's response to our request-for-service message as input.
01207 This command reads the repository's response, extracts and
01208 cross-certifies the BPKI data and service URI, and links the
01209 repository data with the corresponding parent data in our local
01210 database.
01211 """
01212
01213 argv = arg.split()
01214
01215 if len(argv) != 1:
01216 raise RuntimeError, "Need to specify filename for repository.xml on command line"
01217
01218 r = etree_read(argv[0])
01219
01220 parent_handle = r.get("parent_handle")
01221
01222 print "Repository calls us %r" % (r.get("client_handle"))
01223 print "Repository response associated with parent_handle %r" % parent_handle
01224
01225 etree_write(r, self.entitydb("repositories", "%s.xml" % parent_handle))
01226
01227
01228
01229
01230 def configure_resources_main(self, msg = None):
01231 """
01232 Main program of old myrpki.py script. This remains separate
01233 because it's called from more than one place.
01234 """
01235
01236 roa_csv_file = self.cfg.get("roa_csv")
01237 prefix_csv_file = self.cfg.get("prefix_csv")
01238 asn_csv_file = self.cfg.get("asn_csv")
01239
01240
01241
01242 xml_filename = self.cfg.get("xml_filename")
01243
01244 try:
01245 e = etree_read(xml_filename)
01246 bsc_req, bsc_cer = self.bpki_resources.bsc(e.findtext("bpki_bsc_pkcs10"))
01247 service_uri = e.get("service_uri")
01248 server_ta = e.findtext("bpki_server_ta")
01249 except IOError:
01250 bsc_req, bsc_cer = None, None
01251 service_uri = None
01252 server_ta = None
01253
01254 e = Element("myrpki", handle = self.handle)
01255
01256 if service_uri:
01257 e.set("service_uri", service_uri)
01258
01259 roa_requests.from_csv(roa_csv_file).xml(e)
01260
01261 children.from_csv(
01262 prefix_csv_file = prefix_csv_file,
01263 asn_csv_file = asn_csv_file,
01264 fxcert = self.bpki_resources.fxcert,
01265 entitydb = self.entitydb).xml(e)
01266
01267 parents.from_csv( fxcert = self.bpki_resources.fxcert, entitydb = self.entitydb).xml(e)
01268 repositories.from_csv(fxcert = self.bpki_resources.fxcert, entitydb = self.entitydb).xml(e)
01269
01270 PEMElement(e, "bpki_ca_certificate", self.bpki_resources.cer)
01271 PEMElement(e, "bpki_crl", self.bpki_resources.crl)
01272
01273 if bsc_cer:
01274 PEMElement(e, "bpki_bsc_certificate", bsc_cer)
01275
01276 if bsc_req:
01277 PEMElement(e, "bpki_bsc_pkcs10", bsc_req)
01278
01279 if server_ta:
01280 SubElement(e, "bpki_server_ta").text = server_ta
01281
01282 etree_write(e, xml_filename, msg = msg)
01283
01284
01285 def do_configure_resources(self, arg):
01286 """
01287 Read CSV files and all the descriptions of parents and children
01288 that we've built up, package the result up as a single XML file to
01289 be shipped to a hosting rpkid.
01290 """
01291
01292 if arg:
01293 raise RuntimeError, "Unexpected argument %r" % arg
01294 self.configure_resources_main(msg = "Send this file to the rpkid operator who is hosting you")
01295
01296
01297
01298 def do_configure_daemons(self, arg):
01299 """
01300 Configure RPKI daemons with the data built up by the other
01301 commands in this program.
01302
01303 The basic model here is that each entity with resources to certify
01304 runs the myrpki tool, but not all of them necessarily run their
01305 own RPKI engines. The entities that do run RPKI engines get data
01306 from the entities they host via the XML files output by the
01307 configure_resources command. Those XML files are the input to
01308 this command, which uses them to do all the work of configuring
01309 daemons, populating SQL databases, and so forth. A few operations
01310 (eg, BSC construction) generate data which has to be shipped back
01311 to the resource holder, which we do by updating the same XML file.
01312
01313 In essence, the XML files are a sneakernet (or email, or carrier
01314 pigeon) communication channel between the resource holders and the
01315 RPKI engine operators.
01316
01317 As a convenience, for the normal case where the RPKI engine
01318 operator is itself a resource holder, this command in effect runs
01319 the configure_resources command automatically to process the RPKI
01320 engine operator's own resources.
01321
01322 Note that, due to the back and forth nature of some of these
01323 operations, it may take several cycles for data structures to stablize
01324 and everything to reach a steady state. This is normal.
01325 """
01326
01327 argv = arg.split()
01328
01329 try:
01330 import rpki.https, rpki.resource_set, rpki.relaxng, rpki.exceptions
01331 import rpki.left_right, rpki.x509, rpki.async
01332 if hasattr(warnings, "catch_warnings"):
01333 with warnings.catch_warnings():
01334 warnings.simplefilter("ignore", DeprecationWarning)
01335 import MySQLdb
01336 else:
01337 import MySQLdb
01338
01339 except ImportError, e:
01340 print "Sorry, you appear to be missing some of the Python modules needed to run this command"
01341 print "[Error: %r]" % e
01342
01343 def findbase64(tree, name, b64type = rpki.x509.X509):
01344 x = tree.findtext(name)
01345 return b64type(Base64 = x) if x else None
01346
01347
01348
01349
01350 bsc_handle = "bsc"
01351
01352 self.cfg.set_global_flags()
01353
01354
01355
01356
01357 self_crl_interval = self.cfg.getint("self_crl_interval", 2 * 60 * 60)
01358 self_regen_margin = self.cfg.getint("self_regen_margin", self_crl_interval / 4)
01359 pubd_base = "https://%s:%s/" % (self.cfg.get("pubd_server_host"), self.cfg.get("pubd_server_port"))
01360 rpkid_base = "https://%s:%s/" % (self.cfg.get("rpkid_server_host"), self.cfg.get("rpkid_server_port"))
01361
01362
01363
01364 call_rpkid = rpki.async.sync_wrapper(rpki.https.caller(
01365 proto = rpki.left_right,
01366 client_key = rpki.x509.RSA( PEM_file = self.bpki_servers.dir + "/irbe.key"),
01367 client_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/irbe.cer"),
01368 server_ta = rpki.x509.X509(PEM_file = self.bpki_servers.cer),
01369 server_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/rpkid.cer"),
01370 url = rpkid_base + "left-right",
01371 debug = self.show_xml))
01372
01373 if self.run_pubd:
01374
01375 call_pubd = rpki.async.sync_wrapper(rpki.https.caller(
01376 proto = rpki.publication,
01377 client_key = rpki.x509.RSA( PEM_file = self.bpki_servers.dir + "/irbe.key"),
01378 client_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/irbe.cer"),
01379 server_ta = rpki.x509.X509(PEM_file = self.bpki_servers.cer),
01380 server_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/pubd.cer"),
01381 url = pubd_base + "control",
01382 debug = self.show_xml))
01383
01384
01385
01386 call_pubd(rpki.publication.config_elt.make_pdu(
01387 action = "set",
01388 bpki_crl = rpki.x509.CRL(PEM_file = self.bpki_servers.crl)))
01389
01390 irdbd_cfg = rpki.config.parser(self.cfg.get("irdbd_conf", self.cfg_file), "irdbd")
01391
01392 db = MySQLdb.connect(user = irdbd_cfg.get("sql-username"),
01393 db = irdbd_cfg.get("sql-database"),
01394 passwd = irdbd_cfg.get("sql-password"))
01395
01396 cur = db.cursor()
01397
01398 xmlfiles = []
01399
01400
01401
01402
01403
01404 my_xmlfile = self.cfg.get("xml_filename", "")
01405 if my_xmlfile:
01406 self.configure_resources_main()
01407 xmlfiles.append(my_xmlfile)
01408 else:
01409 my_xmlfile = None
01410
01411
01412
01413 xmlfiles.extend(argv)
01414
01415 for xmlfile in xmlfiles:
01416
01417
01418
01419 tree = etree_read(xmlfile, validate = True)
01420
01421 handle = tree.get("handle")
01422
01423
01424
01425 cur.execute(
01426 """
01427 DELETE
01428 FROM roa_request_prefix
01429 USING roa_request, roa_request_prefix
01430 WHERE roa_request.roa_request_id = roa_request_prefix.roa_request_id AND roa_request.roa_request_handle = %s
01431 """, (handle,))
01432
01433 cur.execute("DELETE FROM roa_request WHERE roa_request.roa_request_handle = %s", (handle,))
01434
01435 for x in tree.getiterator("roa_request"):
01436 cur.execute("INSERT roa_request (roa_request_handle, asn) VALUES (%s, %s)", (handle, x.get("asn")))
01437 roa_request_id = cur.lastrowid
01438 for version, prefix_set in ((4, rpki.resource_set.roa_prefix_set_ipv4(x.get("v4"))), (6, rpki.resource_set.roa_prefix_set_ipv6(x.get("v6")))):
01439 if prefix_set:
01440 cur.executemany("INSERT roa_request_prefix (roa_request_id, prefix, prefixlen, max_prefixlen, version) VALUES (%s, %s, %s, %s, %s)",
01441 ((roa_request_id, p.prefix, p.prefixlen, p.max_prefixlen, version) for p in prefix_set))
01442
01443 cur.execute(
01444 """
01445 DELETE
01446 FROM registrant_asn
01447 USING registrant, registrant_asn
01448 WHERE registrant.registrant_id = registrant_asn.registrant_id AND registrant.registry_handle = %s
01449 """ , (handle,))
01450
01451 cur.execute(
01452 """
01453 DELETE FROM registrant_net USING registrant, registrant_net
01454 WHERE registrant.registrant_id = registrant_net.registrant_id AND registrant.registry_handle = %s
01455 """ , (handle,))
01456
01457 cur.execute("DELETE FROM registrant WHERE registrant.registry_handle = %s" , (handle,))
01458
01459 for x in tree.getiterator("child"):
01460 child_handle = x.get("handle")
01461 asns = rpki.resource_set.resource_set_as(x.get("asns"))
01462 ipv4 = rpki.resource_set.resource_set_ipv4(x.get("v4"))
01463 ipv6 = rpki.resource_set.resource_set_ipv6(x.get("v6"))
01464
01465 cur.execute("INSERT registrant (registrant_handle, registry_handle, registrant_name, valid_until) VALUES (%s, %s, %s, %s)",
01466 (child_handle, handle, child_handle, rpki.sundial.datetime.fromXMLtime(x.get("valid_until")).to_sql()))
01467 child_id = cur.lastrowid
01468 if asns:
01469 cur.executemany("INSERT registrant_asn (start_as, end_as, registrant_id) VALUES (%s, %s, %s)",
01470 ((a.min, a.max, child_id) for a in asns))
01471 if ipv4:
01472 cur.executemany("INSERT registrant_net (start_ip, end_ip, version, registrant_id) VALUES (%s, %s, 4, %s)",
01473 ((a.min, a.max, child_id) for a in ipv4))
01474 if ipv6:
01475 cur.executemany("INSERT registrant_net (start_ip, end_ip, version, registrant_id) VALUES (%s, %s, 6, %s)",
01476 ((a.min, a.max, child_id) for a in ipv6))
01477
01478 db.commit()
01479
01480
01481
01482 hosted_cacert = findbase64(tree, "bpki_ca_certificate")
01483 if not hosted_cacert:
01484 print "Nothing else I can do without a trust anchor for the entity I'm hosting."
01485 continue
01486
01487 rpkid_xcert = rpki.x509.X509(PEM_file = self.bpki_servers.fxcert(b64 = hosted_cacert.get_Base64(),
01488 filename = handle + ".cacert.cer",
01489 path_restriction = 1))
01490
01491
01492
01493 if self.run_pubd:
01494 client_pdus = dict((x.client_handle, x)
01495 for x in call_pubd(rpki.publication.client_elt.make_pdu(action = "list"))
01496 if isinstance(x, rpki.publication.client_elt))
01497
01498 rpkid_reply = call_rpkid(
01499 rpki.left_right.self_elt.make_pdu( action = "get", tag = "self", self_handle = handle),
01500 rpki.left_right.bsc_elt.make_pdu( action = "list", tag = "bsc", self_handle = handle),
01501 rpki.left_right.repository_elt.make_pdu(action = "list", tag = "repository", self_handle = handle),
01502 rpki.left_right.parent_elt.make_pdu( action = "list", tag = "parent", self_handle = handle),
01503 rpki.left_right.child_elt.make_pdu( action = "list", tag = "child", self_handle = handle))
01504
01505 self_pdu = rpkid_reply[0]
01506 bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt))
01507 repository_pdus = dict((x.repository_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.repository_elt))
01508 parent_pdus = dict((x.parent_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.parent_elt))
01509 child_pdus = dict((x.child_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.child_elt))
01510
01511 pubd_query = []
01512 rpkid_query = []
01513
01514
01515
01516 if (isinstance(self_pdu, rpki.left_right.report_error_elt) or
01517 self_pdu.crl_interval != self_crl_interval or
01518 self_pdu.regen_margin != self_regen_margin or
01519 self_pdu.bpki_cert != rpkid_xcert):
01520 rpkid_query.append(rpki.left_right.self_elt.make_pdu(
01521 action = "create" if isinstance(self_pdu, rpki.left_right.report_error_elt) else "set",
01522 tag = "self",
01523 self_handle = handle,
01524 bpki_cert = rpkid_xcert,
01525 crl_interval = self_crl_interval,
01526 regen_margin = self_regen_margin))
01527
01528
01529
01530
01531
01532
01533 bsc_cert = findbase64(tree, "bpki_bsc_certificate")
01534 bsc_crl = findbase64(tree, "bpki_crl", rpki.x509.CRL)
01535
01536 bsc_pdu = bsc_pdus.pop(bsc_handle, None)
01537
01538 if bsc_pdu is None:
01539 rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
01540 action = "create",
01541 tag = "bsc",
01542 self_handle = handle,
01543 bsc_handle = bsc_handle,
01544 generate_keypair = "yes"))
01545 elif bsc_pdu.signing_cert != bsc_cert or bsc_pdu.signing_cert_crl != bsc_crl:
01546 rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
01547 action = "set",
01548 tag = "bsc",
01549 self_handle = handle,
01550 bsc_handle = bsc_handle,
01551 signing_cert = bsc_cert,
01552 signing_cert_crl = bsc_crl))
01553
01554 rpkid_query.extend(rpki.left_right.bsc_elt.make_pdu(
01555 action = "destroy", self_handle = handle, bsc_handle = b) for b in bsc_pdus)
01556
01557 bsc_req = None
01558
01559 if bsc_pdu and bsc_pdu.pkcs10_request:
01560 bsc_req = bsc_pdu.pkcs10_request
01561
01562
01563
01564
01565
01566
01567
01568 for repository in tree.getiterator("repository"):
01569
01570 repository_handle = repository.get("handle")
01571 repository_pdu = repository_pdus.pop(repository_handle, None)
01572 repository_uri = repository.get("service_uri")
01573 repository_cert = findbase64(repository, "bpki_certificate")
01574
01575 if (repository_pdu is None or
01576 repository_pdu.bsc_handle != bsc_handle or
01577 repository_pdu.peer_contact_uri != repository_uri or
01578 repository_pdu.bpki_cert != repository_cert):
01579 rpkid_query.append(rpki.left_right.repository_elt.make_pdu(
01580 action = "create" if repository_pdu is None else "set",
01581 tag = repository_handle,
01582 self_handle = handle,
01583 repository_handle = repository_handle,
01584 bsc_handle = bsc_handle,
01585 peer_contact_uri = repository_uri,
01586 bpki_cert = repository_cert))
01587
01588 rpkid_query.extend(rpki.left_right.repository_elt.make_pdu(
01589 action = "destroy", self_handle = handle, repository_handle = r) for r in repository_pdus)
01590
01591
01592
01593
01594
01595
01596 for parent in tree.getiterator("parent"):
01597
01598 parent_handle = parent.get("handle")
01599 parent_pdu = parent_pdus.pop(parent_handle, None)
01600 parent_uri = parent.get("service_uri")
01601 parent_myhandle = parent.get("myhandle")
01602 parent_sia_base = parent.get("sia_base")
01603 parent_cms_cert = findbase64(parent, "bpki_cms_certificate")
01604 parent_https_cert = findbase64(parent, "bpki_https_certificate")
01605
01606 if (parent_pdu is None or
01607 parent_pdu.bsc_handle != bsc_handle or
01608 parent_pdu.repository_handle != parent_handle or
01609 parent_pdu.peer_contact_uri != parent_uri or
01610 parent_pdu.sia_base != parent_sia_base or
01611 parent_pdu.sender_name != parent_myhandle or
01612 parent_pdu.recipient_name != parent_handle or
01613 parent_pdu.bpki_cms_cert != parent_cms_cert or
01614 parent_pdu.bpki_https_cert != parent_https_cert):
01615 rpkid_query.append(rpki.left_right.parent_elt.make_pdu(
01616 action = "create" if parent_pdu is None else "set",
01617 tag = parent_handle,
01618 self_handle = handle,
01619 parent_handle = parent_handle,
01620 bsc_handle = bsc_handle,
01621 repository_handle = parent_handle,
01622 peer_contact_uri = parent_uri,
01623 sia_base = parent_sia_base,
01624 sender_name = parent_myhandle,
01625 recipient_name = parent_handle,
01626 bpki_cms_cert = parent_cms_cert,
01627 bpki_https_cert = parent_https_cert))
01628
01629 rpkid_query.extend(rpki.left_right.parent_elt.make_pdu(
01630 action = "destroy", self_handle = handle, parent_handle = p) for p in parent_pdus)
01631
01632
01633
01634
01635
01636 for child in tree.getiterator("child"):
01637
01638 child_handle = child.get("handle")
01639 child_pdu = child_pdus.pop(child_handle, None)
01640 child_cert = findbase64(child, "bpki_certificate")
01641
01642 if (child_pdu is None or
01643 child_pdu.bsc_handle != bsc_handle or
01644 child_pdu.bpki_cert != child_cert):
01645 rpkid_query.append(rpki.left_right.child_elt.make_pdu(
01646 action = "create" if child_pdu is None else "set",
01647 tag = child_handle,
01648 self_handle = handle,
01649 child_handle = child_handle,
01650 bsc_handle = bsc_handle,
01651 bpki_cert = child_cert))
01652
01653 rpkid_query.extend(rpki.left_right.child_elt.make_pdu(
01654 action = "destroy", self_handle = handle, child_handle = c) for c in child_pdus)
01655
01656
01657
01658 if self.run_pubd:
01659
01660 for f in self.entitydb.iterate("pubclients", "*.xml"):
01661 c = etree_read(f)
01662
01663 client_handle = c.get("client_handle")
01664 client_base_uri = c.get("sia_base")
01665 client_bpki_cert = rpki.x509.X509(PEM_file = self.bpki_servers.fxcert(c.findtext("bpki_client_ta")))
01666 client_pdu = client_pdus.pop(client_handle, None)
01667
01668 if (client_pdu is None or
01669 client_pdu.base_uri != client_base_uri or
01670 client_pdu.bpki_cert != client_bpki_cert):
01671 pubd_query.append(rpki.publication.client_elt.make_pdu(
01672 action = "create" if client_pdu is None else "set",
01673 client_handle = client_handle,
01674 bpki_cert = client_bpki_cert,
01675 base_uri = client_base_uri))
01676
01677 pubd_query.extend(rpki.publication.client_elt.make_pdu(
01678 action = "destroy", client_handle = p) for p in client_pdus)
01679
01680
01681
01682 failed = False
01683
01684 if rpkid_query:
01685 rpkid_reply = call_rpkid(*rpkid_query)
01686 bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt))
01687 if bsc_handle in bsc_pdus and bsc_pdus[bsc_handle].pkcs10_request:
01688 bsc_req = bsc_pdus[bsc_handle].pkcs10_request
01689 for r in rpkid_reply:
01690 if isinstance(r, rpki.left_right.report_error_elt):
01691 failed = True
01692 print "rpkid reported failure:", r.error_code
01693 if r.error_text:
01694 print r.error_text
01695
01696 if failed:
01697 raise RuntimeError
01698
01699 if pubd_query:
01700 assert self.run_pubd
01701 pubd_reply = call_pubd(*pubd_query)
01702 for r in pubd_reply:
01703 if isinstance(r, rpki.publication.report_error_elt):
01704 failed = True
01705 print "pubd reported failure:", r.error_code
01706 if r.error_text:
01707 print r.error_text
01708
01709 if failed:
01710 raise RuntimeError
01711
01712
01713
01714 e = tree.find("bpki_bsc_pkcs10")
01715 if e is not None:
01716 tree.remove(e)
01717 if bsc_req is not None:
01718 SubElement(tree, "bpki_bsc_pkcs10").text = bsc_req.get_Base64()
01719
01720 tree.set("service_uri", rpkid_base + "up-down/" + handle)
01721
01722 e = tree.find("bpki_server_ta")
01723 if e is not None:
01724 tree.remove(e)
01725 PEMElement(tree, "bpki_server_ta", self.bpki_resources.cer)
01726
01727 etree_write(tree, xmlfile, validate = True,
01728 msg = None if xmlfile is my_xmlfile else 'Send this file back to the hosted entity ("%s")' % handle)
01729
01730 db.close()
01731
01732
01733
01734
01735 rpki.async.event_loop()