00001 """RPKI "up-down" protocol.
00002
00003 $Id: up_down.py 1873 2008-06-12 02:49:41Z 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 ca = child.ca_from_class_name(self.class_name)
00281 ca_detail = ca.fetch_active()
00282 self.pkcs10.check_valid_rpki()
00283
00284
00285
00286
00287 irdb_resources = self.gctx.irdb_query(child.self_id, child.child_id)
00288
00289 resources = irdb_resources.intersection(ca_detail.latest_ca_cert.get_3779resources())
00290 req_key = self.pkcs10.getPublicKey()
00291 req_sia = self.pkcs10.get_SIA()
00292 child_cert = child.child_certs(ca_detail = ca_detail, ski = req_key.get_SKI(), unique = True)
00293
00294
00295
00296 if child_cert is None:
00297 child_cert = ca_detail.issue(
00298 ca = ca,
00299 child = child,
00300 subject_key = req_key,
00301 sia = req_sia,
00302 resources = resources)
00303 else:
00304 child_cert = child_cert.reissue(
00305 ca_detail = ca_detail,
00306 sia = req_sia,
00307 resources = resources)
00308
00309
00310 self.gctx.sql.sweep()
00311 assert child_cert and child_cert.sql_in_db
00312 c = certificate_elt()
00313 c.cert_url = multi_uri(child_cert.uri(ca))
00314 c.cert = child_cert.cert
00315 rc = class_elt()
00316 rc.class_name = self.class_name
00317 rc.cert_url = multi_uri(ca_detail.ca_cert_uri)
00318 rc.from_resource_bag(resources)
00319 rc.certs.append(c)
00320 rc.issuer = ca_detail.latest_ca_cert
00321 r_msg.payload = issue_response_pdu()
00322 r_msg.payload.classes.append(rc)
00323
00324 @classmethod
00325 def query(cls, parent, ca, ca_detail):
00326 """Send an "issue" request to parent associated with ca."""
00327 assert ca_detail is not None and ca_detail.state in ("pending", "active")
00328 sia = ((rpki.oids.name2oid["id-ad-caRepository"], ("uri", ca.sia_uri)),
00329 (rpki.oids.name2oid["id-ad-rpkiManifest"], ("uri", ca_detail.manifest_uri(ca))))
00330 self = cls()
00331 self.class_name = ca.parent_resource_class
00332 self.pkcs10 = rpki.x509.PKCS10.create_ca(ca_detail.private_key_id, sia)
00333 return parent.query_up_down(self)
00334
00335 class issue_response_pdu(class_response_syntax):
00336 """Up-Down protocol "issue_response" PDU."""
00337
00338 def check_response(self):
00339 """Check whether this looks like a reasonable issue_response PDU.
00340 XML schema should be tighter for this response.
00341 """
00342 if len(self.classes) != 1 or len(self.classes[0].certs) != 1:
00343 raise rpki.exceptions.BadIssueResponse
00344
00345 class revoke_syntax(base_elt):
00346 """Syntax for Up-Down protocol "revoke" and "revoke_response" PDUs."""
00347
00348 def startElement(self, stack, name, attrs):
00349 """Handle "revoke" PDU."""
00350 self.class_name = attrs["class_name"]
00351 self.ski = attrs["ski"]
00352
00353 def toXML(self):
00354 """Generate payload of "revoke" PDU."""
00355 return [self.make_elt("key", "class_name", "ski")]
00356
00357 class revoke_pdu(revoke_syntax):
00358 """Up-Down protocol "revoke" PDU."""
00359
00360 def get_SKI(self):
00361 """Convert g(SKI) encoding from PDU back to raw SKI."""
00362 return base64.urlsafe_b64decode(self.ski + "=")
00363
00364 def serve_pdu(self, q_msg, r_msg, child):
00365 """Serve one revoke request PDU."""
00366 for ca_detail in child.ca_from_class_name(self.class_name).ca_details():
00367 for child_cert in child.child_certs(ca_detail = ca_detail, ski = self.get_SKI()):
00368 child_cert.revoke()
00369 self.gctx.sql.sweep()
00370 r_msg.payload = revoke_response_pdu()
00371 r_msg.payload.class_name = self.class_name
00372 r_msg.payload.ski = self.ski
00373
00374 @classmethod
00375 def query(cls, ca_detail):
00376 """Send a "revoke" request to parent associated with ca_detail."""
00377 ca = ca_detail.ca()
00378 parent = ca.parent()
00379 self = cls()
00380 self.class_name = ca.parent_resource_class
00381 self.ski = ca_detail.latest_ca_cert.gSKI()
00382 return parent.query_up_down(self)
00383
00384 class revoke_response_pdu(revoke_syntax):
00385 """Up-Down protocol "revoke_response" PDU."""
00386
00387 pass
00388
00389 class error_response_pdu(base_elt):
00390 """Up-Down protocol "error_response" PDU."""
00391
00392 codes = {
00393 1101 : "Already processing request",
00394 1102 : "Version number error",
00395 1103 : "Unrecognised request type",
00396 1201 : "Request - no such resource class",
00397 1202 : "Request - no resources allocated in resource class",
00398 1203 : "Request - badly formed certificate request",
00399 1301 : "Revoke - no such resource class",
00400 1302 : "Revoke - no such key",
00401 2001 : "Internal Server Error - Request not performed" }
00402
00403 exceptions = {}
00404
00405 def __init__(self, exception = None):
00406 """Initialize an error_response PDU from an exception object."""
00407 if exception is not None:
00408 if exception in self.exceptions:
00409 self.status = exceptions[exception]
00410 else:
00411 self.status = 2001
00412 self.description = str(exception)
00413
00414 def endElement(self, stack, name, text):
00415 """Handle "error_response" PDU."""
00416 if name == "status":
00417 code = int(text)
00418 if code not in self.codes:
00419 raise rpki.exceptions.BadStatusCode, "%s is not a known status code"
00420 self.status = code
00421 elif name == "description":
00422 self.description = text
00423 else:
00424 assert name == "message", "Unexpected name %s, stack %s" % (name, stack)
00425 stack.pop()
00426 stack[-1].endElement(stack, name, text)
00427
00428 def toXML(self):
00429 """Generate payload of "error_response" PDU."""
00430 assert self.status in self.codes
00431 elt = self.make_elt("status")
00432 elt.text = str(self.status)
00433 payload = [elt]
00434 if self.description:
00435 elt = self.make_elt("description")
00436 elt.text = str(self.description)
00437 elt.set("{http://www.w3.org/XML/1998/namespace}lang", "en-US")
00438 payload.append(elt)
00439 return payload
00440
00441 def check_response(self):
00442 """Handle an error response. For now, just raise an exception,
00443 perhaps figure out something more clever to do later.
00444 """
00445 raise rpki.exceptions.UpstreamError, self.codes[self.status]
00446
00447 class message_pdu(base_elt):
00448 """Up-Down protocol message wrapper PDU."""
00449
00450 version = 1
00451
00452 name2type = {
00453 "list" : list_pdu,
00454 "list_response" : list_response_pdu,
00455 "issue" : issue_pdu,
00456 "issue_response" : issue_response_pdu,
00457 "revoke" : revoke_pdu,
00458 "revoke_response" : revoke_response_pdu,
00459 "error_response" : error_response_pdu }
00460
00461 type2name = dict((v,k) for k,v in name2type.items())
00462
00463 def toXML(self):
00464 """Generate payload of message PDU."""
00465 elt = self.make_elt("message", "version", "sender", "recipient", "type")
00466 elt.extend(self.payload.toXML())
00467 return elt
00468
00469 def startElement(self, stack, name, attrs):
00470 """Handle message PDU.
00471
00472 Payload of the <message/> element varies depending on the "type"
00473 attribute, so after some basic checks we have to instantiate the
00474 right class object to handle whatever kind of PDU this is.
00475 """
00476 assert name == "message", "Unexpected name %s, stack %s" % (name, stack)
00477 assert self.version == int(attrs["version"])
00478 self.sender = attrs["sender"]
00479 self.recipient = attrs["recipient"]
00480 self.type = attrs["type"]
00481 self.payload = self.name2type[attrs["type"]]()
00482 stack.append(self.payload)
00483
00484 def __str__(self):
00485 """Convert a message PDU to a string."""
00486 lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "UTF-8")
00487
00488 def serve_top_level(self, child):
00489 """Serve one message request PDU."""
00490 r_msg = message_pdu()
00491 r_msg.sender = self.recipient
00492 r_msg.recipient = self.sender
00493 self.payload.serve_pdu(self, r_msg, child)
00494 r_msg.type = self.type2name[type(r_msg.payload)]
00495 return r_msg
00496
00497 def serve_error(self, exception):
00498 """Generate an error_response message PDU."""
00499 r_msg = message_pdu()
00500 r_msg.sender = self.recipient
00501 r_msg.recipient = self.sender
00502 r_msg.payload = error_response_pdu(exception)
00503 r_msg.type = self.type2name[type(r_msg.payload)]
00504 return r_msg
00505
00506 @classmethod
00507 def make_query(cls, payload, sender, recipient):
00508 """Construct one message PDU."""
00509 assert not cls.type2name[type(payload)].endswith("_response")
00510 if sender is None:
00511 sender = "tweedledee"
00512 if recipient is None:
00513 recipient = "tweedledum"
00514 self = cls()
00515 self.sender = sender
00516 self.recipient = recipient
00517 self.payload = payload
00518 self.type = self.type2name[type(payload)]
00519 return self
00520
00521 class sax_handler(rpki.xml_utils.sax_handler):
00522 """SAX handler for Up-Down protocol."""
00523
00524 pdu = message_pdu
00525 name = "message"
00526 version = "1"
00527
00528 class cms_msg(rpki.x509.XML_CMS_object):
00529 """Class to hold a CMS-signed up-down PDU."""
00530
00531 encoding = "UTF-8"
00532 schema = rpki.relaxng.up_down
00533 saxify = sax_handler.saxify