Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d578ea4
[feature] Added support for importing ECDSA certificates #118
stktyagi Jan 11, 2026
c2f3be8
[feature] Included the P-521 curve in choices #118
stktyagi Jan 11, 2026
86a6b9f
[Improvements] Handling of unsupported keys and message clarity #118
stktyagi Jan 11, 2026
678566d
[Update] Remove SECP224R1 (224-bit) curve support #118
stktyagi Jan 11, 2026
cf82aca
[Improvement] Added explicit validation for imported EC certificates …
stktyagi Jan 11, 2026
c6e4de0
[Test] Added test to check whole ecdsa lifecycle #118
stktyagi Jan 11, 2026
f74fa1d
[Improvement] Add curve-appropriate hash algorithms to test different…
stktyagi Jan 11, 2026
00b6542
[Improvements] Added additional assertions for completeness #118
stktyagi Jan 11, 2026
1a5604f
[tests] Added test to verify certificates with added implementation #118
stktyagi Jan 14, 2026
7b4217a
[fix] Fixed Aasertion comparing private_key with certificate variable…
stktyagi Jan 14, 2026
4519ac2
[fix] Fixed incomplete digest normalization pattern #118
stktyagi Jan 14, 2026
e3eaa43
[refactor] Added ECDSA support for CAs and Certificates #118
stktyagi Jan 16, 2026
94b0793
[migrations] Add migrations #118
stktyagi Jan 16, 2026
54316bd
[refactor] Simplified clean() to previous better implementation #118
stktyagi Jan 16, 2026
8b56b13
[fix] Remove 512-bit RSA support #118
stktyagi Jan 17, 2026
0b99dde
Merge branch 'master' into issues/118-import-edcsa-existing-ca-KeyError
stktyagi Jan 30, 2026
70fea92
[Improvements] Merge multiple migration files into one #118
stktyagi Jan 30, 2026
bf2d7ba
[docs] Updated docs to mention ECDSA and RSA support #118
stktyagi Jan 30, 2026
818a53f
[docs] Updated key length choices #118
stktyagi Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 76 additions & 14 deletions django_x509/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@
from .. import settings as app_settings

KEY_LENGTH_CHOICES = (
("512", "512"),
("1024", "1024"),
("2048", "2048"),
("4096", "4096"),
("256", "256 (ECDSA)"),
("384", "384 (ECDSA)"),
("521", "521 (ECDSA)"),
("1024", "1024 (RSA)"),
("2048", "2048 (RSA)"),
("4096", "4096 (RSA)"),
)

RSA_KEY_LENGTHS = ("1024", "2048", "4096")
EC_KEY_LENGTHS = ("256", "384", "521")

DIGEST_CHOICES = (
("sha1", "SHA1"),
("sha224", "SHA224"),
Expand Down Expand Up @@ -165,6 +170,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
Expand All @@ -175,9 +183,11 @@ def clean(self):
"keys (private and public) must be present"
)
)
if self.serial_number:
self._validate_serial_number()
self._verify_extension_format()
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:
Expand Down Expand Up @@ -280,10 +290,24 @@ 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),
)
is_ec = self.key_length in EC_KEY_LENGTHS
if is_ec:
curves = {
"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:
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
Expand All @@ -307,7 +331,12 @@ 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-", "")
.replace("withsha", "sha")
)
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")
Expand All @@ -318,7 +347,11 @@ def _generate(self):
)
self.private_key = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
format=(
serialization.PrivateFormat.PKCS8
if is_ec
else serialization.PrivateFormat.TraditionalOpenSSL
),
encryption_algorithm=encryption,
).decode("utf-8")

Expand Down Expand Up @@ -352,6 +385,36 @@ def _import(self):
imports existing x509 certificates
"""
cert = self.x509
public_key = cert.public_key()
if isinstance(public_key, rsa.RSAPublicKey):
actual_length = str(public_key.key_size)
actual_is_ec = False
elif isinstance(public_key, ec.EllipticCurvePublicKey):
actual_length = str(public_key.curve.key_size)
actual_is_ec = True
else:
raise ValidationError(
_(
"Unsupported key type in certificate. "
"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()
Expand All @@ -366,7 +429,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
Loading
Loading