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