RPKI Engine  1.0
rpkid.py (4014)
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 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)
 All Classes Namespaces Files Functions Variables Properties