diff options
Diffstat (limited to 'rpkid/rpki/irdb/models.py')
-rw-r--r-- | rpkid/rpki/irdb/models.py | 585 |
1 files changed, 585 insertions, 0 deletions
diff --git a/rpkid/rpki/irdb/models.py b/rpkid/rpki/irdb/models.py new file mode 100644 index 00000000..3aaebdcf --- /dev/null +++ b/rpkid/rpki/irdb/models.py @@ -0,0 +1,585 @@ +""" +IR Database, Django-style. + +This is the back-end code's interface to the database. It's intended +to be usable by command line programs and other scripts, not just +Django GUI code, so be careful. + +$Id$ + +Copyright (C) 2011 Internet Systems Consortium ("ISC") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +import django.db.models +import rpki.x509 +import rpki.sundial +import rpki.resource_set +import rpki.ipaddrs +import socket + +## @var ip_version_choices +# Choice argument for fields implementing IP version numbers. + +ip_version_choices = ((4, "IPv4"), (6, "IPv6")) + +## @var ca_certificate_lifetime +# Lifetime for a BPKI CA certificate. + +ca_certificate_lifetime = rpki.sundial.timedelta(days = 3652) + +## @var crl_interval +# Expected interval between BPKI CRL updates + +crl_interval = rpki.sundial.timedelta(days = 1) + +## @var ee_certificate_lifetime +# Lifetime for a BPKI EE certificate. + +ee_certificate_lifetime = rpki.sundial.timedelta(days = 60) + +### + +# Field types + +class HandleField(django.db.models.CharField): + """ + A handle field type. + """ + + description = 'A "handle" in one of the RPKI protocols' + + def __init__(self, *args, **kwargs): + kwargs["max_length"] = 120 + django.db.models.CharField.__init__(self, *args, **kwargs) + +class EnumField(django.db.models.PositiveSmallIntegerField): + """ + An enumeration type that uses strings in Python and small integers + in SQL. + """ + + description = "An enumeration type" + + __metaclass__ = django.db.models.SubfieldBase + + def __init__(self, *args, **kwargs): + if isinstance(kwargs["choices"], (tuple, list)) and isinstance(kwargs["choices"][0], str): + kwargs["choices"] = tuple(enumerate(kwargs["choices"], 1)) + django.db.models.PositiveSmallIntegerField.__init__(self, *args, **kwargs) + self.enum_i2s = dict(self.flatchoices) + self.enum_s2i = dict((v, k) for k, v in self.flatchoices) + + def to_python(self, value): + return self.enum_i2s.get(value, value) + + def get_prep_value(self, value): + return self.enum_s2i.get(value, value) + +class SundialField(django.db.models.DateTimeField): + """ + A field type for our customized datetime objects. + """ + __metaclass__ = django.db.models.SubfieldBase + + description = "A datetime type using our customized datetime objects" + + def to_python(self, value): + if isinstance(value, rpki.sundial.pydatetime.datetime): + return rpki.sundial.datetime.fromdatetime( + django.db.models.DateTimeField.to_python(self, value)) + else: + return value + + def get_prep_value(self, value): + if isinstance(value, rpki.sundial.datetime): + return value.to_sql() + else: + return value + +### + +# Kludge to work around Django 1.2 problem. +# +# This should be a simple abstract base class DERField which we then +# subclass with trivial customization for specific kinds of DER +# objects. Sadly, subclassing of user defined field classes doesn't +# work in Django 1.2 with the django.db.models.SubfieldBase metaclass, +# so instead we fake it by defining methods externally and defining +# each concrete class as a direct subclass of django.db.models.Field. +# +# The bug has been fixed in Django 1.3, so we can revert this to the +# obvious form once we're ready to require Django 1.3 or later. The +# fix may have been backported to the 1.2 branch, but trying to test +# for it is likely more work than just working around it. +# +# See https://code.djangoproject.com/ticket/10728 for details. + +def DERField_init(self, *args, **kwargs): + kwargs["serialize"] = False + kwargs["blank"] = True + kwargs["default"] = None + django.db.models.Field.__init__(self, *args, **kwargs) + +def DERField_db_type(self, connection): + if connection.settings_dict['ENGINE'] == "django.db.backends.posgresql": + return "bytea" + else: + return "BLOB" + +def DERField_to_python(self, value): + assert value is None or isinstance(value, (self.rpki_type, str)) + if isinstance(value, str): + return self.rpki_type(DER = value) + else: + return value + +def DERField_get_prep_value(self, value): + assert value is None or isinstance(value, (self.rpki_type, str)) + if isinstance(value, self.rpki_type): + return value.get_DER() + else: + return value + +def DERField(cls): + cls.__init__ = DERField_init + cls.db_type = DERField_db_type + cls.to_python = DERField_to_python + cls.get_prep_value = DERField_get_prep_value + return cls + +@DERField +class CertificateField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "X.509 certificate" + rpki_type = rpki.x509.X509 + +@DERField +class RSAKeyField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "RSA keypair" + rpki_type = rpki.x509.RSA + +@DERField +class CRLField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "Certificate Revocation List" + rpki_type = rpki.x509.CRL + +@DERField +class PKCS10Field(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "PKCS #10 certificate request" + rpki_type = rpki.x509.PKCS10 + +@DERField +class SignedReferralField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "CMS signed object containing XML" + rpki_type = rpki.x509.SignedReferral + +### + +# Custom managers + +class CertificateManager(django.db.models.Manager): + + def get_or_certify(self, **kwargs): + """ + Sort of like .get_or_create(), but for models containing + certificates which need to be generated based on other fields. + + Takes keyword arguments like .get(), checks for existing object. + If none, creates a new one; if found an existing object but some + of the non-key fields don't match, updates the existing object. + Runs certification method for new or updated objects. Returns a + tuple consisting of the object and a boolean indicating whether + anything has changed. + """ + + changed = False + + try: + obj = self.get(**self._get_or_certify_keys(kwargs)) + + except self.model.DoesNotExist: + obj = self.model(**kwargs) + changed = True + + else: + for k in kwargs: + if getattr(obj, k) != kwargs[k]: + setattr(obj, k, kwargs[k]) + changed = True + + if changed: + obj.avow() + obj.save() + + return obj, changed + + def _get_or_certify_keys(self, kwargs): + assert len(self.model._meta.unique_together) == 1 + return dict((k, kwargs[k]) for k in self.model._meta.unique_together[0]) + +class ResourceHolderCAManager(CertificateManager): + def _get_or_certify_keys(self, kwargs): + return { "handle" : kwargs["handle"] } + +class ServerCAManager(CertificateManager): + def _get_or_certify_keys(self, kwargs): + return { "pk" : 1 } + +class ResourceHolderEEManager(CertificateManager): + def _get_or_certify_keys(self, kwargs): + return { "issuer" : kwargs["issuer"] } + +### + +class CA(django.db.models.Model): + certificate = CertificateField() + private_key = RSAKeyField() + latest_crl = CRLField() + + # Might want to bring these into line with what rpkid does. Current + # variables here were chosen to map easily to what OpenSSL command + # line tool was keeping on disk. + + next_serial = django.db.models.BigIntegerField(default = 1) + next_crl_number = django.db.models.BigIntegerField(default = 1) + last_crl_update = SundialField() + next_crl_update = SundialField() + + class Meta: + abstract = True + + def avow(self): + if self.private_key is None: + self.private_key = rpki.x509.RSA.generate() + now = rpki.sundial.now() + notAfter = now + ca_certificate_lifetime + self.certificate = rpki.x509.X509.bpki_self_certify( + keypair = self.private_key, + subject_name = self.subject_name, + serial = self.next_serial, + now = now, + notAfter = notAfter) + self.next_serial += 1 + self.generate_crl() + return self.certificate + + def certify(self, subject_name, subject_key, validity_interval, is_ca, pathLenConstraint = None): + now = rpki.sundial.now() + notAfter = now + validity_interval + result = self.certificate.bpki_certify( + keypair = self.private_key, + subject_name = subject_name, + subject_key = subject_key, + serial = self.next_serial, + now = now, + notAfter = notAfter, + is_ca = is_ca, + pathLenConstraint = pathLenConstraint) + self.next_serial += 1 + return result + + def revoke(self, cert): + Revocations.objects.create( + issuer = self, + revoked = rpki.sundial.now(), + serial = cert.certificate.getSerial(), + expires = cert.certificate.getNotAfter() + crl_interval) + cert.delete() + self.generate_crl() + + def generate_crl(self): + now = rpki.sundial.now() + self.revocations.filter(expires__lt = now).delete() + revoked = [(r.serial, rpki.sundial.datetime.fromdatetime(r.revoked).toASN1tuple(), ()) + for r in self.revocations.all()] + self.latest_crl = rpki.x509.CRL.generate( + keypair = self.private_key, + issuer = self.certificate, + serial = self.next_crl_number, + thisUpdate = now, + nextUpdate = now + crl_interval, + revokedCertificates = revoked) + self.last_crl_update = now + self.next_crl_update = now + crl_interval + self.next_crl_number += 1 + +class ServerCA(CA): + objects = ServerCAManager() + + def __unicode__(self): + return "" + + @property + def subject_name(self): + if self.certificate is not None: + return self.certificate.getSubject() + else: + return rpki.x509.X501DN("%s BPKI server CA" % socket.gethostname()) + +class ResourceHolderCA(CA): + handle = HandleField(unique = True) + objects = ResourceHolderCAManager() + + def __unicode__(self): + return self.handle + + @property + def subject_name(self): + if self.certificate is not None: + return self.certificate.getSubject() + else: + return rpki.x509.X501DN("%s BPKI resource CA" % self.handle) + +class Certificate(django.db.models.Model): + + certificate = CertificateField() + objects = CertificateManager() + + class Meta: + abstract = True + unique_together = ("issuer", "handle") + + def revoke(self): + self.issuer.revoke(self) + +class CrossCertification(Certificate): + handle = HandleField() + ta = CertificateField() + + class Meta: + abstract = True + + def avow(self): + self.certificate = self.issuer.certify( + subject_name = self.ta.getSubject(), + subject_key = self.ta.getPublicKey(), + validity_interval = ee_certificate_lifetime, + is_ca = True, + pathLenConstraint = 0) + + def __unicode__(self): + return self.handle + +class HostedCA(Certificate): + issuer = django.db.models.ForeignKey(ServerCA) + hosted = django.db.models.OneToOneField(ResourceHolderCA, related_name = "hosted_by") + + def avow(self): + self.certificate = self.issuer.certify( + subject_name = self.hosted.certificate.getSubject(), + subject_key = self.hosted.certificate.getPublicKey(), + validity_interval = ee_certificate_lifetime, + is_ca = True, + pathLenConstraint = 1) + + class Meta: + unique_together = ("issuer", "hosted") + + def __unicode__(self): + return self.hosted_ca.handle + +class Revocation(django.db.models.Model): + serial = django.db.models.BigIntegerField() + revoked = SundialField() + expires = SundialField() + + class Meta: + abstract = True + unique_together = ("issuer", "serial") + +class ServerRevocation(Revocation): + issuer = django.db.models.ForeignKey(ServerCA, related_name = "revocations") + +class ResourceHolderRevocation(Revocation): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "revocations") + +class EECertificate(Certificate): + private_key = RSAKeyField() + + class Meta: + abstract = True + + def avow(self): + if self.private_key is None: + self.private_key = rpki.x509.RSA.generate() + self.certificate = self.issuer.certify( + subject_name = self.subject_name, + subject_key = self.private_key.get_RSApublic(), + validity_interval = ee_certificate_lifetime, + is_ca = False) + +class ServerEE(EECertificate): + issuer = django.db.models.ForeignKey(ServerCA, related_name = "ee_certificates") + purpose = EnumField(choices = ("rpkid", "pubd", "irdbd", "irbe")) + + class Meta: + unique_together = ("issuer", "purpose") + + @property + def subject_name(self): + return rpki.x509.X501DN("%s BPKI %s EE" % (socket.gethostname(), self.get_purpose_display())) + +class Referral(EECertificate): + issuer = django.db.models.OneToOneField(ResourceHolderCA, related_name = "referral_certificate") + objects = ResourceHolderEEManager() + + @property + def subject_name(self): + return rpki.x509.X501DN("%s BPKI Referral EE" % self.issuer.handle) + +class Turtle(django.db.models.Model): + service_uri = django.db.models.CharField(max_length = 255) + +class Rootd(EECertificate, Turtle): + issuer = django.db.models.OneToOneField(ResourceHolderCA, related_name = "rootd") + objects = ResourceHolderEEManager() + + @property + def subject_name(self): + return rpki.x509.X501DN("%s BPKI rootd EE" % self.issuer.handle) + +class BSC(Certificate): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "bscs") + handle = HandleField() + pkcs10 = PKCS10Field() + + def avow(self): + self.certificate = self.issuer.certify( + subject_name = self.pkcs10.getSubject(), + subject_key = self.pkcs10.getPublicKey(), + validity_interval = ee_certificate_lifetime, + is_ca = False) + + def __unicode__(self): + return self.handle + +class Child(CrossCertification): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "children") + name = django.db.models.TextField(null = True, blank = True) + valid_until = SundialField() + + @property + def resource_bag(self): + asns = rpki.resource_set.resource_set_as.from_django( + (a.start_as, a.end_as) for a in self.asns.all()) + ipv4 = rpki.resource_set.resource_set_ipv4.from_django( + (a.start_ip, a.end_ip) for a in self.address_ranges.filter(version = 'IPv4')) + ipv6 = rpki.resource_set.resource_set_ipv6.from_django( + (a.start_ip, a.end_ip) for a in self.address_ranges.filter(version = 'IPv6')) + return rpki.resource_set.resource_bag( + valid_until = self.valid_until, asn = asns, v4 = ipv4, v6 = ipv6) + + # Writing of .setter method deferred until something needs it. + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") + +class ChildASN(django.db.models.Model): + child = django.db.models.ForeignKey(Child, related_name = "asns") + start_as = django.db.models.BigIntegerField() + end_as = django.db.models.BigIntegerField() + + def as_resource_range(self): + return rpki.resource_set.resource_range_as(self.start_as, self.end_as) + + class Meta: + unique_together = ("child", "start_as", "end_as") + +class ChildNet(django.db.models.Model): + child = django.db.models.ForeignKey(Child, related_name = "address_ranges") + start_ip = django.db.models.CharField(max_length = 40) + end_ip = django.db.models.CharField(max_length = 40) + version = EnumField(choices = ip_version_choices) + + def as_resource_range(self): + return rpki.resource_set.resource_range_ip.from_strings(self.start_ip, self.end_ip) + + class Meta: + unique_together = ("child", "start_ip", "end_ip", "version") + +class Parent(CrossCertification, Turtle): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "parents") + parent_handle = HandleField() + child_handle = HandleField() + repository_type = EnumField(choices = ("none", "offer", "referral")) + referrer = HandleField(null = True, blank = True) + referral_authorization = SignedReferralField(null = True, blank = True) + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") + +class ROARequest(django.db.models.Model): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "roa_requests") + asn = django.db.models.BigIntegerField() + + @property + def roa_prefix_bag(self): + v4 = rpki.resource_set.roa_prefix_set_ipv4.from_django( + (p.prefix, p.prefixlen, p.max_prefixlen) for p in self.prefixes.filter(version = 'IPv4')) + v6 = rpki.resource_set.roa_prefix_set_ipv6.from_django( + (p.prefix, p.prefixlen, p.max_prefixlen) for p in self.prefixes.filter(version = 'IPv6')) + return rpki.resource_set.roa_prefix_bag(v4 = v4, v6 = v6) + + # Writing of .setter method deferred until something needs it. + +class ROARequestPrefix(django.db.models.Model): + roa_request = django.db.models.ForeignKey(ROARequest, related_name = "prefixes") + version = EnumField(choices = ip_version_choices) + prefix = django.db.models.CharField(max_length = 40) + prefixlen = django.db.models.PositiveSmallIntegerField() + max_prefixlen = django.db.models.PositiveSmallIntegerField() + + def as_roa_prefix(self): + if self.version == 'IPv4': + return rpki.resource_set.roa_prefix_ipv4(rpki.ipaddrs.v4addr(self.prefix), self.prefixlen, self.max_prefixlen) + else: + return rpki.resource_set.roa_prefix_ipv6(rpki.ipaddrs.v6addr(self.prefix), self.prefixlen, self.max_prefixlen) + + def as_resource_range(self): + return self.as_roa_prefix().to_resource_range() + + class Meta: + unique_together = ("roa_request", "version", "prefix", "prefixlen", "max_prefixlen") + +class GhostbusterRequest(django.db.models.Model): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "ghostbuster_requests") + parent = django.db.models.ForeignKey(Parent, related_name = "ghostbuster_requests", null = True) + vcard = django.db.models.TextField() + +class Repository(CrossCertification): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "repositories") + client_handle = HandleField() + service_uri = django.db.models.CharField(max_length = 255) + sia_base = django.db.models.TextField() + turtle = django.db.models.OneToOneField(Turtle, related_name = "repository") + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") + +class Client(CrossCertification): + issuer = django.db.models.ForeignKey(ServerCA, related_name = "clients") + sia_base = django.db.models.TextField() + parent_handle = HandleField() + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") |