diff options
-rw-r--r-- | rpkid/rpki/irdb/models.py | 102 | ||||
-rw-r--r-- | rpkid/rpki/x509.py | 104 | ||||
-rw-r--r-- | scripts/convert-from-entitydb-to-sql.py | 22 |
3 files changed, 167 insertions, 61 deletions
diff --git a/rpkid/rpki/irdb/models.py b/rpkid/rpki/irdb/models.py index e10a9122..a9bd364f 100644 --- a/rpkid/rpki/irdb/models.py +++ b/rpkid/rpki/irdb/models.py @@ -85,8 +85,6 @@ class CA(django.db.models.Model): identity = django.db.models.ForeignKey(Identity, related_name = "bpki_certificates") purpose_map = ChoiceMap("resources", "servers") purpose = django.db.models.PositiveSmallIntegerField(choices = purpose_map.choices) - certificate = BinaryField() - private_key = BinaryField() next_serial = django.db.models.BigIntegerField(default = 1) next_crl_number = django.db.models.BigIntegerField(default = 1) last_crl_update = django.db.models.DateTimeField() @@ -96,21 +94,60 @@ class CA(django.db.models.Model): unique_together = ("identity", "purpose") class Certificate(django.db.models.Model): - issuer = django.db.models.ForeignKey(CA) certificate = BinaryField() - # We used to use multi-table inheritance here, but that turns out - # not to work so well once we started applying uniqueness - # constraints. So now we use an abstract base class. - # - # This is probably the right approach for data fields, so that we - # can share custom model methods for things like certificate - # issuance, but is a bit tricky for foreign keys due to the - # "related_name" reverse link. See: - # https://docs.djangoproject.com/en/dev/topics/db/models/#model-inheritance + class Meta: + abstract = True + + def get_cert(self): + return rpki.x509.X509(DER = self.certificate) + + def generate_certificate(self): + + # This is sort of vaguely the right idea, but most of it probably + # ought to be a method of the CA, not the object being certified, + # and the rest doesn't yet cover all the different kinds of + # certificates we need to support. + + # Perhaps something where each specialized class has its own + # generate_certificate() method which is mostly just a wrapper for + # a call to the CA object with the right parameters to get the CA + # to issue the certificate. Seems about right. Not awake enough + # to write it now. + + cacert = self.issuer.keyed_certificates.filter(purpose = KeyedCertificate.purpose_map["ca"]) + subject_name, subject_key = self.get_certificate_subject() + cer = cacert.get_cert() + key = cacert.get_key() + + result = cer.bpki_certify(key, subject_name, subject_key, ca.next_serial, + + # This needs to be configurable + rpki.sundial.now() + rpki.sundial.timedelta(days = 60), + + # This is (at least) per-class, not universal + pathLenConstraint = 0, + + # This is per-class too + ca = True) + + self.ca.next_serial += 1 + + self.certificate = result + + + +class CrossCertification(Certificate): + handle = HandleField() + ta = BinaryField() class Meta: abstract = True + unique_together = ("issuer", "handle") + + def get_certificate_subject(self): + ta = rpki.x509.X509(DER = self.ta) + return ta.getSubject(), ta.getPublicKey() class Revocation(django.db.models.Model): issuer = django.db.models.ForeignKey(CA, related_name = "revocations") @@ -121,29 +158,30 @@ class Revocation(django.db.models.Model): class Meta: unique_together = ("issuer", "serial") -class EECertificate(Certificate): - purpose_map = ChoiceMap("rpkid", "pubd", "irdbd", "irbe", "rootd") +class KeyedCertificate(Certificate): + issuer = django.db.models.ForeignKey(CA, related_name = "keyed_certificates") + purpose_map = ChoiceMap("ca", "rpkid", "pubd", "irdbd", "irbe", "rootd") purpose = django.db.models.PositiveSmallIntegerField(choices = purpose_map.choices) private_key = BinaryField() class Meta: unique_together = ("issuer", "purpose") + def get_key(self): + return rpki.x509.RSA(DER = self.private_key) + class BSC(Certificate): + issuer = django.db.models.ForeignKey(CA, related_name = "bscs") handle = HandleField() pkcs10 = BinaryField() class Meta: unique_together = ("issuer", "handle") -class Child(Certificate): - handle = HandleField() +class Child(CrossCertification): + issuer = django.db.models.ForeignKey(CA, related_name = "children") name = django.db.models.TextField(null = True, blank = True) valid_until = django.db.models.DateTimeField() - ta = BinaryField() - - class Meta: - unique_together = ("issuer", "handle") class ChildASN(django.db.models.Model): child = django.db.models.ForeignKey(Child, related_name = "asns") @@ -163,20 +201,16 @@ class ChildNet(django.db.models.Model): class Meta: unique_together = ("child", "start_ip", "end_ip", "version") -class Parent(Certificate): - handle = HandleField() +class Parent(CrossCertification): + issuer = django.db.models.ForeignKey(CA, related_name = "parents") parent_handle = HandleField() child_handle = HandleField() - ta = BinaryField() service_uri = django.db.models.CharField(max_length = 255) repository_type_map = ChoiceMap("none", "offer", "referral") repository_type = django.db.models.PositiveSmallIntegerField(choices = repository_type_map.choices) referrer = HandleField(null = True, blank = True) referral_authorization = BinaryField(null = True, blank = True) - class Meta: - unique_together = ("issuer", "handle") - class ROARequest(django.db.models.Model): identity = django.db.models.ForeignKey(Identity, related_name = "roa_requests") asn = django.db.models.BigIntegerField() @@ -197,20 +231,12 @@ class GhostbusterRequest(django.db.models.Model): parent = django.db.models.ForeignKey(Parent, related_name = "ghostbuster_requests", null = True) vcard = django.db.models.TextField() -class Repository(Certificate): - handle = HandleField() +class Repository(CrossCertification): + issuer = django.db.models.ForeignKey(CA, related_name = "repositories") client_handle = HandleField() - ta = BinaryField() service_uri = django.db.models.CharField(max_length = 255) sia_base = django.db.models.TextField() parent = django.db.models.OneToOneField(Parent, related_name = "repository") - class Meta: - unique_together = ("issuer", "handle") - -class Client(Certificate): - handle = HandleField() - ta = BinaryField() - - class Meta: - unique_together = ("issuer", "handle") +class Client(CrossCertification): + issuer = django.db.models.ForeignKey(CA, related_name = "clients") diff --git a/rpkid/rpki/x509.py b/rpkid/rpki/x509.py index 58129363..2ac9ffca 100644 --- a/rpkid/rpki/x509.py +++ b/rpkid/rpki/x509.py @@ -120,6 +120,74 @@ def _find_xia_uri(extension, name): return location[1] return None +class X501DN(object): + """ + Class to hold an X.501 Distinguished Name. + + This is nothing like a complete implementation, just enough for our + purposes. POW has one interface to this, POW.pkix has another. In + terms of completeness in the Python representation, the POW.pkix + representation is much closer to right, but the whole thing is a + horrible mess. + + See RFC 5280 4.1.2.4 for the ASN.1 details. In brief: + + - A DN is a SEQUENCE of RDNs. + + - A RDN is a set of AttributeAndValues; in practice, multi-value + RDNs are rare, so an RDN is almost always a set with a single + element. + + - An AttributeAndValue is an OID and a value, where a whole bunch + of things including both syntax and semantics of the value are + determined by the OID. + + - The value is some kind of ASN.1 string; there are far too many + encoding options options, most of which are either strongly + discouraged or outright forbidden by the PKIX profile, but which + persist for historical reasons. The only ones PKIX actually + likes are PrintableString and UTF8String, but there are nuances + and special cases where some of the others are required. + + The RPKI profile further restricts DNs to a single mandatory + CommonName attribute with a single optional SerialNumber attribute + (not to be confused with the certificate serial number). + + BPKI certificates should (we hope) follow the general PKIX guideline + but the ones we construct ourselves are likely to be relatively + simple. + + The main purpose of this class is to hide as much as possible of + this mess from code that has to work with these wretched things. + """ + + def __init__(self, ini = None, **kwargs): + assert ini is None or not kwargs + if len(kwargs) == 1 and "CN" in kwargs: + ini = kwargs.pop("CN") + if isinstance(ini, str): + self.dn = (((rpki.oids.name2oid["commonName"], ("printableString", cn)),),) + elif isinstance(ini, tuple): + self.dn = ini + elif kwargs: + raise NotImplementedError #("Sorry, I haven't implemented keyword arguments yet") + elif ini is not None: + raise TypeError #("Don't know how to interpret %r as an X.501 DN" % (ini,), ini) + + def __str__(self): + return "".join("/" + "+".join("%s=%s" % (rpki.oids.oid2name[a[0]], a[1][1]) + for a in rdn) + for rdn in self.dn) + + def __cmp__(self, other): + return cmp(self.dn, other.dn) + + def get_POWpkix(self): + return self.dn + + def get_POW(self): + raise NotImplementedError #("Sorry, I haven't written the conversion to POW format yet") + class DER_object(object): """ Virtual class to hold a generic DER object. @@ -456,13 +524,13 @@ class X509(DER_object): """ Get the issuer of this certificate. """ - return "".join("/%s=%s" % rdn for rdn in self.get_POW().getIssuer()) + return X501DN(self.get_POWpkix().getIssuer()) def getSubject(self): """ Get the subject of this certificate. """ - return "".join("/%s=%s" % rdn for rdn in self.get_POW().getSubject()) + return X501DN(self.get_POWpkix().getSubject()) def getNotBefore(self): """ @@ -560,25 +628,39 @@ class X509(DER_object): Issue a certificate with values taking from an existing certificate. This is used to construct some kinds oF BPKI certificates. """ + return self.bpki_certify(keypair, source_cert.getSubject(), source_cert.getPublicKey(), + serial, notAfter, now, pathLenConstraint, ca = True) + + def bpki_certify(self, keypair, subject_name, subject_key, serial, notAfter, + now = None, pathLenConstraint = None, ca = False): + """ + Issue a BPKI certificate. + """ if now is None: now = rpki.sundial.now() - assert isinstance(pathLenConstraint, int) and pathLenConstraint >= 0 + assert pathLenConstraint is None or (isinstance(pathLenConstraint, (int, long)) and + pathLenConstraint >= 0) + + extensions = [ + (rpki.oids.name2oid["subjectKeyIdentifier" ], False, subject_key.get_SKI())] + if not ca or self.getSubject() != subject_name or self.getPublicKey() != subject_key: + extensions.append( + (rpki.oids.name2oid["authorityKeyIdentifier"], False, (self.get_SKI(), (), None))) + if ca: + extensions.append( + (rpki.oids.name2oid["basicConstraints" ], True, (1, pathLenConstraint))) cert = rpki.POW.pkix.Certificate() cert.setVersion(2) cert.setSerial(serial) cert.setIssuer(self.get_POWpkix().getSubject()) - cert.setSubject(source_cert.get_POWpkix().getSubject()) + cert.setSubject(subject_name.get_POWpkix()) cert.setNotBefore(now.toASN1tuple()) cert.setNotAfter(notAfter.toASN1tuple()) - cert.tbs.subjectPublicKeyInfo.set( - source_cert.get_POWpkix().tbs.subjectPublicKeyInfo.get()) - cert.setExtensions(( - (rpki.oids.name2oid["subjectKeyIdentifier" ], False, source_cert.get_SKI()), - (rpki.oids.name2oid["authorityKeyIdentifier"], False, (self.get_SKI(), (), None)), - (rpki.oids.name2oid["basicConstraints" ], True, (1, 0)))) + cert.tbs.subjectPublicKeyInfo.fromString(subject_key.get_DER()) + cert.setExtensions(extensions) cert.sign(keypair.get_POW(), rpki.POW.SHA256_DIGEST) return X509(POWpkix = cert) @@ -1357,7 +1439,7 @@ class CRL(DER_object): """ Get issuer value of this CRL. """ - return "".join("/%s=%s" % rdn for rdn in self.get_POW().getIssuer()) + return X501DN(self.get_POWpkix().getIssuer()) @classmethod def generate(cls, keypair, issuer, serial, thisUpdate, nextUpdate, revokedCertificates, version = 1, digestType = "sha256WithRSAEncryption"): diff --git a/scripts/convert-from-entitydb-to-sql.py b/scripts/convert-from-entitydb-to-sql.py index 9de2edf2..c8a85620 100644 --- a/scripts/convert-from-entitydb-to-sql.py +++ b/scripts/convert-from-entitydb-to-sql.py @@ -113,33 +113,30 @@ def read_openssl_serial(filename): return int(text.strip(), 16) def get_or_create_CA(purpose): - cer = rpki.x509.X509(Auto_file = os.path.join(bpki, purpose, "ca.cer")) - key = rpki.x509.RSA(Auto_file = os.path.join(bpki, purpose, "ca.key")) crl = rpki.x509.CRL(Auto_file = os.path.join(bpki, purpose, "ca.crl")) serial = read_openssl_serial(os.path.join(bpki, purpose, "serial")) crl_number = read_openssl_serial(os.path.join(bpki, purpose, "crl_number")) return rpki.irdb.CA.objects.get_or_create(identity = identity, purpose = rpki.irdb.CA.purpose_map[purpose], - certificate = cer.get_DER(), - private_key = key.get_DER(), next_serial = serial, next_crl_number = crl_number, last_crl_update = crl.getThisUpdate().to_sql(), next_crl_update = crl.getNextUpdate().to_sql())[0] -def get_or_create_EECertificate(issuer, purpose): +def get_or_create_KeyedCertificate(issuer, purpose): cer = rpki.x509.X509(Auto_file = os.path.join(bpki, "servers", purpose + ".cer")) key = rpki.x509.RSA(Auto_file = os.path.join(bpki, "servers", purpose + ".key")) - rpki.irdb.EECertificate.objects.get_or_create( + rpki.irdb.KeyedCertificate.objects.get_or_create( issuer = issuer, - purpose = rpki.irdb.EECertificate.purpose_map[purpose], + purpose = rpki.irdb.KeyedCertificate.purpose_map[purpose], certificate = cer.get_DER(), private_key = key.get_DER()) # Load BPKI CA data resource_ca = get_or_create_CA("resources") +get_or_create_KeyedCertificate(resource_ca, "ca") # Load BPKI server EE certificates and keys @@ -148,14 +145,15 @@ run_flags = dict((i, cfg.getboolean(i, section = "myrpki")) if any(run_flags.itervalues()): server_ca = get_or_create_CA("servers") - get_or_create_EECertificate(server_ca, "irbe") + get_or_create_KeyedCertificate(server_ca, "ca") + get_or_create_KeyedCertificate(server_ca, "irbe") if run_flags["run_rpkid"]: - get_or_create_EECertificate(server_ca, "rpkid") - get_or_create_EECertificate(server_ca, "irdbd") + get_or_create_KeyedCertificate(server_ca, "rpkid") + get_or_create_KeyedCertificate(server_ca, "irdbd") if run_flags["run_pubd"]: - get_or_create_EECertificate(server_ca, "pubd") + get_or_create_KeyedCertificate(server_ca, "pubd") if run_flags["run_rootd"]: - get_or_create_EECertificate(server_ca, "rootd") + get_or_create_KeyedCertificate(server_ca, "rootd") else: server_ca = None |