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-----