From fe0bf509f528dbdc50c7182f81057c6a4e15e4bd Mon Sep 17 00:00:00 2001 From: Rob Austein Date: Sat, 5 Apr 2014 22:42:12 +0000 Subject: Source tree reorg, phase 1. Almost everything moved, no file contents changed. svn path=/branches/tk685/; revision=5757 --- rpki/up_down.py | 732 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 732 insertions(+) create mode 100644 rpki/up_down.py (limited to 'rpki/up_down.py') diff --git a/rpki/up_down.py b/rpki/up_down.py new file mode 100644 index 00000000..d2ad85d3 --- /dev/null +++ b/rpki/up_down.py @@ -0,0 +1,732 @@ +# $Id$ +# +# Copyright (C) 2013--2014 Dragon Research Labs ("DRL") +# Portions copyright (C) 2009--2012 Internet Systems Consortium ("ISC") +# Portions 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 notices and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND DRL, ISC, AND ARIN DISCLAIM ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL DRL, +# ISC, OR 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 "up-down" protocol. +""" + +import base64 +import lxml.etree +import rpki.resource_set +import rpki.x509 +import rpki.exceptions +import rpki.log +import rpki.xml_utils +import rpki.relaxng + +xmlns = "http://www.apnic.net/specs/rescerts/up-down/" + +nsmap = { None : xmlns } + +class base_elt(object): + """ + Generic PDU object. + + Virtual class, just provides some default methods. + """ + + def startElement(self, stack, name, attrs): + """ + Ignore startElement() if there's no specific handler. + + Some elements have no attributes and we only care about their + text content. + """ + pass + + def endElement(self, stack, name, text): + """ + Ignore endElement() if there's no specific handler. + + If we don't need to do anything else, just pop the stack. + """ + stack.pop() + + def make_elt(self, name, *attrs): + """ + Construct a element, copying over a set of attributes. + """ + elt = lxml.etree.Element("{%s}%s" % (xmlns, name), nsmap=nsmap) + for key in attrs: + val = getattr(self, key, None) + if val is not None: + elt.set(key, str(val)) + return elt + + def make_b64elt(self, elt, name, value): + """ + Construct a sub-element with Base64 text content. + """ + if value is not None and not value.empty(): + lxml.etree.SubElement(elt, "{%s}%s" % (xmlns, name), nsmap=nsmap).text = value.get_Base64() + + def serve_pdu(self, q_msg, r_msg, child, callback, errback): + """ + Default PDU handler to catch unexpected types. + """ + raise rpki.exceptions.BadQuery("Unexpected query type %s" % q_msg.type) + + def check_response(self): + """ + Placeholder for response checking. + """ + pass + +class multi_uri(list): + """ + Container for a set of URIs. + """ + + def __init__(self, ini): + """ + Initialize a set of URIs, which includes basic some syntax checking. + """ + list.__init__(self) + if isinstance(ini, (list, tuple)): + self[:] = ini + elif isinstance(ini, str): + self[:] = ini.split(",") + for s in self: + if s.strip() != s or "://" not in s: + raise rpki.exceptions.BadURISyntax("Bad URI \"%s\"" % s) + else: + raise TypeError + + def __str__(self): + """ + Convert a multi_uri back to a string representation. + """ + return ",".join(self) + + def rsync(self): + """ + Find first rsync://... URI in self. + """ + for s in self: + if s.startswith("rsync://"): + return s + return None + +class certificate_elt(base_elt): + """ + Up-Down protocol representation of an issued certificate. + """ + + def startElement(self, stack, name, attrs): + """ + Handle attributes of element. + """ + assert name == "certificate", "Unexpected name %s, stack %s" % (name, stack) + self.cert_url = multi_uri(attrs["cert_url"]) + self.req_resource_set_as = rpki.resource_set.resource_set_as(attrs.get("req_resource_set_as")) + self.req_resource_set_ipv4 = rpki.resource_set.resource_set_ipv4(attrs.get("req_resource_set_ipv4")) + self.req_resource_set_ipv6 = rpki.resource_set.resource_set_ipv6(attrs.get("req_resource_set_ipv6")) + + def endElement(self, stack, name, text): + """ + Handle text content of a element. + """ + assert name == "certificate", "Unexpected name %s, stack %s" % (name, stack) + self.cert = rpki.x509.X509(Base64 = text) + stack.pop() + + def toXML(self): + """ + Generate a element. + """ + elt = self.make_elt("certificate", "cert_url", + "req_resource_set_as", "req_resource_set_ipv4", "req_resource_set_ipv6") + elt.text = self.cert.get_Base64() + return elt + +class class_elt(base_elt): + """ + Up-Down protocol representation of a resource class. + """ + + issuer = None + + def __init__(self): + """ + Initialize class_elt. + """ + base_elt.__init__(self) + self.certs = [] + + def startElement(self, stack, name, attrs): + """ + Handle elements and their children. + """ + if name == "certificate": + cert = certificate_elt() + self.certs.append(cert) + stack.append(cert) + cert.startElement(stack, name, attrs) + elif name != "issuer": + assert name == "class", "Unexpected name %s, stack %s" % (name, stack) + self.class_name = attrs["class_name"] + self.cert_url = multi_uri(attrs["cert_url"]) + self.suggested_sia_head = attrs.get("suggested_sia_head") + self.resource_set_as = rpki.resource_set.resource_set_as(attrs["resource_set_as"]) + self.resource_set_ipv4 = rpki.resource_set.resource_set_ipv4(attrs["resource_set_ipv4"]) + self.resource_set_ipv6 = rpki.resource_set.resource_set_ipv6(attrs["resource_set_ipv6"]) + self.resource_set_notafter = rpki.sundial.datetime.fromXMLtime(attrs.get("resource_set_notafter")) + + def endElement(self, stack, name, text): + """ + Handle elements and their children. + """ + if name == "issuer": + self.issuer = rpki.x509.X509(Base64 = text) + else: + assert name == "class", "Unexpected name %s, stack %s" % (name, stack) + stack.pop() + + def toXML(self): + """ + Generate a element. + """ + elt = self.make_elt("class", "class_name", "cert_url", "resource_set_as", + "resource_set_ipv4", "resource_set_ipv6", + "resource_set_notafter", "suggested_sia_head") + elt.extend([i.toXML() for i in self.certs]) + self.make_b64elt(elt, "issuer", self.issuer) + return elt + + def to_resource_bag(self): + """ + Build a resource_bag from from this element. + """ + return rpki.resource_set.resource_bag(self.resource_set_as, + self.resource_set_ipv4, + self.resource_set_ipv6, + self.resource_set_notafter) + + def from_resource_bag(self, bag): + """ + Set resources of this class element from a resource_bag. + """ + self.resource_set_as = bag.asn + self.resource_set_ipv4 = bag.v4 + self.resource_set_ipv6 = bag.v6 + self.resource_set_notafter = bag.valid_until + +class list_pdu(base_elt): + """ + Up-Down protocol "list" PDU. + """ + + def toXML(self): + """Generate (empty) payload of "list" PDU.""" + return [] + + def serve_pdu(self, q_msg, r_msg, child, callback, errback): + """ + Serve one "list" PDU. + """ + + def handle(irdb_resources): + + r_msg.payload = list_response_pdu() + + if irdb_resources.valid_until < rpki.sundial.now(): + rpki.log.debug("Child %s's resources expired %s" % (child.child_handle, irdb_resources.valid_until)) + else: + for parent in child.parents: + for ca in parent.cas: + ca_detail = ca.active_ca_detail + if not ca_detail: + rpki.log.debug("No active ca_detail, can't issue to %s" % child.child_handle) + continue + resources = ca_detail.latest_ca_cert.get_3779resources() & irdb_resources + if resources.empty(): + rpki.log.debug("No overlap between received resources and what child %s should get ([%s], [%s])" % (child.child_handle, ca_detail.latest_ca_cert.get_3779resources(), irdb_resources)) + continue + rc = class_elt() + rc.class_name = str(ca.ca_id) + rc.cert_url = multi_uri(ca_detail.ca_cert_uri) + rc.from_resource_bag(resources) + for child_cert in child.fetch_child_certs(ca_detail = ca_detail): + c = certificate_elt() + c.cert_url = multi_uri(child_cert.uri) + c.cert = child_cert.cert + rc.certs.append(c) + rc.issuer = ca_detail.latest_ca_cert + r_msg.payload.classes.append(rc) + + callback() + + self.gctx.irdb_query_child_resources(child.self.self_handle, child.child_handle, handle, errback) + + @classmethod + def query(cls, parent, cb, eb): + """ + Send a "list" query to parent. + """ + try: + rpki.log.info('Sending "list" request to parent %s' % parent.parent_handle) + parent.query_up_down(cls(), cb, eb) + except (rpki.async.ExitNow, SystemExit): + raise + except Exception, e: + eb(e) + +class class_response_syntax(base_elt): + """ + Syntax for Up-Down protocol "list_response" and "issue_response" PDUs. + """ + + def __init__(self): + """ + Initialize class_response_syntax. + """ + base_elt.__init__(self) + self.classes = [] + + def startElement(self, stack, name, attrs): + """ + Handle "list_response" and "issue_response" PDUs. + """ + assert name == "class", "Unexpected name %s, stack %s" % (name, stack) + c = class_elt() + self.classes.append(c) + stack.append(c) + c.startElement(stack, name, attrs) + + def toXML(self): + """Generate payload of "list_response" and "issue_response" PDUs.""" + return [c.toXML() for c in self.classes] + +class list_response_pdu(class_response_syntax): + """ + Up-Down protocol "list_response" PDU. + """ + pass + +class issue_pdu(base_elt): + """ + Up-Down protocol "issue" PDU. + """ + + def startElement(self, stack, name, attrs): + """ + Handle "issue" PDU. + """ + assert name == "request", "Unexpected name %s, stack %s" % (name, stack) + self.class_name = attrs["class_name"] + self.req_resource_set_as = rpki.resource_set.resource_set_as(attrs.get("req_resource_set_as")) + self.req_resource_set_ipv4 = rpki.resource_set.resource_set_ipv4(attrs.get("req_resource_set_ipv4")) + self.req_resource_set_ipv6 = rpki.resource_set.resource_set_ipv6(attrs.get("req_resource_set_ipv6")) + + def endElement(self, stack, name, text): + """ + Handle "issue" PDU. + """ + assert name == "request", "Unexpected name %s, stack %s" % (name, stack) + self.pkcs10 = rpki.x509.PKCS10(Base64 = text) + stack.pop() + + def toXML(self): + """ + Generate payload of "issue" PDU. + """ + elt = self.make_elt("request", "class_name", "req_resource_set_as", + "req_resource_set_ipv4", "req_resource_set_ipv6") + elt.text = self.pkcs10.get_Base64() + return [elt] + + def serve_pdu(self, q_msg, r_msg, child, callback, errback): + """ + Serve one issue request PDU. + """ + + # Subsetting not yet implemented, this is the one place where we + # have to handle it, by reporting that we're lame. + + if self.req_resource_set_as or \ + self.req_resource_set_ipv4 or \ + self.req_resource_set_ipv6: + raise rpki.exceptions.NotImplementedYet("req_* attributes not implemented yet, sorry") + + # Check the request + self.pkcs10.check_valid_request_ca() + ca = child.ca_from_class_name(self.class_name) + ca_detail = ca.active_ca_detail + if ca_detail is None: + raise rpki.exceptions.NoActiveCA("No active CA for class %r" % self.class_name) + + # Check current cert, if any + + def got_resources(irdb_resources): + + if irdb_resources.valid_until < rpki.sundial.now(): + raise rpki.exceptions.IRDBExpired("IRDB entry for child %s expired %s" % ( + child.child_handle, irdb_resources.valid_until)) + + resources = irdb_resources & ca_detail.latest_ca_cert.get_3779resources() + resources.valid_until = irdb_resources.valid_until + req_key = self.pkcs10.getPublicKey() + req_sia = self.pkcs10.get_SIA() + child_cert = child.fetch_child_certs(ca_detail = ca_detail, ski = req_key.get_SKI(), unique = True) + + # Generate new cert or regenerate old one if necessary + + publisher = rpki.rpkid.publication_queue() + + if child_cert is None: + child_cert = ca_detail.issue( + ca = ca, + child = child, + subject_key = req_key, + sia = req_sia, + resources = resources, + publisher = publisher) + else: + child_cert = child_cert.reissue( + ca_detail = ca_detail, + sia = req_sia, + resources = resources, + publisher = publisher) + + def done(): + c = certificate_elt() + c.cert_url = multi_uri(child_cert.uri) + c.cert = child_cert.cert + rc = class_elt() + rc.class_name = self.class_name + rc.cert_url = multi_uri(ca_detail.ca_cert_uri) + rc.from_resource_bag(resources) + rc.certs.append(c) + rc.issuer = ca_detail.latest_ca_cert + r_msg.payload = issue_response_pdu() + r_msg.payload.classes.append(rc) + callback() + + self.gctx.sql.sweep() + assert child_cert and child_cert.sql_in_db + publisher.call_pubd(done, errback) + + self.gctx.irdb_query_child_resources(child.self.self_handle, child.child_handle, got_resources, errback) + + @classmethod + def query(cls, parent, ca, ca_detail, callback, errback): + """ + Send an "issue" request to parent associated with ca. + """ + assert ca_detail is not None and ca_detail.state in ("pending", "active") + self = cls() + self.class_name = ca.parent_resource_class + self.pkcs10 = rpki.x509.PKCS10.create( + keypair = ca_detail.private_key_id, + is_ca = True, + caRepository = ca.sia_uri, + rpkiManifest = ca_detail.manifest_uri) + rpki.log.info('Sending "issue" request to parent %s' % parent.parent_handle) + parent.query_up_down(self, callback, errback) + +class issue_response_pdu(class_response_syntax): + """ + Up-Down protocol "issue_response" PDU. + """ + + def check_response(self): + """ + Check whether this looks like a reasonable issue_response PDU. + XML schema should be tighter for this response. + """ + if len(self.classes) != 1 or len(self.classes[0].certs) != 1: + raise rpki.exceptions.BadIssueResponse + +class revoke_syntax(base_elt): + """ + Syntax for Up-Down protocol "revoke" and "revoke_response" PDUs. + """ + + def startElement(self, stack, name, attrs): + """Handle "revoke" PDU.""" + self.class_name = attrs["class_name"] + self.ski = attrs["ski"] + + def toXML(self): + """Generate payload of "revoke" PDU.""" + return [self.make_elt("key", "class_name", "ski")] + +class revoke_pdu(revoke_syntax): + """ + Up-Down protocol "revoke" PDU. + """ + + def get_SKI(self): + """ + Convert g(SKI) encoding from PDU back to raw SKI. + """ + return base64.urlsafe_b64decode(self.ski + "=") + + def serve_pdu(self, q_msg, r_msg, child, cb, eb): + """ + Serve one revoke request PDU. + """ + + def done(): + r_msg.payload = revoke_response_pdu() + r_msg.payload.class_name = self.class_name + r_msg.payload.ski = self.ski + cb() + + ca = child.ca_from_class_name(self.class_name) + publisher = rpki.rpkid.publication_queue() + for ca_detail in ca.ca_details: + for child_cert in child.fetch_child_certs(ca_detail = ca_detail, ski = self.get_SKI()): + child_cert.revoke(publisher = publisher) + self.gctx.sql.sweep() + publisher.call_pubd(done, eb) + + @classmethod + def query(cls, ca, gski, cb, eb): + """ + Send a "revoke" request for certificate(s) named by gski to parent associated with ca. + """ + parent = ca.parent + self = cls() + self.class_name = ca.parent_resource_class + self.ski = gski + rpki.log.info('Sending "revoke" request for SKI %s to parent %s' % (gski, parent.parent_handle)) + parent.query_up_down(self, cb, eb) + +class revoke_response_pdu(revoke_syntax): + """ + Up-Down protocol "revoke_response" PDU. + """ + + pass + +class error_response_pdu(base_elt): + """ + Up-Down protocol "error_response" PDU. + """ + + codes = { + 1101 : "Already processing request", + 1102 : "Version number error", + 1103 : "Unrecognised request type", + 1201 : "Request - no such resource class", + 1202 : "Request - no resources allocated in resource class", + 1203 : "Request - badly formed certificate request", + 1301 : "Revoke - no such resource class", + 1302 : "Revoke - no such key", + 2001 : "Internal Server Error - Request not performed" } + + exceptions = { + rpki.exceptions.NoActiveCA : 1202, + (rpki.exceptions.ClassNameUnknown, revoke_pdu) : 1301, + rpki.exceptions.ClassNameUnknown : 1201, + (rpki.exceptions.NotInDatabase, revoke_pdu) : 1302 } + + def __init__(self, exception = None, request_payload = None): + """ + Initialize an error_response PDU from an exception object. + """ + base_elt.__init__(self) + if exception is not None: + rpki.log.debug("Constructing up-down error response from exception %s" % exception) + exception_type = type(exception) + request_type = None if request_payload is None else type(request_payload) + rpki.log.debug("Constructing up-down error response: exception_type %s, request_type %s" % ( + exception_type, request_type)) + if False: + self.status = self.exceptions.get((exception_type, request_type), + self.exceptions.get(exception_type, + 2001)) + else: + self.status = self.exceptions.get((exception_type, request_type)) + if self.status is None: + rpki.log.debug("No request-type-specific match, trying exception match") + self.status = self.exceptions.get(exception_type) + if self.status is None: + rpki.log.debug("No exception match either, defaulting") + self.status = 2001 + self.description = str(exception) + rpki.log.debug("Chosen status code: %s" % self.status) + + def endElement(self, stack, name, text): + """ + Handle "error_response" PDU. + """ + if name == "status": + code = int(text) + if code not in self.codes: + raise rpki.exceptions.BadStatusCode("%s is not a known status code" % code) + self.status = code + elif name == "description": + self.description = text + else: + assert name == "message", "Unexpected name %s, stack %s" % (name, stack) + stack.pop() + stack[-1].endElement(stack, name, text) + + def toXML(self): + """ + Generate payload of "error_response" PDU. + """ + assert self.status in self.codes + elt = self.make_elt("status") + elt.text = str(self.status) + payload = [elt] + if self.description: + elt = self.make_elt("description") + elt.text = str(self.description) + elt.set("{http://www.w3.org/XML/1998/namespace}lang", "en-US") + payload.append(elt) + return payload + + def check_response(self): + """ + Handle an error response. For now, just raise an exception, + perhaps figure out something more clever to do later. + """ + raise rpki.exceptions.UpstreamError(self.codes[self.status]) + +class message_pdu(base_elt): + """ + Up-Down protocol message wrapper PDU. + """ + + version = 1 + + name2type = { + "list" : list_pdu, + "list_response" : list_response_pdu, + "issue" : issue_pdu, + "issue_response" : issue_response_pdu, + "revoke" : revoke_pdu, + "revoke_response" : revoke_response_pdu, + "error_response" : error_response_pdu } + + type2name = dict((v, k) for k, v in name2type.items()) + + error_pdu_type = error_response_pdu + + def toXML(self): + """ + Generate payload of message PDU. + """ + elt = self.make_elt("message", "version", "sender", "recipient", "type") + elt.extend(self.payload.toXML()) + return elt + + def startElement(self, stack, name, attrs): + """ + Handle message PDU. + + Payload of the element varies depending on the "type" + attribute, so after some basic checks we have to instantiate the + right class object to handle whatever kind of PDU this is. + """ + assert name == "message", "Unexpected name %s, stack %s" % (name, stack) + assert self.version == int(attrs["version"]) + self.sender = attrs["sender"] + self.recipient = attrs["recipient"] + self.type = attrs["type"] + self.payload = self.name2type[attrs["type"]]() + stack.append(self.payload) + + def __str__(self): + """ + Convert a message PDU to a string. + """ + return lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "UTF-8") + + def serve_top_level(self, child, callback): + """ + Serve one message request PDU. + """ + + r_msg = message_pdu() + r_msg.sender = self.recipient + r_msg.recipient = self.sender + + def done(): + r_msg.type = self.type2name[type(r_msg.payload)] + callback(r_msg) + + def lose(e): + rpki.log.traceback() + callback(self.serve_error(e)) + + try: + self.log_query(child) + self.payload.serve_pdu(self, r_msg, child, done, lose) + except (rpki.async.ExitNow, SystemExit): + raise + except Exception, e: + lose(e) + + def log_query(self, child): + """ + Log query we're handling. Separate method so rootd can override. + """ + rpki.log.info("Serving %s query from child %s [sender %s, recipient %s]" % (self.type, child.child_handle, self.sender, self.recipient)) + + def serve_error(self, exception): + """ + Generate an error_response message PDU. + """ + r_msg = message_pdu() + r_msg.sender = self.recipient + r_msg.recipient = self.sender + r_msg.payload = self.error_pdu_type(exception, self.payload) + r_msg.type = self.type2name[type(r_msg.payload)] + return r_msg + + @classmethod + def make_query(cls, payload, sender, recipient): + """ + Construct one message PDU. + """ + assert not cls.type2name[type(payload)].endswith("_response") + if sender is None: + sender = "tweedledee" + if recipient is None: + recipient = "tweedledum" + self = cls() + self.sender = sender + self.recipient = recipient + self.payload = payload + self.type = self.type2name[type(payload)] + return self + +class sax_handler(rpki.xml_utils.sax_handler): + """ + SAX handler for Up-Down protocol. + """ + + pdu = message_pdu + name = "message" + version = "1" + +class cms_msg(rpki.x509.XML_CMS_object): + """ + Class to hold a CMS-signed up-down PDU. + """ + + encoding = "UTF-8" + schema = rpki.relaxng.up_down + saxify = sax_handler.saxify + allow_extra_certs = True + allow_extra_crls = True -- cgit v1.2.3