aboutsummaryrefslogtreecommitdiff
path: root/rpkid/rpki/left_right.py
diff options
context:
space:
mode:
Diffstat (limited to 'rpkid/rpki/left_right.py')
-rw-r--r--rpkid/rpki/left_right.py1002
1 files changed, 1002 insertions, 0 deletions
diff --git a/rpkid/rpki/left_right.py b/rpkid/rpki/left_right.py
new file mode 100644
index 00000000..8a5e3433
--- /dev/null
+++ b/rpkid/rpki/left_right.py
@@ -0,0 +1,1002 @@
+# $Id$
+
+# Copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
+#
+# 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 ARIN DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ARIN 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.
+
+"""RPKI "left-right" protocol."""
+
+import base64, lxml.etree, time, traceback, os
+import rpki.sax_utils, rpki.resource_set, rpki.x509, rpki.sql, rpki.exceptions
+import rpki.https, rpki.up_down, rpki.relaxng, rpki.sundial, rpki.log
+
+xmlns = "http://www.hactrn.net/uris/rpki/left-right-spec/"
+
+nsmap = { None : xmlns }
+
+class base_elt(object):
+ """Virtual base type for left-right message elements."""
+
+ attributes = ()
+ elements = ()
+ booleans = ()
+
+ def startElement(self, stack, name, attrs):
+ """Default startElement() handler: just process attributes."""
+ self.read_attrs(attrs)
+
+ def endElement(self, stack, name, text):
+ """Default endElement() handler: just pop the stack."""
+ stack.pop()
+
+ def read_attrs(self, attrs):
+ """Template-driven attribute reader."""
+ for key in self.attributes:
+ val = attrs.get(key, None)
+ if isinstance(val, str) and val.isdigit():
+ val = long(val)
+ setattr(self, key, val)
+ for key in self.booleans:
+ setattr(self, key, attrs.get(key, False))
+
+ def make_elt(self):
+ """XML element constructor."""
+ elt = lxml.etree.Element("{%s}%s" % (xmlns, self.element_name), nsmap = nsmap)
+ for key in self.attributes:
+ val = getattr(self, key, None)
+ if val is not None:
+ elt.set(key, str(val))
+ for key in self.booleans:
+ if getattr(self, key, False):
+ elt.set(key, "yes")
+ return elt
+
+ def make_b64elt(self, elt, name, value = None):
+ """Constructor for Base64-encoded subelement."""
+ if value is None:
+ value = getattr(self, name, None)
+ if value is not None:
+ lxml.etree.SubElement(elt, "{%s}%s" % (xmlns, name), nsmap = nsmap).text = base64.b64encode(value)
+
+ def __str__(self):
+ """Convert a base_elt object to string format."""
+ lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "us-ascii")
+
+class data_elt(base_elt, rpki.sql.sql_persistant):
+ """Virtual class for top-level left-right protocol data elements."""
+
+ def self(this, gctx):
+ """Fetch self object to which this object links."""
+ return self_elt.sql_fetch(gctx, this.self_id)
+
+ def bsc(self, gctx):
+ """Return BSC object to which this object links."""
+ return bsc_elt.sql_fetch(gctx, self.bsc_id)
+
+ @classmethod
+ def make_pdu(cls, **kargs):
+ """Generic left-right PDU constructor."""
+ self = cls()
+ for k,v in kargs.items():
+ setattr(self, k, v)
+ return self
+
+ def make_reply(self, r_pdu = None):
+ """Construct a reply PDU."""
+ if r_pdu is None:
+ r_pdu = self.__class__()
+ r_pdu.self_id = self.self_id
+ setattr(r_pdu, self.sql_template.index, getattr(self, self.sql_template.index))
+ else:
+ for b in r_pdu.booleans:
+ setattr(r_pdu, b, False)
+ r_pdu.action = self.action
+ r_pdu.type = "reply"
+ r_pdu.tag = self.tag
+ return r_pdu
+
+ def serve_pre_save_hook(self, gctx, q_pdu, r_pdu):
+ """Overridable hook."""
+ pass
+
+ def serve_post_save_hook(self, gctx, q_pdu, r_pdu):
+ """Overridable hook."""
+ pass
+
+ def serve_create(self, gctx, r_msg):
+ """Handle a create action."""
+ r_pdu = self.make_reply()
+ self.serve_pre_save_hook(gctx, self, r_pdu)
+ self.sql_store(gctx)
+ setattr(r_pdu, self.sql_template.index, getattr(self, self.sql_template.index))
+ self.serve_post_save_hook(gctx, self, r_pdu)
+ r_msg.append(r_pdu)
+
+ def serve_fetch_one(self, gctx):
+ """Find the object on which a get, set, or destroy method should
+ operate. This is a separate method because the self object needs
+ to override it.
+ """
+ where = self.sql_template.index + " = %s AND self_id = %s"
+ args = (getattr(self, self.sql_template.index), self.self_id)
+ r = self.sql_fetch_where1(gctx, where, args)
+ if r is None:
+ raise rpki.exceptions.NotFound, "Lookup failed where %s" + (where % args)
+ return r
+
+ def serve_set(self, gctx, r_msg):
+ """Handle a set action."""
+ db_pdu = self.serve_fetch_one(gctx)
+ r_pdu = self.make_reply()
+ for a in db_pdu.sql_template.columns[1:]:
+ v = getattr(self, a)
+ if v is not None:
+ setattr(db_pdu, a, v)
+ db_pdu.sql_mark_dirty()
+ db_pdu.serve_pre_save_hook(gctx, self, r_pdu)
+ db_pdu.sql_store(gctx)
+ db_pdu.serve_post_save_hook(gctx, self, r_pdu)
+ r_msg.append(r_pdu)
+
+ def serve_get(self, gctx, r_msg):
+ """Handle a get action."""
+ r_pdu = self.serve_fetch_one(gctx)
+ self.make_reply(r_pdu)
+ r_msg.append(r_pdu)
+
+ def serve_list(self, gctx, r_msg):
+ """Handle a list action for non-self objects."""
+ for r_pdu in self.sql_fetch_where(gctx, "self_id = %s", (self.self_id,)):
+ self.make_reply(r_pdu)
+ r_msg.append(r_pdu)
+
+ def serve_destroy(self, gctx, r_msg):
+ """Handle a destroy action."""
+ db_pdu = self.serve_fetch_one(gctx)
+ db_pdu.sql_delete(gctx)
+ r_msg.append(self.make_reply())
+
+ def serve_dispatch(self, gctx, r_msg):
+ """Action dispatch handler."""
+ dispatch = { "create" : self.serve_create,
+ "set" : self.serve_set,
+ "get" : self.serve_get,
+ "list" : self.serve_list,
+ "destroy" : self.serve_destroy }
+ if self.type != "query" or self.action not in dispatch:
+ raise rpki.exceptions.BadQuery, "Unexpected query: type %s, action %s" % (self.type, self.action)
+ dispatch[self.action](gctx, r_msg)
+
+ def unimplemented_control(self, *controls):
+ """Uniform handling for unimplemented control operations."""
+ unimplemented = [x for x in controls if getattr(self, x, False)]
+ if unimplemented:
+ raise rpki.exceptions.NotImplementedYet, "Unimplemented control %s" % ", ".join(unimplemented)
+
+class extension_preference_elt(base_elt):
+ """Container for extension preferences."""
+
+ element_name = "extension_preference"
+ attributes = ("name",)
+
+ def startElement(self, stack, name, attrs):
+ """Handle <extension_preference/> elements."""
+ assert name == "extension_preference", "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+
+ def endElement(self, stack, name, text):
+ """Handle <extension_preference/> elements."""
+ self.value = text
+ stack.pop()
+
+ def toXML(self):
+ """Generate <extension_preference/> elements."""
+ elt = self.make_elt()
+ elt.text = self.value
+ return elt
+
+class self_elt(data_elt):
+ """<self/> element."""
+
+ element_name = "self"
+ attributes = ("action", "type", "tag", "self_id", "crl_interval")
+ elements = ("extension_preference",)
+ booleans = ("rekey", "reissue", "revoke", "run_now", "publish_world_now", "clear_extension_preferences")
+
+ sql_template = rpki.sql.template("self", "self_id", "use_hsm", "crl_interval")
+
+ self_id = None
+ use_hsm = False
+ crl_interval = None
+
+ def __init__(self):
+ """Initialize a self_elt."""
+ self.prefs = []
+
+ def sql_fetch_hook(self, gctx):
+ """Extra SQL fetch actions for self_elt -- handle extension preferences."""
+ gctx.cur.execute("SELECT pref_name, pref_value FROM self_pref WHERE self_id = %s", (self.self_id,))
+ for name, value in gctx.cur.fetchall():
+ e = extension_preference_elt()
+ e.name = name
+ e.value = value
+ self.prefs.append(e)
+
+ def sql_insert_hook(self, gctx):
+ """Extra SQL insert actions for self_elt -- handle extension preferences."""
+ if self.prefs:
+ gctx.cur.executemany("INSERT self_pref (self_id, pref_name, pref_value) VALUES (%s, %s, %s)",
+ ((e.name, e.value, self.self_id) for e in self.prefs))
+
+ def sql_delete_hook(self, gctx):
+ """Extra SQL delete actions for self_elt -- handle extension preferences."""
+ gctx.cur.execute("DELETE FROM self_pref WHERE self_id = %s", (self.self_id,))
+
+ def bscs(self, gctx):
+ """Fetch all BSC objects that link to this self object."""
+ return bsc_elt.sql_fetch_where(gctx, "self_id = %s", (self.self_id,))
+
+ def repositories(self, gctx):
+ """Fetch all repository objects that link to this self object."""
+ return repository_elt.sql_fetch_where(gctx, "self_id = %s", (self.self_id,))
+
+ def parents(self, gctx):
+ """Fetch all parent objects that link to this self object."""
+ return parent_elt.sql_fetch_where(gctx, "self_id = %s", (self.self_id,))
+
+ def children(self, gctx):
+ """Fetch all child objects that link to this self object."""
+ return child_elt.sql_fetch_where(gctx, "self_id = %s", (self.self_id,))
+
+ def route_origins(self, gctx):
+ """Fetch all route_origin objects that link to this self object."""
+ return route_origin_elt.sql_fetch_where(gctx, "self_id = %s", (self.self_id,))
+
+ def serve_pre_save_hook(self, gctx, q_pdu, r_pdu):
+ """Extra server actions for self_elt -- handle extension preferences."""
+ rpki.log.trace()
+ if self is not q_pdu:
+ if q_pdu.clear_extension_preferences:
+ self.prefs = []
+ self.prefs.extend(q_pdu.prefs)
+
+ def serve_post_save_hook(self, gctx, q_pdu, r_pdu):
+ """Extra server actions for self_elt."""
+ rpki.log.trace()
+ if q_pdu.rekey:
+ self.serve_rekey(gctx)
+ if q_pdu.revoke:
+ self.serve_revoke(gctx)
+ self.unimplemented_control("reissue", "run_now", "publish_world_now")
+
+ def serve_rekey(self, gctx):
+ """Handle a left-right rekey action for this self."""
+ rpki.log.trace()
+ for parent in self.parents(gctx):
+ parent.serve_rekey(gctx)
+
+ def serve_revoke(self, gctx):
+ """Handle a left-right revoke action for this self."""
+ rpki.log.trace()
+ for parent in self.parents(gctx):
+ parent.serve_revoke(gctx)
+
+ def serve_fetch_one(self, gctx):
+ """Find the self object on which a get, set, or destroy method
+ should operate.
+ """
+ r = self.sql_fetch(gctx, self.self_id)
+ if r is None:
+ raise rpki.exceptions.NotFound
+ return r
+
+ def serve_list(self, gctx, r_msg):
+ """Handle a list action for self objects. This is different from
+ the list action for all other objects, where list only works
+ within a given self_id context.
+ """
+ for r_pdu in self.sql_fetch_all(gctx):
+ self.make_reply(r_pdu)
+ r_msg.append(r_pdu)
+
+ def startElement(self, stack, name, attrs):
+ """Handle <self/> element."""
+ if name == "extension_preference":
+ pref = extension_preference_elt()
+ self.prefs.append(pref)
+ stack.append(pref)
+ pref.startElement(stack, name, attrs)
+ else:
+ assert name == "self", "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+
+ def endElement(self, stack, name, text):
+ """Handle <self/> element."""
+ assert name == "self", "Unexpected name %s, stack %s" % (name, stack)
+ stack.pop()
+
+ def toXML(self):
+ """Generate <self/> element."""
+ elt = self.make_elt()
+ elt.extend([i.toXML() for i in self.prefs])
+ return elt
+
+ def client_poll(self, gctx):
+ """Run the regular client poll cycle with each of this self's parents in turn."""
+
+ rpki.log.trace()
+
+ for parent in self.parents(gctx):
+
+ # This will need a callback when we go event-driven
+ r_msg = rpki.up_down.list_pdu.query(gctx, parent)
+
+ ca_map = dict((ca.parent_resource_class, ca) for ca in parent.cas(gctx))
+ for rc in r_msg.payload.classes:
+ if rc.class_name in ca_map:
+ ca = ca_map[rc.class_name]
+ del ca_map[rc.class_name]
+ ca.check_for_updates(gctx, parent, rc)
+ else:
+ rpki.sql.ca_obj.create(gctx, parent, rc)
+ for ca in ca_map.values():
+ ca.delete(gctx, parent) # CA not listed by parent
+ rpki.sql.sql_sweep(gctx)
+
+ def update_children(self, gctx):
+ """Check for updated IRDB data for all of this self's children and
+ issue new certs as necessary. Must handle changes both in
+ resources and in expiration date.
+ """
+
+ rpki.log.trace()
+
+ now = rpki.sundial.datetime.utcnow()
+
+ for child in self.children(gctx):
+ child_certs = child.child_certs(gctx)
+ if not child_certs:
+ continue
+
+ # This will require a callback when we go event-driven
+ irdb_resources = rpki.left_right.irdb_query(gctx, child.self_id, child.child_id)
+
+ for child_cert in child_certs:
+ ca_detail = child_cert.ca_detail(gctx)
+ if ca_detail.state != "active":
+ continue
+ old_resources = child_cert.cert.get_3779resources()
+ new_resources = irdb_resources.intersection(old_resources)
+ if old_resources != new_resources:
+ rpki.log.debug("Need to reissue %s" % repr(child_cert))
+ child_cert.reissue(
+ gctx = gctx,
+ ca_detail = ca_detail,
+ resources = new_resources)
+ elif old_resources.valid_until < now:
+ parent = ca.parent(gctx)
+ repository = parent.repository(gctx)
+ child_cert.sql_delete(gctx)
+ ca_detail.generate_manifest(gctx)
+ repository.withdraw(gctx, child_cert.cert, child_cert.uri(ca))
+
+ def regenerate_crls_and_manifests(self, gctx):
+ """Generate new CRLs and manifests as necessary for all of this
+ self's CAs. Extracting nextUpdate from a manifest is hard at the
+ moment due to implementation silliness, so for now we generate a
+ new manifest whenever we generate a new CRL
+
+ This method also cleans up tombstones left behind by revoked
+ ca_detail objects, since we're walking through the relevant
+ portions of the database anyway.
+ """
+
+ rpki.log.trace()
+
+ now = rpki.sundial.datetime.utcnow()
+ for parent in self.parents(gctx):
+ repository = parent.repository(gctx)
+ for ca in parent.cas(gctx):
+ for ca_detail in ca.fetch_revoked(gctx):
+ if now > ca_detail.latest_crl.getNextUpdate():
+ ca_detail.delete(gctx, ca, repository)
+ ca_detail = ca.fetch_active(gctx)
+ if now > ca_detail.latest_crl.getNextUpdate():
+ ca_detail.generate_crl(gctx)
+ ca_detail.generate_manifest(gctx)
+
+class bsc_elt(data_elt):
+ """<bsc/> (Business Signing Context) element."""
+
+ element_name = "bsc"
+ attributes = ("action", "type", "tag", "self_id", "bsc_id", "key_type", "hash_alg", "key_length")
+ elements = ('signing_cert',)
+ booleans = ("generate_keypair", "clear_signing_certs")
+
+ sql_template = rpki.sql.template("bsc", "bsc_id", "self_id",
+ ("public_key", rpki.x509.RSApublic),
+ ("private_key_id", rpki.x509.RSA), "hash_alg")
+
+ pkcs10_cert_request = None
+ public_key = None
+ private_key_id = None
+
+ def __init__(self):
+ """Initialize bsc_elt."""
+ self.signing_cert = rpki.x509.X509_chain()
+
+ def sql_fetch_hook(self, gctx):
+ """Extra SQL fetch actions for bsc_elt -- handle signing certs."""
+ gctx.cur.execute("SELECT cert FROM bsc_cert WHERE bsc_id = %s", (self.bsc_id,))
+ self.signing_cert[:] = [rpki.x509.X509(DER = x) for (x,) in gctx.cur.fetchall()]
+
+ def sql_insert_hook(self, gctx):
+ """Extra SQL insert actions for bsc_elt -- handle signing certs."""
+ if self.signing_cert:
+ gctx.cur.executemany("INSERT bsc_cert (cert, bsc_id) VALUES (%s, %s)",
+ ((x.get_DER(), self.bsc_id) for x in self.signing_cert))
+
+ def sql_delete_hook(self, gctx):
+ """Extra SQL delete actions for bsc_elt -- handle signing certs."""
+ gctx.cur.execute("DELETE FROM bsc_cert WHERE bsc_id = %s", (self.bsc_id,))
+
+ def repositories(self, gctx):
+ """Fetch all repository objects that link to this BSC object."""
+ return repository_elt.sql_fetch_where(gctx, "bsc_id = %s", (self.bsc_id,))
+
+ def parents(self, gctx):
+ """Fetch all parent objects that link to this BSC object."""
+ return parent_elt.sql_fetch_where(gctx, "bsc_id = %s", (self.bsc_id,))
+
+ def children(self, gctx):
+ """Fetch all child objects that link to this BSC object."""
+ return child_elt.sql_fetch_where(gctx, "bsc_id = %s", (self.bsc_id,))
+
+ def serve_pre_save_hook(self, gctx, q_pdu, r_pdu):
+ """Extra server actions for bsc_elt -- handle signing certs and key generation."""
+ if self is not q_pdu:
+ if q_pdu.clear_signing_certs:
+ self.signing_cert[:] = []
+ self.signing_cert.extend(q_pdu.signing_cert)
+ if q_pdu.generate_keypair:
+ #
+ # For the moment we only support 2048-bit RSA with SHA-256, no
+ # HSM. Assertion just checks that the schema hasn't changed out
+ # from under this code.
+ #
+ assert (q_pdu.key_type is None or q_pdu.key_type == "rsa") and \
+ (q_pdu.hash_alg is None or q_pdu.hash_alg == "sha256") and \
+ (q_pdu.key_length is None or q_pdu.key_length == 2048)
+ keypair = rpki.x509.RSA()
+ keypair.generate()
+ self.private_key_id = keypair
+ self.public_key = keypair.get_RSApublic()
+ r_pdu.pkcs10_cert_request = rpki.x509.PKCS10.create(keypair)
+
+ def startElement(self, stack, name, attrs):
+ """Handle <bsc/> element."""
+ if not name in ("signing_cert", "public_key", "pkcs10_cert_request"):
+ assert name == "bsc", "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+
+ def endElement(self, stack, name, text):
+ """Handle <bsc/> element."""
+ if name == "signing_cert":
+ self.signing_cert.append(rpki.x509.X509(Base64 = text))
+ elif name == "public_key":
+ self.public_key = rpki.x509.RSApublic(Base64 = text)
+ elif name == "pkcs10_cert_request":
+ self.pkcs10_cert_request = rpki.x509.PKCS10(Base64 = text)
+ else:
+ assert name == "bsc", "Unexpected name %s, stack %s" % (name, stack)
+ stack.pop()
+
+ def toXML(self):
+ """Generate <bsc/> element."""
+ elt = self.make_elt()
+ for cert in self.signing_cert:
+ self.make_b64elt(elt, "signing_cert", cert.get_DER())
+ if self.pkcs10_cert_request is not None:
+ self.make_b64elt(elt, "pkcs10_cert_request", self.pkcs10_cert_request.get_DER())
+ if self.public_key is not None:
+ self.make_b64elt(elt, "public_key", self.public_key.get_DER())
+ return elt
+
+class parent_elt(data_elt):
+ """<parent/> element."""
+
+ element_name = "parent"
+ attributes = ("action", "type", "tag", "self_id", "parent_id", "bsc_id", "repository_id",
+ "peer_contact_uri", "sia_base", "sender_name", "recipient_name")
+ elements = ("cms_ta", "https_ta")
+ booleans = ("rekey", "reissue", "revoke")
+
+ sql_template = rpki.sql.template("parent", "parent_id", "self_id", "bsc_id", "repository_id",
+ ("cms_ta", rpki.x509.X509), ("https_ta", rpki.x509.X509),
+ "peer_contact_uri", "sia_base", "sender_name", "recipient_name")
+
+ cms_ta = None
+ https_ta = None
+
+ def repository(self, gctx):
+ """Fetch repository object to which this parent object links."""
+ return repository_elt.sql_fetch(gctx, self.repository_id)
+
+ def cas(self, gctx):
+ """Fetch all CA objects that link to this parent object."""
+ return rpki.sql.ca_obj.sql_fetch_where(gctx, "parent_id = %s", (self.parent_id,))
+
+ def serve_post_save_hook(self, gctx, q_pdu, r_pdu):
+ """Extra server actions for parent_elt."""
+ if q_pdu.rekey:
+ self.serve_rekey(gctx)
+ if q_pdu.revoke:
+ self.serve_revoke(gctx)
+ self.unimplemented_control("reissue")
+
+ def serve_rekey(self, gctx):
+ """Handle a left-right rekey action for this parent."""
+ for ca in self.cas(gctx):
+ ca.rekey(gctx)
+
+ def serve_revoke(self, gctx):
+ """Handle a left-right revoke action for this parent."""
+ for ca in self.cas(gctx):
+ ca.revoke(gctx)
+
+ def startElement(self, stack, name, attrs):
+ """Handle <parent/> element."""
+ if name not in ("cms_ta", "https_ta"):
+ assert name == "parent", "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+
+ def endElement(self, stack, name, text):
+ """Handle <parent/> element."""
+ if name == "cms_ta":
+ self.cms_ta = rpki.x509.X509(Base64 = text)
+ elif name == "https_ta":
+ self.https_ta = rpki.x509.X509(Base64 = text)
+ else:
+ assert name == "parent", "Unexpected name %s, stack %s" % (name, stack)
+ stack.pop()
+
+ def toXML(self):
+ """Generate <parent/> element."""
+ elt = self.make_elt()
+ if self.cms_ta and not self.cms_ta.empty():
+ self.make_b64elt(elt, "cms_ta", self.cms_ta.get_DER())
+ if self.https_ta and not self.https_ta.empty():
+ self.make_b64elt(elt, "https_ta", self.https_ta.get_DER())
+ return elt
+
+ def query_up_down(self, gctx, q_pdu):
+ """Client code for sending one up-down query PDU to this parent.
+
+ I haven't figured out yet whether this method should do something
+ clever like dispatching via a method in the response PDU payload,
+ or just hand back the whole response to the caller. In the long
+ run this will have to become event driven with a context object
+ that has methods of its own, but as this method is common code for
+ several different queries and I don't yet know what the response
+ processing looks like, it's too soon to tell what will make sense.
+
+ For now, keep this dead simple lock step, rewrite it later.
+ """
+
+ rpki.log.trace()
+
+ bsc = self.bsc(gctx)
+ if bsc is None:
+ raise rpki.exceptions.BSCNotFound, "Could not find BSC %s" % self.bsc_id
+ q_msg = rpki.up_down.message_pdu.make_query(
+ payload = q_pdu,
+ sender = self.sender_name,
+ recipient = self.recipient_name)
+ q_elt = q_msg.toXML()
+ try:
+ rpki.relaxng.up_down.assertValid(q_elt)
+ except lxml.etree.DocumentInvalid:
+ rpki.log.error("Message does not pass schema check: " + lxml.etree.tostring(q_elt, pretty_print = True))
+ raise
+ q_cms = rpki.cms.xml_sign(q_elt, bsc.private_key_id, bsc.signing_cert, encoding = "UTF-8")
+ r_cms = rpki.https.client(x509TrustList = rpki.x509.X509_chain(self.https_ta),
+ privateKey = gctx.https_key,
+ certChain = gctx.https_certs,
+ msg = q_cms,
+ url = self.peer_contact_uri)
+ r_elt = rpki.cms.xml_verify(r_cms, self.cms_ta)
+ rpki.relaxng.up_down.assertValid(r_elt)
+ r_msg = rpki.up_down.sax_handler.saxify(r_elt)
+ r_msg.payload.check_response()
+ return r_msg
+
+
+class child_elt(data_elt):
+ """<child/> element."""
+
+ element_name = "child"
+ attributes = ("action", "type", "tag", "self_id", "child_id", "bsc_id")
+ elements = ("cms_ta",)
+ booleans = ("reissue", )
+
+ sql_template = rpki.sql.template("child", "child_id", "self_id", "bsc_id", ("cms_ta", rpki.x509.X509))
+
+ cms_ta = None
+
+ def child_certs(self, gctx, ca_detail = None, ski = None, revoked = False, unique = False):
+ """Fetch all child_cert objects that link to this child object."""
+ return rpki.sql.child_cert_obj.fetch(gctx, self, ca_detail, ski, revoked, unique)
+
+ def parents(self, gctx):
+ """Fetch all parent objects that link to self object to which this child object links."""
+ return parent_elt.sql_fetch_where(gctx, "self_id = %s", (self.self_id,))
+
+ def ca_from_class_name(self, gctx, class_name):
+ """Fetch the CA corresponding to an up-down class_name."""
+ if not class_name.isdigit():
+ raise rpki.exceptions.BadClassNameSyntax, "Bad class name %s" % class_name
+ ca = rpki.sql.ca_obj.sql_fetch(gctx, long(class_name))
+ parent = ca.parent(gctx)
+ if self.self_id != parent.self_id:
+ raise rpki.exceptions.ClassNameMismatch, "child.self_id = %d, parent.self_id = %d" % (self.self_id, parent.self_id)
+ return ca
+
+ def serve_post_save_hook(self, gctx, q_pdu, r_pdu):
+ """Extra server actions for child_elt."""
+ self.unimplemented_control("reissue")
+
+ def startElement(self, stack, name, attrs):
+ """Handle <child/> element."""
+ if name != "cms_ta":
+ assert name == "child", "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+
+ def endElement(self, stack, name, text):
+ """Handle <child/> element."""
+ if name == "cms_ta":
+ self.cms_ta = rpki.x509.X509(Base64 = text)
+ else:
+ assert name == "child", "Unexpected name %s, stack %s" % (name, stack)
+ stack.pop()
+
+ def toXML(self):
+ """Generate <child/> element."""
+ elt = self.make_elt()
+ if self.cms_ta:
+ self.make_b64elt(elt, "cms_ta", self.cms_ta.get_DER())
+ return elt
+
+ def serve_up_down(self, gctx, query):
+ """Outer layer of server handling for one up-down PDU from this child."""
+
+ rpki.log.trace()
+
+ bsc = self.bsc(gctx)
+ if bsc is None:
+ raise rpki.exceptions.BSCNotFound, "Could not find BSC %s" % self.bsc_id
+ q_elt = rpki.cms.xml_verify(query, self.cms_ta)
+ rpki.relaxng.up_down.assertValid(q_elt)
+ q_msg = rpki.up_down.sax_handler.saxify(q_elt)
+ #if q_msg.sender != str(self.child_id):
+ # raise rpki.exceptions.BadSender, "Unexpected XML sender %s" % q_msg.sender
+ try:
+ r_msg = q_msg.serve_top_level(gctx, self)
+ except Exception, data:
+ rpki.log.error(traceback.format_exc())
+ r_msg = q_msg.serve_error(data)
+ #
+ # Exceptions from this point on are problematic, as we have no
+ # sane way of reporting errors in the error reporting mechanism.
+ # May require refactoring, ignore the issue for now.
+ #
+ r_elt = r_msg.toXML()
+ try:
+ rpki.relaxng.up_down.assertValid(r_elt)
+ except:
+ rpki.log.debug(lxml.etree.tostring(r_elt, pretty_print = True, encoding = "UTF-8"))
+ rpki.log.error(traceback.format_exc())
+ raise
+ return rpki.cms.xml_sign(r_elt, bsc.private_key_id, bsc.signing_cert, encoding = "UTF-8")
+
+class repository_elt(data_elt):
+ """<repository/> element."""
+
+ element_name = "repository"
+ attributes = ("action", "type", "tag", "self_id", "repository_id", "bsc_id", "peer_contact_uri")
+ elements = ("cms_ta", "https_ta")
+
+ sql_template = rpki.sql.template("repository", "repository_id", "self_id", "bsc_id",
+ ("cms_ta", rpki.x509.X509), "peer_contact_uri",
+ ("https_ta", rpki.x509.X509))
+
+ cms_ta = None
+ https_ta = None
+
+ def parents(self, gctx):
+ """Fetch all parent objects that link to this repository object."""
+ return parent_elt.sql_fetch_where(gctx, "repository_id = %s", (self.repository_id,))
+
+ def startElement(self, stack, name, attrs):
+ """Handle <repository/> element."""
+ if name not in ("cms_ta", "https_ta"):
+ assert name == "repository", "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+
+ def endElement(self, stack, name, text):
+ """Handle <repository/> element."""
+ if name == "cms_ta":
+ self.cms_ta = rpki.x509.X509(Base64 = text)
+ elif name == "https_ta":
+ self.https_ta = rpki.x509.X509(Base64 = text)
+ else:
+ assert name == "repository", "Unexpected name %s, stack %s" % (name, stack)
+ stack.pop()
+
+ def toXML(self):
+ """Generate <repository/> element."""
+ elt = self.make_elt()
+ if self.cms_ta:
+ self.make_b64elt(elt, "cms_ta", self.cms_ta.get_DER())
+ if self.https_ta:
+ self.make_b64elt(elt, "https_ta", self.https_ta.get_DER())
+ return elt
+
+ @staticmethod
+ def uri_to_filename(base, uri):
+ """Convert a URI to a filename. [TEMPORARY]"""
+ if not uri.startswith("rsync://"):
+ raise rpki.exceptions.BadURISyntax
+ filename = base + uri[len("rsync://"):]
+ if filename.find("//") >= 0 or filename.find("/../") >= 0 or filename.endswith("/.."):
+ raise rpki.exceptions.BadURISyntax
+ return filename
+
+ @classmethod
+ def object_write(cls, base, uri, obj):
+ """Write an object to disk. [TEMPORARY]"""
+ rpki.log.trace()
+ filename = cls.uri_to_filename(base, uri)
+ dirname = os.path.dirname(filename)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+ f = open(filename, "wb")
+ f.write(obj.get_DER())
+ f.close()
+
+ @classmethod
+ def object_delete(cls, base, uri):
+ """Delete an object from disk. [TEMPORARY]"""
+ rpki.log.trace()
+ os.remove(cls.uri_to_filename(base, uri))
+
+ def publish(self, gctx, obj, uri):
+ """Placeholder for publication operation. [TEMPORARY]"""
+ rpki.log.trace()
+ rpki.log.info("Publishing %s to repository %s at %s" % (repr(obj), repr(self), repr(uri)))
+ self.object_write(gctx.publication_kludge_base, uri, obj)
+
+ def withdraw(self, gctx, obj, uri):
+ """Placeholder for publication withdrawal operation. [TEMPORARY]"""
+ rpki.log.trace()
+ rpki.log.info("Withdrawing %s from repository %s at %s" % (repr(obj), repr(self), repr(uri)))
+ self.object_delete(gctx.publication_kludge_base, uri)
+
+class route_origin_elt(data_elt):
+ """<route_origin/> element."""
+
+ element_name = "route_origin"
+ attributes = ("action", "type", "tag", "self_id", "route_origin_id", "as_number", "ipv4", "ipv6")
+ booleans = ("suppress_publication",)
+
+ sql_template = rpki.sql.template("route_origin", "route_origin_id", "self_id", "as_number",
+ "ca_detail_id", "roa")
+
+ ca_detail_id = None
+ roa = None
+
+ def sql_fetch_hook(self, gctx):
+ """Extra SQL fetch actions for route_origin_elt -- handle address ranges."""
+ self.ipv4 = rpki.resource_set.resource_set_ipv4.from_sql(gctx.cur, """
+ SELECT start_ip, end_ip FROM route_origin_range
+ WHERE route_origin_id = %s AND start_ip NOT LIKE '%:%'
+ """, (self.route_origin_id,))
+ self.ipv6 = rpki.resource_set.resource_set_ipv6.from_sql(gctx.cur, """
+ SELECT start_ip, end_ip FROM route_origin_range
+ WHERE route_origin_id = %s AND start_ip LIKE '%:%'
+ """, (self.route_origin_id,))
+
+ def sql_insert_hook(self, gctx):
+ """Extra SQL insert actions for route_origin_elt -- handle address ranges."""
+ if self.ipv4 + self.ipv6:
+ gctx.cur.executemany("""
+ INSERT route_origin_range (route_origin_id, start_ip, end_ip)
+ VALUES (%s, %s, %s)""",
+ ((self.route_origin_id, x.min, x.max) for x in self.ipv4 + self.ipv6))
+
+ def sql_delete_hook(self, gctx):
+ """Extra SQL delete actions for route_origin_elt -- handle address ranges."""
+ gctx.cur.execute("DELETE FROM route_origin_range WHERE route_origin_id = %s", (self.route_origin_id,))
+
+ def ca_detail(self, gctx):
+ """Fetch all ca_detail objects that link to this route_origin object."""
+ return rpki.sql.ca_detail_obj.sql_fetch(gctx, self.ca_detail_id)
+
+ def serve_post_save_hook(self, gctx, q_pdu, r_pdu):
+ """Extra server actions for route_origin_elt."""
+ self.unimplemented_control("suppress_publication")
+
+ def startElement(self, stack, name, attrs):
+ """Handle <route_origin/> element."""
+ assert name == "route_origin", "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+ if self.as_number is not None:
+ self.as_number = long(self.as_number)
+ if self.ipv4 is not None:
+ self.ipv4 = rpki.resource_set.resource_set_ipv4(self.ipv4)
+ if self.ipv6 is not None:
+ self.ipv6 = rpki.resource_set.resource_set_ipv6(self.ipv4)
+
+ def endElement(self, stack, name, text):
+ """Handle <route_origin/> element."""
+ assert name == "route_origin", "Unexpected name %s, stack %s" % (name, stack)
+ stack.pop()
+
+ def toXML(self):
+ """Generate <route_origin/> element."""
+ return self.make_elt()
+
+class list_resources_elt(base_elt):
+ """<list_resources/> element."""
+
+ element_name = "list_resources"
+ attributes = ("type", "self_id", "tag", "child_id", "valid_until", "as", "ipv4", "ipv6", "subject_name")
+ valid_until = None
+
+ def startElement(self, stack, name, attrs):
+ """Handle <list_resources/> element."""
+ assert name == "list_resources", "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+ if isinstance(self.valid_until, str):
+ self.valid_until = rpki.sundial.datetime.fromXMLtime(self.valid_until)
+ if self.as is not None:
+ self.as = rpki.resource_set.resource_set_as(self.as)
+ if self.ipv4 is not None:
+ self.ipv4 = rpki.resource_set.resource_set_ipv4(self.ipv4)
+ if self.ipv6 is not None:
+ self.ipv6 = rpki.resource_set.resource_set_ipv6(self.ipv6)
+
+ def toXML(self):
+ """Generate <list_resources/> element."""
+ elt = self.make_elt()
+ if isinstance(self.valid_until, int):
+ elt.set("valid_until", self.valid_until.toXMLtime())
+ return elt
+
+class report_error_elt(base_elt):
+ """<report_error/> element."""
+
+ element_name = "report_error"
+ attributes = ("tag", "self_id", "error_code")
+
+ def startElement(self, stack, name, attrs):
+ """Handle <report_error/> element."""
+ assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+
+ def toXML(self):
+ """Generate <report_error/> element."""
+ return self.make_elt()
+
+ @classmethod
+ def from_exception(cls, exc, self_id = None):
+ """Generate a <report_error/> element from an exception."""
+ self = cls()
+ self.self_id = self_id
+ self.error_code = exc.__class__.__name__
+ return self
+
+class msg(list):
+ """Left-right PDU."""
+
+ ## @var version
+ # Protocol version
+ version = 1
+
+ ## @var pdus
+ # Dispatch table of PDUs for this protocol.
+ pdus = dict((x.element_name, x)
+ for x in (self_elt, child_elt, parent_elt, bsc_elt, repository_elt,
+ route_origin_elt, list_resources_elt, report_error_elt))
+
+ def startElement(self, stack, name, attrs):
+ """Handle left-right PDU."""
+ if name == "msg":
+ assert self.version == int(attrs["version"])
+ else:
+ elt = self.pdus[name]()
+ self.append(elt)
+ stack.append(elt)
+ elt.startElement(stack, name, attrs)
+
+ def endElement(self, stack, name, text):
+ """Handle left-right PDU."""
+ assert name == "msg", "Unexpected name %s, stack %s" % (name, stack)
+ assert len(stack) == 1
+ stack.pop()
+
+ def __str__(self):
+ """Convert msg object to string."""
+ lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "us-ascii")
+
+ def toXML(self):
+ """Generate left-right PDU."""
+ elt = lxml.etree.Element("{%s}msg" % (xmlns), nsmap = nsmap, version = str(self.version))
+ elt.extend([i.toXML() for i in self])
+ return elt
+
+ def serve_top_level(self, gctx):
+ """Serve one msg PDU."""
+ r_msg = self.__class__()
+ for q_pdu in self:
+ q_pdu.serve_dispatch(gctx, r_msg)
+ return r_msg
+
+class sax_handler(rpki.sax_utils.handler):
+ """SAX handler for Left-Right protocol."""
+
+ ## @var pdu
+ # Top-level PDU class
+ pdu = msg
+
+ def create_top_level(self, name, attrs):
+ """Top-level PDU for this protocol is <msg/>."""
+ assert name == "msg" and attrs["version"] == "1"
+ return self.pdu()
+
+def irdb_query(gctx, self_id, child_id = None):
+ """Perform an IRDB callback query. In the long run this should not
+ be a blocking routine, it should instead issue a query and set up a
+ handler to receive the response. For the moment, though, we are
+ doing simple lock step and damn the torpedos. Not yet doing
+ anything useful with subject name. Most likely this function should
+ really be wrapped up in a class that carries both the query result
+ and also the intermediate state needed for the event-driven code
+ that this function will need to become.
+ """
+
+ rpki.log.trace()
+
+ q_msg = msg()
+ q_msg.append(list_resources_elt())
+ q_msg[0].type = "query"
+ q_msg[0].self_id = self_id
+ q_msg[0].child_id = child_id
+ q_elt = q_msg.toXML()
+ rpki.relaxng.left_right.assertValid(q_elt)
+ q_cms = rpki.cms.xml_sign(q_elt, gctx.cms_key, gctx.cms_certs)
+ r_cms = rpki.https.client(
+ privateKey = gctx.https_key,
+ certChain = gctx.https_certs,
+ x509TrustList = gctx.https_ta,
+ url = gctx.irdb_url,
+ msg = q_cms)
+ r_elt = rpki.cms.xml_verify(r_cms, gctx.cms_ta_irdb)
+ rpki.relaxng.left_right.assertValid(r_elt)
+ r_msg = rpki.left_right.sax_handler.saxify(r_elt)
+ if len(r_msg) == 0 or not isinstance(r_msg[0], list_resources_elt) or r_msg[0].type != "reply":
+ raise rpki.exceptions.BadIRDBReply, "Unexpected response to IRDB query: %s" % lxml.etree.tostring(r_msg.toXML(), pretty_print = True, encoding = "us-ascii")
+ return rpki.resource_set.resource_bag(
+ as = r_msg[0].as,
+ v4 = r_msg[0].ipv4,
+ v6 = r_msg[0].ipv6,
+ valid_until = r_msg[0].valid_until)