diff options
author | Rob Austein <sra@hactrn.net> | 2011-12-12 21:07:56 +0000 |
---|---|---|
committer | Rob Austein <sra@hactrn.net> | 2011-12-12 21:07:56 +0000 |
commit | 63676e02d7e58487cb0794659de6602168e36e90 (patch) | |
tree | 3c40faec096fe239a14d5a0348a3801523b5bc9c | |
parent | 9151b67621f017e63a046b872dee1008dff6da5a (diff) |
Checkpoint. Custom IRDB model fields to handle automatic type
conversion. Debug last night's rewrite of BPKI certificate issuance.
svn path=/branches/tk100/; revision=4117
-rw-r--r-- | rpkid/rpki/irdb/models.py | 119 | ||||
-rw-r--r-- | rpkid/rpki/x509.py | 77 | ||||
-rw-r--r-- | rpkid/tests/smoketest.py | 7 | ||||
-rw-r--r-- | scripts/convert-from-entitydb-to-sql.py | 31 |
4 files changed, 176 insertions, 58 deletions
diff --git a/rpkid/rpki/irdb/models.py b/rpkid/rpki/irdb/models.py index a9bd364f..3a898d8d 100644 --- a/rpkid/rpki/irdb/models.py +++ b/rpkid/rpki/irdb/models.py @@ -23,6 +23,7 @@ PERFORMANCE OF THIS SOFTWARE. """ import django.db.models +import rpki.x509 ### @@ -49,14 +50,73 @@ class HandleField(django.db.models.CharField): kwargs["max_length"] = 120 django.db.models.CharField.__init__(self, *args, **kwargs) -class BinaryField(django.db.models.Field): +class DERField(django.db.models.Field): """ - A raw binary field type for Django models. Yes, I know this is - wrong, but breaking out all the ASN.1 isn't practical, and encoding - binary data as Base64 text doesn't seem much of an improvement. + A field type for DER objects. + + This is an abstract class, subclasses need to define rpki_type. """ - description = "Raw binary data" + description = "DER object" + + __metaclass__ = django.db.models.SubfieldBase + + def __init__(self, *args, **kwargs): + kwargs["serialize"] = False + kwargs["blank"] = True + 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): + if isinstance(value, self.rpki_type): + return value + else: + assert isinstance(value, str) + return self.rpki_type(DER = value) + + def get_prep_value(self, value): + if isinstance(value, self.rpki_type): + return value.get_DER() + elif isinstance(value, str): + return value + else: + import sys + sys.stderr.write( + "get_prep_value got %r, expected string or %r\n" % (type(value), self.rpki_type)) + assert isinstance(value, (self.rpki_type, str)) + +class CertificateField(DERField): + description = "X.509 certificate" + rpki_type = rpki.x509.X509 + +class RSAKeyField(DERField): + description = "RSA keypair" + rpki_type = rpki.x509.RSA + +class PKCS10Field(DERField): + description = "PKCS #10 certificate request" + rpki_type = rpki.x509.PKCS10 + +class SignedReferralField(django.db.models.Field): + description = "CMS signed object containing XML" + + # This should be another subclass of DERField, but we don't have a + # suitable subclass of XML_CMS_object yet, in part because the XML + # schema we'd need to validate is really just a fragment of another + # schema. Maybe. Anyway, subclassing DERField here doesn't work + # properly yet, so for the moment this is opaque binary data. + # + # Fix later. + + def __init__(self, *args, **kwargs): + kwargs["serialize"] = False + kwargs["blank"] = True + django.db.models.Field.__init__(self, *args, **kwargs) def db_type(self, connection): if connection.settings_dict['ENGINE'] == "django.db.backends.posgresql": @@ -94,14 +154,12 @@ class CA(django.db.models.Model): unique_together = ("identity", "purpose") class Certificate(django.db.models.Model): - certificate = BinaryField() + + certificate = CertificateField() 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 @@ -115,21 +173,30 @@ class Certificate(django.db.models.Model): # to issue the certificate. Seems about right. Not awake enough # to write it now. + # This doesn't handle self-certification yet either. + # Self-certification may be different enough that we want to move + # the certificate and keypair back to the CA object, since we have + # to special-case it no matter what we do. + 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() + cer = cacert.certificate + key = cacert.private_key - result = cer.bpki_certify(key, subject_name, subject_key, ca.next_serial, + result = cer.bpki_certify( + keypair = key, + subject_name = subject_name, + subject_key = subject_key, + serial = ca.next_serial, + + # This needs to be configurable + notAfter = rpki.sundial.now() + rpki.sundial.timedelta(days = 60), - # 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 (at least) per-class, not universal - pathLenConstraint = 0, - - # This is per-class too - ca = True) + # This is per-class too + is_ca = True) self.ca.next_serial += 1 @@ -139,15 +206,14 @@ class Certificate(django.db.models.Model): class CrossCertification(Certificate): handle = HandleField() - ta = BinaryField() + ta = CertificateField() 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() + return self.certificate.getSubject(), self.certificate.getPublicKey() class Revocation(django.db.models.Model): issuer = django.db.models.ForeignKey(CA, related_name = "revocations") @@ -162,18 +228,15 @@ 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() + private_key = RSAKeyField() 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() + pkcs10 = PKCS10Field() class Meta: unique_together = ("issuer", "handle") @@ -209,7 +272,7 @@ class Parent(CrossCertification): 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) + referral_authorization = SignedReferralField(null = True, blank = True) class ROARequest(django.db.models.Model): identity = django.db.models.ForeignKey(Identity, related_name = "roa_requests") diff --git a/rpkid/rpki/x509.py b/rpkid/rpki/x509.py index 2ac9ffca..2011b416 100644 --- a/rpkid/rpki/x509.py +++ b/rpkid/rpki/x509.py @@ -327,6 +327,8 @@ class DER_object(object): return -1 elif other is None: return 1 + elif isinstance(other, str): + return cmp(self.get_DER(), other) else: return cmp(self.get_DER(), other.get_DER()) @@ -623,39 +625,88 @@ class X509(DER_object): return X509(POWpkix = cert) - def cross_certify(self, keypair, source_cert, serial, notAfter, now = None, pathLenConstraint = 0): + def bpki_cross_certify(self, keypair, source_cert, serial, notAfter, + now = None, pathLenConstraint = 0): """ - Issue a certificate with values taking from an existing certificate. - This is used to construct some kinds oF BPKI certificates. + Issue a BPKI certificate with values taking from an existing certificate. """ - return self.bpki_certify(keypair, source_cert.getSubject(), source_cert.getPublicKey(), - serial, notAfter, now, pathLenConstraint, ca = True) + return self.bpki_certify( + keypair = keypair, + subject_name = source_cert.getSubject(), + subject_key = source_cert.getPublicKey(), + serial = serial, + notAfter = notAfter, + now = now, + pathLenConstraint = pathLenConstraint, + is_ca = True) - def bpki_certify(self, keypair, subject_name, subject_key, serial, notAfter, - now = None, pathLenConstraint = None, ca = False): + @classmethod + def bpki_self_certify(cls, keypair, subject_name, serial, notAfter, + now = None, pathLenConstraint = None): + """ + Issue a self-signed BPKI CA certificate. + """ + return cls._bpki_certify( + keypair = keypair, + issuer_name = subject_name, + subject_name = subject_name, + subject_key = keypair.get_RSApublic(), + serial = serial, + now = now, + notAfter = notAfter, + pathLenConstraint = pathLenConstraint, + is_ca = True) + + def bpki_certify(self, keypair, subject_name, subject_key, serial, notAfter, is_ca, + now = None, pathLenConstraint = None): + """ + Issue a normal BPKI certificate. + """ + assert keypair.get_RSApublic() == self.getPublicKey() + return self._bpki_certify( + keypair = keypair, + issuer_name = self.getSubject(), + subject_name = subject_name, + subject_key = subject_key, + serial = serial, + now = now, + notAfter = notAfter, + pathLenConstraint = pathLenConstraint, + is_ca = is_ca) + + @classmethod + def _bpki_certify(cls, keypair, issuer_name, subject_name, subject_key, + serial, now, notAfter, pathLenConstraint, is_ca): """ - Issue a BPKI certificate. + Issue a BPKI certificate. This internal method does the real + work, after one of the wrapper methods has extracted the relevant + fields. """ if now is None: now = rpki.sundial.now() + issuer_key = keypair.get_RSApublic() + + assert (issuer_key == subject_key) == (issuer_name == subject_name) + assert is_ca or issuer_name != subject_name + assert is_ca or pathLenConstraint is None 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: + if issuer_key != subject_key: extensions.append( - (rpki.oids.name2oid["authorityKeyIdentifier"], False, (self.get_SKI(), (), None))) - if ca: + (rpki.oids.name2oid["authorityKeyIdentifier"], False, (issuer_key.get_SKI(), (), None))) + if is_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.setIssuer(issuer_name.get_POWpkix()) cert.setSubject(subject_name.get_POWpkix()) cert.setNotBefore(now.toASN1tuple()) cert.setNotAfter(notAfter.toASN1tuple()) @@ -663,7 +714,7 @@ class X509(DER_object): cert.setExtensions(extensions) cert.sign(keypair.get_POW(), rpki.POW.SHA256_DIGEST) - return X509(POWpkix = cert) + return cls(POWpkix = cert) @classmethod def normalize_chain(cls, chain): diff --git a/rpkid/tests/smoketest.py b/rpkid/tests/smoketest.py index 8145a0eb..189f6d6a 100644 --- a/rpkid/tests/smoketest.py +++ b/rpkid/tests/smoketest.py @@ -868,7 +868,12 @@ class allocation(object): except IOError: serial = 1 - x = parent.cross_certify(keypair, child, serial, notAfter, now) + x = parent.bpki_cross_certify( + keypair = keypair, + source_cert = child, + serial = serial, + notAfter = notAfter, + now = now) f = open(serial_file, "w") f.write("%02x\n" % (serial + 1)) diff --git a/scripts/convert-from-entitydb-to-sql.py b/scripts/convert-from-entitydb-to-sql.py index c8a85620..8885893b 100644 --- a/scripts/convert-from-entitydb-to-sql.py +++ b/scripts/convert-from-entitydb-to-sql.py @@ -130,8 +130,8 @@ def get_or_create_KeyedCertificate(issuer, purpose): rpki.irdb.KeyedCertificate.objects.get_or_create( issuer = issuer, purpose = rpki.irdb.KeyedCertificate.purpose_map[purpose], - certificate = cer.get_DER(), - private_key = key.get_DER()) + certificate = cer, + private_key = key) # Load BPKI CA data @@ -157,16 +157,15 @@ if any(run_flags.itervalues()): else: server_ca = None -# Load BSC certificates and requests +# Load BSC certificates and requests. Yes, this currently wires in +# exactly one BSC handle, "bsc". So does the old myrpki code. Ick. for fn in glob.iglob(os.path.join(bpki, "resources", "bsc.*.cer")): - cer = rpki.x509.X509(Auto_file = fn) - req = rpki.x509.X509(Auto_file = fn[:-4] + ".req") rpki.irdb.BSC.objects.get_or_create( issuer = resource_ca, - certificate = cer.get_DER(), - pkcs10 = req.get_DER()) - + handle = "bsc", + certificate = rpki.x509.X509(Auto_file = fn), + pkcs10 = rpki.x509.PKCS10(Auto_file = fn[:-4] + ".req")) def xcert_hash(cert): """ @@ -243,8 +242,8 @@ for filename in glob.iglob(os.path.join(entitydb, "children", "*.xml")): child = rpki.irdb.Child.objects.get_or_create( handle = child_handle, valid_until = valid_until.to_sql(), - ta = ta.get_DER(), - certificate = xcert.get_DER(), + ta = ta, + certificate = xcert, issuer = resource_ca)[0] cur.execute(""" @@ -294,8 +293,8 @@ for filename in glob.iglob(os.path.join(entitydb, "parents", "*.xml")): handle = parent_handle, parent_handle = e.get("parent_handle"), child_handle = e.get("child_handle"), - ta = ta.get_DER(), - certificate = xcert.get_DER(), + ta = ta, + certificate = xcert, repository_type = rpki.irdb.Parent.repository_type_map[repository_type], referrer = referrer, referral_authorization = referral_authorization, @@ -336,8 +335,8 @@ for filename in glob.iglob(os.path.join(entitydb, "repositories", "*.xml")): rpki.irdb.Repository.objects.get_or_create( handle = repository_handle, client_handle = e.get("client_handle"), - ta = ta.get_DER(), - certificate = xcert.get_DER(), + ta = ta, + certificate = xcert, service_uri = e.get("service_uri"), sia_base = e.get("sia_base"), parent = parent, @@ -361,8 +360,8 @@ for filename in glob.iglob(os.path.join(entitydb, "pubclients", "*.xml")): rpki.irdb.Repository.objects.get_or_create( handle = client_handle, - ta = ta.get_DER(), - certificate = xcert.get_DER(), + ta = ta, + certificate = xcert, issuer = server_ca) # Copy over any ROA requests |