22from datetime import timezone as dt_timezone
33
44from cryptography import x509
5- from cryptography .hazmat .primitives .asymmetric import rsa
5+ from cryptography .hazmat .primitives import hashes , serialization
6+ from cryptography .hazmat .primitives .asymmetric import ec , rsa
67from cryptography .x509 .oid import NameOID
78from django .core .exceptions import ValidationError
89from django .test import TestCase
@@ -34,29 +35,56 @@ def _prepare_revoked(self):
3435 return (ca , cert )
3536
3637 import_certificate = """-----BEGIN CERTIFICATE-----
37- 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
58+ EQZrw8bbikXRSH3D31NVroN7fw==
4959-----END CERTIFICATE-----"""
5060
5161 import_private_key = """-----BEGIN PRIVATE KEY-----
52- MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEAxwD5toX4+4ZG61wM
53- vgsNl28lFxar1KT+3yT3E1yAU6ipJ04hbJX+F2MzJDCuL/sU/2h6yACgf6Dfqe7A
54- 2uOHRQIDAQABAkEAwQApLuPv/cDUtx6nHQkLPXsFtca/D5SVu0TWe2iS7I5IQuXN
55- XIwUScvp9rd/GnSuphyMXrgE8XI3nn1baTS7AQIhAO6eXAYz4cQHWPWoMS75S6O/
56- uE/ZJVCfBUonFfmS2ELpAiEA1X/mNy4pacJSJLpGv1vajOIzXR76W9ud5lHh23U3
57- z/0CIBj7ES1BDzijgEevhP6i8K1C6/vIAuUO0NHzh5RqMCPJAiEAzBJjyCzMkvWW
58- NNsE0taGsZFpjUIWBoWGiWeNHosNnTUCIQC1wzMjWku4OXk030WC4fFeJwU060WT
59- 9iuP3scMQKQZdg==
62+ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCTI2xoZlq0VO+U
63+ 9DZKiPiDmTNqU+asuQoRfFiQ28gInjfQ+hvUnQ35HlV2E27C5JoJzhuPmXPIcK9J
64+ xJkFpPJOFdfp15FRbW6TxKzx8b4TwkKsAfMpZBVR7u5klOAq0jqE0F83OZUdVe9a
65+ IUFBEw7g5c5b3aEoo+E4hxxP31Bx6U2s7VMpP3ftyeAppJv7RlygCykpZ8VvTLrZ
66+ fHECscL9/mc3anUjz7qH2BDbr4xFebHCW9P2pfZMxEEKPfoPSOCwKAnXQLuJCNm7
67+ d3kW2ZtTt1LgO+UKPyk1dydLhHFceuZkp5jdRWT1M1FjIw53ZwebsVB2sZhY7jFP
68+ 7ku0+QtxAgMBAAECggEANH9kE4/JdyQC41uK72cVfCayMJLE8AWJcRmzo+O26FRD
69+ R/2k5mQu8x5+kYV3dHQJ/cubC85NgEusTx6lFl120qN6iQWP5MStum1m42BEWFps
70+ XWDIuJDsBnLAfgScQssFdBPAlTynVnMt1jOdS7GYEmgMC7z03kIyfm++i0T7N9ji
71+ fyN2CFOXgevgHK5EtTSrBTzg8JkFnhNZKjHPU9IkRyaN8KtOwKrEgxh0glvNM6yp
72+ cmU8PE+DPK4TSQGsIO4X4Z19wKv7O8x6CYLos8w2Yh9jwMHaGeDnv68RVFoY1vgH
73+ q/PJcWylRanDeyoShIm3v2qBCQcBtUqDUqdTotww7QKBgQDH2LeZ4rn7SUuRkt7l
74+ YeM4YtlbWSlycTh1KphqkM3UW0MQA/HcJKgFacbxdEj9hV0Ol/l76qQv39V1Ts3X
75+ Zsf1eSGLdrvi2JYrlh0WYhFco/g7Cqt43dNfqSAnNKb8ihRq3X8zsQ7nTDU3wWoK
76+ fYjeHyYRFYPi7jQF3sYcmrM5OwKBgQC8e1KPKQ2eIJNC5rYbolxRgrSOMxr+AxgD
77+ iiNdhPT0lpeYEPW65z/pbdiiehXt8zrrJhtfDDe7f0dHaYkbSJWq5dzjAqnfwNGO
78+ +gr5+skTL5nkimjbinGHN8+2134eMxh4WmsyQvkY23wk8SJlC9/57/TQvjRNZUAw
79+ nmBmQdojQwKBgQCWFXl9RjqqLxdjkkt3NRZxyDq4UbPA0Kq3w2+HyIvryUYKBwxi
80+ ad0Ng6z2tIAEdV23kga5OzRnB9DFMpOACx5sibXZiSf9au8MeMYLg0bKrhHENXUl
81+ ZmJR2y/cgbxOuFwxDXt0FKq+pgrpfoXmrvRU7EuoVOIhUQccyXs7DCtA9QKBgEoS
82+ vVN98tgePUGhohgiKt3t3D+2XflOBfX+J//s7MfjFxiwMaKOl1OJ1AWmrU+is5kO
83+ lNs51f1d/AlYtIWAdTGAvNqKhXBmOvVR11Z+9N8Rag2jR6pgMlXN3VgiQHJl6kwC
84+ XPaX04WtXJC4I6hKjm+PmksfNTblf+CbnY8SekQ5AoGBALvK6n2UGG+0fKtUFM+M
85+ 0enYrqW065OsDOIXo7roOVZjBhul+vilk7xJ85fhtDD0zll/4OoJ1BG9IN0rUUJh
86+ eMIi3AQRO060kYejKGC2ls5hMxjdhxB/bUGrwXkMxZQPfjBkcP3JqaZkwcFBKq5N
87+ tsND+97h9r73S+UTOhepQTDB
6088-----END PRIVATE KEY-----"""
6189
6290 def test_new (self ):
@@ -132,7 +160,8 @@ def test_import_ca(self):
132160 ca .save ()
133161 cert = ca .x509
134162 # verify attributes
135- self .assertEqual (cert .serial_number , 123456 )
163+ serial = 371904255628934431598705194442539630076148098375
164+ self .assertEqual (cert .serial_number , serial )
136165 subject = cert .subject
137166 self .assertEqual (
138167 subject .get_attributes_for_oid (NameOID .COUNTRY_NAME )[0 ].value , "US"
@@ -157,18 +186,18 @@ def test_import_ca(self):
157186 )
158187 self .assertEqual (cert .issuer , cert .subject )
159188 # verify field attribtues
160- self .assertEqual (ca .key_length , "512 " )
161- self .assertEqual (ca .digest , "sha1 " )
189+ self .assertEqual (ca .key_length , "2048 " )
190+ self .assertEqual (ca .digest , "sha256 " )
162191 self .assertEqual (ca .country_code , "US" )
163192 self .assertEqual (ca .state , "CA" )
164193 self .assertEqual (ca .city , "San Francisco" )
165194 self .assertEqual (ca .organization_name , "ACME" )
166195 self .
assertEqual (
ca .
email ,
"[email protected] " )
167196 self .assertEqual (ca .common_name , "importtest" )
168- self .assertEqual (int (ca .serial_number ), 123456 )
197+ self .assertEqual (int (ca .serial_number ), serial )
169198 self .assertEqual (ca .name , "ImportTest" )
170- start = datetime (2025 , 12 , 25 , 9 , 20 , 56 , tzinfo = dt_timezone .utc )
171- end = datetime (2026 , 12 , 25 , 9 , 20 , 56 , tzinfo = dt_timezone .utc )
199+ start = datetime (2026 , 1 , 17 , 7 , 41 , 5 , tzinfo = dt_timezone .utc )
200+ end = datetime (2036 , 1 , 15 , 7 , 41 , 5 , tzinfo = dt_timezone .utc )
172201 self .assertEqual (ca .validity_start , start )
173202 self .assertEqual (ca .validity_end , end )
174203 # ensure version is 3
@@ -684,3 +713,57 @@ def test_renewal_serial_sync(self):
684713 cert_obj = x509 .load_pem_x509_certificate (cert .certificate .encode ())
685714 pem_serial = cert_obj .serial_number
686715 self .assertEqual (int (cert .serial_number ), pem_serial )
716+
717+ def test_ca_ecdsa_full_lifecycle (self ):
718+ curves_to_test = [
719+ ("256" , ec .SECP256R1 , hashes .SHA256 ()),
720+ ("384" , ec .SECP384R1 , hashes .SHA384 ()),
721+ ("521" , ec .SECP521R1 , hashes .SHA512 ()),
722+ ]
723+ for length , curve_class , digest in curves_to_test :
724+ with self .subTest (key_length = length ):
725+ priv_key = ec .generate_private_key (curve_class ())
726+ key_pem = priv_key .private_bytes (
727+ encoding = serialization .Encoding .PEM ,
728+ format = serialization .PrivateFormat .PKCS8 ,
729+ encryption_algorithm = serialization .NoEncryption (),
730+ ).decode ("utf-8" )
731+ now = datetime .now (dt_timezone .utc )
732+ subject = issuer = x509 .Name (
733+ [x509 .NameAttribute (NameOID .COMMON_NAME , "test" )]
734+ )
735+ cert = (
736+ x509 .CertificateBuilder ()
737+ .subject_name (subject )
738+ .issuer_name (issuer )
739+ .public_key (priv_key .public_key ())
740+ .serial_number (x509 .random_serial_number ())
741+ .not_valid_before (now )
742+ .not_valid_after (now + timedelta (days = 10 ))
743+ .sign (priv_key , digest )
744+ )
745+ cert_pem = cert .public_bytes (serialization .Encoding .PEM ).decode ("utf-8" )
746+ ca = Ca (
747+ name = f"EC-{ length } " ,
748+ certificate = cert_pem ,
749+ private_key = key_pem ,
750+ key_length = length ,
751+ )
752+ ca .full_clean ()
753+ ca .save ()
754+ self .assertEqual (ca .key_length , length )
755+ self .assertIsInstance (ca .pkey , ec .EllipticCurvePrivateKey )
756+ gen_ca = Ca (
757+ name = f"Gen-EC-{ length } " ,
758+ key_length = length ,
759+ )
760+ gen_ca .full_clean ()
761+ gen_ca .save ()
762+ self .assertIsInstance (gen_ca .pkey , ec .EllipticCurvePrivateKey )
763+ original_cert = gen_ca .certificate
764+ original_key = gen_ca .private_key
765+ gen_ca .renew ()
766+ gen_ca .refresh_from_db ()
767+ self .assertEqual (gen_ca .key_length , length )
768+ self .assertNotEqual (gen_ca .private_key , original_key )
769+ self .assertNotEqual (original_cert , gen_ca .certificate )
0 commit comments