diff options
author | Rob Austein <sra@hactrn.net> | 2015-10-26 06:29:00 +0000 |
---|---|---|
committer | Rob Austein <sra@hactrn.net> | 2015-10-26 06:29:00 +0000 |
commit | b46deb1417dc3596e9ac9fe2fe8cc0b7f42457e7 (patch) | |
tree | ca0dc0276d1adc168bc3337ce0564c4ec4957c1b /rpki/rpkidb | |
parent | 397beaf6d9900dc3b3cb612c89ebf1d57b1d16f6 (diff) |
"Any programmer who fails to comply with the standard naming, formatting,
or commenting conventions should be shot. If it so happens that it is
inconvenient to shoot him, then he is to be politely requested to recode
his program in adherence to the above standard."
-- Michael Spier, Digital Equipment Corporation
svn path=/branches/tk705/; revision=6152
Diffstat (limited to 'rpki/rpkidb')
-rw-r--r-- | rpki/rpkidb/models.py | 4060 |
1 files changed, 2030 insertions, 2030 deletions
diff --git a/rpki/rpkidb/models.py b/rpki/rpkidb/models.py index 1a293360..ab16a176 100644 --- a/rpki/rpkidb/models.py +++ b/rpki/rpkidb/models.py @@ -5,7 +5,6 @@ Django ORM models for rpkid. from __future__ import unicode_literals import logging -import base64 import tornado.gen import tornado.web @@ -35,189 +34,189 @@ logger = logging.getLogger(__name__) # very simple change given migrations. class XMLTemplate(object): - """ - Encapsulate all the voodoo for transcoding between lxml and ORM. - """ - - # Type map to simplify declaration of Base64 sub-elements. - - element_type = dict(bpki_cert = rpki.x509.X509, - bpki_glue = rpki.x509.X509, - pkcs10_request = rpki.x509.PKCS10, - signing_cert = rpki.x509.X509, - signing_cert_crl = rpki.x509.CRL) - - - def __init__(self, name, attributes = (), booleans = (), elements = (), readonly = (), handles = ()): - self.name = name - self.handles = handles - self.attributes = attributes - self.booleans = booleans - self.elements = elements - self.readonly = readonly - - - def encode(self, obj, q_pdu, r_msg): - """ - Encode an ORM object as XML. """ - - r_pdu = SubElement(r_msg, rpki.left_right.xmlns + self.name, nsmap = rpki.left_right.nsmap, action = q_pdu.get("action")) - if self.name != "tenant": - r_pdu.set("tenant_handle", obj.tenant.tenant_handle) - r_pdu.set(self.name + "_handle", getattr(obj, self.name + "_handle")) - if q_pdu.get("tag"): - r_pdu.set("tag", q_pdu.get("tag")) - for h in self.handles: - k = h.xml_template.name - v = getattr(obj, k) - if v is not None: - r_pdu.set(k + "_handle", getattr(v, k + "_handle")) - for k in self.attributes: - v = getattr(obj, k) - if v is not None: - r_pdu.set(k, str(v)) - for k in self.booleans: - if getattr(obj, k): - r_pdu.set(k, "yes") - for k in self.elements + self.readonly: - v = getattr(obj, k) - if v is not None and not v.empty(): - SubElement(r_pdu, rpki.left_right.xmlns + k).text = v.get_Base64() - logger.debug("XMLTemplate.encode(): %s", ElementToString(r_pdu)) - - - def acknowledge(self, obj, q_pdu, r_msg): - """ - Add an acknowledgement PDU in response to a create, set, or - destroy action. - - This includes a bit of special-case code for BSC objects which has - to go somewhere; we could handle it via some kind method of - call-out to the BSC model, but it's not worth building a general - mechanism for one case, so we do it inline and have done. - """ - - assert q_pdu.tag == rpki.left_right.xmlns + self.name - action = q_pdu.get("action") - r_pdu = SubElement(r_msg, rpki.left_right.xmlns + self.name, nsmap = rpki.left_right.nsmap, action = action) - if self.name != "tenant": - r_pdu.set("tenant_handle", obj.tenant.tenant_handle) - r_pdu.set(self.name + "_handle", getattr(obj, self.name + "_handle")) - if q_pdu.get("tag"): - r_pdu.set("tag", q_pdu.get("tag")) - if self.name == "bsc" and action != "destroy" and obj.pkcs10_request is not None: - assert not obj.pkcs10_request.empty() - SubElement(r_pdu, rpki.left_right.xmlns + "pkcs10_request").text = obj.pkcs10_request.get_Base64() - logger.debug("XMLTemplate.acknowledge(): %s", ElementToString(r_pdu)) - - - def decode(self, obj, q_pdu): - """ - Decode XML into an ORM object. - """ - - logger.debug("XMLTemplate.decode(): %r %s", obj, ElementToString(q_pdu)) - assert q_pdu.tag == rpki.left_right.xmlns + self.name - for h in self.handles: - k = h.xml_template.name - v = q_pdu.get(k + "_handle") - if v is not None: - setattr(obj, k, h.objects.get(**{k + "_handle" : v, "tenant" : obj.tenant})) - for k in self.attributes: - v = q_pdu.get(k) - if v is not None: - v.encode("ascii") - if v.isdigit(): - v = long(v) - setattr(obj, k, v) - for k in self.booleans: - v = q_pdu.get(k) - if v is not None: - setattr(obj, k, v == "yes") - for k in self.elements: - v = q_pdu.findtext(rpki.left_right.xmlns + k) - if v and v.strip(): - setattr(obj, k, self.element_type[k](Base64 = v)) + Encapsulate all the voodoo for transcoding between lxml and ORM. + """ + + # Type map to simplify declaration of Base64 sub-elements. + + element_type = dict(bpki_cert = rpki.x509.X509, + bpki_glue = rpki.x509.X509, + pkcs10_request = rpki.x509.PKCS10, + signing_cert = rpki.x509.X509, + signing_cert_crl = rpki.x509.CRL) + + + def __init__(self, name, attributes = (), booleans = (), elements = (), readonly = (), handles = ()): + self.name = name + self.handles = handles + self.attributes = attributes + self.booleans = booleans + self.elements = elements + self.readonly = readonly + + + def encode(self, obj, q_pdu, r_msg): + """ + Encode an ORM object as XML. + """ + + r_pdu = SubElement(r_msg, rpki.left_right.xmlns + self.name, nsmap = rpki.left_right.nsmap, action = q_pdu.get("action")) + if self.name != "tenant": + r_pdu.set("tenant_handle", obj.tenant.tenant_handle) + r_pdu.set(self.name + "_handle", getattr(obj, self.name + "_handle")) + if q_pdu.get("tag"): + r_pdu.set("tag", q_pdu.get("tag")) + for h in self.handles: + k = h.xml_template.name + v = getattr(obj, k) + if v is not None: + r_pdu.set(k + "_handle", getattr(v, k + "_handle")) + for k in self.attributes: + v = getattr(obj, k) + if v is not None: + r_pdu.set(k, str(v)) + for k in self.booleans: + if getattr(obj, k): + r_pdu.set(k, "yes") + for k in self.elements + self.readonly: + v = getattr(obj, k) + if v is not None and not v.empty(): + SubElement(r_pdu, rpki.left_right.xmlns + k).text = v.get_Base64() + logger.debug("XMLTemplate.encode(): %s", ElementToString(r_pdu)) + + + def acknowledge(self, obj, q_pdu, r_msg): + """ + Add an acknowledgement PDU in response to a create, set, or + destroy action. + + This includes a bit of special-case code for BSC objects which has + to go somewhere; we could handle it via some kind method of + call-out to the BSC model, but it's not worth building a general + mechanism for one case, so we do it inline and have done. + """ + + assert q_pdu.tag == rpki.left_right.xmlns + self.name + action = q_pdu.get("action") + r_pdu = SubElement(r_msg, rpki.left_right.xmlns + self.name, nsmap = rpki.left_right.nsmap, action = action) + if self.name != "tenant": + r_pdu.set("tenant_handle", obj.tenant.tenant_handle) + r_pdu.set(self.name + "_handle", getattr(obj, self.name + "_handle")) + if q_pdu.get("tag"): + r_pdu.set("tag", q_pdu.get("tag")) + if self.name == "bsc" and action != "destroy" and obj.pkcs10_request is not None: + assert not obj.pkcs10_request.empty() + SubElement(r_pdu, rpki.left_right.xmlns + "pkcs10_request").text = obj.pkcs10_request.get_Base64() + logger.debug("XMLTemplate.acknowledge(): %s", ElementToString(r_pdu)) + + + def decode(self, obj, q_pdu): + """ + Decode XML into an ORM object. + """ + + logger.debug("XMLTemplate.decode(): %r %s", obj, ElementToString(q_pdu)) + assert q_pdu.tag == rpki.left_right.xmlns + self.name + for h in self.handles: + k = h.xml_template.name + v = q_pdu.get(k + "_handle") + if v is not None: + setattr(obj, k, h.objects.get(**{k + "_handle" : v, "tenant" : obj.tenant})) + for k in self.attributes: + v = q_pdu.get(k) + if v is not None: + v.encode("ascii") + if v.isdigit(): + v = long(v) + setattr(obj, k, v) + for k in self.booleans: + v = q_pdu.get(k) + if v is not None: + setattr(obj, k, v == "yes") + for k in self.elements: + v = q_pdu.findtext(rpki.left_right.xmlns + k) + if v and v.strip(): + setattr(obj, k, self.element_type[k](Base64 = v)) class XMLManager(models.Manager): # pylint: disable=W0232 - """ - Add a few methods which locate or create an object or objects - corresponding to the handles in an XML element, as appropriate. - - This assumes that models which use it have an "xml_template" - class attribute holding an XMLTemplate object (above). - """ - - def xml_get_or_create(self, xml): - name = self.model.xml_template.name - action = xml.get("action") - assert xml.tag == rpki.left_right.xmlns + name and action in ("create", "set") - d = { name + "_handle" : xml.get(name + "_handle") } - if name != "tenant" and action != "create": - d["tenant__tenant_handle"] = xml.get("tenant_handle") - logger.debug("XMLManager.xml_get_or_create(): name %s action %s filter %r", name, action, d) - result = self.model(**d) if action == "create" else self.get(**d) - if name != "tenant" and action == "create": - result.tenant = Tenant.objects.get(tenant_handle = xml.get("tenant_handle")) - logger.debug("XMLManager.xml_get_or_create(): name %s action %s filter %r result %r", name, action, d, result) - return result - - def xml_list(self, xml): - name = self.model.xml_template.name - action = xml.get("action") - assert xml.tag == rpki.left_right.xmlns + name and action in ("get", "list") - d = {} - if action == "get": - d[name + "_handle"] = xml.get(name + "_handle") - if name != "tenant": - d["tenant__tenant_handle"] = xml.get("tenant_handle") - logger.debug("XMLManager.xml_list(): name %s action %s filter %r", name, action, d) - result = self.filter(**d) if d else self.all() - logger.debug("XMLManager.xml_list(): name %s action %s filter %r result %r", name, action, d, result) - return result - - def xml_get_for_delete(self, xml): - name = self.model.xml_template.name - action = xml.get("action") - assert xml.tag == rpki.left_right.xmlns + name and action == "destroy" - d = { name + "_handle" : xml.get(name + "_handle") } - if name != "tenant": - d["tenant__tenant_handle"] = xml.get("tenant_handle") - logger.debug("XMLManager.xml_get_for_delete(): name %s action %s filter %r", name, action, d) - result = self.get(**d) - logger.debug("XMLManager.xml_get_for_delete(): name %s action %s filter %r result %r", name, action, d, result) - return result + """ + Add a few methods which locate or create an object or objects + corresponding to the handles in an XML element, as appropriate. + + This assumes that models which use it have an "xml_template" + class attribute holding an XMLTemplate object (above). + """ + + def xml_get_or_create(self, xml): + name = self.model.xml_template.name + action = xml.get("action") + assert xml.tag == rpki.left_right.xmlns + name and action in ("create", "set") + d = { name + "_handle" : xml.get(name + "_handle") } + if name != "tenant" and action != "create": + d["tenant__tenant_handle"] = xml.get("tenant_handle") + logger.debug("XMLManager.xml_get_or_create(): name %s action %s filter %r", name, action, d) + result = self.model(**d) if action == "create" else self.get(**d) + if name != "tenant" and action == "create": + result.tenant = Tenant.objects.get(tenant_handle = xml.get("tenant_handle")) + logger.debug("XMLManager.xml_get_or_create(): name %s action %s filter %r result %r", name, action, d, result) + return result + + def xml_list(self, xml): + name = self.model.xml_template.name + action = xml.get("action") + assert xml.tag == rpki.left_right.xmlns + name and action in ("get", "list") + d = {} + if action == "get": + d[name + "_handle"] = xml.get(name + "_handle") + if name != "tenant": + d["tenant__tenant_handle"] = xml.get("tenant_handle") + logger.debug("XMLManager.xml_list(): name %s action %s filter %r", name, action, d) + result = self.filter(**d) if d else self.all() + logger.debug("XMLManager.xml_list(): name %s action %s filter %r result %r", name, action, d, result) + return result + + def xml_get_for_delete(self, xml): + name = self.model.xml_template.name + action = xml.get("action") + assert xml.tag == rpki.left_right.xmlns + name and action == "destroy" + d = { name + "_handle" : xml.get(name + "_handle") } + if name != "tenant": + d["tenant__tenant_handle"] = xml.get("tenant_handle") + logger.debug("XMLManager.xml_get_for_delete(): name %s action %s filter %r", name, action, d) + result = self.get(**d) + logger.debug("XMLManager.xml_get_for_delete(): name %s action %s filter %r result %r", name, action, d, result) + return result def xml_hooks(cls): - """ - Class decorator to add default XML hooks. - """ + """ + Class decorator to add default XML hooks. + """ - # Maybe inheritance from an abstract model would work here. Then - # again, maybe we could use this decorator to do something prettier - # for the XMLTemplate setup. Whatever. Gussie up later. + # Maybe inheritance from an abstract model would work here. Then + # again, maybe we could use this decorator to do something prettier + # for the XMLTemplate setup. Whatever. Gussie up later. - def default_xml_pre_save_hook(self, q_pdu): - logger.debug("default_xml_pre_save_hook()") + def default_xml_pre_save_hook(self, q_pdu): + logger.debug("default_xml_pre_save_hook()") - @tornado.gen.coroutine - def default_xml_post_save_hook(self, rpkid, q_pdu): - logger.debug("default_xml_post_save_hook()") + @tornado.gen.coroutine + def default_xml_post_save_hook(self, rpkid, q_pdu): + logger.debug("default_xml_post_save_hook()") - @tornado.gen.coroutine - def default_xml_pre_delete_hook(self, rpkid): - logger.debug("default_xml_pre_delete_hook()") + @tornado.gen.coroutine + def default_xml_pre_delete_hook(self, rpkid): + logger.debug("default_xml_pre_delete_hook()") - for name, method in (("xml_pre_save_hook", default_xml_pre_save_hook), - ("xml_post_save_hook", default_xml_post_save_hook), - ("xml_pre_delete_hook", default_xml_pre_delete_hook)): - if not hasattr(cls, name): - setattr(cls, name, method) + for name, method in (("xml_pre_save_hook", default_xml_pre_save_hook), + ("xml_post_save_hook", default_xml_post_save_hook), + ("xml_pre_delete_hook", default_xml_pre_delete_hook)): + if not hasattr(cls, name): + setattr(cls, name, method) - return cls + return cls # Models. @@ -227,2128 +226,2129 @@ def xml_hooks(cls): @xml_hooks class Tenant(models.Model): - tenant_handle = models.SlugField(max_length = 255) - use_hsm = models.BooleanField(default = False) - crl_interval = models.BigIntegerField(null = True) - regen_margin = models.BigIntegerField(null = True) - bpki_cert = CertificateField(null = True) - bpki_glue = CertificateField(null = True) - objects = XMLManager() - - xml_template = XMLTemplate( - name = "tenant", - attributes = ("crl_interval", "regen_margin"), - booleans = ("use_hsm",), - elements = ("bpki_cert", "bpki_glue")) - - @tornado.gen.coroutine - def xml_pre_delete_hook(self, rpkid): - yield [parent.destroy() for parent in self.parents.all()] - - @tornado.gen.coroutine - def xml_post_save_hook(self, rpkid, q_pdu): - rekey = q_pdu.get("rekey") - revoke = q_pdu.get("revoke") - reissue = q_pdu.get("reissue") - revoke_forgotten = q_pdu.get("revoke_forgotten") - - if q_pdu.get("clear_replay_protection"): - for parent in self.parents.all(): - parent.clear_replay_protection() - for child in self.children.all(): - child.clear_replay_protection() - for repository in self.repositories.all(): - repository.clear_replay_protection() - - futures = [] - - if rekey or revoke or reissue or revoke_forgotten: - for parent in self.parents.all(): - if rekey: - futures.append(parent.serve_rekey(rpkid)) - if revoke: - futures.append(parent.serve_revoke(rpkid)) - if reissue: - futures.append(parent.serve_reissue(rpkid)) - if revoke_forgotten: - futures.append(parent.serve_revoke_forgotten(rpkid)) - - if q_pdu.get("publish_world_now"): - futures.append(self.serve_publish_world_now(rpkid)) - if q_pdu.get("run_now"): - futures.append(self.serve_run_now(rpkid)) - - yield futures - - - @tornado.gen.coroutine - def serve_publish_world_now(self, rpkid): - publisher = rpki.rpkid.publication_queue(rpkid) - repositories = set() - objects = dict() - - for parent in self.parents.all(): - - repository = parent.repository - if repository.peer_contact_uri in repositories: - continue - repositories.add(repository.peer_contact_uri) - q_msg = Element(rpki.publication.tag_msg, nsmap = rpki.publication.nsmap, - type = "query", version = rpki.publication.version) - SubElement(q_msg, rpki.publication.tag_list, tag = "list") - - r_msg = yield repository.call_pubd(rpkid, q_msg, length_check = False) - - for r_pdu in r_msg: - assert r_pdu.tag == rpki.publication.tag_list - if r_pdu.get("uri") in objects: - logger.warning("pubd reported multiple published copies of URI %r, this makes no sense, blundering onwards", r_pdu.get("uri")) - else: - objects[r_pdu.get("uri")] = (r_pdu.get("hash"), repository) - - def reconcile(uri, obj, repository): - h, r = objects.pop(uri, (None, None)) - if h is not None: - assert r == repository - publisher.queue(uri = uri, new_obj = obj, old_hash = h, repository = repository) - - for ca_detail in CADetail.objects.filter(ca__parent__tenant = self, state = "active"): - repository = ca_detail.ca.parent.repository - reconcile(uri = ca_detail.crl_uri, obj = ca_detail.latest_crl, repository = repository) - reconcile(uri = ca_detail.manifest_uri, obj = ca_detail.latest_manifest, repository = repository) - for c in ca_detail.child_certs.all(): - reconcile(uri = c.uri, obj = c.cert, repository = repository) - for r in ca_detail.roas.filter(roa__isnull = False): - reconcile(uri = r.uri, obj = r.roa, repository = repository) - for g in ca_detail.ghostbusters.all(): - reconcile(uri = g.uri, obj = g.ghostbuster, repository = repository) - for c in ca_detail.ee_certificates.all(): - reconcile(uri = c.uri, obj = c.cert, repository = repository) - for u in objects: - h, r = objects[u] - publisher.queue(uri = u, old_hash = h, repository = r) - - yield publisher.call_pubd() - - - @tornado.gen.coroutine - def serve_run_now(self, rpkid): - logger.debug("Forced immediate run of periodic actions for tenant %s[%r]", self.tenant_handle, self) - tasks = self.cron_tasks(rpkid) - rpkid.task_add(tasks) - futures = [task.wait() for task in tasks] - rpkid.task_run() - yield futures - - - def cron_tasks(self, rpkid): - try: - return self._cron_tasks - except AttributeError: - self._cron_tasks = tuple(task(rpkid, self) for task in rpki.rpkid_tasks.task_classes) - return self._cron_tasks - - - def find_covering_ca_details(self, resources): - """ - Return all active CADetails for this <tenant/> which cover a - particular set of resources. - - 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 CADetails per <tenant/>, it's probably not worth it. In - any case, this is an optimization we can leave for later. - """ - - return set(ca_detail - for ca_detail in CADetail.objects.filter(ca__parent__tenant = self, state = "active") - if ca_detail.covers(resources)) - - -@xml_hooks -class BSC(models.Model): - bsc_handle = models.SlugField(max_length = 255) - private_key_id = RSAPrivateKeyField() - pkcs10_request = PKCS10Field() - hash_alg = EnumField(choices = ("sha256",), default = "sha256") - signing_cert = CertificateField(null = True) - signing_cert_crl = CRLField(null = True) - tenant = models.ForeignKey(Tenant, related_name = "bscs") - objects = XMLManager() - - class Meta: # pylint: disable=C1001,W0232 - unique_together = ("tenant", "bsc_handle") - - xml_template = XMLTemplate( - name = "bsc", - elements = ("signing_cert", "signing_cert_crl"), - readonly = ("pkcs10_request",)) - - - def xml_pre_save_hook(self, q_pdu): - # Handle key generation, only supports RSA with SHA-256 for now. - if q_pdu.get("generate_keypair"): - assert q_pdu.get("key_type") in (None, "rsa") and q_pdu.get("hash_alg") in (None, "sha256") - self.private_key_id = rpki.x509.RSA.generate(keylength = int(q_pdu.get("key_length", 2048))) - self.pkcs10_request = rpki.x509.PKCS10.create(keypair = self.private_key_id) - - -@xml_hooks -class Repository(models.Model): - repository_handle = models.SlugField(max_length = 255) - peer_contact_uri = models.TextField(null = True) - rrdp_notification_uri = models.TextField(null = True) - bpki_cert = CertificateField(null = True) - bpki_glue = CertificateField(null = True) - last_cms_timestamp = SundialField(null = True) - bsc = models.ForeignKey(BSC, related_name = "repositories") - tenant = models.ForeignKey(Tenant, related_name = "repositories") - objects = XMLManager() - - class Meta: # pylint: disable=C1001,W0232 - unique_together = ("tenant", "repository_handle") + tenant_handle = models.SlugField(max_length = 255) + use_hsm = models.BooleanField(default = False) + crl_interval = models.BigIntegerField(null = True) + regen_margin = models.BigIntegerField(null = True) + bpki_cert = CertificateField(null = True) + bpki_glue = CertificateField(null = True) + objects = XMLManager() + + xml_template = XMLTemplate( + name = "tenant", + attributes = ("crl_interval", "regen_margin"), + booleans = ("use_hsm",), + elements = ("bpki_cert", "bpki_glue")) + + @tornado.gen.coroutine + def xml_pre_delete_hook(self, rpkid): + yield [parent.destroy() for parent in self.parents.all()] + + @tornado.gen.coroutine + def xml_post_save_hook(self, rpkid, q_pdu): + rekey = q_pdu.get("rekey") + revoke = q_pdu.get("revoke") + reissue = q_pdu.get("reissue") + revoke_forgotten = q_pdu.get("revoke_forgotten") + + if q_pdu.get("clear_replay_protection"): + for parent in self.parents.all(): + parent.clear_replay_protection() + for child in self.children.all(): + child.clear_replay_protection() + for repository in self.repositories.all(): + repository.clear_replay_protection() + + futures = [] + + if rekey or revoke or reissue or revoke_forgotten: + for parent in self.parents.all(): + if rekey: + futures.append(parent.serve_rekey(rpkid)) + if revoke: + futures.append(parent.serve_revoke(rpkid)) + if reissue: + futures.append(parent.serve_reissue(rpkid)) + if revoke_forgotten: + futures.append(parent.serve_revoke_forgotten(rpkid)) + + if q_pdu.get("publish_world_now"): + futures.append(self.serve_publish_world_now(rpkid)) + if q_pdu.get("run_now"): + futures.append(self.serve_run_now(rpkid)) + + yield futures + + + @tornado.gen.coroutine + def serve_publish_world_now(self, rpkid): + publisher = rpki.rpkid.publication_queue(rpkid) + repositories = set() + objects = dict() + + for parent in self.parents.all(): + + repository = parent.repository + if repository.peer_contact_uri in repositories: + continue + repositories.add(repository.peer_contact_uri) + q_msg = Element(rpki.publication.tag_msg, nsmap = rpki.publication.nsmap, + type = "query", version = rpki.publication.version) + SubElement(q_msg, rpki.publication.tag_list, tag = "list") + + r_msg = yield repository.call_pubd(rpkid, q_msg, length_check = False) + + for r_pdu in r_msg: + assert r_pdu.tag == rpki.publication.tag_list + if r_pdu.get("uri") in objects: + logger.warning("pubd reported multiple published copies of URI %r, this makes no sense, blundering onwards", r_pdu.get("uri")) + else: + objects[r_pdu.get("uri")] = (r_pdu.get("hash"), repository) + + def reconcile(uri, obj, repository): + h, r = objects.pop(uri, (None, None)) + if h is not None: + assert r == repository + publisher.queue(uri = uri, new_obj = obj, old_hash = h, repository = repository) + + for ca_detail in CADetail.objects.filter(ca__parent__tenant = self, state = "active"): + repository = ca_detail.ca.parent.repository + reconcile(uri = ca_detail.crl_uri, obj = ca_detail.latest_crl, repository = repository) + reconcile(uri = ca_detail.manifest_uri, obj = ca_detail.latest_manifest, repository = repository) + for c in ca_detail.child_certs.all(): + reconcile(uri = c.uri, obj = c.cert, repository = repository) + for r in ca_detail.roas.filter(roa__isnull = False): + reconcile(uri = r.uri, obj = r.roa, repository = repository) + for g in ca_detail.ghostbusters.all(): + reconcile(uri = g.uri, obj = g.ghostbuster, repository = repository) + for c in ca_detail.ee_certificates.all(): + reconcile(uri = c.uri, obj = c.cert, repository = repository) + for u in objects: + h, r = objects[u] + publisher.queue(uri = u, old_hash = h, repository = r) - xml_template = XMLTemplate( - name = "repository", - handles = (BSC,), - attributes = ("peer_contact_uri", "rrdp_notification_uri"), - elements = ("bpki_cert", "bpki_glue")) + yield publisher.call_pubd() - @tornado.gen.coroutine - def xml_post_save_hook(self, rpkid, q_pdu): - if q_pdu.get("clear_replay_protection"): - self.clear_replay_protection() + @tornado.gen.coroutine + def serve_run_now(self, rpkid): + logger.debug("Forced immediate run of periodic actions for tenant %s[%r]", self.tenant_handle, self) + tasks = self.cron_tasks(rpkid) + rpkid.task_add(tasks) + futures = [task.wait() for task in tasks] + rpkid.task_run() + yield futures - def clear_replay_protection(self): - self.last_cms_timestamp = None - self.save() + def cron_tasks(self, rpkid): + try: + return self._cron_tasks + except AttributeError: + self._cron_tasks = tuple(task(rpkid, self) for task in rpki.rpkid_tasks.task_classes) + return self._cron_tasks - @tornado.gen.coroutine - def call_pubd(self, rpkid, q_msg, handlers = {}, length_check = True): # pylint: disable=W0102 - """ - Send a message to publication daemon and return the response. + def find_covering_ca_details(self, resources): + """ + Return all active CADetails for this <tenant/> which cover a + particular set of resources. - As a convenience, attempting to send an empty message returns - immediate success without sending anything. + 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 CADetails per <tenant/>, it's probably not worth it. In + any case, this is an optimization we can leave for later. + """ - handlers is a dict of handler functions to process the response - PDUs. If the tag value in the response PDU appears in the dict, - the associated handler is called to process the PDU. If no tag - matches, a default handler is called to check for errors; a - handler value of False suppresses calling of the default handler. - """ + return set(ca_detail + for ca_detail in CADetail.objects.filter(ca__parent__tenant = self, state = "active") + if ca_detail.covers(resources)) - if len(q_msg) == 0: - return - for q_pdu in q_msg: - logger.info("Sending %r to pubd", q_pdu) +@xml_hooks +class BSC(models.Model): + bsc_handle = models.SlugField(max_length = 255) + private_key_id = RSAPrivateKeyField() + pkcs10_request = PKCS10Field() + hash_alg = EnumField(choices = ("sha256",), default = "sha256") + signing_cert = CertificateField(null = True) + signing_cert_crl = CRLField(null = True) + tenant = models.ForeignKey(Tenant, related_name = "bscs") + objects = XMLManager() - q_der = rpki.publication.cms_msg().wrap(q_msg, self.bsc.private_key_id, self.bsc.signing_cert, self.bsc.signing_cert_crl) + class Meta: # pylint: disable=C1001,W0232 + unique_together = ("tenant", "bsc_handle") - http_request = tornado.httpclient.HTTPRequest( - url = self.peer_contact_uri, - method = "POST", - body = q_der, - headers = { "Content-Type" : rpki.publication.content_type }) + xml_template = XMLTemplate( + name = "bsc", + elements = ("signing_cert", "signing_cert_crl"), + readonly = ("pkcs10_request",)) - http_response = yield rpkid.http_fetch(http_request) - # Tornado already checked http_response.code for us + def xml_pre_save_hook(self, q_pdu): + # Handle key generation, only supports RSA with SHA-256 for now. + if q_pdu.get("generate_keypair"): + assert q_pdu.get("key_type") in (None, "rsa") and q_pdu.get("hash_alg") in (None, "sha256") + self.private_key_id = rpki.x509.RSA.generate(keylength = int(q_pdu.get("key_length", 2048))) + self.pkcs10_request = rpki.x509.PKCS10.create(keypair = self.private_key_id) - content_type = http_response.headers.get("Content-Type") - if content_type not in rpki.publication.allowed_content_types: - raise rpki.exceptions.BadContentType("HTTP Content-Type %r, expected %r" % (rpki.publication.content_type, content_type)) +@xml_hooks +class Repository(models.Model): + repository_handle = models.SlugField(max_length = 255) + peer_contact_uri = models.TextField(null = True) + rrdp_notification_uri = models.TextField(null = True) + bpki_cert = CertificateField(null = True) + bpki_glue = CertificateField(null = True) + last_cms_timestamp = SundialField(null = True) + bsc = models.ForeignKey(BSC, related_name = "repositories") + tenant = models.ForeignKey(Tenant, related_name = "repositories") + objects = XMLManager() - r_der = http_response.body - r_cms = rpki.publication.cms_msg(DER = r_der) - r_msg = r_cms.unwrap((rpkid.bpki_ta, self.tenant.bpki_cert, self.tenant.bpki_glue, self.bpki_cert, self.bpki_glue)) - r_cms.check_replay_sql(self, self.peer_contact_uri) + class Meta: # pylint: disable=C1001,W0232 + unique_together = ("tenant", "repository_handle") - for r_pdu in r_msg: - handler = handlers.get(r_pdu.get("tag"), rpki.publication.raise_if_error) - if handler: - logger.debug("Calling pubd handler %r", handler) - handler(r_pdu) + xml_template = XMLTemplate( + name = "repository", + handles = (BSC,), + attributes = ("peer_contact_uri", "rrdp_notification_uri"), + elements = ("bpki_cert", "bpki_glue")) - if length_check and len(q_msg) != len(r_msg): - raise rpki.exceptions.BadPublicationReply("Wrong number of response PDUs from pubd: sent %r, got %r" % (q_msg, r_msg)) - raise tornado.gen.Return(r_msg) + @tornado.gen.coroutine + def xml_post_save_hook(self, rpkid, q_pdu): + if q_pdu.get("clear_replay_protection"): + self.clear_replay_protection() -@xml_hooks -class Parent(models.Model): - parent_handle = models.SlugField(max_length = 255) - bpki_cert = CertificateField(null = True) - bpki_glue = CertificateField(null = True) - peer_contact_uri = models.TextField(null = True) - sia_base = models.TextField(null = True) - sender_name = models.TextField(null = True) - recipient_name = models.TextField(null = True) - last_cms_timestamp = SundialField(null = True) - tenant = models.ForeignKey(Tenant, related_name = "parents") - bsc = models.ForeignKey(BSC, related_name = "parents") - repository = models.ForeignKey(Repository, related_name = "parents") - objects = XMLManager() - - class Meta: # pylint: disable=C1001,W0232 - unique_together = ("tenant", "parent_handle") - - xml_template = XMLTemplate( - name = "parent", - handles = (BSC, Repository), - attributes = ("peer_contact_uri", "sia_base", "sender_name", "recipient_name"), - elements = ("bpki_cert", "bpki_glue")) - - - @tornado.gen.coroutine - def xml_pre_delete_hook(self, rpkid): - yield self.destroy(rpkid, delete_parent = False) - - @tornado.gen.coroutine - def xml_post_save_hook(self, rpkid, q_pdu): - if q_pdu.get("clear_replay_protection"): - self.clear_replay_protection() - futures = [] - if q_pdu.get("rekey"): - futures.append(self.serve_rekey(rpkid)) - if q_pdu.get("revoke"): - futures.append(self.serve_revoke(rpkid)) - if q_pdu.get("reissue"): - futures.append(self.serve_reissue(rpkid)) - if q_pdu.get("revoke_forgotten"): - futures.append(self.serve_revoke_forgotten(rpkid)) - yield futures - - @tornado.gen.coroutine - def serve_rekey(self, rpkid): - yield [ca.rekey() for ca in self.cas.all()] - - @tornado.gen.coroutine - def serve_revoke(self, rpkid): - yield [ca.revoke() for ca in self.cas.all()] - - @tornado.gen.coroutine - def serve_reissue(self, rpkid): - yield [ca.reissue() for ca in self.cas.all()] - - def clear_replay_protection(self): - self.last_cms_timestamp = None - self.save() - - - @tornado.gen.coroutine - def get_skis(self, rpkid): - """ - Fetch SKIs that this parent thinks we have. In theory this should - agree with our own database, but in practice stuff can happen, so - sometimes we need to know what our parent thinks. + def clear_replay_protection(self): + self.last_cms_timestamp = None + self.save() - Result is a dictionary with the resource class name as key and a - set of SKIs as value. - This, like everything else dealing with SKIs in the up-down - protocol, is mis-named: we're really dealing with g(SKI) values, - not raw SKI values. Sorry. - """ + @tornado.gen.coroutine + def call_pubd(self, rpkid, q_msg, handlers = {}, length_check = True): # pylint: disable=W0102 + """ + Send a message to publication daemon and return the response. - r_msg = yield self.up_down_list_query(rpkid = rpkid) + As a convenience, attempting to send an empty message returns + immediate success without sending anything. - ski_map = {} + handlers is a dict of handler functions to process the response + PDUs. If the tag value in the response PDU appears in the dict, + the associated handler is called to process the PDU. If no tag + matches, a default handler is called to check for errors; a + handler value of False suppresses calling of the default handler. + """ - for rc in r_msg.getiterator(rpki.up_down.tag_class): - skis = set() - for c in rc.getiterator(rpki.up_down.tag_certificate): - skis.add(rpki.x509.X509(Base64 = c.text).gSKI()) - ski_map[rc.get("class_name")] = skis + if len(q_msg) == 0: + return - raise tornado.gen.Return(ski_map) + for q_pdu in q_msg: + logger.info("Sending %r to pubd", q_pdu) + q_der = rpki.publication.cms_msg().wrap(q_msg, self.bsc.private_key_id, self.bsc.signing_cert, self.bsc.signing_cert_crl) - @tornado.gen.coroutine - def revoke_skis(self, rpkid, rc_name, skis_to_revoke): - """ - Revoke a set of SKIs within a particular resource class. - """ + http_request = tornado.httpclient.HTTPRequest( + url = self.peer_contact_uri, + method = "POST", + body = q_der, + headers = { "Content-Type" : rpki.publication.content_type }) - for ski in skis_to_revoke: - logger.debug("Asking parent %r to revoke class %r, g(SKI) %s", self, rc_name, ski) - yield self.up_down_revoke_query(rpkid = rpkid, class_name = rc_name, ski = ski) + http_response = yield rpkid.http_fetch(http_request) + # Tornado already checked http_response.code for us - @tornado.gen.coroutine - def serve_revoke_forgotten(self, rpkid): - """ - Handle a left-right revoke_forgotten action for this parent. - - This is a bit fiddly: we have to compare the result of an up-down - list query with what we have locally and identify the SKIs of any - certificates that have gone missing. This should never happen in - ordinary operation, but can arise if we have somehow lost a - private key, in which case there is nothing more we can do with - the issued cert, so we have to clear it. As this really is not - supposed to happen, we don't clear it automatically, instead we - require an explicit trigger. - """ + content_type = http_response.headers.get("Content-Type") - skis_from_parent = yield self.get_skis(rpkid) - for rc_name, skis_to_revoke in skis_from_parent.iteritems(): - for ca_detail in CADetail.objects.filter(ca__parent = self).exclude(state = "revoked"): - skis_to_revoke.discard(ca_detail.latest_ca_cert.gSKI()) - yield self.revoke_skis(rpkid, rc_name, skis_to_revoke) + if content_type not in rpki.publication.allowed_content_types: + raise rpki.exceptions.BadContentType("HTTP Content-Type %r, expected %r" % (rpki.publication.content_type, content_type)) + r_der = http_response.body + r_cms = rpki.publication.cms_msg(DER = r_der) + r_msg = r_cms.unwrap((rpkid.bpki_ta, self.tenant.bpki_cert, self.tenant.bpki_glue, self.bpki_cert, self.bpki_glue)) + r_cms.check_replay_sql(self, self.peer_contact_uri) - @tornado.gen.coroutine - def destroy(self, rpkid, delete_parent = True): - """ - Delete all the CA stuff under this parent, and perhaps the parent - itself. - """ + for r_pdu in r_msg: + handler = handlers.get(r_pdu.get("tag"), rpki.publication.raise_if_error) + if handler: + logger.debug("Calling pubd handler %r", handler) + handler(r_pdu) - yield [ca.destroy(self) for ca in self.cas()] - yield self.serve_revoke_forgotten(rpkid) - if delete_parent: - self.delete() + if length_check and len(q_msg) != len(r_msg): + raise rpki.exceptions.BadPublicationReply("Wrong number of response PDUs from pubd: sent %r, got %r" % (q_msg, r_msg)) + raise tornado.gen.Return(r_msg) - def _compose_up_down_query(self, query_type): - return Element(rpki.up_down.tag_message, nsmap = rpki.up_down.nsmap, version = rpki.up_down.version, - sender = self.sender_name, recipient = self.recipient_name, type = query_type) + +@xml_hooks +class Parent(models.Model): + parent_handle = models.SlugField(max_length = 255) + bpki_cert = CertificateField(null = True) + bpki_glue = CertificateField(null = True) + peer_contact_uri = models.TextField(null = True) + sia_base = models.TextField(null = True) + sender_name = models.TextField(null = True) + recipient_name = models.TextField(null = True) + last_cms_timestamp = SundialField(null = True) + tenant = models.ForeignKey(Tenant, related_name = "parents") + bsc = models.ForeignKey(BSC, related_name = "parents") + repository = models.ForeignKey(Repository, related_name = "parents") + objects = XMLManager() + + class Meta: # pylint: disable=C1001,W0232 + unique_together = ("tenant", "parent_handle") + + xml_template = XMLTemplate( + name = "parent", + handles = (BSC, Repository), + attributes = ("peer_contact_uri", "sia_base", "sender_name", "recipient_name"), + elements = ("bpki_cert", "bpki_glue")) + + + @tornado.gen.coroutine + def xml_pre_delete_hook(self, rpkid): + yield self.destroy(rpkid, delete_parent = False) + + @tornado.gen.coroutine + def xml_post_save_hook(self, rpkid, q_pdu): + if q_pdu.get("clear_replay_protection"): + self.clear_replay_protection() + futures = [] + if q_pdu.get("rekey"): + futures.append(self.serve_rekey(rpkid)) + if q_pdu.get("revoke"): + futures.append(self.serve_revoke(rpkid)) + if q_pdu.get("reissue"): + futures.append(self.serve_reissue(rpkid)) + if q_pdu.get("revoke_forgotten"): + futures.append(self.serve_revoke_forgotten(rpkid)) + yield futures + + @tornado.gen.coroutine + def serve_rekey(self, rpkid): + yield [ca.rekey() for ca in self.cas.all()] + + @tornado.gen.coroutine + def serve_revoke(self, rpkid): + yield [ca.revoke() for ca in self.cas.all()] + + @tornado.gen.coroutine + def serve_reissue(self, rpkid): + yield [ca.reissue() for ca in self.cas.all()] + + def clear_replay_protection(self): + self.last_cms_timestamp = None + self.save() + + + @tornado.gen.coroutine + def get_skis(self, rpkid): + """ + Fetch SKIs that this parent thinks we have. In theory this should + agree with our own database, but in practice stuff can happen, so + sometimes we need to know what our parent thinks. + + Result is a dictionary with the resource class name as key and a + set of SKIs as value. + + This, like everything else dealing with SKIs in the up-down + protocol, is mis-named: we're really dealing with g(SKI) values, + not raw SKI values. Sorry. + """ + + r_msg = yield self.up_down_list_query(rpkid = rpkid) + + ski_map = {} + + for rc in r_msg.getiterator(rpki.up_down.tag_class): + skis = set() + for c in rc.getiterator(rpki.up_down.tag_certificate): + skis.add(rpki.x509.X509(Base64 = c.text).gSKI()) + ski_map[rc.get("class_name")] = skis + + raise tornado.gen.Return(ski_map) + + + @tornado.gen.coroutine + def revoke_skis(self, rpkid, rc_name, skis_to_revoke): + """ + Revoke a set of SKIs within a particular resource class. + """ + + for ski in skis_to_revoke: + logger.debug("Asking parent %r to revoke class %r, g(SKI) %s", self, rc_name, ski) + yield self.up_down_revoke_query(rpkid = rpkid, class_name = rc_name, ski = ski) + + + @tornado.gen.coroutine + def serve_revoke_forgotten(self, rpkid): + """ + Handle a left-right revoke_forgotten action for this parent. + + This is a bit fiddly: we have to compare the result of an up-down + list query with what we have locally and identify the SKIs of any + certificates that have gone missing. This should never happen in + ordinary operation, but can arise if we have somehow lost a + private key, in which case there is nothing more we can do with + the issued cert, so we have to clear it. As this really is not + supposed to happen, we don't clear it automatically, instead we + require an explicit trigger. + """ + + skis_from_parent = yield self.get_skis(rpkid) + for rc_name, skis_to_revoke in skis_from_parent.iteritems(): + for ca_detail in CADetail.objects.filter(ca__parent = self).exclude(state = "revoked"): + skis_to_revoke.discard(ca_detail.latest_ca_cert.gSKI()) + yield self.revoke_skis(rpkid, rc_name, skis_to_revoke) + + + @tornado.gen.coroutine + def destroy(self, rpkid, delete_parent = True): + """ + Delete all the CA stuff under this parent, and perhaps the parent + itself. + """ + + yield [ca.destroy(self) for ca in self.cas()] + yield self.serve_revoke_forgotten(rpkid) + if delete_parent: + self.delete() - @tornado.gen.coroutine - def up_down_list_query(self, rpkid): - q_msg = self._compose_up_down_query("list") - r_msg = yield self.query_up_down(rpkid, q_msg) - raise tornado.gen.Return(r_msg) + def _compose_up_down_query(self, query_type): + return Element(rpki.up_down.tag_message, nsmap = rpki.up_down.nsmap, version = rpki.up_down.version, + sender = self.sender_name, recipient = self.recipient_name, type = query_type) - @tornado.gen.coroutine - def up_down_issue_query(self, rpkid, ca, ca_detail): - logger.debug("Parent.up_down_issue_query(): caRepository %r rpkiManifest %r rpkiNotify %r", - ca.sia_uri, ca_detail.manifest_uri, ca.parent.repository.rrdp_notification_uri) - pkcs10 = rpki.x509.PKCS10.create( - keypair = ca_detail.private_key_id, - is_ca = True, - caRepository = ca.sia_uri, - rpkiManifest = ca_detail.manifest_uri, - rpkiNotify = ca.parent.repository.rrdp_notification_uri) - q_msg = self._compose_up_down_query("issue") - q_pdu = SubElement(q_msg, rpki.up_down.tag_request, class_name = ca.parent_resource_class) - q_pdu.text = pkcs10.get_Base64() - r_msg = yield self.query_up_down(rpkid, q_msg) - raise tornado.gen.Return(r_msg) + @tornado.gen.coroutine + def up_down_list_query(self, rpkid): + q_msg = self._compose_up_down_query("list") + r_msg = yield self.query_up_down(rpkid, q_msg) + raise tornado.gen.Return(r_msg) - @tornado.gen.coroutine - def up_down_revoke_query(self, rpkid, class_name, ski): - q_msg = self._compose_up_down_query("revoke") - SubElement(q_msg, rpki.up_down.tag_key, class_name = class_name, ski = ski) - r_msg = yield self.query_up_down(rpkid, q_msg) - raise tornado.gen.Return(r_msg) + @tornado.gen.coroutine + def up_down_issue_query(self, rpkid, ca, ca_detail): + logger.debug("Parent.up_down_issue_query(): caRepository %r rpkiManifest %r rpkiNotify %r", + ca.sia_uri, ca_detail.manifest_uri, ca.parent.repository.rrdp_notification_uri) + pkcs10 = rpki.x509.PKCS10.create( + keypair = ca_detail.private_key_id, + is_ca = True, + caRepository = ca.sia_uri, + rpkiManifest = ca_detail.manifest_uri, + rpkiNotify = ca.parent.repository.rrdp_notification_uri) + q_msg = self._compose_up_down_query("issue") + q_pdu = SubElement(q_msg, rpki.up_down.tag_request, class_name = ca.parent_resource_class) + q_pdu.text = pkcs10.get_Base64() + r_msg = yield self.query_up_down(rpkid, q_msg) + raise tornado.gen.Return(r_msg) - @tornado.gen.coroutine - def query_up_down(self, rpkid, q_msg): + @tornado.gen.coroutine + def up_down_revoke_query(self, rpkid, class_name, ski): + q_msg = self._compose_up_down_query("revoke") + SubElement(q_msg, rpki.up_down.tag_key, class_name = class_name, ski = ski) + r_msg = yield self.query_up_down(rpkid, q_msg) + raise tornado.gen.Return(r_msg) - if self.bsc is None: - raise rpki.exceptions.BSCNotFound("Could not find BSC") - if self.bsc.signing_cert is None: - raise rpki.exceptions.BSCNotReady("BSC %r is not yet usable" % self.bsc.bsc_handle) + @tornado.gen.coroutine + def query_up_down(self, rpkid, q_msg): - q_der = rpki.up_down.cms_msg().wrap(q_msg, self.bsc.private_key_id, self.bsc.signing_cert, self.bsc.signing_cert_crl) + if self.bsc is None: + raise rpki.exceptions.BSCNotFound("Could not find BSC") - http_request = tornado.httpclient.HTTPRequest( - url = self.peer_contact_uri, - method = "POST", - body = q_der, - headers = { "Content-Type" : rpki.up_down.content_type }) + if self.bsc.signing_cert is None: + raise rpki.exceptions.BSCNotReady("BSC %r is not yet usable" % self.bsc.bsc_handle) - http_response = yield rpkid.http_fetch(http_request) + q_der = rpki.up_down.cms_msg().wrap(q_msg, self.bsc.private_key_id, self.bsc.signing_cert, self.bsc.signing_cert_crl) - # Tornado already checked http_response.code for us + http_request = tornado.httpclient.HTTPRequest( + url = self.peer_contact_uri, + method = "POST", + body = q_der, + headers = { "Content-Type" : rpki.up_down.content_type }) - content_type = http_response.headers.get("Content-Type") + http_response = yield rpkid.http_fetch(http_request) + + # Tornado already checked http_response.code for us - if content_type not in rpki.up_down.allowed_content_types: - raise rpki.exceptions.BadContentType("HTTP Content-Type %r, expected %r" % (rpki.up_down.content_type, content_type)) + content_type = http_response.headers.get("Content-Type") - r_der = http_response.body - r_cms = rpki.up_down.cms_msg(DER = r_der) - r_msg = r_cms.unwrap((rpkid.bpki_ta, self.tenant.bpki_cert, self.tenant.bpki_glue, self.bpki_cert, self.bpki_glue)) - r_cms.check_replay_sql(self, self.peer_contact_uri) - rpki.up_down.check_response(r_msg, q_msg.get("type")) + if content_type not in rpki.up_down.allowed_content_types: + raise rpki.exceptions.BadContentType("HTTP Content-Type %r, expected %r" % (rpki.up_down.content_type, content_type)) - raise tornado.gen.Return(r_msg) + r_der = http_response.body + r_cms = rpki.up_down.cms_msg(DER = r_der) + r_msg = r_cms.unwrap((rpkid.bpki_ta, self.tenant.bpki_cert, self.tenant.bpki_glue, self.bpki_cert, self.bpki_glue)) + r_cms.check_replay_sql(self, self.peer_contact_uri) + rpki.up_down.check_response(r_msg, q_msg.get("type")) + raise tornado.gen.Return(r_msg) - 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 + 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(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(default = 1) - sia_uri = 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 - # class_name we receive from our parent and the class_name we issue - # to our children: in spite of the obfuscated way that we used to - # handle class names, we never actually added a way for the back-end - # to create new classes. Not clear we want to encourage this, but - # if we wanted to support it, simple approach would probably be an - # optional class_name attribute in the left-right <list_resources/> - # 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") - - - @tornado.gen.coroutine - def check_for_updates(self, rpkid, parent, rc): - """ - 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. - """ - - logger.debug("check_for_updates()") - 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 + 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(default = 1) + sia_uri = 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 + # class_name we receive from our parent and the class_name we issue + # to our children: in spite of the obfuscated way that we used to + # handle class names, we never actually added a way for the back-end + # to create new classes. Not clear we want to encourage this, but + # if we wanted to support it, simple approach would probably be an + # optional class_name attribute in the left-right <list_resources/> + # 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") + + + @tornado.gen.coroutine + def check_for_updates(self, rpkid, parent, rc): + """ + 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. + """ + + logger.debug("check_for_updates()") + 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 + + 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) + + ca_details = self.ca_details.exclude(state = "revoked") + + if not ca_details: + logger.warning("Existing resource class %s to %s from %s with no certificates, rekeying", + class_name, parent.tenant.tenant_handle, parent.parent_handle) + yield self.rekey(rpkid) + return + + for ca_detail in ca_details: + + rc_cert, rc_cert_uri = cert_map.pop(ca_detail.public_key.gSKI(), (None, None)) + + if rc_cert is None: + logger.warning("g(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.tenant.tenant_handle, parent.parent_handle) + publisher = rpki.rpkid.publication_queue(rpkid) + ca_detail.destroy(ca = ca_detail.ca, publisher = publisher) + yield publisher.call_pubd() + continue + + 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"): + continue + + 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)): + + yield ca_detail.update( + rpkid = rpkid, + parent = parent, + ca = self, + rc = rc, + sia_uri_changed = sia_uri_changed, + old_resources = current_resources) + + if cert_map: + logger.warning("Unknown certificate g(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.tenant.tenant_handle, parent.parent_handle) + + + # Called from exactly one place, in rpki.rpkid_tasks.PollParentTask.class_loop(). + # Might want to refactor. + + @classmethod + @tornado.gen.coroutine + def create(cls, rpkid, parent, rc): + """ + Parent has signaled existance of a new resource class, so we need + to create and set up a corresponding CA object. + """ - class_name = rc.get("class_name") + 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) + + logger.debug("Sending issue request to %r from %r", parent, self.create) - 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")) + r_msg = yield parent.up_down_issue_query(rpkid = rpkid, ca = self, ca_detail = ca_detail) - cert_map = {} + c = r_msg[0][0] - 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) + logger.debug("CA %r received certificate %s", self, c.get("cert_url")) - ca_details = self.ca_details.exclude(state = "revoked") + yield ca_detail.activate( + rpkid = rpkid, + ca = self, + cert = rpki.x509.X509(Base64 = c.text), + uri = c.get("cert_url")) - if not ca_details: - logger.warning("Existing resource class %s to %s from %s with no certificates, rekeying", - class_name, parent.tenant.tenant_handle, parent.parent_handle) - yield self.rekey(rpkid) - return - for ca_detail in ca_details: + @tornado.gen.coroutine + def destroy(self, rpkid, parent): + """ + 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...). - rc_cert, rc_cert_uri = cert_map.pop(ca_detail.public_key.gSKI(), (None, None)) + 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. + """ - if rc_cert is None: - logger.warning("g(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.tenant.tenant_handle, parent.parent_handle) publisher = rpki.rpkid.publication_queue(rpkid) - ca_detail.destroy(ca = ca_detail.ca, publisher = publisher) - yield publisher.call_pubd() - continue - - 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"): - continue - - 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)): - - yield ca_detail.update( - rpkid = rpkid, - parent = parent, - ca = self, - rc = rc, - sia_uri_changed = sia_uri_changed, - old_resources = current_resources) - - if cert_map: - logger.warning("Unknown certificate g(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.tenant.tenant_handle, parent.parent_handle) - - - # Called from exactly one place, in rpki.rpkid_tasks.PollParentTask.class_loop(). - # Might want to refactor. - - @classmethod - @tornado.gen.coroutine - def create(cls, rpkid, parent, rc): - """ - 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)) + for ca_detail in self.ca_details.all(): + ca_detail.destroy(ca = self, publisher = publisher, allow_failure = True) - ca_detail = CADetail.create(self) + try: + yield publisher.call_pubd() - logger.debug("Sending issue request to %r from %r", parent, self.create) + except: + logger.exception("Could not delete CA %r, skipping", self) - r_msg = yield parent.up_down_issue_query(rpkid = rpkid, ca = self, ca_detail = ca_detail) + else: + logger.debug("Deleting %r", self) + self.delete() - c = r_msg[0][0] - logger.debug("CA %r received certificate %s", self, c.get("cert_url")) + def next_serial_number(self): + """ + Allocate a certificate serial number. + """ - yield ca_detail.activate( - rpkid = rpkid, - ca = self, - cert = rpki.x509.X509(Base64 = c.text), - uri = c.get("cert_url")) + self.last_issued_sn += 1 + self.save() + return self.last_issued_sn - @tornado.gen.coroutine - def destroy(self, rpkid, parent): - """ - 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 next_manifest_number(self): + """ + Allocate a manifest serial number. + """ - publisher = rpki.rpkid.publication_queue(rpkid) + self.last_manifest_sn += 1 + self.save() + return self.last_manifest_sn - for ca_detail in self.ca_details.all(): - ca_detail.destroy(ca = self, publisher = publisher, allow_failure = True) - try: - yield publisher.call_pubd() + def next_crl_number(self): + """ + Allocate a CRL serial number. + """ - except: - logger.exception("Could not delete CA %r, skipping", self) + self.last_crl_sn += 1 + self.save() + return self.last_crl_sn - else: - logger.debug("Deleting %r", self) - self.delete() + @tornado.gen.coroutine + def rekey(self, rpkid): + """ + 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. + """ - def next_serial_number(self): - """ - Allocate a certificate serial number. - """ + try: + old_detail = self.ca_details.get(state = "active") + except CADetail.DoesNotExist: + old_detail = None - self.last_issued_sn += 1 - self.save() - return self.last_issued_sn + new_detail = CADetail.create(ca = self) # sic: class method, not manager function (for now, anyway) + logger.debug("Sending issue request to %r from %r", self.parent, self.rekey) - def next_manifest_number(self): - """ - Allocate a manifest serial number. - """ + r_msg = yield self.parent.up_down_issue_query(rpkid = rpkid, ca = self, ca_detail = new_detail) - self.last_manifest_sn += 1 - self.save() - return self.last_manifest_sn + c = r_msg[0][0] + logger.debug("CA %r received certificate %s", self, c.get("cert_url")) - def next_crl_number(self): - """ - Allocate a CRL serial number. - """ + yield new_detail.activate( + rpkid = rpkid, + ca = self, + cert = rpki.x509.X509(Base64 = c.text), + uri = c.get("cert_url"), + predecessor = old_detail) - self.last_crl_sn += 1 - self.save() - return self.last_crl_sn + @tornado.gen.coroutine + def revoke(self, 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. + """ - @tornado.gen.coroutine - def rekey(self, rpkid): - """ - 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. - """ + if revoke_all: + ca_details = self.ca_details.all() + else: + ca_details = self.ca_details.filter(state = "deprecated") - try: - old_detail = self.ca_details.get(state = "active") - except CADetail.DoesNotExist: - old_detail = None + yield [ca_detail.revoke() for ca_detail in ca_details] - new_detail = CADetail.create(ca = self) # sic: class method, not manager function (for now, anyway) - logger.debug("Sending issue request to %r from %r", self.parent, self.rekey) + @tornado.gen.coroutine + def reissue(self): + """ + Reissue all current certificates issued by this CA. + """ - r_msg = yield self.parent.up_down_issue_query(rpkid = rpkid, ca = self, ca_detail = new_detail) + ca_detail = self.ca_details.get(state = "active") + if ca_detail: + yield ca_detail.reissue() - c = r_msg[0][0] - logger.debug("CA %r received certificate %s", self, c.get("cert_url")) +class CADetail(models.Model): + public_key = PublicKeyField(null = True) + private_key_id = RSAPrivateKeyField(null = True) + latest_crl = CRLField(null = True) + crl_published = SundialField(null = True) + latest_ca_cert = CertificateField(null = True) + manifest_private_key_id = RSAPrivateKeyField(null = True) + manifest_public_key = PublicKeyField(null = True) + latest_manifest_cert = CertificateField(null = True) + latest_manifest = ManifestField(null = True) + manifest_published = SundialField(null = True) + state = EnumField(choices = ("pending", "active", "deprecated", "revoked")) + ca_cert_uri = models.TextField(null = True) + ca = models.ForeignKey(CA, related_name = "ca_details") - yield new_detail.activate( - rpkid = rpkid, - ca = self, - cert = rpki.x509.X509(Base64 = c.text), - uri = c.get("cert_url"), - predecessor = old_detail) + # 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. - @tornado.gen.coroutine - def revoke(self, 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. - """ - if revoke_all: - ca_details = self.ca_details.all() - else: - ca_details = self.ca_details.filter(state = "deprecated") + @property + def crl_uri(self): + """ + Return publication URI for this ca_detail's CRL. + """ - yield [ca_detail.revoke() for ca_detail in ca_details] + return self.ca.sia_uri + self.crl_uri_tail - @tornado.gen.coroutine - def reissue(self): - """ - Reissue all current certificates issued by this CA. - """ + @property + def crl_uri_tail(self): + """ + Return tail (filename portion) of publication URI for this ca_detail's CRL. + """ - ca_detail = self.ca_details.get(state = "active") - if ca_detail: - yield ca_detail.reissue() + return self.public_key.gSKI() + ".crl" -class CADetail(models.Model): - public_key = PublicKeyField(null = True) - private_key_id = RSAPrivateKeyField(null = True) - latest_crl = CRLField(null = True) - crl_published = SundialField(null = True) - latest_ca_cert = CertificateField(null = True) - manifest_private_key_id = RSAPrivateKeyField(null = True) - manifest_public_key = PublicKeyField(null = True) - latest_manifest_cert = CertificateField(null = True) - latest_manifest = ManifestField(null = True) - manifest_published = SundialField(null = True) - state = EnumField(choices = ("pending", "active", "deprecated", "revoked")) - 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. - """ + @property + def manifest_uri(self): + """ + Return publication URI for this ca_detail's manifest. + """ - return self.ca.sia_uri + self.crl_uri_tail + return self.ca.sia_uri + self.public_key.gSKI() + ".mft" - @property - def crl_uri_tail(self): - """ - Return tail (filename portion) of publication URI for this ca_detail's CRL. - """ + def has_expired(self): + """ + Return whether this ca_detail's certificate has expired. + """ - return self.public_key.gSKI() + ".crl" + return self.latest_ca_cert.getNotAfter() <= rpki.sundial.now() - @property - def manifest_uri(self): - """ - Return publication URI for this ca_detail's manifest. - """ + def covers(self, target): + """ + Test whether this ca-detail covers a given set of resources. + """ - return self.ca.sia_uri + self.public_key.gSKI() + ".mft" + 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 has_expired(self): - """ - Return whether this ca_detail's certificate has expired. - """ + @tornado.gen.coroutine + def activate(self, rpkid, ca, cert, uri, predecessor = None): + """ + Activate this ca_detail. + """ - return self.latest_ca_cert.getNotAfter() <= rpki.sundial.now() + publisher = rpki.rpkid.publication_queue(rpkid) + 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) + yield publisher.call_pubd() - 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 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. + """ - @tornado.gen.coroutine - def activate(self, rpkid, ca, cert, uri, predecessor = None): - """ - Activate this ca_detail. - """ + 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() - publisher = rpki.rpkid.publication_queue(rpkid) - 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) - - yield publisher.call_pubd() - - - 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. - """ + @tornado.gen.coroutine + def revoke(self, rpkid): + """ + Request revocation of all certificates whose g(SKI) matches the key + for this ca_detail. - 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() - - - @tornado.gen.coroutine - def revoke(self, rpkid): - """ - Request revocation of all certificates whose g(SKI) matches the key - for this ca_detail. + Tasks: - Tasks: + - Request revocation of old keypair by parent. - - Request revocation of old keypair by parent. + - Revoke all child certs issued by the old keypair. - - 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 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. - - Generate a corresponding final manifest. + - Destroy old keypairs. - - Destroy old keypairs. + - Leave final CRL and manifest in place until their nextupdate + time has passed. + """ - - Leave final CRL and manifest in place until their nextupdate - time has passed. - """ + gski = self.latest_ca_cert.gSKI() - gski = self.latest_ca_cert.gSKI() + logger.debug("Asking parent to revoke CA certificate matching g(SKI) = %s", gski) - logger.debug("Asking parent to revoke CA certificate matching g(SKI) = %s", gski) + r_msg = yield self.ca.parent.up_down_revoke_query(rpkid = rpkid, class_name = self.ca.parent_resource_class, ski = gski) - r_msg = yield self.ca.parent.up_down_revoke_query(rpkid = rpkid, class_name = self.ca.parent_resource_class, ski = gski) + if r_msg[0].get("class_name") != self.ca.parent_resource_class: + raise rpki.exceptions.ResourceClassMismatch - if r_msg[0].get("class_name") != self.ca.parent_resource_class: - raise rpki.exceptions.ResourceClassMismatch + if r_msg[0].get("ski") != gski: + raise rpki.exceptions.SKIMismatch - if r_msg[0].get("ski") != gski: - raise rpki.exceptions.SKIMismatch + logger.debug("Parent revoked g(SKI) %s, starting cleanup", gski) - logger.debug("Parent revoked g(SKI) %s, starting cleanup", gski) + crl_interval = rpki.sundial.timedelta(seconds = self.ca.parent.tenant.crl_interval) - crl_interval = rpki.sundial.timedelta(seconds = self.ca.parent.tenant.crl_interval) + nextUpdate = rpki.sundial.now() - 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_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()) - if self.latest_crl is not None: - nextUpdate = nextUpdate.later(self.latest_crl.getNextUpdate()) + publisher = rpki.rpkid.publication_queue(rpkid) - publisher = rpki.rpkid.publication_queue(rpkid) + for child_cert in self.child_certs.all(): + nextUpdate = nextUpdate.later(child_cert.cert.getNotAfter()) + child_cert.revoke(publisher = publisher) - 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 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) - for ghostbuster in self.ghostbusters.all(): - nextUpdate = nextUpdate.later(ghostbuster.cert.getNotAfter()) - ghostbuster.revoke(publisher = publisher) + nextUpdate += crl_interval - 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() - 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() + yield publisher.call_pubd() - yield publisher.call_pubd() + @tornado.gen.coroutine + def update(self, rpkid, parent, ca, rc, sia_uri_changed, old_resources): + """ + Need to get a new certificate for this ca_detail and perhaps frob + children of this ca_detail. + """ - @tornado.gen.coroutine - def update(self, rpkid, parent, ca, rc, sia_uri_changed, old_resources): - """ - Need to get a new certificate for this ca_detail and perhaps frob - children of this ca_detail. - """ + logger.debug("Sending issue request to %r from %r", parent, self.update) - logger.debug("Sending issue request to %r from %r", parent, self.update) + r_msg = yield parent.up_down_issue_query(rpkid = rpkid, ca = ca, ca_detail = self) - r_msg = yield parent.up_down_issue_query(rpkid = rpkid, ca = ca, ca_detail = self) + c = r_msg[0][0] - c = r_msg[0][0] + cert = rpki.x509.X509(Base64 = c.text) + cert_url = c.get("cert_url") - cert = rpki.x509.X509(Base64 = c.text) - cert_url = c.get("cert_url") + logger.debug("CA %r received certificate %s", self, cert_url) - logger.debug("CA %r received certificate %s", self, cert_url) + if self.state == "pending": + yield self.activate(rpkid = rpkid, ca = ca, cert = cert, uri = cert_url) + return - if self.state == "pending": - yield self.activate(rpkid = rpkid, ca = ca, cert = cert, uri = cert_url) - return + validity_changed = self.latest_ca_cert is None or self.latest_ca_cert.getNotAfter() != cert.getNotAfter() - validity_changed = self.latest_ca_cert is None or self.latest_ca_cert.getNotAfter() != cert.getNotAfter() + publisher = rpki.rpkid.publication_queue(rpkid) - publisher = rpki.rpkid.publication_queue(rpkid) + 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) - 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() - 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 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 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) - if sia_uri_changed or validity_changed: - for ghostbuster in self.ghostbusters.all(): - ghostbuster.update(publisher = publisher, fast = True) + yield publisher.call_pubd() - yield publisher.call_pubd() + @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, self.ca.parent.repository.rrdp_notification_uri)) + + + 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.gski = cert.gSKI() + 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.tenant.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.tenant.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() + + + @tornado.gen.coroutine + def reissue(self, rpkid): + """ + Reissue all current certificates issued by this ca_detail. + """ - @classmethod - def create(cls, ca): - """ - Create a new ca_detail object for a specified CA. - """ + publisher = rpki.rpkid.publication_queue(rpkid) + 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() + yield publisher.call_pubd() - 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 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_certificates.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) - 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. - """ +@xml_hooks +class Child(models.Model): + child_handle = models.SlugField(max_length = 255) + bpki_cert = CertificateField(null = True) + bpki_glue = CertificateField(null = True) + last_cms_timestamp = SundialField(null = True) + tenant = models.ForeignKey(Tenant, related_name = "children") + bsc = models.ForeignKey(BSC, related_name = "children") + objects = XMLManager() - 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, self.ca.parent.repository.rrdp_notification_uri)) + class Meta: # pylint: disable=C1001,W0232 + unique_together = ("tenant", "child_handle") + xml_template = XMLTemplate( + name = "child", + handles = (BSC,), + elements = ("bpki_cert", "bpki_glue")) - 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.gski = cert.gSKI() - 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. - """ + @tornado.gen.coroutine + def xml_pre_delete_hook(self, rpkid): + publisher = rpki.rpkid.publication_queue(rpkid) + for child_cert in self.child_certs.all(): + child_cert.revoke(publisher = publisher, generate_crl_and_manifest = True) + yield publisher.call_pubd() - self.check_failed_publication(publisher) - crl_interval = rpki.sundial.timedelta(seconds = self.ca.parent.tenant.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() + @tornado.gen.coroutine + def xml_post_save_hook(self, rpkid, q_pdu): + if q_pdu.get("clear_replay_protection"): + self.clear_replay_protection() + if q_pdu.get("reissue"): + yield self.serve_reissue(rpkid) - def generate_manifest(self, publisher, nextUpdate = None): - """ - Generate a new manifest for this ca_detail. - """ + def serve_reissue(self, rpkid): + publisher = rpki.rpkid.publication_queue(rpkid) + for child_cert in self.child_certs.all(): + child_cert.reissue(child_cert.ca_detail, publisher, force = True) + yield publisher.call_pubd() - self.check_failed_publication(publisher) - - crl_interval = rpki.sundial.timedelta(seconds = self.ca.parent.tenant.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 clear_replay_protection(self): + self.last_cms_timestamp = None + self.save() - @tornado.gen.coroutine - def reissue(self, rpkid): - """ - Reissue all current certificates issued by this ca_detail. - """ + @tornado.gen.coroutine + def up_down_handle_list(self, rpkid, q_msg, r_msg): - publisher = rpki.rpkid.publication_queue(rpkid) - 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() - yield publisher.call_pubd() - - - 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. - """ + irdb_resources = yield rpkid.irdb_query_child_resources(self.tenant.tenant_handle, self.child_handle) - 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_certificates.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) + if irdb_resources.valid_until < rpki.sundial.now(): + logger.debug("Child %s's resources expired %s", self.child_handle, irdb_resources.valid_until) + else: -@xml_hooks -class Child(models.Model): - child_handle = models.SlugField(max_length = 255) - bpki_cert = CertificateField(null = True) - bpki_glue = CertificateField(null = True) - last_cms_timestamp = SundialField(null = True) - tenant = models.ForeignKey(Tenant, related_name = "children") - bsc = models.ForeignKey(BSC, related_name = "children") - objects = XMLManager() + for ca_detail in CADetail.objects.filter(ca__parent__tenant = self.tenant, state = "active"): + resources = ca_detail.latest_ca_cert.get_3779resources() & irdb_resources - class Meta: # pylint: disable=C1001,W0232 - unique_together = ("tenant", "child_handle") + if resources.empty(): + logger.debug("No overlap between received resources and what child %s should get ([%s], [%s])", + self.child_handle, ca_detail.latest_ca_cert.get_3779resources(), irdb_resources) + continue - xml_template = XMLTemplate( - name = "child", - handles = (BSC,), - elements = ("bpki_cert", "bpki_glue")) + rc = SubElement(r_msg, rpki.up_down.tag_class, + class_name = ca_detail.ca.parent_resource_class, + cert_url = ca_detail.ca_cert_uri, + resource_set_as = str(resources.asn), + resource_set_ipv4 = str(resources.v4), + resource_set_ipv6 = str(resources.v6), + resource_set_notafter = str(resources.valid_until)) + for child_cert in self.child_certs.filter(ca_detail = ca_detail): + c = SubElement(rc, rpki.up_down.tag_certificate, cert_url = child_cert.uri) + c.text = child_cert.cert.get_Base64() + SubElement(rc, rpki.up_down.tag_issuer).text = ca_detail.latest_ca_cert.get_Base64() - @tornado.gen.coroutine - def xml_pre_delete_hook(self, rpkid): - publisher = rpki.rpkid.publication_queue(rpkid) - for child_cert in self.child_certs.all(): - child_cert.revoke(publisher = publisher, generate_crl_and_manifest = True) - yield publisher.call_pubd() + @tornado.gen.coroutine + def up_down_handle_issue(self, rpkid, q_msg, r_msg): - @tornado.gen.coroutine - def xml_post_save_hook(self, rpkid, q_pdu): - if q_pdu.get("clear_replay_protection"): - self.clear_replay_protection() - if q_pdu.get("reissue"): - yield self.serve_reissue(rpkid) + req = q_msg[0] + assert req.tag == rpki.up_down.tag_request + # Subsetting not yet implemented, this is the one place where we have to handle it, by reporting that we're lame. - def serve_reissue(self, rpkid): - publisher = rpki.rpkid.publication_queue(rpkid) - for child_cert in self.child_certs.all(): - child_cert.reissue(child_cert.ca_detail, publisher, force = True) - yield publisher.call_pubd() + if any(req.get(a) for a in ("req_resource_set_as", "req_resource_set_ipv4", "req_resource_set_ipv6")): + raise rpki.exceptions.NotImplementedYet("req_* attributes not implemented yet, sorry") + class_name = req.get("class_name") + pkcs10 = rpki.x509.PKCS10(Base64 = req.text) + pkcs10.check_valid_request_ca() + ca_detail = CADetail.objects.get(ca__parent__tenant = self.tenant, state = "active", + ca__parent_resource_class = class_name) - def clear_replay_protection(self): - self.last_cms_timestamp = None - self.save() + irdb_resources = yield rpkid.irdb_query_child_resources(self.tenant.tenant_handle, self.child_handle) + if irdb_resources.valid_until < rpki.sundial.now(): + raise rpki.exceptions.IRDBExpired("IRDB entry for child %s expired %s" % ( + self.child_handle, irdb_resources.valid_until)) - @tornado.gen.coroutine - def up_down_handle_list(self, rpkid, q_msg, r_msg): + resources = irdb_resources & ca_detail.latest_ca_cert.get_3779resources() + resources.valid_until = irdb_resources.valid_until + req_key = pkcs10.getPublicKey() + req_sia = pkcs10.get_SIA() - irdb_resources = yield rpkid.irdb_query_child_resources(self.tenant.tenant_handle, self.child_handle) + # Generate new cert or regenerate old one if necessary - if irdb_resources.valid_until < rpki.sundial.now(): - logger.debug("Child %s's resources expired %s", self.child_handle, irdb_resources.valid_until) + publisher = rpki.rpkid.publication_queue(rpkid) - else: + try: + child_cert = self.child_certs.get(ca_detail = ca_detail, gski = req_key.gSKI()) - for ca_detail in CADetail.objects.filter(ca__parent__tenant = self.tenant, state = "active"): - resources = ca_detail.latest_ca_cert.get_3779resources() & irdb_resources + except ChildCert.DoesNotExist: + child_cert = ca_detail.issue( + ca = ca_detail.ca, + child = self, + subject_key = req_key, + sia = req_sia, + resources = resources, + publisher = publisher) - if resources.empty(): - logger.debug("No overlap between received resources and what child %s should get ([%s], [%s])", - self.child_handle, ca_detail.latest_ca_cert.get_3779resources(), irdb_resources) - continue + else: + child_cert = child_cert.reissue( + ca_detail = ca_detail, + sia = req_sia, + resources = resources, + publisher = publisher) + + yield publisher.call_pubd() rc = SubElement(r_msg, rpki.up_down.tag_class, - class_name = ca_detail.ca.parent_resource_class, + class_name = class_name, cert_url = ca_detail.ca_cert_uri, resource_set_as = str(resources.asn), resource_set_ipv4 = str(resources.v4), resource_set_ipv6 = str(resources.v6), resource_set_notafter = str(resources.valid_until)) - - for child_cert in self.child_certs.filter(ca_detail = ca_detail): - c = SubElement(rc, rpki.up_down.tag_certificate, cert_url = child_cert.uri) - c.text = child_cert.cert.get_Base64() + c = SubElement(rc, rpki.up_down.tag_certificate, cert_url = child_cert.uri) + c.text = child_cert.cert.get_Base64() SubElement(rc, rpki.up_down.tag_issuer).text = ca_detail.latest_ca_cert.get_Base64() - @tornado.gen.coroutine - def up_down_handle_issue(self, rpkid, q_msg, r_msg): - - req = q_msg[0] - assert req.tag == rpki.up_down.tag_request - - # Subsetting not yet implemented, this is the one place where we have to handle it, by reporting that we're lame. - - if any(req.get(a) for a in ("req_resource_set_as", "req_resource_set_ipv4", "req_resource_set_ipv6")): - raise rpki.exceptions.NotImplementedYet("req_* attributes not implemented yet, sorry") - - class_name = req.get("class_name") - pkcs10 = rpki.x509.PKCS10(Base64 = req.text) - pkcs10.check_valid_request_ca() - ca_detail = CADetail.objects.get(ca__parent__tenant = self.tenant, state = "active", - ca__parent_resource_class = class_name) - - irdb_resources = yield rpkid.irdb_query_child_resources(self.tenant.tenant_handle, self.child_handle) - - if irdb_resources.valid_until < rpki.sundial.now(): - raise rpki.exceptions.IRDBExpired("IRDB entry for child %s expired %s" % ( - self.child_handle, irdb_resources.valid_until)) - - resources = irdb_resources & ca_detail.latest_ca_cert.get_3779resources() - resources.valid_until = irdb_resources.valid_until - req_key = pkcs10.getPublicKey() - req_sia = pkcs10.get_SIA() - - # Generate new cert or regenerate old one if necessary - - publisher = rpki.rpkid.publication_queue(rpkid) - - try: - child_cert = self.child_certs.get(ca_detail = ca_detail, gski = req_key.gSKI()) - - except ChildCert.DoesNotExist: - child_cert = ca_detail.issue( - ca = ca_detail.ca, - child = self, - subject_key = req_key, - sia = req_sia, - resources = resources, - publisher = publisher) - - else: - child_cert = child_cert.reissue( - ca_detail = ca_detail, - sia = req_sia, - resources = resources, - publisher = publisher) - - yield publisher.call_pubd() - - rc = SubElement(r_msg, rpki.up_down.tag_class, - class_name = class_name, - cert_url = ca_detail.ca_cert_uri, - resource_set_as = str(resources.asn), - resource_set_ipv4 = str(resources.v4), - resource_set_ipv6 = str(resources.v6), - resource_set_notafter = str(resources.valid_until)) - c = SubElement(rc, rpki.up_down.tag_certificate, cert_url = child_cert.uri) - c.text = child_cert.cert.get_Base64() - SubElement(rc, rpki.up_down.tag_issuer).text = ca_detail.latest_ca_cert.get_Base64() - + @tornado.gen.coroutine + def up_down_handle_revoke(self, rpkid, q_msg, r_msg): + key = q_msg[0] + assert key.tag == rpki.up_down.tag_key + class_name = key.get("class_name") + publisher = rpki.rpkid.publication_queue(rpkid) + for child_cert in ChildCert.objects.filter(ca_detail__ca__parent__tenant = self.tenant, + ca_detail__ca__parent_resource_class = class_name, + gski = key.get("ski")): + child_cert.revoke(publisher = publisher) + yield publisher.call_pubd() + SubElement(r_msg, key.tag, class_name = class_name, ski = key.get("ski")) - @tornado.gen.coroutine - def up_down_handle_revoke(self, rpkid, q_msg, r_msg): - key = q_msg[0] - assert key.tag == rpki.up_down.tag_key - class_name = key.get("class_name") - publisher = rpki.rpkid.publication_queue(rpkid) - for child_cert in ChildCert.objects.filter(ca_detail__ca__parent__tenant = self.tenant, - ca_detail__ca__parent_resource_class = class_name, - gski = key.get("ski")): - child_cert.revoke(publisher = publisher) - yield publisher.call_pubd() - SubElement(r_msg, key.tag, class_name = class_name, ski = key.get("ski")) + @tornado.gen.coroutine + def serve_up_down(self, rpkid, q_der): + """ + Outer layer of server handling for one up-down PDU from this child. + """ - @tornado.gen.coroutine - def serve_up_down(self, rpkid, q_der): - """ - Outer layer of server handling for one up-down PDU from this child. - """ + if self.bsc is None: + raise rpki.exceptions.BSCNotFound("Could not find BSC") - if self.bsc is None: - raise rpki.exceptions.BSCNotFound("Could not find BSC") + q_cms = rpki.up_down.cms_msg(DER = q_der) + q_msg = q_cms.unwrap((rpkid.bpki_ta, self.tenant.bpki_cert, self.tenant.bpki_glue, self.bpki_cert, self.bpki_glue)) + q_cms.check_replay_sql(self, "child", self.child_handle) + q_type = q_msg.get("type") - q_cms = rpki.up_down.cms_msg(DER = q_der) - q_msg = q_cms.unwrap((rpkid.bpki_ta, self.tenant.bpki_cert, self.tenant.bpki_glue, self.bpki_cert, self.bpki_glue)) - q_cms.check_replay_sql(self, "child", self.child_handle) - q_type = q_msg.get("type") + logger.info("Serving %s query from child %s [sender %s, recipient %s]", + q_type, self.child_handle, q_msg.get("sender"), q_msg.get("recipient")) - logger.info("Serving %s query from child %s [sender %s, recipient %s]", - q_type, self.child_handle, q_msg.get("sender"), q_msg.get("recipient")) + if rpki.up_down.enforce_strict_up_down_xml_sender and q_msg.get("sender") != self.child_handle: + raise rpki.exceptions.BadSender("Unexpected XML sender %s" % q_msg.get("sender")) - if rpki.up_down.enforce_strict_up_down_xml_sender and q_msg.get("sender") != self.child_handle: - raise rpki.exceptions.BadSender("Unexpected XML sender %s" % q_msg.get("sender")) + r_msg = Element(rpki.up_down.tag_message, nsmap = rpki.up_down.nsmap, version = rpki.up_down.version, + sender = q_msg.get("recipient"), recipient = q_msg.get("sender"), type = q_type + "_response") - r_msg = Element(rpki.up_down.tag_message, nsmap = rpki.up_down.nsmap, version = rpki.up_down.version, - sender = q_msg.get("recipient"), recipient = q_msg.get("sender"), type = q_type + "_response") + try: + yield getattr(self, "up_down_handle_" + q_type)(rpkid, q_msg, r_msg) - try: - yield getattr(self, "up_down_handle_" + q_type)(rpkid, q_msg, r_msg) + except Exception, e: + logger.exception("Unhandled exception serving child %r", self) + rpki.up_down.generate_error_response_from_exception(r_msg, e, q_type) - except Exception, e: - logger.exception("Unhandled exception serving child %r", self) - rpki.up_down.generate_error_response_from_exception(r_msg, e, q_type) - - r_der = rpki.up_down.cms_msg().wrap(r_msg, self.bsc.private_key_id, self.bsc.signing_cert, self.bsc.signing_cert_crl) - raise tornado.gen.Return(r_der) + r_der = rpki.up_down.cms_msg().wrap(r_msg, self.bsc.private_key_id, self.bsc.signing_cert, self.bsc.signing_cert_crl) + raise tornado.gen.Return(r_der) class ChildCert(models.Model): - cert = CertificateField() - published = SundialField(null = True) - gski = models.CharField(max_length = 27) # Assumes SHA-1 -- SHA-256 would be 43, SHA-512 would be 86, etc. - 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.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, gski = self.gski): - 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() + cert = CertificateField() + published = SundialField(null = True) + gski = models.CharField(max_length = 27) # Assumes SHA-1 -- SHA-256 would be 43, SHA-512 would be 86, etc. + 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.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, gski = self.gski): + 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 EECertificate(models.Model): - gski = models.CharField(max_length = 27) # Assumes SHA-1 -- SHA-256 would be 43, SHA-512 would be 86, etc. - cert = CertificateField() - published = SundialField(null = True) - tenant = models.ForeignKey(Tenant, related_name = "ee_certificates") - ca_detail = models.ForeignKey(CADetail, related_name = "ee_certificates") - - - @property - def uri(self): - """ - Return the publication URI for this ee_cert_obj. - """ - - return self.ca_detail.ca.sia_uri + self.uri_tail - - - @property - def uri_tail(self): - """ - Return the tail (filename portion) of the publication URI for this - ee_cert_obj. - """ - - return self.gski + ".cer" - - - @classmethod - def create(cls, ca_detail, subject_name, subject_key, resources, publisher, eku = None): - """ - Generate a new EE certificate. - """ - - # The low-level X.509 code really ought to supply the singleton - # tuple wrapper when handed a string, but that yak will need to - # wait until another day for its shave. - - cn, sn = subject_name.extract_cn_and_sn() - sia = (None, None, - (ca_detail.ca.sia_uri + subject_key.gSKI() + ".cer",), - (ca_detail.ca.parent.repository.rrdp_notification_uri,)) - cert = ca_detail.issue_ee( - ca = ca_detail.ca, - subject_key = subject_key, - sia = sia, - resources = resources, - notAfter = resources.valid_until, - cn = cn, - sn = sn, - eku = eku) - self = cls(tenant = ca_detail.ca.parent.tenant, ca_detail = ca_detail, cert = cert, gski = subject_key.gSKI()) - publisher.queue( - uri = self.uri, - new_obj = self.cert, - repository = ca_detail.ca.parent.repository, - handler = self.published_callback) - self.save() - ca_detail.generate_manifest(publisher = publisher) - logger.debug("New ee_cert %r", self) - return self - - - def revoke(self, publisher, generate_crl_and_manifest = True): - """ - Revoke and withdraw an EE certificate. - """ - - 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, publisher, ca_detail = None, resources = None, force = False): - """ - Reissue an existing EE cert, reusing the public key. If the EE - cert we would generate is identical to the one we already have, we - just return; if we need to reissue, we reuse this ee_cert_obj and - just update its contents, as the publication URI will not have - changed. - """ - - needed = False - old_cert = self.cert - old_ca_detail = self.ca_detail - if ca_detail is None: - ca_detail = old_ca_detail - assert ca_detail.ca is old_ca_detail.ca - old_resources = old_cert.get_3779resources() - if resources is None: - resources = old_resources - assert resources.valid_until is not None and old_resources.valid_until is not None - assert ca_detail.covers(resources) - if ca_detail != self.ca_detail: - logger.debug("ca_detail changed for %r: old %r new %r", self, self.ca_detail, ca_detail) - needed = True - if ca_detail.ca_cert_uri != old_cert.get_AIA()[0]: - logger.debug("AIA changed for %r: old %s new %s", self, old_cert.get_AIA()[0], ca_detail.ca_cert_uri) - 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 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 - must_revoke = old_resources.oversized(resources) or old_resources.valid_until > resources.valid_until - if must_revoke: - logger.debug("Must revoke 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 - cn, sn = self.cert.getSubject().extract_cn_and_sn() - self.cert = ca_detail.issue_ee( - ca = ca_detail.ca, - subject_key = self.cert.getPublicKey(), - eku = self.cert.get_EKU(), - sia = (None, None, self.uri, ca_detail.ca.parent.repository.rrdp_notification_uri), - resources = resources, - notAfter = resources.valid_until, - cn = cn, - sn = sn) - self.save() - publisher.queue( - uri = self.uri, - old_obj = old_cert, - new_obj = self.cert, - repository = ca_detail.ca.parent.repository, - handler = self.published_callback) - if must_revoke: - RevokedCert.revoke(cert = old_cert.cert, ca_detail = old_ca_detail) - ca_detail.generate_crl(publisher = publisher) - ca_detail.generate_manifest(publisher = publisher) - - - def published_callback(self, pdu): - """ - Publication callback: check result and mark published. - """ - - rpki.publication.raise_if_error(pdu) - self.published = None - self.save() + gski = models.CharField(max_length = 27) # Assumes SHA-1 -- SHA-256 would be 43, SHA-512 would be 86, etc. + cert = CertificateField() + published = SundialField(null = True) + tenant = models.ForeignKey(Tenant, related_name = "ee_certificates") + ca_detail = models.ForeignKey(CADetail, related_name = "ee_certificates") + + + @property + def uri(self): + """ + Return the publication URI for this ee_cert_obj. + """ + + return self.ca_detail.ca.sia_uri + self.uri_tail + + + @property + def uri_tail(self): + """ + Return the tail (filename portion) of the publication URI for this + ee_cert_obj. + """ + + return self.gski + ".cer" + + + @classmethod + def create(cls, ca_detail, subject_name, subject_key, resources, publisher, eku = None): + """ + Generate a new EE certificate. + """ + + # The low-level X.509 code really ought to supply the singleton + # tuple wrapper when handed a string, but that yak will need to + # wait until another day for its shave. + + cn, sn = subject_name.extract_cn_and_sn() + sia = (None, None, + (ca_detail.ca.sia_uri + subject_key.gSKI() + ".cer",), + (ca_detail.ca.parent.repository.rrdp_notification_uri,)) + cert = ca_detail.issue_ee( + ca = ca_detail.ca, + subject_key = subject_key, + sia = sia, + resources = resources, + notAfter = resources.valid_until, + cn = cn, + sn = sn, + eku = eku) + self = cls(tenant = ca_detail.ca.parent.tenant, ca_detail = ca_detail, cert = cert, gski = subject_key.gSKI()) + publisher.queue( + uri = self.uri, + new_obj = self.cert, + repository = ca_detail.ca.parent.repository, + handler = self.published_callback) + self.save() + ca_detail.generate_manifest(publisher = publisher) + logger.debug("New ee_cert %r", self) + return self + + + def revoke(self, publisher, generate_crl_and_manifest = True): + """ + Revoke and withdraw an EE certificate. + """ + + 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, publisher, ca_detail = None, resources = None, force = False): + """ + Reissue an existing EE cert, reusing the public key. If the EE + cert we would generate is identical to the one we already have, we + just return; if we need to reissue, we reuse this ee_cert_obj and + just update its contents, as the publication URI will not have + changed. + """ + + needed = False + old_cert = self.cert + old_ca_detail = self.ca_detail + if ca_detail is None: + ca_detail = old_ca_detail + assert ca_detail.ca is old_ca_detail.ca + old_resources = old_cert.get_3779resources() + if resources is None: + resources = old_resources + assert resources.valid_until is not None and old_resources.valid_until is not None + assert ca_detail.covers(resources) + if ca_detail != self.ca_detail: + logger.debug("ca_detail changed for %r: old %r new %r", self, self.ca_detail, ca_detail) + needed = True + if ca_detail.ca_cert_uri != old_cert.get_AIA()[0]: + logger.debug("AIA changed for %r: old %s new %s", self, old_cert.get_AIA()[0], ca_detail.ca_cert_uri) + 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 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 + must_revoke = old_resources.oversized(resources) or old_resources.valid_until > resources.valid_until + if must_revoke: + logger.debug("Must revoke 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 + cn, sn = self.cert.getSubject().extract_cn_and_sn() + self.cert = ca_detail.issue_ee( + ca = ca_detail.ca, + subject_key = self.cert.getPublicKey(), + eku = self.cert.get_EKU(), + sia = (None, None, self.uri, ca_detail.ca.parent.repository.rrdp_notification_uri), + resources = resources, + notAfter = resources.valid_until, + cn = cn, + sn = sn) + self.save() + publisher.queue( + uri = self.uri, + old_obj = old_cert, + new_obj = self.cert, + repository = ca_detail.ca.parent.repository, + handler = self.published_callback) + if must_revoke: + RevokedCert.revoke(cert = old_cert.cert, ca_detail = old_ca_detail) + ca_detail.generate_crl(publisher = publisher) + ca_detail.generate_manifest(publisher = publisher) + + + def published_callback(self, pdu): + """ + Publication callback: check result and mark published. + """ + + rpki.publication.raise_if_error(pdu) + self.published = None + self.save() class Ghostbuster(models.Model): - vcard = models.TextField() - cert = CertificateField() - ghostbuster = GhostbusterField() - published = SundialField(null = True) - tenant = models.ForeignKey(Tenant, related_name = "ghostbusters") - ca_detail = models.ForeignKey(CADetail, related_name = "ghostbusters") - - - def update(self, publisher, fast = False): - """ - Bring this ghostbuster_obj up to date if necesssary. - """ - - if self.ghostbuster is None: - logger.debug("Ghostbuster record doesn't exist, generating") - return self.generate(publisher = publisher, fast = fast) - - now = rpki.sundial.now() - regen_time = self.cert.getNotAfter() - rpki.sundial.timedelta(seconds = self.tenant.regen_margin) - - if now > regen_time and self.cert.getNotAfter() < self.ca_detail.latest_ca_cert.getNotAfter(): - logger.debug("%r past threshold %s, regenerating", self, regen_time) - return self.regenerate(publisher = publisher, fast = fast) - - if now > regen_time: - logger.warning("%r is past threshold %s but so is issuer %r, can't regenerate", self, regen_time, self.ca_detail) - - if self.cert.get_AIA()[0] != self.ca_detail.ca_cert_uri: - logger.debug("%r AIA changed, regenerating", self) - return self.regenerate(publisher = publisher, fast = fast) - - - def generate(self, publisher, fast = False): - """ - Generate a Ghostbuster record - - Once we have the right covering certificate, we generate the - ghostbuster payload, generate a new EE certificate, use the EE - certificate to sign the ghostbuster payload, publish the result, - then throw away the private key for the EE cert. This is modeled - after the way we handle ROAs. - - If fast is set, we leave generating the new manifest for our - caller to handle, presumably at the end of a bulk operation. - """ - - resources = rpki.resource_set.resource_bag.from_inheritance() - keypair = rpki.x509.RSA.generate() - self.cert = self.ca_detail.issue_ee( - ca = self.ca_detail.ca, - resources = resources, - subject_key = keypair.get_public(), - sia = (None, None, self.uri_from_key(keypair), self.ca_detail.ca.parent.repository.rrdp_notification_uri)) - self.ghostbuster = rpki.x509.Ghostbuster.build(self.vcard, keypair, (self.cert,)) - self.published = rpki.sundial.now() - self.save() - logger.debug("Generating Ghostbuster record %r", self.uri) - publisher.queue( - uri = self.uri, - new_obj = self.ghostbuster, - repository = self.ca_detail.ca.parent.repository, - handler = self.published_callback) - if not fast: - self.ca_detail.generate_manifest(publisher = publisher) - - - def published_callback(self, pdu): - """ - Check publication result. - """ - - rpki.publication.raise_if_error(pdu) - self.published = None - self.save() - - - def revoke(self, publisher, regenerate = False, allow_failure = False, fast = False): - """ - Withdraw Ghostbuster associated with this ghostbuster_obj. - - In order to preserve make-before-break properties without - duplicating code, this method also handles generating a - replacement ghostbuster when requested. - - If allow_failure is set, failing to withdraw the ghostbuster will not be - considered an error. - - If fast is set, SQL actions will be deferred, on the assumption - that our caller will handle regenerating CRL and manifest and - flushing the SQL cache. - """ - - ca_detail = self.ca_detail - logger.debug("%s %r, ca_detail %r state is %s", - "Regenerating" if regenerate else "Not regenerating", - self, ca_detail, ca_detail.state) - if regenerate: - self.generate(publisher = publisher, fast = fast) - logger.debug("Withdrawing %r %s and revoking its EE cert", self, self.uri) - RevokedCert.revoke(cert = self.cert, ca_detail = ca_detail) - publisher.queue(uri = self.uri, - old_obj = self.ghostbuster, - repository = ca_detail.ca.parent.repository, - handler = False if allow_failure else None) - if not regenerate: - self.delete() - if not fast: - ca_detail.generate_crl(publisher = publisher) - ca_detail.generate_manifest(publisher = publisher) - - - def regenerate(self, publisher, fast = False): - """ - Reissue Ghostbuster associated with this ghostbuster_obj. - """ + vcard = models.TextField() + cert = CertificateField() + ghostbuster = GhostbusterField() + published = SundialField(null = True) + tenant = models.ForeignKey(Tenant, related_name = "ghostbusters") + ca_detail = models.ForeignKey(CADetail, related_name = "ghostbusters") + + + def update(self, publisher, fast = False): + """ + Bring this ghostbuster_obj up to date if necesssary. + """ + + if self.ghostbuster is None: + logger.debug("Ghostbuster record doesn't exist, generating") + return self.generate(publisher = publisher, fast = fast) + + now = rpki.sundial.now() + regen_time = self.cert.getNotAfter() - rpki.sundial.timedelta(seconds = self.tenant.regen_margin) + + if now > regen_time and self.cert.getNotAfter() < self.ca_detail.latest_ca_cert.getNotAfter(): + logger.debug("%r past threshold %s, regenerating", self, regen_time) + return self.regenerate(publisher = publisher, fast = fast) + + if now > regen_time: + logger.warning("%r is past threshold %s but so is issuer %r, can't regenerate", self, regen_time, self.ca_detail) + + if self.cert.get_AIA()[0] != self.ca_detail.ca_cert_uri: + logger.debug("%r AIA changed, regenerating", self) + return self.regenerate(publisher = publisher, fast = fast) + + + def generate(self, publisher, fast = False): + """ + Generate a Ghostbuster record + + Once we have the right covering certificate, we generate the + ghostbuster payload, generate a new EE certificate, use the EE + certificate to sign the ghostbuster payload, publish the result, + then throw away the private key for the EE cert. This is modeled + after the way we handle ROAs. + + If fast is set, we leave generating the new manifest for our + caller to handle, presumably at the end of a bulk operation. + """ + + resources = rpki.resource_set.resource_bag.from_inheritance() + keypair = rpki.x509.RSA.generate() + self.cert = self.ca_detail.issue_ee( + ca = self.ca_detail.ca, + resources = resources, + subject_key = keypair.get_public(), + sia = (None, None, self.uri_from_key(keypair), self.ca_detail.ca.parent.repository.rrdp_notification_uri)) + self.ghostbuster = rpki.x509.Ghostbuster.build(self.vcard, keypair, (self.cert,)) + self.published = rpki.sundial.now() + self.save() + logger.debug("Generating Ghostbuster record %r", self.uri) + publisher.queue( + uri = self.uri, + new_obj = self.ghostbuster, + repository = self.ca_detail.ca.parent.repository, + handler = self.published_callback) + if not fast: + self.ca_detail.generate_manifest(publisher = publisher) + + + def published_callback(self, pdu): + """ + Check publication result. + """ + + rpki.publication.raise_if_error(pdu) + self.published = None + self.save() + + + def revoke(self, publisher, regenerate = False, allow_failure = False, fast = False): + """ + Withdraw Ghostbuster associated with this ghostbuster_obj. + + In order to preserve make-before-break properties without + duplicating code, this method also handles generating a + replacement ghostbuster when requested. + + If allow_failure is set, failing to withdraw the ghostbuster will not be + considered an error. + + If fast is set, SQL actions will be deferred, on the assumption + that our caller will handle regenerating CRL and manifest and + flushing the SQL cache. + """ + + ca_detail = self.ca_detail + logger.debug("%s %r, ca_detail %r state is %s", + "Regenerating" if regenerate else "Not regenerating", + self, ca_detail, ca_detail.state) + if regenerate: + self.generate(publisher = publisher, fast = fast) + logger.debug("Withdrawing %r %s and revoking its EE cert", self, self.uri) + RevokedCert.revoke(cert = self.cert, ca_detail = ca_detail) + publisher.queue(uri = self.uri, + old_obj = self.ghostbuster, + repository = ca_detail.ca.parent.repository, + handler = False if allow_failure else None) + if not regenerate: + self.delete() + if not fast: + ca_detail.generate_crl(publisher = publisher) + ca_detail.generate_manifest(publisher = publisher) + + + def regenerate(self, publisher, fast = False): + """ + Reissue Ghostbuster associated with this ghostbuster_obj. + """ + + if self.ghostbuster is None: + self.generate(publisher = publisher, fast = fast) + else: + self.revoke(publisher = publisher, regenerate = True, fast = fast) - if self.ghostbuster is None: - self.generate(publisher = publisher, fast = fast) - else: - self.revoke(publisher = publisher, regenerate = True, fast = fast) + def uri_from_key(self, key): + """ + Return publication URI for a public key. + """ - def uri_from_key(self, key): - """ - Return publication URI for a public key. - """ + return self.ca_detail.ca.sia_uri + key.gSKI() + ".gbr" - return self.ca_detail.ca.sia_uri + key.gSKI() + ".gbr" + @property + def uri(self): + """ + Return the publication URI for this ghostbuster_obj's ghostbuster. + """ - @property - def uri(self): - """ - Return the publication URI for this ghostbuster_obj's ghostbuster. - """ + return self.ca_detail.ca.sia_uri + self.uri_tail - return self.ca_detail.ca.sia_uri + self.uri_tail + @property + def uri_tail(self): + """ + Return the tail (filename portion) of the publication URI for this + ghostbuster_obj's ghostbuster. + """ - @property - def uri_tail(self): - """ - Return the tail (filename portion) of the publication URI for this - ghostbuster_obj's ghostbuster. - """ - - return self.cert.gSKI() + ".gbr" + return self.cert.gSKI() + ".gbr" class RevokedCert(models.Model): - serial = models.BigIntegerField() - revoked = SundialField() - expires = SundialField() - ca_detail = models.ForeignKey(CADetail, related_name = "revoked_certs") + serial = models.BigIntegerField() + revoked = SundialField() + expires = SundialField() + ca_detail = models.ForeignKey(CADetail, related_name = "revoked_certs") - @classmethod - def revoke(cls, cert, ca_detail): - """ - Revoke a certificate. - """ + @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) + 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() - ipv4 = models.TextField(null = True) - ipv6 = models.TextField(null = True) - cert = CertificateField() - roa = ROAField() - published = SundialField(null = True) - tenant = models.ForeignKey(Tenant, related_name = "roas") - ca_detail = models.ForeignKey(CADetail, related_name = "roas") - - - def update(self, publisher, fast = False): - """ - Bring ROA up to date if necesssary. - """ - - if self.roa is None: - logger.debug("%r doesn't exist, generating", self) - return self.generate(publisher = publisher, fast = fast) - - if self.ca_detail is None: - logger.debug("%r has no associated ca_detail, generating", self) - return self.generate(publisher = publisher, fast = fast) - - if self.ca_detail.state != "active": - logger.debug("ca_detail associated with %r not active (state %s), regenerating", self, self.ca_detail.state) - return self.regenerate(publisher = publisher, fast = fast) - - now = rpki.sundial.now() - regen_time = self.cert.getNotAfter() - rpki.sundial.timedelta(seconds = self.tenant.regen_margin) - - if now > regen_time and self.cert.getNotAfter() < self.ca_detail.latest_ca_cert.getNotAfter(): - logger.debug("%r past threshold %s, regenerating", self, regen_time) - return self.regenerate(publisher = publisher, fast = fast) - - if now > regen_time: - logger.warning("%r is past threshold %s but so is issuer %r, can't regenerate", self, regen_time, self.ca_detail) - - ca_resources = self.ca_detail.latest_ca_cert.get_3779resources() - ee_resources = self.cert.get_3779resources() - - if ee_resources.oversized(ca_resources): - logger.debug("%r oversized with respect to CA, regenerating", self) - return self.regenerate(publisher = publisher, fast = fast) - - v4 = rpki.resource_set.resource_set_ipv4(self.ipv4) - v6 = rpki.resource_set.resource_set_ipv6(self.ipv6) - - if ee_resources.v4 != v4 or ee_resources.v6 != v6: - logger.debug("%r resources do not match EE, regenerating", self) - return self.regenerate(publisher = publisher, fast = fast) - - if self.cert.get_AIA()[0] != self.ca_detail.ca_cert_uri: - logger.debug("%r AIA changed, regenerating", self) - return self.regenerate(publisher = publisher, fast = fast) - - - def generate(self, publisher, fast = False): - """ - Generate a ROA. - - At present we have no way of performing a direct lookup from a - desired set of resources to a covering certificate, so we have to - search. This could be quite slow if we have a lot of active - ca_detail objects. Punt on the issue for now, revisit if - profiling shows this as a hotspot. - - Once we have the right covering certificate, we generate the ROA - payload, generate a new EE certificate, use the EE certificate to - sign the ROA payload, publish the result, then throw away the - private key for the EE cert, all per the ROA specification. This - implies that generating a lot of ROAs will tend to thrash - /dev/random, but there is not much we can do about that. - - If fast is set, we leave generating the new manifest for our - caller to handle, presumably at the end of a bulk operation. - """ - - if self.ipv4 is None and self.ipv6 is None: - raise rpki.exceptions.EmptyROAPrefixList - - v4 = rpki.resource_set.resource_set_ipv4(self.ipv4) - v6 = rpki.resource_set.resource_set_ipv6(self.ipv6) - - # http://stackoverflow.com/questions/26270042/how-do-you-catch-this-exception - # "Django is amazing when its not terrifying." - try: - ca_detail = self.ca_detail - except CADetail.DoesNotExist: - ca_detail = None - - if ca_detail is not None and ca_detail.state == "active" and not ca_detail.has_expired(): - logger.debug("Keeping old ca_detail %r for ROA %r", ca_detail, self) - else: - logger.debug("Searching for new ca_detail for ROA %r", self) - for ca_detail in CADetail.objects.filter(ca__parent__tenant = self.tenant, state = "active"): - resources = ca_detail.latest_ca_cert.get_3779resources() - if not ca_detail.has_expired() and v4.issubset(resources.v4) and v6.issubset(resources.v6): - logger.debug("Using new ca_detail %r for ROA %r", ca_detail, self) - self.ca_detail = ca_detail - break - else: - raise rpki.exceptions.NoCoveringCertForROA("Could not find a certificate covering %r" % self) - - resources = rpki.resource_set.resource_bag(v4 = v4, v6 = v6) - keypair = rpki.x509.RSA.generate() - - self.cert = self.ca_detail.issue_ee( - ca = self.ca_detail.ca, - resources = resources, - subject_key = keypair.get_public(), - sia = (None, None, self.uri_from_key(keypair), self.ca_detail.ca.parent.repository.rrdp_notification_uri)) - self.roa = rpki.x509.ROA.build(self.asn, - rpki.resource_set.roa_prefix_set_ipv4(self.ipv4), - rpki.resource_set.roa_prefix_set_ipv6(self.ipv6), - keypair, - (self.cert,)) - self.published = rpki.sundial.now() - self.save() - - logger.debug("Generating %r URI %s", self, self.uri) - publisher.queue(uri = self.uri, new_obj = self.roa, - repository = self.ca_detail.ca.parent.repository, - handler = self.published_callback) - if not fast: - self.ca_detail.generate_manifest(publisher = publisher) - - - def published_callback(self, pdu): - """ - Check publication result. - """ - - rpki.publication.raise_if_error(pdu) - self.published = None - self.save() - - - def revoke(self, publisher, regenerate = False, allow_failure = False, fast = False): - """ - Withdraw ROA associated with this roa_obj. - - In order to preserve make-before-break properties without - duplicating code, this method also handles generating a - replacement ROA when requested. - - If allow_failure is set, failing to withdraw the ROA will not be - considered an error. - - If fast is set, SQL actions will be deferred, on the assumption - that our caller will handle regenerating CRL and manifest and - flushing the SQL cache. - """ - - ca_detail = self.ca_detail - logger.debug("%s %r, ca_detail %r state is %s", - "Regenerating" if regenerate else "Not regenerating", - self, ca_detail, ca_detail.state) - if regenerate: - self.generate(publisher = publisher, fast = fast) - logger.debug("Withdrawing %r %s and revoking its EE cert", self, self.uri) - RevokedCert.revoke(cert = self.cert, ca_detail = ca_detail) - publisher.queue(uri = self.uri, old_obj = self.roa, - repository = ca_detail.ca.parent.repository, - handler = False if allow_failure else None) - if not regenerate: - self.delete() - if not fast: - ca_detail.generate_crl(publisher = publisher) - ca_detail.generate_manifest(publisher = publisher) - - - def regenerate(self, publisher, fast = False): - """ - Reissue ROA associated with this roa_obj. - """ + asn = models.BigIntegerField() + ipv4 = models.TextField(null = True) + ipv6 = models.TextField(null = True) + cert = CertificateField() + roa = ROAField() + published = SundialField(null = True) + tenant = models.ForeignKey(Tenant, related_name = "roas") + ca_detail = models.ForeignKey(CADetail, related_name = "roas") + + + def update(self, publisher, fast = False): + """ + Bring ROA up to date if necesssary. + """ + + if self.roa is None: + logger.debug("%r doesn't exist, generating", self) + return self.generate(publisher = publisher, fast = fast) + + if self.ca_detail is None: + logger.debug("%r has no associated ca_detail, generating", self) + return self.generate(publisher = publisher, fast = fast) + + if self.ca_detail.state != "active": + logger.debug("ca_detail associated with %r not active (state %s), regenerating", self, self.ca_detail.state) + return self.regenerate(publisher = publisher, fast = fast) + + now = rpki.sundial.now() + regen_time = self.cert.getNotAfter() - rpki.sundial.timedelta(seconds = self.tenant.regen_margin) + + if now > regen_time and self.cert.getNotAfter() < self.ca_detail.latest_ca_cert.getNotAfter(): + logger.debug("%r past threshold %s, regenerating", self, regen_time) + return self.regenerate(publisher = publisher, fast = fast) + + if now > regen_time: + logger.warning("%r is past threshold %s but so is issuer %r, can't regenerate", self, regen_time, self.ca_detail) + + ca_resources = self.ca_detail.latest_ca_cert.get_3779resources() + ee_resources = self.cert.get_3779resources() + + if ee_resources.oversized(ca_resources): + logger.debug("%r oversized with respect to CA, regenerating", self) + return self.regenerate(publisher = publisher, fast = fast) + + v4 = rpki.resource_set.resource_set_ipv4(self.ipv4) + v6 = rpki.resource_set.resource_set_ipv6(self.ipv6) + + if ee_resources.v4 != v4 or ee_resources.v6 != v6: + logger.debug("%r resources do not match EE, regenerating", self) + return self.regenerate(publisher = publisher, fast = fast) + + if self.cert.get_AIA()[0] != self.ca_detail.ca_cert_uri: + logger.debug("%r AIA changed, regenerating", self) + return self.regenerate(publisher = publisher, fast = fast) + + + def generate(self, publisher, fast = False): + """ + Generate a ROA. + + At present we have no way of performing a direct lookup from a + desired set of resources to a covering certificate, so we have to + search. This could be quite slow if we have a lot of active + ca_detail objects. Punt on the issue for now, revisit if + profiling shows this as a hotspot. + + Once we have the right covering certificate, we generate the ROA + payload, generate a new EE certificate, use the EE certificate to + sign the ROA payload, publish the result, then throw away the + private key for the EE cert, all per the ROA specification. This + implies that generating a lot of ROAs will tend to thrash + /dev/random, but there is not much we can do about that. + + If fast is set, we leave generating the new manifest for our + caller to handle, presumably at the end of a bulk operation. + """ + + if self.ipv4 is None and self.ipv6 is None: + raise rpki.exceptions.EmptyROAPrefixList + + v4 = rpki.resource_set.resource_set_ipv4(self.ipv4) + v6 = rpki.resource_set.resource_set_ipv6(self.ipv6) + + # http://stackoverflow.com/questions/26270042/how-do-you-catch-this-exception + # "Django is amazing when its not terrifying." + try: + ca_detail = self.ca_detail + except CADetail.DoesNotExist: + ca_detail = None + + if ca_detail is not None and ca_detail.state == "active" and not ca_detail.has_expired(): + logger.debug("Keeping old ca_detail %r for ROA %r", ca_detail, self) + else: + logger.debug("Searching for new ca_detail for ROA %r", self) + for ca_detail in CADetail.objects.filter(ca__parent__tenant = self.tenant, state = "active"): + resources = ca_detail.latest_ca_cert.get_3779resources() + if not ca_detail.has_expired() and v4.issubset(resources.v4) and v6.issubset(resources.v6): + logger.debug("Using new ca_detail %r for ROA %r", ca_detail, self) + self.ca_detail = ca_detail + break + else: + raise rpki.exceptions.NoCoveringCertForROA("Could not find a certificate covering %r" % self) + + resources = rpki.resource_set.resource_bag(v4 = v4, v6 = v6) + keypair = rpki.x509.RSA.generate() + + self.cert = self.ca_detail.issue_ee( + ca = self.ca_detail.ca, + resources = resources, + subject_key = keypair.get_public(), + sia = (None, None, self.uri_from_key(keypair), self.ca_detail.ca.parent.repository.rrdp_notification_uri)) + self.roa = rpki.x509.ROA.build(self.asn, + rpki.resource_set.roa_prefix_set_ipv4(self.ipv4), + rpki.resource_set.roa_prefix_set_ipv6(self.ipv6), + keypair, + (self.cert,)) + self.published = rpki.sundial.now() + self.save() + + logger.debug("Generating %r URI %s", self, self.uri) + publisher.queue(uri = self.uri, new_obj = self.roa, + repository = self.ca_detail.ca.parent.repository, + handler = self.published_callback) + if not fast: + self.ca_detail.generate_manifest(publisher = publisher) + + + def published_callback(self, pdu): + """ + Check publication result. + """ + + rpki.publication.raise_if_error(pdu) + self.published = None + self.save() + + + def revoke(self, publisher, regenerate = False, allow_failure = False, fast = False): + """ + Withdraw ROA associated with this roa_obj. + + In order to preserve make-before-break properties without + duplicating code, this method also handles generating a + replacement ROA when requested. + + If allow_failure is set, failing to withdraw the ROA will not be + considered an error. + + If fast is set, SQL actions will be deferred, on the assumption + that our caller will handle regenerating CRL and manifest and + flushing the SQL cache. + """ + + ca_detail = self.ca_detail + logger.debug("%s %r, ca_detail %r state is %s", + "Regenerating" if regenerate else "Not regenerating", + self, ca_detail, ca_detail.state) + if regenerate: + self.generate(publisher = publisher, fast = fast) + logger.debug("Withdrawing %r %s and revoking its EE cert", self, self.uri) + RevokedCert.revoke(cert = self.cert, ca_detail = ca_detail) + publisher.queue(uri = self.uri, old_obj = self.roa, + repository = ca_detail.ca.parent.repository, + handler = False if allow_failure else None) + if not regenerate: + self.delete() + if not fast: + ca_detail.generate_crl(publisher = publisher) + ca_detail.generate_manifest(publisher = publisher) + + + def regenerate(self, publisher, fast = False): + """ + Reissue ROA associated with this roa_obj. + """ + + if self.ca_detail is None: + self.generate(publisher = publisher, fast = fast) + else: + self.revoke(publisher = publisher, regenerate = True, fast = fast) - if self.ca_detail is None: - self.generate(publisher = publisher, fast = fast) - else: - self.revoke(publisher = publisher, regenerate = True, fast = fast) + def uri_from_key(self, key): + """ + Return publication URI for a public key. + """ - def uri_from_key(self, key): - """ - Return publication URI for a public key. - """ + return self.ca_detail.ca.sia_uri + key.gSKI() + ".roa" - return self.ca_detail.ca.sia_uri + key.gSKI() + ".roa" + @property + def uri(self): + """ + Return the publication URI for this roa_obj's ROA. + """ - @property - def uri(self): - """ - Return the publication URI for this roa_obj's ROA. - """ - - return self.ca_detail.ca.sia_uri + self.uri_tail + return self.ca_detail.ca.sia_uri + self.uri_tail - @property - def uri_tail(self): - """ - Return the tail (filename portion) of the publication URI for this - roa_obj's ROA. - """ + @property + def uri_tail(self): + """ + Return the tail (filename portion) of the publication URI for this + roa_obj's ROA. + """ - return self.cert.gSKI() + ".roa" + return self.cert.gSKI() + ".roa" |