00001 """
00002 Global context for rpkid.
00003
00004 $Id: rpki_engine.py 2510 2009-06-09 20:25:16Z 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, lxml.etree, re
00036 import rpki.resource_set, rpki.up_down, rpki.left_right, rpki.x509, rpki.sql
00037 import rpki.https, rpki.config, rpki.exceptions, rpki.relaxng, rpki.log, rpki.async
00038
00039 class rpkid_context(object):
00040 """
00041 A container for various global rpkid parameters.
00042 """
00043
00044 def __init__(self, cfg):
00045
00046 self.sql = rpki.sql.session(cfg)
00047
00048 self.bpki_ta = rpki.x509.X509(Auto_file = cfg.get("bpki-ta"))
00049 self.irdb_cert = rpki.x509.X509(Auto_file = cfg.get("irdb-cert"))
00050 self.irbe_cert = rpki.x509.X509(Auto_file = cfg.get("irbe-cert"))
00051 self.rpkid_cert = rpki.x509.X509(Auto_file = cfg.get("rpkid-cert"))
00052 self.rpkid_key = rpki.x509.RSA( Auto_file = cfg.get("rpkid-key"))
00053
00054 self.irdb_url = cfg.get("irdb-url")
00055
00056 self.https_server_host = cfg.get("server-host", "")
00057 self.https_server_port = int(cfg.get("server-port", "4433"))
00058
00059 self.publication_kludge_base = cfg.get("publication-kludge-base", "publication/")
00060
00061 def irdb_query(self, q_pdu, callback, errback):
00062 """
00063 Perform an IRDB callback query.
00064 """
00065
00066 rpki.log.trace()
00067
00068 q_msg = rpki.left_right.msg()
00069 q_msg.type = "query"
00070 q_msg.append(q_pdu)
00071 q_cms = rpki.left_right.cms_msg.wrap(q_msg, self.rpkid_key, self.rpkid_cert)
00072
00073 def unwrap(der):
00074 r_msg = rpki.left_right.cms_msg.unwrap(der, (self.bpki_ta, self.irdb_cert))
00075 if r_msg.type != "reply" or [r_pdu for r_pdu in r_msg if type(r_pdu) is not type(q_pdu)]:
00076 errback(rpki.exceptions.BadIRDBReply(
00077 "Unexpected response to IRDB query: %s" % lxml.etree.tostring(r_msg.toXML(), pretty_print = True, encoding = "us-ascii")))
00078 else:
00079 callback(r_msg)
00080
00081 rpki.https.client(
00082 server_ta = (self.bpki_ta, self.irdb_cert),
00083 client_key = self.rpkid_key,
00084 client_cert = self.rpkid_cert,
00085 url = self.irdb_url,
00086 msg = q_cms,
00087 callback = unwrap,
00088 errback = errback)
00089
00090 def irdb_query_child_resources(self, self_handle, child_handle, callback, errback):
00091 """
00092 Ask IRDB about a child's resources.
00093 """
00094
00095 rpki.log.trace()
00096
00097 q_pdu = rpki.left_right.list_resources_elt()
00098 q_pdu.self_handle = self_handle
00099 q_pdu.child_handle = child_handle
00100
00101 def done(r_msg):
00102 if len(r_msg) == 1:
00103 callback(rpki.resource_set.resource_bag(
00104 asn = r_msg[0].asn,
00105 v4 = r_msg[0].ipv4,
00106 v6 = r_msg[0].ipv6,
00107 valid_until = r_msg[0].valid_until))
00108 else:
00109 errback(rpki.exceptions.BadIRDBReply(
00110 "Expected exactly one PDU from IRDB: %s" % lxml.etree.tostring(r_msg.toXML(), pretty_print = True, encoding = "us-ascii")))
00111
00112 self.irdb_query(q_pdu, done, errback)
00113
00114 def irdb_query_roa_requests(self, self_handle, callback, errback):
00115 """
00116 Ask IRDB about self's ROA requests.
00117 """
00118
00119 rpki.log.trace()
00120
00121 q_pdu = rpki.left_right.list_roa_requests_elt()
00122 q_pdu.self_handle = self_handle
00123
00124 self.irdb_query(q_pdu, callback, errback)
00125
00126 def left_right_handler(self, query, path, cb):
00127 """
00128 Process one left-right PDU.
00129 """
00130
00131 rpki.log.trace()
00132
00133 def done(r_msg):
00134 reply = rpki.left_right.cms_msg.wrap(r_msg, self.rpkid_key, self.rpkid_cert)
00135 self.sql.sweep()
00136 cb(200, reply)
00137
00138 try:
00139 self.sql.ping()
00140 q_msg = rpki.left_right.cms_msg.unwrap(query, (self.bpki_ta, self.irbe_cert))
00141 if q_msg.type != "query":
00142 raise rpki.exceptions.BadQuery, "Message type is not query"
00143 q_msg.serve_top_level(self, done)
00144 except (rpki.async.ExitNow, SystemExit):
00145 raise
00146 except Exception, data:
00147 rpki.log.error(traceback.format_exc())
00148 cb(500, "Unhandled exception %s" % data)
00149
00150 up_down_url_regexp = re.compile("/up-down/([-A-Z0-9_]+)/([-A-Z0-9_]+)$", re.I)
00151
00152 def up_down_handler(self, query, path, cb):
00153 """
00154 Process one up-down PDU.
00155 """
00156
00157 rpki.log.trace()
00158
00159 def done(reply):
00160 self.sql.sweep()
00161 cb(200, reply)
00162
00163 try:
00164 self.sql.ping()
00165 match = self.up_down_url_regexp.search(path)
00166 if match is None:
00167 raise rpki.exceptions.BadContactURL, "Bad path: %s" % path
00168 self_handle, child_handle = match.groups()
00169 child = rpki.left_right.child_elt.sql_fetch_where1(self, "self.self_handle = %s AND child.child_handle = %s AND child.self_id = self.self_id",
00170 (self_handle, child_handle), "self")
00171 if child is None:
00172 raise rpki.exceptions.ChildNotFound, "Could not find child %s" % child_handle
00173 child.serve_up_down(query, done)
00174 except (rpki.async.ExitNow, SystemExit):
00175 raise
00176 except Exception, data:
00177 rpki.log.error(traceback.format_exc())
00178 cb(400, "Could not process PDU: %s" % data)
00179
00180 def cronjob_handler(self, query, path, cb):
00181 """
00182 Periodic tasks. This is somewhat obsolete now that we have
00183 internal timers, but the test framework still uses this, and I
00184 haven't yet refactored this code to use the new timers.
00185 """
00186
00187 rpki.log.trace()
00188 self.sql.ping()
00189
00190 def loop(iterator, s):
00191
00192 def one():
00193 rpki.log.debug("Self %s polling parents" % s.self_id)
00194 s.client_poll(two)
00195
00196 def two():
00197 rpki.log.debug("Self %s updating children" % s.self_id)
00198 s.update_children(three)
00199
00200 def three():
00201 rpki.log.debug("Self %s updating ROAs" % s.self_id)
00202 s.update_roas(four)
00203
00204 def four():
00205 rpki.log.debug("Self %s regenerating CRLs and manifests" % s.self_id)
00206 s.regenerate_crls_and_manifests(iterator)
00207
00208 one()
00209
00210 def done():
00211 self.sql.sweep()
00212 cb(200, "OK")
00213
00214 rpki.async.iterator(rpki.left_right.self_elt.sql_fetch_all(self), loop, done)
00215
00216
00217
00218 https_ta_cache = None
00219
00220 def clear_https_ta_cache(self):
00221 """
00222 Clear dynamic TLS trust anchors.
00223 """
00224
00225 if self.https_ta_cache is not None:
00226 rpki.log.debug("Clearing HTTPS trusted cert cache")
00227 self.https_ta_cache = None
00228
00229 def build_https_ta_cache(self):
00230 """
00231 Build dynamic TLS trust anchors.
00232 """
00233
00234 if self.https_ta_cache is None:
00235
00236 selves = rpki.left_right.self_elt.sql_fetch_all(self)
00237 children = rpki.left_right.child_elt.sql_fetch_all(self)
00238
00239 self.https_ta_cache = rpki.https.build_https_ta_cache(
00240 [c.bpki_cert for c in children if c.bpki_cert is not None] +
00241 [c.bpki_glue for c in children if c.bpki_glue is not None] +
00242 [s.bpki_cert for s in selves if s.bpki_cert is not None] +
00243 [s.bpki_glue for s in selves if s.bpki_glue is not None] +
00244 [self.irbe_cert, self.irdb_cert, self.bpki_ta])
00245
00246 return self.https_ta_cache
00247
00248
00249 class ca_obj(rpki.sql.sql_persistent):
00250 """
00251 Internal CA object.
00252 """
00253
00254 sql_template = rpki.sql.template(
00255 "ca",
00256 "ca_id",
00257 "last_crl_sn",
00258 ("next_crl_update", rpki.sundial.datetime),
00259 "last_issued_sn", "last_manifest_sn",
00260 ("next_manifest_update", rpki.sundial.datetime),
00261 "sia_uri", "parent_id", "parent_resource_class")
00262
00263 last_crl_sn = 0
00264 last_issued_sn = 0
00265 last_manifest_sn = 0
00266
00267 def parent(self):
00268 """Fetch parent object to which this CA object links."""
00269 return rpki.left_right.parent_elt.sql_fetch(self.gctx, self.parent_id)
00270
00271 def ca_details(self):
00272 """Fetch all ca_detail objects that link to this CA object."""
00273 return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s", (self.ca_id,))
00274
00275 def fetch_pending(self):
00276 """Fetch the pending ca_details for this CA, if any."""
00277 return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'pending'", (self.ca_id,))
00278
00279 def fetch_active(self):
00280 """Fetch the active ca_detail for this CA, if any."""
00281 return ca_detail_obj.sql_fetch_where1(self.gctx, "ca_id = %s AND state = 'active'", (self.ca_id,))
00282
00283 def fetch_deprecated(self):
00284 """Fetch deprecated ca_details for this CA, if any."""
00285 return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'deprecated'", (self.ca_id,))
00286
00287 def fetch_revoked(self):
00288 """Fetch revoked ca_details for this CA, if any."""
00289 return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'revoked'", (self.ca_id,))
00290
00291 def construct_sia_uri(self, parent, rc):
00292 """
00293 Construct the sia_uri value for this CA given configured
00294 information and the parent's up-down protocol list_response PDU.
00295 """
00296
00297 sia_uri = rc.suggested_sia_head and rc.suggested_sia_head.rsync()
00298 if not sia_uri or not sia_uri.startswith(parent.sia_base):
00299 sia_uri = parent.sia_base
00300 elif not sia_uri.endswith("/"):
00301 raise rpki.exceptions.BadURISyntax, "SIA URI must end with a slash: %s" % sia_uri
00302 return sia_uri + str(self.ca_id) + "/"
00303
00304 def check_for_updates(self, parent, rc, cb, eb):
00305 """
00306 Parent has signaled continued existance of a resource class we
00307 already knew about, so we need to check for an updated
00308 certificate, changes in resource coverage, revocation and reissue
00309 with the same key, etc.
00310 """
00311
00312 sia_uri = self.construct_sia_uri(parent, rc)
00313 sia_uri_changed = self.sia_uri != sia_uri
00314 if sia_uri_changed:
00315 self.sia_uri = sia_uri
00316 self.sql_mark_dirty()
00317
00318 rc_resources = rc.to_resource_bag()
00319 cert_map = dict((c.cert.get_SKI(), c) for c in rc.certs)
00320
00321 def loop(iterator, ca_detail):
00322
00323 ski = ca_detail.latest_ca_cert.get_SKI()
00324
00325 if ski not in cert_map:
00326 rpki.log.warn("Certificate in database missing from list_response, class %s, SKI %s, maybe parent certificate went away?"
00327 % (repr(rc.class_name), ca_detail.latest_ca_cert.gSKI()))
00328 ca_detail.delete(self, parent.repository(), iterator, eb)
00329 return
00330
00331 def cleanup():
00332 del cert_map[ski]
00333 iterator()
00334
00335 if ca_detail.state in ("pending", "active"):
00336 current_resources = ca_detail.latest_ca_cert.get_3779resources()
00337 if (sia_uri_changed or
00338 ca_detail.latest_ca_cert != cert_map[ski].cert or
00339 current_resources.undersized(rc_resources) or
00340 current_resources.oversized(rc_resources)):
00341 ca_detail.update(
00342 parent = parent,
00343 ca = self,
00344 rc = rc,
00345 sia_uri_changed = sia_uri_changed,
00346 old_resources = current_resources,
00347 callback = cleanup,
00348 errback = eb)
00349 return
00350
00351 cleanup()
00352
00353 def done():
00354 if cert_map:
00355 rpki.log.warn("Certificates in list_response missing from our database, class %s, SKIs %s"
00356 % (repr(rc.class_name), ", ".join(c.cert.gSKI() for c in cert_map.values())))
00357 cb()
00358
00359 rpki.async.iterator(ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND latest_ca_cert IS NOT NULL AND state != 'revoked'", (self.ca_id,)), loop, done)
00360
00361 @classmethod
00362 def create(cls, parent, rc, cb, eb):
00363 """
00364 Parent has signaled existance of a new resource class, so we need
00365 to create and set up a corresponding CA object.
00366 """
00367
00368 self = cls()
00369 self.gctx = parent.gctx
00370 self.parent_id = parent.parent_id
00371 self.parent_resource_class = rc.class_name
00372 self.sql_store()
00373 self.sia_uri = self.construct_sia_uri(parent, rc)
00374 ca_detail = ca_detail_obj.create(self)
00375
00376 def done(issue_response):
00377 ca_detail.activate(
00378 ca = self,
00379 cert = issue_response.payload.classes[0].certs[0].cert,
00380 uri = issue_response.payload.classes[0].certs[0].cert_url,
00381 callback = cb,
00382 errback = eb)
00383
00384 rpki.up_down.issue_pdu.query(parent, self, ca_detail, done, eb)
00385
00386 def delete(self, parent, callback):
00387 """
00388 The list of current resource classes received from parent does not
00389 include the class corresponding to this CA, so we need to delete
00390 it (and its little dog too...).
00391
00392 All certs published by this CA are now invalid, so need to
00393 withdraw them, the CRL, and the manifest from the repository,
00394 delete all child_cert and ca_detail records associated with this
00395 CA, then finally delete this CA itself.
00396 """
00397
00398 def fail(e):
00399 rpki.log.error(traceback.format_exc())
00400 rpki.log.warn("Could not delete CA %r, skipping: %s" % (self, e))
00401 callback()
00402
00403 def done():
00404 self.sql_delete()
00405 callback()
00406
00407 repository = parent.repository()
00408
00409 def loop(iterator, ca_detail):
00410 ca_detail.delete(self, repository, iterator, fail)
00411
00412 rpki.async.iterator(self.ca_details(), loop, done)
00413
00414 def next_serial_number(self):
00415 """
00416 Allocate a certificate serial number.
00417 """
00418 self.last_issued_sn += 1
00419 self.sql_mark_dirty()
00420 return self.last_issued_sn
00421
00422 def next_manifest_number(self):
00423 """
00424 Allocate a manifest serial number.
00425 """
00426 self.last_manifest_sn += 1
00427 self.sql_mark_dirty()
00428 return self.last_manifest_sn
00429
00430 def next_crl_number(self):
00431 """
00432 Allocate a CRL serial number.
00433 """
00434 self.last_crl_sn += 1
00435 self.sql_mark_dirty()
00436 return self.last_crl_sn
00437
00438 def rekey(self, cb, eb):
00439 """
00440 Initiate a rekey operation for this ca. Generate a new keypair.
00441 Request cert from parent using new keypair. Mark result as our
00442 active ca_detail. Reissue all child certs issued by this ca using
00443 the new ca_detail.
00444 """
00445
00446 rpki.log.trace()
00447
00448 parent = self.parent()
00449 old_detail = self.fetch_active()
00450 new_detail = ca_detail_obj.create(self)
00451
00452 def done(issue_response):
00453 new_detail.activate(
00454 ca = self,
00455 cert = issue_response.payload.classes[0].certs[0].cert,
00456 uri = issue_response.payload.classes[0].certs[0].cert_url,
00457 predecessor = old_detail,
00458 callback = cb,
00459 errback = eb)
00460
00461 rpki.up_down.issue_pdu.query(parent, self, new_detail, done, eb)
00462
00463 def revoke(self, cb, eb):
00464 """
00465 Revoke deprecated ca_detail objects associated with this ca.
00466 """
00467
00468 rpki.log.trace()
00469
00470 def loop(iterator, ca_detail):
00471 ca_detail.revoke(iterator, eb)
00472
00473 rpki.async.iterator(self.fetch_deprecated(), loop, cb)
00474
00475 class ca_detail_obj(rpki.sql.sql_persistent):
00476 """
00477 Internal CA detail object.
00478 """
00479
00480 sql_template = rpki.sql.template(
00481 "ca_detail",
00482 "ca_detail_id",
00483 ("private_key_id", rpki.x509.RSA),
00484 ("public_key", rpki.x509.RSApublic),
00485 ("latest_ca_cert", rpki.x509.X509),
00486 ("manifest_private_key_id", rpki.x509.RSA),
00487 ("manifest_public_key", rpki.x509.RSApublic),
00488 ("latest_manifest_cert", rpki.x509.X509),
00489 ("latest_manifest", rpki.x509.SignedManifest),
00490 ("latest_crl", rpki.x509.CRL),
00491 "state",
00492 "ca_cert_uri",
00493 "ca_id")
00494
00495 def sql_decode(self, vals):
00496 """
00497 Extra assertions for SQL decode of a ca_detail_obj.
00498 """
00499 rpki.sql.sql_persistent.sql_decode(self, vals)
00500 assert (self.public_key is None and self.private_key_id is None) or \
00501 self.public_key.get_DER() == self.private_key_id.get_public_DER()
00502 assert (self.manifest_public_key is None and self.manifest_private_key_id is None) or \
00503 self.manifest_public_key.get_DER() == self.manifest_private_key_id.get_public_DER()
00504
00505 def ca(self):
00506 """Fetch CA object to which this ca_detail links."""
00507 return ca_obj.sql_fetch(self.gctx, self.ca_id)
00508
00509 def child_certs(self, child = None, ski = None, unique = False):
00510 """Fetch all child_cert objects that link to this ca_detail."""
00511 return rpki.rpki_engine.child_cert_obj.fetch(self.gctx, child, self, ski, unique)
00512
00513 def revoked_certs(self):
00514 """Fetch all revoked_cert objects that link to this ca_detail."""
00515 return revoked_cert_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,))
00516
00517 def roas(self):
00518 """Fetch all ROA objects that link to this ca_detail."""
00519 return rpki.rpki_engine.roa_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,))
00520
00521 def crl_uri(self, ca):
00522 """Return publication URI for this ca_detail's CRL."""
00523 return ca.sia_uri + self.crl_uri_tail()
00524
00525 def crl_uri_tail(self):
00526 """Return tail (filename portion) of publication URI for this ca_detail's CRL."""
00527 return self.public_key.gSKI() + ".crl"
00528
00529 def manifest_uri(self, ca):
00530 """Return publication URI for this ca_detail's manifest."""
00531 return ca.sia_uri + self.public_key.gSKI() + ".mnf"
00532
00533 def activate(self, ca, cert, uri, callback, errback, predecessor = None):
00534 """
00535 Activate this ca_detail.
00536 """
00537
00538 self.latest_ca_cert = cert
00539 self.ca_cert_uri = uri.rsync()
00540 self.generate_manifest_cert(ca)
00541
00542 def did_crl():
00543 self.generate_manifest(callback = did_manifest, errback = errback)
00544
00545 def did_manifest():
00546 self.state = "active"
00547 self.sql_mark_dirty()
00548 if predecessor is None:
00549 callback()
00550 else:
00551 predecessor.state = "deprecated"
00552 predecessor.sql_mark_dirty()
00553 rpki.async.iterator(predecessor.child_certs(), do_one_child_cert, done_child_certs)
00554
00555 def do_one_child_cert(iterator, child_cert):
00556 child_cert.reissue(self, iterator.ignore, errback)
00557
00558 def done_child_certs():
00559 rpki.async.iterator(predecessor.roas(), do_one_roa, callback)
00560
00561 def do_one_roa(iterator, roa):
00562 roa.regenerate_roa(iterator, errback)
00563
00564 self.generate_crl(callback = did_crl, errback = errback)
00565
00566 def delete(self, ca, repository, cb, eb):
00567 """
00568 Delete this ca_detail and all of the certs it issued.
00569 """
00570
00571 def withdraw_one_child(iterator, child_cert):
00572 repository.withdraw(child_cert.cert, child_cert.uri(ca), iterator, eb)
00573
00574 def child_certs_done():
00575 rpki.async.iterator(self.roas(), withdraw_one_roa, withdraw_manifest)
00576
00577 def withdraw_one_roa(iterator, roa):
00578 roa.withdraw_roa(iterator)
00579
00580 def withdraw_manifest():
00581 repository.withdraw(self.latest_manifest, self.manifest_uri(ca), withdraw_crl, eb)
00582
00583 def withdraw_crl():
00584 repository.withdraw(self.latest_crl, self.crl_uri(ca), done, eb)
00585
00586 def done():
00587 for cert in self.child_certs() + self.revoked_certs():
00588 cert.sql_delete()
00589 self.sql_delete()
00590 cb()
00591
00592 rpki.async.iterator(self.child_certs(), withdraw_one_child, child_certs_done)
00593
00594 def revoke(self, cb, eb):
00595 """
00596 Request revocation of all certificates whose SKI matches the key
00597 for this ca_detail.
00598
00599 Tasks:
00600
00601 - Request revocation of old keypair by parent.
00602
00603 - Revoke all child certs issued by the old keypair.
00604
00605 - Generate a final CRL, signed with the old keypair, listing all
00606 the revoked certs, with a next CRL time after the last cert or
00607 CRL signed by the old keypair will have expired.
00608
00609 - Generate a corresponding final manifest.
00610
00611 - Destroy old keypairs.
00612
00613 - Leave final CRL and manifest in place until their nextupdate
00614 time has passed.
00615 """
00616
00617 def parent_revoked(r_msg):
00618
00619 if r_msg.payload.ski != self.latest_ca_cert.gSKI():
00620 raise rpki.exceptions.SKIMismatch
00621
00622 ca = self.ca()
00623 parent = ca.parent()
00624 crl_interval = rpki.sundial.timedelta(seconds = parent.self().crl_interval)
00625
00626 self.nextUpdate = rpki.sundial.now()
00627
00628 if self.latest_manifest is not None:
00629 self.nextUpdate = self.nextUpdate.later(self.latest_manifest.getNextUpdate())
00630
00631 if self.latest_crl is not None:
00632 self.nextUpdate = self.nextUpdate.later(self.latest_crl.getNextUpdate())
00633
00634 def revoke_one_child(iterator, child_cert):
00635 self.nextUpdate = self.nextUpdate.later(child_cert.cert.getNotAfter())
00636 child_cert.revoke(iterator, eb)
00637
00638 def final_crl():
00639 self.nextUpdate += crl_interval
00640 self.generate_crl(callback = final_manifest, errback = eb, nextUpdate = self.nextUpdate)
00641
00642 def final_manifest():
00643 self.generate_manifest(callback = done, errback = eb, nextUpdate = self.nextUpdate)
00644
00645 def done():
00646 self.private_key_id = None
00647 self.manifest_private_key_id = None
00648 self.manifest_public_key = None
00649 self.latest_manifest_cert = None
00650 self.state = "revoked"
00651 self.sql_mark_dirty()
00652 cb()
00653
00654 rpki.async.iterator(self.child_certs(), revoke_one_child, final_crl)
00655
00656 rpki.up_down.revoke_pdu.query(self, parent_revoked, eb)
00657
00658 def update(self, parent, ca, rc, sia_uri_changed, old_resources, callback, errback):
00659 """
00660 Need to get a new certificate for this ca_detail and perhaps frob
00661 children of this ca_detail.
00662 """
00663
00664 def issued(issue_response):
00665 self.latest_ca_cert = issue_response.payload.classes[0].certs[0].cert
00666 new_resources = self.latest_ca_cert.get_3779resources()
00667
00668 def loop(iterator, child_cert):
00669 child_resources = child_cert.cert.get_3779resources()
00670 if sia_uri_changed or child_resources.oversized(new_resources):
00671 child_cert.reissue(
00672 ca_detail = self,
00673 resources = child_resources.intersection(new_resources),
00674 callback = iterator.ignore,
00675 errback = errback)
00676 else:
00677 iterator()
00678
00679 if sia_uri_changed or old_resources.oversized(new_resources):
00680 rpki.async.iterator(self.child_certs(), loop, callback)
00681 else:
00682 callback()
00683
00684 rpki.up_down.issue_pdu.query(parent, ca, self, issued, errback)
00685
00686 @classmethod
00687 def create(cls, ca):
00688 """
00689 Create a new ca_detail object for a specified CA.
00690 """
00691 self = cls()
00692 self.gctx = ca.gctx
00693 self.ca_id = ca.ca_id
00694 self.state = "pending"
00695
00696 self.private_key_id = rpki.x509.RSA.generate()
00697 self.public_key = self.private_key_id.get_RSApublic()
00698
00699 self.manifest_private_key_id = rpki.x509.RSA.generate()
00700 self.manifest_public_key = self.manifest_private_key_id.get_RSApublic()
00701
00702 self.sql_store()
00703 return self
00704
00705 def issue_ee(self, ca, resources, subject_key, sia = None):
00706 """
00707 Issue a new EE certificate.
00708 """
00709
00710 return self.latest_ca_cert.issue(
00711 keypair = self.private_key_id,
00712 subject_key = subject_key,
00713 serial = ca.next_serial_number(),
00714 sia = sia,
00715 aia = self.ca_cert_uri,
00716 crldp = self.crl_uri(ca),
00717 resources = resources,
00718 notAfter = self.latest_ca_cert.getNotAfter(),
00719 is_ca = False)
00720
00721
00722 def generate_manifest_cert(self, ca):
00723 """
00724 Generate a new manifest certificate for this ca_detail.
00725 """
00726
00727 resources = rpki.resource_set.resource_bag(
00728 asn = rpki.resource_set.resource_set_as("<inherit>"),
00729 v4 = rpki.resource_set.resource_set_ipv4("<inherit>"),
00730 v6 = rpki.resource_set.resource_set_ipv6("<inherit>"))
00731
00732 self.latest_manifest_cert = self.issue_ee(ca, resources, self.manifest_public_key)
00733
00734 def issue(self, ca, child, subject_key, sia, resources, callback, errback, child_cert = None):
00735 """
00736 Issue a new certificate to a child. Optional child_cert argument
00737 specifies an existing child_cert object to update in place; if not
00738 specified, we create a new one. Returns the child_cert object
00739 containing the newly issued cert.
00740 """
00741
00742 assert child_cert is None or (child_cert.child_id == child.child_id and
00743 child_cert.ca_detail_id == self.ca_detail_id)
00744
00745 cert = self.latest_ca_cert.issue(
00746 keypair = self.private_key_id,
00747 subject_key = subject_key,
00748 serial = ca.next_serial_number(),
00749 aia = self.ca_cert_uri,
00750 crldp = self.crl_uri(ca),
00751 sia = sia,
00752 resources = resources,
00753 notAfter = resources.valid_until)
00754
00755 if child_cert is None:
00756 child_cert = rpki.rpki_engine.child_cert_obj(
00757 gctx = child.gctx,
00758 child_id = child.child_id,
00759 ca_detail_id = self.ca_detail_id,
00760 cert = cert)
00761 rpki.log.debug("Created new child_cert %s" % repr(child_cert))
00762 else:
00763 child_cert.cert = cert
00764 rpki.log.debug("Reusing existing child_cert %s" % repr(child_cert))
00765
00766 child_cert.ski = cert.get_SKI()
00767
00768 child_cert.sql_store()
00769
00770 def published():
00771 self.generate_manifest(done, errback)
00772
00773 def done():
00774 callback(child_cert)
00775
00776 ca.parent().repository().publish(child_cert.cert, child_cert.uri(ca), published, errback)
00777
00778 def generate_crl(self, callback, errback, nextUpdate = None):
00779 """
00780 Generate a new CRL for this ca_detail. At the moment this is
00781 unconditional, that is, it is up to the caller to decide whether a
00782 new CRL is needed.
00783 """
00784
00785 ca = self.ca()
00786 parent = ca.parent()
00787 repository = parent.repository()
00788 crl_interval = rpki.sundial.timedelta(seconds = parent.self().crl_interval)
00789 now = rpki.sundial.now()
00790
00791 if nextUpdate is None:
00792 nextUpdate = now + crl_interval
00793
00794 certlist = []
00795 for revoked_cert in self.revoked_certs():
00796 if now > revoked_cert.expires + crl_interval:
00797 revoked_cert.sql_delete()
00798 else:
00799 certlist.append((revoked_cert.serial, revoked_cert.revoked.toASN1tuple(), ()))
00800 certlist.sort()
00801
00802 self.latest_crl = rpki.x509.CRL.generate(
00803 keypair = self.private_key_id,
00804 issuer = self.latest_ca_cert,
00805 serial = ca.next_crl_number(),
00806 thisUpdate = now,
00807 nextUpdate = nextUpdate,
00808 revokedCertificates = certlist)
00809
00810 repository.publish(self.latest_crl, self.crl_uri(ca), callback = callback, errback = errback)
00811
00812 def generate_manifest(self, callback, errback, nextUpdate = None):
00813 """
00814 Generate a new manifest for this ca_detail.
00815 """
00816
00817 ca = self.ca()
00818 parent = ca.parent()
00819 repository = parent.repository()
00820 crl_interval = rpki.sundial.timedelta(seconds = parent.self().crl_interval)
00821 now = rpki.sundial.now()
00822
00823 if nextUpdate is None:
00824 nextUpdate = now + crl_interval
00825
00826 roas = [r for r in self.roas() if r.cert is not None and r.roa is not None]
00827
00828 if self.latest_manifest_cert is None or self.latest_manifest_cert.getNotAfter() < nextUpdate:
00829 self.generate_manifest_cert(ca)
00830
00831 certs = [(c.uri_tail(), c.cert) for c in self.child_certs()] + \
00832 [(r.roa_uri_tail(), r.roa) for r in roas] + \
00833 [(self.crl_uri_tail(), self.latest_crl)]
00834
00835 self.latest_manifest = rpki.x509.SignedManifest.build(
00836 serial = ca.next_manifest_number(),
00837 thisUpdate = now,
00838 nextUpdate = nextUpdate,
00839 names_and_objs = certs,
00840 keypair = self.manifest_private_key_id,
00841 certs = self.latest_manifest_cert)
00842
00843 repository.publish(self.latest_manifest, self.manifest_uri(ca), callback = callback, errback = errback)
00844
00845 class child_cert_obj(rpki.sql.sql_persistent):
00846 """
00847 Certificate that has been issued to a child.
00848 """
00849
00850 sql_template = rpki.sql.template(
00851 "child_cert",
00852 "child_cert_id",
00853 ("cert", rpki.x509.X509),
00854 "child_id",
00855 "ca_detail_id",
00856 "ski")
00857
00858 def __init__(self, gctx = None, child_id = None, ca_detail_id = None, cert = None):
00859 """
00860 Initialize a child_cert_obj.
00861 """
00862 rpki.sql.sql_persistent.__init__(self)
00863 self.gctx = gctx
00864 self.child_id = child_id
00865 self.ca_detail_id = ca_detail_id
00866 self.cert = cert
00867 if child_id or ca_detail_id or cert:
00868 self.sql_mark_dirty()
00869
00870 def child(self):
00871 """Fetch child object to which this child_cert object links."""
00872 return rpki.left_right.child_elt.sql_fetch(self.gctx, self.child_id)
00873
00874 def ca_detail(self):
00875 """Fetch ca_detail object to which this child_cert object links."""
00876 return ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
00877
00878 def uri_tail(self):
00879 """Return the tail (filename) portion of the URI for this child_cert."""
00880 return self.cert.gSKI() + ".cer"
00881
00882 def uri(self, ca):
00883 """Return the publication URI for this child_cert."""
00884 return ca.sia_uri + self.uri_tail()
00885
00886 def revoke(self, callback, errback, withdraw = True):
00887 """
00888 Revoke a child cert.
00889 """
00890
00891 rpki.log.debug("Revoking %s" % repr(self))
00892 ca_detail = self.ca_detail()
00893 ca = ca_detail.ca()
00894 revoked_cert_obj.revoke(cert = self.cert, ca_detail = ca_detail)
00895
00896 def done():
00897 self.gctx.sql.sweep()
00898 self.sql_delete()
00899 callback()
00900
00901 if withdraw:
00902 ca.parent().repository().withdraw(self.cert, self.uri(ca), done, errback)
00903 else:
00904 rpki.log.info("Suppressing withdrawal of %r" % self.cert)
00905 done()
00906
00907 def reissue(self, ca_detail, callback = None, errback = None, resources = None, sia = None):
00908 """
00909 Reissue an existing cert, reusing the public key. If the cert we
00910 would generate is identical to the one we already have, we just
00911 return the one we already have. If we have to revoke the old
00912 certificate when generating the new one, we have to generate a new
00913 child_cert_obj, so calling code that needs the updated
00914 child_cert_obj must use the return value from this method.
00915 """
00916
00917
00918 assert callback is not None
00919 assert errback is not None
00920
00921 ca = ca_detail.ca()
00922 child = self.child()
00923
00924 old_resources = self.cert.get_3779resources()
00925 old_sia = self.cert.get_SIA()
00926 old_ca_detail = self.ca_detail()
00927
00928 if resources is None:
00929 resources = old_resources
00930
00931 if sia is None:
00932 sia = old_sia
00933
00934 assert resources.valid_until is not None and old_resources.valid_until is not None
00935
00936 if resources == old_resources and sia == old_sia and ca_detail == old_ca_detail:
00937 return callback(self)
00938
00939 must_revoke = old_resources.oversized(resources) or old_resources.valid_until > resources.valid_until
00940 new_issuer = ca_detail != old_ca_detail
00941
00942 rpki.log.debug("Reissuing %r, must_revoke %s, new_issuer %s" % (self, must_revoke, new_issuer))
00943
00944 if resources.valid_until != old_resources.valid_until:
00945 rpki.log.debug("Validity changed: %s %s" % ( old_resources.valid_until, resources.valid_until))
00946
00947 def revoke(child_cert):
00948
00949 uri = child_cert.uri(ca)
00950 rpki.log.debug("New child_cert %r uri %s" % (child_cert, uri))
00951
00952 def loop(iterator, x):
00953 rpki.log.debug("Revoking child_cert %r" % x)
00954 x.revoke(iterator, errback, withdraw = x.uri(ca) != uri)
00955
00956 def manifest():
00957 ca_detail.generate_manifest(done, errback)
00958
00959 def done():
00960 callback(child_cert)
00961
00962 certs_to_revoke = [x for x in child.child_certs(ca_detail = ca_detail, ski = self.ski) if x is not child_cert]
00963
00964 if certs_to_revoke:
00965 rpki.async.iterator(certs_to_revoke, loop, manifest)
00966 else:
00967 done()
00968
00969 ca_detail.issue(
00970 ca = ca,
00971 child = child,
00972 subject_key = self.cert.getPublicKey(),
00973 sia = sia,
00974 resources = resources,
00975 child_cert = None if must_revoke or new_issuer else self,
00976 callback = revoke if must_revoke else callback,
00977 errback = errback)
00978
00979 @classmethod
00980 def fetch(cls, gctx = None, child = None, ca_detail = None, ski = None, unique = False):
00981 """
00982 Fetch all child_cert objects matching a particular set of
00983 parameters. This is a wrapper to consolidate various queries that
00984 would otherwise be inline SQL WHERE expressions. In most cases
00985 code calls this indirectly, through methods in other classes.
00986 """
00987
00988 args = []
00989 where = []
00990
00991 if child:
00992 where.append("child_id = %s")
00993 args.append(child.child_id)
00994
00995 if ca_detail:
00996 where.append("ca_detail_id = %s")
00997 args.append(ca_detail.ca_detail_id)
00998
00999 if ski:
01000 where.append("ski = %s")
01001 args.append(ski)
01002
01003 where = " AND ".join(where)
01004
01005 gctx = gctx or (child and child.gctx) or (ca_detail and ca_detail.gctx) or None
01006
01007 if unique:
01008 return cls.sql_fetch_where1(gctx, where, args)
01009 else:
01010 return cls.sql_fetch_where(gctx, where, args)
01011
01012 class revoked_cert_obj(rpki.sql.sql_persistent):
01013 """
01014 Tombstone for a revoked certificate.
01015 """
01016
01017 sql_template = rpki.sql.template(
01018 "revoked_cert",
01019 "revoked_cert_id",
01020 "serial",
01021 "ca_detail_id",
01022 ("revoked", rpki.sundial.datetime),
01023 ("expires", rpki.sundial.datetime))
01024
01025 def __init__(self, gctx = None, serial = None, revoked = None, expires = None, ca_detail_id = None):
01026 """Initialize a revoked_cert_obj."""
01027 rpki.sql.sql_persistent.__init__(self)
01028 self.gctx = gctx
01029 self.serial = serial
01030 self.revoked = revoked
01031 self.expires = expires
01032 self.ca_detail_id = ca_detail_id
01033 if serial or revoked or expires or ca_detail_id:
01034 self.sql_mark_dirty()
01035
01036 def ca_detail(self):
01037 """Fetch ca_detail object to which this revoked_cert_obj links."""
01038 return ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
01039
01040 @classmethod
01041 def revoke(cls, cert, ca_detail):
01042 """
01043 Revoke a certificate.
01044 """
01045 return cls(
01046 serial = cert.getSerial(),
01047 expires = cert.getNotAfter(),
01048 revoked = rpki.sundial.now(),
01049 gctx = ca_detail.gctx,
01050 ca_detail_id = ca_detail.ca_detail_id)
01051
01052 class roa_obj(rpki.sql.sql_persistent):
01053 """
01054 Route Origin Authorization.
01055 """
01056
01057 sql_template = rpki.sql.template(
01058 "roa",
01059 "roa_id",
01060 "ca_detail_id",
01061 "self_id",
01062 "asn",
01063 ("roa", rpki.x509.ROA),
01064 ("cert", rpki.x509.X509))
01065
01066 ca_detail_id = None
01067 cert = None
01068 roa = None
01069
01070 def self(self):
01071 """
01072 Fetch self object to which this roa_obj links.
01073 """
01074 return rpki.left_right.self_elt.sql_fetch(self.gctx, self.self_id)
01075
01076 def sql_fetch_hook(self):
01077 """
01078 Extra SQL fetch actions for roa_obj -- handle prefix lists.
01079 """
01080 for version, datatype, attribute in ((4, rpki.resource_set.roa_prefix_set_ipv4, "ipv4"),
01081 (6, rpki.resource_set.roa_prefix_set_ipv6, "ipv6")):
01082 setattr(self, attribute, datatype.from_sql(
01083 self.gctx.sql,
01084 """
01085 SELECT prefix, prefixlen, max_prefixlen FROM roa_prefix
01086 WHERE roa_id = %s AND version = %s
01087 """,
01088 (self.roa_id, version)))
01089
01090 def sql_insert_hook(self):
01091 """
01092 Extra SQL insert actions for roa_obj -- handle prefix lists.
01093 """
01094 for version, prefix_set in ((4, self.ipv4), (6, self.ipv6)):
01095 if prefix_set:
01096 self.gctx.sql.executemany(
01097 """
01098 INSERT roa_prefix (roa_id, prefix, prefixlen, max_prefixlen, version)
01099 VALUES (%s, %s, %s, %s, %s)
01100 """,
01101 ((self.roa_id, x.prefix, x.prefixlen, x.max_prefixlen, version)
01102 for x in prefix_set))
01103
01104 def sql_delete_hook(self):
01105 """
01106 Extra SQL delete actions for roa_obj -- handle prefix lists.
01107 """
01108 self.gctx.sql.execute("DELETE FROM roa_prefix WHERE roa_id = %s", (self.roa_id,))
01109
01110 def ca_detail(self):
01111 """
01112 Fetch all ca_detail objects that link to this roa_obj.
01113 """
01114 return rpki.rpki_engine.ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
01115
01116 def update_roa(self, callback):
01117 """
01118 Bring this roa_obj's ROA up to date if necesssary.
01119 """
01120
01121 def lose(e):
01122 rpki.log.error(traceback.format_exc())
01123 rpki.log.warn("Could not update ROA %r, skipping: %s" % (self, e))
01124 callback()
01125 return
01126
01127 if self.roa is None:
01128 self.generate_roa(callback, lose)
01129 return
01130
01131 ca_detail = self.ca_detail()
01132
01133 if ca_detail is None or ca_detail.state != "active":
01134 self.regenerate_roa(callback, lose)
01135 return
01136
01137 regen_margin = rpki.sundial.timedelta(seconds = self.self().regen_margin)
01138
01139 if rpki.sundial.now() + regen_margin > self.cert.getNotAfter():
01140 self.regenerate_roa(callback, lose)
01141 return
01142
01143 ca_resources = ca_detail.latest_ca_cert.get_3779resources()
01144 ee_resources = self.cert.get_3779resources()
01145
01146 if ee_resources.oversized(ca_resources):
01147 self.regenerate_roa(callback, lose)
01148 return
01149
01150 v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4()
01151 v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6()
01152
01153 if ee_resources.v4 != v4 or ee_resources.v6 != v6:
01154 self.regenerate_roa(callback, lose)
01155 return
01156
01157 callback()
01158
01159 def generate_roa(self, callback, errback):
01160 """
01161 Generate a ROA.
01162
01163 At present this does not support ROAs with multiple signatures
01164 (neither does the current CMS code).
01165
01166 At present we have no way of performing a direct lookup from a
01167 desired set of resources to a covering certificate, so we have to
01168 search. This could be quite slow if we have a lot of active
01169 ca_detail objects. Punt on the issue for now, revisit if
01170 profiling shows this as a hotspot.
01171
01172 Once we have the right covering certificate, we generate the ROA
01173 payload, generate a new EE certificate, use the EE certificate to
01174 sign the ROA payload, publish the result, then throw away the
01175 private key for the EE cert, all per the ROA specification. This
01176 implies that generating a lot of ROAs will tend to thrash
01177 /dev/random, but there is not much we can do about that.
01178 """
01179
01180 if self.ipv4 is None and self.ipv6 is None:
01181 errback(rpki.exceptions.EmptyROAPrefixList())
01182 return
01183
01184
01185
01186
01187
01188 v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4()
01189 v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6()
01190
01191 ca_detail = self.ca_detail()
01192 if ca_detail is None or ca_detail.state != "active":
01193 ca_detail = None
01194 for parent in self.self().parents():
01195 for ca in parent.cas():
01196 ca_detail = ca.fetch_active()
01197 if ca_detail is not None:
01198 resources = ca_detail.latest_ca_cert.get_3779resources()
01199 if v4.issubset(resources.v4) and v6.issubset(resources.v6):
01200 break
01201 ca_detail = None
01202 if ca_detail is not None:
01203 break
01204
01205 if ca_detail is None:
01206 errback(rpki.exceptions.NoCoveringCertForROA("generate_roa() could not find a certificate covering %s %s" % (v4, v6)))
01207 return
01208
01209 ca = ca_detail.ca()
01210
01211 resources = rpki.resource_set.resource_bag(v4 = v4, v6 = v6)
01212
01213 keypair = rpki.x509.RSA.generate()
01214
01215 self.ca_detail_id = ca_detail.ca_detail_id
01216
01217 self.cert = ca_detail.issue_ee(ca, resources, keypair.get_RSApublic(),
01218 sia = ((rpki.oids.name2oid["id-ad-signedObject"],
01219 ("uri", self.roa_uri(keypair))),))
01220
01221 self.roa = rpki.x509.ROA.build(self.asn, self.ipv4, self.ipv6, keypair, (self.cert,))
01222
01223 self.sql_store()
01224
01225 def done():
01226 ca_detail.generate_manifest(callback, errback)
01227
01228 ca.parent().repository().publish(self.roa, self.roa_uri(), done, errback)
01229
01230 def withdraw_roa(self, callback, errback, regenerate = False):
01231 """
01232 Withdraw ROA associated with this roa_obj.
01233
01234 In order to preserve make-before-break properties without
01235 duplicating code, this method also handles generating a
01236 replacement ROA when requested.
01237 """
01238
01239 ca_detail = self.ca_detail()
01240 cert = self.cert
01241 roa = self.roa
01242 roa_uri = self.roa_uri()
01243 ee_uri = self.ee_uri()
01244
01245 if ca_detail.state != 'active':
01246 self.ca_detail_id = None
01247
01248 def one():
01249 rpki.log.debug("Withdrawing ROA and revoking its EE cert")
01250 rpki.rpki_engine.revoked_cert_obj.revoke(cert = cert, ca_detail = ca_detail)
01251 ca_detail.ca().parent().repository().withdraw(roa, roa_uri, two, errback)
01252
01253 def two():
01254 self.gctx.sql.sweep()
01255 ca_detail.generate_crl(three, errback)
01256
01257 def three():
01258 ca_detail.generate_manifest(callback, errback)
01259
01260 if regenerate:
01261 self.generate_roa(one, errback)
01262 else:
01263 one()
01264
01265 def regenerate_roa(self, callback, errback):
01266 """
01267 Reissue ROA associated with this roa_obj.
01268 """
01269 if self.ca_detail() is None:
01270 self.generate_roa(callback, errback)
01271 else:
01272 self.withdraw_roa(callback, errback, regenerate = True)
01273
01274 def roa_uri(self, key = None):
01275 """
01276 Return the publication URI for this roa_obj's ROA.
01277 """
01278 return self.ca_detail().ca().sia_uri + self.roa_uri_tail(key)
01279
01280 def roa_uri_tail(self, key = None):
01281 """
01282 Return the tail (filename portion) of the publication URI for this
01283 roa_obj's ROA.
01284 """
01285 return (key or self.cert).gSKI() + ".roa"
01286
01287 def ee_uri_tail(self):
01288 """
01289 Return the tail (filename) portion of the URI for this roa_obj's
01290 ROA's EE certificate.
01291 """
01292 return self.cert.gSKI() + ".cer"
01293
01294 def ee_uri(self):
01295 """
01296 Return the publication URI for this roa_obj's ROA's EE
01297 certificate.
01298 """
01299 return self.ca_detail().ca().sia_uri + self.ee_uri_tail()
01300