Skip to content

Commit 2ad2303

Browse files
committed
Add ML-DSA (FIPS 204) post-quantum key type support
Add MLDSA44, MLDSA65, and MLDSA87 key types for generating post-quantum certificates using the cryptography library's ML-DSA support (requires OpenSSL 3.5+).
1 parent ac8482f commit 2ad2303

2 files changed

Lines changed: 74 additions & 12 deletions

File tree

src/trustme/__init__.py

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import idna
1414
from cryptography import x509
1515
from cryptography.hazmat.primitives import hashes
16-
from cryptography.hazmat.primitives.asymmetric import ec, rsa
16+
from cryptography.hazmat.primitives.asymmetric import ec, mldsa, rsa
1717
from cryptography.hazmat.primitives.serialization import (
1818
Encoding,
1919
NoEncryption,
@@ -27,8 +27,20 @@
2727
if TYPE_CHECKING: # pragma: no cover
2828
import OpenSSL.SSL
2929

30-
CERTIFICATE_PUBLIC_KEY_TYPES = Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey]
31-
CERTIFICATE_PRIVATE_KEY_TYPES = Union[rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey]
30+
CERTIFICATE_PUBLIC_KEY_TYPES = Union[
31+
rsa.RSAPublicKey,
32+
ec.EllipticCurvePublicKey,
33+
mldsa.MLDSA44PublicKey,
34+
mldsa.MLDSA65PublicKey,
35+
mldsa.MLDSA87PublicKey,
36+
]
37+
CERTIFICATE_PRIVATE_KEY_TYPES = Union[
38+
rsa.RSAPrivateKey,
39+
ec.EllipticCurvePrivateKey,
40+
mldsa.MLDSA44PrivateKey,
41+
mldsa.MLDSA65PrivateKey,
42+
mldsa.MLDSA87PrivateKey,
43+
]
3244

3345
__all__ = ["CA"]
3446

@@ -214,6 +226,9 @@ class KeyType(Enum):
214226

215227
RSA = 0
216228
ECDSA = 1
229+
MLDSA44 = 2
230+
MLDSA65 = 3
231+
MLDSA87 = 4
217232

218233
def _generate_key(self) -> CERTIFICATE_PRIVATE_KEY_TYPES:
219234
if self is KeyType.RSA:
@@ -223,9 +238,44 @@ def _generate_key(self) -> CERTIFICATE_PRIVATE_KEY_TYPES:
223238
return rsa.generate_private_key(public_exponent=65537, key_size=2048)
224239
elif self is KeyType.ECDSA:
225240
return ec.generate_private_key(ec.SECP256R1())
241+
elif self is KeyType.MLDSA44:
242+
return mldsa.MLDSA44PrivateKey.generate()
243+
elif self is KeyType.MLDSA65:
244+
return mldsa.MLDSA65PrivateKey.generate()
245+
elif self is KeyType.MLDSA87:
246+
return mldsa.MLDSA87PrivateKey.generate()
226247
else: # pragma: no cover
227248
raise ValueError("Unknown key type")
228249

250+
@property
251+
def _hash_algorithm(self) -> Optional[hashes.SHA256]:
252+
"""ML-DSA uses intrinsic hashing; RSA/ECDSA use SHA-256."""
253+
if self in (KeyType.MLDSA44, KeyType.MLDSA65, KeyType.MLDSA87):
254+
return None
255+
return hashes.SHA256()
256+
257+
@property
258+
def _private_key_format(self) -> PrivateFormat:
259+
"""ML-DSA keys don't support TraditionalOpenSSL format."""
260+
if self in (KeyType.MLDSA44, KeyType.MLDSA65, KeyType.MLDSA87):
261+
return PrivateFormat.PKCS8
262+
return PrivateFormat.TraditionalOpenSSL
263+
264+
265+
def _detect_key_type(private_key: CERTIFICATE_PRIVATE_KEY_TYPES) -> KeyType:
266+
if isinstance(private_key, rsa.RSAPrivateKey):
267+
return KeyType.RSA
268+
elif isinstance(private_key, ec.EllipticCurvePrivateKey):
269+
return KeyType.ECDSA
270+
elif isinstance(private_key, mldsa.MLDSA44PrivateKey):
271+
return KeyType.MLDSA44
272+
elif isinstance(private_key, mldsa.MLDSA65PrivateKey):
273+
return KeyType.MLDSA65
274+
elif isinstance(private_key, mldsa.MLDSA87PrivateKey):
275+
return KeyType.MLDSA87
276+
else:
277+
raise TypeError(f"Unsupported key type: {type(private_key)}")
278+
229279

