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