"""
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()