RPKI Engine 1.0
|
00001 """ 00002 RPKI engine daemon. 00003 00004 Usage: python rpkid.py [ { -c | --config } configfile ] 00005 [ { -h | --help } ] 00006 [ { -p | --profile } outputfile ] 00007 00008 $Id: rpkid.py 3793 2011-04-27 04:34:52Z sra $ 00009 00010 Copyright (C) 2009--2010 Internet Systems Consortium ("ISC") 00011 00012 Permission to use, copy, modify, and distribute this software for any 00013 purpose with or without fee is hereby granted, provided that the above 00014 copyright notice and this permission notice appear in all copies. 00015 00016 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH 00017 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 00018 AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, 00019 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 00020 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 00021 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 00022 PERFORMANCE OF THIS SOFTWARE. 00023 00024 Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN") 00025 00026 Permission to use, copy, modify, and distribute this software for any 00027 purpose with or without fee is hereby granted, provided that the above 00028 copyright notice and this permission notice appear in all copies. 00029 00030 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH 00031 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 00032 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, 00033 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 00034 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 00035 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 00036 PERFORMANCE OF THIS SOFTWARE. 00037 """ 00038 00039 import os, time, getopt, sys, lxml.etree, re, random 00040 import rpki.resource_set, rpki.up_down, rpki.left_right, rpki.x509, rpki.sql 00041 import rpki.http, rpki.config, rpki.exceptions, rpki.relaxng, rpki.log, rpki.async 00042 00043 class main(object): 00044 """ 00045 Main program for rpkid. 00046 """ 00047 00048 def __init__(self): 00049 00050 os.environ["TZ"] = "UTC" 00051 time.tzset() 00052 00053 self.cfg_file = None 00054 self.profile = None 00055 00056 opts, argv = getopt.getopt(sys.argv[1:], "c:dhp:?", ["config=", "debug", "help", "profile="]) 00057 for o, a in opts: 00058 if o in ("-h", "--help", "-?"): 00059 print __doc__ 00060 sys.exit(0) 00061 elif o in ("-d", "--debug"): 00062 rpki.log.use_syslog = False 00063 elif o in ("-c", "--config"): 00064 self.cfg_file = a 00065 elif o in ("-p", "--profile"): 00066 self.profile = a 00067 if argv: 00068 raise rpki.exceptions.CommandParseFailure, "Unexpected arguments %s" % argv 00069 00070 rpki.log.init("rpkid") 00071 00072 if self.profile: 00073 import cProfile 00074 cProfile.run("self.main()", self.profile) 00075 else: 00076 self.main() 00077 00078 def main(self): 00079 00080 self.cfg = rpki.config.parser(self.cfg_file, "rpkid") 00081 00082 startup_msg = self.cfg.get("startup-message", "") 00083 if startup_msg: 00084 rpki.log.info(startup_msg) 00085 00086 if self.profile: 00087 rpki.log.info("Running in profile mode with output to %s" % self.profile) 00088 00089 self.cfg.set_global_flags() 00090 00091 self.sql = rpki.sql.session(self.cfg) 00092 00093 self.bpki_ta = rpki.x509.X509(Auto_update = self.cfg.get("bpki-ta")) 00094 self.irdb_cert = rpki.x509.X509(Auto_update = self.cfg.get("irdb-cert")) 00095 self.irbe_cert = rpki.x509.X509(Auto_update = self.cfg.get("irbe-cert")) 00096 self.rpkid_cert = rpki.x509.X509(Auto_update = self.cfg.get("rpkid-cert")) 00097 self.rpkid_key = rpki.x509.RSA( Auto_update = self.cfg.get("rpkid-key")) 00098 00099 self.irdb_url = self.cfg.get("irdb-url") 00100 00101 self.http_server_host = self.cfg.get("server-host", "") 00102 self.http_server_port = self.cfg.getint("server-port", 4433) 00103 00104 self.publication_kludge_base = self.cfg.get("publication-kludge-base", "publication/") 00105 00106 self.use_internal_cron = self.cfg.getboolean("use-internal-cron", True) 00107 00108 self.initial_delay = random.randint(self.cfg.getint("initial-delay-min", 10), 00109 self.cfg.getint("initial-delay-max", 120)) 00110 00111 # Should be much longer in production 00112 self.cron_period = rpki.sundial.timedelta(seconds = self.cfg.getint("cron-period", 120)) 00113 self.cron_keepalive = rpki.sundial.timedelta(seconds = self.cfg.getint("cron-keepalive", 0)) 00114 if not self.cron_keepalive: 00115 self.cron_keepalive = self.cron_period * 4 00116 self.cron_timeout = None 00117 00118 self.start_cron() 00119 00120 rpki.http.server( 00121 host = self.http_server_host, 00122 port = self.http_server_port, 00123 handlers = (("/left-right", self.left_right_handler), 00124 ("/up-down/", self.up_down_handler), 00125 ("/cronjob", self.cronjob_handler))) 00126 00127 00128 def start_cron(self): 00129 """ 00130 Start clock for rpkid's internal cron process. 00131 """ 00132 00133 if self.use_internal_cron: 00134 self.cron_timer = rpki.async.timer(handler = self.cron) 00135 when = rpki.sundial.now() + rpki.sundial.timedelta(seconds = self.initial_delay) 00136 rpki.log.debug("Scheduling initial cron pass at %s" % when) 00137 self.cron_timer.set(when) 00138 else: 00139 rpki.log.debug("Not using internal clock, start_cron() call ignored") 00140 00141 def irdb_query(self, callback, errback, *q_pdus, **kwargs): 00142 """ 00143 Perform an IRDB callback query. 00144 """ 00145 00146 rpki.log.trace() 00147 00148 q_types = tuple(type(q_pdu) for q_pdu in q_pdus) 00149 00150 expected_pdu_count = kwargs.pop("expected_pdu_count", None) 00151 assert len(kwargs) == 0 00152 00153 q_msg = rpki.left_right.msg.query() 00154 q_msg.extend(q_pdus) 00155 q_der = rpki.left_right.cms_msg().wrap(q_msg, self.rpkid_key, self.rpkid_cert) 00156 00157 def unwrap(r_der): 00158 r_cms = rpki.left_right.cms_msg(DER = r_der) 00159 r_msg = r_cms.unwrap((self.bpki_ta, self.irdb_cert)) 00160 if not r_msg.is_reply() or not all(type(r_pdu) in q_types for r_pdu in r_msg): 00161 raise rpki.exceptions.BadIRDBReply, "Unexpected response to IRDB query: %s" % r_cms.pretty_print_content() 00162 if expected_pdu_count is not None and len(r_msg) != expected_pdu_count: 00163 assert isinstance(expected_pdu_count, (int, long)) 00164 raise rpki.exceptions.BadIRDBReply, "Expected exactly %d PDU%s from IRDB: %s" % ( 00165 expected_pdu_count, "" if expected_pdu_count == 1 else "s", r_cms.pretty_print_content()) 00166 callback(r_msg) 00167 00168 rpki.http.client( 00169 url = self.irdb_url, 00170 msg = q_der, 00171 callback = unwrap, 00172 errback = errback) 00173 00174 def irdb_query_child_resources(self, self_handle, child_handle, callback, errback): 00175 """ 00176 Ask IRDB about a child's resources. 00177 """ 00178 00179 rpki.log.trace() 00180 00181 q_pdu = rpki.left_right.list_resources_elt() 00182 q_pdu.self_handle = self_handle 00183 q_pdu.child_handle = child_handle 00184 00185 def done(r_msg): 00186 callback(rpki.resource_set.resource_bag( 00187 asn = r_msg[0].asn, 00188 v4 = r_msg[0].ipv4, 00189 v6 = r_msg[0].ipv6, 00190 valid_until = r_msg[0].valid_until)) 00191 00192 self.irdb_query(done, errback, q_pdu, expected_pdu_count = 1) 00193 00194 def irdb_query_roa_requests(self, self_handle, callback, errback): 00195 """ 00196 Ask IRDB about self's ROA requests. 00197 """ 00198 00199 rpki.log.trace() 00200 00201 q_pdu = rpki.left_right.list_roa_requests_elt() 00202 q_pdu.self_handle = self_handle 00203 00204 self.irdb_query(callback, errback, q_pdu) 00205 00206 def irdb_query_ghostbuster_requests(self, self_handle, parent_handles, callback, errback): 00207 """ 00208 Ask IRDB about self's ghostbuster record requests. 00209 """ 00210 00211 rpki.log.trace() 00212 00213 q_pdus = [] 00214 00215 for parent_handle in parent_handles: 00216 q_pdu = rpki.left_right.list_ghostbuster_requests_elt() 00217 q_pdu.self_handle = self_handle 00218 q_pdu.parent_handle = parent_handle 00219 q_pdus.append(q_pdu) 00220 00221 self.irdb_query(callback, errback, *q_pdus) 00222 00223 def left_right_handler(self, query, path, cb): 00224 """ 00225 Process one left-right PDU. 00226 """ 00227 00228 rpki.log.trace() 00229 00230 def done(r_msg): 00231 reply = rpki.left_right.cms_msg().wrap(r_msg, self.rpkid_key, self.rpkid_cert) 00232 self.sql.sweep() 00233 cb(200, body = reply) 00234 00235 try: 00236 self.sql.ping() 00237 q_msg = rpki.left_right.cms_msg(DER = query).unwrap((self.bpki_ta, self.irbe_cert)) 00238 if not q_msg.is_query(): 00239 raise rpki.exceptions.BadQuery, "Message type is not query" 00240 q_msg.serve_top_level(self, done) 00241 except (rpki.async.ExitNow, SystemExit): 00242 raise 00243 except Exception, data: 00244 rpki.log.traceback() 00245 cb(500, reason = "Unhandled exception %s" % data) 00246 00247 up_down_url_regexp = re.compile("/up-down/([-A-Z0-9_]+)/([-A-Z0-9_]+)$", re.I) 00248 00249 def up_down_handler(self, query, path, cb): 00250 """ 00251 Process one up-down PDU. 00252 """ 00253 00254 rpki.log.trace() 00255 00256 def done(reply): 00257 self.sql.sweep() 00258 cb(200, body = reply) 00259 00260 try: 00261 self.sql.ping() 00262 match = self.up_down_url_regexp.search(path) 00263 if match is None: 00264 raise rpki.exceptions.BadContactURL, "Bad URL path received in up_down_handler(): %s" % path 00265 self_handle, child_handle = match.groups() 00266 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", 00267 (self_handle, child_handle), "self") 00268 if child is None: 00269 raise rpki.exceptions.ChildNotFound, "Could not find child %s of self %s in up_down_handler()" % (child_handle, self_handle) 00270 child.serve_up_down(query, done) 00271 except (rpki.async.ExitNow, SystemExit): 00272 raise 00273 except (rpki.exceptions.ChildNotFound, rpki.exceptions.BadContactURL), e: 00274 rpki.log.warn(str(e)) 00275 cb(400, reason = str(e)) 00276 except Exception, e: 00277 rpki.log.traceback() 00278 cb(400, reason = "Could not process PDU: %s" % e) 00279 00280 def checkpoint(self): 00281 """ 00282 Record that we were still alive when we got here, by resetting 00283 keepalive timer. 00284 """ 00285 if self.cron_timeout is not None: 00286 self.cron_timeout = rpki.sundial.now() + self.cron_keepalive 00287 00288 def cron(self, cb = None): 00289 """ 00290 Periodic tasks. 00291 """ 00292 00293 rpki.log.trace() 00294 self.sql.ping() 00295 00296 now = rpki.sundial.now() 00297 00298 assert self.use_internal_cron or self.cron_timeout is None 00299 00300 if self.use_internal_cron: 00301 00302 if self.cron_timeout is not None and self.cron_timeout < now: 00303 rpki.log.warn("cron keepalive threshold %s has expired, breaking lock" % self.cron_timeout) 00304 self.cron_timeout = None 00305 00306 when = now + self.cron_period 00307 rpki.log.debug("Scheduling next cron run at %s" % when) 00308 self.cron_timer.set(when) 00309 00310 if self.cron_timeout is not None: 00311 rpki.log.warn("cron already running, keepalive will expire at %s" % self.cron_timeout) 00312 return 00313 00314 self.cron_timeout = True 00315 self.checkpoint() 00316 00317 def loop(iterator, s): 00318 self.checkpoint() 00319 s.cron(iterator) 00320 00321 def done(): 00322 self.sql.sweep() 00323 self.cron_timeout = None 00324 rpki.log.info("Finished cron run started at %s" % now) 00325 if not self.use_internal_cron: 00326 cb() 00327 00328 def lose(e): 00329 self.cron_timeout = None 00330 if self.use_internal_cron: 00331 rpki.log.traceback() 00332 else: 00333 raise 00334 00335 try: 00336 rpki.async.iterator(rpki.left_right.self_elt.sql_fetch_all(self), loop, done) 00337 00338 except (rpki.async.ExitNow, SystemExit): 00339 self.cron_timeout = None 00340 raise 00341 00342 except Exception, e: 00343 lose(e) 00344 00345 def cronjob_handler(self, query, path, cb): 00346 """ 00347 External trigger for periodic tasks. This is somewhat obsolete 00348 now that we have internal timers, but the test framework still 00349 uses it. 00350 """ 00351 00352 def done(): 00353 cb(200, body = "OK") 00354 00355 if self.use_internal_cron: 00356 cb(500, reason = "Running cron internally") 00357 else: 00358 self.cron(done) 00359 00360 class ca_obj(rpki.sql.sql_persistent): 00361 """ 00362 Internal CA object. 00363 """ 00364 00365 sql_template = rpki.sql.template( 00366 "ca", 00367 "ca_id", 00368 "last_crl_sn", 00369 ("next_crl_update", rpki.sundial.datetime), 00370 "last_issued_sn", "last_manifest_sn", 00371 ("next_manifest_update", rpki.sundial.datetime), 00372 "sia_uri", "parent_id", "parent_resource_class") 00373 00374 last_crl_sn = 0 00375 last_issued_sn = 0 00376 last_manifest_sn = 0 00377 00378 @property 00379 def parent(self): 00380 """ 00381 Fetch parent object to which this CA object links. 00382 """ 00383 return rpki.left_right.parent_elt.sql_fetch(self.gctx, self.parent_id) 00384 00385 @property 00386 def ca_details(self): 00387 """ 00388 Fetch all ca_detail objects that link to this CA object. 00389 """ 00390 return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s", (self.ca_id,)) 00391 00392 @property 00393 def pending_ca_details(self): 00394 """ 00395 Fetch the pending ca_details for this CA, if any. 00396 """ 00397 return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'pending'", (self.ca_id,)) 00398 00399 @property 00400 def active_ca_detail(self): 00401 """ 00402 Fetch the active ca_detail for this CA, if any. 00403 """ 00404 return ca_detail_obj.sql_fetch_where1(self.gctx, "ca_id = %s AND state = 'active'", (self.ca_id,)) 00405 00406 @property 00407 def deprecated_ca_details(self): 00408 """ 00409 Fetch deprecated ca_details for this CA, if any. 00410 """ 00411 return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'deprecated'", (self.ca_id,)) 00412 00413 @property 00414 def revoked_ca_details(self): 00415 """ 00416 Fetch revoked ca_details for this CA, if any. 00417 """ 00418 return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'revoked'", (self.ca_id,)) 00419 00420 @property 00421 def issue_response_candidate_ca_details(self): 00422 """ 00423 Fetch ca_details which are candidates for consideration when 00424 processing an up-down issue_response PDU. 00425 """ 00426 #return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND latest_ca_cert IS NOT NULL AND state != 'revoked'", (self.ca_id,)) 00427 return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state != 'revoked'", (self.ca_id,)) 00428 00429 def construct_sia_uri(self, parent, rc): 00430 """ 00431 Construct the sia_uri value for this CA given configured 00432 information and the parent's up-down protocol list_response PDU. 00433 """ 00434 00435 sia_uri = rc.suggested_sia_head and rc.suggested_sia_head.rsync() 00436 if not sia_uri or not sia_uri.startswith(parent.sia_base): 00437 sia_uri = parent.sia_base 00438 if not sia_uri.endswith("/"): 00439 raise rpki.exceptions.BadURISyntax, "SIA URI must end with a slash: %s" % sia_uri 00440 return sia_uri + str(self.ca_id) + "/" 00441 00442 def check_for_updates(self, parent, rc, cb, eb): 00443 """ 00444 Parent has signaled continued existance of a resource class we 00445 already knew about, so we need to check for an updated 00446 certificate, changes in resource coverage, revocation and reissue 00447 with the same key, etc. 00448 """ 00449 00450 sia_uri = self.construct_sia_uri(parent, rc) 00451 sia_uri_changed = self.sia_uri != sia_uri 00452 if sia_uri_changed: 00453 self.sia_uri = sia_uri 00454 self.sql_mark_dirty() 00455 00456 rc_resources = rc.to_resource_bag() 00457 cert_map = dict((c.cert.get_SKI(), c) for c in rc.certs) 00458 00459 def loop(iterator, ca_detail): 00460 00461 self.gctx.checkpoint() 00462 00463 rc_cert = cert_map.pop(ca_detail.public_key.get_SKI(), None) 00464 00465 if rc_cert is None: 00466 00467 rpki.log.warn("SKI %s in resource class %s is in my database but missing from list_response received from %s, maybe parent certificate went away?" 00468 % (ca_detail.public_key.gSKI(), rc.class_name, parent.parent_handle)) 00469 publisher = publication_queue() 00470 ca_detail.delete(ca = ca_detail.ca, publisher = publisher) 00471 return publisher.call_pubd(iterator, eb) 00472 00473 else: 00474 00475 if ca_detail.state in ("pending", "active"): 00476 00477 if ca_detail.state == "pending": 00478 current_resources = rpki.resource_set.resource_bag() 00479 else: 00480 current_resources = ca_detail.latest_ca_cert.get_3779resources() 00481 00482 if (ca_detail.state == "pending" or 00483 sia_uri_changed or 00484 ca_detail.latest_ca_cert != rc_cert.cert or 00485 current_resources.undersized(rc_resources) or 00486 current_resources.oversized(rc_resources)): 00487 return ca_detail.update( 00488 parent = parent, 00489 ca = self, 00490 rc = rc, 00491 sia_uri_changed = sia_uri_changed, 00492 old_resources = current_resources, 00493 callback = iterator, 00494 errback = eb) 00495 00496 iterator() 00497 00498 def done(): 00499 if cert_map: 00500 rpki.log.warn("Certificate SKIs in resource class %s in list_response from parent %s that are missing from our database: %s" 00501 % (rc.class_name, parent.parent_handle, ", ".join(c.cert.gSKI() for c in cert_map.values()))) 00502 self.gctx.checkpoint() 00503 cb() 00504 00505 ca_details = self.issue_response_candidate_ca_details 00506 00507 if True: 00508 skis_parent = set(x.cert.gSKI() 00509 for x in cert_map.itervalues()) 00510 skis_me = set(x.latest_ca_cert.gSKI() 00511 for x in ca_details 00512 if x.latest_ca_cert is not None) 00513 for ski in skis_parent & skis_me: 00514 rpki.log.debug("Parent %s and I agree that I have SKI %s in resource class %s" 00515 % (parent.parent_handle, ski, rc.class_name)) 00516 for ski in skis_parent - skis_me: 00517 rpki.log.debug("Parent %s thinks I have SKI %s in resource class %s but I don't think so" 00518 % (parent.parent_handle, ski, rc.class_name)) 00519 for ski in skis_me - skis_parent: 00520 rpki.log.debug("I think I have SKI %s in resource class %s but parent %s doesn't think so" 00521 % (ski, rc.class_name, parent.parent_handle)) 00522 00523 if ca_details: 00524 rpki.async.iterator(ca_details, loop, done) 00525 else: 00526 rpki.log.warn("Existing resource class %s from parent %s with no certificates, rekeying" % (rc.class_name, parent.parent_handle)) 00527 self.gctx.checkpoint() 00528 self.rekey(cb, eb) 00529 00530 @classmethod 00531 def create(cls, parent, rc, cb, eb): 00532 """ 00533 Parent has signaled existance of a new resource class, so we need 00534 to create and set up a corresponding CA object. 00535 """ 00536 00537 self = cls() 00538 self.gctx = parent.gctx 00539 self.parent_id = parent.parent_id 00540 self.parent_resource_class = rc.class_name 00541 self.sql_store() 00542 self.sia_uri = self.construct_sia_uri(parent, rc) 00543 ca_detail = ca_detail_obj.create(self) 00544 00545 def done(issue_response): 00546 ca_detail.activate( 00547 ca = self, 00548 cert = issue_response.payload.classes[0].certs[0].cert, 00549 uri = issue_response.payload.classes[0].certs[0].cert_url, 00550 callback = cb, 00551 errback = eb) 00552 00553 rpki.up_down.issue_pdu.query(parent, self, ca_detail, done, eb) 00554 00555 def delete(self, parent, callback): 00556 """ 00557 The list of current resource classes received from parent does not 00558 include the class corresponding to this CA, so we need to delete 00559 it (and its little dog too...). 00560 00561 All certs published by this CA are now invalid, so need to 00562 withdraw them, the CRL, and the manifest from the repository, 00563 delete all child_cert and ca_detail records associated with this 00564 CA, then finally delete this CA itself. 00565 """ 00566 00567 def lose(e): 00568 rpki.log.traceback() 00569 rpki.log.warn("Could not delete CA %r, skipping: %s" % (self, e)) 00570 callback() 00571 00572 def done(): 00573 self.sql_delete() 00574 callback() 00575 00576 publisher = publication_queue() 00577 for ca_detail in self.ca_details: 00578 ca_detail.delete(ca = self, publisher = publisher) 00579 publisher.call_pubd(done, lose) 00580 00581 def next_serial_number(self): 00582 """ 00583 Allocate a certificate serial number. 00584 """ 00585 self.last_issued_sn += 1 00586 self.sql_mark_dirty() 00587 return self.last_issued_sn 00588 00589 def next_manifest_number(self): 00590 """ 00591 Allocate a manifest serial number. 00592 """ 00593 self.last_manifest_sn += 1 00594 self.sql_mark_dirty() 00595 return self.last_manifest_sn 00596 00597 def next_crl_number(self): 00598 """ 00599 Allocate a CRL serial number. 00600 """ 00601 self.last_crl_sn += 1 00602 self.sql_mark_dirty() 00603 return self.last_crl_sn 00604 00605 def rekey(self, cb, eb): 00606 """ 00607 Initiate a rekey operation for this ca. Generate a new keypair. 00608 Request cert from parent using new keypair. Mark result as our 00609 active ca_detail. Reissue all child certs issued by this ca using 00610 the new ca_detail. 00611 """ 00612 00613 rpki.log.trace() 00614 00615 parent = self.parent 00616 old_detail = self.active_ca_detail 00617 new_detail = ca_detail_obj.create(self) 00618 00619 def done(issue_response): 00620 new_detail.activate( 00621 ca = self, 00622 cert = issue_response.payload.classes[0].certs[0].cert, 00623 uri = issue_response.payload.classes[0].certs[0].cert_url, 00624 predecessor = old_detail, 00625 callback = cb, 00626 errback = eb) 00627 00628 rpki.up_down.issue_pdu.query(parent, self, new_detail, done, eb) 00629 00630 def revoke(self, cb, eb): 00631 """ 00632 Revoke deprecated ca_detail objects associated with this ca. 00633 """ 00634 00635 rpki.log.trace() 00636 00637 def loop(iterator, ca_detail): 00638 ca_detail.revoke(cb = iterator, eb = eb) 00639 00640 rpki.async.iterator(self.deprecated_ca_details, loop, cb) 00641 00642 def reissue(self, cb, eb): 00643 """ 00644 Reissue all current certificates issued by this CA. 00645 """ 00646 00647 ca_detail = self.active_ca_detail 00648 if ca_detail: 00649 ca_detail.reissue(cb, eb) 00650 else: 00651 cb() 00652 00653 class ca_detail_obj(rpki.sql.sql_persistent): 00654 """ 00655 Internal CA detail object. 00656 """ 00657 00658 sql_template = rpki.sql.template( 00659 "ca_detail", 00660 "ca_detail_id", 00661 ("private_key_id", rpki.x509.RSA), 00662 ("public_key", rpki.x509.RSApublic), 00663 ("latest_ca_cert", rpki.x509.X509), 00664 ("manifest_private_key_id", rpki.x509.RSA), 00665 ("manifest_public_key", rpki.x509.RSApublic), 00666 ("latest_manifest_cert", rpki.x509.X509), 00667 ("latest_manifest", rpki.x509.SignedManifest), 00668 ("latest_crl", rpki.x509.CRL), 00669 ("crl_published", rpki.sundial.datetime), 00670 ("manifest_published", rpki.sundial.datetime), 00671 "state", 00672 "ca_cert_uri", 00673 "ca_id") 00674 00675 crl_published = None 00676 manifest_published = None 00677 latest_ca_cert = None 00678 00679 def sql_decode(self, vals): 00680 """ 00681 Extra assertions for SQL decode of a ca_detail_obj. 00682 """ 00683 rpki.sql.sql_persistent.sql_decode(self, vals) 00684 assert self.public_key is None or self.private_key_id is None or self.public_key.get_DER() == self.private_key_id.get_public_DER() 00685 assert self.manifest_public_key is None or self.manifest_private_key_id is None or self.manifest_public_key.get_DER() == self.manifest_private_key_id.get_public_DER() 00686 00687 @property 00688 def ca(self): 00689 """ 00690 Fetch CA object to which this ca_detail links. 00691 """ 00692 return ca_obj.sql_fetch(self.gctx, self.ca_id) 00693 00694 def fetch_child_certs(self, child = None, ski = None, unique = False): 00695 """ 00696 Fetch all child_cert objects that link to this ca_detail. 00697 """ 00698 return rpki.rpkid.child_cert_obj.fetch(self.gctx, child, self, ski, unique) 00699 00700 @property 00701 def child_certs(self): 00702 """ 00703 Fetch all child_cert objects that link to this ca_detail. 00704 """ 00705 return self.fetch_child_certs() 00706 00707 @property 00708 def revoked_certs(self): 00709 """ 00710 Fetch all revoked_cert objects that link to this ca_detail. 00711 """ 00712 return revoked_cert_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,)) 00713 00714 @property 00715 def roas(self): 00716 """ 00717 Fetch all ROA objects that link to this ca_detail. 00718 """ 00719 return rpki.rpkid.roa_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,)) 00720 00721 @property 00722 def ghostbusters(self): 00723 """ 00724 Fetch all Ghostbuster objects that link to this ca_detail. 00725 """ 00726 return rpki.rpkid.ghostbuster_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,)) 00727 00728 @property 00729 def crl_uri(self): 00730 """ 00731 Return publication URI for this ca_detail's CRL. 00732 """ 00733 return self.ca.sia_uri + self.crl_uri_tail 00734 00735 @property 00736 def crl_uri_tail(self): 00737 """ 00738 Return tail (filename portion) of publication URI for this ca_detail's CRL. 00739 """ 00740 return self.public_key.gSKI() + ".crl" 00741 00742 @property 00743 def manifest_uri(self): 00744 """ 00745 Return publication URI for this ca_detail's manifest. 00746 """ 00747 return self.ca.sia_uri + self.public_key.gSKI() + ".mnf" 00748 00749 def has_expired(self): 00750 """ 00751 Return whether this ca_detail's certificate has expired. 00752 """ 00753 return self.latest_ca_cert.getNotAfter() <= rpki.sundial.now() 00754 00755 def activate(self, ca, cert, uri, callback, errback, predecessor = None): 00756 """ 00757 Activate this ca_detail. 00758 """ 00759 00760 publisher = publication_queue() 00761 00762 self.latest_ca_cert = cert 00763 self.ca_cert_uri = uri.rsync() 00764 self.generate_manifest_cert() 00765 self.state = "active" 00766 self.generate_crl(publisher = publisher) 00767 self.generate_manifest(publisher = publisher) 00768 self.sql_mark_dirty() 00769 00770 if predecessor is not None: 00771 predecessor.state = "deprecated" 00772 predecessor.sql_mark_dirty() 00773 for child_cert in predecessor.child_certs: 00774 child_cert.reissue(ca_detail = self, publisher = publisher) 00775 for roa in predecessor.roas: 00776 roa.regenerate(publisher = publisher) 00777 00778 # Need to do something to regenerate ghostbusters here? 00779 00780 publisher.call_pubd(callback, errback) 00781 00782 def delete(self, ca, publisher, allow_failure = False): 00783 """ 00784 Delete this ca_detail and all of the certs it issued. 00785 00786 If allow_failure is true, we clean up as much as we can but don't 00787 raise an exception. 00788 """ 00789 00790 repository = ca.parent.repository 00791 for child_cert in self.child_certs: 00792 publisher.withdraw(cls = rpki.publication.certificate_elt, uri = child_cert.uri, obj = child_cert.cert, repository = repository, 00793 handler = False if allow_failure else None) 00794 for roa in self.roas: 00795 roa.revoke(publisher = publisher, allow_failure = allow_failure) 00796 for ghostbuster in self.ghostbusters: 00797 ghostbuster.revoke(publisher = publisher, allow_failure = allow_failure) 00798 try: 00799 latest_manifest = self.latest_manifest 00800 except AttributeError: 00801 latest_manifest = None 00802 if latest_manifest is not None: 00803 publisher.withdraw(cls = rpki.publication.manifest_elt, uri = self.manifest_uri, obj = self.latest_manifest, repository = repository, 00804 handler = False if allow_failure else None) 00805 try: 00806 latest_crl = self.latest_crl 00807 except AttributeError: 00808 latest_crl = None 00809 if latest_crl is not None: 00810 publisher.withdraw(cls = rpki.publication.crl_elt, uri = self.crl_uri, obj = self.latest_crl, repository = repository, 00811 handler = False if allow_failure else None) 00812 for cert in self.child_certs + self.revoked_certs: 00813 cert.sql_delete() 00814 self.sql_delete() 00815 00816 def revoke(self, cb, eb): 00817 """ 00818 Request revocation of all certificates whose SKI matches the key 00819 for this ca_detail. 00820 00821 Tasks: 00822 00823 - Request revocation of old keypair by parent. 00824 00825 - Revoke all child certs issued by the old keypair. 00826 00827 - Generate a final CRL, signed with the old keypair, listing all 00828 the revoked certs, with a next CRL time after the last cert or 00829 CRL signed by the old keypair will have expired. 00830 00831 - Generate a corresponding final manifest. 00832 00833 - Destroy old keypairs. 00834 00835 - Leave final CRL and manifest in place until their nextupdate 00836 time has passed. 00837 """ 00838 00839 ca = self.ca 00840 parent = ca.parent 00841 00842 def parent_revoked(r_msg): 00843 00844 if r_msg.payload.ski != self.latest_ca_cert.gSKI(): 00845 raise rpki.exceptions.SKIMismatch 00846 00847 crl_interval = rpki.sundial.timedelta(seconds = parent.self.crl_interval) 00848 00849 nextUpdate = rpki.sundial.now() 00850 00851 if self.latest_manifest is not None: 00852 try: 00853 self.latest_manifest.get_content() 00854 except rpki.exceptions.CMSContentNotSet: 00855 self.latest_manifest.extract() 00856 nextUpdate = nextUpdate.later(self.latest_manifest.getNextUpdate()) 00857 00858 if self.latest_crl is not None: 00859 nextUpdate = nextUpdate.later(self.latest_crl.getNextUpdate()) 00860 00861 publisher = publication_queue() 00862 00863 for child_cert in self.child_certs: 00864 nextUpdate = nextUpdate.later(child_cert.cert.getNotAfter()) 00865 child_cert.revoke(publisher = publisher) 00866 00867 for roa in self.roas: 00868 nextUpdate = nextUpdate.later(roa.cert.getNotAfter()) 00869 roa.revoke(publisher = publisher) 00870 00871 for ghostbuster in self.ghostbusters: 00872 nextUpdate = nextUpdate.later(ghostbuster.cert.getNotAfter()) 00873 ghostbuster.revoke(publisher = publisher) 00874 00875 nextUpdate += crl_interval 00876 self.generate_crl(publisher = publisher, nextUpdate = nextUpdate) 00877 self.generate_manifest(publisher = publisher, nextUpdate = nextUpdate) 00878 self.private_key_id = None 00879 self.manifest_private_key_id = None 00880 self.manifest_public_key = None 00881 self.latest_manifest_cert = None 00882 self.state = "revoked" 00883 self.sql_mark_dirty() 00884 publisher.call_pubd(cb, eb) 00885 00886 rpki.up_down.revoke_pdu.query(ca, self.latest_ca_cert.gSKI(), parent_revoked, eb) 00887 00888 def update(self, parent, ca, rc, sia_uri_changed, old_resources, callback, errback): 00889 """ 00890 Need to get a new certificate for this ca_detail and perhaps frob 00891 children of this ca_detail. 00892 """ 00893 00894 def issued(issue_response): 00895 self.latest_ca_cert = issue_response.payload.classes[0].certs[0].cert 00896 new_resources = self.latest_ca_cert.get_3779resources() 00897 publisher = publication_queue() 00898 00899 if sia_uri_changed or old_resources.oversized(new_resources): 00900 for child_cert in self.child_certs: 00901 child_resources = child_cert.cert.get_3779resources() 00902 if sia_uri_changed or child_resources.oversized(new_resources): 00903 child_cert.reissue( 00904 ca_detail = self, 00905 resources = child_resources.intersection(new_resources), 00906 publisher = publisher) 00907 00908 publisher.call_pubd(callback, errback) 00909 00910 rpki.up_down.issue_pdu.query(parent, ca, self, issued, errback) 00911 00912 @classmethod 00913 def create(cls, ca): 00914 """ 00915 Create a new ca_detail object for a specified CA. 00916 """ 00917 self = cls() 00918 self.gctx = ca.gctx 00919 self.ca_id = ca.ca_id 00920 self.state = "pending" 00921 00922 self.private_key_id = rpki.x509.RSA.generate() 00923 self.public_key = self.private_key_id.get_RSApublic() 00924 00925 self.manifest_private_key_id = rpki.x509.RSA.generate() 00926 self.manifest_public_key = self.manifest_private_key_id.get_RSApublic() 00927 00928 self.sql_store() 00929 return self 00930 00931 def issue_ee(self, ca, resources, subject_key, sia = None): 00932 """ 00933 Issue a new EE certificate. 00934 """ 00935 00936 return self.latest_ca_cert.issue( 00937 keypair = self.private_key_id, 00938 subject_key = subject_key, 00939 serial = ca.next_serial_number(), 00940 sia = sia, 00941 aia = self.ca_cert_uri, 00942 crldp = self.crl_uri, 00943 resources = resources, 00944 notAfter = self.latest_ca_cert.getNotAfter(), 00945 is_ca = False) 00946 00947 00948 def generate_manifest_cert(self): 00949 """ 00950 Generate a new manifest certificate for this ca_detail. 00951 """ 00952 00953 resources = rpki.resource_set.resource_bag.from_inheritance() 00954 self.latest_manifest_cert = self.issue_ee(self.ca, resources, self.manifest_public_key) 00955 00956 def issue(self, ca, child, subject_key, sia, resources, publisher, child_cert = None): 00957 """ 00958 Issue a new certificate to a child. Optional child_cert argument 00959 specifies an existing child_cert object to update in place; if not 00960 specified, we create a new one. Returns the child_cert object 00961 containing the newly issued cert. 00962 """ 00963 00964 assert child_cert is None or (child_cert.child_id == child.child_id and 00965 child_cert.ca_detail_id == self.ca_detail_id) 00966 00967 cert = self.latest_ca_cert.issue( 00968 keypair = self.private_key_id, 00969 subject_key = subject_key, 00970 serial = ca.next_serial_number(), 00971 aia = self.ca_cert_uri, 00972 crldp = self.crl_uri, 00973 sia = sia, 00974 resources = resources, 00975 notAfter = resources.valid_until) 00976 00977 if child_cert is None: 00978 child_cert = rpki.rpkid.child_cert_obj( 00979 gctx = child.gctx, 00980 child_id = child.child_id, 00981 ca_detail_id = self.ca_detail_id, 00982 cert = cert) 00983 rpki.log.debug("Created new child_cert %r" % child_cert) 00984 else: 00985 child_cert.cert = cert 00986 rpki.log.debug("Reusing existing child_cert %r" % child_cert) 00987 00988 child_cert.ski = cert.get_SKI() 00989 child_cert.published = rpki.sundial.now() 00990 child_cert.sql_store() 00991 publisher.publish( 00992 cls = rpki.publication.certificate_elt, 00993 uri = child_cert.uri, 00994 obj = child_cert.cert, 00995 repository = ca.parent.repository, 00996 handler = child_cert.published_callback) 00997 self.generate_manifest(publisher = publisher) 00998 return child_cert 00999 01000 def generate_crl(self, publisher, nextUpdate = None): 01001 """ 01002 Generate a new CRL for this ca_detail. At the moment this is 01003 unconditional, that is, it is up to the caller to decide whether a 01004 new CRL is needed. 01005 """ 01006 01007 ca = self.ca 01008 parent = ca.parent 01009 crl_interval = rpki.sundial.timedelta(seconds = parent.self.crl_interval) 01010 now = rpki.sundial.now() 01011 01012 if nextUpdate is None: 01013 nextUpdate = now + crl_interval 01014 01015 certlist = [] 01016 for revoked_cert in self.revoked_certs: 01017 if now > revoked_cert.expires + crl_interval: 01018 revoked_cert.sql_delete() 01019 else: 01020 certlist.append((revoked_cert.serial, revoked_cert.revoked.toASN1tuple(), ())) 01021 certlist.sort() 01022 01023 self.latest_crl = rpki.x509.CRL.generate( 01024 keypair = self.private_key_id, 01025 issuer = self.latest_ca_cert, 01026 serial = ca.next_crl_number(), 01027 thisUpdate = now, 01028 nextUpdate = nextUpdate, 01029 revokedCertificates = certlist) 01030 01031 self.crl_published = rpki.sundial.now() 01032 self.sql_mark_dirty() 01033 publisher.publish(cls = rpki.publication.crl_elt, uri = self.crl_uri, obj = self.latest_crl, repository = parent.repository, 01034 handler = self.crl_published_callback) 01035 01036 def crl_published_callback(self, pdu): 01037 """ 01038 Check result of CRL publication. 01039 """ 01040 pdu.raise_if_error() 01041 self.crl_published = None 01042 self.sql_mark_dirty() 01043 01044 def generate_manifest(self, publisher, nextUpdate = None): 01045 """ 01046 Generate a new manifest for this ca_detail. 01047 """ 01048 01049 ca = self.ca 01050 parent = ca.parent 01051 crl_interval = rpki.sundial.timedelta(seconds = parent.self.crl_interval) 01052 now = rpki.sundial.now() 01053 01054 if nextUpdate is None: 01055 nextUpdate = now + crl_interval 01056 01057 if self.latest_manifest_cert is None or self.latest_manifest_cert.getNotAfter() < nextUpdate: 01058 self.generate_manifest_cert() 01059 01060 objs = [(self.crl_uri_tail, self.latest_crl)] 01061 objs.extend((c.uri_tail, c.cert) for c in self.child_certs) 01062 objs.extend((r.uri_tail, r.roa) for r in self.roas if r.roa is not None) 01063 objs.extend((g.uri_tail, g.ghostbuster) for g in self.ghostbusters) 01064 01065 self.latest_manifest = rpki.x509.SignedManifest.build( 01066 serial = ca.next_manifest_number(), 01067 thisUpdate = now, 01068 nextUpdate = nextUpdate, 01069 names_and_objs = objs, 01070 keypair = self.manifest_private_key_id, 01071 certs = self.latest_manifest_cert) 01072 01073 01074 self.manifest_published = rpki.sundial.now() 01075 self.sql_mark_dirty() 01076 publisher.publish(cls = rpki.publication.manifest_elt, uri = self.manifest_uri, obj = self.latest_manifest, repository = parent.repository, 01077 handler = self.manifest_published_callback) 01078 01079 def manifest_published_callback(self, pdu): 01080 """ 01081 Check result of manifest publication. 01082 """ 01083 pdu.raise_if_error() 01084 self.manifest_published = None 01085 self.sql_mark_dirty() 01086 01087 def reissue(self, cb, eb): 01088 """ 01089 Reissue all current certificates issued by this ca_detail. 01090 """ 01091 01092 publisher = publication_queue() 01093 for roa in self.roas: 01094 roa.regenerate(publisher, fast = True) 01095 for ghostbuster in self.ghostbusters: 01096 ghostbuster.regenerate(publisher, fast = True) 01097 for child_cert in self.child_certs: 01098 child_cert.reissue(self, publisher, force = True) 01099 publisher.call_pubd(cb, eb) 01100 01101 class child_cert_obj(rpki.sql.sql_persistent): 01102 """ 01103 Certificate that has been issued to a child. 01104 """ 01105 01106 sql_template = rpki.sql.template( 01107 "child_cert", 01108 "child_cert_id", 01109 ("cert", rpki.x509.X509), 01110 "child_id", 01111 "ca_detail_id", 01112 "ski", 01113 ("published", rpki.sundial.datetime)) 01114 01115 def __init__(self, gctx = None, child_id = None, ca_detail_id = None, cert = None): 01116 """ 01117 Initialize a child_cert_obj. 01118 """ 01119 rpki.sql.sql_persistent.__init__(self) 01120 self.gctx = gctx 01121 self.child_id = child_id 01122 self.ca_detail_id = ca_detail_id 01123 self.cert = cert 01124 self.published = None 01125 if child_id or ca_detail_id or cert: 01126 self.sql_mark_dirty() 01127 01128 @property 01129 def child(self): 01130 """ 01131 Fetch child object to which this child_cert object links. 01132 """ 01133 return rpki.left_right.child_elt.sql_fetch(self.gctx, self.child_id) 01134 01135 @property 01136 def ca_detail(self): 01137 """ 01138 Fetch ca_detail object to which this child_cert object links. 01139 """ 01140 return ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id) 01141 01142 @property 01143 def uri_tail(self): 01144 """ 01145 Return the tail (filename) portion of the URI for this child_cert. 01146 """ 01147 return self.cert.gSKI() + ".cer" 01148 01149 @property 01150 def uri(self): 01151 """ 01152 Return the publication URI for this child_cert. 01153 """ 01154 return self.ca_detail.ca.sia_uri + self.uri_tail 01155 01156 def revoke(self, publisher, generate_crl_and_manifest = False): 01157 """ 01158 Revoke a child cert. 01159 """ 01160 01161 ca_detail = self.ca_detail 01162 ca = ca_detail.ca 01163 rpki.log.debug("Revoking %r %r" % (self, self.uri)) 01164 revoked_cert_obj.revoke(cert = self.cert, ca_detail = ca_detail) 01165 publisher.withdraw(cls = rpki.publication.certificate_elt, uri = self.uri, obj = self.cert, repository = ca.parent.repository) 01166 self.gctx.sql.sweep() 01167 self.sql_delete() 01168 if generate_crl_and_manifest: 01169 ca_detail.generate_crl(publisher = publisher) 01170 ca_detail.generate_manifest(publisher = publisher) 01171 01172 def reissue(self, ca_detail, publisher, resources = None, sia = None, force = False): 01173 """ 01174 Reissue an existing child cert, reusing the public key. If the 01175 child cert we would generate is identical to the one we already 01176 have, we just return the one we already have. If we have to 01177 revoke the old child cert when generating the new one, we have to 01178 generate a new child_cert_obj, so calling code that needs the 01179 updated child_cert_obj must use the return value from this method. 01180 """ 01181 01182 ca = ca_detail.ca 01183 child = self.child 01184 01185 old_resources = self.cert.get_3779resources() 01186 old_sia = self.cert.get_SIA() 01187 old_ca_detail = self.ca_detail 01188 01189 needed = False 01190 01191 if resources is None: 01192 resources = old_resources 01193 01194 if sia is None: 01195 sia = old_sia 01196 01197 assert resources.valid_until is not None and old_resources.valid_until is not None 01198 01199 if resources != old_resources: 01200 rpki.log.debug("Resources changed for %r" % self) 01201 needed = True 01202 01203 if sia != old_sia: 01204 rpki.log.debug("SIA changed for %r" % self) 01205 needed = True 01206 01207 if ca_detail != old_ca_detail: 01208 rpki.log.debug("Issuer changed for %r" % self) 01209 needed = True 01210 01211 must_revoke = old_resources.oversized(resources) or old_resources.valid_until > resources.valid_until 01212 if must_revoke: 01213 rpki.log.debug("Must revoke any existing cert(s) for %r" % self) 01214 needed = True 01215 01216 new_issuer = ca_detail != old_ca_detail 01217 if new_issuer: 01218 rpki.log.debug("Issuer changed for %r" % self) 01219 needed = True 01220 01221 if resources.valid_until != old_resources.valid_until: 01222 rpki.log.debug("Validity changed for %r: %s %s" % (self, old_resources.valid_until, resources.valid_until)) 01223 needed = True 01224 01225 if not needed and force: 01226 rpki.log.debug("No change needed for %r, forcing reissuance anyway" % self) 01227 needed = True 01228 01229 if not needed: 01230 rpki.log.debug("No change to %r" % self) 01231 return self 01232 01233 if must_revoke: 01234 for x in child.fetch_child_certs(ca_detail = ca_detail, ski = self.ski): 01235 rpki.log.debug("Revoking child_cert %r" % x) 01236 x.revoke(publisher = publisher) 01237 ca_detail.generate_crl(publisher = publisher) 01238 ca_detail.generate_manifest(publisher = publisher) 01239 01240 child_cert = ca_detail.issue( 01241 ca = ca, 01242 child = child, 01243 subject_key = self.cert.getPublicKey(), 01244 sia = sia, 01245 resources = resources, 01246 child_cert = None if must_revoke or new_issuer else self, 01247 publisher = publisher) 01248 01249 rpki.log.debug("New child_cert %r uri %s" % (child_cert, child_cert.uri)) 01250 01251 return child_cert 01252 01253 @classmethod 01254 def fetch(cls, gctx = None, child = None, ca_detail = None, ski = None, unique = False): 01255 """ 01256 Fetch all child_cert objects matching a particular set of 01257 parameters. This is a wrapper to consolidate various queries that 01258 would otherwise be inline SQL WHERE expressions. In most cases 01259 code calls this indirectly, through methods in other classes. 01260 """ 01261 01262 args = [] 01263 where = [] 01264 01265 if child: 01266 where.append("child_id = %s") 01267 args.append(child.child_id) 01268 01269 if ca_detail: 01270 where.append("ca_detail_id = %s") 01271 args.append(ca_detail.ca_detail_id) 01272 01273 if ski: 01274 where.append("ski = %s") 01275 args.append(ski) 01276 01277 where = " AND ".join(where) 01278 01279 gctx = gctx or (child and child.gctx) or (ca_detail and ca_detail.gctx) or None 01280 01281 if unique: 01282 return cls.sql_fetch_where1(gctx, where, args) 01283 else: 01284 return cls.sql_fetch_where(gctx, where, args) 01285 01286 def published_callback(self, pdu): 01287 """ 01288 Publication callback: check result and mark published. 01289 """ 01290 pdu.raise_if_error() 01291 self.published = None 01292 self.sql_mark_dirty() 01293 01294 class revoked_cert_obj(rpki.sql.sql_persistent): 01295 """ 01296 Tombstone for a revoked certificate. 01297 """ 01298 01299 sql_template = rpki.sql.template( 01300 "revoked_cert", 01301 "revoked_cert_id", 01302 "serial", 01303 "ca_detail_id", 01304 ("revoked", rpki.sundial.datetime), 01305 ("expires", rpki.sundial.datetime)) 01306 01307 def __init__(self, gctx = None, serial = None, revoked = None, expires = None, ca_detail_id = None): 01308 """ 01309 Initialize a revoked_cert_obj. 01310 """ 01311 rpki.sql.sql_persistent.__init__(self) 01312 self.gctx = gctx 01313 self.serial = serial 01314 self.revoked = revoked 01315 self.expires = expires 01316 self.ca_detail_id = ca_detail_id 01317 if serial or revoked or expires or ca_detail_id: 01318 self.sql_mark_dirty() 01319 01320 @property 01321 def ca_detail(self): 01322 """ 01323 Fetch ca_detail object to which this revoked_cert_obj links. 01324 """ 01325 return ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id) 01326 01327 @classmethod 01328 def revoke(cls, cert, ca_detail): 01329 """ 01330 Revoke a certificate. 01331 """ 01332 return cls( 01333 serial = cert.getSerial(), 01334 expires = cert.getNotAfter(), 01335 revoked = rpki.sundial.now(), 01336 gctx = ca_detail.gctx, 01337 ca_detail_id = ca_detail.ca_detail_id) 01338 01339 class roa_obj(rpki.sql.sql_persistent): 01340 """ 01341 Route Origin Authorization. 01342 """ 01343 01344 sql_template = rpki.sql.template( 01345 "roa", 01346 "roa_id", 01347 "ca_detail_id", 01348 "self_id", 01349 "asn", 01350 ("roa", rpki.x509.ROA), 01351 ("cert", rpki.x509.X509), 01352 ("published", rpki.sundial.datetime)) 01353 01354 ca_detail_id = None 01355 cert = None 01356 roa = None 01357 published = None 01358 01359 @property 01360 def self(self): 01361 """ 01362 Fetch self object to which this roa_obj links. 01363 """ 01364 return rpki.left_right.self_elt.sql_fetch(self.gctx, self.self_id) 01365 01366 @property 01367 def ca_detail(self): 01368 """ 01369 Fetch ca_detail object to which this roa_obj links. 01370 """ 01371 return rpki.rpkid.ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id) 01372 01373 def sql_fetch_hook(self): 01374 """ 01375 Extra SQL fetch actions for roa_obj -- handle prefix lists. 01376 """ 01377 for version, datatype, attribute in ((4, rpki.resource_set.roa_prefix_set_ipv4, "ipv4"), 01378 (6, rpki.resource_set.roa_prefix_set_ipv6, "ipv6")): 01379 setattr(self, attribute, datatype.from_sql( 01380 self.gctx.sql, 01381 """ 01382 SELECT prefix, prefixlen, max_prefixlen FROM roa_prefix 01383 WHERE roa_id = %s AND version = %s 01384 """, 01385 (self.roa_id, version))) 01386 01387 def sql_insert_hook(self): 01388 """ 01389 Extra SQL insert actions for roa_obj -- handle prefix lists. 01390 """ 01391 for version, prefix_set in ((4, self.ipv4), (6, self.ipv6)): 01392 if prefix_set: 01393 self.gctx.sql.executemany( 01394 """ 01395 INSERT roa_prefix (roa_id, prefix, prefixlen, max_prefixlen, version) 01396 VALUES (%s, %s, %s, %s, %s) 01397 """, 01398 ((self.roa_id, x.prefix, x.prefixlen, x.max_prefixlen, version) 01399 for x in prefix_set)) 01400 01401 def sql_delete_hook(self): 01402 """ 01403 Extra SQL delete actions for roa_obj -- handle prefix lists. 01404 """ 01405 self.gctx.sql.execute("DELETE FROM roa_prefix WHERE roa_id = %s", (self.roa_id,)) 01406 01407 def __repr__(self): 01408 v4 = "" if self.ipv4 is None else self.ipv4 01409 v6 = "" if self.ipv6 is None else self.ipv6 01410 return rpki.log.log_repr(self, self.asn, ("%s,%s" % (v4, v6)).strip(",")) 01411 01412 def __init__(self, gctx = None, self_id = None, asn = None, ipv4 = None, ipv6 = None): 01413 rpki.sql.sql_persistent.__init__(self) 01414 self.gctx = gctx 01415 self.self_id = self_id 01416 self.asn = asn 01417 self.ipv4 = ipv4 01418 self.ipv6 = ipv6 01419 01420 # Defer marking new ROA as dirty until .generate() has a chance to 01421 # finish setup, otherwise we get SQL consistency errors. 01422 # 01423 #if self_id or asn or ipv4 or ipv6: self.sql_mark_dirty() 01424 01425 def update(self, publisher, fast = False): 01426 """ 01427 Bring this roa_obj's ROA up to date if necesssary. 01428 """ 01429 01430 v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4() 01431 v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6() 01432 01433 if self.roa is None: 01434 rpki.log.debug("%r doesn't exist, generating" % self) 01435 return self.generate(publisher = publisher, fast = fast) 01436 01437 ca_detail = self.ca_detail 01438 01439 if ca_detail is None: 01440 rpki.log.debug("%r has no associated ca_detail, generating" % self) 01441 return self.generate(publisher = publisher, fast = fast) 01442 01443 if ca_detail.state != "active": 01444 rpki.log.debug("ca_detail associated with %r not active (state %s), regenerating" % (self, ca_detail.state)) 01445 return self.regenerate(publisher = publisher, fast = fast) 01446 01447 regen_time = self.cert.getNotAfter() - rpki.sundial.timedelta(seconds = self.self.regen_margin) 01448 01449 if rpki.sundial.now() > regen_time: 01450 rpki.log.debug("%r past threshold %s, regenerating" % (self, regen_time)) 01451 return self.regenerate(publisher = publisher, fast = fast) 01452 01453 ca_resources = ca_detail.latest_ca_cert.get_3779resources() 01454 ee_resources = self.cert.get_3779resources() 01455 01456 if ee_resources.oversized(ca_resources): 01457 rpki.log.debug("%r oversized with respect to CA, regenerating" % self) 01458 return self.regenerate(publisher = publisher, fast = fast) 01459 01460 if ee_resources.v4 != v4 or ee_resources.v6 != v6: 01461 rpki.log.debug("%r resources do not match EE, regenerating" % self) 01462 return self.regenerate(publisher = publisher, fast = fast) 01463 01464 def generate(self, publisher, fast = False): 01465 """ 01466 Generate a ROA. 01467 01468 At present we have no way of performing a direct lookup from a 01469 desired set of resources to a covering certificate, so we have to 01470 search. This could be quite slow if we have a lot of active 01471 ca_detail objects. Punt on the issue for now, revisit if 01472 profiling shows this as a hotspot. 01473 01474 Once we have the right covering certificate, we generate the ROA 01475 payload, generate a new EE certificate, use the EE certificate to 01476 sign the ROA payload, publish the result, then throw away the 01477 private key for the EE cert, all per the ROA specification. This 01478 implies that generating a lot of ROAs will tend to thrash 01479 /dev/random, but there is not much we can do about that. 01480 01481 If fast is set, we leave generating the new manifest for our 01482 caller to handle, presumably at the end of a bulk operation. 01483 """ 01484 01485 if self.ipv4 is None and self.ipv6 is None: 01486 raise rpki.exceptions.EmptyROAPrefixList 01487 01488 # Ugly and expensive search for covering ca_detail, there has to 01489 # be a better way, but it would require the ability to test for 01490 # resource subsets in SQL. 01491 01492 v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4() 01493 v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6() 01494 01495 ca_detail = self.ca_detail 01496 if ca_detail is None or ca_detail.state != "active" or ca_detail.has_expired(): 01497 ca_detail = None 01498 for parent in self.self.parents: 01499 for ca in parent.cas: 01500 ca_detail = ca.active_ca_detail 01501 if ca_detail is not None and not ca_detail.has_expired(): 01502 resources = ca_detail.latest_ca_cert.get_3779resources() 01503 if v4.issubset(resources.v4) and v6.issubset(resources.v6): 01504 break 01505 ca_detail = None 01506 if ca_detail is not None: 01507 break 01508 01509 if ca_detail is None: 01510 raise rpki.exceptions.NoCoveringCertForROA, "Could not find a certificate covering %r" % self 01511 01512 ca = ca_detail.ca 01513 resources = rpki.resource_set.resource_bag(v4 = v4, v6 = v6) 01514 keypair = rpki.x509.RSA.generate() 01515 01516 self.ca_detail_id = ca_detail.ca_detail_id 01517 self.cert = ca_detail.issue_ee( 01518 ca = ca, 01519 resources = resources, 01520 subject_key = keypair.get_RSApublic(), 01521 sia = ((rpki.oids.name2oid["id-ad-signedObject"], ("uri", self.uri_from_key(keypair))),)) 01522 self.roa = rpki.x509.ROA.build(self.asn, self.ipv4, self.ipv6, keypair, (self.cert,)) 01523 self.published = rpki.sundial.now() 01524 self.sql_store() 01525 01526 rpki.log.debug("Generating %r URI %s" % (self, self.uri)) 01527 publisher.publish(cls = rpki.publication.roa_elt, uri = self.uri, obj = self.roa, repository = ca.parent.repository, handler = self.published_callback) 01528 if not fast: 01529 ca_detail.generate_manifest(publisher = publisher) 01530 01531 def published_callback(self, pdu): 01532 """ 01533 Check publication result. 01534 """ 01535 pdu.raise_if_error() 01536 self.published = None 01537 self.sql_mark_dirty() 01538 01539 def revoke(self, publisher, regenerate = False, allow_failure = False, fast = False): 01540 """ 01541 Withdraw ROA associated with this roa_obj. 01542 01543 In order to preserve make-before-break properties without 01544 duplicating code, this method also handles generating a 01545 replacement ROA when requested. 01546 01547 If allow_failure is set, failing to withdraw the ROA will not be 01548 considered an error. 01549 01550 If fast is set, SQL actions will be deferred, on the assumption 01551 that our caller will handle regenerating CRL and manifest and 01552 flushing the SQL cache. 01553 """ 01554 01555 ca_detail = self.ca_detail 01556 cert = self.cert 01557 roa = self.roa 01558 uri = self.uri 01559 01560 if ca_detail.state != 'active': 01561 self.ca_detail_id = None 01562 01563 if regenerate: 01564 self.generate(publisher = publisher, fast = fast) 01565 01566 rpki.log.debug("Withdrawing %r %s and revoking its EE cert" % (self, uri)) 01567 rpki.rpkid.revoked_cert_obj.revoke(cert = cert, ca_detail = ca_detail) 01568 publisher.withdraw(cls = rpki.publication.roa_elt, uri = uri, obj = roa, repository = ca_detail.ca.parent.repository, 01569 handler = False if allow_failure else None) 01570 self.sql_mark_deleted() 01571 if not fast: 01572 ca_detail.generate_crl(publisher = publisher) 01573 ca_detail.generate_manifest(publisher = publisher) 01574 self.gctx.sql.sweep() 01575 01576 def regenerate(self, publisher, fast = False): 01577 """ 01578 Reissue ROA associated with this roa_obj. 01579 """ 01580 if self.ca_detail is None: 01581 self.generate(publisher = publisher, fast = fast) 01582 else: 01583 self.revoke(publisher = publisher, regenerate = True, fast = fast) 01584 01585 def uri_from_key(self, key): 01586 """ 01587 Return publication URI for a public key. 01588 """ 01589 return self.ca_detail.ca.sia_uri + key.gSKI() + ".roa" 01590 01591 @property 01592 def uri(self): 01593 """ 01594 Return the publication URI for this roa_obj's ROA. 01595 """ 01596 return self.ca_detail.ca.sia_uri + self.uri_tail 01597 01598 @property 01599 def uri_tail(self): 01600 """ 01601 Return the tail (filename portion) of the publication URI for this 01602 roa_obj's ROA. 01603 """ 01604 return self.cert.gSKI() + ".roa" 01605 01606 01607 class ghostbuster_obj(rpki.sql.sql_persistent): 01608 """ 01609 Ghostbusters record. 01610 """ 01611 01612 sql_template = rpki.sql.template( 01613 "ghostbuster", 01614 "ghostbuster_id", 01615 "ca_detail_id", 01616 "self_id", 01617 "vcard", 01618 ("ghostbuster", rpki.x509.Ghostbuster), 01619 ("cert", rpki.x509.X509), 01620 ("published", rpki.sundial.datetime)) 01621 01622 ca_detail_id = None 01623 cert = None 01624 ghostbuster = None 01625 published = None 01626 vcard = None 01627 01628 @property 01629 def self(self): 01630 """ 01631 Fetch self object to which this ghostbuster_obj links. 01632 """ 01633 return rpki.left_right.self_elt.sql_fetch(self.gctx, self.self_id) 01634 01635 @property 01636 def ca_detail(self): 01637 """ 01638 Fetch ca_detail object to which this ghostbuster_obj links. 01639 """ 01640 return rpki.rpkid.ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id) 01641 01642 def __init__(self, gctx = None, self_id = None, ca_detail_id = None, vcard = None): 01643 rpki.sql.sql_persistent.__init__(self) 01644 self.gctx = gctx 01645 self.self_id = self_id 01646 self.ca_detail_id = ca_detail_id 01647 self.vcard = vcard 01648 01649 # Defer marking new ghostbuster as dirty until .generate() has a chance to 01650 # finish setup, otherwise we get SQL consistency errors. 01651 01652 def update(self, publisher, fast = False): 01653 """ 01654 Bring this ghostbuster_obj up to date if necesssary. 01655 """ 01656 01657 if self.ghostbuster is None: 01658 rpki.log.debug("Ghostbuster record doesn't exist, generating") 01659 return self.generate(publisher = publisher, fast = fast) 01660 01661 regen_time = self.cert.getNotAfter() - rpki.sundial.timedelta(seconds = self.self.regen_margin) 01662 01663 if rpki.sundial.now() > regen_time: 01664 rpki.log.debug("Ghostbuster record past threshold %s, regenerating" % (regen_time,)) 01665 return self.regenerate(publisher = publisher, fast = fast) 01666 01667 def generate(self, publisher, fast = False): 01668 """ 01669 Generate a Ghostbuster record 01670 01671 Once we have the right covering certificate, we generate the 01672 ghostbuster payload, generate a new EE certificate, use the EE 01673 certificate to sign the ghostbuster payload, publish the result, 01674 then throw away the private key for the EE cert. This is modeled 01675 after the way we handle ROAs. 01676 01677 If fast is set, we leave generating the new manifest for our 01678 caller to handle, presumably at the end of a bulk operation. 01679 """ 01680 01681 ca_detail = self.ca_detail 01682 ca = ca_detail.ca 01683 01684 resources = rpki.resource_set.resource_bag.from_inheritance() 01685 keypair = rpki.x509.RSA.generate() 01686 01687 self.cert = ca_detail.issue_ee( 01688 ca = ca, 01689 resources = resources, 01690 subject_key = keypair.get_RSApublic(), 01691 sia = ((rpki.oids.name2oid["id-ad-signedObject"], ("uri", self.uri_from_key(keypair))),)) 01692 self.ghostbuster = rpki.x509.Ghostbuster.build(self.vcard, keypair, (self.cert,)) 01693 self.published = rpki.sundial.now() 01694 self.sql_store() 01695 01696 rpki.log.debug("Generating Ghostbuster record %r" % self.uri) 01697 publisher.publish(cls = rpki.publication.ghostbuster_elt, uri = self.uri, obj = self.ghostbuster, repository = ca.parent.repository, handler = self.published_callback) 01698 if not fast: 01699 ca_detail.generate_manifest(publisher = publisher) 01700 01701 def published_callback(self, pdu): 01702 """ 01703 Check publication result. 01704 """ 01705 pdu.raise_if_error() 01706 self.published = None 01707 self.sql_mark_dirty() 01708 01709 def revoke(self, publisher, regenerate = False, allow_failure = False, fast = False): 01710 """ 01711 Withdraw Ghostbuster associated with this ghostbuster_obj. 01712 01713 In order to preserve make-before-break properties without 01714 duplicating code, this method also handles generating a 01715 replacement ghostbuster when requested. 01716 01717 If allow_failure is set, failing to withdraw the ghostbuster will not be 01718 considered an error. 01719 01720 If fast is set, SQL actions will be deferred, on the assumption 01721 that our caller will handle regenerating CRL and manifest and 01722 flushing the SQL cache. 01723 """ 01724 01725 ca_detail = self.ca_detail 01726 cert = self.cert 01727 ghostbuster = self.ghostbuster 01728 uri = self.uri 01729 01730 if regenerate: 01731 assert ca_detail.state == 'active' 01732 self.generate(publisher = publisher, fast = fast) 01733 01734 rpki.log.debug("Withdrawing Ghostbuster record %r and revoking its EE cert" % uri) 01735 rpki.rpkid.revoked_cert_obj.revoke(cert = cert, ca_detail = ca_detail) 01736 publisher.withdraw(cls = rpki.publication.ghostbuster_elt, uri = uri, obj = ghostbuster, repository = ca_detail.ca.parent.repository, 01737 handler = False if allow_failure else None) 01738 self.sql_mark_deleted() 01739 if not fast: 01740 ca_detail.generate_crl(publisher = publisher) 01741 ca_detail.generate_manifest(publisher = publisher) 01742 self.gctx.sql.sweep() 01743 01744 def regenerate(self, publisher, fast = False): 01745 """ 01746 Reissue Ghostbuster associated with this ghostbuster_obj. 01747 """ 01748 if self.ghostbuster is None: 01749 self.generate(publisher = publisher, fast = fast) 01750 else: 01751 self.revoke(publisher = publisher, regenerate = True, fast = fast) 01752 01753 def uri_from_key(self, key): 01754 """ 01755 Return publication URI for a public key. 01756 """ 01757 return self.ca_detail.ca.sia_uri + key.gSKI() + ".gbr" 01758 01759 @property 01760 def uri(self): 01761 """ 01762 Return the publication URI for this ghostbuster_obj's ghostbuster. 01763 """ 01764 return self.ca_detail.ca.sia_uri + self.uri_tail 01765 01766 @property 01767 def uri_tail(self): 01768 """ 01769 Return the tail (filename portion) of the publication URI for this 01770 ghostbuster_obj's ghostbuster. 01771 """ 01772 return self.cert.gSKI() + ".gbr" 01773 01774 01775 class publication_queue(object): 01776 """ 01777 Utility to simplify publication from within rpkid. 01778 01779 General idea here is to accumulate a collection of objects to be 01780 published, in one or more repositories, each potentially with its 01781 own completion callback. Eventually we want to publish everything 01782 we've accumulated, at which point we need to iterate over the 01783 collection and do repository.call_pubd() for each repository. 01784 """ 01785 01786 replace = True 01787 01788 def __init__(self): 01789 self.repositories = {} 01790 self.msgs = {} 01791 self.handlers = {} 01792 if self.replace: 01793 self.uris = {} 01794 01795 def _add(self, uri, obj, repository, handler, make_pdu): 01796 rid = id(repository) 01797 if rid not in self.repositories: 01798 self.repositories[rid] = repository 01799 self.msgs[rid] = rpki.publication.msg.query() 01800 if self.replace and uri in self.uris: 01801 rpki.log.debug("Removing publication duplicate <%s %r %r>" % (self.uris[uri].action, self.uris[uri].uri, self.uris[uri].payload)) 01802 self.msgs[rid].remove(self.uris.pop(uri)) 01803 pdu = make_pdu(uri = uri, obj = obj) 01804 if handler is not None: 01805 self.handlers[id(pdu)] = handler 01806 pdu.tag = id(pdu) 01807 self.msgs[rid].append(pdu) 01808 if self.replace: 01809 self.uris[uri] = pdu 01810 01811 def publish(self, cls, uri, obj, repository, handler = None): 01812 return self._add( uri, obj, repository, handler, cls.make_publish) 01813 01814 def withdraw(self, cls, uri, obj, repository, handler = None): 01815 return self._add( uri, obj, repository, handler, cls.make_withdraw) 01816 01817 def call_pubd(self, cb, eb): 01818 def loop(iterator, rid): 01819 self.repositories[rid].call_pubd(iterator, eb, self.msgs[rid], self.handlers) 01820 rpki.async.iterator(self.repositories, loop, cb)