00001 """
00002 RPKI "left-right" protocol.
00003
00004 $Id: left_right.py 2481 2009-06-01 05:07:46Z sra $
00005
00006 Copyright (C) 2009 Internet Systems Consortium ("ISC")
00007
00008 Permission to use, copy, modify, and distribute this software for any
00009 purpose with or without fee is hereby granted, provided that the above
00010 copyright notice and this permission notice appear in all copies.
00011
00012 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
00013 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00014 AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
00015 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00016 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00017 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00018 PERFORMANCE OF THIS SOFTWARE.
00019
00020 Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
00021
00022 Permission to use, copy, modify, and distribute this software for any
00023 purpose with or without fee is hereby granted, provided that the above
00024 copyright notice and this permission notice appear in all copies.
00025
00026 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
00027 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00028 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
00029 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00030 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00031 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00032 PERFORMANCE OF THIS SOFTWARE.
00033 """
00034
00035 import traceback
00036 import rpki.resource_set, rpki.x509, rpki.sql, rpki.exceptions, rpki.xml_utils
00037 import rpki.https, rpki.up_down, rpki.relaxng, rpki.sundial, rpki.log, rpki.roa
00038 import rpki.publication, rpki.async
00039
00040
00041 enforce_strict_up_down_xml_sender = False
00042
00043 class left_right_namespace(object):
00044 """
00045 XML namespace parameters for left-right protocol.
00046 """
00047
00048 xmlns = "http://www.hactrn.net/uris/rpki/left-right-spec/"
00049 nsmap = { None : xmlns }
00050
00051 class data_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, left_right_namespace):
00052 """
00053 Virtual class for top-level left-right protocol data elements.
00054 """
00055
00056 def self(self):
00057 """Fetch self object to which this object links."""
00058 return self_elt.sql_fetch(self.gctx, self.self_id)
00059
00060 def bsc(self):
00061 """Return BSC object to which this object links."""
00062 return bsc_elt.sql_fetch(self.gctx, self.bsc_id)
00063
00064 def make_reply_clone_hook(self, r_pdu):
00065 """Set self_id when cloning."""
00066 r_pdu.self_id = self.self_id
00067
00068 def serve_fetch_one(self):
00069 """
00070 Find the object on which a get, set, or destroy method should
00071 operate.
00072 """
00073 where = self.sql_template.index + " = %s AND self_id = %s"
00074 args = (getattr(self, self.sql_template.index), self.self_id)
00075 r = self.sql_fetch_where1(self.gctx, where, args)
00076 if r is None:
00077 raise rpki.exceptions.NotFound, "Lookup failed where " + (where % args)
00078 return r
00079
00080 def serve_fetch_all(self):
00081 """Find the objects on which a list method should operate."""
00082 return self.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00083
00084 def unimplemented_control(self, *controls):
00085 """
00086 Uniform handling for unimplemented control operations.
00087 """
00088 unimplemented = [x for x in controls if getattr(self, x, False)]
00089 if unimplemented:
00090 raise rpki.exceptions.NotImplementedYet, "Unimplemented control %s" % ", ".join(unimplemented)
00091
00092 class self_elt(data_elt):
00093 """
00094 <self/> element.
00095 """
00096
00097 element_name = "self"
00098 attributes = ("action", "tag", "self_id", "crl_interval", "regen_margin")
00099 elements = ("bpki_cert", "bpki_glue")
00100 booleans = ("rekey", "reissue", "revoke", "run_now", "publish_world_now")
00101
00102 sql_template = rpki.sql.template("self", "self_id", "use_hsm", "crl_interval", "regen_margin",
00103 ("bpki_cert", rpki.x509.X509), ("bpki_glue", rpki.x509.X509))
00104
00105 self_id = None
00106 use_hsm = False
00107 crl_interval = None
00108 regen_margin = None
00109 bpki_cert = None
00110 bpki_glue = None
00111
00112 def bscs(self):
00113 """Fetch all BSC objects that link to this self object."""
00114 return bsc_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00115
00116 def repositories(self):
00117 """Fetch all repository objects that link to this self object."""
00118 return repository_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00119
00120 def parents(self):
00121 """Fetch all parent objects that link to this self object."""
00122 return parent_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00123
00124 def children(self):
00125 """Fetch all child objects that link to this self object."""
00126 return child_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00127
00128 def route_origins(self):
00129 """Fetch all route_origin objects that link to this self object."""
00130 return route_origin_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00131
00132 def serve_post_save_hook(self, q_pdu, r_pdu, cb, eb):
00133 """
00134 Extra server actions for self_elt.
00135 """
00136 rpki.log.trace()
00137 if q_pdu.rekey:
00138 self.serve_rekey(cb, eb)
00139 elif q_pdu.revoke:
00140 self.serve_revoke(cb, eb)
00141 else:
00142 self.unimplemented_control("reissue", "run_now", "publish_world_now")
00143 cb()
00144
00145 def serve_rekey(self, cb, eb):
00146 """
00147 Handle a left-right rekey action for this self.
00148 """
00149 rpki.log.trace()
00150
00151 def loop(iterator, parent):
00152 parent.serve_rekey(iterator, eb)
00153
00154 rpki.async.iterator(self.parents(), loop, cb)
00155
00156 def serve_revoke(self, cb, eb):
00157 """
00158 Handle a left-right revoke action for this self.
00159 """
00160 rpki.log.trace()
00161
00162 def loop(iterator, parent):
00163 parent.serve_revoke(iterator, eb)
00164
00165 rpki.async.iterator(self.parents(), loop, cb)
00166
00167 def serve_fetch_one(self):
00168 """
00169 Find the self object upon which a get, set, or destroy action
00170 should operate.
00171 """
00172 r = self.sql_fetch(self.gctx, self.self_id)
00173 if r is None:
00174 raise rpki.exceptions.NotFound
00175 return r
00176
00177 def serve_fetch_all(self):
00178 """
00179 Find the self objects upon which a list action should operate.
00180 This is different from the list action for all other objects,
00181 where list only works within a given self_id context.
00182 """
00183 return self.sql_fetch_all(self.gctx)
00184
00185 def client_poll(self, callback):
00186 """
00187 Run the regular client poll cycle with each of this self's parents
00188 in turn.
00189 """
00190
00191 rpki.log.trace()
00192
00193 def parent_loop(parent_iterator, parent):
00194
00195 def got_list(r_msg):
00196 ca_map = dict((ca.parent_resource_class, ca) for ca in parent.cas())
00197
00198 def class_loop(class_iterator, rc):
00199
00200 def class_update_failed(e):
00201 rpki.log.error(traceback.format_exc())
00202 rpki.log.warn("Couldn't update class, skipping: %s" % e)
00203 class_iterator()
00204
00205 def class_create_failed(e):
00206 rpki.log.error(traceback.format_exc())
00207 rpki.log.warn("Couldn't create class, skipping: %s" % e)
00208 class_iterator()
00209
00210 if rc.class_name in ca_map:
00211 ca = ca_map[rc.class_name]
00212 del ca_map[rc.class_name]
00213 ca.check_for_updates(parent, rc, class_iterator, class_update_failed)
00214 else:
00215 rpki.rpki_engine.ca_obj.create(parent, rc, class_iterator, class_create_failed)
00216
00217 def class_done():
00218
00219 def ca_loop(iterator, ca):
00220 ca.delete(parent, iterator)
00221
00222 def ca_done():
00223 self.gctx.sql.sweep()
00224 parent_iterator()
00225
00226 rpki.async.iterator(ca_map.values(), ca_loop, ca_done)
00227
00228 rpki.async.iterator(r_msg.payload.classes, class_loop, class_done)
00229
00230 def list_failed(e):
00231 rpki.log.error(traceback.format_exc())
00232 rpki.log.warn("Couldn't get resource class list from parent %r, skipping: %s" % (parent, e))
00233
00234 rpki.up_down.list_pdu.query(parent, got_list, list_failed)
00235
00236 rpki.async.iterator(self.parents(), parent_loop, callback)
00237
00238 def update_children(self, cb):
00239 """
00240 Check for updated IRDB data for all of this self's children and
00241 issue new certs as necessary. Must handle changes both in
00242 resources and in expiration date.
00243 """
00244
00245 rpki.log.trace()
00246
00247 now = rpki.sundial.now()
00248
00249 rsn = now + rpki.sundial.timedelta(seconds = self.regen_margin)
00250
00251 def loop1(iterator1, child):
00252
00253 def got_resources(irdb_resources):
00254
00255 def loop2(iterator2, child_cert):
00256
00257 ca_detail = child_cert.ca_detail()
00258
00259 if ca_detail.state == "active":
00260 old_resources = child_cert.cert.get_3779resources()
00261 new_resources = irdb_resources.intersection(old_resources)
00262
00263 if old_resources != new_resources or (old_resources.valid_until < rsn and irdb_resources.valid_until > now):
00264 rpki.log.debug("Need to reissue child certificate SKI %s" % child_cert.cert.gSKI())
00265
00266 def reissue_failed(e):
00267 rpki.log.error(traceback.format_exc())
00268 rpki.log.warn("Couldn't reissue child_cert %r, skipping: %s" % (child_cert, e))
00269 iterator2()
00270
00271 child_cert.reissue(
00272 ca_detail = ca_detail,
00273 resources = new_resources,
00274 callback = iterator2.ignore,
00275 errback = reissue_failed)
00276 return
00277
00278 if old_resources.valid_until < now:
00279 rpki.log.debug("Child certificate SKI %s has expired: cert.valid_until %s, irdb.valid_until %s"
00280 % (child_cert.cert.gSKI(), old_resources.valid_until, irdb_resources.valid_until))
00281 ca = ca_detail.ca()
00282 parent = ca.parent()
00283 repository = parent.repository()
00284 child_cert.sql_delete()
00285
00286 def withdraw():
00287 repository.withdraw(child_cert.cert, child_cert.uri(ca), iterator2, withdraw_failed)
00288
00289 def manifest_failed(e):
00290 rpki.log.error(traceback.format_exc())
00291 rpki.log.warn("Couldn't reissue manifest for %r, skipping: %s" % (ca_detail, e))
00292 iterator2()
00293
00294 def withdraw_failed(e):
00295 rpki.log.error(traceback.format_exc())
00296 rpki.log.warn("Couldn't withdraw old child_cert %r, skipping: %s" % (child_cert, e))
00297 iterator2()
00298
00299 ca_detail.generate_manifest(withdraw, manifest_failed)
00300 return
00301
00302 iterator2()
00303
00304 rpki.async.iterator(child_certs, loop2, iterator1)
00305
00306 def irdb_lookup_failed(e):
00307 rpki.log.error(traceback.format_exc())
00308 rpki.log.warn("Couldn't look up child's resources in IRDB, skipping child %r: %s" % (child, e))
00309 iterator1()
00310
00311 child_certs = child.child_certs()
00312 if child_certs:
00313 self.gctx.irdb_query(child.self_id, child.child_id, got_resources, irdb_lookup_failed)
00314 else:
00315 iterator1()
00316
00317 rpki.async.iterator(self.children(), loop1, cb)
00318
00319
00320 def regenerate_crls_and_manifests(self, cb):
00321 """
00322 Generate new CRLs and manifests as necessary for all of this
00323 self's CAs. Extracting nextUpdate from a manifest is hard at the
00324 moment due to implementation silliness, so for now we generate a
00325 new manifest whenever we generate a new CRL
00326
00327 This method also cleans up tombstones left behind by revoked
00328 ca_detail objects, since we're walking through the relevant
00329 portions of the database anyway.
00330 """
00331
00332 rpki.log.trace()
00333
00334 now = rpki.sundial.now()
00335
00336 def loop1(iterator1, parent):
00337 repository = parent.repository()
00338
00339 def loop2(iterator2, ca):
00340
00341 def fail2(e):
00342 rpki.log.error(traceback.format_exc())
00343 rpki.log.warn("Couldn't regenerate CRLs and manifests for CA %r, skipping: %s" % (ca, e))
00344 iterator2()
00345
00346 def loop3(iterator3, ca_detail):
00347 ca_detail.delete(ca, repository, iterator3, fail2)
00348
00349 def done3():
00350
00351 ca_detail = ca.fetch_active()
00352
00353 def do_crl():
00354 ca_detail.generate_crl(do_manifest, fail2)
00355
00356 def do_manifest():
00357 ca_detail.generate_manifest(iterator2, fail2)
00358
00359 if ca_detail is not None and now > ca_detail.latest_crl.getNextUpdate():
00360 do_crl()
00361 else:
00362 iterator2()
00363
00364 rpki.async.iterator([x for x in ca.fetch_revoked() if now > x.latest_crl.getNextUpdate()], loop3, done3)
00365
00366 rpki.async.iterator(parent.cas(), loop2, iterator1)
00367
00368 rpki.async.iterator(self.parents(), loop1, cb)
00369
00370
00371 def update_roas(self, cb):
00372 """
00373 Generate or update ROAs for this self's route_origin objects.
00374 """
00375
00376 def loop(iterator, route_origin):
00377 route_origin.update_roa(iterator)
00378
00379 rpki.async.iterator(self.route_origins(), loop, cb)
00380
00381
00382 class bsc_elt(data_elt):
00383 """
00384 <bsc/> (Business Signing Context) element.
00385 """
00386
00387 element_name = "bsc"
00388 attributes = ("action", "tag", "self_id", "bsc_id", "key_type", "hash_alg", "key_length")
00389 elements = ("signing_cert", "signing_cert_crl", "pkcs10_request")
00390 booleans = ("generate_keypair",)
00391
00392 sql_template = rpki.sql.template("bsc", "bsc_id", "self_id", "hash_alg",
00393 ("private_key_id", rpki.x509.RSA),
00394 ("pkcs10_request", rpki.x509.PKCS10),
00395 ("signing_cert", rpki.x509.X509),
00396 ("signing_cert_crl", rpki.x509.CRL))
00397
00398 private_key_id = None
00399 pkcs10_request = None
00400 signing_cert = None
00401 signing_cert_crl = None
00402
00403 def repositories(self):
00404 """Fetch all repository objects that link to this BSC object."""
00405 return repository_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
00406
00407 def parents(self):
00408 """Fetch all parent objects that link to this BSC object."""
00409 return parent_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
00410
00411 def children(self):
00412 """Fetch all child objects that link to this BSC object."""
00413 return child_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
00414
00415 def serve_pre_save_hook(self, q_pdu, r_pdu, cb, eb):
00416 """
00417 Extra server actions for bsc_elt -- handle key generation. For
00418 now this only allows RSA with SHA-256.
00419 """
00420 if q_pdu.generate_keypair:
00421 assert q_pdu.key_type in (None, "rsa") and q_pdu.hash_alg in (None, "sha256")
00422 self.private_key_id = rpki.x509.RSA.generate(keylength = q_pdu.key_length or 2048)
00423 self.pkcs10_request = rpki.x509.PKCS10.create(self.private_key_id)
00424 r_pdu.pkcs10_request = self.pkcs10_request
00425 cb()
00426
00427 class parent_elt(data_elt):
00428 """
00429 <parent/> element.
00430 """
00431
00432 element_name = "parent"
00433 attributes = ("action", "tag", "self_id", "parent_id", "bsc_id", "repository_id",
00434 "peer_contact_uri", "sia_base", "sender_name", "recipient_name")
00435 elements = ("bpki_cms_cert", "bpki_cms_glue", "bpki_https_cert", "bpki_https_glue")
00436 booleans = ("rekey", "reissue", "revoke")
00437
00438 sql_template = rpki.sql.template("parent", "parent_id", "self_id", "bsc_id", "repository_id",
00439 ("bpki_cms_cert", rpki.x509.X509), ("bpki_cms_glue", rpki.x509.X509),
00440 ("bpki_https_cert", rpki.x509.X509), ("bpki_https_glue", rpki.x509.X509),
00441 "peer_contact_uri", "sia_base", "sender_name", "recipient_name")
00442
00443 bpki_cms_cert = None
00444 bpki_cms_glue = None
00445 bpki_https_cert = None
00446 bpki_https_glue = None
00447
00448 def repository(self):
00449 """Fetch repository object to which this parent object links."""
00450 return repository_elt.sql_fetch(self.gctx, self.repository_id)
00451
00452 def cas(self):
00453 """Fetch all CA objects that link to this parent object."""
00454 return rpki.rpki_engine.ca_obj.sql_fetch_where(self.gctx, "parent_id = %s", (self.parent_id,))
00455
00456 def serve_post_save_hook(self, q_pdu, r_pdu, cb, eb):
00457 """
00458 Extra server actions for parent_elt.
00459 """
00460 if q_pdu.rekey:
00461 self.serve_rekey(cb, eb)
00462 elif q_pdu.revoke:
00463 self.serve_revoke(cb, eb)
00464 else:
00465 self.unimplemented_control("reissue")
00466 cb()
00467
00468 def serve_rekey(self, cb, eb):
00469 """
00470 Handle a left-right rekey action for this parent.
00471 """
00472
00473 def loop(iterator, ca):
00474 ca.rekey(iterator, eb)
00475
00476 rpki.async.iterator(self.cas(), loop, cb)
00477
00478 def serve_revoke(self, cb, eb):
00479 """
00480 Handle a left-right revoke action for this parent.
00481 """
00482
00483 def loop(iterator, ca):
00484 ca.revoke(iterator, eb)
00485
00486 rpki.async.iterator(self.cas(), loop, cb)
00487
00488 def query_up_down(self, q_pdu, cb, eb):
00489 """
00490 Client code for sending one up-down query PDU to this parent.
00491 """
00492
00493 rpki.log.trace()
00494
00495 bsc = self.bsc()
00496 if bsc is None:
00497 raise rpki.exceptions.BSCNotFound, "Could not find BSC %s" % self.bsc_id
00498
00499 q_msg = rpki.up_down.message_pdu.make_query(
00500 payload = q_pdu,
00501 sender = self.sender_name,
00502 recipient = self.recipient_name)
00503
00504 q_cms = rpki.up_down.cms_msg.wrap(q_msg, bsc.private_key_id,
00505 bsc.signing_cert,
00506 bsc.signing_cert_crl)
00507
00508 def unwrap(der):
00509 r_msg = rpki.up_down.cms_msg.unwrap(der, (self.gctx.bpki_ta,
00510 self.self().bpki_cert, self.self().bpki_glue,
00511 self.bpki_cms_cert, self.bpki_cms_glue))
00512 r_msg.payload.check_response()
00513 cb(r_msg)
00514
00515 rpki.https.client(server_ta = (self.gctx.bpki_ta,
00516 self.self().bpki_cert, self.self().bpki_glue,
00517 self.bpki_https_cert, self.bpki_https_glue),
00518 client_key = bsc.private_key_id,
00519 client_cert = bsc.signing_cert,
00520 msg = q_cms,
00521 url = self.peer_contact_uri,
00522 callback = unwrap,
00523 errback = eb)
00524
00525 class child_elt(data_elt):
00526 """
00527 <child/> element.
00528 """
00529
00530 element_name = "child"
00531 attributes = ("action", "tag", "self_id", "child_id", "bsc_id")
00532 elements = ("bpki_cert", "bpki_glue")
00533 booleans = ("reissue", )
00534
00535 sql_template = rpki.sql.template("child", "child_id", "self_id", "bsc_id",
00536 ("bpki_cert", rpki.x509.X509),
00537 ("bpki_glue", rpki.x509.X509))
00538
00539 bpki_cert = None
00540 bpki_glue = None
00541 clear_https_ta_cache = False
00542
00543 def child_certs(self, ca_detail = None, ski = None, unique = False):
00544 """Fetch all child_cert objects that link to this child object."""
00545 return rpki.rpki_engine.child_cert_obj.fetch(self.gctx, self, ca_detail, ski, unique)
00546
00547 def parents(self):
00548 """Fetch all parent objects that link to self object to which this child object links."""
00549 return parent_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00550
00551 def ca_from_class_name(self, class_name):
00552 """
00553 Fetch the CA corresponding to an up-down class_name.
00554 """
00555 if not class_name.isdigit():
00556 raise rpki.exceptions.BadClassNameSyntax, "Bad class name %s" % class_name
00557 ca = rpki.rpki_engine.ca_obj.sql_fetch(self.gctx, long(class_name))
00558 if ca is None:
00559 raise rpki.exceptions.ClassNameUnknown, "Unknown class name %s" % class_name
00560 parent = ca.parent()
00561 if self.self_id != parent.self_id:
00562 raise rpki.exceptions.ClassNameMismatch, "Class name mismatch: child.self_id = %d, parent.self_id = %d" % (self.self_id, parent.self_id)
00563 return ca
00564
00565 def serve_post_save_hook(self, q_pdu, r_pdu, cb, eb):
00566 """
00567 Extra server actions for child_elt.
00568 """
00569 self.unimplemented_control("reissue")
00570 if self.clear_https_ta_cache:
00571 self.gctx.clear_https_ta_cache()
00572 self.clear_https_ta_cache = False
00573 cb()
00574
00575 def endElement(self, stack, name, text):
00576 """
00577 Handle subelements of <child/> element. These require special
00578 handling because modifying them invalidates the HTTPS trust anchor
00579 cache.
00580 """
00581 rpki.xml_utils.data_elt.endElement(self, stack, name, text)
00582 if name in self.elements:
00583 self.clear_https_ta_cache = True
00584
00585 def serve_up_down(self, query, callback):
00586 """
00587 Outer layer of server handling for one up-down PDU from this child.
00588 """
00589
00590 rpki.log.trace()
00591
00592 bsc = self.bsc()
00593 if bsc is None:
00594 raise rpki.exceptions.BSCNotFound, "Could not find BSC %s" % self.bsc_id
00595 q_msg = rpki.up_down.cms_msg.unwrap(query, (self.gctx.bpki_ta,
00596 self.self().bpki_cert, self.self().bpki_glue,
00597 self.bpki_cert, self.bpki_glue))
00598 q_msg.payload.gctx = self.gctx
00599 if enforce_strict_up_down_xml_sender and q_msg.sender != str(self.child_id):
00600 raise rpki.exceptions.BadSender, "Unexpected XML sender %s" % q_msg.sender
00601
00602 def done(r_msg):
00603
00604
00605
00606
00607
00608 r_cms = rpki.up_down.cms_msg.wrap(r_msg, bsc.private_key_id,
00609 bsc.signing_cert, bsc.signing_cert_crl)
00610 callback(r_cms)
00611
00612 try:
00613 q_msg.serve_top_level(self, done)
00614 except (rpki.async.ExitNow, SystemExit):
00615 raise
00616 except rpki.exceptions.NoActiveCA, data:
00617 done(q_msg.serve_error(data))
00618 except Exception, data:
00619 rpki.log.error(traceback.format_exc())
00620 done(q_msg.serve_error(data))
00621
00622 class repository_elt(data_elt):
00623 """
00624 <repository/> element.
00625 """
00626
00627 element_name = "repository"
00628 attributes = ("action", "tag", "self_id", "repository_id", "bsc_id", "peer_contact_uri")
00629 elements = ("bpki_cms_cert", "bpki_cms_glue", "bpki_https_cert", "bpki_https_glue")
00630
00631 sql_template = rpki.sql.template("repository", "repository_id", "self_id", "bsc_id", "peer_contact_uri",
00632 ("bpki_cms_cert", rpki.x509.X509), ("bpki_cms_glue", rpki.x509.X509),
00633 ("bpki_https_cert", rpki.x509.X509), ("bpki_https_glue", rpki.x509.X509))
00634
00635 bpki_cms_cert = None
00636 bpki_cms_glue = None
00637 bpki_https_cert = None
00638 bpki_https_glue = None
00639
00640 def parents(self):
00641 """Fetch all parent objects that link to this repository object."""
00642 return parent_elt.sql_fetch_where(self.gctx, "repository_id = %s", (self.repository_id,))
00643
00644 def call_pubd(self, callback, errback, *pdus):
00645 """
00646 Send a message to publication daemon and return the response.
00647 """
00648 rpki.log.trace()
00649 bsc = self.bsc()
00650 q_msg = rpki.publication.msg(pdus)
00651 q_msg.type = "query"
00652 q_cms = rpki.publication.cms_msg.wrap(q_msg, bsc.private_key_id, bsc.signing_cert, bsc.signing_cert_crl)
00653 bpki_ta_path = (self.gctx.bpki_ta, self.self().bpki_cert, self.self().bpki_glue, self.bpki_https_cert, self.bpki_https_glue)
00654
00655 def done(r_cms):
00656 try:
00657 r_msg = rpki.publication.cms_msg.unwrap(r_cms, bpki_ta_path)
00658 if len(r_msg) != 1 or isinstance(r_msg[0], rpki.publication.report_error_elt):
00659 raise rpki.exceptions.BadPublicationReply, "Unexpected response from pubd: %s" % msg
00660 callback()
00661 except (rpki.async.ExitNow, SystemExit):
00662 raise
00663 except Exception, edata:
00664 errback(edata)
00665
00666 rpki.https.client(
00667 client_key = bsc.private_key_id,
00668 client_cert = bsc.signing_cert,
00669 server_ta = bpki_ta_path,
00670 url = self.peer_contact_uri,
00671 msg = q_cms,
00672 callback = done,
00673 errback = errback)
00674
00675 def publish(self, obj, uri, callback, errback):
00676 """
00677 Publish one object in the repository.
00678 """
00679 rpki.log.trace()
00680 rpki.log.info("Publishing %s as %s" % (repr(obj), repr(uri)))
00681 self.call_pubd(callback, errback, rpki.publication.obj2elt[type(obj)].make_pdu(action = "publish", uri = uri, payload = obj))
00682
00683 def withdraw(self, obj, uri, callback, errback):
00684 """
00685 Withdraw one object from the repository.
00686 """
00687 rpki.log.trace()
00688 rpki.log.info("Withdrawing %s from at %s" % (repr(obj), repr(uri)))
00689 self.call_pubd(callback, errback, rpki.publication.obj2elt[type(obj)].make_pdu(action = "withdraw", uri = uri))
00690
00691 class route_origin_elt(data_elt):
00692 """
00693 <route_origin/> element.
00694 """
00695
00696 element_name = "route_origin"
00697 attributes = ("action", "tag", "self_id", "route_origin_id", "as_number", "ipv4", "ipv6")
00698 booleans = ("suppress_publication",)
00699
00700 sql_template = rpki.sql.template("route_origin", "route_origin_id", "ca_detail_id",
00701 "self_id", "as_number",
00702 ("roa", rpki.x509.ROA),
00703 ("cert", rpki.x509.X509))
00704
00705 ca_detail_id = None
00706 cert = None
00707 roa = None
00708
00709
00710
00711 publish_ee_separately = False
00712
00713 def sql_fetch_hook(self):
00714 """
00715 Extra SQL fetch actions for route_origin_elt -- handle prefix list.
00716 """
00717 self.ipv4 = rpki.resource_set.roa_prefix_set_ipv4.from_sql(
00718 self.gctx.sql,
00719 """
00720 SELECT address, prefixlen, max_prefixlen FROM route_origin_prefix
00721 WHERE route_origin_id = %s AND address NOT LIKE '%:%'
00722 """, (self.route_origin_id,))
00723 self.ipv6 = rpki.resource_set.roa_prefix_set_ipv6.from_sql(
00724 self.gctx.sql,
00725 """
00726 SELECT address, prefixlen, max_prefixlen FROM route_origin_prefix
00727 WHERE route_origin_id = %s AND address LIKE '%:%'
00728 """, (self.route_origin_id,))
00729
00730 def sql_insert_hook(self):
00731 """
00732 Extra SQL insert actions for route_origin_elt -- handle address
00733 ranges.
00734 """
00735 if self.ipv4 or self.ipv6:
00736 self.gctx.sql.executemany(
00737 """
00738 INSERT route_origin_prefix (route_origin_id, address, prefixlen, max_prefixlen)
00739 VALUES (%s, %s, %s, %s)
00740 """,
00741 ((self.route_origin_id, x.address, x.prefixlen, x.max_prefixlen)
00742 for x in (self.ipv4 or []) + (self.ipv6 or [])))
00743
00744 def sql_delete_hook(self):
00745 """
00746 Extra SQL delete actions for route_origin_elt -- handle address
00747 ranges.
00748 """
00749 self.gctx.sql.execute("DELETE FROM route_origin_prefix WHERE route_origin_id = %s", (self.route_origin_id,))
00750
00751 def ca_detail(self):
00752 """Fetch all ca_detail objects that link to this route_origin object."""
00753 return rpki.rpki_engine.ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
00754
00755 def serve_post_save_hook(self, q_pdu, r_pdu, cb, eb):
00756 """
00757 Extra server actions for route_origin_elt.
00758 """
00759 self.unimplemented_control("suppress_publication")
00760 cb()
00761
00762 def startElement(self, stack, name, attrs):
00763 """
00764 Handle <route_origin/> element. This requires special processing
00765 due to the data types of some of the attributes.
00766 """
00767 assert name == "route_origin", "Unexpected name %s, stack %s" % (name, stack)
00768 self.read_attrs(attrs)
00769 if self.as_number is not None:
00770 self.as_number = long(self.as_number)
00771 if self.ipv4 is not None:
00772 self.ipv4 = rpki.resource_set.roa_prefix_set_ipv4(self.ipv4)
00773 if self.ipv6 is not None:
00774 self.ipv6 = rpki.resource_set.roa_prefix_set_ipv6(self.ipv6)
00775
00776 def update_roa(self, callback):
00777 """
00778 Bring this route_origin's ROA up to date if necesssary.
00779 """
00780
00781 def lose(e):
00782 rpki.log.error(traceback.format_exc())
00783 rpki.log.warn("Could not update ROA %r, skipping: %s" % (self, e))
00784 callback()
00785 return
00786
00787 if self.roa is None:
00788 self.generate_roa(callback, lose)
00789 return
00790
00791 ca_detail = self.ca_detail()
00792
00793 if ca_detail is None or ca_detail.state != "active":
00794 self.regenerate_roa(callback, lose)
00795 return
00796
00797 regen_margin = rpki.sundial.timedelta(seconds = self.self().regen_margin)
00798
00799 if rpki.sundial.now() + regen_margin > self.cert.getNotAfter():
00800 self.regenerate_roa(callback, lose)
00801 return
00802
00803 ca_resources = ca_detail.latest_ca_cert.get_3779resources()
00804 ee_resources = self.cert.get_3779resources()
00805
00806 if ee_resources.oversized(ca_resources):
00807 self.regenerate_roa(callback, lose)
00808 return
00809
00810 v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4()
00811 v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6()
00812
00813 if ee_resources.v4 != v4 or ee_resources.v6 != v6:
00814 self.regenerate_roa(callback, lose)
00815 return
00816
00817 callback()
00818
00819 def generate_roa(self, callback, errback):
00820 """
00821 Generate a ROA based on this <route_origin/> object.
00822
00823 At present this does not support ROAs with multiple signatures
00824 (neither does the current CMS code).
00825
00826 At present we have no way of performing a direct lookup from a
00827 desired set of resources to a covering certificate, so we have to
00828 search. This could be quite slow if we have a lot of active
00829 ca_detail objects. Punt on the issue for now, revisit if
00830 profiling shows this as a hotspot.
00831
00832 Once we have the right covering certificate, we generate the ROA
00833 payload, generate a new EE certificate, use the EE certificate to
00834 sign the ROA payload, publish the result, then throw away the
00835 private key for the EE cert, all per the ROA specification. This
00836 implies that generating a lot of ROAs will tend to thrash
00837 /dev/random, but there is not much we can do about that.
00838 """
00839
00840 if self.ipv4 is None and self.ipv6 is None:
00841 rpki.log.warn("Can't generate ROA for empty prefix list")
00842 return
00843
00844
00845
00846
00847
00848 v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4()
00849 v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6()
00850
00851 ca_detail = self.ca_detail()
00852 if ca_detail is None or ca_detail.state != "active":
00853 ca_detail = None
00854 for parent in self.self().parents():
00855 for ca in parent.cas():
00856 ca_detail = ca.fetch_active()
00857 if ca_detail is not None:
00858 resources = ca_detail.latest_ca_cert.get_3779resources()
00859 if v4.issubset(resources.v4) and v6.issubset(resources.v6):
00860 break
00861 ca_detail = None
00862 if ca_detail is not None:
00863 break
00864
00865 if ca_detail is None:
00866 rpki.log.warn("generate_roa() could not find a certificate covering %s %s" % (v4, v6))
00867 return
00868
00869 ca = ca_detail.ca()
00870
00871 resources = rpki.resource_set.resource_bag(v4 = v4, v6 = v6)
00872
00873 keypair = rpki.x509.RSA.generate()
00874
00875 sia = ((rpki.oids.name2oid["id-ad-signedObject"], ("uri", self.roa_uri(ca, keypair))),)
00876
00877 self.cert = ca_detail.issue_ee(ca, resources, keypair.get_RSApublic(), sia = sia)
00878 self.roa = rpki.x509.ROA.build(self.as_number, self.ipv4, self.ipv6, keypair, (self.cert,))
00879 self.ca_detail_id = ca_detail.ca_detail_id
00880 self.sql_store()
00881
00882 repository = ca.parent().repository()
00883
00884 def one():
00885 repository.publish(self.cert, self.ee_uri(ca), two, errback)
00886
00887 def two():
00888 ca_detail.generate_manifest(callback, errback)
00889
00890 repository.publish(self.roa, self.roa_uri(ca),
00891 one if self.publish_ee_separately else two,
00892 errback)
00893
00894 def withdraw_roa(self, callback, errback, regenerate = False):
00895 """
00896 Withdraw ROA associated with this route_origin.
00897
00898 In order to preserve make-before-break properties without
00899 duplicating code, this method also handles generating a
00900 replacement ROA when requested.
00901 """
00902
00903 ca_detail = self.ca_detail()
00904 ca = ca_detail.ca()
00905 repository = ca.parent().repository()
00906 cert = self.cert
00907 roa = self.roa
00908 roa_uri = self.roa_uri(ca)
00909 ee_uri = self.ee_uri(ca)
00910
00911 if ca_detail.state != 'active':
00912 self.ca_detail_id = None
00913
00914 def one():
00915 rpki.log.debug("Withdrawing ROA and revoking its EE cert")
00916 rpki.rpki_engine.revoked_cert_obj.revoke(cert = cert, ca_detail = ca_detail)
00917 repository.withdraw(roa, roa_uri,
00918 two if self.publish_ee_separately else three,
00919 errback)
00920
00921 def two():
00922 repository.withdraw(cert, ee_uri, three, errback)
00923
00924 def three():
00925 self.gctx.sql.sweep()
00926 ca_detail.generate_crl(four, errback)
00927
00928 def four():
00929 ca_detail.generate_manifest(callback, errback)
00930
00931 if regenerate:
00932 self.generate_roa(one, errback)
00933 else:
00934 one()
00935
00936 def regenerate_roa(self, callback, errback):
00937 """
00938 Reissue ROA associated with this route_origin.
00939 """
00940 if self.ca_detail() is None:
00941 self.generate_roa(callback, errback)
00942 else:
00943 self.withdraw_roa(callback, errback, regenerate = True)
00944
00945 def roa_uri(self, ca, key = None):
00946 """Return the publication URI for this route_origin's ROA."""
00947 return ca.sia_uri + self.roa_uri_tail(key)
00948
00949 def roa_uri_tail(self, key = None):
00950 """Return the tail (filename portion) of the publication URI for this route_origin's ROA."""
00951 return (key or self.cert).gSKI() + ".roa"
00952
00953 def ee_uri_tail(self):
00954 """Return the tail (filename) portion of the URI for this route_origin's ROA's EE certificate."""
00955 return self.cert.gSKI() + ".cer"
00956
00957 def ee_uri(self, ca):
00958 """Return the publication URI for this route_origin's ROA's EE certificate."""
00959 return ca.sia_uri + self.ee_uri_tail()
00960
00961 class list_resources_elt(rpki.xml_utils.base_elt, left_right_namespace):
00962 """
00963 <list_resources/> element.
00964 """
00965
00966 element_name = "list_resources"
00967 attributes = ("self_id", "tag", "child_id", "valid_until", "asn", "ipv4", "ipv6", "subject_name")
00968 valid_until = None
00969
00970 def startElement(self, stack, name, attrs):
00971 """
00972 Handle <list_resources/> element. This requires special handling
00973 due to the data types of some of the attributes.
00974 """
00975 assert name == "list_resources", "Unexpected name %s, stack %s" % (name, stack)
00976 self.read_attrs(attrs)
00977 if isinstance(self.valid_until, str):
00978 self.valid_until = rpki.sundial.datetime.fromXMLtime(self.valid_until)
00979 if self.asn is not None:
00980 self.asn = rpki.resource_set.resource_set_as(self.asn)
00981 if self.ipv4 is not None:
00982 self.ipv4 = rpki.resource_set.resource_set_ipv4(self.ipv4)
00983 if self.ipv6 is not None:
00984 self.ipv6 = rpki.resource_set.resource_set_ipv6(self.ipv6)
00985
00986 def toXML(self):
00987 """
00988 Generate <list_resources/> element. This requires special
00989 handling due to the data types of some of the attributes.
00990 """
00991 elt = self.make_elt()
00992 if isinstance(self.valid_until, int):
00993 elt.set("valid_until", self.valid_until.toXMLtime())
00994 return elt
00995
00996 class report_error_elt(rpki.xml_utils.base_elt, left_right_namespace):
00997 """
00998 <report_error/> element.
00999 """
01000
01001 element_name = "report_error"
01002 attributes = ("tag", "self_id", "error_code")
01003
01004 @classmethod
01005 def from_exception(cls, e, self_id = None):
01006 """
01007 Generate a <report_error/> element from an exception.
01008 """
01009 self = cls()
01010 self.self_id = self_id
01011 self.error_code = e.__class__.__name__
01012 self.text = str(e)
01013 return self
01014
01015 class msg(rpki.xml_utils.msg, left_right_namespace):
01016 """
01017 Left-right PDU.
01018 """
01019
01020
01021
01022 version = 1
01023
01024
01025
01026 pdus = dict((x.element_name, x)
01027 for x in (self_elt, child_elt, parent_elt, bsc_elt, repository_elt,
01028 route_origin_elt, list_resources_elt, report_error_elt))
01029
01030 def serve_top_level(self, gctx, cb):
01031 """
01032 Serve one msg PDU.
01033 """
01034 r_msg = self.__class__()
01035 r_msg.type = "reply"
01036
01037 def loop(iterator, q_pdu):
01038
01039 def fail(e):
01040 rpki.log.error(traceback.format_exc())
01041 r_msg.append(report_error_elt.from_exception(e, self_id = q_pdu.self_id))
01042 cb(r_msg)
01043
01044 try:
01045 q_pdu.gctx = gctx
01046 q_pdu.serve_dispatch(r_msg, iterator, fail)
01047 except (rpki.async.ExitNow, SystemExit):
01048 raise
01049 except Exception, edata:
01050 fail(edata)
01051
01052 def done():
01053 cb(r_msg)
01054
01055 rpki.async.iterator(self, loop, done)
01056
01057 class sax_handler(rpki.xml_utils.sax_handler):
01058 """
01059 SAX handler for Left-Right protocol.
01060 """
01061
01062 pdu = msg
01063 name = "msg"
01064 version = "1"
01065
01066 class cms_msg(rpki.x509.XML_CMS_object):
01067 """
01068 Class to hold a CMS-signed left-right PDU.
01069 """
01070
01071 encoding = "us-ascii"
01072 schema = rpki.relaxng.left_right
01073 saxify = sax_handler.saxify