From d578ea4dc1ca11ca2d6e6659274a413c26b65dc8 Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sun, 11 Jan 2026 16:29:42 +0530 Subject: [PATCH 01/18] [feature] Added support for importing ECDSA certificates #118 Updated KEY_LENGTH_CHOICES to include 224, 256, and 384 bits to support Elliptic Curve algorithms. Fixes #118 --- django_x509/base/models.py | 67 ++++++++++-- ...t_key_type_alter_ca_key_length_and_more.py | 72 +++++++++++++ ...t_key_type_customcert_key_type_and_more.py | 101 ++++++++++++++++++ 3 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 django_x509/migrations/0010_ca_key_type_cert_key_type_alter_ca_key_length_and_more.py create mode 100644 tests/openwisp2/sample_x509/migrations/0003_ca_key_type_cert_key_type_customcert_key_type_and_more.py diff --git a/django_x509/base/models.py b/django_x509/base/models.py index 3a0395d..9bf27d4 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -19,12 +19,23 @@ from .. import settings as app_settings KEY_LENGTH_CHOICES = ( + ("224", "224"), + ("256", "256"), + ("384", "384"), ("512", "512"), ("1024", "1024"), ("2048", "2048"), ("4096", "4096"), ) +KEY_TYPE_CHOICES = ( + ("rsa", "RSA"), + ("ec", "Elliptic Curve"), +) + +RSA_KEY_LENGTHS = ("512", "1024", "2048", "4096") +EC_KEY_LENGTHS = ("224", "256", "384") + DIGEST_CHOICES = ( ("sha1", "SHA1"), ("sha224", "SHA224"), @@ -148,6 +159,9 @@ class BaseX509(models.Model): blank=True, help_text=_("Passphrase for the private key, if present"), ) + key_type = models.CharField( + _("key type"), max_length=3, choices=KEY_TYPE_CHOICES, default="rsa" + ) class Meta: abstract = True @@ -178,6 +192,15 @@ def clean(self): if self.serial_number: self._validate_serial_number() self._verify_extension_format() + if self.key_type == "rsa" and self.key_length in EC_KEY_LENGTHS: + raise ValidationError( + {"key_length": _("RSA keys must be at least 512 bits.")} + ) + + if self.key_type == "ec" and self.key_length in RSA_KEY_LENGTHS: + raise ValidationError( + {"key_length": _("Selected length is not valid for Elliptic Curve.")} + ) def save(self, *args, **kwargs): if self._state.adding and not self.certificate and not self.private_key: @@ -280,10 +303,26 @@ def _generate(self): for attr in ["x509", "pkey"]: if attr in self.__dict__: del self.__dict__[attr] - key = rsa.generate_private_key( - public_exponent=65537, - key_size=int(self.key_length), - ) + if self.key_type == "rsa": + key = rsa.generate_private_key( + public_exponent=65537, + key_size=int(self.key_length), + ) + elif self.key_type == "ec": + curves = { + "224": ec.SECP224R1(), + "256": ec.SECP256R1(), + "384": ec.SECP384R1(), + "521": ec.SECP521R1(), + } + curve = curves.get(self.key_length) + if not curve: + raise ValidationError( + _("Unsupported EC key length: %s") % self.key_length + ) + key = ec.generate_private_key(curve) + else: + raise ValidationError(_("Unsupported key type: %s") % self.key_type) if hasattr(self, "ca"): signing_key = self.ca.pkey issuer_name = self.ca.x509.subject @@ -307,7 +346,11 @@ def _generate(self): "sha384": hashes.SHA384, "sha512": hashes.SHA512, } - digest_name = self.digest.lower().replace("withrsaencryption", "") + digest_name = ( + self.digest.lower() + .replace("withrsaencryption", "") + .replace("ecdsa-with-", "") + ) digest_alg = HASH_MAP.get(digest_name, hashes.SHA256)() cert = builder.sign(signing_key, digest_alg) self.certificate = cert.public_bytes(serialization.Encoding.PEM).decode("utf-8") @@ -318,7 +361,11 @@ def _generate(self): ) self.private_key = key.private_bytes( encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, + format=( + serialization.PrivateFormat.PKCS8 + if self.key_type == "ec" + else serialization.PrivateFormat.TraditionalOpenSSL + ), encryption_algorithm=encryption, ).decode("utf-8") @@ -352,6 +399,13 @@ def _import(self): imports existing x509 certificates """ cert = self.x509 + public_key = cert.public_key() + if isinstance(public_key, rsa.RSAPublicKey): + self.key_type = "rsa" + self.key_length = str(public_key.key_size) + elif isinstance(public_key, ec.EllipticCurvePublicKey): + self.key_type = "ec" + self.key_length = str(public_key.curve.key_size) # when importing an end entity certificate if hasattr(self, "ca"): self._verify_ca() @@ -366,7 +420,6 @@ def _import(self): email = str(attrs[0].value) if attrs else "" self.email = email - self.key_length = str(cert.public_key().key_size) self.digest = cert.signature_hash_algorithm.name.lower() self.validity_start = cert.not_valid_before_utc self.validity_end = cert.not_valid_after_utc diff --git a/django_x509/migrations/0010_ca_key_type_cert_key_type_alter_ca_key_length_and_more.py b/django_x509/migrations/0010_ca_key_type_cert_key_type_alter_ca_key_length_and_more.py new file mode 100644 index 0000000..09d6a4c --- /dev/null +++ b/django_x509/migrations/0010_ca_key_type_cert_key_type_alter_ca_key_length_and_more.py @@ -0,0 +1,72 @@ +# Generated by Django 5.2.9 on 2026-01-11 10:50 + +import django_x509.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("django_x509", "0009_alter_ca_digest_alter_ca_key_length_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="ca", + name="key_type", + field=models.CharField( + choices=[("rsa", "RSA"), ("ec", "Elliptic Curve")], + default="rsa", + max_length=3, + verbose_name="key type", + ), + ), + migrations.AddField( + model_name="cert", + name="key_type", + field=models.CharField( + choices=[("rsa", "RSA"), ("ec", "Elliptic Curve")], + default="rsa", + max_length=3, + verbose_name="key type", + ), + ), + migrations.AlterField( + model_name="ca", + name="key_length", + field=models.CharField( + choices=[ + ("224", "224"), + ("256", "256"), + ("384", "384"), + ("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", + ), + ), + migrations.AlterField( + model_name="cert", + name="key_length", + field=models.CharField( + choices=[ + ("224", "224"), + ("256", "256"), + ("384", "384"), + ("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", + ), + ), + ] diff --git a/tests/openwisp2/sample_x509/migrations/0003_ca_key_type_cert_key_type_customcert_key_type_and_more.py b/tests/openwisp2/sample_x509/migrations/0003_ca_key_type_cert_key_type_customcert_key_type_and_more.py new file mode 100644 index 0000000..5974816 --- /dev/null +++ b/tests/openwisp2/sample_x509/migrations/0003_ca_key_type_cert_key_type_customcert_key_type_and_more.py @@ -0,0 +1,101 @@ +# Generated by Django 5.2.9 on 2026-01-11 10:55 + +import django_x509.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sample_x509", "0002_common_name_max_length"), + ] + + operations = [ + migrations.AddField( + model_name="ca", + name="key_type", + field=models.CharField( + choices=[("rsa", "RSA"), ("ec", "Elliptic Curve")], + default="rsa", + max_length=3, + verbose_name="key type", + ), + ), + migrations.AddField( + model_name="cert", + name="key_type", + field=models.CharField( + choices=[("rsa", "RSA"), ("ec", "Elliptic Curve")], + default="rsa", + max_length=3, + verbose_name="key type", + ), + ), + migrations.AddField( + model_name="customcert", + name="key_type", + field=models.CharField( + choices=[("rsa", "RSA"), ("ec", "Elliptic Curve")], + default="rsa", + max_length=3, + verbose_name="key type", + ), + ), + migrations.AlterField( + model_name="ca", + name="key_length", + field=models.CharField( + choices=[ + ("224", "224"), + ("256", "256"), + ("384", "384"), + ("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", + ), + ), + migrations.AlterField( + model_name="cert", + name="key_length", + field=models.CharField( + choices=[ + ("224", "224"), + ("256", "256"), + ("384", "384"), + ("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", + ), + ), + migrations.AlterField( + model_name="customcert", + name="key_length", + field=models.CharField( + choices=[ + ("224", "224"), + ("256", "256"), + ("384", "384"), + ("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", + ), + ), + ] From c2f3be8f75b42b00b84742d89aa3a9b96f5853b1 Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sun, 11 Jan 2026 16:43:52 +0530 Subject: [PATCH 02/18] [feature] Included the P-521 curve in choices #118 Added P-521 curve in KEY_LENGTH_CHOICES and EC_KEY_LENGTHS Fixes #118 --- django_x509/base/models.py | 3 +- ...ter_ca_key_length_alter_cert_key_length.py | 54 ++++++++++++++ ...y_length_alter_cert_key_length_and_more.py | 74 +++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py create mode 100644 tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py diff --git a/django_x509/base/models.py b/django_x509/base/models.py index 9bf27d4..71c6ba1 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -22,6 +22,7 @@ ("224", "224"), ("256", "256"), ("384", "384"), + ("521", "521"), ("512", "512"), ("1024", "1024"), ("2048", "2048"), @@ -34,7 +35,7 @@ ) RSA_KEY_LENGTHS = ("512", "1024", "2048", "4096") -EC_KEY_LENGTHS = ("224", "256", "384") +EC_KEY_LENGTHS = ("224", "256", "384", "521") DIGEST_CHOICES = ( ("sha1", "SHA1"), diff --git a/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py b/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py new file mode 100644 index 0000000..3785051 --- /dev/null +++ b/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py @@ -0,0 +1,54 @@ +# Generated by Django 5.2.9 on 2026-01-11 11:10 + +import django_x509.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("django_x509", "0010_ca_key_type_cert_key_type_alter_ca_key_length_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="ca", + name="key_length", + field=models.CharField( + choices=[ + ("224", "224"), + ("256", "256"), + ("384", "384"), + ("521", "521"), + ("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", + ), + ), + migrations.AlterField( + model_name="cert", + name="key_length", + field=models.CharField( + choices=[ + ("224", "224"), + ("256", "256"), + ("384", "384"), + ("521", "521"), + ("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", + ), + ), + ] diff --git a/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py b/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py new file mode 100644 index 0000000..b00e62e --- /dev/null +++ b/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py @@ -0,0 +1,74 @@ +# Generated by Django 5.2.9 on 2026-01-11 11:11 + +import django_x509.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sample_x509", "0003_ca_key_type_cert_key_type_customcert_key_type_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="ca", + name="key_length", + field=models.CharField( + choices=[ + ("224", "224"), + ("256", "256"), + ("384", "384"), + ("521", "521"), + ("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", + ), + ), + migrations.AlterField( + model_name="cert", + name="key_length", + field=models.CharField( + choices=[ + ("224", "224"), + ("256", "256"), + ("384", "384"), + ("521", "521"), + ("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", + ), + ), + migrations.AlterField( + model_name="customcert", + name="key_length", + field=models.CharField( + choices=[ + ("224", "224"), + ("256", "256"), + ("384", "384"), + ("521", "521"), + ("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", + ), + ), + ] From 86a6b9fe5cc0e20f82023db2e1376e5bf6d82464 Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sun, 11 Jan 2026 16:56:17 +0530 Subject: [PATCH 03/18] [Improvements] Handling of unsupported keys and message clarity #118 Improved error message clarity and added validation of unsupported keys. Fixes #118 --- django_x509/base/models.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/django_x509/base/models.py b/django_x509/base/models.py index 71c6ba1..f4d16b6 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -195,7 +195,12 @@ def clean(self): self._verify_extension_format() if self.key_type == "rsa" and self.key_length in EC_KEY_LENGTHS: raise ValidationError( - {"key_length": _("RSA keys must be at least 512 bits.")} + { + "key_length": _( + "Selected length is only valid for Elliptic Curve keys. " + "RSA keys must use 512, 1024, 2048, or 4096 bits." + ) + } ) if self.key_type == "ec" and self.key_length in RSA_KEY_LENGTHS: @@ -407,6 +412,13 @@ def _import(self): elif isinstance(public_key, ec.EllipticCurvePublicKey): self.key_type = "ec" self.key_length = str(public_key.curve.key_size) + else: + raise ValidationError( + _( + "Unsupported key type in certificate. " + "Only RSA and EC keys are supported." + ) + ) # when importing an end entity certificate if hasattr(self, "ca"): self._verify_ca() From 678566d7a454beae2ebaf3d804214c030bef74aa Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sun, 11 Jan 2026 17:19:55 +0530 Subject: [PATCH 04/18] [Update] Remove SECP224R1 (224-bit) curve support #118 Removed 224-bit curve support as modern standards recommend 256-bit curves (SECP256R1/P-256) as the minimum. Fixes #118 --- django_x509/base/models.py | 4 +- ...ter_ca_key_length_alter_cert_key_length.py | 52 ++++++++++++++ ...y_length_alter_cert_key_length_and_more.py | 71 +++++++++++++++++++ 3 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 django_x509/migrations/0012_alter_ca_key_length_alter_cert_key_length.py create mode 100644 tests/openwisp2/sample_x509/migrations/0005_alter_ca_key_length_alter_cert_key_length_and_more.py diff --git a/django_x509/base/models.py b/django_x509/base/models.py index f4d16b6..8a9d8eb 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -19,7 +19,6 @@ from .. import settings as app_settings KEY_LENGTH_CHOICES = ( - ("224", "224"), ("256", "256"), ("384", "384"), ("521", "521"), @@ -35,7 +34,7 @@ ) RSA_KEY_LENGTHS = ("512", "1024", "2048", "4096") -EC_KEY_LENGTHS = ("224", "256", "384", "521") +EC_KEY_LENGTHS = ("256", "384", "521") DIGEST_CHOICES = ( ("sha1", "SHA1"), @@ -316,7 +315,6 @@ def _generate(self): ) elif self.key_type == "ec": curves = { - "224": ec.SECP224R1(), "256": ec.SECP256R1(), "384": ec.SECP384R1(), "521": ec.SECP521R1(), diff --git a/django_x509/migrations/0012_alter_ca_key_length_alter_cert_key_length.py b/django_x509/migrations/0012_alter_ca_key_length_alter_cert_key_length.py new file mode 100644 index 0000000..436ab75 --- /dev/null +++ b/django_x509/migrations/0012_alter_ca_key_length_alter_cert_key_length.py @@ -0,0 +1,52 @@ +# Generated by Django 5.2.9 on 2026-01-11 11:47 + +import django_x509.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("django_x509", "0011_alter_ca_key_length_alter_cert_key_length"), + ] + + operations = [ + migrations.AlterField( + model_name="ca", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256"), + ("384", "384"), + ("521", "521"), + ("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", + ), + ), + migrations.AlterField( + model_name="cert", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256"), + ("384", "384"), + ("521", "521"), + ("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", + ), + ), + ] diff --git a/tests/openwisp2/sample_x509/migrations/0005_alter_ca_key_length_alter_cert_key_length_and_more.py b/tests/openwisp2/sample_x509/migrations/0005_alter_ca_key_length_alter_cert_key_length_and_more.py new file mode 100644 index 0000000..6dab86a --- /dev/null +++ b/tests/openwisp2/sample_x509/migrations/0005_alter_ca_key_length_alter_cert_key_length_and_more.py @@ -0,0 +1,71 @@ +# Generated by Django 5.2.9 on 2026-01-11 11:48 + +import django_x509.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sample_x509", "0004_alter_ca_key_length_alter_cert_key_length_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="ca", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256"), + ("384", "384"), + ("521", "521"), + ("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", + ), + ), + migrations.AlterField( + model_name="cert", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256"), + ("384", "384"), + ("521", "521"), + ("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", + ), + ), + migrations.AlterField( + model_name="customcert", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256"), + ("384", "384"), + ("521", "521"), + ("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", + ), + ), + ] From cf82aca1a686b908b44e1a84f0575094307106de Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sun, 11 Jan 2026 17:50:34 +0530 Subject: [PATCH 05/18] [Improvement] Added explicit validation for imported EC certificates with unsupported key lengths #118 Added explicit validation in _import() to reject unsupported EC curves with a clear, user-friendly error message. Fixes #118 --- django_x509/base/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/django_x509/base/models.py b/django_x509/base/models.py index 8a9d8eb..4ac7854 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -354,6 +354,7 @@ def _generate(self): self.digest.lower() .replace("withrsaencryption", "") .replace("ecdsa-with-", "") + .replace("withsha384", "sha384") ) digest_alg = HASH_MAP.get(digest_name, hashes.SHA256)() cert = builder.sign(signing_key, digest_alg) @@ -409,7 +410,10 @@ def _import(self): self.key_length = str(public_key.key_size) elif isinstance(public_key, ec.EllipticCurvePublicKey): self.key_type = "ec" - self.key_length = str(public_key.curve.key_size) + curve_size = str(public_key.curve.key_size) + if curve_size not in EC_KEY_LENGTHS: + raise ValidationError(_("Unsupported EC curve size: %s") % curve_size) + self.key_length = curve_size else: raise ValidationError( _( From c6e4de0023e8b7f20b7b3cbf32022981a4de6727 Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sun, 11 Jan 2026 18:15:30 +0530 Subject: [PATCH 06/18] [Test] Added test to check whole ecdsa lifecycle #118 Added a test to verify full edcsa lifecycle with respect to the changes. Fixes #118 --- django_x509/tests/test_ca.py | 48 +++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/django_x509/tests/test_ca.py b/django_x509/tests/test_ca.py index fbb8ba1..6d978d6 100644 --- a/django_x509/tests/test_ca.py +++ b/django_x509/tests/test_ca.py @@ -2,7 +2,8 @@ from datetime import timezone as dt_timezone from cryptography import x509 -from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.x509.oid import NameOID from django.core.exceptions import ValidationError from django.test import TestCase @@ -684,3 +685,48 @@ def test_renewal_serial_sync(self): cert_obj = x509.load_pem_x509_certificate(cert.certificate.encode()) pem_serial = cert_obj.serial_number self.assertEqual(int(cert.serial_number), pem_serial) + + def test_ecdsa_full_lifecycle(self): + curves_to_test = [ + ("256", ec.SECP256R1), + ("384", ec.SECP384R1), + ("521", ec.SECP521R1), + ] + for length, curve_class in curves_to_test: + priv_key = ec.generate_private_key(curve_class()) + key_pem = priv_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ).decode("utf-8") + subject = issuer = x509.Name( + [ + x509.NameAttribute(x509.NameOID.COMMON_NAME, "test-import"), + ] + ) + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(priv_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(timezone.now()) + .not_valid_after(timezone.now() + timedelta(days=10)) + .sign(priv_key, hashes.SHA256()) + ) + cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode("utf-8") + ca = Ca( + name=f"ECDSA-{length}-Import", certificate=cert_pem, private_key=key_pem + ) + ca.full_clean() + ca.save() + self.assertEqual(ca.key_type, "ec") + self.assertEqual(ca.key_length, length) + gen_ca = Ca(name=f"Generated-EC-{length}", key_type="ec", key_length=length) + gen_ca.save() + self.assertIsInstance(gen_ca.pkey, ec.EllipticCurvePrivateKey) + original_cert = gen_ca.certificate + gen_ca.renew() + self.assertNotEqual(original_cert, gen_ca.certificate) + self.assertEqual(gen_ca.key_type, "ec") + self.assertIsInstance(gen_ca.pkey, ec.EllipticCurvePrivateKey) From f74fa1dd0dff1ab4b50f5fd24133ef13905c31c4 Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sun, 11 Jan 2026 18:37:21 +0530 Subject: [PATCH 07/18] [Improvement] Add curve-appropriate hash algorithms to test different ECDSA signature variants #118 Updated test implementation to now pair curves with their standard matching digests and use datetime.now(dt_timezone.utc) instead of timezone.now() for the certificate validity dates. Fixes #118 --- django_x509/tests/test_ca.py | 87 +++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/django_x509/tests/test_ca.py b/django_x509/tests/test_ca.py index 6d978d6..7fb68b3 100644 --- a/django_x509/tests/test_ca.py +++ b/django_x509/tests/test_ca.py @@ -688,45 +688,50 @@ def test_renewal_serial_sync(self): def test_ecdsa_full_lifecycle(self): curves_to_test = [ - ("256", ec.SECP256R1), - ("384", ec.SECP384R1), - ("521", ec.SECP521R1), + ("256", ec.SECP256R1, hashes.SHA256()), + ("384", ec.SECP384R1, hashes.SHA384()), + ("521", ec.SECP521R1, hashes.SHA512()), ] - for length, curve_class in curves_to_test: - priv_key = ec.generate_private_key(curve_class()) - key_pem = priv_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ).decode("utf-8") - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COMMON_NAME, "test-import"), - ] - ) - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(priv_key.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(timezone.now()) - .not_valid_after(timezone.now() + timedelta(days=10)) - .sign(priv_key, hashes.SHA256()) - ) - cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode("utf-8") - ca = Ca( - name=f"ECDSA-{length}-Import", certificate=cert_pem, private_key=key_pem - ) - ca.full_clean() - ca.save() - self.assertEqual(ca.key_type, "ec") - self.assertEqual(ca.key_length, length) - gen_ca = Ca(name=f"Generated-EC-{length}", key_type="ec", key_length=length) - gen_ca.save() - self.assertIsInstance(gen_ca.pkey, ec.EllipticCurvePrivateKey) - original_cert = gen_ca.certificate - gen_ca.renew() - self.assertNotEqual(original_cert, gen_ca.certificate) - self.assertEqual(gen_ca.key_type, "ec") - self.assertIsInstance(gen_ca.pkey, ec.EllipticCurvePrivateKey) + for length, curve_class, digest in curves_to_test: + with self.subTest(key_length=length): + priv_key = ec.generate_private_key(curve_class()) + key_pem = priv_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ).decode("utf-8") + now = datetime.now(dt_timezone.utc) + subject = issuer = x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "test")] + ) + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(priv_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now) + .not_valid_after(now + timedelta(days=10)) + .sign(priv_key, digest) + ) + cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode("utf-8") + ca = Ca( + name=f"EC-{length}", + certificate=cert_pem, + private_key=key_pem, + ) + ca.full_clean() + ca.save() + self.assertEqual(ca.key_type, "ec") + self.assertEqual(ca.key_length, length) + gen_ca = Ca( + name=f"Gen-EC-{length}", + key_type="ec", + key_length=length, + ) + gen_ca.full_clean() + gen_ca.save() + self.assertIsInstance(gen_ca.pkey, ec.EllipticCurvePrivateKey) + original_cert = gen_ca.certificate + gen_ca.renew() + self.assertNotEqual(original_cert, gen_ca.certificate) From 00b6542305a98e3042bb2536fc87773df1c4b281 Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sun, 11 Jan 2026 18:48:25 +0530 Subject: [PATCH 08/18] [Improvements] Added additional assertions for completeness #118 Added assertions to verify imported CA key type and verify renewal preserves key metadata. Fixes #118 --- django_x509/tests/test_ca.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/django_x509/tests/test_ca.py b/django_x509/tests/test_ca.py index 7fb68b3..a1bd276 100644 --- a/django_x509/tests/test_ca.py +++ b/django_x509/tests/test_ca.py @@ -724,6 +724,7 @@ def test_ecdsa_full_lifecycle(self): ca.save() self.assertEqual(ca.key_type, "ec") self.assertEqual(ca.key_length, length) + self.assertIsInstance(ca.pkey, ec.EllipticCurvePrivateKey) gen_ca = Ca( name=f"Gen-EC-{length}", key_type="ec", @@ -734,4 +735,8 @@ def test_ecdsa_full_lifecycle(self): self.assertIsInstance(gen_ca.pkey, ec.EllipticCurvePrivateKey) original_cert = gen_ca.certificate gen_ca.renew() + gen_ca.refresh_from_db() + self.assertEqual(gen_ca.key_type, "ec") + self.assertEqual(gen_ca.key_length, length) + self.assertNotEqual(gen_ca.private_key, original_cert) self.assertNotEqual(original_cert, gen_ca.certificate) From 1a5604ffc90b7bea500ab14b8285bc4de4747e54 Mon Sep 17 00:00:00 2001 From: stktyagi Date: Wed, 14 Jan 2026 13:39:58 +0530 Subject: [PATCH 09/18] [tests] Added test to verify certificates with added implementation #118 Verifies Cert ECDSA import, generation, and renewal issued by an ECDSA CA. Fixes #118 --- django_x509/base/models.py | 1 - django_x509/tests/test_ca.py | 2 +- django_x509/tests/test_cert.py | 60 +++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/django_x509/base/models.py b/django_x509/base/models.py index 4ac7854..55f52e4 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -201,7 +201,6 @@ def clean(self): ) } ) - if self.key_type == "ec" and self.key_length in RSA_KEY_LENGTHS: raise ValidationError( {"key_length": _("Selected length is not valid for Elliptic Curve.")} diff --git a/django_x509/tests/test_ca.py b/django_x509/tests/test_ca.py index a1bd276..ee6d2c3 100644 --- a/django_x509/tests/test_ca.py +++ b/django_x509/tests/test_ca.py @@ -686,7 +686,7 @@ def test_renewal_serial_sync(self): pem_serial = cert_obj.serial_number self.assertEqual(int(cert.serial_number), pem_serial) - def test_ecdsa_full_lifecycle(self): + def test_ca_ecdsa_full_lifecycle(self): curves_to_test = [ ("256", ec.SECP256R1, hashes.SHA256()), ("384", ec.SECP384R1, hashes.SHA384()), diff --git a/django_x509/tests/test_cert.py b/django_x509/tests/test_cert.py index 4c33fb6..7e17c5d 100644 --- a/django_x509/tests/test_cert.py +++ b/django_x509/tests/test_cert.py @@ -2,7 +2,8 @@ from datetime import timezone as dt_timezone from cryptography import x509 -from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.x509.oid import NameOID from django.core.exceptions import ValidationError from django.test import TestCase @@ -490,3 +491,60 @@ def test_cert_common_name_length(self): message_dict = context_manager.exception.message_dict self.assertIn("common_name", message_dict) self.assertEqual(message_dict["common_name"][0], msg) + + def test_cert_ecdsa_full_lifecycle(self): + curves_to_test = [ + ("256", ec.SECP256R1, hashes.SHA256()), + ("384", ec.SECP384R1, hashes.SHA384()), + ("521", ec.SECP521R1, hashes.SHA512()), + ] + for length, curve_class, digest in curves_to_test: + with self.subTest(key_length=length): + ca = Ca(name=f"CA-{length}", key_type="ec", key_length=length) + ca.full_clean() + ca.save() + priv_key = ec.generate_private_key(curve_class()) + key_pem = priv_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ).decode("utf-8") + now = datetime.now(dt_timezone.utc) + subject = x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "test-cert")] + ) + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(ca.x509.subject) + .public_key(priv_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now) + .not_valid_after(now + timedelta(days=10)) + .sign(ca.pkey, digest) + ) + cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode("utf-8") + entity_cert = Cert( + name=f"EC-{length}-Import", + ca=ca, + certificate=cert_pem, + private_key=key_pem, + ) + entity_cert.full_clean() + entity_cert.save() + self.assertEqual(entity_cert.key_type, "ec") + self.assertEqual(entity_cert.key_length, length) + gen_cert = Cert( + name=f"Gen-EC-{length}", + ca=ca, + key_type="ec", + key_length=length, + ) + gen_cert.full_clean() + gen_cert.save() + self.assertIsInstance(gen_cert.pkey, ec.EllipticCurvePrivateKey) + original_pem = gen_cert.certificate + gen_cert.renew() + gen_cert.refresh_from_db() + self.assertEqual(gen_cert.key_type, "ec") + self.assertNotEqual(original_pem, gen_cert.certificate) From 7b4217a6a788e6b21a7d98fa64a949047d49be2c Mon Sep 17 00:00:00 2001 From: stktyagi Date: Wed, 14 Jan 2026 13:50:20 +0530 Subject: [PATCH 10/18] [fix] Fixed Aasertion comparing private_key with certificate variable #118 Fixed inconsistent assertion by storing and comparing the original private key. Fixes #118 --- django_x509/tests/test_ca.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django_x509/tests/test_ca.py b/django_x509/tests/test_ca.py index ee6d2c3..6c7125e 100644 --- a/django_x509/tests/test_ca.py +++ b/django_x509/tests/test_ca.py @@ -734,9 +734,10 @@ def test_ca_ecdsa_full_lifecycle(self): gen_ca.save() self.assertIsInstance(gen_ca.pkey, ec.EllipticCurvePrivateKey) original_cert = gen_ca.certificate + original_key = gen_ca.private_key gen_ca.renew() gen_ca.refresh_from_db() self.assertEqual(gen_ca.key_type, "ec") self.assertEqual(gen_ca.key_length, length) - self.assertNotEqual(gen_ca.private_key, original_cert) + self.assertNotEqual(gen_ca.private_key, original_key) self.assertNotEqual(original_cert, gen_ca.certificate) From 4519ac2025b8a01323e2384bddc718b93af6df3f Mon Sep 17 00:00:00 2001 From: stktyagi Date: Wed, 14 Jan 2026 21:04:22 +0530 Subject: [PATCH 11/18] [fix] Fixed incomplete digest normalization pattern #118 Updated digest pattern to more generalized approach to handle every pattern. Fixes #118 --- django_x509/base/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_x509/base/models.py b/django_x509/base/models.py index 55f52e4..8588eea 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -353,7 +353,7 @@ def _generate(self): self.digest.lower() .replace("withrsaencryption", "") .replace("ecdsa-with-", "") - .replace("withsha384", "sha384") + .replace("withsha", "sha") ) digest_alg = HASH_MAP.get(digest_name, hashes.SHA256)() cert = builder.sign(signing_key, digest_alg) From e3eaa43dfca09d2598f68afb42ca4f218d9908ae Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sat, 17 Jan 2026 02:49:26 +0530 Subject: [PATCH 12/18] [refactor] Added ECDSA support for CAs and Certificates #118 Implemented ECDSA support using internal logic to automatically infer the algorithm from the key_length choice, ensuring full backward compatibility without adding new database fields or migrations. Fixes #118 --- django_x509/base/models.py | 125 ++++++++++++------ ...t_key_type_alter_ca_key_length_and_more.py | 72 ---------- ...ter_ca_key_length_alter_cert_key_length.py | 54 -------- ...ter_ca_key_length_alter_cert_key_length.py | 52 -------- django_x509/tests/test_ca.py | 4 +- django_x509/tests/test_cert.py | 6 +- ...t_key_type_customcert_key_type_and_more.py | 101 -------------- ...y_length_alter_cert_key_length_and_more.py | 74 ----------- ...y_length_alter_cert_key_length_and_more.py | 71 ---------- 9 files changed, 85 insertions(+), 474 deletions(-) delete mode 100644 django_x509/migrations/0010_ca_key_type_cert_key_type_alter_ca_key_length_and_more.py delete mode 100644 django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py delete mode 100644 django_x509/migrations/0012_alter_ca_key_length_alter_cert_key_length.py delete mode 100644 tests/openwisp2/sample_x509/migrations/0003_ca_key_type_cert_key_type_customcert_key_type_and_more.py delete mode 100644 tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py delete mode 100644 tests/openwisp2/sample_x509/migrations/0005_alter_ca_key_length_alter_cert_key_length_and_more.py diff --git a/django_x509/base/models.py b/django_x509/base/models.py index 8588eea..eb2b4c2 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -19,18 +19,13 @@ from .. import settings as app_settings KEY_LENGTH_CHOICES = ( - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("512", "512"), - ("1024", "1024"), - ("2048", "2048"), - ("4096", "4096"), -) - -KEY_TYPE_CHOICES = ( - ("rsa", "RSA"), - ("ec", "Elliptic Curve"), + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("512", "512 (RSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), ) RSA_KEY_LENGTHS = ("512", "1024", "2048", "4096") @@ -159,9 +154,6 @@ class BaseX509(models.Model): blank=True, help_text=_("Passphrase for the private key, if present"), ) - key_type = models.CharField( - _("key type"), max_length=3, choices=KEY_TYPE_CHOICES, default="rsa" - ) class Meta: abstract = True @@ -192,19 +184,54 @@ def clean(self): if self.serial_number: self._validate_serial_number() self._verify_extension_format() - if self.key_type == "rsa" and self.key_length in EC_KEY_LENGTHS: - raise ValidationError( - { - "key_length": _( - "Selected length is only valid for Elliptic Curve keys. " - "RSA keys must use 512, 1024, 2048, or 4096 bits." - ) - } - ) - if self.key_type == "ec" and self.key_length in RSA_KEY_LENGTHS: - raise ValidationError( - {"key_length": _("Selected length is not valid for Elliptic Curve.")} - ) + if self.certificate: + try: + public_key = self.x509.public_key() + is_ec_length = self.key_length in EC_KEY_LENGTHS + is_rsa_length = self.key_length in RSA_KEY_LENGTHS + if isinstance(public_key, rsa.RSAPublicKey): + if is_ec_length: + raise ValidationError( + { + "key_length": _( + "Selected length is only valid for ECDSA, " + "but an RSA key was provided." + ) + } + ) + if str(public_key.key_size) != self.key_length: + raise ValidationError( + { + "key_length": _( + "The provided RSA key size (%s) " + "does not match the selected length." + ) + % public_key.key_size + } + ) + elif isinstance(public_key, ec.EllipticCurvePublicKey): + if is_rsa_length: + raise ValidationError( + { + "key_length": _( + "Selected length is only valid for RSA, " + "but an ECDSA key was provided." + ) + } + ) + actual_ec_length = str(public_key.curve.key_size) + if actual_ec_length != self.key_length: + raise ValidationError( + { + "key_length": _( + "The provided ECDSA curve size (%s) " + "does not match the selected length." + ) + % actual_ec_length + } + ) + except (ValueError, UnsupportedAlgorithm): + pass def save(self, *args, **kwargs): if self._state.adding and not self.certificate and not self.private_key: @@ -307,12 +334,8 @@ def _generate(self): for attr in ["x509", "pkey"]: if attr in self.__dict__: del self.__dict__[attr] - if self.key_type == "rsa": - key = rsa.generate_private_key( - public_exponent=65537, - key_size=int(self.key_length), - ) - elif self.key_type == "ec": + is_ec = self.key_length in EC_KEY_LENGTHS + if is_ec: curves = { "256": ec.SECP256R1(), "384": ec.SECP384R1(), @@ -325,7 +348,10 @@ def _generate(self): ) key = ec.generate_private_key(curve) else: - raise ValidationError(_("Unsupported key type: %s") % self.key_type) + key = rsa.generate_private_key( + public_exponent=65537, + key_size=int(self.key_length), + ) if hasattr(self, "ca"): signing_key = self.ca.pkey issuer_name = self.ca.x509.subject @@ -367,7 +393,7 @@ def _generate(self): encoding=serialization.Encoding.PEM, format=( serialization.PrivateFormat.PKCS8 - if self.key_type == "ec" + if is_ec else serialization.PrivateFormat.TraditionalOpenSSL ), encryption_algorithm=encryption, @@ -405,14 +431,11 @@ def _import(self): cert = self.x509 public_key = cert.public_key() if isinstance(public_key, rsa.RSAPublicKey): - self.key_type = "rsa" - self.key_length = str(public_key.key_size) + actual_length = str(public_key.key_size) + actual_is_ec = False elif isinstance(public_key, ec.EllipticCurvePublicKey): - self.key_type = "ec" - curve_size = str(public_key.curve.key_size) - if curve_size not in EC_KEY_LENGTHS: - raise ValidationError(_("Unsupported EC curve size: %s") % curve_size) - self.key_length = curve_size + actual_length = str(public_key.curve.key_size) + actual_is_ec = True else: raise ValidationError( _( @@ -420,6 +443,22 @@ def _import(self): "Only RSA and EC keys are supported." ) ) + selected_is_ec = self.key_length in EC_KEY_LENGTHS + if selected_is_ec != actual_is_ec: + algorithm_expected = "ECDSA" if selected_is_ec else "RSA" + algorithm_provided = "ECDSA" if actual_is_ec else "RSA" + raise ValidationError( + { + "key_length": _( + "Algorithm mismatch: You selected a length for %s, " + "but the provided certificate contains an %s key." + ) + % (algorithm_expected, algorithm_provided) + } + ) + if actual_is_ec and actual_length not in EC_KEY_LENGTHS: + raise ValidationError(_("Unsupported EC curve size: %s") % actual_length) + self.key_length = actual_length # when importing an end entity certificate if hasattr(self, "ca"): self._verify_ca() diff --git a/django_x509/migrations/0010_ca_key_type_cert_key_type_alter_ca_key_length_and_more.py b/django_x509/migrations/0010_ca_key_type_cert_key_type_alter_ca_key_length_and_more.py deleted file mode 100644 index 09d6a4c..0000000 --- a/django_x509/migrations/0010_ca_key_type_cert_key_type_alter_ca_key_length_and_more.py +++ /dev/null @@ -1,72 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-11 10:50 - -import django_x509.base.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("django_x509", "0009_alter_ca_digest_alter_ca_key_length_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="ca", - name="key_type", - field=models.CharField( - choices=[("rsa", "RSA"), ("ec", "Elliptic Curve")], - default="rsa", - max_length=3, - verbose_name="key type", - ), - ), - migrations.AddField( - model_name="cert", - name="key_type", - field=models.CharField( - choices=[("rsa", "RSA"), ("ec", "Elliptic Curve")], - default="rsa", - max_length=3, - verbose_name="key type", - ), - ), - migrations.AlterField( - model_name="ca", - name="key_length", - field=models.CharField( - choices=[ - ("224", "224"), - ("256", "256"), - ("384", "384"), - ("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", - ), - ), - migrations.AlterField( - model_name="cert", - name="key_length", - field=models.CharField( - choices=[ - ("224", "224"), - ("256", "256"), - ("384", "384"), - ("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", - ), - ), - ] diff --git a/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py b/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py deleted file mode 100644 index 3785051..0000000 --- a/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-11 11:10 - -import django_x509.base.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("django_x509", "0010_ca_key_type_cert_key_type_alter_ca_key_length_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="ca", - name="key_length", - field=models.CharField( - choices=[ - ("224", "224"), - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("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", - ), - ), - migrations.AlterField( - model_name="cert", - name="key_length", - field=models.CharField( - choices=[ - ("224", "224"), - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("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", - ), - ), - ] diff --git a/django_x509/migrations/0012_alter_ca_key_length_alter_cert_key_length.py b/django_x509/migrations/0012_alter_ca_key_length_alter_cert_key_length.py deleted file mode 100644 index 436ab75..0000000 --- a/django_x509/migrations/0012_alter_ca_key_length_alter_cert_key_length.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-11 11:47 - -import django_x509.base.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("django_x509", "0011_alter_ca_key_length_alter_cert_key_length"), - ] - - operations = [ - migrations.AlterField( - model_name="ca", - name="key_length", - field=models.CharField( - choices=[ - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("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", - ), - ), - migrations.AlterField( - model_name="cert", - name="key_length", - field=models.CharField( - choices=[ - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("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", - ), - ), - ] diff --git a/django_x509/tests/test_ca.py b/django_x509/tests/test_ca.py index 6c7125e..ffefcc8 100644 --- a/django_x509/tests/test_ca.py +++ b/django_x509/tests/test_ca.py @@ -719,15 +719,14 @@ def test_ca_ecdsa_full_lifecycle(self): name=f"EC-{length}", certificate=cert_pem, private_key=key_pem, + key_length=length, ) ca.full_clean() ca.save() - self.assertEqual(ca.key_type, "ec") self.assertEqual(ca.key_length, length) self.assertIsInstance(ca.pkey, ec.EllipticCurvePrivateKey) gen_ca = Ca( name=f"Gen-EC-{length}", - key_type="ec", key_length=length, ) gen_ca.full_clean() @@ -737,7 +736,6 @@ def test_ca_ecdsa_full_lifecycle(self): original_key = gen_ca.private_key gen_ca.renew() gen_ca.refresh_from_db() - self.assertEqual(gen_ca.key_type, "ec") self.assertEqual(gen_ca.key_length, length) self.assertNotEqual(gen_ca.private_key, original_key) self.assertNotEqual(original_cert, gen_ca.certificate) diff --git a/django_x509/tests/test_cert.py b/django_x509/tests/test_cert.py index 7e17c5d..f0f50a1 100644 --- a/django_x509/tests/test_cert.py +++ b/django_x509/tests/test_cert.py @@ -500,7 +500,7 @@ def test_cert_ecdsa_full_lifecycle(self): ] for length, curve_class, digest in curves_to_test: with self.subTest(key_length=length): - ca = Ca(name=f"CA-{length}", key_type="ec", key_length=length) + ca = Ca(name=f"CA-{length}", key_length=length) ca.full_clean() ca.save() priv_key = ec.generate_private_key(curve_class()) @@ -529,15 +529,14 @@ def test_cert_ecdsa_full_lifecycle(self): ca=ca, certificate=cert_pem, private_key=key_pem, + key_length=length, ) entity_cert.full_clean() entity_cert.save() - self.assertEqual(entity_cert.key_type, "ec") self.assertEqual(entity_cert.key_length, length) gen_cert = Cert( name=f"Gen-EC-{length}", ca=ca, - key_type="ec", key_length=length, ) gen_cert.full_clean() @@ -546,5 +545,4 @@ def test_cert_ecdsa_full_lifecycle(self): original_pem = gen_cert.certificate gen_cert.renew() gen_cert.refresh_from_db() - self.assertEqual(gen_cert.key_type, "ec") self.assertNotEqual(original_pem, gen_cert.certificate) diff --git a/tests/openwisp2/sample_x509/migrations/0003_ca_key_type_cert_key_type_customcert_key_type_and_more.py b/tests/openwisp2/sample_x509/migrations/0003_ca_key_type_cert_key_type_customcert_key_type_and_more.py deleted file mode 100644 index 5974816..0000000 --- a/tests/openwisp2/sample_x509/migrations/0003_ca_key_type_cert_key_type_customcert_key_type_and_more.py +++ /dev/null @@ -1,101 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-11 10:55 - -import django_x509.base.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("sample_x509", "0002_common_name_max_length"), - ] - - operations = [ - migrations.AddField( - model_name="ca", - name="key_type", - field=models.CharField( - choices=[("rsa", "RSA"), ("ec", "Elliptic Curve")], - default="rsa", - max_length=3, - verbose_name="key type", - ), - ), - migrations.AddField( - model_name="cert", - name="key_type", - field=models.CharField( - choices=[("rsa", "RSA"), ("ec", "Elliptic Curve")], - default="rsa", - max_length=3, - verbose_name="key type", - ), - ), - migrations.AddField( - model_name="customcert", - name="key_type", - field=models.CharField( - choices=[("rsa", "RSA"), ("ec", "Elliptic Curve")], - default="rsa", - max_length=3, - verbose_name="key type", - ), - ), - migrations.AlterField( - model_name="ca", - name="key_length", - field=models.CharField( - choices=[ - ("224", "224"), - ("256", "256"), - ("384", "384"), - ("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", - ), - ), - migrations.AlterField( - model_name="cert", - name="key_length", - field=models.CharField( - choices=[ - ("224", "224"), - ("256", "256"), - ("384", "384"), - ("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", - ), - ), - migrations.AlterField( - model_name="customcert", - name="key_length", - field=models.CharField( - choices=[ - ("224", "224"), - ("256", "256"), - ("384", "384"), - ("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", - ), - ), - ] diff --git a/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py b/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py deleted file mode 100644 index b00e62e..0000000 --- a/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py +++ /dev/null @@ -1,74 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-11 11:11 - -import django_x509.base.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("sample_x509", "0003_ca_key_type_cert_key_type_customcert_key_type_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="ca", - name="key_length", - field=models.CharField( - choices=[ - ("224", "224"), - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("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", - ), - ), - migrations.AlterField( - model_name="cert", - name="key_length", - field=models.CharField( - choices=[ - ("224", "224"), - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("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", - ), - ), - migrations.AlterField( - model_name="customcert", - name="key_length", - field=models.CharField( - choices=[ - ("224", "224"), - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("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", - ), - ), - ] diff --git a/tests/openwisp2/sample_x509/migrations/0005_alter_ca_key_length_alter_cert_key_length_and_more.py b/tests/openwisp2/sample_x509/migrations/0005_alter_ca_key_length_alter_cert_key_length_and_more.py deleted file mode 100644 index 6dab86a..0000000 --- a/tests/openwisp2/sample_x509/migrations/0005_alter_ca_key_length_alter_cert_key_length_and_more.py +++ /dev/null @@ -1,71 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-11 11:48 - -import django_x509.base.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("sample_x509", "0004_alter_ca_key_length_alter_cert_key_length_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="ca", - name="key_length", - field=models.CharField( - choices=[ - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("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", - ), - ), - migrations.AlterField( - model_name="cert", - name="key_length", - field=models.CharField( - choices=[ - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("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", - ), - ), - migrations.AlterField( - model_name="customcert", - name="key_length", - field=models.CharField( - choices=[ - ("256", "256"), - ("384", "384"), - ("521", "521"), - ("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", - ), - ), - ] From 94b0793c14a6b9102a13883637ea1031956cf246 Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sat, 17 Jan 2026 03:13:04 +0530 Subject: [PATCH 13/18] [migrations] Add migrations #118 Add migrations files related to changes made to the KEY_LENGTH_CHOICES Fixes #118 --- ...ter_ca_key_length_alter_cert_key_length.py | 52 ++++++++++++++ ...y_length_alter_cert_key_length_and_more.py | 71 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 django_x509/migrations/0010_alter_ca_key_length_alter_cert_key_length.py create mode 100644 tests/openwisp2/sample_x509/migrations/0003_alter_ca_key_length_alter_cert_key_length_and_more.py diff --git a/django_x509/migrations/0010_alter_ca_key_length_alter_cert_key_length.py b/django_x509/migrations/0010_alter_ca_key_length_alter_cert_key_length.py new file mode 100644 index 0000000..7d2c344 --- /dev/null +++ b/django_x509/migrations/0010_alter_ca_key_length_alter_cert_key_length.py @@ -0,0 +1,52 @@ +# Generated by Django 6.0.1 on 2026-01-16 21:41 + +import django_x509.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("django_x509", "0009_alter_ca_digest_alter_ca_key_length_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="ca", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("512", "512 (RSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), + ], + default=django_x509.base.models.default_key_length, + help_text="bits", + max_length=6, + verbose_name="key length", + ), + ), + migrations.AlterField( + model_name="cert", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("512", "512 (RSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), + ], + default=django_x509.base.models.default_key_length, + help_text="bits", + max_length=6, + verbose_name="key length", + ), + ), + ] diff --git a/tests/openwisp2/sample_x509/migrations/0003_alter_ca_key_length_alter_cert_key_length_and_more.py b/tests/openwisp2/sample_x509/migrations/0003_alter_ca_key_length_alter_cert_key_length_and_more.py new file mode 100644 index 0000000..2527bb8 --- /dev/null +++ b/tests/openwisp2/sample_x509/migrations/0003_alter_ca_key_length_alter_cert_key_length_and_more.py @@ -0,0 +1,71 @@ +# Generated by Django 6.0.1 on 2026-01-16 21:40 + +import django_x509.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sample_x509", "0002_common_name_max_length"), + ] + + operations = [ + migrations.AlterField( + model_name="ca", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("512", "512 (RSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), + ], + default=django_x509.base.models.default_key_length, + help_text="bits", + max_length=6, + verbose_name="key length", + ), + ), + migrations.AlterField( + model_name="cert", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("512", "512 (RSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), + ], + default=django_x509.base.models.default_key_length, + help_text="bits", + max_length=6, + verbose_name="key length", + ), + ), + migrations.AlterField( + model_name="customcert", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("512", "512 (RSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), + ], + default=django_x509.base.models.default_key_length, + help_text="bits", + max_length=6, + verbose_name="key length", + ), + ), + ] From 54316bdcd35e5040e6e7b348acd75d64e8b2930d Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sat, 17 Jan 2026 03:57:15 +0530 Subject: [PATCH 14/18] [refactor] Simplified clean() to previous better implementation #118 Modified clean() to check for unsupported key lengths and removed previous complex mismatch checks Fixes #118 --- django_x509/base/models.py | 59 ++++++-------------------------------- 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/django_x509/base/models.py b/django_x509/base/models.py index eb2b4c2..844e801 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -171,6 +171,9 @@ def clean_fields(self, *args, **kwargs): super().clean_fields(*args, **kwargs) def clean(self): + if self.serial_number: + self._validate_serial_number() + self._verify_extension_format() # when importing, both public and private must be present if (self.certificate and not self.private_key) or ( self.private_key and not self.certificate @@ -181,57 +184,11 @@ def clean(self): "keys (private and public) must be present" ) ) - if self.serial_number: - self._validate_serial_number() - self._verify_extension_format() - if self.certificate: - try: - public_key = self.x509.public_key() - is_ec_length = self.key_length in EC_KEY_LENGTHS - is_rsa_length = self.key_length in RSA_KEY_LENGTHS - if isinstance(public_key, rsa.RSAPublicKey): - if is_ec_length: - raise ValidationError( - { - "key_length": _( - "Selected length is only valid for ECDSA, " - "but an RSA key was provided." - ) - } - ) - if str(public_key.key_size) != self.key_length: - raise ValidationError( - { - "key_length": _( - "The provided RSA key size (%s) " - "does not match the selected length." - ) - % public_key.key_size - } - ) - elif isinstance(public_key, ec.EllipticCurvePublicKey): - if is_rsa_length: - raise ValidationError( - { - "key_length": _( - "Selected length is only valid for RSA, " - "but an ECDSA key was provided." - ) - } - ) - actual_ec_length = str(public_key.curve.key_size) - if actual_ec_length != self.key_length: - raise ValidationError( - { - "key_length": _( - "The provided ECDSA curve size (%s) " - "does not match the selected length." - ) - % actual_ec_length - } - ) - except (ValueError, UnsupportedAlgorithm): - pass + all_supported = list(RSA_KEY_LENGTHS) + list(EC_KEY_LENGTHS) + if self.key_length not in all_supported: + raise ValidationError( + {"key_length": _("Unsupported key length: %s") % self.key_length} + ) def save(self, *args, **kwargs): if self._state.adding and not self.certificate and not self.private_key: From 8b56b1315160763caa46227bdb119cf2d6a3bdf2 Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sat, 17 Jan 2026 21:31:53 +0530 Subject: [PATCH 15/18] [fix] Remove 512-bit RSA support #118 Remove 512-bit RSA support from models and refactor test data by replacing 512-bit certificates with 2048-bit RSA Fixes #118 --- django_x509/base/models.py | 3 +- ...ter_ca_key_length_alter_cert_key_length.py | 50 ++++++ django_x509/tests/test_ca.py | 80 ++++++---- django_x509/tests/test_cert.py | 143 ++++++++++++------ ...y_length_alter_cert_key_length_and_more.py | 68 +++++++++ 5 files changed, 269 insertions(+), 75 deletions(-) create mode 100644 django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py create mode 100644 tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py diff --git a/django_x509/base/models.py b/django_x509/base/models.py index 844e801..c1b6ea6 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -22,13 +22,12 @@ ("256", "256 (ECDSA)"), ("384", "384 (ECDSA)"), ("521", "521 (ECDSA)"), - ("512", "512 (RSA)"), ("1024", "1024 (RSA)"), ("2048", "2048 (RSA)"), ("4096", "4096 (RSA)"), ) -RSA_KEY_LENGTHS = ("512", "1024", "2048", "4096") +RSA_KEY_LENGTHS = ("1024", "2048", "4096") EC_KEY_LENGTHS = ("256", "384", "521") DIGEST_CHOICES = ( diff --git a/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py b/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py new file mode 100644 index 0000000..f7cc883 --- /dev/null +++ b/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py @@ -0,0 +1,50 @@ +# Generated by Django 6.0.1 on 2026-01-17 15:58 + +import django_x509.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("django_x509", "0010_alter_ca_key_length_alter_cert_key_length"), + ] + + operations = [ + migrations.AlterField( + model_name="ca", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), + ], + default=django_x509.base.models.default_key_length, + help_text="bits", + max_length=6, + verbose_name="key length", + ), + ), + migrations.AlterField( + model_name="cert", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), + ], + default=django_x509.base.models.default_key_length, + help_text="bits", + max_length=6, + verbose_name="key length", + ), + ), + ] diff --git a/django_x509/tests/test_ca.py b/django_x509/tests/test_ca.py index ffefcc8..ed4efb2 100644 --- a/django_x509/tests/test_ca.py +++ b/django_x509/tests/test_ca.py @@ -35,29 +35,56 @@ def _prepare_revoked(self): return (ca, cert) import_certificate = """-----BEGIN CERTIFICATE----- -MIICNDCCAd6gAwIBAgIDAeJAMA0GCSqGSIb3DQEBBQUAMHcxCzAJBgNVBAYTAlVT -MQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwE -QUNNRTETMBEGA1UEAwwKaW1wb3J0dGVzdDEfMB0GCSqGSIb3DQEJARYQY29udGFj -dEBhY21lLmNvbTAeFw0yNTEyMjUwOTIwNTZaFw0yNjEyMjUwOTIwNTZaMHcxCzAJ -BgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEN -MAsGA1UECgwEQUNNRTETMBEGA1UEAwwKaW1wb3J0dGVzdDEfMB0GCSqGSIb3DQEJ -ARYQY29udGFjdEBhY21lLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDHAPm2 -hfj7hkbrXAy+Cw2XbyUXFqvUpP7fJPcTXIBTqKknTiFslf4XYzMkMK4v+xT/aHrI -AKB/oN+p7sDa44dFAgMBAAGjUzBRMB0GA1UdDgQWBBQgNew7Ykf+970QpbdN2hsg -YHhUCDAfBgNVHSMEGDAWgBQgNew7Ykf+970QpbdN2hsgYHhUCDAPBgNVHRMBAf8E -BTADAQH/MA0GCSqGSIb3DQEBBQUAA0EAj2EzamJKyBjGNEDEvOY4Gyh+kD2843Ay -qtOGFGIC2GdT3K2ewBfITTWzecqaSLcQ2PeJcLLg0i3ra5nXAAZBeg== +MIIDzzCCAregAwIBAgIUQSTDetixAO35vLfJ7jlCH7/UpUcwDQYJKoZIhvcNAQEL +BQAwdzETMBEGA1UEAwwKaW1wb3J0dGVzdDELMAkGA1UEBhMCVVMxCzAJBgNVBAgM +AkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARBQ01FMR8wHQYJ +KoZIhvcNAQkBFhBjb250YWN0QGFjbWUuY29tMB4XDTI2MDExNzA3NDEwNVoXDTM2 +MDExNTA3NDEwNVowdzETMBEGA1UEAwwKaW1wb3J0dGVzdDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARB +Q01FMR8wHQYJKoZIhvcNAQkBFhBjb250YWN0QGFjbWUuY29tMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkyNsaGZatFTvlPQ2Soj4g5kzalPmrLkKEXxY +kNvICJ430Pob1J0N+R5VdhNuwuSaCc4bj5lzyHCvScSZBaTyThXX6deRUW1uk8Ss +8fG+E8JCrAHzKWQVUe7uZJTgKtI6hNBfNzmVHVXvWiFBQRMO4OXOW92hKKPhOIcc +T99QcelNrO1TKT937cngKaSb+0ZcoAspKWfFb0y62XxxArHC/f5nN2p1I8+6h9gQ +26+MRXmxwlvT9qX2TMRBCj36D0jgsCgJ10C7iQjZu3d5FtmbU7dS4DvlCj8pNXcn +S4RxXHrmZKeY3UVk9TNRYyMOd2cHm7FQdrGYWO4xT+5LtPkLcQIDAQABo1MwUTAd +BgNVHQ4EFgQUrnxElH6h9VmQZYHG+aGHSuhDayMwHwYDVR0jBBgwFoAUrnxElH6h +9VmQZYHG+aGHSuhDayMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEACshO+uDpXE779/5zrm6w83IKJHqYnX2pdFMJM1WuJBXlo0r+WMrwDTarQc+I +NhuL60bnoYrmrja8o5cOuBBMqpIn2ct1H7xE4C0t6BY4+khmEBLM700oxKWhOThG +IKAcdLrbqGECQdbttMS5kiMhlH5mQANtnPQFHZgua/kPrBjIeeOzK0Wt+2Lnd3/o +q24y18BVEbJAZxTsEberrvrSAxrdSNk9A4nMrz5UpjOxJ4QWKJctGjUjZrCtpLqP +/fPO6RV+C1jIBYvP2NduuCiQgCqfRArPqhqqWbQodUCwBL8mTu/piL5e1dIotYwH +EQZrw8bbikXRSH3D31NVroN7fw== -----END CERTIFICATE-----""" import_private_key = """-----BEGIN PRIVATE KEY----- -MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEAxwD5toX4+4ZG61wM -vgsNl28lFxar1KT+3yT3E1yAU6ipJ04hbJX+F2MzJDCuL/sU/2h6yACgf6Dfqe7A -2uOHRQIDAQABAkEAwQApLuPv/cDUtx6nHQkLPXsFtca/D5SVu0TWe2iS7I5IQuXN -XIwUScvp9rd/GnSuphyMXrgE8XI3nn1baTS7AQIhAO6eXAYz4cQHWPWoMS75S6O/ -uE/ZJVCfBUonFfmS2ELpAiEA1X/mNy4pacJSJLpGv1vajOIzXR76W9ud5lHh23U3 -z/0CIBj7ES1BDzijgEevhP6i8K1C6/vIAuUO0NHzh5RqMCPJAiEAzBJjyCzMkvWW -NNsE0taGsZFpjUIWBoWGiWeNHosNnTUCIQC1wzMjWku4OXk030WC4fFeJwU060WT -9iuP3scMQKQZdg== +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCTI2xoZlq0VO+U +9DZKiPiDmTNqU+asuQoRfFiQ28gInjfQ+hvUnQ35HlV2E27C5JoJzhuPmXPIcK9J +xJkFpPJOFdfp15FRbW6TxKzx8b4TwkKsAfMpZBVR7u5klOAq0jqE0F83OZUdVe9a +IUFBEw7g5c5b3aEoo+E4hxxP31Bx6U2s7VMpP3ftyeAppJv7RlygCykpZ8VvTLrZ +fHECscL9/mc3anUjz7qH2BDbr4xFebHCW9P2pfZMxEEKPfoPSOCwKAnXQLuJCNm7 +d3kW2ZtTt1LgO+UKPyk1dydLhHFceuZkp5jdRWT1M1FjIw53ZwebsVB2sZhY7jFP +7ku0+QtxAgMBAAECggEANH9kE4/JdyQC41uK72cVfCayMJLE8AWJcRmzo+O26FRD +R/2k5mQu8x5+kYV3dHQJ/cubC85NgEusTx6lFl120qN6iQWP5MStum1m42BEWFps +XWDIuJDsBnLAfgScQssFdBPAlTynVnMt1jOdS7GYEmgMC7z03kIyfm++i0T7N9ji +fyN2CFOXgevgHK5EtTSrBTzg8JkFnhNZKjHPU9IkRyaN8KtOwKrEgxh0glvNM6yp +cmU8PE+DPK4TSQGsIO4X4Z19wKv7O8x6CYLos8w2Yh9jwMHaGeDnv68RVFoY1vgH +q/PJcWylRanDeyoShIm3v2qBCQcBtUqDUqdTotww7QKBgQDH2LeZ4rn7SUuRkt7l +YeM4YtlbWSlycTh1KphqkM3UW0MQA/HcJKgFacbxdEj9hV0Ol/l76qQv39V1Ts3X +Zsf1eSGLdrvi2JYrlh0WYhFco/g7Cqt43dNfqSAnNKb8ihRq3X8zsQ7nTDU3wWoK +fYjeHyYRFYPi7jQF3sYcmrM5OwKBgQC8e1KPKQ2eIJNC5rYbolxRgrSOMxr+AxgD +iiNdhPT0lpeYEPW65z/pbdiiehXt8zrrJhtfDDe7f0dHaYkbSJWq5dzjAqnfwNGO ++gr5+skTL5nkimjbinGHN8+2134eMxh4WmsyQvkY23wk8SJlC9/57/TQvjRNZUAw +nmBmQdojQwKBgQCWFXl9RjqqLxdjkkt3NRZxyDq4UbPA0Kq3w2+HyIvryUYKBwxi +ad0Ng6z2tIAEdV23kga5OzRnB9DFMpOACx5sibXZiSf9au8MeMYLg0bKrhHENXUl +ZmJR2y/cgbxOuFwxDXt0FKq+pgrpfoXmrvRU7EuoVOIhUQccyXs7DCtA9QKBgEoS +vVN98tgePUGhohgiKt3t3D+2XflOBfX+J//s7MfjFxiwMaKOl1OJ1AWmrU+is5kO +lNs51f1d/AlYtIWAdTGAvNqKhXBmOvVR11Z+9N8Rag2jR6pgMlXN3VgiQHJl6kwC +XPaX04WtXJC4I6hKjm+PmksfNTblf+CbnY8SekQ5AoGBALvK6n2UGG+0fKtUFM+M +0enYrqW065OsDOIXo7roOVZjBhul+vilk7xJ85fhtDD0zll/4OoJ1BG9IN0rUUJh +eMIi3AQRO060kYejKGC2ls5hMxjdhxB/bUGrwXkMxZQPfjBkcP3JqaZkwcFBKq5N +tsND+97h9r73S+UTOhepQTDB -----END PRIVATE KEY-----""" def test_new(self): @@ -133,7 +160,8 @@ def test_import_ca(self): ca.save() cert = ca.x509 # verify attributes - self.assertEqual(cert.serial_number, 123456) + serial = 371904255628934431598705194442539630076148098375 + self.assertEqual(cert.serial_number, serial) subject = cert.subject self.assertEqual( subject.get_attributes_for_oid(NameOID.COUNTRY_NAME)[0].value, "US" @@ -158,18 +186,18 @@ def test_import_ca(self): ) self.assertEqual(cert.issuer, cert.subject) # verify field attribtues - self.assertEqual(ca.key_length, "512") - self.assertEqual(ca.digest, "sha1") + self.assertEqual(ca.key_length, "2048") + self.assertEqual(ca.digest, "sha256") 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(int(ca.serial_number), 123456) + self.assertEqual(int(ca.serial_number), serial) self.assertEqual(ca.name, "ImportTest") - start = datetime(2025, 12, 25, 9, 20, 56, tzinfo=dt_timezone.utc) - end = datetime(2026, 12, 25, 9, 20, 56, tzinfo=dt_timezone.utc) + start = datetime(2026, 1, 17, 7, 41, 5, tzinfo=dt_timezone.utc) + end = datetime(2036, 1, 15, 7, 41, 5, tzinfo=dt_timezone.utc) self.assertEqual(ca.validity_start, start) self.assertEqual(ca.validity_end, end) # ensure version is 3 diff --git a/django_x509/tests/test_cert.py b/django_x509/tests/test_cert.py index f0f50a1..8b5e035 100644 --- a/django_x509/tests/test_cert.py +++ b/django_x509/tests/test_cert.py @@ -20,63 +20,112 @@ class TestCert(AssertNumQueriesSubTestMixin, TestX509Mixin, TestCase): import_certificate = """ -----BEGIN CERTIFICATE----- -MIICMTCCAdugAwIBAgIDAeJAMA0GCSqGSIb3DQEBBQUAMGgxETAPBgNVBAoMCE9w -ZW5XSVNQMQswCQYDVQQGEwJJVDEMMAoGA1UEAwwDb3cyMQ0wCwYDVQQHDARSb21l -MRwwGgYJKoZIhvcNAQkBFg10ZXN0QHRlc3QuY29tMQswCQYDVQQIDAJSTTAiGA8y -MDE1MTEwMTAwMDAwMFoYDzIxMTgxMTAyMTgwMDI1WjAAMFwwDQYJKoZIhvcNAQEB -BQADSwAwSAJBANh0Y7oG5JUl9cCBs6E11cJ2xLul6zw8cEoD1L7NazrPXG/NGTLt -OF2TOEUob24aQ+YagMD6HLbejV0baTXwXakCAwEAAaOB0TCBzjAJBgNVHRMEAjAA -MAsGA1UdDwQEAwIFoDAdBgNVHQ4EFgQUpcvUDhxzJFpMvjlTQjBaCjQI/3QwgZQG -A1UdIwSBjDCBiYAUwfnP0B5rF3xo7yDRAda+1nj6QqahbKRqMGgxETAPBgNVBAoM -CE9wZW5XSVNQMQswCQYDVQQGEwJJVDEMMAoGA1UEAwwDb3cyMQ0wCwYDVQQHDARS -b21lMRwwGgYJKoZIhvcNAQkBFg10ZXN0QHRlc3QuY29tMQswCQYDVQQIDAJSTYID -AeJAMA0GCSqGSIb3DQEBBQUAA0EAUKog+BPsM8j34Clec2BAACcuyJlwX41vQ3kG -FqQS2KfO7YIk5ITWhX8y0P//u+ENWRlnVTRQma9d5tYYJvL8+Q== +MIIDNjCCAh6gAwIBAgIDAeJAMA0GCSqGSIb3DQEBCwUAMHcxEzARBgNVBAMMCmlt +cG9ydHRlc3QxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2Fu +IEZyYW5jaXNjbzENMAsGA1UECgwEQUNNRTEfMB0GCSqGSIb3DQEJARYQY29udGFj +dEBhY21lLmNvbTAeFw0yNjAxMTcxNTUxNTVaFw0zNjAxMTUxNTUxNTVaMAAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5NNAOi6Fqj+1bDT/PXSqnB6qc +1F1j9DYvGw4/YBZTDvyII66rQQ34/SgEkNhYH5f5Q7xxEaZHD29TGUE3wj+sps8L +WrgJd6shzIHYArZNQ21ZIy93aTZT87KznCBcsr0pFr6yHa0rVAug/x4dtxhq2wGA +ESdwkzbTPP3yXjASLt20CMBQP7ZSIVmJSO/ZJ+ukdFz7psUnZhpXcav97lLsi8+A +yXaRDSBkDOjiw0p0JO6syOxX7CNohNFJstiLc20A2YahN00RfjSRq92+3iLUHzVW +RpiQs2EA3lg4SSre3dthqvWTNm48Sdy1x5AgipRHipaa/XSiAVbBkHwn/g1rAgMB +AAGjQjBAMB0GA1UdDgQWBBSgr3fucGsd3A1fX0A2I0Ai9yy5ajAfBgNVHSMEGDAW +gBSufESUfqH1WZBlgcb5oYdK6ENrIzANBgkqhkiG9w0BAQsFAAOCAQEACRNSrr20 +rpl8WSo+hzavnE6jzd3S4PYl4IU3cY03C/1uJ7ode00TOpq0LGIC5QToxcUNOqmb +AU+JINaEXXy5jyud/p/SzsN1jXfP1MQcvTyL4thxcUftXYeoFaq+u8YgRFbuzCeD +cnn0S+QXNKIOkASEEuEWRaMjWFDL17CNhEsW2HXV3MOhZv5L9ft5ua007LfDGTk9 +1Y+RQ+6/nAn9K0zRoKebnWWVWfgmUDCRYronbn13BU0elecMGqcsZzq+zTo8ePDG ++Hx97JA9mJhRsGQkIAYk24SKF9mOPUb2MCUHW41UHa09yUlKENgshwR5zMksioZ6 +CdicXI48yT96WQ== -----END CERTIFICATE----- """ import_private_key = """ -----BEGIN PRIVATE KEY----- -MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2HRjugbklSX1wIGz -oTXVwnbEu6XrPDxwSgPUvs1rOs9cb80ZMu04XZM4RShvbhpD5hqAwPoctt6NXRtp -NfBdqQIDAQABAkEAx9M7NcOjRqXSqDOU92DRxEMNAAb+kY9iQpIi1zqgoZqWduVK -tq0X0ous54j2ItqKDHxqEbbBzlo/BxMn5zkdOQIhAPIlngBgjgM0FFt+4bw6+5mW -VvjxIQoVHkmd1HsfHkPvAiEA5NZ+Zqbbv6T7oLgixye1nbcJ3mQ5+IUuamGp7dVq -/+cCIQDpxVNCffTcNt0ob9gyRqc74Z5Ze0EwYK761zqZGrO3VQIgYp0UZ4QsWo/s -Z7wyMISqPUbtl8q1OKWb9PgVVIqNy60CIEpi865urZNSIz4SRrxn4r+WV9Mxlfxs -1xtxYxSjiqrj +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5NNAOi6Fqj+1b +DT/PXSqnB6qc1F1j9DYvGw4/YBZTDvyII66rQQ34/SgEkNhYH5f5Q7xxEaZHD29T +GUE3wj+sps8LWrgJd6shzIHYArZNQ21ZIy93aTZT87KznCBcsr0pFr6yHa0rVAug +/x4dtxhq2wGAESdwkzbTPP3yXjASLt20CMBQP7ZSIVmJSO/ZJ+ukdFz7psUnZhpX +cav97lLsi8+AyXaRDSBkDOjiw0p0JO6syOxX7CNohNFJstiLc20A2YahN00RfjSR +q92+3iLUHzVWRpiQs2EA3lg4SSre3dthqvWTNm48Sdy1x5AgipRHipaa/XSiAVbB +kHwn/g1rAgMBAAECggEAOFlAqgRCnrzejvLXhLxIY1xaRO/54BTnvWpCafbOpAOt +wq/0j0cyPJytZcI6CInIP78joNUpXXptOP+4j4HqxJlV6hL2Zm8B4r0pjjK5C4Xl +yZaCdRbOQDmnl6z7TajWE5/HckLEMqgWB6xHGexgofYzHSda9A3eQuPOMcUFZCpX +H//sZPCOgkidObZ/jNQ3pH1A5JJ5BdZtz3BFvHkv+OHjEY8nNPbMuylMiOSbyLDZ +zrF9ghSJYp1KpbxdojkzVYJSrZ9P2bDX+JJXqrmQ62LbPbRtETRxHc+VA4u4ZhS/ +/GC20bXkuoOLJg0LJw1R4qEJ8o3g9XVni/cBUzR6ZQKBgQD9nQYKULSG8vlFP711 +uaP2TQhaCMEPESzg1SS6H3lLBH5NYdG5gY8omoNBzoIZk2XHiaDsTFpGMsPZrufP +BvN5VQukaYYe8jPBfq5ktlSbAkvQRVmuFlFT+bA1n/5jZCQyAd1Un8aJBYr+AdNl +BzpCQUeTmP069OO50V4hjbbBJQKBgQC68v2YlGxpOBlngscKGZzSNjcxrZG+erul +ZTt899waDaJEH3cxUjPcEZbAjin7gmsDehWN+kZ5kn4rd3+9M89SH63cd2lQq+fb +pckOH5yvKjRsNCcwG5mlNcAkAWnbOL6ajNbdHVwNwPB4f4fEdsflTkuHgoUWfLB4 +UtY5oTu3TwKBgFWgYXy0GO+DM5Qk3CPWRLyQ76PuVrhulRdn/1lz7PDeGIKp5zRZ +wOr1mCFsxtI5yOBg4FtHwCb5VtS1UAC/GQ87Ho4pLqZeIglPazQHt3MKiGxOLeQw +Fs9iexLv7OTD19CmfoLm2xJCM9Zk6Wmv0gSyo6b6vWzdZ9HCFaUAgtadAoGBAKNg +3BNOEvhZWIpHlh7Th2OGkfHOWEJ5DChdMgHisu3p4FdckFQAHOZEUNTy6OmubktZ +lCDCCnkQd0cRZgc5kgOZP94eVWF0+mnQlsbLBalnXuz5Hw5B8KKbONG+kn5NNvXm +A5i1oc87QGxuN36Qt91D8Wn5vMmMKsTcz+8JYyCtAoGBAK2vieDw6rD8k4UAp4Nn +hYMtcPeVKNPcZxGWzh9abX0YcW02F6kP9xd0Mlv9AhCPl2xwoSB9y1WoOlCzvijq +0pl8/DG+QHhnd0h362CaOuzpuH7863QhoAyIqVejED+XWkmYwjYK3CUqSRnnqFK/ +v/Qdu78ntVYDbDKuoaWE3uPa -----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 +MIIDzzCCAregAwIBAgIUQSTDetixAO35vLfJ7jlCH7/UpUcwDQYJKoZIhvcNAQEL +BQAwdzETMBEGA1UEAwwKaW1wb3J0dGVzdDELMAkGA1UEBhMCVVMxCzAJBgNVBAgM +AkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARBQ01FMR8wHQYJ +KoZIhvcNAQkBFhBjb250YWN0QGFjbWUuY29tMB4XDTI2MDExNzA3NDEwNVoXDTM2 +MDExNTA3NDEwNVowdzETMBEGA1UEAwwKaW1wb3J0dGVzdDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARB +Q01FMR8wHQYJKoZIhvcNAQkBFhBjb250YWN0QGFjbWUuY29tMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkyNsaGZatFTvlPQ2Soj4g5kzalPmrLkKEXxY +kNvICJ430Pob1J0N+R5VdhNuwuSaCc4bj5lzyHCvScSZBaTyThXX6deRUW1uk8Ss +8fG+E8JCrAHzKWQVUe7uZJTgKtI6hNBfNzmVHVXvWiFBQRMO4OXOW92hKKPhOIcc +T99QcelNrO1TKT937cngKaSb+0ZcoAspKWfFb0y62XxxArHC/f5nN2p1I8+6h9gQ +26+MRXmxwlvT9qX2TMRBCj36D0jgsCgJ10C7iQjZu3d5FtmbU7dS4DvlCj8pNXcn +S4RxXHrmZKeY3UVk9TNRYyMOd2cHm7FQdrGYWO4xT+5LtPkLcQIDAQABo1MwUTAd +BgNVHQ4EFgQUrnxElH6h9VmQZYHG+aGHSuhDayMwHwYDVR0jBBgwFoAUrnxElH6h +9VmQZYHG+aGHSuhDayMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEACshO+uDpXE779/5zrm6w83IKJHqYnX2pdFMJM1WuJBXlo0r+WMrwDTarQc+I +NhuL60bnoYrmrja8o5cOuBBMqpIn2ct1H7xE4C0t6BY4+khmEBLM700oxKWhOThG +IKAcdLrbqGECQdbttMS5kiMhlH5mQANtnPQFHZgua/kPrBjIeeOzK0Wt+2Lnd3/o +q24y18BVEbJAZxTsEberrvrSAxrdSNk9A4nMrz5UpjOxJ4QWKJctGjUjZrCtpLqP +/fPO6RV+C1jIBYvP2NduuCiQgCqfRArPqhqqWbQodUCwBL8mTu/piL5e1dIotYwH +EQZrw8bbikXRSH3D31NVroN7fw== -----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 +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCTI2xoZlq0VO+U +9DZKiPiDmTNqU+asuQoRfFiQ28gInjfQ+hvUnQ35HlV2E27C5JoJzhuPmXPIcK9J +xJkFpPJOFdfp15FRbW6TxKzx8b4TwkKsAfMpZBVR7u5klOAq0jqE0F83OZUdVe9a +IUFBEw7g5c5b3aEoo+E4hxxP31Bx6U2s7VMpP3ftyeAppJv7RlygCykpZ8VvTLrZ +fHECscL9/mc3anUjz7qH2BDbr4xFebHCW9P2pfZMxEEKPfoPSOCwKAnXQLuJCNm7 +d3kW2ZtTt1LgO+UKPyk1dydLhHFceuZkp5jdRWT1M1FjIw53ZwebsVB2sZhY7jFP +7ku0+QtxAgMBAAECggEANH9kE4/JdyQC41uK72cVfCayMJLE8AWJcRmzo+O26FRD +R/2k5mQu8x5+kYV3dHQJ/cubC85NgEusTx6lFl120qN6iQWP5MStum1m42BEWFps +XWDIuJDsBnLAfgScQssFdBPAlTynVnMt1jOdS7GYEmgMC7z03kIyfm++i0T7N9ji +fyN2CFOXgevgHK5EtTSrBTzg8JkFnhNZKjHPU9IkRyaN8KtOwKrEgxh0glvNM6yp +cmU8PE+DPK4TSQGsIO4X4Z19wKv7O8x6CYLos8w2Yh9jwMHaGeDnv68RVFoY1vgH +q/PJcWylRanDeyoShIm3v2qBCQcBtUqDUqdTotww7QKBgQDH2LeZ4rn7SUuRkt7l +YeM4YtlbWSlycTh1KphqkM3UW0MQA/HcJKgFacbxdEj9hV0Ol/l76qQv39V1Ts3X +Zsf1eSGLdrvi2JYrlh0WYhFco/g7Cqt43dNfqSAnNKb8ihRq3X8zsQ7nTDU3wWoK +fYjeHyYRFYPi7jQF3sYcmrM5OwKBgQC8e1KPKQ2eIJNC5rYbolxRgrSOMxr+AxgD +iiNdhPT0lpeYEPW65z/pbdiiehXt8zrrJhtfDDe7f0dHaYkbSJWq5dzjAqnfwNGO ++gr5+skTL5nkimjbinGHN8+2134eMxh4WmsyQvkY23wk8SJlC9/57/TQvjRNZUAw +nmBmQdojQwKBgQCWFXl9RjqqLxdjkkt3NRZxyDq4UbPA0Kq3w2+HyIvryUYKBwxi +ad0Ng6z2tIAEdV23kga5OzRnB9DFMpOACx5sibXZiSf9au8MeMYLg0bKrhHENXUl +ZmJR2y/cgbxOuFwxDXt0FKq+pgrpfoXmrvRU7EuoVOIhUQccyXs7DCtA9QKBgEoS +vVN98tgePUGhohgiKt3t3D+2XflOBfX+J//s7MfjFxiwMaKOl1OJ1AWmrU+is5kO +lNs51f1d/AlYtIWAdTGAvNqKhXBmOvVR11Z+9N8Rag2jR6pgMlXN3VgiQHJl6kwC +XPaX04WtXJC4I6hKjm+PmksfNTblf+CbnY8SekQ5AoGBALvK6n2UGG+0fKtUFM+M +0enYrqW065OsDOIXo7roOVZjBhul+vilk7xJ85fhtDD0zll/4OoJ1BG9IN0rUUJh +eMIi3AQRO060kYejKGC2ls5hMxjdhxB/bUGrwXkMxZQPfjBkcP3JqaZkwcFBKq5N +tsND+97h9r73S+UTOhepQTDB -----END PRIVATE KEY----- + """ def test_new(self): @@ -166,14 +215,14 @@ def test_import_cert(self): # verify issuer (using CA subject for comparison) self.assertEqual(x509_obj.issuer, ca.x509.subject) # verify field attributes - self.assertEqual(cert.key_length, "512") - self.assertEqual(cert.digest, "sha1") + self.assertEqual(cert.key_length, "2048") + self.assertEqual(cert.digest, "sha256") self.assertEqual(int(cert.serial_number), 123456) self.assertEqual(cert.country_code, "") self.assertEqual(cert.common_name, "") - start = datetime(2015, 11, 1, 0, 0, 0, tzinfo=dt_timezone.utc) - end = datetime(2118, 11, 2, 18, 0, 25, tzinfo=dt_timezone.utc) + start = datetime(2026, 1, 17, 15, 51, 55, tzinfo=dt_timezone.utc) + end = datetime(2036, 1, 15, 15, 51, 55, tzinfo=dt_timezone.utc) self.assertEqual(cert.validity_start, start) self.assertEqual(cert.validity_end, end) # ensure version is 3 diff --git a/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py b/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py new file mode 100644 index 0000000..f64380f --- /dev/null +++ b/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 6.0.1 on 2026-01-17 15:59 + +import django_x509.base.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sample_x509", "0003_alter_ca_key_length_alter_cert_key_length_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="ca", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), + ], + default=django_x509.base.models.default_key_length, + help_text="bits", + max_length=6, + verbose_name="key length", + ), + ), + migrations.AlterField( + model_name="cert", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), + ], + default=django_x509.base.models.default_key_length, + help_text="bits", + max_length=6, + verbose_name="key length", + ), + ), + migrations.AlterField( + model_name="customcert", + name="key_length", + field=models.CharField( + choices=[ + ("256", "256 (ECDSA)"), + ("384", "384 (ECDSA)"), + ("521", "521 (ECDSA)"), + ("1024", "1024 (RSA)"), + ("2048", "2048 (RSA)"), + ("4096", "4096 (RSA)"), + ], + default=django_x509.base.models.default_key_length, + help_text="bits", + max_length=6, + verbose_name="key length", + ), + ), + ] From 70fea92e97176c48d94abba699bc30da939d060b Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sat, 31 Jan 2026 00:00:28 +0530 Subject: [PATCH 16/18] [Improvements] Merge multiple migration files into one #118 There were two existing migrations refering a rather similar change which are now merged Fixes #118 --- ...ter_ca_key_length_alter_cert_key_length.py | 4 +- ...ter_ca_key_length_alter_cert_key_length.py | 50 -------------- ...y_length_alter_cert_key_length_and_more.py | 5 +- ...y_length_alter_cert_key_length_and_more.py | 68 ------------------- 4 files changed, 2 insertions(+), 125 deletions(-) delete mode 100644 django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py delete mode 100644 tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py diff --git a/django_x509/migrations/0010_alter_ca_key_length_alter_cert_key_length.py b/django_x509/migrations/0010_alter_ca_key_length_alter_cert_key_length.py index 7d2c344..7bee8a8 100644 --- a/django_x509/migrations/0010_alter_ca_key_length_alter_cert_key_length.py +++ b/django_x509/migrations/0010_alter_ca_key_length_alter_cert_key_length.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.1 on 2026-01-16 21:41 +# Generated by Django 6.0.1 on 2026-01-30 18:18 import django_x509.base.models from django.db import migrations, models @@ -19,7 +19,6 @@ class Migration(migrations.Migration): ("256", "256 (ECDSA)"), ("384", "384 (ECDSA)"), ("521", "521 (ECDSA)"), - ("512", "512 (RSA)"), ("1024", "1024 (RSA)"), ("2048", "2048 (RSA)"), ("4096", "4096 (RSA)"), @@ -38,7 +37,6 @@ class Migration(migrations.Migration): ("256", "256 (ECDSA)"), ("384", "384 (ECDSA)"), ("521", "521 (ECDSA)"), - ("512", "512 (RSA)"), ("1024", "1024 (RSA)"), ("2048", "2048 (RSA)"), ("4096", "4096 (RSA)"), diff --git a/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py b/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py deleted file mode 100644 index f7cc883..0000000 --- a/django_x509/migrations/0011_alter_ca_key_length_alter_cert_key_length.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 6.0.1 on 2026-01-17 15:58 - -import django_x509.base.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("django_x509", "0010_alter_ca_key_length_alter_cert_key_length"), - ] - - operations = [ - migrations.AlterField( - model_name="ca", - name="key_length", - field=models.CharField( - choices=[ - ("256", "256 (ECDSA)"), - ("384", "384 (ECDSA)"), - ("521", "521 (ECDSA)"), - ("1024", "1024 (RSA)"), - ("2048", "2048 (RSA)"), - ("4096", "4096 (RSA)"), - ], - default=django_x509.base.models.default_key_length, - help_text="bits", - max_length=6, - verbose_name="key length", - ), - ), - migrations.AlterField( - model_name="cert", - name="key_length", - field=models.CharField( - choices=[ - ("256", "256 (ECDSA)"), - ("384", "384 (ECDSA)"), - ("521", "521 (ECDSA)"), - ("1024", "1024 (RSA)"), - ("2048", "2048 (RSA)"), - ("4096", "4096 (RSA)"), - ], - default=django_x509.base.models.default_key_length, - help_text="bits", - max_length=6, - verbose_name="key length", - ), - ), - ] diff --git a/tests/openwisp2/sample_x509/migrations/0003_alter_ca_key_length_alter_cert_key_length_and_more.py b/tests/openwisp2/sample_x509/migrations/0003_alter_ca_key_length_alter_cert_key_length_and_more.py index 2527bb8..887ebc6 100644 --- a/tests/openwisp2/sample_x509/migrations/0003_alter_ca_key_length_alter_cert_key_length_and_more.py +++ b/tests/openwisp2/sample_x509/migrations/0003_alter_ca_key_length_alter_cert_key_length_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.1 on 2026-01-16 21:40 +# Generated by Django 6.0.1 on 2026-01-30 18:26 import django_x509.base.models from django.db import migrations, models @@ -19,7 +19,6 @@ class Migration(migrations.Migration): ("256", "256 (ECDSA)"), ("384", "384 (ECDSA)"), ("521", "521 (ECDSA)"), - ("512", "512 (RSA)"), ("1024", "1024 (RSA)"), ("2048", "2048 (RSA)"), ("4096", "4096 (RSA)"), @@ -38,7 +37,6 @@ class Migration(migrations.Migration): ("256", "256 (ECDSA)"), ("384", "384 (ECDSA)"), ("521", "521 (ECDSA)"), - ("512", "512 (RSA)"), ("1024", "1024 (RSA)"), ("2048", "2048 (RSA)"), ("4096", "4096 (RSA)"), @@ -57,7 +55,6 @@ class Migration(migrations.Migration): ("256", "256 (ECDSA)"), ("384", "384 (ECDSA)"), ("521", "521 (ECDSA)"), - ("512", "512 (RSA)"), ("1024", "1024 (RSA)"), ("2048", "2048 (RSA)"), ("4096", "4096 (RSA)"), diff --git a/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py b/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py deleted file mode 100644 index f64380f..0000000 --- a/tests/openwisp2/sample_x509/migrations/0004_alter_ca_key_length_alter_cert_key_length_and_more.py +++ /dev/null @@ -1,68 +0,0 @@ -# Generated by Django 6.0.1 on 2026-01-17 15:59 - -import django_x509.base.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("sample_x509", "0003_alter_ca_key_length_alter_cert_key_length_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="ca", - name="key_length", - field=models.CharField( - choices=[ - ("256", "256 (ECDSA)"), - ("384", "384 (ECDSA)"), - ("521", "521 (ECDSA)"), - ("1024", "1024 (RSA)"), - ("2048", "2048 (RSA)"), - ("4096", "4096 (RSA)"), - ], - default=django_x509.base.models.default_key_length, - help_text="bits", - max_length=6, - verbose_name="key length", - ), - ), - migrations.AlterField( - model_name="cert", - name="key_length", - field=models.CharField( - choices=[ - ("256", "256 (ECDSA)"), - ("384", "384 (ECDSA)"), - ("521", "521 (ECDSA)"), - ("1024", "1024 (RSA)"), - ("2048", "2048 (RSA)"), - ("4096", "4096 (RSA)"), - ], - default=django_x509.base.models.default_key_length, - help_text="bits", - max_length=6, - verbose_name="key length", - ), - ), - migrations.AlterField( - model_name="customcert", - name="key_length", - field=models.CharField( - choices=[ - ("256", "256 (ECDSA)"), - ("384", "384 (ECDSA)"), - ("521", "521 (ECDSA)"), - ("1024", "1024 (RSA)"), - ("2048", "2048 (RSA)"), - ("4096", "4096 (RSA)"), - ], - default=django_x509.base.models.default_key_length, - help_text="bits", - max_length=6, - verbose_name="key length", - ), - ), - ] From bf2d7babd4886b40233dc82758dda3a4d13916ee Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sat, 31 Jan 2026 00:09:47 +0530 Subject: [PATCH 17/18] [docs] Updated docs to mention ECDSA and RSA support #118 Added a point under current features section to mention ECDSA and RSA support Fixes #118 --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 4b730e4..3b8771e 100644 --- a/README.rst +++ b/README.rst @@ -68,6 +68,8 @@ Current features - Possibility to generate and import passphrase protected x509 certificates/CAs - Passphrase protected x509 content will be shown encrypted in the web UI +- RSA and ECDSA support: generate or import certificates using RSA or + ECDSA algorithms. Project goals ------------- From 818a53fdc0dcd38c9f3f9de9ea04d88917e311c5 Mon Sep 17 00:00:00 2001 From: stktyagi Date: Sat, 31 Jan 2026 00:17:23 +0530 Subject: [PATCH 18/18] [docs] Updated key length choices #118 explicitly stated RSA-only sizes and mapping to EC curves. Fixes #118 --- README.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 3b8771e..2cd08ee 100644 --- a/README.rst +++ b/README.rst @@ -241,10 +241,8 @@ Default key length for new CAs and new certificates. Must be one of the following values: -- ``512`` -- ``1024`` -- ``2048`` -- ``4096`` +- RSA: ``1024``, ``2048``, ``4096`` +- ECDSA: ``256`` (P-256), ``384`` (P-384), ``521`` (P-521) ``DJANGO_X509_DEFAULT_DIGEST_ALGORITHM`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~