00001 """RPKI "left-right" protocol.
00002
00003 $Id: left_right.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, traceback, os
00021 import rpki.resource_set, rpki.x509, rpki.sql, rpki.exceptions, rpki.xml_utils
00022 import rpki.https, rpki.up_down, rpki.relaxng, rpki.sundial, rpki.log, rpki.roa
00023 import rpki.publication
00024
00025
00026 enforce_strict_up_down_xml_sender = False
00027
00028 class left_right_namespace(object):
00029 """XML namespace parameters for left-right protocol."""
00030
00031 xmlns = "http://www.hactrn.net/uris/rpki/left-right-spec/"
00032 nsmap = { None : xmlns }
00033
00034 class data_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistant, left_right_namespace):
00035 """Virtual class for top-level left-right protocol data elements."""
00036
00037 def self(this):
00038 """Fetch self object to which this object links."""
00039 return self_elt.sql_fetch(this.gctx, this.self_id)
00040
00041 def bsc(self):
00042 """Return BSC object to which this object links."""
00043 return bsc_elt.sql_fetch(self.gctx, self.bsc_id)
00044
00045 def make_reply_clone_hook(self, r_pdu):
00046 """Set self_id when cloning."""
00047 r_pdu.self_id = self.self_id
00048
00049 def serve_fetch_one(self):
00050 """Find the object on which a get, set, or destroy method should
00051 operate.
00052 """
00053 where = self.sql_template.index + " = %s AND self_id = %s"
00054 args = (getattr(self, self.sql_template.index), self.self_id)
00055 r = self.sql_fetch_where1(self.gctx, where, args)
00056 if r is None:
00057 raise rpki.exceptions.NotFound, "Lookup failed where %s" + (where % args)
00058 return r
00059
00060 def serve_fetch_all(self):
00061 """Find the objects on which a list method should operate."""
00062 return self.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00063
00064 def unimplemented_control(self, *controls):
00065 """Uniform handling for unimplemented control operations."""
00066 unimplemented = [x for x in controls if getattr(self, x, False)]
00067 if unimplemented:
00068 raise rpki.exceptions.NotImplementedYet, "Unimplemented control %s" % ", ".join(unimplemented)
00069
00070 class self_elt(data_elt):
00071 """<self/> element."""
00072
00073 element_name = "self"
00074 attributes = ("action", "tag", "self_id", "crl_interval", "regen_margin")
00075 elements = ("bpki_cert", "bpki_glue")
00076 booleans = ("rekey", "reissue", "revoke", "run_now", "publish_world_now")
00077
00078 sql_template = rpki.sql.template("self", "self_id", "use_hsm", "crl_interval", "regen_margin",
00079 ("bpki_cert", rpki.x509.X509), ("bpki_glue", rpki.x509.X509))
00080
00081 self_id = None
00082 use_hsm = False
00083 crl_interval = None
00084 regen_margin = None
00085 bpki_cert = None
00086 bpki_glue = None
00087
00088 def bscs(self):
00089 """Fetch all BSC objects that link to this self object."""
00090 return bsc_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00091
00092 def repositories(self):
00093 """Fetch all repository objects that link to this self object."""
00094 return repository_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00095
00096 def parents(self):
00097 """Fetch all parent objects that link to this self object."""
00098 return parent_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00099
00100 def children(self):
00101 """Fetch all child objects that link to this self object."""
00102 return child_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00103
00104 def route_origins(self):
00105 """Fetch all route_origin objects that link to this self object."""
00106 return route_origin_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00107
00108 def serve_post_save_hook(self, q_pdu, r_pdu):
00109 """Extra server actions for self_elt."""
00110 rpki.log.trace()
00111 if q_pdu.rekey:
00112 self.serve_rekey()
00113 if q_pdu.revoke:
00114 self.serve_revoke()
00115 self.unimplemented_control("reissue", "run_now", "publish_world_now")
00116
00117 def serve_rekey(self):
00118 """Handle a left-right rekey action for this self."""
00119 rpki.log.trace()
00120 for parent in self.parents():
00121 parent.serve_rekey()
00122
00123 def serve_revoke(self):
00124 """Handle a left-right revoke action for this self."""
00125 rpki.log.trace()
00126 for parent in self.parents():
00127 parent.serve_revoke()
00128
00129 def serve_fetch_one(self):
00130 """Find the self object upon which a get, set, or destroy action
00131 should operate.
00132 """
00133 r = self.sql_fetch(self.gctx, self.self_id)
00134 if r is None:
00135 raise rpki.exceptions.NotFound
00136 return r
00137
00138 def serve_fetch_all(self):
00139 """Find the self objects upon which a list action should operate.
00140 This is different from the list action for all other objects,
00141 where list only works within a given self_id context.
00142 """
00143 return self.sql_fetch_all(self.gctx)
00144
00145 def client_poll(self):
00146 """Run the regular client poll cycle with each of this self's parents in turn."""
00147
00148 rpki.log.trace()
00149
00150 for parent in self.parents():
00151
00152
00153 r_msg = rpki.up_down.list_pdu.query(parent)
00154
00155 ca_map = dict((ca.parent_resource_class, ca) for ca in parent.cas())
00156 for rc in r_msg.payload.classes:
00157 if rc.class_name in ca_map:
00158 ca = ca_map[rc.class_name]
00159 del ca_map[rc.class_name]
00160 ca.check_for_updates(parent, rc)
00161 else:
00162 rpki.rpki_engine.ca_obj.create(parent, rc)
00163 for ca in ca_map.values():
00164 ca.delete(parent)
00165 self.gctx.sql.sweep()
00166
00167 def update_children(self):
00168 """Check for updated IRDB data for all of this self's children and
00169 issue new certs as necessary. Must handle changes both in
00170 resources and in expiration date.
00171 """
00172
00173 rpki.log.trace()
00174
00175 now = rpki.sundial.now()
00176
00177 for child in self.children():
00178 child_certs = child.child_certs()
00179 if not child_certs:
00180 continue
00181
00182
00183 irdb_resources = self.gctx.irdb_query(child.self_id, child.child_id)
00184
00185 for child_cert in child_certs:
00186 ca_detail = child_cert.ca_detail()
00187 if ca_detail.state != "active":
00188 continue
00189 old_resources = child_cert.cert.get_3779resources()
00190 new_resources = irdb_resources.intersection(old_resources)
00191 if old_resources != new_resources:
00192 rpki.log.debug("Need to reissue %s" % repr(child_cert))
00193 child_cert.reissue(
00194 ca_detail = ca_detail,
00195 resources = new_resources)
00196 elif old_resources.valid_until < now:
00197 ca = ca_detail.ca()
00198 parent = ca.parent()
00199 repository = parent.repository()
00200 child_cert.sql_delete()
00201 ca_detail.generate_manifest()
00202 repository.withdraw(child_cert.cert, child_cert.uri(ca))
00203
00204 def regenerate_crls_and_manifests(self):
00205 """Generate new CRLs and manifests as necessary for all of this
00206 self's CAs. Extracting nextUpdate from a manifest is hard at the
00207 moment due to implementation silliness, so for now we generate a
00208 new manifest whenever we generate a new CRL
00209
00210 This method also cleans up tombstones left behind by revoked
00211 ca_detail objects, since we're walking through the relevant
00212 portions of the database anyway.
00213 """
00214
00215 rpki.log.trace()
00216
00217 now = rpki.sundial.now()
00218 for parent in self.parents():
00219 repository = parent.repository()
00220 for ca in parent.cas():
00221 for ca_detail in ca.fetch_revoked():
00222 if now > ca_detail.latest_crl.getNextUpdate():
00223 ca_detail.delete(ca, repository)
00224 ca_detail = ca.fetch_active()
00225 if ca_detail is not None and now > ca_detail.latest_crl.getNextUpdate():
00226 ca_detail.generate_crl()
00227 ca_detail.generate_manifest()
00228
00229 def update_roas(self):
00230 """Generate or update ROAs for this self's route_origin objects."""
00231
00232 for route_origin in self.route_origins():
00233 route_origin.update_roa()
00234
00235 class bsc_elt(data_elt):
00236 """<bsc/> (Business Signing Context) element."""
00237
00238 element_name = "bsc"
00239 attributes = ("action", "tag", "self_id", "bsc_id", "key_type", "hash_alg", "key_length")
00240 elements = ("signing_cert", "signing_cert_crl", "pkcs10_request")
00241 booleans = ("generate_keypair",)
00242
00243 sql_template = rpki.sql.template("bsc", "bsc_id", "self_id", "hash_alg",
00244 ("private_key_id", rpki.x509.RSA),
00245 ("pkcs10_request", rpki.x509.PKCS10),
00246 ("signing_cert", rpki.x509.X509),
00247 ("signing_cert_crl", rpki.x509.CRL))
00248
00249 private_key_id = None
00250 pkcs10_request = None
00251 signing_cert = None
00252 signing_cert_crl = None
00253
00254 def repositories(self):
00255 """Fetch all repository objects that link to this BSC object."""
00256 return repository_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
00257
00258 def parents(self):
00259 """Fetch all parent objects that link to this BSC object."""
00260 return parent_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
00261
00262 def children(self):
00263 """Fetch all child objects that link to this BSC object."""
00264 return child_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
00265
00266 def serve_pre_save_hook(self, q_pdu, r_pdu):
00267 """Extra server actions for bsc_elt -- handle key generation.
00268 For now this only allows RSA with SHA-256.
00269 """
00270 if q_pdu.generate_keypair:
00271 assert q_pdu.key_type in (None, "rsa") and q_pdu.hash_alg in (None, "sha256")
00272 self.private_key_id = rpki.x509.RSA.generate(keylength = q_pdu.key_length or 2048)
00273 self.pkcs10_request = rpki.x509.PKCS10.create(self.private_key_id)
00274 r_pdu.pkcs10_request = self.pkcs10_request
00275
00276 class parent_elt(data_elt):
00277 """<parent/> element."""
00278
00279 element_name = "parent"
00280 attributes = ("action", "tag", "self_id", "parent_id", "bsc_id", "repository_id",
00281 "peer_contact_uri", "sia_base", "sender_name", "recipient_name")
00282 elements = ("bpki_cms_cert", "bpki_cms_glue", "bpki_https_cert", "bpki_https_glue")
00283 booleans = ("rekey", "reissue", "revoke")
00284
00285 sql_template = rpki.sql.template("parent", "parent_id", "self_id", "bsc_id", "repository_id",
00286 ("bpki_cms_cert", rpki.x509.X509), ("bpki_cms_glue", rpki.x509.X509),
00287 ("bpki_https_cert", rpki.x509.X509), ("bpki_https_glue", rpki.x509.X509),
00288 "peer_contact_uri", "sia_base", "sender_name", "recipient_name")
00289
00290 bpki_cms_cert = None
00291 bpki_cms_glue = None
00292 bpki_https_cert = None
00293 bpki_https_glue = None
00294
00295 def repository(self):
00296 """Fetch repository object to which this parent object links."""
00297 return repository_elt.sql_fetch(self.gctx, self.repository_id)
00298
00299 def cas(self):
00300 """Fetch all CA objects that link to this parent object."""
00301 return rpki.rpki_engine.ca_obj.sql_fetch_where(self.gctx, "parent_id = %s", (self.parent_id,))
00302
00303 def serve_post_save_hook(self, q_pdu, r_pdu):
00304 """Extra server actions for parent_elt."""
00305 if q_pdu.rekey:
00306 self.serve_rekey()
00307 if q_pdu.revoke:
00308 self.serve_revoke()
00309 self.unimplemented_control("reissue")
00310
00311 def serve_rekey(self):
00312 """Handle a left-right rekey action for this parent."""
00313 for ca in self.cas():
00314 ca.rekey()
00315
00316 def serve_revoke(self):
00317 """Handle a left-right revoke action for this parent."""
00318 for ca in self.cas():
00319 ca.revoke()
00320
00321 def query_up_down(self, q_pdu):
00322 """Client code for sending one up-down query PDU to this parent.
00323
00324 I haven't figured out yet whether this method should do something
00325 clever like dispatching via a method in the response PDU payload,
00326 or just hand back the whole response to the caller. In the long
00327 run this will have to become event driven with a context object
00328 that has methods of its own, but as this method is common code for
00329 several different queries and I don't yet know what the response
00330 processing looks like, it's too soon to tell what will make sense.
00331
00332 For now, keep this dead simple lock step, rewrite it later.
00333 """
00334
00335 rpki.log.trace()
00336
00337 bsc = self.bsc()
00338 if bsc is None:
00339 raise rpki.exceptions.BSCNotFound, "Could not find BSC %s" % self.bsc_id
00340
00341 q_msg = rpki.up_down.message_pdu.make_query(
00342 payload = q_pdu,
00343 sender = self.sender_name,
00344 recipient = self.recipient_name)
00345
00346 q_cms = rpki.up_down.cms_msg.wrap(q_msg, bsc.private_key_id,
00347 bsc.signing_cert,
00348 bsc.signing_cert_crl)
00349
00350 der = rpki.https.client(server_ta = (self.gctx.bpki_ta,
00351 self.self().bpki_cert, self.self().bpki_glue,
00352 self.bpki_https_cert, self.bpki_https_glue),
00353 client_key = bsc.private_key_id,
00354 client_cert = bsc.signing_cert,
00355 msg = q_cms,
00356 url = self.peer_contact_uri)
00357
00358 r_msg = rpki.up_down.cms_msg.unwrap(der, (self.gctx.bpki_ta,
00359 self.self().bpki_cert, self.self().bpki_glue,
00360 self.bpki_cms_cert, self.bpki_cms_glue))
00361
00362 r_msg.payload.check_response()
00363 return r_msg
00364
00365
00366 class child_elt(data_elt):
00367 """<child/> element."""
00368
00369 element_name = "child"
00370 attributes = ("action", "tag", "self_id", "child_id", "bsc_id")
00371 elements = ("bpki_cert", "bpki_glue")
00372 booleans = ("reissue", )
00373
00374 sql_template = rpki.sql.template("child", "child_id", "self_id", "bsc_id",
00375 ("bpki_cert", rpki.x509.X509),
00376 ("bpki_glue", rpki.x509.X509))
00377
00378 bpki_cert = None
00379 bpki_glue = None
00380 clear_https_ta_cache = False
00381
00382 def child_certs(self, ca_detail = None, ski = None, unique = False):
00383 """Fetch all child_cert objects that link to this child object."""
00384 return rpki.rpki_engine.child_cert_obj.fetch(self.gctx, self, ca_detail, ski, unique)
00385
00386 def parents(self):
00387 """Fetch all parent objects that link to self object to which this child object links."""
00388 return parent_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00389
00390 def ca_from_class_name(self, class_name):
00391 """Fetch the CA corresponding to an up-down class_name."""
00392 if not class_name.isdigit():
00393 raise rpki.exceptions.BadClassNameSyntax, "Bad class name %s" % class_name
00394 ca = rpki.rpki_engine.ca_obj.sql_fetch(self.gctx, long(class_name))
00395 parent = ca.parent()
00396 if self.self_id != parent.self_id:
00397 raise rpki.exceptions.ClassNameMismatch, "child.self_id = %d, parent.self_id = %d" % (self.self_id, parent.self_id)
00398 return ca
00399
00400 def serve_post_save_hook(self, q_pdu, r_pdu):
00401 """Extra server actions for child_elt."""
00402 self.unimplemented_control("reissue")
00403 if self.clear_https_ta_cache:
00404 self.gctx.clear_https_ta_cache()
00405 self.clear_https_ta_cache = False
00406
00407 def endElement(self, stack, name, text):
00408 """Handle subelements of <child/> element. These require special
00409 handling because modifying them invalidates the HTTPS trust anchor
00410 cache.
00411 """
00412 rpki.xml_utils.data_elt.endElement(self, stack, name, text)
00413 if name in self.elements:
00414 self.clear_https_ta_cache = True
00415
00416 def serve_up_down(self, query):
00417 """Outer layer of server handling for one up-down PDU from this child."""
00418
00419 rpki.log.trace()
00420
00421 bsc = self.bsc()
00422 if bsc is None:
00423 raise rpki.exceptions.BSCNotFound, "Could not find BSC %s" % self.bsc_id
00424 q_msg = rpki.up_down.cms_msg.unwrap(query, (self.gctx.bpki_ta,
00425 self.self().bpki_cert, self.self().bpki_glue,
00426 self.bpki_cert, self.bpki_glue))
00427 q_msg.payload.gctx = self.gctx
00428 if enforce_strict_up_down_xml_sender and q_msg.sender != str(self.child_id):
00429 raise rpki.exceptions.BadSender, "Unexpected XML sender %s" % q_msg.sender
00430 try:
00431 r_msg = q_msg.serve_top_level(self)
00432 except Exception, data:
00433 rpki.log.error(traceback.format_exc())
00434 r_msg = q_msg.serve_error(data)
00435
00436
00437
00438
00439
00440 r_cms = rpki.up_down.cms_msg.wrap(r_msg, bsc.private_key_id,
00441 bsc.signing_cert, bsc.signing_cert_crl)
00442 return r_cms
00443
00444 class repository_elt(data_elt):
00445 """<repository/> element."""
00446
00447 element_name = "repository"
00448 attributes = ("action", "tag", "self_id", "repository_id", "bsc_id", "peer_contact_uri")
00449 elements = ("bpki_cms_cert", "bpki_cms_glue", "bpki_https_cert", "bpki_https_glue")
00450
00451 sql_template = rpki.sql.template("repository", "repository_id", "self_id", "bsc_id", "peer_contact_uri",
00452 ("bpki_cms_cert", rpki.x509.X509), ("bpki_cms_glue", rpki.x509.X509),
00453 ("bpki_https_cert", rpki.x509.X509), ("bpki_https_glue", rpki.x509.X509))
00454
00455 bpki_cms_cert = None
00456 bpki_cms_glue = None
00457 bpki_https_cert = None
00458 bpki_https_glue = None
00459
00460 use_pubd = True
00461
00462 def parents(self):
00463 """Fetch all parent objects that link to this repository object."""
00464 return parent_elt.sql_fetch_where(self.gctx, "repository_id = %s", (self.repository_id,))
00465
00466 @staticmethod
00467 def uri_to_filename(base, uri):
00468 """Convert a URI to a filename. [TEMPORARY]"""
00469 if not uri.startswith("rsync://"):
00470 raise rpki.exceptions.BadURISyntax
00471 filename = base + uri[len("rsync://"):]
00472 if filename.find("//") >= 0 or filename.find("/../") >= 0 or filename.endswith("/.."):
00473 raise rpki.exceptions.BadURISyntax
00474 return filename
00475
00476 @classmethod
00477 def object_write(cls, base, uri, obj):
00478 """Write an object to disk. [TEMPORARY]"""
00479 rpki.log.trace()
00480 filename = cls.uri_to_filename(base, uri)
00481 dirname = os.path.dirname(filename)
00482 if not os.path.isdir(dirname):
00483 os.makedirs(dirname)
00484 f = open(filename, "wb")
00485 f.write(obj.get_DER())
00486 f.close()
00487
00488 @classmethod
00489 def object_delete(cls, base, uri):
00490 """Delete an object from disk. [TEMPORARY]"""
00491 rpki.log.trace()
00492 os.remove(cls.uri_to_filename(base, uri))
00493
00494 def call_pubd(self, *pdus):
00495 """Send a message to publication daemon and return the response."""
00496 rpki.log.trace()
00497 bsc = self.bsc()
00498 q_msg = rpki.publication.msg(pdus)
00499 q_msg.type = "query"
00500 q_cms = rpki.publication.cms_msg.wrap(q_msg, bsc.private_key_id, bsc.signing_cert, bsc.signing_cert_crl)
00501 bpki_ta_path = (self.gctx.bpki_ta, self.self().bpki_cert, self.self().bpki_glue, self.bpki_https_cert, self.bpki_https_glue)
00502 r_cms = rpki.https.client(
00503 client_key = bsc.private_key_id,
00504 client_cert = bsc.signing_cert,
00505 server_ta = bpki_ta_path,
00506 url = self.peer_contact_uri,
00507 msg = q_cms)
00508 r_msg = rpki.publication.cms_msg.unwrap(r_cms, bpki_ta_path)
00509 assert len(r_msg) == 1
00510 return r_msg[0]
00511
00512 def publish(self, obj, uri):
00513 """Placeholder for publication operation. [TEMPORARY]"""
00514 rpki.log.trace()
00515 rpki.log.info("Publishing %s as %s" % (repr(obj), repr(uri)))
00516 if self.use_pubd:
00517 self.call_pubd(rpki.publication.obj2elt[type(obj)].make_pdu(action = "publish", uri = uri, payload = obj))
00518 else:
00519 self.object_write(self.gctx.publication_kludge_base, uri, obj)
00520
00521 def withdraw(self, obj, uri):
00522 """Placeholder for publication withdrawal operation. [TEMPORARY]"""
00523 rpki.log.trace()
00524 rpki.log.info("Withdrawing %s from at %s" % (repr(obj), repr(uri)))
00525 if self.use_pubd:
00526 self.call_pubd(rpki.publication.obj2elt[type(obj)].make_pdu(action = "withdraw", uri = uri))
00527 else:
00528 self.object_delete(self.gctx.publication_kludge_base, uri)
00529
00530 class route_origin_elt(data_elt):
00531 """<route_origin/> element."""
00532
00533 element_name = "route_origin"
00534 attributes = ("action", "tag", "self_id", "route_origin_id", "as_number", "ipv4", "ipv6")
00535 booleans = ("suppress_publication",)
00536
00537 sql_template = rpki.sql.template("route_origin", "route_origin_id", "ca_detail_id",
00538 "self_id", "as_number",
00539 ("roa", rpki.x509.ROA),
00540 ("cert", rpki.x509.X509))
00541
00542 ca_detail_id = None
00543 cert = None
00544 roa = None
00545
00546
00547
00548 publish_ee_separately = False
00549
00550 def sql_fetch_hook(self):
00551 """Extra SQL fetch actions for route_origin_elt -- handle prefix list."""
00552 self.ipv4 = rpki.resource_set.roa_prefix_set_ipv4.from_sql(
00553 self.gctx.sql,
00554 """
00555 SELECT address, prefixlen, max_prefixlen FROM route_origin_prefix
00556 WHERE route_origin_id = %s AND address NOT LIKE '%:%'
00557 """, (self.route_origin_id,))
00558 self.ipv6 = rpki.resource_set.roa_prefix_set_ipv6.from_sql(
00559 self.gctx.sql,
00560 """
00561 SELECT address, prefixlen, max_prefixlen FROM route_origin_prefix
00562 WHERE route_origin_id = %s AND address LIKE '%:%'
00563 """, (self.route_origin_id,))
00564
00565 def sql_insert_hook(self):
00566 """Extra SQL insert actions for route_origin_elt -- handle address ranges."""
00567 if self.ipv4 or self.ipv6:
00568 self.gctx.sql.executemany("""
00569 INSERT route_origin_prefix (route_origin_id, address, prefixlen, max_prefixlen)
00570 VALUES (%s, %s, %s, %s)""",
00571 ((self.route_origin_id, x.address, x.prefixlen, x.max_prefixlen)
00572 for x in (self.ipv4 or []) + (self.ipv6 or [])))
00573
00574 def sql_delete_hook(self):
00575 """Extra SQL delete actions for route_origin_elt -- handle address ranges."""
00576 self.gctx.sql.execute("DELETE FROM route_origin_prefix WHERE route_origin_id = %s", (self.route_origin_id,))
00577
00578 def ca_detail(self):
00579 """Fetch all ca_detail objects that link to this route_origin object."""
00580 return rpki.rpki_engine.ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
00581
00582 def serve_post_save_hook(self, q_pdu, r_pdu):
00583 """Extra server actions for route_origin_elt."""
00584 self.unimplemented_control("suppress_publication")
00585
00586 def startElement(self, stack, name, attrs):
00587 """Handle <route_origin/> element. This requires special
00588 processing due to the data types of some of the attributes.
00589 """
00590 assert name == "route_origin", "Unexpected name %s, stack %s" % (name, stack)
00591 self.read_attrs(attrs)
00592 if self.as_number is not None:
00593 self.as_number = long(self.as_number)
00594 if self.ipv4 is not None:
00595 self.ipv4 = rpki.resource_set.roa_prefix_set_ipv4(self.ipv4)
00596 if self.ipv6 is not None:
00597 self.ipv6 = rpki.resource_set.roa_prefix_set_ipv6(self.ipv6)
00598
00599 def update_roa(self):
00600 """Bring this route_origin's ROA up to date if necesssary."""
00601
00602 if self.roa is None:
00603 return self.generate_roa()
00604
00605 ca_detail = self.ca_detail()
00606
00607 if ca_detail.state != "active":
00608 return self.regenerate_roa()
00609
00610 regen_margin = rpki.sundial.timedelta(seconds = self.self().regen_margin)
00611
00612 if rpki.sundial.now() + regen_margin > self.cert.getNotAfter():
00613 return self.regenerate_roa()
00614
00615 ca_resources = ca_detail.latest_ca_cert.get_3779resources()
00616 ee_resources = self.cert.get_3779resources()
00617
00618 if ee_resources.oversized(ca_resources):
00619 return self.regenerate_roa()
00620
00621 v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4()
00622 v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6()
00623
00624 if ee_resources.v4 != v4 or ee_resources.v6 != v6:
00625 return self.regenerate_roa()
00626
00627 def generate_roa(self):
00628 """Generate a ROA based on this <route_origin/> object.
00629
00630 At present this does not support ROAs with multiple signatures
00631 (neither does the current CMS code).
00632
00633 At present we have no way of performing a direct lookup from a
00634 desired set of resources to a covering certificate, so we have to
00635 search. This could be quite slow if we have a lot of active
00636 ca_detail objects. Punt on the issue for now, revisit if
00637 profiling shows this as a hotspot.
00638
00639 Once we have the right covering certificate, we generate the ROA
00640 payload, generate a new EE certificate, use the EE certificate to
00641 sign the ROA payload, publish the result, then throw away the
00642 private key for the EE cert, all per the ROA specification. This
00643 implies that generating a lot of ROAs will tend to thrash
00644 /dev/random, but there is not much we can do about that.
00645 """
00646
00647 if self.ipv4 is None and self.ipv6 is None:
00648 rpki.log.warn("Can't generate ROA for empty prefix list")
00649 return
00650
00651
00652
00653
00654
00655
00656
00657
00658 v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4()
00659 v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6()
00660
00661 ca_detail = self.ca_detail()
00662 if ca_detail is None or ca_detail.state != "active":
00663 ca_detail = None
00664 for parent in self.self().parents():
00665 for ca in parent.cas():
00666 ca_detail = ca.fetch_active()
00667 if ca_detail is not None:
00668 resources = ca_detail.latest_ca_cert.get_3779resources()
00669 if v4.issubset(resources.v4) and v6.issubset(resources.v6):
00670 break
00671 ca_detail = None
00672 if ca_detail is not None:
00673 break
00674
00675 if ca_detail is None:
00676 rpki.log.warn("generate_roa() could not find a certificate covering %s %s" % (v4, v6))
00677 return
00678
00679 resources = rpki.resource_set.resource_bag(v4 = v4, v6 = v6)
00680
00681 keypair = rpki.x509.RSA.generate()
00682
00683 sia = ((rpki.oids.name2oid["id-ad-signedObject"], ("uri", self.roa_uri(ca, keypair))),)
00684
00685 self.cert = ca_detail.issue_ee(ca, resources, keypair.get_RSApublic(), sia = sia)
00686 self.roa = rpki.x509.ROA.build(self.as_number, self.ipv4, self.ipv6, keypair, (self.cert,))
00687 self.ca_detail_id = ca_detail.ca_detail_id
00688 self.sql_store()
00689
00690 repository = parent.repository()
00691 repository.publish(self.roa, self.roa_uri(ca))
00692 if self.publish_ee_separately:
00693 repository.publish(self.cert, self.ee_uri(ca))
00694 ca_detail.generate_manifest()
00695
00696 def withdraw_roa(self, regenerate = False):
00697 """Withdraw ROA associated with this route_origin.
00698
00699 In order to preserve make-before-break properties without
00700 duplicating code, this method also handles generating a
00701 replacement ROA when requested.
00702 """
00703
00704 ca_detail = self.ca_detail()
00705 ca = ca_detail.ca()
00706 repository = ca.parent().repository()
00707 cert = self.cert
00708 roa = self.roa
00709 roa_uri = self.roa_uri(ca)
00710 ee_uri = self.ee_uri(ca)
00711
00712 if ca_detail.state != 'active':
00713 self.ca_detail_id = None
00714 if regenerate:
00715 self.generate_roa()
00716
00717 rpki.log.debug("Withdrawing ROA and revoking its EE cert")
00718 rpki.rpki_engine.revoked_cert_obj.revoke(cert = cert, ca_detail = ca_detail)
00719 repository.withdraw(roa, roa_uri)
00720 if self.publish_ee_separately:
00721 repository.withdraw(cert, ee_uri)
00722 self.gctx.sql.sweep()
00723 ca_detail.generate_crl()
00724 ca_detail.generate_manifest()
00725
00726 def regenerate_roa(self):
00727 """Reissue ROA associated with this route_origin."""
00728 self.withdraw_roa(regenerate = True)
00729
00730 def roa_uri(self, ca, key = None):
00731 """Return the publication URI for this route_origin's ROA."""
00732 return ca.sia_uri + self.roa_uri_tail(key)
00733
00734 def roa_uri_tail(self, key = None):
00735 """Return the tail (filename portion) of the publication URI for this route_origin's ROA."""
00736 return (key or self.cert).gSKI() + ".roa"
00737
00738 def ee_uri_tail(self):
00739 """Return the tail (filename) portion of the URI for this route_origin's ROA's EE certificate."""
00740 return self.cert.gSKI() + ".cer"
00741
00742 def ee_uri(self, ca):
00743 """Return the publication URI for this route_origin's ROA's EE certificate."""
00744 return ca.sia_uri + self.ee_uri_tail()
00745
00746 class list_resources_elt(rpki.xml_utils.base_elt, left_right_namespace):
00747 """<list_resources/> element."""
00748
00749 element_name = "list_resources"
00750 attributes = ("self_id", "tag", "child_id", "valid_until", "asn", "ipv4", "ipv6", "subject_name")
00751 valid_until = None
00752
00753 def startElement(self, stack, name, attrs):
00754 """Handle <list_resources/> element. This requires special
00755 handling due to the data types of some of the attributes.
00756 """
00757 assert name == "list_resources", "Unexpected name %s, stack %s" % (name, stack)
00758 self.read_attrs(attrs)
00759 if isinstance(self.valid_until, str):
00760 self.valid_until = rpki.sundial.datetime.fromXMLtime(self.valid_until)
00761 if self.asn is not None:
00762 self.asn = rpki.resource_set.resource_set_as(self.asn)
00763 if self.ipv4 is not None:
00764 self.ipv4 = rpki.resource_set.resource_set_ipv4(self.ipv4)
00765 if self.ipv6 is not None:
00766 self.ipv6 = rpki.resource_set.resource_set_ipv6(self.ipv6)
00767
00768 def toXML(self):
00769 """Generate <list_resources/> element. This requires special
00770 handling due to the data types of some of the attributes.
00771 """
00772 elt = self.make_elt()
00773 if isinstance(self.valid_until, int):
00774 elt.set("valid_until", self.valid_until.toXMLtime())
00775 return elt
00776
00777 class report_error_elt(rpki.xml_utils.base_elt, left_right_namespace):
00778 """<report_error/> element."""
00779
00780 element_name = "report_error"
00781 attributes = ("tag", "self_id", "error_code")
00782
00783 @classmethod
00784 def from_exception(cls, exc, self_id = None):
00785 """Generate a <report_error/> element from an exception."""
00786 self = cls()
00787 self.self_id = self_id
00788 self.error_code = exc.__class__.__name__
00789 return self
00790
00791 class msg(rpki.xml_utils.msg, left_right_namespace):
00792 """Left-right PDU."""
00793
00794
00795
00796 version = 1
00797
00798
00799
00800 pdus = dict((x.element_name, x)
00801 for x in (self_elt, child_elt, parent_elt, bsc_elt, repository_elt,
00802 route_origin_elt, list_resources_elt, report_error_elt))
00803
00804 def serve_top_level(self, gctx):
00805 """Serve one msg PDU."""
00806 r_msg = self.__class__()
00807 r_msg.type = "reply"
00808 for q_pdu in self:
00809 q_pdu.gctx = gctx
00810 q_pdu.serve_dispatch(r_msg)
00811 return r_msg
00812
00813 class sax_handler(rpki.xml_utils.sax_handler):
00814 """SAX handler for Left-Right protocol."""
00815
00816 pdu = msg
00817 name = "msg"
00818 version = "1"
00819
00820 class cms_msg(rpki.x509.XML_CMS_object):
00821 """Class to hold a CMS-signed left-right PDU."""
00822
00823 encoding = "us-ascii"
00824 schema = rpki.relaxng.left_right
00825 saxify = sax_handler.saxify