aboutsummaryrefslogtreecommitdiff
path: root/rpki/irdb/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'rpki/irdb/models.py')
-rw-r--r--rpki/irdb/models.py646
1 files changed, 646 insertions, 0 deletions
diff --git a/rpki/irdb/models.py b/rpki/irdb/models.py
new file mode 100644
index 00000000..1ad9b4e3
--- /dev/null
+++ b/rpki/irdb/models.py
@@ -0,0 +1,646 @@
+# $Id$
+#
+# Copyright (C) 2013--2014 Dragon Research Labs ("DRL")
+# Portions copyright (C) 2011--2012 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 notices and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND DRL AND ISC DISCLAIM ALL
+# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL DRL OR
+# 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.
+
+"""
+Internet Registry (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.
+"""
+
+# pylint: disable=W0232
+
+import django.db.models
+import rpki.x509
+import rpki.sundial
+import rpki.resource_set
+import socket
+import rpki.POW
+from south.modelsinspector import add_introspection_rules
+
+## @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. This should be a little
+# longer than the real regeneration cycle, so that the old CRL will
+# not go stale while we're generating the new one. Eg, if we
+# regenerate daily, an interval of 24 hours is too short, but 25 hours
+# would be OK, as would 24 hours and 30 minutes.
+
+crl_interval = rpki.sundial.timedelta(hours = 25)
+
+## @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.get("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.from_datetime(
+ 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_datetime()
+ else:
+ return value
+
+
+class DERField(django.db.models.Field):
+ """
+ Field types for DER objects.
+ """
+
+ __metaclass__ = django.db.models.SubfieldBase
+
+ def __init__(self, *args, **kwargs):
+ kwargs["serialize"] = False
+ kwargs["blank"] = True
+ kwargs["default"] = None
+ django.db.models.Field.__init__(self, *args, **kwargs)
+
+ def db_type(self, connection):
+ if connection.settings_dict['ENGINE'] == "django.db.backends.posgresql":
+ return "bytea"
+ else:
+ return "BLOB"
+
+ def 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 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
+
+class CertificateField(DERField):
+ description = "X.509 certificate"
+ rpki_type = rpki.x509.X509
+
+class RSAKeyField(DERField):
+ description = "RSA keypair"
+ rpki_type = rpki.x509.RSA
+
+class CRLField(DERField):
+ description = "Certificate Revocation List"
+ rpki_type = rpki.x509.CRL
+
+class PKCS10Field(DERField):
+ description = "PKCS #10 certificate request"
+ rpki_type = rpki.x509.PKCS10
+
+class SignedReferralField(DERField):
+ 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(quiet = True)
+ 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):
+ Revocation.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, r.revoked) 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.from_cn("%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.from_cn("%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.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(quiet = True)
+ self.certificate = self.issuer.certify(
+ subject_name = self.subject_name,
+ subject_key = self.private_key.get_public(),
+ 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.from_cn("%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.from_cn("%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.from_cn("%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 ResourceSet(django.db.models.Model):
+ valid_until = SundialField()
+
+ class Meta:
+ abstract = True
+
+ @property
+ def resource_bag(self):
+ raw_asn, raw_net = self._select_resource_bag()
+ asns = rpki.resource_set.resource_set_as.from_django(
+ (a.start_as, a.end_as) for a in raw_asn)
+ ipv4 = rpki.resource_set.resource_set_ipv4.from_django(
+ (a.start_ip, a.end_ip) for a in raw_net if a.version == "IPv4")
+ ipv6 = rpki.resource_set.resource_set_ipv6.from_django(
+ (a.start_ip, a.end_ip) for a in raw_net if a.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.
+
+class ResourceSetASN(django.db.models.Model):
+ start_as = django.db.models.BigIntegerField()
+ end_as = django.db.models.BigIntegerField()
+
+ class Meta:
+ abstract = True
+
+ def as_resource_range(self):
+ return rpki.resource_set.resource_range_as(self.start_as, self.end_as)
+
+class ResourceSetNet(django.db.models.Model):
+ start_ip = django.db.models.CharField(max_length = 40)
+ end_ip = django.db.models.CharField(max_length = 40)
+ version = EnumField(choices = ip_version_choices)
+
+ class Meta:
+ abstract = True
+
+ def as_resource_range(self):
+ return rpki.resource_set.resource_range_ip.from_strings(self.start_ip, self.end_ip)
+
+class Child(CrossCertification, ResourceSet):
+ issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "children")
+ name = django.db.models.TextField(null = True, blank = True)
+
+ def _select_resource_bag(self):
+ child_asn = rpki.irdb.ChildASN.objects.raw("""
+ SELECT *
+ FROM irdb_childasn
+ WHERE child_id = %s
+ """, [self.id])
+ child_net = list(rpki.irdb.ChildNet.objects.raw("""
+ SELECT *
+ FROM irdb_childnet
+ WHERE child_id = %s
+ """, [self.id]))
+ return child_asn, child_net
+
+ class Meta:
+ unique_together = ("issuer", "handle")
+
+class ChildASN(ResourceSetASN):
+ child = django.db.models.ForeignKey(Child, related_name = "asns")
+
+ class Meta:
+ unique_together = ("child", "start_as", "end_as")
+
+class ChildNet(ResourceSetNet):
+ child = django.db.models.ForeignKey(Child, related_name = "address_ranges")
+
+ 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):
+ prefixes = list(rpki.irdb.ROARequestPrefix.objects.raw("""
+ SELECT *
+ FROM irdb_roarequestprefix
+ WHERE roa_request_id = %s
+ """, [self.id]))
+ v4 = rpki.resource_set.roa_prefix_set_ipv4.from_django(
+ (p.prefix, p.prefixlen, p.max_prefixlen) for p in prefixes if p.version == "IPv4")
+ v6 = rpki.resource_set.roa_prefix_set_ipv6.from_django(
+ (p.prefix, p.prefixlen, p.max_prefixlen) for p in prefixes if p.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.POW.IPAddress(self.prefix), self.prefixlen, self.max_prefixlen)
+ else:
+ return rpki.resource_set.roa_prefix_ipv6(rpki.POW.IPAddress(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 EECertificateRequest(ResourceSet):
+ issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "ee_certificate_requests")
+ pkcs10 = PKCS10Field()
+ gski = django.db.models.CharField(max_length = 27)
+ cn = django.db.models.CharField(max_length = 64)
+ sn = django.db.models.CharField(max_length = 64)
+ eku = django.db.models.TextField(null = True)
+
+ def _select_resource_bag(self):
+ ee_asn = rpki.irdb.EECertificateRequestASN.objects.raw("""
+ SELECT *
+ FROM irdb_eecertificaterequestasn
+ WHERE ee_certificate_request_id = %s
+ """, [self.id])
+ ee_net = rpki.irdb.EECertificateRequestNet.objects.raw("""
+ SELECT *
+ FROM irdb_eecertificaterequestnet
+ WHERE ee_certificate_request_id = %s
+ """, [self.id])
+ return ee_asn, ee_net
+
+ class Meta:
+ unique_together = ("issuer", "gski")
+
+class EECertificateRequestASN(ResourceSetASN):
+ ee_certificate_request = django.db.models.ForeignKey(EECertificateRequest, related_name = "asns")
+
+ class Meta:
+ unique_together = ("ee_certificate_request", "start_as", "end_as")
+
+class EECertificateRequestNet(ResourceSetNet):
+ ee_certificate_request = django.db.models.ForeignKey(EECertificateRequest, related_name = "address_ranges")
+
+ class Meta:
+ unique_together = ("ee_certificate_request", "start_ip", "end_ip", "version")
+
+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")
+
+# for Django South -- these are just simple subclasses
+add_introspection_rules([],
+ ('^rpki\.irdb\.models\.CertificateField',
+ '^rpki\.irdb\.models\.CRLField',
+ '^rpki\.irdb\.models\.EnumField',
+ '^rpki\.irdb\.models\.HandleField',
+ '^rpki\.irdb\.models\.RSAKeyField',
+ '^rpki\.irdb\.models\.SignedReferralField',
+ '^rpki\.irdb\.models\.SundialField'))