diff --git a/django_x509/admin.py b/django_x509/admin.py
index 40a22e7..fa1a417 100644
--- a/django_x509/admin.py
+++ b/django_x509/admin.py
@@ -1,7 +1,7 @@
from django.contrib import admin
-from .base.admin import AbstractCaAdmin, AbstractCertAdmin
-from .models import Ca, Cert
+from .base.admin import AbstractCaAdmin, AbstractCertAdmin, AbstractUUIDCaAdmin, AbstractUUIDCertAdmin
+from .models import Ca, Cert, UUIDCa, UUIDCert
class CertAdmin(AbstractCertAdmin):
@@ -12,5 +12,15 @@ class CaAdmin(AbstractCaAdmin):
pass
+class UUIDCaAdmin(AbstractUUIDCaAdmin):
+ pass
+
+
+class UUIDCertAdmin(AbstractUUIDCertAdmin):
+ pass
+
+
admin.site.register(Ca, CaAdmin)
admin.site.register(Cert, CertAdmin)
+admin.site.register(UUIDCa, UUIDCaAdmin)
+admin.site.register(UUIDCert, UUIDCertAdmin)
diff --git a/django_x509/base/admin.py b/django_x509/base/admin.py
index a31092e..42382a1 100644
--- a/django_x509/base/admin.py
+++ b/django_x509/base/admin.py
@@ -97,6 +97,33 @@ class Media:
js = ('django-x509/js/x509-admin.js',)
+class AbstractUUIDCaAdmin(BaseAdmin):
+ list_filter = ['key_length', 'digest', 'created']
+ fields = ['operation_type',
+ 'name',
+ 'notes',
+ 'key_length',
+ 'digest',
+ 'validity_start',
+ 'validity_end',
+ 'country_code',
+ 'state',
+ 'city',
+ 'organization_name',
+ 'organizational_unit_name',
+ 'email',
+ 'common_name',
+ 'extensions',
+ 'serial_number',
+ 'certificate',
+ 'private_key',
+ 'created',
+ 'modified']
+
+ class Media:
+ js = ('django-x509/js/x509-admin.js',)
+
+
class AbstractCertAdmin(BaseAdmin):
list_filter = ['ca', 'revoked', 'key_length', 'digest', 'created']
list_select_related = ['ca']
@@ -152,6 +179,61 @@ def revoke_action(self, request, queryset):
revoke_action.short_description = _('Revoke selected certificates')
+class AbstractUUIDCertAdmin(BaseAdmin):
+ list_filter = ['ca', 'revoked', 'key_length', 'digest', 'created']
+ list_select_related = ['ca']
+ readonly_fields = ['revoked', 'revoked_at']
+ fields = ['operation_type',
+ 'name',
+ 'ca',
+ 'notes',
+ 'revoked',
+ 'revoked_at',
+ 'key_length',
+ 'digest',
+ 'validity_start',
+ 'validity_end',
+ 'country_code',
+ 'state',
+ 'city',
+ 'organization_name',
+ 'organizational_unit_name',
+ 'email',
+ 'common_name',
+ 'extensions',
+ 'serial_number',
+ 'certificate',
+ 'private_key',
+ 'created',
+ 'modified']
+ actions = ['revoke_action']
+
+ class Media:
+ js = ('django-x509/js/x509-admin.js',)
+
+ def ca_url(self, obj):
+ url = reverse('admin:{0}_ca_change'.format(self.opts.app_label), args=[obj.ca.id])
+ return format_html("{text}",
+ url=url,
+ text=obj.ca.name)
+ ca_url.short_description = 'CA'
+
+ def revoke_action(self, request, queryset):
+ rows = 0
+ for cert in queryset:
+ cert.revoke()
+ rows += 1
+ if rows == 1:
+ bit = '1 certificate was'
+ else:
+ bit = '{0} certificates were'.format(rows)
+ message = '{0} revoked.'.format(bit)
+ self.message_user(request, _(message))
+
+ revoke_action.short_description = _('Revoke selected certificates')
+
+
+
# For backward compatibility
CaAdmin = AbstractCaAdmin
CertAdmin = AbstractCertAdmin
@@ -161,3 +243,13 @@ def revoke_action(self, request, queryset):
AbstractCertAdmin.list_display.insert(5, 'revoked')
AbstractCertAdmin.readonly_edit = BaseAdmin.readonly_edit[:]
AbstractCertAdmin.readonly_edit += ('ca',)
+
+# Same UUID Classes
+CaAdmin = AbstractUUIDCaAdmin
+CertAdmin = AbstractUUIDCertAdmin
+
+AbstractUUIDCertAdmin.list_display = BaseAdmin.list_display[:]
+AbstractUUIDCertAdmin.list_display.insert(1, 'ca_url')
+AbstractUUIDCertAdmin.list_display.insert(5, 'revoked')
+AbstractUUIDCertAdmin.readonly_edit = BaseAdmin.readonly_edit[:]
+AbstractUUIDCertAdmin.readonly_edit += ('ca',)
diff --git a/django_x509/base/models.py b/django_x509/base/models.py
index 3eac4cf..fd3bc0b 100644
--- a/django_x509/base/models.py
+++ b/django_x509/base/models.py
@@ -88,7 +88,8 @@ def default_key_length():
def default_digest_algorithm():
"""
returns default value for digest field
- (this avoids to set the exact default value in the database migration)
+ (this avoids to set the exact default
+ value in the database migration)
"""
return app_settings.DEFAULT_DIGEST_ALGORITHM
@@ -121,7 +122,8 @@ class BaseX509(models.Model):
country_code = models.CharField(max_length=2, blank=True)
state = models.CharField(_('state or province'), max_length=64, blank=True)
city = models.CharField(_('city'), max_length=64, blank=True)
- organization_name = models.CharField(_('organization'), max_length=64, blank=True)
+ organization_name = models.CharField(
+ _('organization'), max_length=64, blank=True)
organizational_unit_name = models.CharField(_('organizational unit name'),
max_length=64, blank=True)
email = models.EmailField(_('email address'), blank=True)
@@ -129,18 +131,23 @@ class BaseX509(models.Model):
extensions = JSONField(_('extensions'),
default=list,
blank=True,
- help_text=_('additional x509 certificate extensions'),
- load_kwargs={'object_pairs_hook': collections.OrderedDict},
+ help_text=_(
+ 'additional x509 certificate extensions'),
+ load_kwargs={
+ 'object_pairs_hook': collections.OrderedDict},
dump_kwargs={'indent': 4})
# serial_number is set to CharField as a UUID integer is too big for a
# PositiveIntegerField and an IntegerField on SQLite
serial_number = models.CharField(_('serial number'),
- help_text=_('leave blank to determine automatically'),
+ help_text=_(
+ 'leave blank to determine automatically'),
blank=True,
null=True,
max_length=39)
- certificate = models.TextField(blank=True, help_text='certificate in X.509 PEM format')
- private_key = models.TextField(blank=True, help_text='private key in X.509 PEM format')
+ certificate = models.TextField(
+ blank=True, help_text='certificate in X.509 PEM format')
+ private_key = models.TextField(
+ blank=True, help_text='private key in X.509 PEM format')
created = AutoCreatedField(_('created'), editable=True)
modified = AutoLastModifiedField(_('modified'), editable=True)
passphrase = models.CharField(max_length=64,
@@ -228,10 +235,12 @@ def _validate_pem(self):
args = (crypto.FILETYPE_PEM, getattr(self, field))
kwargs = {}
if method_name == 'load_privatekey':
- kwargs['passphrase'] = getattr(self, 'passphrase').encode('utf8')
+ kwargs['passphrase'] = getattr(
+ self, 'passphrase').encode('utf8')
load_pem(*args, **kwargs)
except OpenSSL.crypto.Error as e:
- errors[field] = ValidationError(_('OpenSSL error: {0}'.format(e.args[0])))
+ errors[field] = ValidationError(
+ _('OpenSSL error: {0}'.format(e.args[0])))
if errors:
raise ValidationError(errors)
@@ -243,7 +252,8 @@ def _validate_serial_number(self):
try:
int(self.serial_number)
except ValueError:
- raise ValidationError({'serial_number': _('Serial number must be an integer')})
+ raise ValidationError(
+ {'serial_number': _('Serial number must be an integer')})
def _generate(self):
"""
@@ -257,8 +267,10 @@ def _generate(self):
cert.set_version(0x2) # version 3 (0 indexed counting)
cert.set_subject(subject)
cert.set_serial_number(int(self.serial_number))
- cert.set_notBefore(bytes_compat(self.validity_start.strftime(generalized_time)))
- cert.set_notAfter(bytes_compat(self.validity_end.strftime(generalized_time)))
+ cert.set_notBefore(bytes_compat(
+ self.validity_start.strftime(generalized_time)))
+ cert.set_notAfter(bytes_compat(
+ self.validity_end.strftime(generalized_time)))
# generating certificate for CA
if not hasattr(self, 'ca'):
issuer = cert.get_subject()
@@ -271,13 +283,10 @@ def _generate(self):
cert.set_pubkey(key)
cert = self._add_extensions(cert)
cert.sign(issuer_key, str(self.digest))
- self.certificate = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8")
- key_args = (crypto.FILETYPE_PEM, key)
- key_kwargs = {}
- if self.passphrase:
- key_kwargs['passphrase'] = self.passphrase.encode('utf-8')
- key_kwargs['cipher'] = 'DES-EDE3-CBC'
- self.private_key = crypto.dump_privatekey(*key_args, **key_kwargs).decode("utf-8")
+ self.certificate = crypto.dump_certificate(
+ crypto.FILETYPE_PEM, cert).decode("utf-8")
+ self.private_key = crypto.dump_privatekey(
+ crypto.FILETYPE_PEM, key).decode("utf-8")
def _fill_subject(self, subject):
"""
@@ -421,6 +430,99 @@ def _add_extensions(self, cert):
return cert
+class BaseUUID(BaseX509):
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+
+ def save(self, *args, **kwargs):
+ generate = False
+ if not self.certificate and not self.private_key:
+ generate = True
+ super(BaseUUID, self).save(*args, **kwargs)
+ if generate:
+ # automatically determine serial number
+ if not self.serial_number:
+ self.serial_number = uuid.uuid4().int
+ self._generate()
+ kwargs['force_insert'] = False
+ super(BaseUUID, self).save(*args, **kwargs)
+
+ class Meta:
+ abstract = True
+
+
+class AbstractUUIDCa(BaseUUID):
+ """
+ Abstract UUID Ca model
+ """
+ class Meta:
+ abstract = True
+ verbose_name = _('CA')
+ verbose_name_plural = _('CAs')
+
+ def get_revoked_certs(self):
+ """
+ Returns revoked certificates of this CA
+ (does not include expired certificates)
+ """
+ now = timezone.now()
+ return self.uuidcert_set.filter(revoked=True,
+ validity_start__lte=now,
+ validity_end__gte=now)
+
+ @property
+ def crl(self):
+ """
+ Returns up to date CRL of this CA
+ """
+ revoked_certs = self.get_revoked_certs()
+ crl = crypto.CRL()
+ now_str = timezone.now().strftime(generalized_time)
+ for cert in revoked_certs:
+ revoked = crypto.Revoked()
+ revoked.set_serial(bytes_compat(cert.serial_number))
+ revoked.set_reason(b'unspecified')
+ revoked.set_rev_date(bytes_compat(now_str))
+ crl.add_revoked(revoked)
+ return crl.export(self.x509, self.pkey, days=1, digest=b'sha256')
+
+
+AbstractUUIDCa._meta.get_field(
+ 'validity_end').default = default_ca_validity_end
+
+
+class AbstractUUIDCert(BaseUUID):
+ """
+ Abstract UUID Cert model
+ """
+ ca = models.ForeignKey('django_x509.UUIDCa',
+ on_delete=models.CASCADE, verbose_name=_('CA'))
+ revoked = models.BooleanField(_('revoked'),
+ default=False)
+ revoked_at = models.DateTimeField(_('revoked at'),
+ blank=True,
+ null=True,
+ default=None)
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ abstract = True
+ verbose_name = _('certificate')
+ verbose_name_plural = _('certificates')
+ unique_together = ('ca', 'serial_number')
+
+ def revoke(self):
+ """
+ * flag certificate as revoked
+ * fill in revoked_at DateTimeField
+ """
+ now = timezone.now()
+ self.revoked = True
+ self.revoked_at = now
+ self.save()
+
+
class AbstractCa(BaseX509):
"""
Abstract Ca model
@@ -464,7 +566,11 @@ class AbstractCert(BaseX509):
"""
Abstract Cert model
"""
- ca = models.ForeignKey('django_x509.Ca', on_delete=models.CASCADE, verbose_name=_('CA'))
+ ca = models.ForeignKey(
+ 'django_x509.Ca',
+ on_delete=models.CASCADE,
+ verbose_name=_('CA')
+ )
revoked = models.BooleanField(_('revoked'),
default=False)
revoked_at = models.DateTimeField(_('revoked at'),
diff --git a/django_x509/base/views.py b/django_x509/base/views.py
index ea69594..460defe 100644
--- a/django_x509/base/views.py
+++ b/django_x509/base/views.py
@@ -1,20 +1,35 @@
from django.http import HttpResponse
from django.utils.translation import ugettext_lazy as _
-
from .. import settings as app_settings
+import uuid
-def crl(request, pk):
- """
- returns CRL of a CA
- """
+
+def crl_common(request, pk, model):
authenticated = request.user.is_authenticated
authenticated = authenticated() if callable(authenticated) else authenticated
if app_settings.CRL_PROTECTED and not authenticated:
return HttpResponse(_('Forbidden'),
status=403,
content_type='text/plain')
- ca = crl.ca_model.objects.get(pk=pk)
+ ca = model.objects.get(pk=pk)
return HttpResponse(ca.crl,
status=200,
content_type='application/x-pem-file')
+
+
+def crl(request, pk):
+ """
+ returns CRL of a CA
+ """
+ model = crl.ca_model
+ return crl_common(request, pk, model)
+
+
+def uuidcrl(request, pk):
+ """
+ returns CRL of a UUID_CA
+ """
+ pk = uuid.UUID(pk)
+ model = uuidcrl.ca_model
+ return crl_common(request, pk, model)
diff --git a/django_x509/migrations/0006_auto_20180826_2044.py b/django_x509/migrations/0006_auto_20180826_2044.py
new file mode 100644
index 0000000..0200d6e
--- /dev/null
+++ b/django_x509/migrations/0006_auto_20180826_2044.py
@@ -0,0 +1,86 @@
+# Generated by Django 2.1 on 2018-08-26 18:44
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import django_x509.base.models
+import jsonfield.fields
+import model_utils.fields
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_x509', '0005_organizational_unit_name'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UUIDCa',
+ fields=[
+ ('name', models.CharField(max_length=64)),
+ ('notes', models.TextField(blank=True)),
+ ('key_length', models.CharField(blank=True, choices=[('', ''), ('512', '512'), ('1024', '1024'), ('2048', '2048'), ('4096', '4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length')),
+ ('digest', models.CharField(blank=True, choices=[('', ''), ('sha1', 'SHA1'), ('sha224', 'SHA224'), ('sha256', 'SHA256'), ('sha384', 'SHA384'), ('sha512', 'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm')),
+ ('validity_start', models.DateTimeField(blank=True, default=django_x509.base.models.default_validity_start, null=True)),
+ ('validity_end', models.DateTimeField(blank=True, default=django_x509.base.models.default_ca_validity_end, null=True)),
+ ('country_code', models.CharField(blank=True, max_length=2)),
+ ('state', models.CharField(blank=True, max_length=64, verbose_name='state or province')),
+ ('city', models.CharField(blank=True, max_length=64, verbose_name='city')),
+ ('organization_name', models.CharField(blank=True, max_length=64, verbose_name='organization')),
+ ('organizational_unit_name', models.CharField(blank=True, max_length=64, verbose_name='organizational unit name')),
+ ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
+ ('common_name', models.CharField(blank=True, max_length=63, verbose_name='common name')),
+ ('extensions', jsonfield.fields.JSONField(blank=True, default=list, help_text='additional x509 certificate extensions', verbose_name='extensions')),
+ ('serial_number', models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=39, null=True, verbose_name='serial number')),
+ ('certificate', models.TextField(blank=True, help_text='certificate in X.509 PEM format')),
+ ('private_key', models.TextField(blank=True, help_text='private key in X.509 PEM format')),
+ ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
+ ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ],
+ options={
+ 'verbose_name': 'CA',
+ 'verbose_name_plural': 'CAs',
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='UUIDCert',
+ fields=[
+ ('name', models.CharField(max_length=64)),
+ ('notes', models.TextField(blank=True)),
+ ('key_length', models.CharField(blank=True, choices=[('', ''), ('512', '512'), ('1024', '1024'), ('2048', '2048'), ('4096', '4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length')),
+ ('digest', models.CharField(blank=True, choices=[('', ''), ('sha1', 'SHA1'), ('sha224', 'SHA224'), ('sha256', 'SHA256'), ('sha384', 'SHA384'), ('sha512', 'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm')),
+ ('validity_start', models.DateTimeField(blank=True, default=django_x509.base.models.default_validity_start, null=True)),
+ ('validity_end', models.DateTimeField(blank=True, default=django_x509.base.models.default_cert_validity_end, null=True)),
+ ('country_code', models.CharField(blank=True, max_length=2)),
+ ('state', models.CharField(blank=True, max_length=64, verbose_name='state or province')),
+ ('city', models.CharField(blank=True, max_length=64, verbose_name='city')),
+ ('organization_name', models.CharField(blank=True, max_length=64, verbose_name='organization')),
+ ('organizational_unit_name', models.CharField(blank=True, max_length=64, verbose_name='organizational unit name')),
+ ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
+ ('common_name', models.CharField(blank=True, max_length=63, verbose_name='common name')),
+ ('extensions', jsonfield.fields.JSONField(blank=True, default=list, help_text='additional x509 certificate extensions', verbose_name='extensions')),
+ ('serial_number', models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=39, null=True, verbose_name='serial number')),
+ ('certificate', models.TextField(blank=True, help_text='certificate in X.509 PEM format')),
+ ('private_key', models.TextField(blank=True, help_text='private key in X.509 PEM format')),
+ ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
+ ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('revoked', models.BooleanField(default=False, verbose_name='revoked')),
+ ('revoked_at', models.DateTimeField(blank=True, default=None, null=True, verbose_name='revoked at')),
+ ('ca', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_x509.UUIDCa', verbose_name='CA')),
+ ],
+ options={
+ 'verbose_name': 'certificate',
+ 'verbose_name_plural': 'certificates',
+ 'abstract': False,
+ },
+ ),
+ migrations.AlterUniqueTogether(
+ name='uuidcert',
+ unique_together={('ca', 'serial_number')},
+ ),
+ ]
diff --git a/django_x509/models.py b/django_x509/models.py
index bb487b7..abf03d4 100644
--- a/django_x509/models.py
+++ b/django_x509/models.py
@@ -1,4 +1,4 @@
-from .base.models import AbstractCa, AbstractCert
+from .base.models import AbstractCa, AbstractCert, AbstractUUIDCa, AbstractUUIDCert
class Ca(AbstractCa):
@@ -15,3 +15,18 @@ class Cert(AbstractCert):
"""
class Meta(AbstractCert.Meta):
abstract = False
+
+class UUIDCa(AbstractUUIDCa):
+ """
+ Concrete UUID Ca model
+ """
+ class Meta(AbstractUUIDCa.Meta):
+ abstract = False
+
+
+class UUIDCert(AbstractUUIDCert):
+ """
+ Concrete UUID Cert model
+ """
+ class Meta(AbstractUUIDCert.Meta):
+ abstract = False
diff --git a/django_x509/templates/admin/django_x509/change_form.html b/django_x509/templates/admin/django_x509/change_form.html
index d168fe7..f524680 100644
--- a/django_x509/templates/admin/django_x509/change_form.html
+++ b/django_x509/templates/admin/django_x509/change_form.html
@@ -9,6 +9,13 @@
{% endif %}
+{% if opts.model_name == 'uuidca' %}
+
+
+ {% trans "Download CRL" %}
+
+
+{% endif %}
{{ block.super }}
{% endblock %}
diff --git a/django_x509/tests/test_uuidca.py b/django_x509/tests/test_uuidca.py
new file mode 100644
index 0000000..596dbb3
--- /dev/null
+++ b/django_x509/tests/test_uuidca.py
@@ -0,0 +1,501 @@
+from datetime import datetime, timedelta
+
+from django.core.exceptions import ValidationError
+from django.test import TestCase
+from django.urls import reverse
+from django.utils import timezone
+from OpenSSL import crypto
+
+from . import TestX509Mixin
+from .. import settings as app_settings
+from ..base.models import generalized_time
+from ..models import UUIDCa, UUIDCert
+
+import uuid
+
+class TestCa(TestX509Mixin, TestCase):
+ """
+ tests for Ca model
+ """
+ ca_model = UUIDCa
+ cert_model = UUIDCert
+
+ def _prepare_revoked(self):
+ ca = self._create_ca()
+ crl = crypto.load_crl(crypto.FILETYPE_PEM, ca.crl)
+ self.assertIsNone(crl.get_revoked())
+ cert = self._create_cert(ca=ca)
+ cert.revoke()
+ return (ca, cert)
+
+ import_certificate = """
+-----BEGIN CERTIFICATE-----
+MIIB4zCCAY2gAwIBAwIDAeJAMA0GCSqGSIb3DQEBBQUAMHcxCzAJBgNVBAYTAlVT
+MQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwE
+QUNNRTEfMB0GCSqGSIb3DQEJARYQY29udGFjdEBhY21lLmNvbTETMBEGA1UEAwwK
+aW1wb3J0dGVzdDAiGA8yMDE1MDEwMTAwMDAwMFoYDzIwMjAwMTAxMDAwMDAwWjB3
+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz
+Y28xDTALBgNVBAoMBEFDTUUxHzAdBgkqhkiG9w0BCQEWEGNvbnRhY3RAYWNtZS5j
+b20xEzARBgNVBAMMCmltcG9ydHRlc3QwXDANBgkqhkiG9w0BAQEFAANLADBIAkEA
+v42Y9u9pYUiFRb36lwqdLmG8hCjl0g0HlMo2WqvHCTLk2CJvprBEuggSnaRCAmG9
+ipCIds/ggaJ/w4KqJabNQQIDAQABMA0GCSqGSIb3DQEBBQUAA0EAAfEPPqbY1TLw
+6IXNVelAXKxUp2f8FYCnlb0pQ3tswvefpad3h3oHrI2RGkIsM70axo7dAEk05Tj0
+Zt3jXRLGAQ==
+-----END CERTIFICATE-----
+"""
+ import_private_key = """
+-----BEGIN PRIVATE KEY-----
+MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAv42Y9u9pYUiFRb36
+lwqdLmG8hCjl0g0HlMo2WqvHCTLk2CJvprBEuggSnaRCAmG9ipCIds/ggaJ/w4Kq
+JabNQQIDAQABAkEAqpB3CEqeVxWwNi24GQ5Gb6pvpm6UVblsary0MYCLtk+jK6fg
+KCptUIryQ4cblZF54y3+wrLzJ9LUOStkk10DwQIhAPItbg5PqSZTCE/Ql20jUggo
+BHpXO7FI157oMxXnBJtVAiEAynx4ocYpgVtmJ9iSooZRtPp9ullEdUtU2pedSgY6
+oj0CIHtcBs6FZ20dKIO3hhrSvgtnjvhejQp+R08rijIi7ibNAiBUOhR/zosjSN6k
+gnz0aAUC0BOOeWV1mQFR8DE4QoEPTQIhAIdGrho1hsZ3Cs7mInJiLLhh4zwnndQx
+WRyKPvMvJzWT
+-----END PRIVATE KEY-----
+"""
+
+ def test_new(self):
+ ca = self._create_ca()
+ self.assertNotEqual(ca.certificate, '')
+ self.assertNotEqual(ca.private_key, '')
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca.certificate)
+ self.assertEqual(int(cert.get_serial_number()), int(ca.serial_number))
+ subject = cert.get_subject()
+ self.assertEqual(subject.countryName, ca.country_code)
+ self.assertEqual(subject.stateOrProvinceName, ca.state)
+ self.assertEqual(subject.localityName, ca.city)
+ self.assertEqual(subject.organizationName, ca.organization_name)
+ self.assertEqual(subject.emailAddress, ca.email)
+ self.assertEqual(subject.commonName, ca.common_name)
+ issuer = cert.get_issuer()
+ self.assertEqual(issuer.countryName, ca.country_code)
+ self.assertEqual(issuer.stateOrProvinceName, ca.state)
+ self.assertEqual(issuer.localityName, ca.city)
+ self.assertEqual(issuer.organizationName, ca.organization_name)
+ self.assertEqual(issuer.emailAddress, ca.email)
+ self.assertEqual(issuer.commonName, ca.common_name)
+ # ensure version is 3
+ self.assertEqual(cert.get_version(), 2)
+ # basic constraints
+ e = cert.get_extension(0)
+ self.assertEqual(e.get_critical(), 1)
+ self.assertEqual(e.get_short_name().decode(), 'basicConstraints')
+ self.assertEqual(e.get_data(), b'0\x06\x01\x01\xff\x02\x01\x00')
+
+ def test_x509_property(self):
+ ca = self._create_ca()
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca.certificate)
+ self.assertEqual(ca.x509.get_subject(), cert.get_subject())
+ self.assertEqual(ca.x509.get_issuer(), cert.get_issuer())
+
+ def test_x509_property_none(self):
+ self.assertIsNone(UUIDCa().x509)
+
+ def test_pkey_property(self):
+ ca = self._create_ca()
+ self.assertIsInstance(ca.pkey, crypto.PKey)
+
+ def test_pkey_property_none(self):
+ self.assertIsNone(UUIDCa().pkey)
+
+ def test_default_validity_end(self):
+ ca = UUIDCa()
+ self.assertEqual(ca.validity_end.year, datetime.now().year + 10)
+
+ def test_default_validity_start(self):
+ ca = UUIDCa()
+ expected = datetime.now() - timedelta(days=1)
+ self.assertEqual(ca.validity_start.year, expected.year)
+ self.assertEqual(ca.validity_start.month, expected.month)
+ self.assertEqual(ca.validity_start.day, expected.day)
+ self.assertEqual(ca.validity_start.hour, 0)
+ self.assertEqual(ca.validity_start.minute, 0)
+ self.assertEqual(ca.validity_start.second, 0)
+
+ def test_import_ca(self):
+ ca = UUIDCa(name='ImportTest')
+ ca.certificate = self.import_certificate
+ ca.private_key = self.import_private_key
+ ca.full_clean()
+ ca.save()
+ cert = ca.x509
+ # verify attributes
+ self.assertEqual(cert.get_serial_number(), 123456)
+ subject = cert.get_subject()
+ self.assertEqual(subject.countryName, 'US')
+ self.assertEqual(subject.stateOrProvinceName, 'CA')
+ self.assertEqual(subject.localityName, 'San Francisco')
+ self.assertEqual(subject.organizationName, 'ACME')
+ self.assertEqual(subject.emailAddress, 'contact@acme.com')
+ self.assertEqual(subject.commonName, 'importtest')
+ issuer = cert.get_issuer()
+ self.assertEqual(issuer.countryName, 'US')
+ self.assertEqual(issuer.stateOrProvinceName, 'CA')
+ self.assertEqual(issuer.localityName, 'San Francisco')
+ self.assertEqual(issuer.organizationName, 'ACME')
+ self.assertEqual(issuer.emailAddress, 'contact@acme.com')
+ self.assertEqual(issuer.commonName, 'importtest')
+ # verify field attribtues
+ self.assertEqual(ca.key_length, '512')
+ self.assertEqual(ca.digest, 'sha1')
+ start = timezone.make_aware(datetime.strptime('20150101000000Z', generalized_time))
+ self.assertEqual(ca.validity_start, start)
+ end = timezone.make_aware(datetime.strptime('20200101000000Z', generalized_time))
+ self.assertEqual(ca.validity_end, end)
+ self.assertEqual(ca.country_code, 'US')
+ self.assertEqual(ca.state, 'CA')
+ self.assertEqual(ca.city, 'San Francisco')
+ self.assertEqual(ca.organization_name, 'ACME')
+ self.assertEqual(ca.email, 'contact@acme.com')
+ self.assertEqual(ca.common_name, 'importtest')
+ self.assertEqual(ca.name, 'ImportTest')
+ self.assertEqual(int(ca.serial_number), 123456)
+ # ensure version is 3
+ self.assertEqual(cert.get_version(), 3)
+ ca.delete()
+ # test auto name
+ ca = UUIDCa(certificate=self.import_certificate,
+ private_key=self.import_private_key)
+ ca.full_clean()
+ ca.save()
+ self.assertEqual(ca.name, 'importtest')
+
+ def test_import_private_key_empty(self):
+ ca = UUIDCa(name='ImportTest')
+ ca.certificate = self.import_certificate
+ try:
+ ca.full_clean()
+ except ValidationError as e:
+ # verify error message
+ self.assertIn('importing an existing certificate', str(e))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_basic_constraints_not_critical(self):
+ setattr(app_settings, 'CA_BASIC_CONSTRAINTS_CRITICAL', False)
+ ca = self._create_ca()
+ e = ca.x509.get_extension(0)
+ self.assertEqual(e.get_critical(), 0)
+ setattr(app_settings, 'CA_BASIC_CONSTRAINTS_CRITICAL', True)
+
+ def test_basic_constraints_pathlen(self):
+ setattr(app_settings, 'CA_BASIC_CONSTRAINTS_PATHLEN', 2)
+ ca = self._create_ca()
+ e = ca.x509.get_extension(0)
+ self.assertEqual(e.get_data(), b'0\x06\x01\x01\xff\x02\x01\x02')
+ setattr(app_settings, 'CA_BASIC_CONSTRAINTS_PATHLEN', 0)
+
+ def test_basic_constraints_pathlen_none(self):
+ setattr(app_settings, 'CA_BASIC_CONSTRAINTS_PATHLEN', None)
+ ca = self._create_ca()
+ e = ca.x509.get_extension(0)
+ self.assertEqual(e.get_data(), b'0\x03\x01\x01\xff')
+ setattr(app_settings, 'CA_BASIC_CONSTRAINTS_PATHLEN', 0)
+
+ def test_keyusage(self):
+ ca = self._create_ca()
+ e = ca.x509.get_extension(1)
+ self.assertEqual(e.get_short_name().decode(), 'keyUsage')
+ self.assertEqual(e.get_critical(), True)
+ self.assertEqual(e.get_data(), b'\x03\x02\x01\x06')
+
+ def test_keyusage_not_critical(self):
+ setattr(app_settings, 'CA_KEYUSAGE_CRITICAL', False)
+ ca = self._create_ca()
+ e = ca.x509.get_extension(1)
+ self.assertEqual(e.get_short_name().decode(), 'keyUsage')
+ self.assertEqual(e.get_critical(), False)
+ setattr(app_settings, 'CA_KEYUSAGE_CRITICAL', True)
+
+ def test_keyusage_value(self):
+ setattr(app_settings, 'CA_KEYUSAGE_VALUE', 'cRLSign, keyCertSign, keyAgreement')
+ ca = self._create_ca()
+ e = ca.x509.get_extension(1)
+ self.assertEqual(e.get_short_name().decode(), 'keyUsage')
+ self.assertEqual(e.get_data(), b'\x03\x02\x01\x0e')
+ setattr(app_settings, 'CA_KEYUSAGE_VALUE', 'cRLSign, keyCertSign')
+
+ def test_subject_key_identifier(self):
+ ca = self._create_ca()
+ e = ca.x509.get_extension(2)
+ self.assertEqual(e.get_short_name().decode(), 'subjectKeyIdentifier')
+ self.assertEqual(e.get_critical(), False)
+ e2 = crypto.X509Extension(b'subjectKeyIdentifier',
+ False,
+ b'hash',
+ subject=ca.x509)
+ self.assertEqual(e.get_data(), e2.get_data())
+
+ def test_authority_key_identifier(self):
+ ca = self._create_ca()
+ e = ca.x509.get_extension(3)
+ self.assertEqual(e.get_short_name().decode(), 'authorityKeyIdentifier')
+ self.assertEqual(e.get_critical(), False)
+ e2 = crypto.X509Extension(b'authorityKeyIdentifier',
+ False,
+ b'keyid:always,issuer:always',
+ issuer=ca.x509)
+ self.assertEqual(e.get_data(), e2.get_data())
+
+ def test_extensions(self):
+ extensions = [
+ {
+ "name": "nsComment",
+ "critical": False,
+ "value": "CA - autogenerated Certificate"
+ }
+ ]
+ ca = self._create_ca(extensions=extensions)
+ e1 = ca.x509.get_extension(4)
+ self.assertEqual(e1.get_short_name().decode(), 'nsComment')
+ self.assertEqual(e1.get_critical(), False)
+ self.assertEqual(e1.get_data(), b'\x16\x1eCA - autogenerated Certificate')
+
+ def test_extensions_error1(self):
+ extensions = {}
+ try:
+ self._create_ca(extensions=extensions)
+ except ValidationError as e:
+ # verify error message
+ self.assertIn('Extension format invalid', str(e.message_dict['__all__'][0]))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_extensions_error2(self):
+ extensions = [{"wrong": "wrong"}]
+ try:
+ self._create_ca(extensions=extensions)
+ except ValidationError as e:
+ # verify error message
+ self.assertIn('Extension format invalid', str(e.message_dict['__all__'][0]))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_get_revoked_certs(self):
+ ca = self._create_ca()
+ c1 = self._create_cert(ca=ca)
+ c2 = self._create_cert(ca=ca)
+ c3 = self._create_cert(ca=ca) # noqa
+ self.assertEqual(ca.get_revoked_certs().count(), 0)
+ c1.revoke()
+ self.assertEqual(ca.get_revoked_certs().count(), 1)
+ c2.revoke()
+ self.assertEqual(ca.get_revoked_certs().count(), 2)
+ now = timezone.now()
+ # expired certificates are not counted
+ start = now - timedelta(days=6650)
+ end = now - timedelta(days=6600)
+ c4 = self._create_cert(ca=ca,
+ validity_start=start,
+ validity_end=end)
+ c4.revoke()
+ self.assertEqual(ca.get_revoked_certs().count(), 2)
+ # inactive not counted yet
+ start = now + timedelta(days=2)
+ end = now + timedelta(days=365)
+ c5 = self._create_cert(ca=ca,
+ validity_start=start,
+ validity_end=end)
+ c5.revoke()
+ self.assertEqual(ca.get_revoked_certs().count(), 2)
+
+ def test_crl(self):
+ ca, cert = self._prepare_revoked()
+ crl = crypto.load_crl(crypto.FILETYPE_PEM, ca.crl)
+ revoked_list = crl.get_revoked()
+ self.assertIsNotNone(revoked_list)
+ self.assertEqual(len(revoked_list), 1)
+ self.assertEqual(int(revoked_list[0].get_serial()), cert.serial_number)
+
+ def test_crl_view(self):
+ ca, cert = self._prepare_revoked()
+ response = self.client.get(reverse('x509:uuidcrl', args=[ca.pk]))
+ self.assertEqual(response.status_code, 200)
+ crl = crypto.load_crl(crypto.FILETYPE_PEM, response.content)
+ revoked_list = crl.get_revoked()
+ self.assertIsNotNone(revoked_list)
+ self.assertEqual(len(revoked_list), 1)
+ self.assertEqual(int(revoked_list[0].get_serial()), cert.serial_number)
+
+ def test_crl_view_403(self):
+ setattr(app_settings, 'CRL_PROTECTED', True)
+ ca, cert = self._prepare_revoked()
+ response = self.client.get(reverse('x509:uuidcrl', args=[ca.pk]))
+ self.assertEqual(response.status_code, 403)
+ setattr(app_settings, 'CRL_PROTECTED', False)
+
+ def test_x509_text(self):
+ ca = self._create_ca()
+ text = crypto.dump_certificate(crypto.FILETYPE_TEXT, ca.x509)
+ self.assertEqual(ca.x509_text, text.decode('utf-8'))
+
+ def test_x509_import_exception_fixed(self):
+ certificate = """-----BEGIN CERTIFICATE-----
+MIIEBTCCAu2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBRMQswCQYDVQQGEwJJVDEL
+MAkGA1UECAwCUk0xDTALBgNVBAcMBFJvbWExDzANBgNVBAoMBkNpbmVjYTEVMBMG
+A1UEAwwMUHJvdmEgQ2luZWNhMB4XDTE2MDkyMTA5MDQyOFoXDTM2MDkyMTA5MDQy
+OFowUTELMAkGA1UEBhMCSVQxCzAJBgNVBAgMAlJNMQ0wCwYDVQQHDARSb21hMQ8w
+DQYDVQQKDAZDaW5lY2ExFTATBgNVBAMMDFByb3ZhIENpbmVjYTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAMV26pysBdm3OqhyyZjbWZ3ThmH6QTIDScTj
++1y3nGgnIwgpHWJmZiO/XrwYburLttE+NP7qwgtRcVoxTJFnhuunSei8vE9lyooD
+l1wRUU0qMZSWB/Q3OF+S+FhRMtymx+H6a46yC5Wqxk0apNlvAJ1avuBtZjvipQHS
+Z3ub5iHpHr0LZKYbqq2yXna6SbGUjnGjVieIXTilbi/9yjukhNvoHC1fSXciV8hO
+8GFuR5bUF/6kQFFMZsk3vXNTsKVx5ef7+zpN6n8lGmNAC8D28EqBxar4YAhuu8Jw
++gvguEOji5BsF8pTu4NVBXia0xWjD1DKLmueVLu9rd4l2HGxsA0CAwEAAaOB5zCB
+5DAMBgNVHRMEBTADAQH/MC0GCWCGSAGG+EIBDQQgFh5DQSAtIGF1dG9nZW5lcmF0
+ZWQgQ2VydGlmaWNhdGUwCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQjUcBhP7i26o7R
+iaVbmRStMVsggTB5BgNVHSMEcjBwgBQjUcBhP7i26o7RiaVbmRStMVsggaFVpFMw
+UTELMAkGA1UEBhMCSVQxCzAJBgNVBAgMAlJNMQ0wCwYDVQQHDARSb21hMQ8wDQYD
+VQQKDAZDaW5lY2ExFTATBgNVBAMMDFByb3ZhIENpbmVjYYIBATANBgkqhkiG9w0B
+AQUFAAOCAQEAg0yQ8CGHGl4p2peALn63HxkAxKzxc8bD/bCItXHq3QFJAYRe5nuu
+eGBMdlVvlzh+N/xW1Jcl3+dg9UOlB5/eFr0BWXyk/0vtnJoMKjc4eVAcOlcbgk9s
+c0J4ZACrfjbBH9bU7OgYy4NwVXWQFbQqDZ4/beDnuA8JZcGV5+gK3H85pqGBndev
+4DUTCrYk+kRLMyWLfurH7dSyw/9DXAmOVPB6SMkTK6sqkhwUmT6hEdADFUBTujes
+AjGrlOCMA8XDvvxVEl5nA6JjoPAQ8EIjYvxMykZE+nk0ZO4mqMG5DWCp/2ggodAD
+tnpHdm8yeMsoFPm+yZVDHDXjAirS6MX28w==
+-----END CERTIFICATE-----"""
+ private_key = """-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAxXbqnKwF2bc6qHLJmNtZndOGYfpBMgNJxOP7XLecaCcjCCkd
+YmZmI79evBhu6su20T40/urCC1FxWjFMkWeG66dJ6Ly8T2XKigOXXBFRTSoxlJYH
+9Dc4X5L4WFEy3KbH4fprjrILlarGTRqk2W8AnVq+4G1mO+KlAdJne5vmIekevQtk
+phuqrbJedrpJsZSOcaNWJ4hdOKVuL/3KO6SE2+gcLV9JdyJXyE7wYW5HltQX/qRA
+UUxmyTe9c1OwpXHl5/v7Ok3qfyUaY0ALwPbwSoHFqvhgCG67wnD6C+C4Q6OLkGwX
+ylO7g1UFeJrTFaMPUMoua55Uu72t3iXYcbGwDQIDAQABAoIBAD2pWa/c4+LNncqW
+Na++52gqcm9MB2nHrxSFoKueRoAboIve0uc0VLba/ok8E/7L6GXEyCXGRxvjrcLd
+XCyXqIET9zdvIFqmza11W6GLYtj20Q62Hvu69qaZrWVezcQrbIV7fnTL0mRFNLFF
+Ha8sQ4Pfn3VTlDYlGyPLgTcPQrjZlwD5OlzRNEbko/LkdNXZ3pvf4q17pjsxP3E7
+XqD+d+dny+pBZL748Hp1RmNo/XfhF2Y4iIV4+3/CyBiTlnn8sURqQCeuoA42iCIH
+y28SBz0WS2FD/yVNbH0c4ZU+/R3Fwz5l7sHfaBieJeTFeqr5kuRU7Rro0EfFpa41
+rT3fTz0CgYEA9/XpNsMtRLoMLqb01zvylgLO1cKNkAmoVFhAnh9nH1n3v55Vt48h
+K9NkHUPbVwSIVdQxDzQy+YXw9IEjieVCBOPHTxRHfX90Azup5dFVXznw6qs1GiW2
+mXK+fLToVoTSCi9sHIbIkCAnKS7B5hzKxu+OicKKvouo7UM/NWiSGpsCgYEAy93i
+gN8leZPRSGXgS5COXOJ7zf8mqYWbzytnD5wh3XjWA2SNap93xyclCB7rlMfnOAXy
+9rIgjrDEBBW7BwUyrYcB8M/qLvFfuf3rXgdhVzvA2OctdUdyzGERXObhiRopa2kq
+jFj4QyRa5kv7VTe85t9Ap2bqpE2nVD1wxRdaFncCgYBN0M+ijvfq5JQkI+MclMSZ
+jUIJ1WeFt3IrHhMRTHuZXCui5/awh2t6jHmTsZLpKRP8E35d7hy9L+qhYNGdWeQx
+Eqaey5dv7AqlZRj5dYtcOhvAGYCttv4qA9eB3Wg4lrAv4BgGj8nraRvBEdpp88kz
+S0SpOPM/vyaBZyQ0B6AqVwKBgQCvDvV03Cj94SSRGooj2RmmQQU2uqakYwqMNyTk
+jpm16BE+EJYuvIjKBp8R/hslQxMVVGZx2DuEy91F9LMJMDl4MLpF4wOhE7uzpor5
+zzSTB8htePXcA2Jche227Ls2U7TFeyUCJ1Pns8wqfYxwfNBFH+gQ15sdQ2EwQSIY
+3BiLuQKBgGG+yqKnBceb9zybnshSAVdGt933XjEwRUbaoXGnHjnCxsTtSGa0JkCT
+2yrYrwM4KOr7LrKtvz703ApicJf+oRO+vW27+N5t0pyLCjsYJyL55RpM0KWJhKhT
+KQV8C/ciDV+lIw2yBmlCNvUmy7GAsHSZM+C8y29+GFR7an6WV+xa
+-----END RSA PRIVATE KEY-----"""
+ ca = UUIDCa(name='ImportTest error')
+ ca.certificate = certificate
+ ca.private_key = private_key
+ ca.full_clean()
+ ca.save()
+ self.assertEqual(ca.email, '')
+
+ def test_fill_subject_non_strings(self):
+ ca1 = self._create_ca()
+ ca2 = UUIDCa(name='ca', organization_name=ca1)
+ x509 = crypto.X509()
+ subject = ca2._fill_subject(x509.get_subject())
+ self.assertEqual(subject.organizationName, 'Test CA')
+
+ # this certificate has an invalid country code
+ problematic_certificate = """-----BEGIN CERTIFICATE-----
+MIIEjzCCA3egAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQ8wDQYDVQQGEwZJdGFs
+aWExFjAUBgNVBAgMDUxhbWV6aWEgVGVybWUxFjAUBgNVBAcMDUxhbWV6aWEgVGVy
+bWUxIDAeBgNVBAoMF0NvbXVuZSBkaSBMYW1lemlhIFRlcm1lMRgwFgYDVQQDDA9M
+YW1lemlhZnJlZXdpZmkwHhcNMTIwMjE3MTQzMzAyWhcNMjIwMjE3MTQzMzAyWjB9
+MQ8wDQYDVQQGEwZJdGFsaWExFjAUBgNVBAgMDUxhbWV6aWEgVGVybWUxFjAUBgNV
+BAcMDUxhbWV6aWEgVGVybWUxIDAeBgNVBAoMF0NvbXVuZSBkaSBMYW1lemlhIFRl
+cm1lMRgwFgYDVQQDDA9MYW1lemlhZnJlZXdpZmkwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDBsEbRkpsgl9PZO+eb6M+2XDuENaDKIWxzEqhlQWqfivM5
+SJNpIBij9n8vIgRu2ie7DmomBkU93tQWwL5EcZcSuqAnBgzkNmko5bsk9w7v6Apq
+V4UckIhtie7KRDCrG1XJaZ/0V4uYcW7+d1fYTCfMcgchpzMQsHAdjikyzRXc5TJn
+noV6eZf76zQGSaZllwl90VwQvEVe3VCKSja+zpYxsOjQgnKgrDx1O0l/RGxtCWGG
+fY9bizlD01nH4WuMT9ObO9F1YqnBc7pWtmRm4DfArr3yW5LKxkRrilwV1UCgQ80z
+yMYSeEIufChexzo1JBzrL7aEKnSm5fDvt3iJV3OlAgMBAAGjggEYMIIBFDAMBgNV
+HRMEBTADAQH/MC0GCWCGSAGG+EIBDQQgFh5DQSAtIGF1dG9nZW5lcmF0ZWQgQ2Vy
+dGlmaWNhdGUwCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBSsrs2asN5B2nSL36P72EBR
+MOLgijCBqAYDVR0jBIGgMIGdgBSsrs2asN5B2nSL36P72EBRMOLgiqGBgaR/MH0x
+DzANBgNVBAYTBkl0YWxpYTEWMBQGA1UECAwNTGFtZXppYSBUZXJtZTEWMBQGA1UE
+BwwNTGFtZXppYSBUZXJtZTEgMB4GA1UECgwXQ29tdW5lIGRpIExhbWV6aWEgVGVy
+bWUxGDAWBgNVBAMMD0xhbWV6aWFmcmVld2lmaYIBATANBgkqhkiG9w0BAQUFAAOC
+AQEAf6qG2iFfTv31bOWeE2GBO5VyT1l2MjB/waAXT4vPE2P3RVMoZguBZLc3hmbx
+nF6L5JlG7VbRqEE8wJMS5WeURuJe94CVftXJhzcd8ZnsISoGAh0IiRCLuTmpa/5q
+3eWjgUwr3KldEJ77Sts72qSzRAD6C6RCMxnZTvcQzEjpomLLj1ID82lTrlrYl/in
+MDl+i5LuDRMlgj6PQhUgV+WoRESnZ/jL2MMxA/hcFPzfDDw6A2Kzgz4wzS5FMyHM
+iOCe57IN5gNeO2FAL351FHBONYQMtqeEEL82eSc53oFcLKCJf3E2yo1w6p5HB08H
+IuRFwXXuD2zUkZtldBcYeAa2oA==
+-----END CERTIFICATE-----"""
+ problematic_private_key = """-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAwbBG0ZKbIJfT2Tvnm+jPtlw7hDWgyiFscxKoZUFqn4rzOUiT
+aSAYo/Z/LyIEbtonuw5qJgZFPd7UFsC+RHGXErqgJwYM5DZpKOW7JPcO7+gKaleF
+HJCIbYnuykQwqxtVyWmf9FeLmHFu/ndX2EwnzHIHIaczELBwHY4pMs0V3OUyZ56F
+enmX++s0BkmmZZcJfdFcELxFXt1Qiko2vs6WMbDo0IJyoKw8dTtJf0RsbQlhhn2P
+W4s5Q9NZx+FrjE/TmzvRdWKpwXO6VrZkZuA3wK698luSysZEa4pcFdVAoEPNM8jG
+EnhCLnwoXsc6NSQc6y+2hCp0puXw77d4iVdzpQIDAQABAoIBAQCvQLPjftbUV+x8
+++ImRTJkm/HSP7/8BOAfAvvRmq5CK7TF2TBgh4UkHq6X1BzUvJoEfBd5zmSqhcu7
+xqyiO3FppemxRZ02hTEDq1J5MP6X/oomDIjJ/tEi5BJne+nZeMNXmjX8HZaW2dSH
+dS7L7KR6LZbcUXA4Ip1fcLlAWSb2Fe0bcuSLPaZZSmiA1Q3B/Q6nIOqPXDWq1/yz
+Vs7doSfniAt8CQse+NeWybevAHhaLjHIbqtvmAqmq91ehEiy87Cyj9VA5l4ggM8n
+O6DcmjSaiXfkLgJlrMQ50Ddxoqf35pf+vzebwFdYmyt3fGlIP1OaeVsfIGbkNFZG
+NQkdjEwhAoGBAObDqy8HMv070U+EXSdbv2x1A1glkA2ZUI1Ki+zXIrNV8ohZ4w71
+/v2UsAAXxTCtx28EMFo923dHGk9OXM3EhmyNqYBRX97rB5V7Gt5FxmJs75punYaB
+IfMvo83Hn8mrBUUb74pQhhJ2TVVv/N3nefuElys6lMwyVgUBsu0xPt1pAoGBANbe
+qKouEl+lKdhfABbLCsgCp5yXhFEgNMuGArj5Op/mw/RWOYs4TuN35WmzpmsQZ2PD
++cr+/oN+eJ7zgyStDJmMkeG4vtUVJ5F4wWFWgwgY7zU1J3tu0e/EvgaaLkqWtLRE
+xGJ0zc0qHQdOGGxnQPUy49yvMsdrVwHT/RQiJdDdAoGAAnxlIbKQKA426QZiAoSI
+gWCZUp/E94CJT5xX+YsvwoLQhAuD2Ktpvc2WP8oBw857cYS4CKDV9mj7rZMIiObv
+E8hK5Sj7QWmCwWd8GJzj0DegNSev5r0JYpdGyna2D/QZsG7mm7TWXOiNWLhGHxXZ
+SI5bGoodBD4ekxs7lDaNmNECgYEAoVVd3ynosdgZq1TphDPATJ1xrKo3t5IvEgH1
+WV4JHrbuuy9i1Z3Z3gHQR6WUdx9CAi7MCBeekq0LdI3zEj69Dy30+z70Spovs5Kv
+4J5MlG/kbFcU5iE3kIhxBhQOXgL6e8CGlEaPoFTWpv2EaSC+LV2gqbsCralzEvRR
+OiTJsCECgYEAzdFUEea4M6Uavsd36mBbCLAYkYvhMMYUcrebFpDFwZUFaOrNV0ju
+5YkQTn0EQuwQWKcfs+Z+HRiqMmqj5RdgxQs6pCQG9nfp0uVSflZATOiweshGjn6f
+wZWuZRQLPPTAdiW+drs3gz8w0u3Y9ihgvHQqFcGJ1+j6ANJ0XdE/D5Y=
+-----END RSA PRIVATE KEY-----"""
+
+ def test_ca_invalid_country(self):
+ ca = self._create_ca(name='ImportTest error',
+ certificate=self.problematic_certificate,
+ private_key=self.problematic_private_key)
+ self.assertEqual(ca.country_code, '')
+
+ def test_import_ca_cert_validation_error(self):
+ certificate = self.import_certificate[20:]
+ private_key = self.import_private_key
+ ca = UUIDCa(name="TestCaCertValidation")
+ try:
+ ca.certificate = certificate
+ ca.private_key = private_key
+ ca.full_clean()
+ except ValidationError as e:
+ self.assertIn("[('PEM routines', 'PEM_read_bio', 'no start line')]",
+ str(e.message_dict['certificate'][0]))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_import_ca_key_validation_error(self):
+ certificate = self.import_certificate
+ private_key = self.import_private_key[20:]
+ ca = UUIDCa(name="TestCaKeyValidation")
+ try:
+ ca.certificate = certificate
+ ca.private_key = private_key
+ ca.full_clean()
+ ca.save()
+ except ValidationError as e:
+ self.assertIn("[('PEM routines', 'PEM_read_bio', 'no start line')]",
+ str(e.message_dict['private_key'][0]))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_create_old_serial_ca(self):
+ ca = self._create_ca(serial_number=3)
+ self.assertEqual(int(ca.serial_number), 3)
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca.certificate)
+ self.assertEqual(int(cert.get_serial_number()), int(ca.serial_number))
+
+ def test_bad_serial_number_ca(self):
+ try:
+ self._create_ca(serial_number='notIntegers')
+ except ValidationError as e:
+ self.assertEqual("Serial number must be an integer", str(e.message_dict['serial_number'][0]))
diff --git a/django_x509/tests/test_uuidcert.py b/django_x509/tests/test_uuidcert.py
new file mode 100644
index 0000000..cc89645
--- /dev/null
+++ b/django_x509/tests/test_uuidcert.py
@@ -0,0 +1,429 @@
+from datetime import datetime, timedelta
+
+from django.core.exceptions import ValidationError
+from django.test import TestCase
+from django.utils import timezone
+from OpenSSL import crypto
+
+from . import TestX509Mixin
+from .. import settings as app_settings
+from ..base.models import generalized_time
+from ..models import UUIDCa, UUIDCert
+
+
+class TestCert(TestX509Mixin, TestCase):
+ """
+ tests for Cert model
+ """
+ ca_model = UUIDCa
+ cert_model = UUIDCert
+ import_certificate = """
+-----BEGIN CERTIFICATE-----
+MIICMTCCAdugAwIBAgIDAeJAMA0GCSqGSIb3DQEBBQUAMGgxETAPBgNVBAoMCE9w
+ZW5XSVNQMQswCQYDVQQGEwJJVDEMMAoGA1UEAwwDb3cyMQ0wCwYDVQQHDARSb21l
+MRwwGgYJKoZIhvcNAQkBFg10ZXN0QHRlc3QuY29tMQswCQYDVQQIDAJSTTAiGA8y
+MDE1MTEwMTAwMDAwMFoYDzIxMTgxMTAyMTgwMDI1WjAAMFwwDQYJKoZIhvcNAQEB
+BQADSwAwSAJBANh0Y7oG5JUl9cCBs6E11cJ2xLul6zw8cEoD1L7NazrPXG/NGTLt
+OF2TOEUob24aQ+YagMD6HLbejV0baTXwXakCAwEAAaOB0TCBzjAJBgNVHRMEAjAA
+MAsGA1UdDwQEAwIFoDAdBgNVHQ4EFgQUpcvUDhxzJFpMvjlTQjBaCjQI/3QwgZQG
+A1UdIwSBjDCBiYAUwfnP0B5rF3xo7yDRAda+1nj6QqahbKRqMGgxETAPBgNVBAoM
+CE9wZW5XSVNQMQswCQYDVQQGEwJJVDEMMAoGA1UEAwwDb3cyMQ0wCwYDVQQHDARS
+b21lMRwwGgYJKoZIhvcNAQkBFg10ZXN0QHRlc3QuY29tMQswCQYDVQQIDAJSTYID
+AeJAMA0GCSqGSIb3DQEBBQUAA0EAUKog+BPsM8j34Clec2BAACcuyJlwX41vQ3kG
+FqQS2KfO7YIk5ITWhX8y0P//u+ENWRlnVTRQma9d5tYYJvL8+Q==
+-----END CERTIFICATE-----
+"""
+ import_private_key = """
+-----BEGIN PRIVATE KEY-----
+MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2HRjugbklSX1wIGz
+oTXVwnbEu6XrPDxwSgPUvs1rOs9cb80ZMu04XZM4RShvbhpD5hqAwPoctt6NXRtp
+NfBdqQIDAQABAkEAx9M7NcOjRqXSqDOU92DRxEMNAAb+kY9iQpIi1zqgoZqWduVK
+tq0X0ous54j2ItqKDHxqEbbBzlo/BxMn5zkdOQIhAPIlngBgjgM0FFt+4bw6+5mW
+VvjxIQoVHkmd1HsfHkPvAiEA5NZ+Zqbbv6T7oLgixye1nbcJ3mQ5+IUuamGp7dVq
+/+cCIQDpxVNCffTcNt0ob9gyRqc74Z5Ze0EwYK761zqZGrO3VQIgYp0UZ4QsWo/s
+Z7wyMISqPUbtl8q1OKWb9PgVVIqNy60CIEpi865urZNSIz4SRrxn4r+WV9Mxlfxs
+1xtxYxSjiqrj
+-----END PRIVATE KEY-----
+
+"""
+ import_ca_certificate = """
+-----BEGIN CERTIFICATE-----
+MIICpTCCAk+gAwIBAgIDAeJAMA0GCSqGSIb3DQEBBQUAMGgxETAPBgNVBAoMCE9w
+ZW5XSVNQMQswCQYDVQQGEwJJVDEMMAoGA1UEAwwDb3cyMQ0wCwYDVQQHDARSb21l
+MRwwGgYJKoZIhvcNAQkBFg10ZXN0QHRlc3QuY29tMQswCQYDVQQIDAJSTTAiGA8y
+MDE1MTEwMTAwMDAwMFoYDzIxMjcxMDMxMTc1OTI1WjBoMREwDwYDVQQKDAhPcGVu
+V0lTUDELMAkGA1UEBhMCSVQxDDAKBgNVBAMMA293MjENMAsGA1UEBwwEUm9tZTEc
+MBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTELMAkGA1UECAwCUk0wXDANBgkq
+hkiG9w0BAQEFAANLADBIAkEAsz5ORGAkryOe3bHRsuBJjCbwvPh4peSfpdrRV9CS
+iz7HQWq1s+wdzHONvc8pin+lmnB+RhGm0LrZDOWRyfzjMwIDAQABo4HdMIHaMBIG
+A1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTB+c/Q
+HmsXfGjvINEB1r7WePpCpjCBlAYDVR0jBIGMMIGJgBTB+c/QHmsXfGjvINEB1r7W
+ePpCpqFspGowaDERMA8GA1UECgwIT3BlbldJU1AxCzAJBgNVBAYTAklUMQwwCgYD
+VQQDDANvdzIxDTALBgNVBAcMBFJvbWUxHDAaBgkqhkiG9w0BCQEWDXRlc3RAdGVz
+dC5jb20xCzAJBgNVBAgMAlJNggMB4kAwDQYJKoZIhvcNAQEFBQADQQAeHppFPgUx
+TPJ0Vv9oZHcaOTww6S2p/X/F6yCHZMYq83B+cVxcJ4v+MVxRLg7DBVAIA8gOEFy2
+sKMLWX3IKJmh
+-----END CERTIFICATE-----
+"""
+ import_ca_private_key = """
+-----BEGIN PRIVATE KEY-----
+MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAsz5ORGAkryOe3bHR
+suBJjCbwvPh4peSfpdrRV9CSiz7HQWq1s+wdzHONvc8pin+lmnB+RhGm0LrZDOWR
+yfzjMwIDAQABAkEAnG5ICEyQN3my8HB8PsyX44UonQOM59s7qZfrE+SnwHU2ywhE
+k9Y1S1C9VB0YsDZTeZUggJNSDN4YrKjIevYZQQIhAOWec6vngM/PlI1adrFndd3d
+2WlyfnXwE/RFzVDOfOcrAiEAx9Y1ZbtTr2AL6wsf+wpRbkq9dPEiWi4C+0ms3Uw2
+8BkCIGRctohLnqS2QWLrSHfQFdeM0StizN11uvMI023fYv6TAiEAxujn85/3V1wh
+4M4NAiMuFLseQ5V1XQ/pddjK0Od405kCIC2ezclTgDBbRkHXKFtKnoj3/pGUsa3K
+5XIa5rp5Is47
+-----END PRIVATE KEY-----
+"""
+
+ def test_new(self):
+ cert = self._create_cert()
+ self.assertNotEqual(cert.certificate, '')
+ self.assertNotEqual(cert.private_key, '')
+ x509 = cert.x509
+ self.assertEqual(x509.get_serial_number(), cert.serial_number)
+ subject = x509.get_subject()
+ # check subject
+ self.assertEqual(subject.countryName, cert.country_code)
+ self.assertEqual(subject.stateOrProvinceName, cert.state)
+ self.assertEqual(subject.localityName, cert.city)
+ self.assertEqual(subject.organizationName, cert.organization_name)
+ self.assertEqual(subject.emailAddress, cert.email)
+ self.assertEqual(subject.commonName, cert.common_name)
+ # check issuer
+ issuer = x509.get_issuer()
+ ca = cert.ca
+ self.assertEqual(issuer.countryName, ca.country_code)
+ self.assertEqual(issuer.stateOrProvinceName, ca.state)
+ self.assertEqual(issuer.localityName, ca.city)
+ self.assertEqual(issuer.organizationName, ca.organization_name)
+ self.assertEqual(issuer.emailAddress, ca.email)
+ self.assertEqual(issuer.commonName, ca.common_name)
+ # check signature
+ store = crypto.X509Store()
+ store.add_cert(ca.x509)
+ store_ctx = crypto.X509StoreContext(store, cert.x509)
+ store_ctx.verify_certificate()
+ # ensure version is 3 (indexed 0 based counting)
+ self.assertEqual(x509.get_version(), 2)
+ # basic constraints
+ e = cert.x509.get_extension(0)
+ self.assertEqual(e.get_critical(), 0)
+ self.assertEqual(e.get_short_name().decode(), 'basicConstraints')
+ self.assertEqual(e.get_data(), b'0\x00')
+
+ def test_x509_property(self):
+ cert = self._create_cert()
+ x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert.certificate)
+ self.assertEqual(cert.x509.get_subject(), x509.get_subject())
+ self.assertEqual(cert.x509.get_issuer(), x509.get_issuer())
+
+ def test_x509_property_none(self):
+ self.assertIsNone(UUIDCert().x509)
+
+ def test_pkey_property(self):
+ cert = self._create_cert()
+ self.assertIsInstance(cert.pkey, crypto.PKey)
+
+ def test_pkey_property_none(self):
+ self.assertIsNone(UUIDCert().pkey)
+
+ def test_default_validity_end(self):
+ cert = UUIDCert()
+ self.assertEqual(cert.validity_end.year, datetime.now().year + 1)
+
+ def test_default_validity_start(self):
+ cert = UUIDCert()
+ expected = datetime.now() - timedelta(days=1)
+ self.assertEqual(cert.validity_start.year, expected.year)
+ self.assertEqual(cert.validity_start.month, expected.month)
+ self.assertEqual(cert.validity_start.day, expected.day)
+ self.assertEqual(cert.validity_start.hour, 0)
+ self.assertEqual(cert.validity_start.minute, 0)
+ self.assertEqual(cert.validity_start.second, 0)
+
+ def test_import_cert(self):
+ ca = UUIDCa(name='ImportTest')
+ ca.certificate = self.import_ca_certificate
+ ca.private_key = self.import_ca_private_key
+ ca.full_clean()
+ ca.save()
+ cert = UUIDCert(name='ImportCertTest',
+ ca=ca,
+ certificate=self.import_certificate,
+ private_key=self.import_private_key)
+ cert.full_clean()
+ cert.save()
+ x509 = cert.x509
+ # verify attributes
+ self.assertEqual(int(x509.get_serial_number()), 123456)
+ subject = x509.get_subject()
+ self.assertEqual(subject.countryName, None)
+ self.assertEqual(subject.stateOrProvinceName, None)
+ self.assertEqual(subject.localityName, None)
+ self.assertEqual(subject.organizationName, None)
+ self.assertEqual(subject.emailAddress, None)
+ self.assertEqual(subject.commonName, None)
+ issuer = x509.get_issuer()
+ self.assertEqual(issuer.countryName, 'IT')
+ self.assertEqual(issuer.stateOrProvinceName, 'RM')
+ self.assertEqual(issuer.localityName, 'Rome')
+ self.assertEqual(issuer.organizationName, 'OpenWISP')
+ self.assertEqual(issuer.emailAddress, 'test@test.com')
+ self.assertEqual(issuer.commonName, 'ow2')
+ # verify field attribtues
+ self.assertEqual(cert.key_length, '512')
+ self.assertEqual(cert.digest, 'sha1')
+ start = timezone.make_aware(datetime.strptime('20151101000000Z', generalized_time))
+ self.assertEqual(cert.validity_start, start)
+ end = timezone.make_aware(datetime.strptime('21181102180025Z', generalized_time))
+ self.assertEqual(cert.validity_end, end)
+ self.assertEqual(cert.country_code, '')
+ self.assertEqual(cert.state, '')
+ self.assertEqual(cert.city, '')
+ self.assertEqual(cert.organization_name, '')
+ self.assertEqual(cert.email, '')
+ self.assertEqual(cert.common_name, '')
+ self.assertEqual(int(cert.serial_number), 123456)
+ # ensure version is 3 (indexed 0 based counting)
+ self.assertEqual(x509.get_version(), 2)
+ cert.delete()
+ # test auto name
+ cert = UUIDCert(certificate=self.import_certificate,
+ private_key=self.import_private_key,
+ ca=ca)
+ cert.full_clean()
+ cert.save()
+ self.assertEqual(cert.name, '123456')
+
+ def test_import_private_key_empty(self):
+ ca = UUIDCa(name='ImportTest')
+ ca.certificate = self.import_ca_certificate
+ ca.private_key = self.import_ca_private_key
+ ca.full_clean()
+ ca.save()
+ cert = UUIDCert(name='ImportTest',
+ ca=ca)
+ cert.certificate = self.import_certificate
+ try:
+ cert.full_clean()
+ except ValidationError as e:
+ # verify error message
+ self.assertIn('importing an existing certificate', str(e))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_import_wrong_ca(self):
+ # test auto name
+ cert = UUIDCert(certificate=self.import_certificate,
+ private_key=self.import_private_key,
+ ca=self._create_ca())
+ try:
+ cert.full_clean()
+ except ValidationError as e:
+ # verify error message
+ self.assertIn('CA doesn\'t match', str(e.message_dict['__all__'][0]))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_keyusage(self):
+ cert = self._create_cert()
+ e = cert.x509.get_extension(1)
+ self.assertEqual(e.get_short_name().decode(), 'keyUsage')
+ self.assertEqual(e.get_critical(), False)
+ self.assertEqual(e.get_data(), b'\x03\x02\x05\xa0')
+
+ def test_keyusage_critical(self):
+ setattr(app_settings, 'CERT_KEYUSAGE_CRITICAL', True)
+ cert = self._create_cert()
+ e = cert.x509.get_extension(1)
+ self.assertEqual(e.get_short_name().decode(), 'keyUsage')
+ self.assertEqual(e.get_critical(), True)
+ setattr(app_settings, 'CERT_KEYUSAGE_CRITICAL', False)
+
+ def test_keyusage_value(self):
+ setattr(app_settings, 'CERT_KEYUSAGE_VALUE', 'digitalSignature')
+ cert = self._create_cert()
+ e = cert.x509.get_extension(1)
+ self.assertEqual(e.get_short_name().decode(), 'keyUsage')
+ self.assertEqual(e.get_data(), b'\x03\x02\x07\x80')
+ setattr(app_settings, 'CERT_KEYUSAGE_VALUE', 'digitalSignature, keyEncipherment')
+
+ def test_subject_key_identifier(self):
+ cert = self._create_cert()
+ e = cert.x509.get_extension(2)
+ self.assertEqual(e.get_short_name().decode(), 'subjectKeyIdentifier')
+ self.assertEqual(e.get_critical(), False)
+ e2 = crypto.X509Extension(b'subjectKeyIdentifier',
+ False,
+ b'hash',
+ subject=cert.x509)
+ self.assertEqual(e.get_data(), e2.get_data())
+
+ def test_authority_key_identifier(self):
+ cert = self._create_cert()
+ e = cert.x509.get_extension(3)
+ self.assertEqual(e.get_short_name().decode(), 'authorityKeyIdentifier')
+ self.assertEqual(e.get_critical(), False)
+ e2 = crypto.X509Extension(b'authorityKeyIdentifier',
+ False,
+ b'keyid:always,issuer:always',
+ issuer=cert.ca.x509)
+ self.assertEqual(e.get_data(), e2.get_data())
+
+ def test_extensions(self):
+ extensions = [
+ {
+ "name": "nsCertType",
+ "critical": False,
+ "value": "client"
+ },
+ {
+ "name": "extendedKeyUsage",
+ "critical": True, # critical just for testing purposes
+ "value": "clientAuth"
+ }
+ ]
+ cert = self._create_cert(extensions=extensions)
+ e1 = cert.x509.get_extension(4)
+ self.assertEqual(e1.get_short_name().decode(), 'nsCertType')
+ self.assertEqual(e1.get_critical(), False)
+ self.assertEqual(e1.get_data(), b'\x03\x02\x07\x80')
+ e2 = cert.x509.get_extension(5)
+ self.assertEqual(e2.get_short_name().decode(), 'extendedKeyUsage')
+ self.assertEqual(e2.get_critical(), True)
+ self.assertEqual(e2.get_data(), b'0\n\x06\x08+\x06\x01\x05\x05\x07\x03\x02')
+
+ def test_extensions_error1(self):
+ extensions = {}
+ try:
+ self._create_cert(extensions=extensions)
+ except ValidationError as e:
+ # verify error message
+ self.assertIn('Extension format invalid', str(e.message_dict['__all__'][0]))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_extensions_error2(self):
+ extensions = [
+ {"wrong": "wrong"}
+ ]
+ try:
+ self._create_cert(extensions=extensions)
+ except ValidationError as e:
+ # verify error message
+ self.assertIn('Extension format invalid', str(e.message_dict['__all__'][0]))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_revoke(self):
+ cert = self._create_cert()
+ self.assertFalse(cert.revoked)
+ self.assertIsNone(cert.revoked_at)
+ cert.revoke()
+ self.assertTrue(cert.revoked)
+ self.assertIsNotNone(cert.revoked_at)
+
+ def test_x509_text(self):
+ cert = self._create_cert()
+ text = crypto.dump_certificate(crypto.FILETYPE_TEXT, cert.x509)
+ self.assertEqual(cert.x509_text, text.decode('utf-8'))
+
+ def test_fill_subject_None_attrs(self):
+ # ensure no exception raised if model attrs are set to None
+ x509 = crypto.X509()
+ cert = UUIDCert(name='test', ca=self._create_ca())
+ cert._fill_subject(x509.get_subject())
+ self.country_code = 'IT'
+ cert._fill_subject(x509.get_subject())
+ self.state = 'RM'
+ cert._fill_subject(x509.get_subject())
+ self.city = 'Rome'
+ cert._fill_subject(x509.get_subject())
+ self.organization_name = 'OpenWISP'
+ cert._fill_subject(x509.get_subject())
+ self.email = 'test@test.com'
+ cert._fill_subject(x509.get_subject())
+
+ def test_cert_create(self):
+ ca = UUIDCa(name='Test CA')
+ ca.full_clean()
+ ca.save()
+
+ UUIDCert.objects.create(
+ ca=ca,
+ common_name='TestCert1',
+ name='TestCert1',
+ )
+
+ def test_import_cert_validation_error(self):
+ certificate = self.import_certificate[20:]
+ private_key = self.import_private_key
+ ca = UUIDCa(name='TestImportCertValidation')
+ ca.certificate = self.import_ca_certificate
+ ca.private_key = self.import_ca_private_key
+ ca.full_clean()
+ ca.save()
+ try:
+ cert = UUIDCert(name='TestCertValidation',
+ ca=ca,
+ certificate=certificate,
+ private_key=private_key)
+ cert.full_clean()
+ except ValidationError as e:
+ self.assertIn("[('PEM routines', 'PEM_read_bio', 'no start line')]",
+ str(e.message_dict['certificate'][0]))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_import_key_validation_error(self):
+ certificate = self.import_certificate
+ private_key = self.import_private_key[20:]
+ ca = UUIDCa(name='TestImportKeyValidation')
+ ca.certificate = self.import_certificate
+ ca.private_key = self.import_private_key
+ ca.full_clean()
+ ca.save()
+ try:
+ cert = UUIDCert(name='TestKeyValidation',
+ ca=ca,
+ certificate=certificate,
+ private_key=private_key)
+ cert.full_clean()
+ except ValidationError as e:
+ self.assertIn("[('PEM routines', 'PEM_read_bio', 'no start line')]",
+ str(e.message_dict['private_key'][0]))
+ else:
+ self.fail('ValidationError not raised')
+
+ def test_create_old_serial_certificate(self):
+ cert = self._create_cert(serial_number=3)
+ self.assertEqual(int(cert.serial_number), 3)
+ x509 = cert.x509
+ self.assertEqual(int(x509.get_serial_number()), 3)
+
+ def test_bad_serial_number_cert(self):
+ try:
+ self._create_cert(serial_number='notIntegers')
+ except ValidationError as e:
+ self.assertEqual("Serial number must be an integer", str(e.message_dict['serial_number'][0]))
+
+ def test_serial_number_clash(self):
+ ca = UUIDCa(name='TestSerialClash')
+ ca.certificate = self.import_ca_certificate
+ ca.private_key = self.import_ca_private_key
+ ca.save()
+ cert = self._create_cert(serial_number=123456, ca=ca)
+ cert.full_clean()
+ cert.save()
+ _cert = UUIDCert(name='TestClash',
+ ca=ca,
+ certificate=self.import_certificate,
+ private_key=self.import_private_key)
+ try:
+ _cert.full_clean()
+ except ValidationError as e:
+ self.assertEqual("Certificate with this CA and Serial number already exists.",
+ str(e.message_dict['__all__'][0]))
diff --git a/django_x509/urls.py b/django_x509/urls.py
index e1e80ba..8c36c53 100644
--- a/django_x509/urls.py
+++ b/django_x509/urls.py
@@ -5,4 +5,5 @@
app_name = 'x509'
urlpatterns = [
url(r'^x509/ca/(?P[^/]+).crl$', views.crl, name='crl'),
+ url(r'^x509/uuidca/(?P[^/]+).crl$', views.uuidcrl, name='uuidcrl'),
]
diff --git a/django_x509/views.py b/django_x509/views.py
index 6fbbff5..ad6eb84 100644
--- a/django_x509/views.py
+++ b/django_x509/views.py
@@ -1,4 +1,5 @@
-from .base.views import crl
-from .models import Ca
+from .base.views import crl, uuidcrl
+from .models import Ca, UUIDCa
crl.ca_model = Ca
+uuidcrl.ca_model = UUIDCa
diff --git a/tests/delete/cert.pem b/tests/delete/cert.pem
new file mode 100644
index 0000000..45e2685
--- /dev/null
+++ b/tests/delete/cert.pem
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIJALBakMQ6jKiXMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTgwOTEyMTEwNDQxWhcNMTkwOTEyMTEwNDQxWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAyr9vdxNd0sCSW9dZNc8zxCuBKQKaxp7JkIhXTHGP2HVSnd6PIJGWv5rh
+qtQfeByrhaMuy+25ZlG+W/prf2Me1OyyqTCoeS2LF4BBX+Y2iDJ+xdcH/B7P7BTx
+oc5yS4Ch/zRrSlvaqZBqn9Xzw/UBqKlqkfLeu8ZJ0W6M0N06TG6kUKnWtopAdF1v
+bUn/BVw5A194QvJsDDMB4UQf1PFsBphKJGBU8qQDeWY7UPJR2MfrJubgPJ0nJxwf
+nkETtC9KlaD/S5A8Crabkbsj4lnxgLdPFt2WwznuKfui2GDlntg2EpeEF+KXfWCq
+VxymEt6ywTcmRo1QWBwhsGkCVd1umHzgCN4faAdXnryLvFlHsvcQG7DgX1LlhfH9
+OgnwNj2vKCLhVJ3Cc7eyEgYh8oG1XrqeOO+wmcHEl0pbK+6j/pCgca5cKtgZLgks
+zs9kxNLvwdXWO8ff2tI1v4jAe0RABEVUpV4PorKO1v/YQK5F9eeSHrugsQNy0xkk
+LIhg/09nAKYJhPV6DEhlK+RXiMsBEKK1nVsBwg7tP1KQgAxJzAYQ+WxjVQofflRL
+9Qe/9pKQ7NsnKJ650P0BPpvbCDIeSWontn89iXInS0mZTla15Dg52f9zbj+noXor
+f+EkMyCOUQ/qnTAYLv3thC+Vpotu6PowYyqi5hk1C7Ej8CmgAEUCAwEAAaNTMFEw
+HQYDVR0OBBYEFFkmbk20nv4wov6c3vjG6dcRahuiMB8GA1UdIwQYMBaAFFkmbk20
+nv4wov6c3vjG6dcRahuiMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD
+ggIBAESyCOCC3xFcVekEd7YrAMJXVxeD9YIQg/O/CU4zqkXQh7FR6GKvE+A72qxm
+2r5cC2Cxn9GRUuUBN06IJp6jkpmPvJ8RiDUT9maxLS7cuYo/3sO776L4q66sDWPQ
+SouUmUsu0dI0kjoGEWuYe/ffzK6/pft6bZTxs/EhQFNrlFwuJ5m0Yum97Y867a63
+lgBKTNf1VgOVMvEV2naS16hrbfF8TVWwkLtLlqSfNv115v+kbPOWz52yRZiUAq46
+ztfaGQTg1WQlLijHT8s53+j1fro1boa5BYNMMKd8pQHxIEfi55fCMLzhyDokBYEy
+at3p8Dr2ftbPknHSiQQ3FZXF9UvSZpH8PDoMl6I2P5gi3ob9IAS590f4Wb808hng
+n57SZUeYmDGvZCyBjHLTjY365ESMPbp8/6/MkFVOHnCqrRv4dP6+6h3oHl9c1WNH
+r0DtiHF1Rlp8yhrJBI3pYPzXslBxenfCuXm6Foc7MmxfmD3r1qeKJpJQe0Qc1QVn
+lXdi6AnJF1/o0qtHgIzToI5+CgwOIg4CssVxIoW22fZC5RWu/2MByS+43SP/sEBg
+BPtPnXQWZvTfnTeYjKI0Q9wT5LsdUtSXu4Zu6W6nWYvMEE3wNG922A06O90xkd5I
+VmPsmoSbgL6jD1Wpvd2Kt9PfcPFvK9iL5nibD82p90G0hEah
+-----END CERTIFICATE-----
diff --git a/tests/delete/key.pem b/tests/delete/key.pem
new file mode 100644
index 0000000..2fabca2
--- /dev/null
+++ b/tests/delete/key.pem
@@ -0,0 +1,54 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+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
+W+hqktoqC95gWvFzNGVaaQ==
+-----END ENCRYPTED PRIVATE KEY-----