aboutsummaryrefslogtreecommitdiff
path: root/myrpki/myrpki.py
diff options
context:
space:
mode:
Diffstat (limited to 'myrpki/myrpki.py')
-rw-r--r--myrpki/myrpki.py1742
1 files changed, 1742 insertions, 0 deletions
diff --git a/myrpki/myrpki.py b/myrpki/myrpki.py
new file mode 100644
index 00000000..a67ce15e
--- /dev/null
+++ b/myrpki/myrpki.py
@@ -0,0 +1,1742 @@
+"""
+This program is now the merger of three different tools: the old
+myrpki.py script, the old myirbe.py script, and the newer setup.py CLI
+tool. As such, it is still in need of some cleanup, but the need to
+provide a saner user interface is more urgent than internal code
+prettiness at the moment. In the long run, 90% of the code in this
+file probably ought to move to well-designed library modules.
+
+Overall goal here is to build up the configuration necessary to run
+rpkid and friends, by reading a config file, a collection of .CSV
+files, and the results of a few out-of-band XML setup messages
+exchanged with one's parents, children, and so forth.
+
+The config file is in an OpenSSL-compatible format, the CSV files are
+simple tab-delimited text. The XML files are all generated by this
+program, either the local instance or an instance being run by another
+player in the system; the mechanism used to exchange these setup
+messages is outside the scope of this program, feel free to use
+PGP-signed mail, a web interface (not provided), USB stick, carrier
+pigeons, whatever works.
+
+With one exception, the commands in this program avoid using any
+third-party Python code other than the rpki libraries themselves; with
+the same one exception, all OpenSSL work is done with the OpenSSL
+command line tool (the one built as a side effect of building rcynic
+will do, if your platform has no system copy or the system copy is too
+old). This is all done in an attempt to make the code more portable,
+so one can run most of the RPKI back end software on a laptop or
+whatever. The one exception is the configure_daemons command, which
+must, of necessity, use the same communication libraries as the
+daemons with which it is conversing. So that one command will not
+work if the correct Python modules are not available.
+
+
+$Id$
+
+Copyright (C) 2009-2010 Internet Systems Consortium ("ISC")
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+"""
+
+from __future__ import with_statement
+
+import subprocess, csv, re, os, getopt, sys, base64, time, glob, copy, warnings
+import rpki.config, rpki.cli, rpki.sundial, rpki.log, rpki.oids
+
+try:
+ from lxml.etree import (Element, SubElement, ElementTree,
+ fromstring as ElementFromString,
+ tostring as ElementToString)
+except ImportError:
+ from xml.etree.ElementTree import (Element, SubElement, ElementTree,
+ fromstring as ElementFromString,
+ tostring as ElementToString)
+
+
+
+# Our XML namespace and protocol version.
+
+namespace = "http://www.hactrn.net/uris/rpki/myrpki/"
+version = "2"
+namespaceQName = "{" + namespace + "}"
+
+# Whether to include incomplete entries when rendering to XML.
+
+allow_incomplete = False
+
+# Whether to whine about incomplete entries while rendering to XML.
+
+whine = False
+
+class comma_set(set):
+ """
+ Minor customization of set(), to provide a print syntax.
+ """
+
+ def __str__(self):
+ return ",".join(self)
+
+class EntityDB(object):
+ """
+ Wrapper for entitydb path lookups. Hmm, maybe some or all of the
+ entitydb glob stuff should end up here too? Later.
+ """
+
+ def __init__(self, cfg):
+ self.dir = cfg.get("entitydb_dir", "entitydb")
+
+ def __call__(self, *args):
+ return os.path.join(self.dir, *args)
+
+ def iterate(self, *args):
+ return glob.iglob(os.path.join(self.dir, *args))
+
+class roa_request(object):
+ """
+ Representation of a ROA request.
+ """
+
+ v4re = re.compile("^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]+(-[0-9]+)?$", re.I)
+ v6re = re.compile("^([0-9a-f]{0,4}:){0,15}[0-9a-f]{0,4}/[0-9]+(-[0-9]+)?$", re.I)
+
+ def __init__(self, asn, group):
+ self.asn = asn
+ self.group = group
+ self.v4 = comma_set()
+ self.v6 = comma_set()
+
+ def __repr__(self):
+ s = "<%s asn %s group %s" % (self.__class__.__name__, self.asn, self.group)
+ if self.v4:
+ s += " v4 %s" % self.v4
+ if self.v6:
+ s += " v6 %s" % self.v6
+ return s + ">"
+
+ def add(self, prefix):
+ """
+ Add one prefix to this ROA request.
+ """
+ if self.v4re.match(prefix):
+ self.v4.add(prefix)
+ elif self.v6re.match(prefix):
+ self.v6.add(prefix)
+ else:
+ raise RuntimeError, "Bad prefix syntax: %r" % (prefix,)
+
+ def xml(self, e):
+ """
+ Generate XML element represeting representing this ROA request.
+ """
+ e = SubElement(e, "roa_request",
+ asn = self.asn,
+ v4 = str(self.v4),
+ v6 = str(self.v6))
+ e.tail = "\n"
+
+class roa_requests(dict):
+ """
+ Database of ROA requests.
+ """
+
+ def add(self, asn, group, prefix):
+ """
+ Add one <ASN, group, prefix> set to ROA request database.
+ """
+ key = (asn, group)
+ if key not in self:
+ self[key] = roa_request(asn, group)
+ self[key].add(prefix)
+
+ def xml(self, e):
+ """
+ Render ROA requests as XML elements.
+ """
+ for r in self.itervalues():
+ r.xml(e)
+
+ @classmethod
+ def from_csv(cls, roa_csv_file):
+ """
+ Parse ROA requests from CSV file.
+ """
+ self = cls()
+ # format: p/n-m asn group
+ for pnm, asn, group in csv_reader(roa_csv_file, columns = 3):
+ self.add(asn = asn, group = group, prefix = pnm)
+ return self
+
+class child(object):
+ """
+ Representation of one child entity.
+ """
+
+ 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)
+ 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)
+
+ def __init__(self, handle):
+ self.handle = handle
+ self.asns = comma_set()
+ self.v4 = comma_set()
+ self.v6 = comma_set()
+ self.validity = None
+ self.bpki_certificate = None
+
+ def __repr__(self):
+ s = "<%s %s" % (self.__class__.__name__, self.handle)
+ if self.asns:
+ s += " asn %s" % self.asns
+ if self.v4:
+ s += " v4 %s" % self.v4
+ if self.v6:
+ s += " v6 %s" % self.v6
+ if self.validity:
+ s += " valid %s" % self.validity
+ if self.bpki_certificate:
+ s += " cert %s" % self.bpki_certificate
+ return s + ">"
+
+ def add(self, prefix = None, asn = None, validity = None, bpki_certificate = None):
+ """
+ Add prefix, autonomous system number, validity date, or BPKI
+ certificate for this child.
+ """
+ if prefix is not None:
+ if self.v4re.match(prefix):
+ self.v4.add(prefix)
+ elif self.v6re.match(prefix):
+ self.v6.add(prefix)
+ else:
+ raise RuntimeError, "Bad prefix syntax: %r" % (prefix,)
+ if asn is not None:
+ self.asns.add(asn)
+ if validity is not None:
+ self.validity = validity
+ if bpki_certificate is not None:
+ self.bpki_certificate = bpki_certificate
+
+ def xml(self, e):
+ """
+ Render this child as an XML element.
+ """
+ complete = self.bpki_certificate and self.validity
+ if whine and not complete:
+ print "Incomplete child entry %s" % self
+ if complete or allow_incomplete:
+ e = SubElement(e, "child",
+ handle = self.handle,
+ valid_until = self.validity,
+ asns = str(self.asns),
+ v4 = str(self.v4),
+ v6 = str(self.v6))
+ e.tail = "\n"
+ if self.bpki_certificate:
+ PEMElement(e, "bpki_certificate", self.bpki_certificate)
+
+class children(dict):
+ """
+ Database of children.
+ """
+
+ def add(self, handle, prefix = None, asn = None, validity = None, bpki_certificate = None):
+ """
+ Add resources to a child, creating the child object if necessary.
+ """
+ if handle not in self:
+ self[handle] = child(handle)
+ self[handle].add(prefix = prefix, asn = asn, validity = validity, bpki_certificate = bpki_certificate)
+
+ def xml(self, e):
+ """
+ Render children database to XML.
+ """
+ for c in self.itervalues():
+ c.xml(e)
+
+ @classmethod
+ def from_csv(cls, prefix_csv_file, asn_csv_file, fxcert, entitydb):
+ """
+ Parse child resources, certificates, and validity dates from CSV files.
+ """
+ self = cls()
+ for f in entitydb.iterate("children", "*.xml"):
+ c = etree_read(f)
+ self.add(handle = os.path.splitext(os.path.split(f)[-1])[0],
+ validity = c.get("valid_until"),
+ bpki_certificate = fxcert(c.findtext("bpki_child_ta")))
+ # childname p/n
+ for handle, pn in csv_reader(prefix_csv_file, columns = 2):
+ self.add(handle = handle, prefix = pn)
+ # childname asn
+ for handle, asn in csv_reader(asn_csv_file, columns = 2):
+ self.add(handle = handle, asn = asn)
+ return self
+
+class parent(object):
+ """
+ Representation of one parent entity.
+ """
+
+ def __init__(self, handle):
+ self.handle = handle
+ self.service_uri = None
+ self.bpki_cms_certificate = None
+ self.bpki_https_certificate = None
+ self.myhandle = None
+ self.sia_base = None
+
+ def __repr__(self):
+ s = "<%s %s" % (self.__class__.__name__, self.handle)
+ if self.myhandle:
+ s += " myhandle %s" % self.myhandle
+ if self.service_uri:
+ s += " uri %s" % self.service_uri
+ if self.sia_base:
+ s += " sia %s" % self.sia_base
+ if self.bpki_cms_certificate:
+ s += " cms %s" % self.bpki_cms_certificate
+ if self.bpki_https_certificate:
+ s += " https %s" % self.bpki_https_certificate
+ return s + ">"
+
+ def add(self, service_uri = None,
+ bpki_cms_certificate = None,
+ bpki_https_certificate = None,
+ myhandle = None,
+ sia_base = None):
+ """
+ Add service URI or BPKI certificates to this parent object.
+ """
+ if service_uri is not None:
+ self.service_uri = service_uri
+ if bpki_cms_certificate is not None:
+ self.bpki_cms_certificate = bpki_cms_certificate
+ if bpki_https_certificate is not None:
+ self.bpki_https_certificate = bpki_https_certificate
+ if myhandle is not None:
+ self.myhandle = myhandle
+ if sia_base is not None:
+ self.sia_base = sia_base
+
+ def xml(self, e):
+ """
+ Render this parent object to XML.
+ """
+ complete = self.bpki_cms_certificate and self.bpki_https_certificate and self.myhandle and self.service_uri and self.sia_base
+ if whine and not complete:
+ print "Incomplete parent entry %s" % self
+ if complete or allow_incomplete:
+ e = SubElement(e, "parent",
+ handle = self.handle,
+ myhandle = self.myhandle,
+ service_uri = self.service_uri,
+ sia_base = self.sia_base)
+ e.tail = "\n"
+ if self.bpki_cms_certificate:
+ PEMElement(e, "bpki_cms_certificate", self.bpki_cms_certificate)
+ if self.bpki_https_certificate:
+ PEMElement(e, "bpki_https_certificate", self.bpki_https_certificate)
+
+class parents(dict):
+ """
+ Database of parent objects.
+ """
+
+ def add(self, handle,
+ service_uri = None,
+ bpki_cms_certificate = None,
+ bpki_https_certificate = None,
+ myhandle = None,
+ sia_base = None):
+ """
+ Add service URI or certificates to parent object, creating it if necessary.
+ """
+ if handle not in self:
+ self[handle] = parent(handle)
+ self[handle].add(service_uri = service_uri,
+ bpki_cms_certificate = bpki_cms_certificate,
+ bpki_https_certificate = bpki_https_certificate,
+ myhandle = myhandle,
+ sia_base = sia_base)
+
+ def xml(self, e):
+ for c in self.itervalues():
+ c.xml(e)
+
+ @classmethod
+ def from_csv(cls, fxcert, entitydb):
+ """
+ Parse parent data from entitydb.
+ """
+ self = cls()
+ for f in entitydb.iterate("parents", "*.xml"):
+ h = os.path.splitext(os.path.split(f)[-1])[0]
+ p = etree_read(f)
+ r = etree_read(f.replace(os.path.sep + "parents" + os.path.sep,
+ os.path.sep + "repositories" + os.path.sep))
+ assert r.get("type") == "confirmed"
+ self.add(handle = h,
+ service_uri = p.get("service_uri"),
+ bpki_cms_certificate = fxcert(p.findtext("bpki_resource_ta")),
+ bpki_https_certificate = fxcert(p.findtext("bpki_server_ta")),
+ myhandle = p.get("child_handle"),
+ sia_base = r.get("sia_base"))
+ return self
+
+class repository(object):
+ """
+ Representation of one repository entity.
+ """
+
+ def __init__(self, handle):
+ self.handle = handle
+ self.service_uri = None
+ self.bpki_certificate = None
+
+ def __repr__(self):
+ s = "<%s %s" % (self.__class__.__name__, self.handle)
+ if self.service_uri:
+ s += " uri %s" % self.service_uri
+ if self.bpki_certificate:
+ s += " cert %s" % self.bpki_certificate
+ return s + ">"
+
+ def add(self, service_uri = None, bpki_certificate = None):
+ """
+ Add service URI or BPKI certificates to this repository object.
+ """
+ if service_uri is not None:
+ self.service_uri = service_uri
+ if bpki_certificate is not None:
+ self.bpki_certificate = bpki_certificate
+
+ def xml(self, e):
+ """
+ Render this repository object to XML.
+ """
+ complete = self.bpki_certificate and self.service_uri
+ if whine and not complete:
+ print "Incomplete repository entry %s" % self
+ if complete or allow_incomplete:
+ e = SubElement(e, "repository",
+ handle = self.handle,
+ service_uri = self.service_uri)
+ e.tail = "\n"
+ if self.bpki_certificate:
+ PEMElement(e, "bpki_certificate", self.bpki_certificate)
+
+class repositories(dict):
+ """
+ Database of repository objects.
+ """
+
+ def add(self, handle,
+ service_uri = None,
+ bpki_certificate = None):
+ """
+ Add service URI or certificate to repository object, creating it if necessary.
+ """
+ if handle not in self:
+ self[handle] = repository(handle)
+ self[handle].add(service_uri = service_uri,
+ bpki_certificate = bpki_certificate)
+
+ def xml(self, e):
+ for c in self.itervalues():
+ c.xml(e)
+
+ @classmethod
+ def from_csv(cls, fxcert, entitydb):
+ """
+ Parse repository data from entitydb.
+ """
+ self = cls()
+ for f in entitydb.iterate("repositories", "*.xml"):
+ h = os.path.splitext(os.path.split(f)[-1])[0]
+ r = etree_read(f)
+ assert r.get("type") == "confirmed"
+ self.add(handle = h,
+ service_uri = r.get("service_uri"),
+ bpki_certificate = fxcert(r.findtext("bpki_server_ta")))
+ return self
+
+class csv_reader(object):
+ """
+ Reader for tab-delimited text that's (slightly) friendlier than the
+ stock Python csv module (which isn't intended for direct use by
+ humans anyway, and neither was this package originally, but that
+ seems to be the way that it has evolved...).
+
+ Columns parameter specifies how many columns users of the reader
+ expect to see; lines with fewer columns will be padded with None
+ values.
+
+ Original API design for this class courtesy of Warren Kumari, but
+ don't blame him if you don't like what I did with his ideas.
+ """
+
+ def __init__(self, filename, columns = None, min_columns = None, comment_characters = "#;"):
+ assert columns is None or isinstance(columns, int)
+ assert min_columns is None or isinstance(min_columns, int)
+ if columns is not None and min_columns is None:
+ min_columns = columns
+ self.filename = filename
+ self.columns = columns
+ self.min_columns = min_columns
+ self.comment_characters = comment_characters
+ self.file = open(filename, "r")
+
+ def __iter__(self):
+ line_number = 0
+ for line in self.file:
+ line_number += 1
+ line = line.strip()
+ if not line or line[0] in self.comment_characters:
+ continue
+ fields = line.split()
+ if self.min_columns is not None and len(fields) < self.min_columns:
+ raise RuntimeError, "%s:%d: Not enough columns in line %r" % (self.filename, line_number, line)
+ if self.columns is not None and len(fields) > self.columns:
+ raise RuntimeError, "%s:%d: Too many columns in line %r" % (self.filename, line_number, line)
+ if self.columns is not None and len(fields) < self.columns:
+ fields += tuple(None for i in xrange(self.columns - len(fields)))
+ yield fields
+
+def csv_writer(filename):
+ """
+ Writer object for tab delimited text. We just use the stock CSV
+ module in excel-tab mode for this.
+ """
+ return csv.writer(open(filename, "w"), dialect = csv.get_dialect("excel-tab"))
+
+
+def PEMElement(e, tag, filename, **kwargs):
+ """
+ Create an XML element containing Base64 encoded data taken from a
+ PEM file.
+ """
+ lines = open(filename).readlines()
+ while lines:
+ if lines.pop(0).startswith("-----BEGIN "):
+ break
+ while lines:
+ if lines.pop(-1).startswith("-----END "):
+ break
+ if e.text is None:
+ e.text = "\n"
+ se = SubElement(e, tag, **kwargs)
+ se.text = "\n" + "".join(lines)
+ se.tail = "\n"
+ return se
+
+class CA(object):
+ """
+ Representation of one certification authority.
+ """
+
+ # Mapping of path restriction values we use to OpenSSL config file
+ # section names.
+
+ path_restriction = { 0 : "ca_x509_ext_xcert0",
+ 1 : "ca_x509_ext_xcert1" }
+
+ def __init__(self, cfg_file, dir):
+ self.cfg = cfg_file
+ self.dir = dir
+ self.cer = dir + "/ca.cer"
+ self.key = dir + "/ca.key"
+ self.req = dir + "/ca.req"
+ self.crl = dir + "/ca.crl"
+ self.index = dir + "/index"
+ self.serial = dir + "/serial"
+ self.crlnum = dir + "/crl_number"
+
+ cfg = rpki.config.parser(cfg_file, "myrpki")
+ self.openssl = cfg.get("openssl", "openssl")
+
+ self.env = { "PATH" : os.environ["PATH"],
+ "BPKI_DIRECTORY" : dir,
+ "RANDFILE" : ".OpenSSL.whines.unless.I.set.this",
+ "OPENSSL_CONF" : cfg_file }
+
+ def run_openssl(self, *cmd, **kwargs):
+ """
+ Run an OpenSSL command, suppresses stderr unless OpenSSL returns
+ failure, and returns stdout.
+ """
+ stdin = kwargs.pop("stdin", None)
+ env = self.env.copy()
+ env.update(kwargs)
+ cmd = (self.openssl,) + cmd
+ p = subprocess.Popen(cmd, env = env, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
+ stdout, stderr = p.communicate(stdin)
+ if p.wait() != 0:
+ sys.stderr.write("OpenSSL command failed: " + stderr + "\n")
+ raise subprocess.CalledProcessError(returncode = p.returncode, cmd = cmd)
+ return stdout
+
+ def run_ca(self, *args):
+ """
+ Run OpenSSL "ca" command with common initial arguments.
+ """
+ self.run_openssl("ca", "-batch", "-config", self.cfg, *args)
+
+ def run_req(self, key_file, req_file, log_key = sys.stdout):
+ """
+ Run OpenSSL "genrsa" and "req" commands.
+ """
+ if not os.path.exists(key_file):
+ if log_key:
+ log_key.write("Generating 2048-bit RSA key %s\n" % os.path.realpath(key_file))
+ self.run_openssl("genrsa", "-out", key_file, "2048")
+ if not os.path.exists(req_file):
+ self.run_openssl("req", "-new", "-sha256", "-config", self.cfg, "-key", key_file, "-out", req_file)
+
+ def run_dgst(self, input, algorithm = "md5"):
+ """
+ Run OpenSSL "dgst" command, return cleaned-up result.
+ """
+ hash = self.run_openssl("dgst", "-" + algorithm, stdin = input)
+ #
+ # Twits just couldn't leave well enough alone, grr.
+ hash = "".join(hash.split())
+ if hash.startswith("(stdin)="):
+ hash = hash[len("(stdin)="):]
+ return hash
+
+ @staticmethod
+ def touch_file(filename, content = None):
+ """
+ Create dumb little text files expected by OpenSSL "ca" utility.
+ """
+ if not os.path.exists(filename):
+ f = open(filename, "w")
+ if content is not None:
+ f.write(content)
+ f.close()
+
+ def setup(self, ca_name):
+ """
+ Set up this CA. ca_name is an X.509 distinguished name in
+ /tag=val/tag=val format.
+ """
+
+ modified = False
+
+ if not os.path.exists(self.dir):
+ os.makedirs(self.dir)
+ self.touch_file(self.index)
+ self.touch_file(self.serial, "01\n")
+ self.touch_file(self.crlnum, "01\n")
+
+ self.run_req(key_file = self.key, req_file = self.req)
+
+ if not os.path.exists(self.cer):
+ modified = True
+ self.run_ca("-selfsign", "-extensions", "ca_x509_ext_ca", "-subj", ca_name, "-in", self.req, "-out", self.cer)
+
+ if not os.path.exists(self.crl):
+ modified = True
+ self.run_ca("-gencrl", "-out", self.crl)
+
+ return modified
+
+ def ee(self, ee_name, base_name):
+ """
+ Issue an end-enity certificate.
+ """
+ key_file = "%s/%s.key" % (self.dir, base_name)
+ req_file = "%s/%s.req" % (self.dir, base_name)
+ cer_file = "%s/%s.cer" % (self.dir, base_name)
+ self.run_req(key_file = key_file, req_file = req_file)
+ if not os.path.exists(cer_file):
+ self.run_ca("-extensions", "ca_x509_ext_ee", "-subj", ee_name, "-in", req_file, "-out", cer_file)
+ return True
+ else:
+ return False
+
+ def cms_xml_sign(self, ee_name, base_name, elt):
+ """
+ Sign an XML object with CMS, return Base64 text.
+ """
+ self.ee(ee_name, base_name)
+ return base64.b64encode(self.run_openssl(
+ "cms", "-sign", "-binary", "-outform", "DER",
+ "-keyid", "-md", "sha256", "-nodetach", "-nosmimecap",
+ "-econtent_type", ".".join(str(i) for i in rpki.oids.name2oid["id-ct-xml"]),
+ "-inkey", "%s/%s.key" % (self.dir, base_name),
+ "-signer", "%s/%s.cer" % (self.dir, base_name),
+ stdin = ElementToString(etree_pre_write(elt))))
+
+ def cms_xml_verify(self, b64, ca):
+ """
+ Attempt to verify and extract XML from a Base64-encoded signed CMS
+ object. CA is the filename of a certificate that we expect to be
+ the issuer of the EE certificate bundled with the CMS, and must
+ previously have been cross-certified under our trust anchor.
+ """
+ # In theory, we should be able to use the -certfile parameter to
+ # pass in the CA certificate, but in practice, I have never gotten
+ # this to work, either with the command line tool or in the
+ # OpenSSL C API. Dunno why. Passing both TA and CA via -CAfile
+ # does work, so we do that, using a temporary file, sigh.
+ CAfile = os.path.join(self.dir, "temp.%s.pem" % os.getpid())
+ try:
+ f = open(CAfile, "w")
+ f.write(open(self.cer).read())
+ f.write(open(ca).read())
+ f.close()
+ return etree_post_read(ElementFromString(self.run_openssl(
+ "cms", "-verify", "-inform", "DER", "-CAfile", CAfile,
+ stdin = base64.b64decode(b64))))
+ finally:
+ if os.path.exists(CAfile):
+ os.unlink(CAfile)
+
+ def bsc(self, pkcs10):
+ """
+ Issue BSC certificiate, if we have a PKCS #10 request for it.
+ """
+
+ if pkcs10 is None:
+ return None, None
+
+ pkcs10 = base64.b64decode(pkcs10)
+
+ hash = self.run_dgst(pkcs10)
+
+ req_file = "%s/bsc.%s.req" % (self.dir, hash)
+ cer_file = "%s/bsc.%s.cer" % (self.dir, hash)
+
+ if not os.path.exists(cer_file):
+ self.run_openssl("req", "-inform", "DER", "-out", req_file, stdin = pkcs10)
+ self.run_ca("-extensions", "ca_x509_ext_ee", "-in", req_file, "-out", cer_file)
+
+ return req_file, cer_file
+
+ def fxcert(self, b64, filename = None, path_restriction = 0):
+ """
+ Write PEM certificate to file, then cross-certify.
+ """
+ fn = os.path.join(self.dir, filename or "temp.%s.cer" % os.getpid())
+ try:
+ self.run_openssl("x509", "-inform", "DER", "-out", fn,
+ stdin = base64.b64decode(b64))
+ return self.xcert(fn, path_restriction)
+ finally:
+ if not filename and os.path.exists(fn):
+ os.unlink(fn)
+ pass
+
+ def xcert(self, cert, path_restriction = 0):
+ """
+ Cross-certify a certificate represented as a PEM file.
+ """
+
+ if not cert or not os.path.exists(cert):
+ return None
+
+ # Extract public key and subject name from PEM file and hash it so
+ # we can use the result as a tag for cross-certifying this cert.
+
+ hash = self.run_dgst(self.run_openssl(
+ "x509", "-noout", "-pubkey", "-subject", "-in", cert))
+
+ # Cross-certify the cert we were given, if we haven't already.
+ # This only works for self-signed certs, due to limitations of the
+ # OpenSSL command line tool, but that suffices for our purposes.
+
+ xcert = "%s/xcert.%s.cer" % (self.dir, hash.strip())
+ if not os.path.exists(xcert):
+ self.run_ca("-ss_cert", cert, "-out", xcert, "-extensions", self.path_restriction[path_restriction])
+ return xcert
+
+def etree_validate(e):
+ # This is a kludge, schema should be loaded as module or configured
+ # in .conf, but it will do as a temporary debugging hack.
+ schema = os.getenv("MYRPKI_RNG")
+ if schema:
+ try:
+ import lxml.etree
+ except ImportError:
+ return
+ try:
+ lxml.etree.RelaxNG(file = schema).assertValid(e)
+ except lxml.etree.RelaxNGParseError:
+ return
+ except lxml.etree.DocumentInvalid:
+ print lxml.etree.tostring(e, pretty_print = True)
+ raise
+
+def etree_write(e, filename, verbose = False, validate = True, msg = None):
+ """
+ Write out an etree to a file, safely.
+
+ I still miss SYSCAL(RENMWO).
+ """
+ filename = os.path.realpath(filename)
+ tempname = filename
+ if not filename.startswith("/dev/"):
+ tempname += ".tmp"
+ if verbose or msg:
+ print "Writing", filename
+ if msg:
+ print msg
+ e = etree_pre_write(e, validate)
+ ElementTree(e).write(tempname)
+ if tempname != filename:
+ os.rename(tempname, filename)
+
+def etree_pre_write(e, validate = True):
+ """
+ Do the namespace frobbing needed on write; broken out of
+ etree_write() because also needed with ElementToString().
+ """
+ e = copy.deepcopy(e)
+ e.set("version", version)
+ for i in e.getiterator():
+ if i.tag[0] != "{":
+ i.tag = namespaceQName + i.tag
+ assert i.tag.startswith(namespaceQName)
+ if validate:
+ etree_validate(e)
+ return e
+
+def etree_read(filename, verbose = False, validate = True):
+ """
+ Read an etree from a file, verifying then stripping XML namespace
+ cruft.
+ """
+ if verbose:
+ print "Reading", filename
+ e = ElementTree(file = filename).getroot()
+ return etree_post_read(e, validate)
+
+def etree_post_read(e, validate = True):
+ """
+ Do the namespace frobbing needed on read; broken out of etree_read()
+ beause also needed by ElementFromString().
+ """
+ if validate:
+ etree_validate(e)
+ for i in e.getiterator():
+ if i.tag.startswith(namespaceQName):
+ i.tag = i.tag[len(namespaceQName):]
+ else:
+ raise RuntimeError, "XML tag %r is not in namespace %r" % (i.tag, namespace)
+ return e
+
+def b64_equal(thing1, thing2):
+ """
+ Compare two Base64-encoded values for equality.
+ """
+ return "".join(thing1.split()) == "".join(thing2.split())
+
+
+
+class main(rpki.cli.Cmd):
+
+ prompt = "myrpki> "
+
+ completedefault = rpki.cli.Cmd.filename_complete
+
+ show_xml = False
+
+ def __init__(self):
+ os.environ["TZ"] = "UTC"
+ time.tzset()
+
+ rpki.log.use_syslog = False
+
+ self.cfg_file = os.getenv("MYRPKI_CONF", "myrpki.conf")
+
+ opts, argv = getopt.getopt(sys.argv[1:], "c:h?", ["config=", "help"])
+ for o, a in opts:
+ if o in ("-c", "--config"):
+ self.cfg_file = a
+ elif o in ("-h", "--help", "-?"):
+ argv = ["help"]
+
+ if not argv or argv[0] != "help":
+ rpki.log.init("myrpki")
+ self.read_config()
+
+ rpki.cli.Cmd.__init__(self, argv)
+
+
+ def help_overview(self):
+ """
+ Show program __doc__ string. Perhaps there's some clever way to
+ do this using the textwrap module, but for now something simple
+ and crude will suffice.
+ """
+ for line in __doc__.splitlines(True):
+ self.stdout.write(" " * 4 + line)
+ self.stdout.write("\n")
+
+ def read_config(self):
+
+ self.cfg = rpki.config.parser(self.cfg_file, "myrpki")
+
+ self.histfile = self.cfg.get("history_file", ".myrpki_history")
+ self.handle = self.cfg.get("handle")
+ self.run_rpkid = self.cfg.getboolean("run_rpkid")
+ self.run_pubd = self.cfg.getboolean("run_pubd")
+ self.run_rootd = self.cfg.getboolean("run_rootd")
+ self.entitydb = EntityDB(self.cfg)
+
+ if self.run_rootd and (not self.run_pubd or not self.run_rpkid):
+ raise RuntimeError, "Can't run rootd unless also running rpkid and pubd"
+
+ self.bpki_resources = CA(self.cfg_file, self.cfg.get("bpki_resources_directory"))
+ if self.run_rpkid or self.run_pubd or self.run_rootd:
+ self.bpki_servers = CA(self.cfg_file, self.cfg.get("bpki_servers_directory"))
+
+ self.pubd_contact_info = self.cfg.get("pubd_contact_info", "")
+
+ self.rsync_module = self.cfg.get("publication_rsync_module")
+ self.rsync_server = self.cfg.get("publication_rsync_server")
+
+
+ def do_initialize(self, arg):
+ """
+ Initialize an RPKI installation. This command reads the
+ configuration file, creates the BPKI and EntityDB directories,
+ generates the initial BPKI certificates, and creates an XML file
+ describing the resource-holding aspect of this RPKI installation.
+ """
+
+ if arg:
+ raise RuntimeError, "This command takes no arguments"
+
+ self.bpki_resources.setup(self.cfg.get("bpki_resources_ta_dn",
+ "/CN=%s BPKI Resource Trust Anchor" % self.handle))
+ if self.run_rpkid or self.run_pubd or self.run_rootd:
+ self.bpki_servers.setup(self.cfg.get("bpki_servers_ta_dn",
+ "/CN=%s BPKI Server Trust Anchor" % self.handle))
+
+ # Create entitydb directories.
+
+ for i in ("parents", "children", "repositories", "pubclients"):
+ d = self.entitydb(i)
+ if not os.path.exists(d):
+ os.makedirs(d)
+
+ if self.run_rpkid or self.run_pubd or self.run_rootd:
+
+ if self.run_rpkid:
+ self.bpki_servers.ee(self.cfg.get("bpki_rpkid_ee_dn",
+ "/CN=%s rpkid server certificate" % self.handle), "rpkid")
+ self.bpki_servers.ee(self.cfg.get("bpki_irdbd_ee_dn",
+ "/CN=%s irdbd server certificate" % self.handle), "irdbd")
+ if self.run_pubd:
+ self.bpki_servers.ee(self.cfg.get("bpki_pubd_ee_dn",
+ "/CN=%s pubd server certificate" % self.handle), "pubd")
+ if self.run_rpkid or self.run_pubd:
+ self.bpki_servers.ee(self.cfg.get("bpki_irbe_ee_dn",
+ "/CN=%s irbe client certificate" % self.handle), "irbe")
+ if self.run_rootd:
+ self.bpki_servers.ee(self.cfg.get("bpki_rootd_ee_dn",
+ "/CN=%s rootd server certificate" % self.handle), "rootd")
+
+ # Build the identity.xml file. Need to check for existing file so we don't
+ # overwrite? Worry about that later.
+
+ e = Element("identity", handle = self.handle)
+ PEMElement(e, "bpki_ta", self.bpki_resources.cer)
+ etree_write(e, self.entitydb("identity.xml"),
+ msg = None if self.run_rootd else 'This is the "identity" file you will need to send to your parent')
+
+ # If we're running rootd, construct a fake parent to go with it,
+ # and cross-certify in both directions so we can talk to rootd.
+
+ if self.run_rootd:
+
+ e = Element("parent", parent_handle = self.handle, child_handle = self.handle,
+ service_uri = "https://localhost:%s/" % self.cfg.get("rootd_server_port"),
+ valid_until = str(rpki.sundial.now() + rpki.sundial.timedelta(days = 365)))
+ PEMElement(e, "bpki_resource_ta", self.bpki_servers.cer)
+ PEMElement(e, "bpki_server_ta", self.bpki_servers.cer)
+ PEMElement(e, "bpki_child_ta", self.bpki_resources.cer)
+ SubElement(e, "repository", type = "offer")
+ etree_write(e, self.entitydb("parents", "%s.xml" % self.handle))
+
+ self.bpki_resources.xcert(self.bpki_servers.cer)
+
+ rootd_child_fn = self.cfg.get("child-bpki-cert", None, "rootd")
+ if not os.path.exists(rootd_child_fn):
+ os.link(self.bpki_servers.xcert(self.bpki_resources.cer), rootd_child_fn)
+
+ repo_file_name = self.entitydb("repositories", "%s.xml" % self.handle)
+
+ try:
+ want_offer = etree_read(repo_file_name).get("type") != "confirmed"
+ except IOError:
+ want_offer = True
+
+ if want_offer:
+ e = Element("repository", type = "offer", handle = self.handle, parent_handle = self.handle)
+ PEMElement(e, "bpki_client_ta", self.bpki_resources.cer)
+ etree_write(e, repo_file_name,
+ msg = 'This is the "repository offer" file for you to use if you want to publish in your own repository')
+
+ def do_configure_child(self, arg):
+ """
+ Configure a new child of this RPKI entity, given the child's XML
+ identity file as an input. This command extracts the child's data
+ from the XML, cross-certifies the child's resource-holding BPKI
+ certificate, and generates an XML file describing the relationship
+ between the child and this parent, including this parent's BPKI
+ data and up-down protocol service URI.
+ """
+
+ child_handle = None
+
+ opts, argv = getopt.getopt(arg.split(), "", ["child_handle="])
+ for o, a in opts:
+ if o == "--child_handle":
+ child_handle = a
+
+ if len(argv) != 1:
+ raise RuntimeError, "Need to specify filename for child.xml"
+
+ c = etree_read(argv[0])
+
+ if child_handle is None:
+ child_handle = c.get("handle")
+
+ try:
+ e = etree_read(self.cfg.get("xml_filename"))
+ service_uri_base = e.get("service_uri")
+ except IOError:
+ service_uri_base = None
+
+ if not service_uri_base and self.run_rpkid:
+ service_uri_base = "https://%s:%s/up-down/%s" % (self.cfg.get("rpkid_server_host"),
+ self.cfg.get("rpkid_server_port"),
+ self.handle)
+ if not service_uri_base:
+ print "Sorry, you can't set up children of a hosted config that itself has not yet been set up"
+ return
+
+ print "Child calls itself %r, we call it %r" % (c.get("handle"), child_handle)
+
+ self.bpki_servers.fxcert(c.findtext("bpki_ta"))
+
+ e = Element("parent", parent_handle = self.handle, child_handle = child_handle,
+ service_uri = "%s/%s" % (service_uri_base, child_handle),
+ valid_until = str(rpki.sundial.now() + rpki.sundial.timedelta(days = 365)))
+
+ PEMElement(e, "bpki_resource_ta", self.bpki_resources.cer)
+ PEMElement(e, "bpki_server_ta", self.bpki_servers.cer)
+ SubElement(e, "bpki_child_ta").text = c.findtext("bpki_ta")
+
+ try:
+ repo = None
+ for f in self.entitydb.iterate("repositories", "*.xml"):
+ r = etree_read(f)
+ if r.get("type") == "confirmed":
+ if repo is not None:
+ raise RuntimeError, "Too many repositories, I don't know what to do, not giving referral"
+ repo_handle = os.path.splitext(os.path.split(f)[-1])[0]
+ repo = r
+ if repo is None:
+ raise RuntimeError, "Couldn't find any usable repositories, not giving referral"
+
+ if repo_handle == self.handle:
+ SubElement(e, "repository", type = "offer")
+ else:
+ proposed_sia_base = repo.get("sia_base") + child_handle + "/"
+ r = Element("referral", authorized_sia_base = proposed_sia_base)
+ r.text = c.findtext("bpki_ta")
+ auth = self.bpki_resources.cms_xml_sign(
+ "/CN=%s Publication Referral" % self.handle, "referral", r)
+ r = SubElement(e, "repository", type = "referral")
+ SubElement(r, "authorization", referrer = repo.get("client_handle")).text = auth
+ SubElement(r, "contact_info").text = repo.findtext("contact_info")
+
+ except RuntimeError, err:
+ print err
+
+ etree_write(e, self.entitydb("children", "%s.xml" % child_handle),
+ msg = "Send this file back to the child you just configured")
+
+
+ def do_configure_parent(self, arg):
+ """
+ Configure a new parent of this RPKI entity, given the output of
+ the parent's configure_child command as input. This command reads
+ the parent's response XML, extracts the parent's BPKI and service
+ URI information, cross-certifies the parent's BPKI data into this
+ entity's BPKI, and checks for offers or referrals of publication
+ service. If a publication offer or referral is present, we
+ generate a request-for-service message to that repository, in case
+ the user wants to avail herself of the referral or offer.
+ """
+
+ parent_handle = None
+
+ opts, argv = getopt.getopt(arg.split(), "", ["parent_handle="])
+ for o, a in opts:
+ if o == "--parent_handle":
+ parent_handle = a
+
+ if len(argv) != 1:
+ raise RuntimeError, "Need to specify filename for parent.xml on command line"
+
+ p = etree_read(argv[0])
+
+ if parent_handle is None:
+ parent_handle = p.get("parent_handle")
+
+ print "Parent calls itself %r, we call it %r" % (p.get("parent_handle"), parent_handle)
+ print "Parent calls us %r" % p.get("child_handle")
+
+ self.bpki_resources.fxcert(p.findtext("bpki_resource_ta"))
+ self.bpki_resources.fxcert(p.findtext("bpki_server_ta"))
+
+ etree_write(p, self.entitydb("parents", "%s.xml" % parent_handle))
+
+ r = p.find("repository")
+
+ if r is not None and r.get("type") in ("offer", "referral"):
+ r.set("handle", self.handle)
+ r.set("parent_handle", parent_handle)
+ PEMElement(r, "bpki_client_ta", self.bpki_resources.cer)
+ etree_write(r, self.entitydb("repositories", "%s.xml" % parent_handle),
+ msg = 'This is the "repository %s" file to send to the repository operator' % r.get("type"))
+ else:
+ print "Couldn't find repository offer or referral"
+
+
+ def do_configure_publication_client(self, arg):
+ """
+ Configure publication server to know about a new client, given the
+ client's request-for-service message as input. This command reads
+ the client's request for service, cross-certifies the client's
+ BPKI data, and generates a response message containing the
+ repository's BPKI data and service URI.
+ """
+
+ sia_base = None
+
+ opts, argv = getopt.getopt(arg.split(), "", ["sia_base="])
+ for o, a in opts:
+ if o == "--sia_base":
+ sia_base = a
+
+ if len(argv) != 1:
+ raise RuntimeError, "Need to specify filename for client.xml"
+
+ client = etree_read(argv[0])
+
+ if sia_base is None:
+
+ auth = client.find("authorization")
+ if auth is not None:
+ print "Found <authorization/> element, this looks like a referral"
+ referrer = etree_read(self.entitydb("pubclients", "%s.xml" % auth.get("referrer").replace("/",".")))
+ referrer = self.bpki_servers.fxcert(referrer.findtext("bpki_client_ta"))
+ referral = self.bpki_servers.cms_xml_verify(auth.text, referrer)
+ if not b64_equal(referral.text, client.findtext("bpki_client_ta")):
+ raise RuntimeError, "Referral trust anchor does not match"
+ sia_base = referral.get("authorized_sia_base")
+
+ elif client.get("parent_handle") == self.handle:
+ print "Client claims to be our child, checking"
+ client_ta = client.findtext("bpki_client_ta")
+ assert client_ta
+ for child in self.entitydb.iterate("children", "*.xml"):
+ c = etree_read(child)
+ if b64_equal(c.findtext("bpki_child_ta"), client_ta):
+ sia_base = "rsync://%s/%s/%s/%s/" % (self.rsync_server, self.rsync_module,
+ self.handle, client.get("handle"))
+ break
+
+ # If we still haven't figured out what to do with this client, it
+ # gets a top-level tree of its own, no attempt at nesting.
+
+ if sia_base is None:
+ print "Don't know where to nest this client, defaulting to top-level"
+ sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, client.get("handle"))
+
+ assert sia_base.startswith("rsync://")
+
+ client_handle = "/".join(sia_base.rstrip("/").split("/")[4:])
+
+ parent_handle = client.get("parent_handle")
+
+ print "Client calls itself %r, we call it %r" % (client.get("handle"), client_handle)
+ print "Client says its parent handle is %r" % parent_handle
+
+ self.bpki_servers.fxcert(client.findtext("bpki_client_ta"))
+
+ e = Element("repository", type = "confirmed",
+ client_handle = client_handle,
+ parent_handle = parent_handle,
+ sia_base = sia_base,
+ service_uri = "https://%s:%s/client/%s" % (self.cfg.get("pubd_server_host"),
+ self.cfg.get("pubd_server_port"),
+ client_handle))
+
+ PEMElement(e, "bpki_server_ta", self.bpki_servers.cer)
+ SubElement(e, "bpki_client_ta").text = client.findtext("bpki_client_ta")
+ SubElement(e, "contact_info").text = self.pubd_contact_info
+ etree_write(e, self.entitydb("pubclients", "%s.xml" % client_handle.replace("/", ".")),
+ msg = "Send this file back to the publication client you just configured")
+
+
+ def do_configure_repository(self, arg):
+ """
+ Configure a publication repository for this RPKI entity, given the
+ repository's response to our request-for-service message as input.
+ This command reads the repository's response, extracts and
+ cross-certifies the BPKI data and service URI, and links the
+ repository data with the corresponding parent data in our local
+ database.
+ """
+
+ argv = arg.split()
+
+ if len(argv) != 1:
+ raise RuntimeError, "Need to specify filename for repository.xml on command line"
+
+ r = etree_read(argv[0])
+
+ parent_handle = r.get("parent_handle")
+
+ print "Repository calls us %r" % (r.get("client_handle"))
+ print "Repository response associated with parent_handle %r" % parent_handle
+
+ etree_write(r, self.entitydb("repositories", "%s.xml" % parent_handle))
+
+
+
+
+ def configure_resources_main(self, msg = None):
+ """
+ Main program of old myrpki.py script. This remains separate
+ because it's called from more than one place.
+ """
+
+ roa_csv_file = self.cfg.get("roa_csv")
+ prefix_csv_file = self.cfg.get("prefix_csv")
+ asn_csv_file = self.cfg.get("asn_csv")
+
+ # This probably should become an argument instead of (or in
+ # addition to a default from?) a config file option.
+ xml_filename = self.cfg.get("xml_filename")
+
+ try:
+ e = etree_read(xml_filename)
+ bsc_req, bsc_cer = self.bpki_resources.bsc(e.findtext("bpki_bsc_pkcs10"))
+ service_uri = e.get("service_uri")
+ server_ta = e.findtext("bpki_server_ta")
+ except IOError:
+ bsc_req, bsc_cer = None, None
+ service_uri = None
+ server_ta = None
+
+ e = Element("myrpki", handle = self.handle)
+
+ if service_uri:
+ e.set("service_uri", service_uri)
+
+ roa_requests.from_csv(roa_csv_file).xml(e)
+
+ children.from_csv(
+ prefix_csv_file = prefix_csv_file,
+ asn_csv_file = asn_csv_file,
+ fxcert = self.bpki_resources.fxcert,
+ entitydb = self.entitydb).xml(e)
+
+ parents.from_csv( fxcert = self.bpki_resources.fxcert, entitydb = self.entitydb).xml(e)
+ repositories.from_csv(fxcert = self.bpki_resources.fxcert, entitydb = self.entitydb).xml(e)
+
+ PEMElement(e, "bpki_ca_certificate", self.bpki_resources.cer)
+ PEMElement(e, "bpki_crl", self.bpki_resources.crl)
+
+ if bsc_cer:
+ PEMElement(e, "bpki_bsc_certificate", bsc_cer)
+
+ if bsc_req:
+ PEMElement(e, "bpki_bsc_pkcs10", bsc_req)
+
+ if server_ta:
+ SubElement(e, "bpki_server_ta").text = server_ta
+
+ etree_write(e, xml_filename, msg = msg)
+
+
+ def do_configure_resources(self, arg):
+ """
+ Read CSV files and all the descriptions of parents and children
+ that we've built up, package the result up as a single XML file to
+ be shipped to a hosting rpkid.
+ """
+
+ if arg:
+ raise RuntimeError, "Unexpected argument %r" % arg
+ self.configure_resources_main(msg = "Send this file to the rpkid operator who is hosting you")
+
+
+
+ def do_configure_daemons(self, arg):
+ """
+ Configure RPKI daemons with the data built up by the other
+ commands in this program.
+
+ The basic model here is that each entity with resources to certify
+ runs the myrpki tool, but not all of them necessarily run their
+ own RPKI engines. The entities that do run RPKI engines get data
+ from the entities they host via the XML files output by the
+ configure_resources command. Those XML files are the input to
+ this command, which uses them to do all the work of configuring
+ daemons, populating SQL databases, and so forth. A few operations
+ (eg, BSC construction) generate data which has to be shipped back
+ to the resource holder, which we do by updating the same XML file.
+
+ In essence, the XML files are a sneakernet (or email, or carrier
+ pigeon) communication channel between the resource holders and the
+ RPKI engine operators.
+
+ As a convenience, for the normal case where the RPKI engine
+ operator is itself a resource holder, this command in effect runs
+ the configure_resources command automatically to process the RPKI
+ engine operator's own resources.
+
+ Note that, due to the back and forth nature of some of these
+ operations, it may take several cycles for data structures to stablize
+ and everything to reach a steady state. This is normal.
+ """
+
+ argv = arg.split()
+
+ try:
+ import rpki.https, rpki.resource_set, rpki.relaxng, rpki.exceptions
+ import rpki.left_right, rpki.x509, rpki.async, lxml.etree
+ if hasattr(warnings, "catch_warnings"):
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ import MySQLdb
+ else:
+ import MySQLdb
+
+ except ImportError, e:
+ print "Sorry, you appear to be missing some of the Python modules needed to run this command"
+ print "[Error: %r]" % e
+
+ def findbase64(tree, name, b64type = rpki.x509.X509):
+ x = tree.findtext(name)
+ return b64type(Base64 = x) if x else None
+
+ # We can use a single BSC for everything -- except BSC key
+ # rollovers. Drive off that bridge when we get to it.
+
+ bsc_handle = "bsc"
+
+ self.cfg.set_global_flags()
+
+ # Default values for CRL parameters are low, for testing. Not
+ # quite as low as they once were, too much expired CRL whining.
+
+ self_crl_interval = self.cfg.getint("self_crl_interval", 2 * 60 * 60)
+ self_regen_margin = self.cfg.getint("self_regen_margin", 30 * 60)
+ pubd_base = "https://%s:%s/" % (self.cfg.get("pubd_server_host"), self.cfg.get("pubd_server_port"))
+ rpkid_base = "https://%s:%s/" % (self.cfg.get("rpkid_server_host"), self.cfg.get("rpkid_server_port"))
+
+ # Wrappers to simplify calling rpkid and pubd.
+
+ call_rpkid = rpki.async.sync_wrapper(rpki.https.caller(
+ proto = rpki.left_right,
+ client_key = rpki.x509.RSA( PEM_file = self.bpki_servers.dir + "/irbe.key"),
+ client_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/irbe.cer"),
+ server_ta = rpki.x509.X509(PEM_file = self.bpki_servers.cer),
+ server_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/rpkid.cer"),
+ url = rpkid_base + "left-right",
+ debug = self.show_xml))
+
+ if self.run_pubd:
+
+ call_pubd = rpki.async.sync_wrapper(rpki.https.caller(
+ proto = rpki.publication,
+ client_key = rpki.x509.RSA( PEM_file = self.bpki_servers.dir + "/irbe.key"),
+ client_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/irbe.cer"),
+ server_ta = rpki.x509.X509(PEM_file = self.bpki_servers.cer),
+ server_cert = rpki.x509.X509(PEM_file = self.bpki_servers.dir + "/pubd.cer"),
+ url = pubd_base + "control",
+ debug = self.show_xml))
+
+ # Make sure that pubd's BPKI CRL is up to date.
+
+ call_pubd(rpki.publication.config_elt.make_pdu(
+ action = "set",
+ bpki_crl = rpki.x509.CRL(PEM_file = self.bpki_servers.crl)))
+
+ irdbd_cfg = rpki.config.parser(self.cfg.get("irdbd_conf", self.cfg_file), "irdbd")
+
+ db = MySQLdb.connect(user = irdbd_cfg.get("sql-username"),
+ db = irdbd_cfg.get("sql-database"),
+ passwd = irdbd_cfg.get("sql-password"))
+
+ cur = db.cursor()
+
+ xmlfiles = []
+
+ # If [myrpki] section includes an "xml_filename" setting, run
+ # myrpki.py internally, as a convenience, and include its output at
+ # the head of our list of XML files to process.
+
+ my_xmlfile = self.cfg.get("xml_filename", "")
+ if my_xmlfile:
+ self.configure_resources_main()
+ xmlfiles.append(my_xmlfile)
+ else:
+ my_xmlfile = None
+
+ # Add any other XML files specified on the command line
+
+ xmlfiles.extend(argv)
+
+ my_handle = None
+
+ for xmlfile in xmlfiles:
+
+ # Parse XML file and validate it against our scheme
+
+ tree = etree_read(xmlfile, validate = True)
+
+ handle = tree.get("handle")
+
+ if xmlfile == my_xmlfile:
+ my_handle = handle
+
+ # Update IRDB with parsed resource and roa-request data.
+
+ cur.execute(
+ """
+ DELETE
+ FROM roa_request_prefix
+ USING roa_request, roa_request_prefix
+ WHERE roa_request.roa_request_id = roa_request_prefix.roa_request_id AND roa_request.roa_request_handle = %s
+ """, (handle,))
+
+ cur.execute("DELETE FROM roa_request WHERE roa_request.roa_request_handle = %s", (handle,))
+
+ for x in tree.getiterator("roa_request"):
+ cur.execute("INSERT roa_request (roa_request_handle, asn) VALUES (%s, %s)", (handle, x.get("asn")))
+ roa_request_id = cur.lastrowid
+ 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")))):
+ if prefix_set:
+ cur.executemany("INSERT roa_request_prefix (roa_request_id, prefix, prefixlen, max_prefixlen, version) VALUES (%s, %s, %s, %s, %s)",
+ ((roa_request_id, p.prefix, p.prefixlen, p.max_prefixlen, version) for p in prefix_set))
+
+ cur.execute(
+ """
+ DELETE
+ FROM registrant_asn
+ USING registrant, registrant_asn
+ WHERE registrant.registrant_id = registrant_asn.registrant_id AND registrant.registry_handle = %s
+ """ , (handle,))
+
+ cur.execute(
+ """
+ DELETE FROM registrant_net USING registrant, registrant_net
+ WHERE registrant.registrant_id = registrant_net.registrant_id AND registrant.registry_handle = %s
+ """ , (handle,))
+
+ cur.execute("DELETE FROM registrant WHERE registrant.registry_handle = %s" , (handle,))
+
+ for x in tree.getiterator("child"):
+ child_handle = x.get("handle")
+ asns = rpki.resource_set.resource_set_as(x.get("asns"))
+ ipv4 = rpki.resource_set.resource_set_ipv4(x.get("v4"))
+ ipv6 = rpki.resource_set.resource_set_ipv6(x.get("v6"))
+
+ cur.execute("INSERT registrant (registrant_handle, registry_handle, registrant_name, valid_until) VALUES (%s, %s, %s, %s)",
+ (child_handle, handle, child_handle, rpki.sundial.datetime.fromXMLtime(x.get("valid_until")).to_sql()))
+ child_id = cur.lastrowid
+ if asns:
+ cur.executemany("INSERT registrant_asn (start_as, end_as, registrant_id) VALUES (%s, %s, %s)",
+ ((a.min, a.max, child_id) for a in asns))
+ if ipv4:
+ cur.executemany("INSERT registrant_net (start_ip, end_ip, version, registrant_id) VALUES (%s, %s, 4, %s)",
+ ((a.min, a.max, child_id) for a in ipv4))
+ if ipv6:
+ cur.executemany("INSERT registrant_net (start_ip, end_ip, version, registrant_id) VALUES (%s, %s, 6, %s)",
+ ((a.min, a.max, child_id) for a in ipv6))
+
+ db.commit()
+
+ # Check for certificates before attempting anything else
+
+ hosted_cacert = findbase64(tree, "bpki_ca_certificate")
+ if not hosted_cacert:
+ print "Nothing else I can do without a trust anchor for the entity I'm hosting."
+ continue
+
+ rpkid_xcert = rpki.x509.X509(PEM_file = self.bpki_servers.fxcert(b64 = hosted_cacert.get_Base64(),
+ filename = handle + ".cacert.cer",
+ path_restriction = 1))
+
+ # See what rpkid and pubd already have on file for this entity.
+
+ if self.run_pubd:
+ client_pdus = dict((x.client_handle, x)
+ for x in call_pubd(rpki.publication.client_elt.make_pdu(action = "list"))
+ if isinstance(x, rpki.publication.client_elt))
+
+ rpkid_reply = call_rpkid(
+ rpki.left_right.self_elt.make_pdu( action = "get", tag = "self", self_handle = handle),
+ rpki.left_right.bsc_elt.make_pdu( action = "list", tag = "bsc", self_handle = handle),
+ rpki.left_right.repository_elt.make_pdu(action = "list", tag = "repository", self_handle = handle),
+ rpki.left_right.parent_elt.make_pdu( action = "list", tag = "parent", self_handle = handle),
+ rpki.left_right.child_elt.make_pdu( action = "list", tag = "child", self_handle = handle))
+
+ self_pdu = rpkid_reply[0]
+ bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt))
+ repository_pdus = dict((x.repository_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.repository_elt))
+ parent_pdus = dict((x.parent_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.parent_elt))
+ child_pdus = dict((x.child_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.child_elt))
+
+ pubd_query = []
+ rpkid_query = []
+
+ # There should be exactly one <self/> object per hosted entity, by definition
+
+ if (isinstance(self_pdu, rpki.left_right.report_error_elt) or
+ self_pdu.crl_interval != self_crl_interval or
+ self_pdu.regen_margin != self_regen_margin or
+ self_pdu.bpki_cert != rpkid_xcert):
+ rpkid_query.append(rpki.left_right.self_elt.make_pdu(
+ action = "create" if isinstance(self_pdu, rpki.left_right.report_error_elt) else "set",
+ tag = "self",
+ self_handle = handle,
+ bpki_cert = rpkid_xcert,
+ crl_interval = self_crl_interval,
+ regen_margin = self_regen_margin))
+
+ # In general we only need one <bsc/> per <self/>. BSC objects are a
+ # little unusual in that the PKCS #10 subelement is generated by rpkid
+ # in response to generate_keypair, so there's more of a separation
+ # between create and set than with other objects.
+
+ bsc_cert = findbase64(tree, "bpki_bsc_certificate")
+ bsc_crl = findbase64(tree, "bpki_crl", rpki.x509.CRL)
+
+ bsc_pdu = bsc_pdus.pop(bsc_handle, None)
+
+ if bsc_pdu is None:
+ rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
+ action = "create",
+ tag = "bsc",
+ self_handle = handle,
+ bsc_handle = bsc_handle,
+ generate_keypair = "yes"))
+ elif bsc_pdu.signing_cert != bsc_cert or bsc_pdu.signing_cert_crl != bsc_crl:
+ rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
+ action = "set",
+ tag = "bsc",
+ self_handle = handle,
+ bsc_handle = bsc_handle,
+ signing_cert = bsc_cert,
+ signing_cert_crl = bsc_crl))
+
+ rpkid_query.extend(rpki.left_right.bsc_elt.make_pdu(
+ action = "destroy", self_handle = handle, bsc_handle = b) for b in bsc_pdus)
+
+ bsc_req = None
+
+ if bsc_pdu and bsc_pdu.pkcs10_request:
+ bsc_req = bsc_pdu.pkcs10_request
+
+ # At present we need one <repository/> per <parent/>, not because
+ # rpkid requires that, but because pubd does. pubd probably should
+ # be fixed to support a single client allowed to update multiple
+ # trees, but for the moment the easiest way forward is just to
+ # enforce a 1:1 mapping between <parent/> and <repository/> objects
+
+ for repository in tree.getiterator("repository"):
+
+ repository_handle = repository.get("handle")
+ repository_pdu = repository_pdus.pop(repository_handle, None)
+ repository_uri = repository.get("service_uri")
+ repository_cert = findbase64(repository, "bpki_certificate")
+
+ if (repository_pdu is None or
+ repository_pdu.bsc_handle != bsc_handle or
+ repository_pdu.peer_contact_uri != repository_uri or
+ repository_pdu.bpki_cert != repository_cert):
+ rpkid_query.append(rpki.left_right.repository_elt.make_pdu(
+ action = "create" if repository_pdu is None else "set",
+ tag = repository_handle,
+ self_handle = handle,
+ repository_handle = repository_handle,
+ bsc_handle = bsc_handle,
+ peer_contact_uri = repository_uri,
+ bpki_cert = repository_cert))
+
+ rpkid_query.extend(rpki.left_right.repository_elt.make_pdu(
+ action = "destroy", self_handle = handle, repository_handle = r) for r in repository_pdus)
+
+ # <parent/> setup code currently assumes 1:1 mapping between
+ # <repository/> and <parent/>, and further assumes that the handles
+ # for an associated pair are the identical (that is:
+ # parent.repository_handle == parent.parent_handle).
+
+ for parent in tree.getiterator("parent"):
+
+ parent_handle = parent.get("handle")
+ parent_pdu = parent_pdus.pop(parent_handle, None)
+ parent_uri = parent.get("service_uri")
+ parent_myhandle = parent.get("myhandle")
+ parent_sia_base = parent.get("sia_base")
+ parent_cms_cert = findbase64(parent, "bpki_cms_certificate")
+ parent_https_cert = findbase64(parent, "bpki_https_certificate")
+
+ if (parent_pdu is None or
+ parent_pdu.bsc_handle != bsc_handle or
+ parent_pdu.repository_handle != parent_handle or
+ parent_pdu.peer_contact_uri != parent_uri or
+ parent_pdu.sia_base != parent_sia_base or
+ parent_pdu.sender_name != parent_myhandle or
+ parent_pdu.recipient_name != parent_handle or
+ parent_pdu.bpki_cms_cert != parent_cms_cert or
+ parent_pdu.bpki_https_cert != parent_https_cert):
+ rpkid_query.append(rpki.left_right.parent_elt.make_pdu(
+ action = "create" if parent_pdu is None else "set",
+ tag = parent_handle,
+ self_handle = handle,
+ parent_handle = parent_handle,
+ bsc_handle = bsc_handle,
+ repository_handle = parent_handle,
+ peer_contact_uri = parent_uri,
+ sia_base = parent_sia_base,
+ sender_name = parent_myhandle,
+ recipient_name = parent_handle,
+ bpki_cms_cert = parent_cms_cert,
+ bpki_https_cert = parent_https_cert))
+
+ rpkid_query.extend(rpki.left_right.parent_elt.make_pdu(
+ action = "destroy", self_handle = handle, parent_handle = p) for p in parent_pdus)
+
+ # Children are simpler than parents, because they call us, so no URL
+ # to construct and figuring out what certificate to use is their
+ # problem, not ours.
+
+ for child in tree.getiterator("child"):
+
+ child_handle = child.get("handle")
+ child_pdu = child_pdus.pop(child_handle, None)
+ child_cert = findbase64(child, "bpki_certificate")
+
+ if (child_pdu is None or
+ child_pdu.bsc_handle != bsc_handle or
+ child_pdu.bpki_cert != child_cert):
+ rpkid_query.append(rpki.left_right.child_elt.make_pdu(
+ action = "create" if child_pdu is None else "set",
+ tag = child_handle,
+ self_handle = handle,
+ child_handle = child_handle,
+ bsc_handle = bsc_handle,
+ bpki_cert = child_cert))
+
+ rpkid_query.extend(rpki.left_right.child_elt.make_pdu(
+ action = "destroy", self_handle = handle, child_handle = c) for c in child_pdus)
+
+ # Publication setup.
+
+ if self.run_pubd:
+
+ for f in self.entitydb.iterate("pubclients", "*.xml"):
+ c = etree_read(f)
+
+ client_handle = c.get("client_handle")
+ client_base_uri = c.get("sia_base")
+ client_bpki_cert = rpki.x509.X509(PEM_file = self.bpki_servers.fxcert(c.findtext("bpki_client_ta")))
+ client_pdu = client_pdus.pop(client_handle, None)
+
+ if (client_pdu is None or
+ client_pdu.base_uri != client_base_uri or
+ client_pdu.bpki_cert != client_bpki_cert):
+ pubd_query.append(rpki.publication.client_elt.make_pdu(
+ action = "create" if client_pdu is None else "set",
+ client_handle = client_handle,
+ bpki_cert = client_bpki_cert,
+ base_uri = client_base_uri))
+
+ pubd_query.extend(rpki.publication.client_elt.make_pdu(
+ action = "destroy", client_handle = p) for p in client_pdus)
+
+ # If we changed anything, ship updates off to daemons
+
+ failed = False
+
+ if rpkid_query:
+ rpkid_reply = call_rpkid(*rpkid_query)
+ bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt))
+ if bsc_handle in bsc_pdus and bsc_pdus[bsc_handle].pkcs10_request:
+ bsc_req = bsc_pdus[bsc_handle].pkcs10_request
+ for r in rpkid_reply:
+ if isinstance(r, rpki.left_right.report_error_elt):
+ failed = True
+ print "rpkid reported failure:", r.error_code
+ if r.error_text:
+ print r.error_text
+
+ if failed:
+ raise RuntimeError
+
+ if pubd_query:
+ assert self.run_pubd
+ pubd_reply = call_pubd(*pubd_query)
+ for r in pubd_reply:
+ if isinstance(r, rpki.publication.report_error_elt):
+ failed = True
+ print "pubd reported failure:", r.error_code
+ if r.error_text:
+ print r.error_text
+
+ if failed:
+ raise RuntimeError
+
+ # Rewrite XML.
+
+ e = tree.find("bpki_bsc_pkcs10")
+ if e is not None:
+ tree.remove(e)
+ if bsc_req is not None:
+ SubElement(tree, "bpki_bsc_pkcs10").text = bsc_req.get_Base64()
+
+ tree.set("service_uri", rpkid_base + "up-down/" + handle)
+
+ e = tree.find("bpki_server_ta")
+ if e is not None:
+ tree.remove(e)
+ PEMElement(tree, "bpki_server_ta", self.bpki_resources.cer)
+
+ etree_write(tree, xmlfile, validate = True,
+ msg = None if xmlfile is my_xmlfile else 'Send this file back to the hosted entity ("%s")' % handle)
+
+ db.close()
+
+ # Run event loop again to give TLS connections a chance to shut down cleanly.
+ # Might need to add a timeout here, dunno yet.
+
+ rpki.async.event_loop()
+
+
+
+if __name__ == "__main__":
+ main()