00001 """RPKI "up-down" protocol.
00002
00003 $Id: up_down.py 2000 2008-07-16 00:58:32Z sra $
00004
00005 Copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
00006
00007 Permission to use, copy, modify, and distribute this software for any
00008 purpose with or without fee is hereby granted, provided that the above
00009 copyright notice and this permission notice appear in all copies.
00010
00011 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
00012 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00013 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
00014 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00015 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00016 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00017 PERFORMANCE OF THIS SOFTWARE.
00018 """
00019
00020 import base64, lxml.etree, time
00021 import rpki.resource_set, rpki.x509, rpki.exceptions
00022 import rpki.xml_utils, rpki.relaxng
00023
00024 xmlns="http://www.apnic.net/specs/rescerts/up-down/"
00025
00026 nsmap = { None : xmlns }
00027
00028 class base_elt(object):
00029 """Generic PDU object.
00030
00031 Virtual class, just provides some default methods.
00032 """
00033
00034 def startElement(self, stack, name, attrs):
00035 """Ignore startElement() if there's no specific handler.
00036
00037 Some elements have no attributes and we only care about their
00038 text content.
00039 """
00040 pass
00041
00042 def endElement(self, stack, name, text):
00043 """Ignore endElement() if there's no specific handler.
00044
00045 If we don't need to do anything else, just pop the stack.
00046 """
00047 stack.pop()
00048
00049 def make_elt(self, name, *attrs):
00050 """Construct a element, copying over a set of attributes."""
00051 elt = lxml.etree.Element("{%s}%s" % (xmlns, name), nsmap=nsmap)
00052 for key in attrs:
00053 val = getattr(self, key, None)
00054 if val is not None:
00055 elt.set(key, str(val))
00056 return elt
00057
00058 def make_b64elt(self, elt, name, value=None):
00059 """Construct a sub-element with Base64 text content."""
00060 if value is None:
00061 value = getattr(self, name, None)
00062 if value is not None:
00063 lxml.etree.SubElement(elt, "{%s}%s" % (xmlns, name), nsmap=nsmap).text = base64.b64encode(value)
00064
00065 def serve_pdu(self, q_msg, r_msg, child):
00066 """Default PDU handler to catch unexpected types."""
00067 raise rpki.exceptions.BadQuery, "Unexpected query type %s" % q_msg.type
00068
00069 def check_response(self):
00070 """Placeholder for response checking."""
00071 pass
00072
00073 class multi_uri(list):
00074 """Container for a set of URIs."""
00075
00076 def __init__(self, ini):
00077 """Initialize a set of URIs, which includes basic some syntax checking."""
00078 if isinstance(ini, (list, tuple)):
00079 self[:] = ini
00080 elif isinstance(ini, str):
00081 self[:] = ini.split(",")
00082 for s in self:
00083 if s.strip() != s or s.find("://") < 0:
00084 raise rpki.exceptions.BadURISyntax, "Bad URI \"%s\"" % s
00085 else:
00086 raise TypeError
00087
00088 def __str__(self):
00089 """Convert a multi_uri back to a string representation."""
00090 return ",".join(self)
00091
00092 def rsync(self):
00093 """Find first rsync://... URI in self."""
00094 for s in self:
00095 if s.startswith("rsync://"):
00096 return s
00097 return None
00098
00099 class certificate_elt(base_elt):
00100 """Up-Down protocol representation of an issued certificate."""
00101
00102 def startElement(self, stack, name, attrs):
00103 """Handle attributes of <certificate/> element."""
00104 assert name == "certificate", "Unexpected name %s, stack %s" % (name, stack)
00105 self.cert_url = multi_uri(attrs["cert_url"])
00106 self.req_resource_set_as = rpki.resource_set.resource_set_as(attrs.get("req_resource_set_as"))
00107 self.req_resource_set_ipv4 = rpki.resource_set.resource_set_ipv4(attrs.get("req_resource_set_ipv4"))
00108 self.req_resource_set_ipv6 = rpki.resource_set.resource_set_ipv6(attrs.get("req_resource_set_ipv6"))
00109
00110 def endElement(self, stack, name, text):
00111 """Handle text content of a <certificate/> element."""
00112 assert name == "certificate", "Unexpected name %s, stack %s" % (name, stack)
00113 self.cert = rpki.x509.X509(Base64=text)
00114 stack.pop()
00115
00116 def toXML(self):
00117 """Generate a <certificate/> element."""
00118 elt = self.make_elt("certificate", "cert_url",
00119 "req_resource_set_as", "req_resource_set_ipv4", "req_resource_set_ipv6")
00120 elt.text = self.cert.get_Base64()
00121 return elt
00122
00123 class class_elt(base_elt):
00124 """Up-Down protocol representation of a resource class."""
00125
00126 issuer = None
00127
00128 def __init__(self):
00129 """Initialize class_elt."""
00130 self.certs = []
00131
00132 def startElement(self, stack, name, attrs):
00133 """Handle <class/> elements and their children."""
00134 if name == "certificate":
00135 cert = certificate_elt()
00136 self.certs.append(cert)
00137 stack.append(cert)
00138 cert.startElement(stack, name, attrs)
00139 elif name != "issuer":
00140 assert name == "class", "Unexpected name %s, stack %s" % (name, stack)
00141 self.class_name = attrs["class_name"]
00142 self.cert_url = multi_uri(attrs["cert_url"])
00143 self.suggested_sia_head = attrs.get("suggested_sia_head")
00144 self.resource_set_as = rpki.resource_set.resource_set_as(attrs["resource_set_as"])
00145 self.resource_set_ipv4 = rpki.resource_set.resource_set_ipv4(attrs["resource_set_ipv4"])
00146 self.resource_set_ipv6 = rpki.resource_set.resource_set_ipv6(attrs["resource_set_ipv6"])
00147 self.resource_set_notafter = rpki.sundial.datetime.fromXMLtime(attrs.get("resource_set_notafter"))
00148
00149 def endElement(self, stack, name, text):
00150 """Handle <class/> elements and their children."""
00151 if name == "issuer":
00152 self.issuer = rpki.x509.X509(Base64=text)
00153 else:
00154 assert name == "class", "Unexpected name %s, stack %s" % (name, stack)
00155 stack.pop()
00156
00157 def toXML(self):
00158 """Generate a <class/> element."""
00159 elt = self.make_elt("class", "class_name", "cert_url", "resource_set_as",
00160 "resource_set_ipv4", "resource_set_ipv6",
00161 "resource_set_notafter", "suggested_sia_head")
00162 elt.extend([i.toXML() for i in self.certs])
00163 if self.issuer is not None:
00164 self.make_b64elt(elt, "issuer", self.issuer.get_DER())
00165 return elt
00166
00167 def to_resource_bag(self):
00168 """Build a resource_bag from from this <class/> element."""
00169 return rpki.resource_set.resource_bag(self.resource_set_as,
00170 self.resource_set_ipv4,
00171 self.resource_set_ipv6,
00172 self.resource_set_notafter)
00173
00174 def from_resource_bag(self, bag):
00175 """Set resources of this class element from a resource_bag."""
00176 self.resource_set_as = bag.asn
00177 self.resource_set_ipv4 = bag.v4
00178 self.resource_set_ipv6 = bag.v6
00179 self.resource_set_notafter = bag.valid_until
00180
00181 class list_pdu(base_elt):
00182 """Up-Down protocol "list" PDU."""
00183
00184 def toXML(self):
00185 """Generate (empty) payload of "list" PDU."""
00186 return []
00187
00188 def serve_pdu(self, q_msg, r_msg, child):
00189 """Serve one "list" PDU."""
00190 r_msg.payload = list_response_pdu()
00191
00192
00193 irdb_resources = self.gctx.irdb_query(child.self_id, child.child_id)
00194
00195 for parent in child.parents():
00196 for ca in parent.cas():
00197 ca_detail = ca.fetch_active()
00198 if not ca_detail:
00199 continue
00200 resources = ca_detail.latest_ca_cert.get_3779resources().intersection(irdb_resources)
00201 if resources.empty():
00202 continue
00203 rc = class_elt()
00204 rc.class_name = str(ca.ca_id)
00205 rc.cert_url = multi_uri(ca_detail.ca_cert_uri)
00206 rc.from_resource_bag(resources)
00207 for child_cert in child.child_certs(ca_detail = ca_detail):
00208 c = certificate_elt()
00209 c.cert_url = multi_uri(child_cert.uri(ca))
00210 c.cert = child_cert.cert
00211 rc.certs.append(c)
00212 rc.issuer = ca_detail.latest_ca_cert
00213 r_msg.payload.classes.append(rc)
00214
00215 @classmethod
00216 def query(cls, parent):
00217 """Send a "list" query to parent."""
00218 return parent.query_up_down(cls())
00219
00220 class class_response_syntax(base_elt):
00221 """Syntax for Up-Down protocol "list_response" and "issue_response" PDUs."""
00222
00223 def __init__(self):
00224 """Initialize class_response_syntax."""
00225 self.classes = []
00226
00227 def startElement(self, stack, name, attrs):
00228 """Handle "list_response" and "issue_response" PDUs."""
00229 assert name == "class", "Unexpected name %s, stack %s" % (name, stack)
00230 c = class_elt()
00231 self.classes.append(c)
00232 stack.append(c)
00233 c.startElement(stack, name, attrs)
00234
00235 def toXML(self):
00236 """Generate payload of "list_response" and "issue_response" PDUs."""
00237 return [c.toXML() for c in self.classes]
00238
00239 class list_response_pdu(class_response_syntax):
00240 """Up-Down protocol "list_response" PDU."""
00241
00242 pass
00243
00244 class issue_pdu(base_elt):
00245 """Up-Down protocol "issue" PDU."""
00246
00247 def startElement(self, stack, name, attrs):
00248 """Handle "issue" PDU."""
00249 assert name == "request", "Unexpected name %s, stack %s" % (name, stack)
00250 self.class_name = attrs["class_name"]
00251 self.req_resource_set_as = rpki.resource_set.resource_set_as(attrs.get("req_resource_set_as"))
00252 self.req_resource_set_ipv4 = rpki.resource_set.resource_set_ipv4(attrs.get("req_resource_set_ipv4"))
00253 self.req_resource_set_ipv6 = rpki.resource_set.resource_set_ipv6(attrs.get("req_resource_set_ipv6"))
00254
00255 def endElement(self, stack, name, text):
00256 """Handle "issue" PDU."""
00257 assert name == "request", "Unexpected name %s, stack %s" % (name, stack)
00258 self.pkcs10 = rpki.x509.PKCS10(Base64=text)
00259 stack.pop()
00260
00261 def toXML(self):
00262 """Generate payload of "issue" PDU."""
00263 elt = self.make_elt("request", "class_name", "req_resource_set_as",
00264 "req_resource_set_ipv4", "req_resource_set_ipv6")
00265 elt.text = self.pkcs10.get_Base64()
00266 return [elt]
00267
00268 def serve_pdu(self, q_msg, r_msg, child):
00269 """Serve one issue request PDU."""
00270
00271
00272
00273
00274 if self.req_resource_set_as or \
00275 self.req_resource_set_ipv4 or \
00276 self.req_resource_set_ipv6:
00277 raise rpki.exceptions.NotImplementedYet, "req_* attributes not implemented yet, sorry"
00278
00279
00280 self.pkcs10.check_valid_rpki()
00281 ca = child.ca_from_class_name(self.class_name)
00282 ca_detail = ca.fetch_active()
00283 if ca_detail is None:
00284 raise rpki.exceptions.NoActiveCA, "No active CA for class %s" % self.class_name
00285
00286
00287
00288
00289 irdb_resources = self.gctx.irdb_query(child.self_id, child.child_id)
00290
00291 resources = irdb_resources.intersection(ca_detail.latest_ca_cert.get_3779resources())
00292 req_key = self.pkcs10.getPublicKey()
00293 req_sia = self.pkcs10.get_SIA()
00294 child_cert = child.child_certs(ca_detail = ca_detail, ski = req_key.get_SKI(), unique = True)
00295
00296
00297
00298 if child_cert is None:
00299 child_cert = ca_detail.issue(
00300 ca = ca,
00301 child = child,
00302 subject_key = req_key,
00303 sia = req_sia,
00304 resources = resources)
00305 else:
00306 child_cert = child_cert.reissue(
00307 ca_detail = ca_detail,
00308 sia = req_sia,
00309 resources = resources)
00310
00311
00312 self.gctx.sql.sweep()
00313 assert child_cert and child_cert.sql_in_db
00314 c = certificate_elt()
00315 c.cert_url = multi_uri(child_cert.uri(ca))
00316 c.cert = child_cert.cert
00317 rc = class_elt()
00318 rc.class_name = self.class_name
00319 rc.cert_url = multi_uri(ca_detail.ca_cert_uri)
00320 rc.from_resource_bag(resources)
00321 rc.certs.append(c)
00322 rc.issuer = ca_detail.latest_ca_cert
00323 r_msg.payload = issue_response_pdu()
00324 r_msg.payload.classes.append(rc)
00325
00326 @classmethod
00327 def query(cls, parent, ca, ca_detail):
00328 """Send an "issue" request to parent associated with ca."""
00329 assert ca_detail is not None and ca_detail.state in ("pending", "active")
00330 sia = ((rpki.oids.name2oid["id-ad-caRepository"], ("uri", ca.sia_uri)),
00331 (rpki.oids.name2oid["id-ad-rpkiManifest"], ("uri", ca_detail.manifest_uri(ca))))
00332 self = cls()
00333 self.class_name = ca.parent_resource_class
00334 self.pkcs10 = rpki.x509.PKCS10.create_ca(ca_detail.private_key_id, sia)
00335 return parent.query_up_down(self)
00336
00337 class issue_response_pdu(class_response_syntax):
00338 """Up-Down protocol "issue_response" PDU."""
00339
00340 def check_response(self):
00341 """Check whether this looks like a reasonable issue_response PDU.
00342 XML schema should be tighter for this response.
00343 """
00344 if len(self.classes) != 1 or len(self.classes[0].certs) != 1:
00345 raise rpki.exceptions.BadIssueResponse
00346
00347 class revoke_syntax(base_elt):
00348 """Syntax for Up-Down protocol "revoke" and "revoke_response" PDUs."""
00349
00350 def startElement(self, stack, name, attrs):
00351 """Handle "revoke" PDU."""
00352 self.class_name = attrs["class_name"]
00353 self.ski = attrs["ski"]
00354
00355 def toXML(self):
00356 """Generate payload of "revoke" PDU."""
00357 return [self.make_elt("key", "class_name", "ski")]
00358
00359 class revoke_pdu(revoke_syntax):
00360 """Up-Down protocol "revoke" PDU."""
00361
00362 def get_SKI(self):
00363 """Convert g(SKI) encoding from PDU back to raw SKI."""
00364 return base64.urlsafe_b64decode(self.ski + "=")
00365
00366 def serve_pdu(self, q_msg, r_msg, child):
00367 """Serve one revoke request PDU."""
00368 for ca_detail in child.ca_from_class_name(self.class_name).ca_details():
00369 for child_cert in child.child_certs(ca_detail = ca_detail, ski = self.get_SKI()):
00370 child_cert.revoke()
00371 self.gctx.sql.sweep()
00372 r_msg.payload = revoke_response_pdu()
00373 r_msg.payload.class_name = self.class_name
00374 r_msg.payload.ski = self.ski
00375
00376 @classmethod
00377 def query(cls, ca_detail):
00378 """Send a "revoke" request to parent associated with ca_detail."""
00379 ca = ca_detail.ca()
00380 parent = ca.parent()
00381 self = cls()
00382 self.class_name = ca.parent_resource_class
00383 self.ski = ca_detail.latest_ca_cert.gSKI()
00384 return parent.query_up_down(self)
00385
00386 class revoke_response_pdu(revoke_syntax):
00387 """Up-Down protocol "revoke_response" PDU."""
00388
00389 pass
00390
00391 class error_response_pdu(base_elt):
00392 """Up-Down protocol "error_response" PDU."""
00393
00394 codes = {
00395 1101 : "Already processing request",
00396 1102 : "Version number error",
00397 1103 : "Unrecognised request type",
00398 1201 : "Request - no such resource class",
00399 1202 : "Request - no resources allocated in resource class",
00400 1203 : "Request - badly formed certificate request",
00401 1301 : "Revoke - no such resource class",
00402 1302 : "Revoke - no such key",
00403 2001 : "Internal Server Error - Request not performed" }
00404
00405 exceptions = {}
00406
00407 def __init__(self, exception = None):
00408 """Initialize an error_response PDU from an exception object."""
00409 if exception is not None:
00410 if exception in self.exceptions:
00411 self.status = exceptions[exception]
00412 else:
00413 self.status = 2001
00414 self.description = str(exception)
00415
00416 def endElement(self, stack, name, text):
00417 """Handle "error_response" PDU."""
00418 if name == "status":
00419 code = int(text)
00420 if code not in self.codes:
00421 raise rpki.exceptions.BadStatusCode, "%s is not a known status code"
00422 self.status = code
00423 elif name == "description":
00424 self.description = text
00425 else:
00426 assert name == "message", "Unexpected name %s, stack %s" % (name, stack)
00427 stack.pop()
00428 stack[-1].endElement(stack, name, text)
00429
00430 def toXML(self):
00431 """Generate payload of "error_response" PDU."""
00432 assert self.status in self.codes
00433 elt = self.make_elt("status")
00434 elt.text = str(self.status)
00435 payload = [elt]
00436 if self.description:
00437 elt = self.make_elt("description")
00438 elt.text = str(self.description)
00439 elt.set("{http://www.w3.org/XML/1998/namespace}lang", "en-US")
00440 payload.append(elt)
00441 return payload
00442
00443 def check_response(self):
00444 """Handle an error response. For now, just raise an exception,
00445 perhaps figure out something more clever to do later.
00446 """
00447 raise rpki.exceptions.UpstreamError, self.codes[self.status]
00448
00449 class message_pdu(base_elt):
00450 """Up-Down protocol message wrapper PDU."""
00451
00452 version = 1
00453
00454 name2type = {
00455 "list" : list_pdu,
00456 "list_response" : list_response_pdu,
00457 "issue" : issue_pdu,
00458 "issue_response" : issue_response_pdu,
00459 "revoke" : revoke_pdu,
00460 "revoke_response" : revoke_response_pdu,
00461 "error_response" : error_response_pdu }
00462
00463 type2name = dict((v,k) for k,v in name2type.items())
00464
00465 def toXML(self):
00466 """Generate payload of message PDU."""
00467 elt = self.make_elt("message", "version", "sender", "recipient", "type")
00468 elt.extend(self.payload.toXML())
00469 return elt
00470
00471 def startElement(self, stack, name, attrs):
00472 """Handle message PDU.
00473
00474 Payload of the <message/> element varies depending on the "type"
00475 attribute, so after some basic checks we have to instantiate the
00476 right class object to handle whatever kind of PDU this is.
00477 """
00478 assert name == "message", "Unexpected name %s, stack %s" % (name, stack)
00479 assert self.version == int(attrs["version"])
00480 self.sender = attrs["sender"]
00481 self.recipient = attrs["recipient"]
00482 self.type = attrs["type"]
00483 self.payload = self.name2type[attrs["type"]]()
00484 stack.append(self.payload)
00485
00486 def __str__(self):
00487 """Convert a message PDU to a string."""
00488 lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "UTF-8")
00489
00490 def serve_top_level(self, child):
00491 """Serve one message request PDU."""
00492 r_msg = message_pdu()
00493 r_msg.sender = self.recipient
00494 r_msg.recipient = self.sender
00495 self.payload.serve_pdu(self, r_msg, child)
00496 r_msg.type = self.type2name[type(r_msg.payload)]
00497 return r_msg
00498
00499 def serve_error(self, exception):
00500 """Generate an error_response message PDU."""
00501 r_msg = message_pdu()
00502 r_msg.sender = self.recipient
00503 r_msg.recipient = self.sender
00504 r_msg.payload = error_response_pdu(exception)
00505 r_msg.type = self.type2name[type(r_msg.payload)]
00506 return r_msg
00507
00508 @classmethod
00509 def make_query(cls, payload, sender, recipient):
00510 """Construct one message PDU."""
00511 assert not cls.type2name[type(payload)].endswith("_response")
00512 if sender is None:
00513 sender = "tweedledee"
00514 if recipient is None:
00515 recipient = "tweedledum"
00516 self = cls()
00517 self.sender = sender
00518 self.recipient = recipient
00519 self.payload = payload
00520 self.type = self.type2name[type(payload)]
00521 return self
00522
00523 class sax_handler(rpki.xml_utils.sax_handler):
00524 """SAX handler for Up-Down protocol."""
00525
00526 pdu = message_pdu
00527 name = "message"
00528 version = "1"
00529
00530 class cms_msg(rpki.x509.XML_CMS_object):
00531 """Class to hold a CMS-signed up-down PDU."""
00532
00533 encoding = "UTF-8"
00534 schema = rpki.relaxng.up_down
00535 saxify = sax_handler.saxify