aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rpki/rpkid.py15
-rw-r--r--rpki/rpkidb/models.py898
2 files changed, 901 insertions, 12 deletions
diff --git a/rpki/rpkid.py b/rpki/rpkid.py
index 5ffd99d1..24f92a46 100644
--- a/rpki/rpkid.py
+++ b/rpki/rpkid.py
@@ -863,6 +863,8 @@ class ca_obj(rpki.sql.sql_persistent):
self.gctx.checkpoint()
self.rekey(cb, eb)
+ # Called from exactly one place, in rpki.rpkid_tasks.PollParentTask.class_loop().
+ # Probably want to refactor.
@classmethod
def create(cls, parent, rc, cb, eb):
"""
@@ -1093,7 +1095,8 @@ class ca_detail_obj(rpki.sql.sql_persistent):
attempted publication dates older than when.
"""
- return rpki.rpkid.roa_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s AND published IS NOT NULL and published < %s", (self.ca_detail_id, when))
+ return rpki.rpkid.roa_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s AND published IS NOT NULL and published < %s",
+ (self.ca_detail_id, when))
@property
def ghostbusters(self):
@@ -1109,7 +1112,9 @@ class ca_detail_obj(rpki.sql.sql_persistent):
ca_detail with attempted publication dates older than when.
"""
- return rpki.rpkid.ghostbuster_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s AND published IS NOT NULL and published < %s", (self.ca_detail_id, when))
+ return rpki.rpkid.ghostbuster_obj.sql_fetch_where(self.gctx,
+ "ca_detail_id = %s AND published IS NOT NULL and published < %s",
+ (self.ca_detail_id, when))
@property
def ee_certificates(self):
@@ -1125,7 +1130,9 @@ class ca_detail_obj(rpki.sql.sql_persistent):
ca_detail with attempted publication dates older than when.
"""
- return rpki.rpkid.ee_cert_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s AND published IS NOT NULL and published < %s", (self.ca_detail_id, when))
+ return rpki.rpkid.ee_cert_obj.sql_fetch_where(self.gctx,
+ "ca_detail_id = %s AND published IS NOT NULL and published < %s",
+ (self.ca_detail_id, when))
@property
def crl_uri(self):
@@ -1160,7 +1167,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
def covers(self, target):
"""
- Test whether this ca-detail covers a given set of resources.
+ Test whether this ca_detail covers a given set of resources.
"""
assert not target.asn.inherit and not target.v4.inherit and not target.v6.inherit
diff --git a/rpki/rpkidb/models.py b/rpki/rpkidb/models.py
index 6bfeed61..db41debf 100644
--- a/rpki/rpkidb/models.py
+++ b/rpki/rpkidb/models.py
@@ -300,13 +300,13 @@ class Self(models.Model):
def find_covering_ca_details(self, resources):
"""
- Return all active ca_detail_objs for this <self/> which cover a
+ Return all active CADetails for this <self/> which cover a
particular set of resources.
- If we expected there to be a large number of ca_detail_objs, we
+ If we expected there to be a large number of CADetails, we
could add index tables and write fancy SQL query to do this, but
for the expected common case where there are only one or two
- active ca_detail_objs per <self/>, it's probably not worth it. In
+ active CADetails per <self/>, it's probably not worth it. In
any case, this is an optimization we can leave for later.
"""
@@ -646,14 +646,29 @@ class Parent(models.Model):
content_type = rpki.up_down.content_type)
+ def construct_sia_uri(self, rc):
+ """
+ Construct the sia_uri value for a CA under this parent given
+ configured information and the parent's up-down protocol
+ list_response PDU.
+ """
+
+ sia_uri = rc.get("suggested_sia_head", "")
+ if not sia_uri.startswith("rsync://") or not sia_uri.startswith(self.sia_base):
+ sia_uri = self.sia_base
+ if not sia_uri.endswith("/"):
+ raise rpki.exceptions.BadURISyntax("SIA URI must end with a slash: %s" % sia_uri)
+ return sia_uri
+
+
class CA(models.Model):
- last_crl_sn = models.BigIntegerField()
- last_manifest_sn = models.BigIntegerField()
+ last_crl_sn = models.BigIntegerField(default = 1)
+ last_manifest_sn = models.BigIntegerField(default = 1)
next_manifest_update = SundialField(null = True)
next_crl_update = SundialField(null = True)
- last_issued_sn = models.BigIntegerField()
+ last_issued_sn = models.BigIntegerField(default = 1)
sia_uri = models.TextField(null = True)
- parent_resource_class = models.TextField(null = True)
+ parent_resource_class = models.TextField(null = True) # Not sure this should allow NULL
parent = models.ForeignKey(Parent, related_name = "cas")
# So it turns out that there's always a 1:1 mapping between the
@@ -666,6 +681,220 @@ class CA(models.Model):
# response; if not present, we'd use parent's class_name as now,
# otherwise we'd use the supplied class_name.
+ # ca_obj has a zillion properties encoding various specialized
+ # ca_detail queries. ORM query syntax probably renders this OBE,
+ # but need to translate in existing code.
+ #
+ #def pending_ca_details(self): return self.ca_details.filter(state = "pending")
+ #def active_ca_detail(self): return self.ca_details.get(state = "active")
+ #def deprecated_ca_details(self): return self.ca_details.filter(state = "deprecated")
+ #def active_or_deprecated_ca_details(self): return self.ca_details.filter(state__in = ("active", "deprecated"))
+ #def revoked_ca_details(self): return self.ca_details.filter(state = "revoked")
+ #def issue_response_candidate_ca_details(self): return self.ca_details.exclude(state = "revoked")
+
+
+ def check_for_updates(self, parent, rc, cb, eb):
+ """
+ Parent has signaled continued existance of a resource class we
+ already knew about, so we need to check for an updated
+ certificate, changes in resource coverage, revocation and reissue
+ with the same key, etc.
+ """
+
+ sia_uri = parent.construct_sia_uri(rc)
+ sia_uri_changed = self.sia_uri != sia_uri
+ if sia_uri_changed:
+ logger.debug("SIA changed: was %s now %s", self.sia_uri, sia_uri)
+ self.sia_uri = sia_uri
+ self.sql_mark_dirty()
+ class_name = rc.get("class_name")
+ rc_resources = rpki.resource_set.resource_bag(
+ rc.get("resource_set_as"),
+ rc.get("resource_set_ipv4"),
+ rc.get("resource_set_ipv6"),
+ rc.get("resource_set_notafter"))
+ cert_map = {}
+ for c in rc.getiterator(rpki.up_down.tag_certificate):
+ x = rpki.x509.X509(Base64 = c.text)
+ u = rpki.up_down.multi_uri(c.get("cert_url")).rsync()
+ cert_map[x.gSKI()] = (x, u)
+ def loop(iterator, ca_detail):
+ rc_cert, rc_cert_uri = cert_map.pop(ca_detail.public_key.gSKI(), (None, None))
+ if rc_cert is None:
+ logger.warning("SKI %s in resource class %s is in database but missing from list_response to %s from %s, "
+ "maybe parent certificate went away?",
+ ca_detail.public_key.gSKI(), class_name, parent.self.self_handle, parent.parent_handle)
+ publisher = publication_queue()
+ ca_detail.destroy(ca = ca_detail.ca, publisher = publisher)
+ return publisher.call_pubd(iterator, eb)
+ if ca_detail.state == "active" and ca_detail.ca_cert_uri != rc_cert_uri:
+ logger.debug("AIA changed: was %s now %s", ca_detail.ca_cert_uri, rc_cert_uri)
+ ca_detail.ca_cert_uri = rc_cert_uri
+ ca_detail.save()
+ if ca_detail.state not in ("pending", "active"):
+ return iterator()
+ if ca_detail.state == "pending":
+ current_resources = rpki.resource_set.resource_bag()
+ else:
+ current_resources = ca_detail.latest_ca_cert.get_3779resources()
+ if (ca_detail.state == "pending" or
+ sia_uri_changed or
+ ca_detail.latest_ca_cert != rc_cert or
+ ca_detail.latest_ca_cert.getNotAfter() != rc_resources.valid_until or
+ current_resources.undersized(rc_resources) or
+ current_resources.oversized(rc_resources)):
+ return ca_detail.update(
+ parent = parent,
+ ca = self,
+ rc = rc,
+ sia_uri_changed = sia_uri_changed,
+ old_resources = current_resources,
+ callback = iterator,
+ errback = eb)
+ iterator()
+ def done():
+ if cert_map:
+ logger.warning("Unknown certificate SKI%s %s in resource class %s in list_response to %s from %s, maybe you want to \"revoke_forgotten\"?",
+ "" if len(cert_map) == 1 else "s", ", ".join(cert_map), class_name, parent.self.self_handle, parent.parent_handle)
+ cb()
+ ca_details = self.ca_details.exclude(state = "revoked")
+ if ca_details:
+ rpki.async.iterator(ca_details, loop, done)
+ else:
+ logger.warning("Existing resource class %s to %s from %s with no certificates, rekeying",
+ class_name, parent.self.self_handle, parent.parent_handle)
+ self.rekey(cb, eb)
+
+
+ # Called from exactly one place, in rpki.rpkid_tasks.PollParentTask.class_loop().
+ # Might want to refactor.
+
+ @classmethod
+ def create(cls, parent, rc, cb, eb):
+ """
+ Parent has signaled existance of a new resource class, so we need
+ to create and set up a corresponding CA object.
+ """
+
+ self = cls.objects.create(parent = parent,
+ parent_resource_class = rc.get("class_name"),
+ sia_uri = parent.construct_sia_uri(rc))
+ ca_detail = CADetail.create(self)
+ def done(r_msg):
+ c = r_msg[0][0]
+ logger.debug("CA %r received certificate %s", self, c.get("cert_url"))
+ ca_detail.activate(
+ ca = self,
+ cert = rpki.x509.X509(Base64 = c.text),
+ uri = c.get("cert_url"),
+ callback = cb,
+ errback = eb)
+ logger.debug("Sending issue request to %r from %r", parent, self.create)
+ parent.up_down_issue_query(self, ca_detail, done, eb)
+
+
+ # Was .delete()
+ def destroy(self, parent, callback):
+ """
+ The list of current resource classes received from parent does not
+ include the class corresponding to this CA, so we need to delete
+ it (and its little dog too...).
+
+ All certs published by this CA are now invalid, so need to
+ withdraw them, the CRL, and the manifest from the repository,
+ delete all child_cert and ca_detail records associated with this
+ CA, then finally delete this CA itself.
+ """
+
+ def lose(e):
+ logger.exception("Could not delete CA %r, skipping", self)
+ callback()
+ def done():
+ logger.debug("Deleting %r", self)
+ self.delete()
+ callback()
+ publisher = publication_queue()
+ for ca_detail in self.ca_details.all():
+ ca_detail.destroy(ca = self, publisher = publisher, allow_failure = True)
+ publisher.call_pubd(done, lose)
+
+
+ def next_serial_number(self):
+ """
+ Allocate a certificate serial number.
+ """
+
+ self.last_issued_sn += 1
+ self.save()
+ return self.last_issued_sn
+
+
+ def next_manifest_number(self):
+ """
+ Allocate a manifest serial number.
+ """
+
+ self.last_manifest_sn += 1
+ self.save()
+ return self.last_manifest_sn
+
+
+ def next_crl_number(self):
+ """
+ Allocate a CRL serial number.
+ """
+
+ self.last_crl_sn += 1
+ self.save()
+ return self.last_crl_sn
+
+
+ def rekey(self, cb, eb):
+ """
+ Initiate a rekey operation for this CA. Generate a new keypair.
+ Request cert from parent using new keypair. Mark result as our
+ active ca_detail. Reissue all child certs issued by this CA using
+ the new ca_detail.
+ """
+
+ old_detail = self.ca_details.get(state = "active")
+ new_detail = CADetail.create(self)
+ def done(r_msg):
+ c = r_msg[0][0]
+ logger.debug("CA %r received certificate %s", self, c.get("cert_url"))
+ new_detail.activate(
+ ca = self,
+ cert = rpki.x509.X509(Base64 = c.text),
+ uri = c.get("cert_url"),
+ predecessor = old_detail,
+ callback = cb,
+ errback = eb)
+ logger.debug("Sending issue request to %r from %r", self.parent, self.rekey)
+ self.parent.up_down_issue_query(self, new_detail, done, eb)
+
+
+ def revoke(self, cb, eb, revoke_all = False):
+ """
+ Revoke deprecated ca_detail objects associated with this CA, or
+ all ca_details associated with this CA if revoke_all is set.
+ """
+
+ def loop(iterator, ca_detail):
+ ca_detail.revoke(cb = iterator, eb = eb)
+ rpki.async.iterator(self.ca_details.all() if revoke_all else self.ca_details.filter(state = "deprecated"),
+ loop, cb)
+
+
+ def reissue(self, cb, eb):
+ """
+ Reissue all current certificates issued by this CA.
+ """
+
+ ca_detail = self.ca_details.get(state = "active")
+ if ca_detail:
+ ca_detail.reissue(cb, eb)
+ else:
+ cb()
class CADetail(models.Model):
@@ -683,6 +912,500 @@ class CADetail(models.Model):
ca_cert_uri = models.TextField(null = True)
ca = models.ForeignKey(CA, related_name = "ca_details")
+
+ # Like the old ca_obj class, the old ca_detail_obj class had ten
+ # zillion properties and methods encapsulating SQL queries.
+ # Translate as we go.
+
+
+ @property
+ def crl_uri(self):
+ """
+ Return publication URI for this ca_detail's CRL.
+ """
+
+ return self.ca.sia_uri + self.crl_uri_tail
+
+
+ @property
+ def crl_uri_tail(self):
+ """
+ Return tail (filename portion) of publication URI for this ca_detail's CRL.
+ """
+
+ return self.public_key.gSKI() + ".crl"
+
+
+ @property
+ def manifest_uri(self):
+ """
+ Return publication URI for this ca_detail's manifest.
+ """
+
+ return self.ca.sia_uri + self.public_key.gSKI() + ".mft"
+
+
+ def has_expired(self):
+ """
+ Return whether this ca_detail's certificate has expired.
+ """
+
+ return self.latest_ca_cert.getNotAfter() <= rpki.sundial.now()
+
+
+ def covers(self, target):
+ """
+ Test whether this ca-detail covers a given set of resources.
+ """
+
+ assert not target.asn.inherit and not target.v4.inherit and not target.v6.inherit
+ me = self.latest_ca_cert.get_3779resources()
+ return target.asn <= me.asn and target.v4 <= me.v4 and target.v6 <= me.v6
+
+
+ def activate(self, ca, cert, uri, callback, errback, predecessor = None):
+ """
+ Activate this ca_detail.
+ """
+
+ publisher = publication_queue()
+ self.latest_ca_cert = cert
+ self.ca_cert_uri = uri
+ self.generate_manifest_cert()
+ self.state = "active"
+ self.generate_crl(publisher = publisher)
+ self.generate_manifest(publisher = publisher)
+ self.save()
+ if predecessor is not None:
+ predecessor.state = "deprecated"
+ predecessor.save()
+ for child_cert in predecessor.child_certs.all():
+ child_cert.reissue(ca_detail = self, publisher = publisher)
+ for roa in predecessor.roas.all():
+ roa.regenerate(publisher = publisher)
+ for ghostbuster in predecessor.ghostbusters.all():
+ ghostbuster.regenerate(publisher = publisher)
+ predecessor.generate_crl(publisher = publisher)
+ predecessor.generate_manifest(publisher = publisher)
+ publisher.call_pubd(callback, errback)
+
+
+ def destroy(self, ca, publisher, allow_failure = False):
+ """
+ Delete this ca_detail and all of the certs it issued.
+
+ If allow_failure is true, we clean up as much as we can but don't
+ raise an exception.
+ """
+
+ repository = ca.parent.repository
+ handler = False if allow_failure else None
+ for child_cert in self.child_certs.all():
+ publisher.queue(uri = child_cert.uri, old_obj = child_cert.cert, repository = repository, handler = handler)
+ child_cert.delete()
+ for roa in self.roas.all():
+ roa.revoke(publisher = publisher, allow_failure = allow_failure, fast = True)
+ for ghostbuster in self.ghostbusters.all():
+ ghostbuster.revoke(publisher = publisher, allow_failure = allow_failure, fast = True)
+ if self.latest_manifest is not None:
+ publisher.queue(uri = self.manifest_uri, old_obj = self.latest_manifest, repository = repository, handler = handler)
+ if self.latest_crl is not None:
+ publisher.queue(uri = self.crl_uri, old_obj = self.latest_crl, repository = repository, handler = handler)
+ for cert in self.revoked_certs.all(): # + self.child_certs.all()
+ logger.debug("Deleting %r", cert)
+ cert.delete()
+ logger.debug("Deleting %r", self)
+ self.delete()
+
+ def revoke(self, cb, eb):
+ """
+ Request revocation of all certificates whose SKI matches the key
+ for this ca_detail.
+
+ Tasks:
+
+ - Request revocation of old keypair by parent.
+
+ - Revoke all child certs issued by the old keypair.
+
+ - Generate a final CRL, signed with the old keypair, listing all
+ the revoked certs, with a next CRL time after the last cert or
+ CRL signed by the old keypair will have expired.
+
+ - Generate a corresponding final manifest.
+
+ - Destroy old keypairs.
+
+ - Leave final CRL and manifest in place until their nextupdate
+ time has passed.
+ """
+
+ ca = self.ca
+ parent = ca.parent
+ class_name = ca.parent_resource_class
+ gski = self.latest_ca_cert.gSKI()
+
+ def parent_revoked(r_msg):
+ if r_msg[0].get("class_name") != class_name:
+ raise rpki.exceptions.ResourceClassMismatch
+ if r_msg[0].get("ski") != gski:
+ raise rpki.exceptions.SKIMismatch
+ logger.debug("Parent revoked %s, starting cleanup", gski)
+ crl_interval = rpki.sundial.timedelta(seconds = parent.self.crl_interval)
+ nextUpdate = rpki.sundial.now()
+ if self.latest_manifest is not None:
+ self.latest_manifest.extract_if_needed()
+ nextUpdate = nextUpdate.later(self.latest_manifest.getNextUpdate())
+ if self.latest_crl is not None:
+ nextUpdate = nextUpdate.later(self.latest_crl.getNextUpdate())
+ publisher = publication_queue()
+ for child_cert in self.child_certs.all():
+ nextUpdate = nextUpdate.later(child_cert.cert.getNotAfter())
+ child_cert.revoke(publisher = publisher)
+ for roa in self.roas.all():
+ nextUpdate = nextUpdate.later(roa.cert.getNotAfter())
+ roa.revoke(publisher = publisher)
+ for ghostbuster in self.ghostbusters.all():
+ nextUpdate = nextUpdate.later(ghostbuster.cert.getNotAfter())
+ ghostbuster.revoke(publisher = publisher)
+ nextUpdate += crl_interval
+ self.generate_crl(publisher = publisher, nextUpdate = nextUpdate)
+ self.generate_manifest(publisher = publisher, nextUpdate = nextUpdate)
+ self.private_key_id = None
+ self.manifest_private_key_id = None
+ self.manifest_public_key = None
+ self.latest_manifest_cert = None
+ self.state = "revoked"
+ self.save()
+ publisher.call_pubd(cb, eb)
+ logger.debug("Asking parent to revoke CA certificate %s", gski)
+ parent.up_down_revoke_query(class_name, gski, parent_revoked, eb)
+
+
+ def update(self, parent, ca, rc, sia_uri_changed, old_resources, callback, errback):
+ """
+ Need to get a new certificate for this ca_detail and perhaps frob
+ children of this ca_detail.
+ """
+
+ def issued(r_msg):
+ c = r_msg[0][0]
+ cert = rpki.x509.X509(Base64 = c.text)
+ cert_url = c.get("cert_url")
+ logger.debug("CA %r received certificate %s", self, cert_url)
+ if self.state == "pending":
+ return self.activate(ca = ca, cert = cert, uri = cert_url, callback = callback, errback = errback)
+ validity_changed = self.latest_ca_cert is None or self.latest_ca_cert.getNotAfter() != cert.getNotAfter()
+ publisher = publication_queue()
+ if self.latest_ca_cert != cert:
+ self.latest_ca_cert = cert
+ self.save()
+ self.generate_manifest_cert()
+ self.generate_crl(publisher = publisher)
+ self.generate_manifest(publisher = publisher)
+ new_resources = self.latest_ca_cert.get_3779resources()
+ if sia_uri_changed or old_resources.oversized(new_resources):
+ for child_cert in self.child_certs.all():
+ child_resources = child_cert.cert.get_3779resources()
+ if sia_uri_changed or child_resources.oversized(new_resources):
+ child_cert.reissue(ca_detail = self, resources = child_resources & new_resources, publisher = publisher)
+ if sia_uri_changed or validity_changed or old_resources.oversized(new_resources):
+ for roa in self.roas.all():
+ roa.update(publisher = publisher, fast = True)
+ if sia_uri_changed or validity_changed:
+ for ghostbuster in self.ghostbusters.all():
+ ghostbuster.update(publisher = publisher, fast = True)
+ publisher.call_pubd(callback, errback)
+ logger.debug("Sending issue request to %r from %r", parent, self.update)
+ parent.up_down_issue_query(ca, self, issued, errback)
+
+
+ @classmethod
+ def create(cls, ca):
+ """
+ Create a new ca_detail object for a specified CA.
+ """
+
+ cer_keypair = rpki.x509.RSA.generate()
+ mft_keypair = rpki.x509.RSA.generate()
+ return cls.objects.create(ca = ca, state = "pending",
+ private_key_id = cer_keypair, public_key = cer_keypair.get_public(),
+ manifest_private_key_id = mft_keypair, manifest_public_key = mft_keypair.get_public())
+
+
+ def issue_ee(self, ca, resources, subject_key, sia,
+ cn = None, sn = None, notAfter = None, eku = None):
+ """
+ Issue a new EE certificate.
+ """
+
+ if notAfter is None:
+ notAfter = self.latest_ca_cert.getNotAfter()
+ return self.latest_ca_cert.issue(
+ keypair = self.private_key_id,
+ subject_key = subject_key,
+ serial = ca.next_serial_number(),
+ sia = sia,
+ aia = self.ca_cert_uri,
+ crldp = self.crl_uri,
+ resources = resources,
+ notAfter = notAfter,
+ is_ca = False,
+ cn = cn,
+ sn = sn,
+ eku = eku)
+
+
+ def generate_manifest_cert(self):
+ """
+ Generate a new manifest certificate for this ca_detail.
+ """
+
+ resources = rpki.resource_set.resource_bag.from_inheritance()
+ self.latest_manifest_cert = self.issue_ee(
+ ca = self.ca,
+ resources = resources,
+ subject_key = self.manifest_public_key,
+ sia = (None, None, self.manifest_uri, rpki.publication.rrdp_sia_uri_kludge))
+
+
+ def issue(self, ca, child, subject_key, sia, resources, publisher, child_cert = None):
+ """
+ Issue a new certificate to a child. Optional child_cert argument
+ specifies an existing child_cert object to update in place; if not
+ specified, we create a new one. Returns the child_cert object
+ containing the newly issued cert.
+ """
+
+ self.check_failed_publication(publisher)
+ cert = self.latest_ca_cert.issue(
+ keypair = self.private_key_id,
+ subject_key = subject_key,
+ serial = ca.next_serial_number(),
+ aia = self.ca_cert_uri,
+ crldp = self.crl_uri,
+ sia = sia,
+ resources = resources,
+ notAfter = resources.valid_until)
+ if child_cert is None:
+ old_cert = None
+ child_cert = ChildCert(child = child, ca_detail = self, cert = cert)
+ logger.debug("Created new child_cert %r", child_cert)
+ else:
+ old_cert = child_cert.cert
+ child_cert.cert = cert
+ child_cert.ca_detail = self
+ logger.debug("Reusing existing child_cert %r", child_cert)
+ child_cert.ski = cert.get_SKI()
+ child_cert.published = rpki.sundial.now()
+ child_cert.save()
+ publisher.queue(
+ uri = child_cert.uri,
+ old_obj = old_cert,
+ new_obj = child_cert.cert,
+ repository = ca.parent.repository,
+ handler = child_cert.published_callback)
+ self.generate_manifest(publisher = publisher)
+ return child_cert
+
+
+ def generate_crl(self, publisher, nextUpdate = None):
+ """
+ Generate a new CRL for this ca_detail. At the moment this is
+ unconditional, that is, it is up to the caller to decide whether a
+ new CRL is needed.
+ """
+
+ self.check_failed_publication(publisher)
+ crl_interval = rpki.sundial.timedelta(seconds = self.ca.parent.self.crl_interval)
+ now = rpki.sundial.now()
+ if nextUpdate is None:
+ nextUpdate = now + crl_interval
+ certlist = []
+ for revoked_cert in self.revoked_certs.all():
+ if now > revoked_cert.expires + crl_interval:
+ revoked_cert.delete()
+ else:
+ certlist.append((revoked_cert.serial, revoked_cert.revoked))
+ certlist.sort()
+ old_crl = self.latest_crl
+ self.latest_crl = rpki.x509.CRL.generate(
+ keypair = self.private_key_id,
+ issuer = self.latest_ca_cert,
+ serial = self.ca.next_crl_number(),
+ thisUpdate = now,
+ nextUpdate = nextUpdate,
+ revokedCertificates = certlist)
+ self.crl_published = now
+ self.save()
+ publisher.queue(
+ uri = self.crl_uri,
+ old_obj = old_crl,
+ new_obj = self.latest_crl,
+ repository = self.ca.parent.repository,
+ handler = self.crl_published_callback)
+
+
+ def crl_published_callback(self, pdu):
+ """
+ Check result of CRL publication.
+ """
+
+ rpki.publication.raise_if_error(pdu)
+ self.crl_published = None
+ self.save()
+
+
+ def generate_manifest(self, publisher, nextUpdate = None):
+ """
+ Generate a new manifest for this ca_detail.
+ """
+
+ self.check_failed_publication(publisher)
+
+ crl_interval = rpki.sundial.timedelta(seconds = self.ca.parent.self.crl_interval)
+ now = rpki.sundial.now()
+ uri = self.manifest_uri
+ if nextUpdate is None:
+ nextUpdate = now + crl_interval
+ if (self.latest_manifest_cert is None or
+ (self.latest_manifest_cert.getNotAfter() < nextUpdate and
+ self.latest_manifest_cert.getNotAfter() < self.latest_ca_cert.getNotAfter())):
+ logger.debug("Generating EE certificate for %s", uri)
+ self.generate_manifest_cert()
+ logger.debug("Latest CA cert notAfter %s, new %s EE notAfter %s",
+ self.latest_ca_cert.getNotAfter(), uri, self.latest_manifest_cert.getNotAfter())
+ logger.debug("Constructing manifest object list for %s", uri)
+ objs = [(self.crl_uri_tail, self.latest_crl)]
+ objs.extend((c.uri_tail, c.cert) for c in self.child_certs.all())
+ objs.extend((r.uri_tail, r.roa) for r in self.roas.filter(roa__isnull = False))
+ objs.extend((g.uri_tail, g.ghostbuster) for g in self.ghostbusters.all())
+ objs.extend((e.uri_tail, e.cert) for e in self.ee_certificates.all())
+ logger.debug("Building manifest object %s", uri)
+ old_manifest = self.latest_manifest
+ self.latest_manifest = rpki.x509.SignedManifest.build(
+ serial = self.ca.next_manifest_number(),
+ thisUpdate = now,
+ nextUpdate = nextUpdate,
+ names_and_objs = objs,
+ keypair = self.manifest_private_key_id,
+ certs = self.latest_manifest_cert)
+ logger.debug("Manifest generation took %s", rpki.sundial.now() - now)
+ self.manifest_published = now
+ self.save()
+ publisher.queue(uri = uri,
+ old_obj = old_manifest,
+ new_obj = self.latest_manifest,
+ repository = self.ca.parent.repository,
+ handler = self.manifest_published_callback)
+
+
+ def manifest_published_callback(self, pdu):
+ """
+ Check result of manifest publication.
+ """
+
+ rpki.publication.raise_if_error(pdu)
+ self.manifest_published = None
+ self.save()
+
+
+ def reissue(self, cb, eb):
+ """
+ Reissue all current certificates issued by this ca_detail.
+ """
+
+ publisher = publication_queue()
+ self.check_failed_publication(publisher)
+ for roa in self.roas.all():
+ roa.regenerate(publisher, fast = True)
+ for ghostbuster in self.ghostbusters.all():
+ ghostbuster.regenerate(publisher, fast = True)
+ for ee_certificate in self.ee_certificates.all():
+ ee_certificate.reissue(publisher, force = True)
+ for child_cert in self.child_certs.all():
+ child_cert.reissue(self, publisher, force = True)
+ self.generate_manifest_cert()
+ self.save()
+ self.generate_crl(publisher = publisher)
+ self.generate_manifest(publisher = publisher)
+ self.save()
+ publisher.call_pubd(cb, eb)
+
+
+ def check_failed_publication(self, publisher, check_all = True):
+ """
+ Check for failed publication of objects issued by this ca_detail.
+
+ All publishable objects have timestamp fields recording time of
+ last attempted publication, and callback methods which clear these
+ timestamps once publication has succeeded. Our task here is to
+ look for objects issued by this ca_detail which have timestamps
+ set (indicating that they have not been published) and for which
+ the timestamps are not very recent (for some definition of very
+ recent -- intent is to allow a bit of slack in case pubd is just
+ being slow). In such cases, we want to retry publication.
+
+ As an optimization, we can probably skip checking other products
+ if manifest and CRL have been published, thus saving ourselves
+ several complex SQL queries. Not sure yet whether this
+ optimization is worthwhile.
+
+ For the moment we check everything without optimization, because
+ it simplifies testing.
+
+ For the moment our definition of staleness is hardwired; this
+ should become configurable.
+ """
+
+ logger.debug("Checking for failed publication for %r", self)
+
+ stale = rpki.sundial.now() - rpki.sundial.timedelta(seconds = 60)
+ repository = self.ca.parent.repository
+ if self.latest_crl is not None and self.crl_published is not None and self.crl_published < stale:
+ logger.debug("Retrying publication for %s", self.crl_uri)
+ publisher.queue(uri = self.crl_uri,
+ new_obj = self.latest_crl,
+ repository = repository,
+ handler = self.crl_published_callback)
+ if self.latest_manifest is not None and self.manifest_published is not None and self.manifest_published < stale:
+ logger.debug("Retrying publication for %s", self.manifest_uri)
+ publisher.queue(uri = self.manifest_uri,
+ new_obj = self.latest_manifest,
+ repository = repository,
+ handler = self.manifest_published_callback)
+ if not check_all:
+ return
+ for child_cert in self.child_certs.filter(published__isnull = False, published__lt = stale):
+ logger.debug("Retrying publication for %s", child_cert)
+ publisher.queue(
+ uri = child_cert.uri,
+ new_obj = child_cert.cert,
+ repository = repository,
+ handler = child_cert.published_callback)
+ for roa in self.roas.filter(published__isnull = False, published__lt = stale):
+ logger.debug("Retrying publication for %s", roa)
+ publisher.queue(
+ uri = roa.uri,
+ new_obj = roa.roa,
+ repository = repository,
+ handler = roa.published_callback)
+ for ghostbuster in self.ghostbusters.filter(published__isnull = False, published__lt = stale):
+ logger.debug("Retrying publication for %s", ghostbuster)
+ publisher.queue(
+ uri = ghostbuster.uri,
+ new_obj = ghostbuster.ghostbuster,
+ repository = repository,
+ handler = ghostbuster.published_callback)
+ for ee_cert in self.ee_certs.filter(published__isnull = False, published__lt = stale):
+ logger.debug("Retrying publication for %s", ee_cert)
+ publisher.queue(
+ uri = ee_cert.uri,
+ new_obj = ee_cert.cert,
+ repository = repository,
+ handler = ee_cert.published_callback)
+
+
class Child(models.Model):
child_handle = models.SlugField(max_length = 255)
bpki_cert = CertificateField(null = True)
@@ -878,7 +1601,6 @@ class Child(models.Model):
lose(e)
-
class ChildCert(models.Model):
cert = CertificateField()
published = SundialField(null = True)
@@ -886,6 +1608,116 @@ class ChildCert(models.Model):
child = models.ForeignKey(Child, related_name = "child_certs")
ca_detail = models.ForeignKey(CADetail, related_name = "child_certs")
+
+ @property
+ def uri_tail(self):
+ """
+ Return the tail (filename) portion of the URI for this child_cert.
+ """
+
+ return self.cert.gSKI() + ".cer"
+
+
+ @property
+ def uri(self):
+ """
+ Return the publication URI for this child_cert.
+ """
+
+ return self.ca_detail.ca.sia_uri + self.uri_tail
+
+
+ def revoke(self, publisher, generate_crl_and_manifest = True):
+ """
+ Revoke a child cert.
+ """
+
+ ca_detail = self.ca_detail
+ logger.debug("Revoking %r %r", self, self.uri)
+ RevokedCert.revoke(cert = self.cert, ca_detail = ca_detail)
+ publisher.queue(uri = self.uri, old_obj = self.cert, repository = ca_detail.ca.parent.repository)
+ self.delete()
+ if generate_crl_and_manifest:
+ ca_detail.generate_crl(publisher = publisher)
+ ca_detail.generate_manifest(publisher = publisher)
+
+
+ def reissue(self, ca_detail, publisher, resources = None, sia = None, force = False):
+ """
+ Reissue an existing child cert, reusing the public key. If the
+ child cert we would generate is identical to the one we already
+ have, we just return the one we already have. If we have to
+ revoke the old child cert when generating the new one, we have to
+ generate a new child_cert_obj, so calling code that needs the
+ updated child_cert_obj must use the return value from this method.
+ """
+
+ ca = ca_detail.ca
+ child = self.child
+ old_resources = self.cert.get_3779resources()
+ old_sia = self.cert.get_SIA()
+ old_aia = self.cert.get_AIA()[0]
+ old_ca_detail = self.ca_detail
+ needed = False
+ if resources is None:
+ resources = old_resources
+ if sia is None:
+ sia = old_sia
+ assert resources.valid_until is not None and old_resources.valid_until is not None
+ if resources.asn != old_resources.asn or resources.v4 != old_resources.v4 or resources.v6 != old_resources.v6:
+ logger.debug("Resources changed for %r: old %s new %s", self, old_resources, resources)
+ needed = True
+ if resources.valid_until != old_resources.valid_until:
+ logger.debug("Validity changed for %r: old %s new %s",
+ self, old_resources.valid_until, resources.valid_until)
+ needed = True
+ if sia != old_sia:
+ logger.debug("SIA changed for %r: old %r new %r", self, old_sia, sia)
+ needed = True
+ if ca_detail != old_ca_detail:
+ logger.debug("Issuer changed for %r: old %r new %r", self, old_ca_detail, ca_detail)
+ needed = True
+ if ca_detail.ca_cert_uri != old_aia:
+ logger.debug("AIA changed for %r: old %r new %r", self, old_aia, ca_detail.ca_cert_uri)
+ needed = True
+ must_revoke = old_resources.oversized(resources) or old_resources.valid_until > resources.valid_until
+ if must_revoke:
+ logger.debug("Must revoke any existing cert(s) for %r", self)
+ needed = True
+ if not needed and force:
+ logger.debug("No change needed for %r, forcing reissuance anyway", self)
+ needed = True
+ if not needed:
+ logger.debug("No change to %r", self)
+ return self
+ if must_revoke:
+ for x in child.child_certs.filter(ca_detail = ca_detail, ski = self.ski):
+ logger.debug("Revoking child_cert %r", x)
+ x.revoke(publisher = publisher)
+ ca_detail.generate_crl(publisher = publisher)
+ ca_detail.generate_manifest(publisher = publisher)
+ child_cert = ca_detail.issue(
+ ca = ca,
+ child = child,
+ subject_key = self.cert.getPublicKey(),
+ sia = sia,
+ resources = resources,
+ child_cert = None if must_revoke else self,
+ publisher = publisher)
+ logger.debug("New child_cert %r uri %s", child_cert, child_cert.uri)
+ return child_cert
+
+
+ def published_callback(self, pdu):
+ """
+ Publication callback: check result and mark published.
+ """
+
+ rpki.publication.raise_if_error(pdu)
+ self.published = None
+ self.save()
+
+
class EECert(models.Model):
ski = BlobField()
cert = CertificateField()
@@ -907,6 +1739,19 @@ class RevokedCert(models.Model):
expires = SundialField()
ca_detail = models.ForeignKey(CADetail, related_name = "revoked_certs")
+ @classmethod
+ def revoke(cls, cert, ca_detail):
+ """
+ Revoke a certificate.
+ """
+
+ return cls.objects.create(
+ serial = cert.getSerial(),
+ expires = cert.getNotAfter(),
+ revoked = rpki.sundial.now(),
+ ca_detail = ca_detail)
+
+
class ROA(models.Model):
asn = models.BigIntegerField()
cert = CertificateField()
@@ -915,6 +1760,43 @@ class ROA(models.Model):
self = models.ForeignKey(Self, related_name = "roas")
ca_detail = models.ForeignKey(CADetail, related_name = "roas")
+ # Is there a good reason why we even bother with the ROAPrefix table
+ # or the asn field here? It looks like we only use this data to
+ # store and reconstruct the complete resource set, which we already
+ # have present in the form of the signed ROA. We pay a bit of
+ # overhead on this either way (SQL vs ASN.1) but since we have to
+ # store the complete ROA anyway, and since we don't allow it to be
+ # NULL, perhaps we can simplify this considerably by dropping the
+ # asn field and the ROAPrefix table completely.
+ #
+ # If we do need this stuff, see rpki.irdb.models.ROARequest.
+ #
+ # Compromise that might make sense: do store the prefix list, but in
+ # text form: it's what we're getting from XML in any case, and
+ # almost certainly faster to convert to and from resource_set than
+ # any of the other options here (no SQL, no ASN.1).
+ #
+ # The one query that we might someday want to be able to make in SQL
+ # rather than in Python to speed up processing for really big ROA
+ # sets is not on the ROAs anyway, it's on the covering certificates.
+ # In theory, for really big data sets, it might be worth setting up
+ # a secondary lookup table for resources which would let us use SQL
+ # to figure out which certificates cover a particular ROA request.
+ # But the SQL query would be so hideous to construct that we'd have
+ # to be desperate for it to be worthwhile. Basically, for each
+ # prefix in a ROA prefix set, one would look for a covering range in
+ # the lookup table, then take the intersection of those results to
+ # see if any ca_detail matched all the criteria. SQL would look
+ # something like:
+ #
+ # SELECT x.ca_detail_id FROM x WHERE x.min <= prefix1.min AND x.max >= prefix1.max
+ # INTERSECT
+ # SELECT x.ca_detail_id FROM x WHERE x.min <= prefix2.min AND x.max >= prefix2.max
+ # INTERSECT
+ # SELECT x.ca_detail_id FROM x WHERE x.min <= prefix3.min AND x.max >= prefix3.max
+ # ...;
+
+
class ROAPrefix(models.Model):
prefix = models.CharField(max_length = 40)
prefixlen = models.SmallIntegerField()