RPKI Engine 1.0

rpkid.py (3793)

Go to the documentation of this file.
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)
 All Classes Namespaces Files Functions Variables