""" IRBE-side stuff for myrpki tools. 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 myrpki tool. Those XML files are the input to this script, which uses them to do all the work of constructing certificates, 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 script also runs the myrpki script directly 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. $Id$ Copyright (C) 2009 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 lxml.etree, base64, subprocess, sys, os, time, re, getopt, warnings import rpki.https, rpki.config, rpki.resource_set, rpki.relaxng import rpki.exceptions, rpki.left_right, rpki.log, rpki.x509, rpki.async import myrpki, schema # Silence warning while loading MySQLdb in Python 2.6, sigh if hasattr(warnings, "catch_warnings"): with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) import MySQLdb else: import MySQLdb def tag(t): """ Wrap an element name in the right XML namespace goop. """ return "{http://www.hactrn.net/uris/rpki/myrpki/}" + t def findbase64(tree, name, b64type = rpki.x509.X509): """ Find and extract a base64-encoded XML element, if present. """ x = tree.findtext(tag(name)) return b64type(Base64 = x) if x else None # For simple cases we don't really care what these value are, so long # as we're consistant about them, so wiring them in is fine. bsc_handle = "bsc" repository_handle = "repository" os.environ["TZ"] = "UTC" time.tzset() rpki.log.use_syslog = False rpki.log.init("myirbe") cfg_file = "myrpki.conf" bpki_only = False opts, argv = getopt.getopt(sys.argv[1:], "bc:h?", ["bpki_only", "config=", "help"]) for o, a in opts: if o in ("-b", "--bpki_only"): bpki_only = True elif o in ("-c", "--config"): cfg_file = a elif o in ("-h", "--help", "-?"): print __doc__ sys.exit(0) cfg = rpki.config.parser(cfg_file, "myirbe") cfg.set_global_flags() myrpki.openssl = cfg.get("openssl", "openssl", "myrpki") handle = cfg.get("handle", cfg.get("handle", "Amnesiac", "myrpki")) want_pubd = cfg.getboolean("want_pubd", False) want_rootd = cfg.getboolean("want_rootd", False) bpki_modified = False bpki = myrpki.CA(cfg_file, cfg.get("bpki_directory")) bpki_modified |= bpki.setup(cfg.get("bpki_ta_dn", "/CN=%s BPKI TA" % handle)) bpki_modified |= bpki.ee( cfg.get("bpki_rpkid_ee_dn", "/CN=%s rpkid EE" % handle), "rpkid") bpki_modified |= bpki.ee( cfg.get("bpki_irdbd_ee_dn", "/CN=%s irdbd EE" % handle), "irdbd") bpki_modified |= bpki.ee( cfg.get("bpki_irbe_ee_dn", "/CN=%s irbe EE" % handle), "irbe") if want_pubd: bpki_modified |= bpki.ee( cfg.get("bpki_pubd_ee_dn", "/CN=%s pubd EE" % handle), "pubd") if want_rootd: bpki_modified |= bpki.ee( cfg.get("bpki_rootd_ee_dn", "/CN=%s rootd EE" % handle), "rootd") if bpki_modified: print "BPKI (re)initialized. You need to (re)start daemons before continuing." if bpki_modified or bpki_only: sys.exit() # Default values for CRL parameters are very low, for testing. self_crl_interval = cfg.getint("self_crl_interval", 900) self_regen_margin = cfg.getint("self_regen_margin", 300) pubd_base = cfg.get("pubd_base").rstrip("/") + "/" rpkid_base = cfg.get("rpkid_base").rstrip("/") + "/" # Nasty regexp for parsing rpkid's up-down service URLs. updown_regexp = re.compile(re.escape(rpkid_base) + "up-down/([-A-Z0-9_]+)/([-A-Z0-9_]+)$", re.I) # 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 = bpki.dir + "/irbe.key"), client_cert = rpki.x509.X509(PEM_file = bpki.dir + "/irbe.cer"), server_ta = rpki.x509.X509(PEM_file = bpki.cer), server_cert = rpki.x509.X509(PEM_file = bpki.dir + "/rpkid.cer"), url = rpkid_base + "left-right")) if want_pubd: call_pubd = rpki.async.sync_wrapper(rpki.https.caller( proto = rpki.publication, client_key = rpki.x509.RSA( PEM_file = bpki.dir + "/irbe.key"), client_cert = rpki.x509.X509(PEM_file = bpki.dir + "/irbe.cer"), server_ta = rpki.x509.X509(PEM_file = bpki.cer), server_cert = rpki.x509.X509(PEM_file = bpki.dir + "/pubd.cer"), url = pubd_base + "control")) # 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 = bpki.crl))) irdbd_cfg = rpki.config.parser(cfg.get("irdbd_conf", 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 is present in config file, run myrpki.py # internally, as a convenience, and include its output at the head of # our list of XML files to process. if cfg.has_section("myrpki"): myrpki.main(("-c", cfg_file)) my_xmlfile = cfg.get("xml_filename", None, "myrpki") assert my_xmlfile is not None xmlfiles.append(my_xmlfile) # 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 = lxml.etree.parse(xmlfile).getroot() try: schema.myrpki.assertValid(tree) except lxml.etree.DocumentInvalid: print lxml.etree.tostring(tree, pretty_print = True) raise 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(tag("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(tag("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 = bpki.fxcert(handle + ".cacert.cer", hosted_cacert.get_PEM(), path_restriction = 1)) # See what rpkid and pubd already have on file for this entity. if want_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 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 per . 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 # In general we need one per publication daemon with # whom this has a relationship. In practice there is rarely # (never?) a good reason for a single to use multiple # publication services, so in normal use we only need one # object. If for some reason you really need more # than this, you'll have to hack. repository_cert = findbase64(tree, "bpki_repository_certificate") if repository_cert: repository_pdu = repository_pdus.pop(repository_handle, None) repository_uri = pubd_base + "client/" + tree.get("repository_handle") 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) # setup code here used to be ridiculously complex. Most # of the insanity was due to a misguided attempt to deduce pubd # setup from other data; now that pubd setup is driven by # pubclients.csv, parent setup should be relatively straightforward, # but beware of lingering excessive cleverness in anything dealing # with parent objects in this script. for parent in tree.getiterator(tag("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 != repository_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 = repository_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(tag("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, used to be inferred (badly) from parent setup, # now handled explictly via yet another freaking .csv file. if want_pubd: for client_handle, client_bpki_cert, client_base_uri in myrpki.csv_open(cfg.get("pubclients_csv", "pubclients.csv")): if os.path.exists(client_bpki_cert): client_pdu = client_pdus.pop(client_handle, None) client_bpki_cert = rpki.x509.X509(PEM_file = bpki.xcert(client_bpki_cert)) 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 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: assert not isinstance(r, rpki.left_right.report_error_elt) if pubd_query: assert want_pubd pubd_reply = call_pubd(*pubd_query) for r in pubd_reply: assert not isinstance(r, rpki.publication.report_error_elt) # Rewrite XML. e = tree.find(tag("bpki_bsc_pkcs10")) if e is None and bsc_req is not None: e = lxml.etree.SubElement(tree, "bpki_bsc_pkcs10") elif bsc_req is None: tree.remove(e) if bsc_req is not None: assert e is not None e.text = bsc_req.get_Base64() # Something weird going on here with lxml linked against recent # versions of libxml2. Looks like modifying the tree above somehow # produces validation errors, but it works fine if we convert it to # a string and parse it again. I'm not seeing any problems with any # of the other code that uses lxml to do validation, just this one # place. Weird. Kludge around it for now. tree = lxml.etree.fromstring(lxml.etree.tostring(tree)) try: schema.myrpki.assertValid(tree) except lxml.etree.DocumentInvalid: print lxml.etree.tostring(tree, pretty_print = True) raise lxml.etree.ElementTree(tree).write(xmlfile + ".tmp", pretty_print = True) os.rename(xmlfile + ".tmp", xmlfile) db.close()