230280
class CA:
231281
"""A certificate authority."""
@@ -241,6 +291,7 @@ def __init__(
241291
key_type: KeyType = KeyType.ECDSA,
242292
) -> None:
243293
self.parent_cert = parent_cert
294+
self._key_type = key_type
244295
self._private_key = key_type._generate_key()
245296
self._path_length = path_length
246297

@@ -250,9 +301,11 @@ def __init__(
250301
)
251302
issuer = name
252303
sign_key = self._private_key
304+
sign_key_type = key_type
253305
aki: Optional[x509.AuthorityKeyIdentifier]
254306
if parent_cert is not None:
255307
sign_key = parent_cert._private_key
308+
sign_key_type = parent_cert._key_type
256309
parent_certificate = parent_cert._certificate
257310
issuer = parent_certificate.subject
258311
ski_ext = parent_certificate.extensions.get_extension_for_class(
@@ -286,7 +339,7 @@ def __init__(
286339
critical=True,
287340
).sign(
288341
private_key=sign_key,
289-
algorithm=hashes.SHA256(),
342+
algorithm=sign_key_type._hash_algorithm,
290343
)
291344

292345
@property
@@ -301,7 +354,7 @@ def private_key_pem(self) -> Blob:
301354
other certificates from this CA."""
302355
return Blob(
303356
self._private_key.private_bytes(
304-
Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()
357+
Encoding.PEM, self._key_type._private_key_format, NoEncryption()
305358
)
306359
)
307360

@@ -440,7 +493,7 @@ def issue_cert(
440493
)
441494
.sign(
442495
private_key=self._private_key,
443-
algorithm=hashes.SHA256(),
496+
algorithm=self._key_type._hash_algorithm,
444497
)
445498
)
446499

@@ -453,7 +506,7 @@ def issue_cert(
453506
return LeafCert(
454507
key.private_bytes(
455508
Encoding.PEM,
456-
PrivateFormat.TraditionalOpenSSL,
509+
key_type._private_key_format,
457510
NoEncryption(),
458511
),
459512
cert.public_bytes(Encoding.PEM),
@@ -499,6 +552,7 @@ def from_pem(cls, cert_bytes: bytes, private_key_bytes: bytes) -> "CA":
499552
ca.parent_cert = None
500553
ca._certificate = x509.load_pem_x509_certificate(cert_bytes)
501554
ca._private_key = load_pem_private_key(private_key_bytes, password=None) # type: ignore[assignment]
555+
ca._key_type = _detect_key_type(ca._private_key)
502556

503557
return ca
504558

tests/test_trustme.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,21 @@ def assert_is_leaf(leaf_cert: x509.Certificate) -> None:
6666

6767

6868
@pytest.mark.parametrize(
69-
"key_type,expected_key_header", [(KeyType.RSA, b"RSA"), (KeyType.ECDSA, b"EC")]
69+
"key_type,expected_key_header",
70+
[
71+
(KeyType.RSA, b"BEGIN RSA PRIVATE KEY"),
72+
(KeyType.ECDSA, b"BEGIN EC PRIVATE KEY"),
73+
(KeyType.MLDSA44, b"BEGIN PRIVATE KEY"),
74+
(KeyType.MLDSA65, b"BEGIN PRIVATE KEY"),
75+
(KeyType.MLDSA87, b"BEGIN PRIVATE KEY"),
76+
],
7077
)
7178
def test_basics(key_type: KeyType, expected_key_header: bytes) -> None:
7279
ca = CA(key_type=key_type)
7380

7481
today = datetime.datetime.now(datetime.timezone.utc)
7582

76-
assert (
77-
b"BEGIN " + expected_key_header + b" PRIVATE KEY" in ca.private_key_pem.bytes()
78-
)
83+
assert expected_key_header in ca.private_key_pem.bytes()
7984
assert b"BEGIN CERTIFICATE" in ca.cert_pem.bytes()
8085

8186
private_key = load_pem_private_key(ca.private_key_pem.bytes(), password=None)
@@ -357,7 +362,10 @@ def doit(ca: CA, hostname: str, server_cert: LeafCert) -> None:
357362
doit(bad_ca, hostname, ca.issue_cert(hostname, key_type=key_type))
358363

359364

360-
@pytest.mark.parametrize("key_type", [KeyType.RSA, KeyType.ECDSA])
365+
@pytest.mark.parametrize(
366+
"key_type",
367+
[KeyType.RSA, KeyType.ECDSA, KeyType.MLDSA44, KeyType.MLDSA65, KeyType.MLDSA87],
368+
)
361369
def test_stdlib_end_to_end(key_type: KeyType) -> None:
362370
def wrap_client(
363371
ca: CA, raw_client_sock: socket.socket, hostname: str

0 commit comments

Comments
 (0)