diff options
-rw-r--r-- | rpkid/myrpki.py | 21 | ||||
-rw-r--r-- | rpkid/rpki/myrpki.py | 2056 |
2 files changed, 0 insertions, 2077 deletions
diff --git a/rpkid/myrpki.py b/rpkid/myrpki.py deleted file mode 100644 index d8f68627..00000000 --- a/rpkid/myrpki.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -$Id$ - -Copyright (C) 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. -""" - -if __name__ == "__main__": - import rpki.myrpki - rpki.myrpki.main() diff --git a/rpkid/rpki/myrpki.py b/rpkid/rpki/myrpki.py deleted file mode 100644 index ec36371c..00000000 --- a/rpkid/rpki/myrpki.py +++ /dev/null @@ -1,2056 +0,0 @@ -""" -This (oversized) module used to be an (oversized) program. -Refactoring in progress, some doc still needs updating. - - -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--2011 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. -""" - -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 = True - -class BadCommandSyntax(Exception): - """ - Bad command line syntax. - """ - -class BadPrefixSyntax(Exception): - """ - Bad prefix syntax. - """ - -class CouldntTalkToDaemon(Exception): - """ - Couldn't talk to daemon. - """ - -class BadCSVSyntax(Exception): - """ - Bad CSV syntax. - """ - -class BadXMLMessage(Exception): - """ - Bad XML message. - """ - -class PastExpiration(Exception): - """ - Expiration date has already passed. - """ - -class CantRunRootd(Exception): - """ - Can't run rootd. - """ - -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 and iterations. - """ - - def __init__(self, cfg): - self.dir = cfg.get("entitydb_dir", "entitydb") - self.identity = os.path.join(self.dir, "identity.xml") - - def __call__(self, dirname, filebase = None): - if filebase is None: - return os.path.join(self.dir, dirname) - else: - return os.path.join(self.dir, dirname, filebase + ".xml") - - def iterate(self, dir, base = "*"): - return glob.iglob(os.path.join(self.dir, dir, base + ".xml")) - -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 BadPrefixSyntax, "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 BadPrefixSyntax, "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_entitydb(cls, prefix_csv_file, asn_csv_file, fxcert, entitydb): - """ - Parse child data from entitydb. - """ - self = cls() - for f in entitydb.iterate("children"): - 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.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 - return s + ">" - - def add(self, service_uri = None, - bpki_cms_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 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.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) - -class parents(dict): - """ - Database of parent objects. - """ - - def add(self, handle, - service_uri = None, - bpki_cms_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, - myhandle = myhandle, - sia_base = sia_base) - - def xml(self, e): - for c in self.itervalues(): - c.xml(e) - - @classmethod - def from_entitydb(cls, fxcert, entitydb): - """ - Parse parent data from entitydb. - """ - self = cls() - for f in entitydb.iterate("parents"): - 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)) - if r.get("type") == "confirmed": - self.add(handle = h, - service_uri = p.get("service_uri"), - bpki_cms_certificate = fxcert(p.findtext("bpki_resource_ta")), - myhandle = p.get("child_handle"), - sia_base = r.get("sia_base")) - elif whine: - print "Parent %s's repository entry in state %s, skipping this parent" % (h, r.get("type")) - 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_entitydb(cls, fxcert, entitydb): - """ - Parse repository data from entitydb. - """ - self = cls() - for f in entitydb.iterate("repositories"): - h = os.path.splitext(os.path.split(f)[-1])[0] - r = etree_read(f) - if r.get("type") == "confirmed": - self.add(handle = h, - service_uri = r.get("service_uri"), - bpki_certificate = fxcert(r.findtext("bpki_server_ta"))) - elif whine: - print "Repository %s in state %s, skipping this repository" % (h, r.get("type")) - - 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 BadCSVSyntax, "%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 BadCSVSyntax, "%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 - -class csv_writer(object): - """ - Writer object for tab delimited text. We just use the stock CSV - module in excel-tab mode for this. - - If "renmwo" is set (default), the file will be written to - a temporary name and renamed to the real filename after closing. - """ - - def __init__(self, filename, renmwo = True): - self.filename = filename - self.renmwo = "%s.~renmwo%d~" % (filename, os.getpid()) if renmwo else filename - self.file = open(self.renmwo, "w") - self.writer = csv.writer(self.file, dialect = csv.get_dialect("excel-tab")) - - def close(self): - """ - Close this writer. - """ - if self.file is not None: - self.file.close() - self.file = None - if self.filename != self.renmwo: - os.rename(self.renmwo, self.filename) - - def __getattr__(self, attr): - """ - Fake inheritance from whatever object csv.writer deigns to give us. - """ - return getattr(self.writer, attr) - -def PEMBase64(filename): - """ - Extract Base64 encoded data 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 - return "".join(lines) - -def PEMElement(e, tag, filename, **kwargs): - """ - Create an XML element containing Base64 encoded data taken from a - PEM file. - """ - if e.text is None: - e.text = "\n" - se = SubElement(e, tag, **kwargs) - se.text = "\n" + PEMBase64(filename) - 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 certificate, 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()) - der = base64.b64decode(b64) - if True: - try: - text = self.run_openssl("x509", "-inform", "DER", "-noout", - "-issuer", "-subject", stdin = der) - except: - text = "" - print "fxcert():", self.dir, filename, text - try: - self.run_openssl("x509", "-inform", "DER", "-out", fn, - stdin = der) - return self.xcert(fn, path_restriction) - finally: - if not filename and os.path.exists(fn): - os.unlink(fn) - - def xcert_filename(self, cert): - """ - Generate filename for a cross-certification. - - Extracts 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. - """ - - if cert and os.path.exists(cert): - return "%s/xcert.%s.cer" % (self.dir, self.run_dgst(self.run_openssl( - "x509", "-noout", "-pubkey", "-subject", "-in", cert)).strip()) - else: - return None - - def xcert(self, cert, path_restriction = 0): - """ - Cross-certify a certificate represented as a PEM file, 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 = self.xcert_filename(cert) - if not os.path.exists(xcert): - self.run_ca("-ss_cert", cert, "-out", xcert, "-extensions", self.path_restriction[path_restriction]) - return xcert - - def xcert_revoke(self, cert): - """ - Revoke a cross-certification and regenerate CRL. - """ - - xcert = self.xcert_filename(cert) - if xcert: - self.run_ca("-revoke", xcert) - self.run_ca("-gencrl", "-out", self.crl) - -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 BadXMLMessage, "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 IRDB(object): - """ - Front-end to the IRDB. This is broken out from class main so - that other applications (namely, the portal-gui) can reuse it. - """ - def __init__(self, cfg): - """ - Opens a new connection to the IRDB, using the configuration - information from a rpki.config.parser object. - """ - - from rpki.mysql_import import MySQLdb - - irdbd_cfg = rpki.config.parser(cfg.get("irdbd_conf", cfg.filename), "irdbd") - - self.db = MySQLdb.connect(user = irdbd_cfg.get("sql-username"), - db = irdbd_cfg.get("sql-database"), - passwd = irdbd_cfg.get("sql-password")) - - def update(self, handle, roa_requests, children, ghostbusters=None): - """ - Update the IRDB for a given resource handle. Removes all - existing data and replaces it with that specified in the - argument list. - - The "roa_requests" argument is a sequence of tuples of the form - (asID, v4_addresses, v6_addresses), where "v*_addresses" are - instances of rpki.resource_set.roa_prefix_set_ipv*. - - The "children" argument is a sequence of tuples of the form - (child_handle, asns, v4addrs, v6addrs, valid_until), - where "asns" is an instance of rpki.resource_set.resource_set_asn, - "v*addrs" are instances of rpki.resource_set.resource_set_ipv*, - and "valid_until" is an instance of rpki.sundial.datetime. - - The "ghostbusters" argument is a sequence of tuples of the form - (parent_handle, vcard_string). "parent_handle" may be value None, - in which case the specified vcard object will be used for all - parents. - """ - - cur = self.db.cursor() - - 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 asID, v4addrs, v6addrs in roa_requests: - assert isinstance(v4addrs, rpki.resource_set.roa_prefix_set_ipv4) - assert isinstance(v6addrs, rpki.resource_set.roa_prefix_set_ipv6) - cur.execute("INSERT roa_request (roa_request_handle, asn) VALUES (%s, %s)", (handle, asID)) - roa_request_id = cur.lastrowid - for version, prefix_set in ((4, v4addrs), (6, v6addrs)): - 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 child_handle, asns, ipv4, ipv6, valid_until in children: - cur.execute("INSERT registrant (registrant_handle, registry_handle, registrant_name, valid_until) VALUES (%s, %s, %s, %s)", - (child_handle, handle, child_handle, 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)) - - # don't munge the ghostbuster_request table when the arg is None. - # this allows the cli to safely run configure_resources without - # stomping on GBRs created by the portal gui. - if ghostbusters is not None: - cur.execute("DELETE FROM ghostbuster_request WHERE self_handle = %s", (handle,)) - if ghostbusters: - cur.executemany("INSERT INTO ghostbuster_request (self_handle, parent_handle, vcard) VALUES (%s, %s, %s)", - ((handle, parent_handle, vcard) for parent_handle, vcard in ghostbusters)) - - self.db.commit() - - def close(self): - """ - Close the connection to the IRDB. - """ - self.db.close() - - - -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 = None - - 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 entitydb_complete(self, prefix, text, line, begidx, endidx): - """ - Completion helper for entitydb filenames. - """ - names = [] - for name in self.entitydb.iterate(prefix): - name = os.path.splitext(os.path.basename(name))[0] - if name.startswith(text): - names.append(name) - return names - - 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 CantRunRootd, "Can't run rootd unless also running rpkid and pubd" - - self.bpki_resources = CA(self.cfg.filename, self.cfg.get("bpki_resources_directory")) - if self.run_rpkid or self.run_pubd or self.run_rootd: - self.bpki_servers = CA(self.cfg.filename, self.cfg.get("bpki_servers_directory")) - else: - self.bpki_servers = None - - self.default_repository = self.cfg.get("default_repository", "") - 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 BadCommandSyntax, "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, - 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 = "http://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_child_ta", self.bpki_resources.cer) - SubElement(e, "repository", type = "offer") - etree_write(e, self.entitydb("parents", 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", 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_update_bpki(self, arg): - """ - Update BPKI certificates. Assumes an existing RPKI installation. - - Basic plan here is to reissue all BPKI certificates we can, right - now. In the long run we might want to be more clever about only - touching ones that need maintenance, but this will do for a start. - - Most likely this should be run under cron. - """ - - if self.bpki_servers: - bpkis = (self.bpki_resources, self.bpki_servers) - else: - bpkis = (self.bpki_resources,) - - for bpki in bpkis: - for cer in glob.iglob("%s/*.cer" % bpki.dir): - key = cer[0:-4] + ".key" - req = cer[0:-4] + ".req" - if os.path.exists(key): - print "Regenerating BPKI PKCS #10", req - bpki.run_openssl("x509", "-x509toreq", "-in", cer, "-out", req, "-signkey", key) - print "Clearing BPKI certificate", cer - os.unlink(cer) - if cer == bpki.cer: - assert req == bpki.req - print "Regenerating certificate", cer - bpki.run_ca("-selfsign", "-extensions", "ca_x509_ext_ca", "-in", req, "-out", cer) - - print "Regenerating CRLs" - for bpki in bpkis: - bpki.run_ca("-gencrl", "-out", bpki.crl) - - self.do_initialize(None) - if self.run_rpkid or self.run_pubd or self.run_rootd: - self.do_configure_daemons(arg) - else: - self.do_configure_resources(None) - - - 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 BadCommandSyntax, "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: - if self.run_rpkid: - service_uri_base = "http://%s:%s/up-down/%s" % (self.cfg.get("rpkid_server_host"), - self.cfg.get("rpkid_server_port"), - self.handle) - else: - service_uri_base = None - - 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) - - if self.run_rpkid or self.run_pubd or self.run_rootd: - 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) - SubElement(e, "bpki_child_ta").text = c.findtext("bpki_ta") - - repo = None - for f in self.entitydb.iterate("repositories"): - r = etree_read(f) - if r.get("type") == "confirmed": - h = os.path.splitext(os.path.split(f)[-1])[0] - if repo is None or h == self.default_repository: - repo_handle = h - repo = r - - if repo is None: - print "Couldn't find any usable repositories, not giving referral" - - elif 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") - - etree_write(e, self.entitydb("children", child_handle), - msg = "Send this file back to the child you just configured") - - - def do_delete_child(self, arg): - """ - Delete a child of this RPKI entity. - - This should check that the XML file it's deleting really is a - child, but doesn't, yet. - """ - - try: - os.unlink(self.entitydb("children", arg)) - except OSError: - print "No such child \"%s\"" % arg - - def complete_delete_child(self, *args): - return self.entitydb_complete("children", *args) - - - 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 BadCommandSyntax, "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")) - - etree_write(p, self.entitydb("parents", parent_handle)) - - r = p.find("repository") - - if r is None or r.get("type") not in ("offer", "referral"): - r = Element("repository", type = "none") - - 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", parent_handle), - msg = "This is the file to send to the repository operator") - - - def do_delete_parent(self, arg): - """ - Delete a parent of this RPKI entity. - - This should check that the XML file it's deleting really is a - parent, but doesn't, yet. - """ - - try: - os.unlink(self.entitydb("parents", arg)) - except OSError: - print "No such parent \"%s\"" % arg - - def complete_delete_parent(self, *args): - return self.entitydb_complete("parents", *args) - - - 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 BadCommandSyntax, "Need to specify filename for client.xml" - - client = etree_read(argv[0]) - - if sia_base is None and client.get("handle") == self.handle and b64_equal(PEMBase64(self.bpki_resources.cer), client.findtext("bpki_client_ta")): - print "This looks like self-hosted publication" - sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, self.handle) - - if sia_base is None and client.get("type") == "referral": - print "This looks like a referral, checking" - try: - auth = client.find("authorization") - if auth is None: - raise BadXMLMessage, "Malformed referral, couldn't find <auth/> element" - referrer = etree_read(self.entitydb("pubclients", 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 BadXMLMessage, "Referral trust anchor does not match" - sia_base = referral.get("authorized_sia_base") - except IOError: - print "We have no record of client (%s) alleged to have made this referral" % auth.get("referrer") - - if sia_base is None and client.get("type") == "offer" and client.get("parent_handle") == self.handle: - print "This looks like an offer, client claims to be our child, checking" - client_ta = client.findtext("bpki_client_ta") - if not client_ta: - raise BadXMLMessage, "Malformed offer, couldn't find <bpki_client_ta/> element" - for child in self.entitydb.iterate("children"): - 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")) - - if not sia_base.startswith("rsync://"): - raise BadXMLMessage, "Malformed sia_base parameter %r, should start with 'rsync://'" % sia_base - - 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 = "http://%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", client_handle.replace("/", ".")), - msg = "Send this file back to the publication client you just configured") - - - def do_delete_publication_client(self, arg): - """ - Delete a publication client of this RPKI entity. - - This should check that the XML file it's deleting really is a - client, but doesn't, yet. - """ - - try: - os.unlink(self.entitydb("pubclients", arg)) - except OSError: - print "No such client \"%s\"" % arg - - def complete_delete_publication_client(self, *args): - return self.entitydb_complete("pubclients", *args) - - - 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. - """ - - 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 BadCommandSyntax, "Need to specify filename for repository.xml on command line" - - r = etree_read(argv[0]) - - if parent_handle is None: - 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", parent_handle)) - - - def do_delete_repository(self, arg): - """ - Delete a repository of this RPKI entity. - - This should check that the XML file it's deleting really is a - repository, but doesn't, yet. - """ - - try: - os.unlink(self.entitydb("repositories", arg)) - except OSError: - print "No such repository \"%s\"" % arg - - def complete_delete_repository(self, *args): - return self.entitydb_complete("repositories", *args) - - - def renew_children_common(self, arg, plural): - """ - Common code for renew_child and renew_all_children commands. - """ - - valid_until = None - - opts, argv = getopt.getopt(arg.split(), "", ["valid_until"]) - for o, a in opts: - if o == "--valid_until": - valid_until = a - - if plural: - if len(argv) != 0: - raise BadCommandSyntax, "Unexpected arguments" - children = "*" - else: - if len(argv) != 1: - raise BadCommandSyntax, "Need to specify child handle" - children = argv[0] - - if valid_until is None: - valid_until = rpki.sundial.now() + rpki.sundial.timedelta(days = 365) - else: - valid_until = rpki.sundial.fromXMLtime(valid_until) - if valid_until < rpki.sundial.now(): - raise PastExpiration, "Specified new expiration time %s has passed" % valid_until - - print "New validity date", valid_until - - for f in self.entitydb.iterate("children", children): - c = etree_read(f) - c.set("valid_until", str(valid_until)) - etree_write(c, f) - - def do_renew_child(self, arg): - """ - Update validity period for one child entity. - """ - return self.renew_children_common(arg, False) - - def complete_renew_child(self, *args): - return self.entitydb_complete("children", *args) - - def do_renew_all_children(self, arg): - """ - Update validity period for all child entities. - """ - return self.renew_children_common(arg, True) - - - - - 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") - except IOError: - bsc_req, bsc_cer = None, None - service_uri = 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_entitydb( - prefix_csv_file = prefix_csv_file, - asn_csv_file = asn_csv_file, - fxcert = self.bpki_resources.fxcert, - entitydb = self.entitydb).xml(e) - - parents.from_entitydb( - fxcert = self.bpki_resources.fxcert, - entitydb = self.entitydb).xml(e) - - repositories.from_entitydb( - 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) - - 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 BadCommandSyntax, "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.http, rpki.resource_set, rpki.relaxng, rpki.exceptions - import rpki.left_right, rpki.x509, rpki.async - - 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", self_crl_interval / 4) - pubd_base = "http://%s:%s/" % (self.cfg.get("pubd_server_host"), self.cfg.get("pubd_server_port")) - rpkid_base = "http://%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.http.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.http.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))) - - irdb = IRDB(self.cfg) - - 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) - - for xmlfile in xmlfiles: - - # Parse XML file and validate it against our scheme - - tree = etree_read(xmlfile, validate = True) - - handle = tree.get("handle") - - # Update IRDB with parsed resource and roa-request data. - - roa_requests = [( - x.get('asn'), - rpki.resource_set.roa_prefix_set_ipv4(x.get("v4")), - rpki.resource_set.roa_prefix_set_ipv6(x.get("v6"))) for x in tree.getiterator("roa_request")] - - children = [( - x.get("handle"), - rpki.resource_set.resource_set_as(x.get("asns")), - rpki.resource_set.resource_set_ipv4(x.get("v4")), - rpki.resource_set.resource_set_ipv6(x.get("v6")), - rpki.sundial.datetime.fromXMLtime(x.get("valid_until"))) for x in tree.getiterator("child")] - - # ghostbusters are ignored for now - irdb.update(handle, roa_requests, children) - - # 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") - - 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): - 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)) - - 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"): - 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 CouldntTalkToDaemon - - 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 CouldntTalkToDaemon - - # 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) - - etree_write(tree, xmlfile, validate = True, - msg = None if xmlfile is my_xmlfile else 'Send this file back to the hosted entity ("%s")' % handle) - - irdb.close() - - # We used to run event loop again to give TLS connections a chance to shut down cleanly. - # Seems not to be needed (and sometimes hangs forever, which is odd) with TLS out of the picture. - #rpki.async.event_loop() |