diff options
Diffstat (limited to 'rpki/up_down.py')
-rw-r--r-- | rpki/up_down.py | 745 |
1 files changed, 87 insertions, 658 deletions
diff --git a/rpki/up_down.py b/rpki/up_down.py index 5339e9a7..e2292efb 100644 --- a/rpki/up_down.py +++ b/rpki/up_down.py @@ -21,520 +21,78 @@ RPKI "up-down" protocol. """ -import base64 import logging -import lxml.etree import rpki.resource_set import rpki.x509 import rpki.exceptions import rpki.log -import rpki.xml_utils import rpki.relaxng +from lxml.etree import SubElement, tostring as ElementToString + logger = logging.getLogger(__name__) -xmlns = rpki.relaxng.up_down.xmlns -nsmap = rpki.relaxng.up_down.nsmap +xmlns = rpki.relaxng.up_down.xmlns +nsmap = rpki.relaxng.up_down.nsmap +version = "1" ## @var content_type # MIME content type to use when sending up-down queries. -#content_type = "application/rpki-updown" -content_type = "application/x-rpki" +content_type = "application/rpki-updown" +#content_type = "application/x-rpki" ## @var allowed_content_types # MIME content types which we consider acceptable for incoming up-down # queries. allowed_content_types = ("application/rpki-updown", "application/x-rpki") -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() +## @var enforce_strict_up_down_xml_sender +# Enforce strict checking of XML "sender" field in up-down protocol - def make_elt(self, name, *attrs): - """ - Construct a element, copying over a set of attributes. - """ - elt = lxml.etree.Element(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, xmlns + name, nsmap = nsmap).text = value.get_Base64() +enforce_strict_up_down_xml_sender = False - 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) +tag_certificate = xmlns + "certificate" +tag_class = xmlns + "class" +tag_description = xmlns + "description" +tag_issuer = xmlns + "issuer" +tag_key = xmlns + "key" +tag_message = xmlns + "message" +tag_request = xmlns + "request" +tag_status = xmlns + "status" - 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. + Container for a set of URIs. This probably could be simplified. """ - return ",".join(self) - def rsync(self): - """ - Find first rsync://... URI in self. - """ - for s in self: - if s.startswith("rsync://"): - return s - return None + def __init__(self, ini): + 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 -class certificate_elt(base_elt): - """ - Up-Down protocol representation of an issued certificate. - """ + def __str__(self): + return ",".join(self) - def startElement(self, stack, name, attrs): - """ - Handle attributes of <certificate/> 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 rsync(self): + """ + Find first rsync://... URI in self. + """ - def endElement(self, stack, name, text): - """ - Handle text content of a <certificate/> element. - """ - assert name == "certificate", "Unexpected name %s, stack %s" % (name, stack) - self.cert = rpki.x509.X509(Base64 = text) - stack.pop() + for s in self: + if s.startswith("rsync://"): + return s + return None - def toXML(self): - """ - Generate a <certificate/> 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 <class/> 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 <class/> 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 <class/> 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 <class/> 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(): - logger.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: - logger.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(): - logger.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: - logger.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) - logger.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 - logger.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 = { +error_response_codes = { 1101 : "Already processing request", 1102 : "Version number error", 1103 : "Unrecognised request type", @@ -545,200 +103,71 @@ class error_response_pdu(base_elt): 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: - logger.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) - logger.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: - logger.debug("No request-type-specific match, trying exception match") - self.status = self.exceptions.get(exception_type) - if self.status is None: - logger.debug("No exception match either, defaulting") - self.status = 2001 - self.description = str(exception) - logger.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): +exception_map = { + rpki.exceptions.NoActiveCA : 1202, + (rpki.exceptions.ClassNameUnknown, "revoke") : 1301, + rpki.exceptions.ClassNameUnknown : 1201, + (rpki.exceptions.NotInDatabase, "revoke") : 1302 } + + +def check_response(r_msg, q_type): """ - Handle an error response. For now, just raise an exception, - perhaps figure out something more clever to do later. + Additional checks beyond the XML schema for whether this looks like + a reasonable up-down response message. """ - raise rpki.exceptions.UpstreamError(self.codes[self.status]) -class message_pdu(base_elt): - """ - Up-Down protocol message wrapper PDU. - """ + r_type = r_msg.get("type") - version = 1 + if r_type == "error_response": + raise rpki.exceptions.UpstreamError(error_response_codes[int(r_msg.findtext(tag_status))]) - 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 } + if r_type != q_type + "_response": + raise rpki.exceptions.UnexpectedUpDownResponse - type2name = dict((v, k) for k, v in name2type.items()) + if r_type == "issue_response" and (len(r_msg) != 1 or len(r_msg[0]) != 2): + logger.debug("Weird issue_response %r: len(r_msg) %s len(r_msg[0]) %s", + r_msg, len(r_msg), len(r_msg[0]) if len(r_msg) else None) + logger.debug("Offending message\n%s", ElementToString(r_msg)) + raise rpki.exceptions.BadIssueResponse - error_pdu_type = error_response_pdu - def toXML(self): +def generate_error_response(r_msg, status = 2001, description = None): """ - Generate payload of message PDU. + Generate an error response. If status is given, it specifies the + numeric code to use, otherwise we default to "internal error". + If description is specified, we use it as the description, otherwise + we just use the default string associated with status. """ - 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. + assert status in error_response_codes + del r_msg[:] + r_msg.set("type", "error_response") + SubElement(r_msg, tag_status).text = str(status) + se = SubElement(r_msg, tag_description) + se.set("{http://www.w3.org/XML/1998/namespace}lang", "en-US") + se.text = str(description or error_response_codes[status]) - Payload of the <message/> 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): +def generate_error_response_from_exception(r_msg, e, q_type): """ - Serve one message request PDU. + Construct an error response from an exception. q_type + specifies the kind of query to which this is a response, since the + same exception can generate different codes in response to different + queries. """ - r_msg = message_pdu() - r_msg.sender = self.recipient - r_msg.recipient = self.sender + t = type(e) + code = (exception_map.get((t, q_type)) or exception_map.get(t) or 2001) + generate_error_response(r_msg, code, e) - def done(): - r_msg.type = self.type2name[type(r_msg.payload)] - callback(r_msg) - def lose(e): - logger.exception("Unhandled exception serving child %r", child) - 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. - """ - logger.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): +class cms_msg(rpki.x509.XML_CMS_object): """ - Construct one message PDU. + CMS-signed up-down 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 + encoding = "UTF-8" + schema = rpki.relaxng.up_down + allow_extra_certs = True + allow_extra_crls = True |