Skip to content

Commit 68dbd10

Browse files
committed
ML-DSA: Implement ML-DSA-44 (FIPS 204) signature algorithm.
Changes: - Add mldsa44_supported() backend method - Implement MlDsa44PrivateKey and MlDsa44PublicKey classes - Add Rust backend implementation with OpenSSL 3.5+ support - Add PKCS8/SPKI key parsing and serialization - Add comprehensive test suite - Add API documentation ML-DSA-44 provides NIST Level 2 post-quantum security with the smallest signature and key sizes of the ML-DSA family. Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
1 parent 498bd1f commit 68dbd10

24 files changed

Lines changed: 1571 additions & 83 deletions

File tree

docs/hazmat/primitives/asymmetric/index.rst

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ private key is able to decrypt it.
2727
x25519
2828
ed448
2929
x448
30+
mldsa
3031
ec
3132
rsa
3233
dh
@@ -58,7 +59,8 @@ union type aliases can be used instead to reference a multitude of key types.
5859
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey`,
5960
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey`,
6061
:class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`,
61-
:class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey`.
62+
:class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey`,
63+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MlDsa44PublicKey`.
6264

6365
.. data:: PrivateKeyTypes
6466

@@ -72,7 +74,8 @@ union type aliases can be used instead to reference a multitude of key types.
7274
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`,
7375
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`,
7476
:class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey`,
75-
:class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey`.
77+
:class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey`,
78+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MlDsa44PrivateKey`.
7679

7780
.. data:: CertificatePublicKeyTypes
7881

@@ -86,7 +89,8 @@ union type aliases can be used instead to reference a multitude of key types.
8689
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey`,
8790
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey`,
8891
:class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`,
89-
:class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey`.
92+
:class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey`,
93+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MlDsa44PublicKey`.
9094

9195
.. data:: CertificateIssuerPublicKeyTypes
9296

@@ -101,7 +105,8 @@ union type aliases can be used instead to reference a multitude of key types.
101105
:class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`,
102106
:class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`,
103107
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey`,
104-
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey`.
108+
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey`,
109+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MlDsa44PublicKey`.
105110

106111
.. data:: CertificateIssuerPrivateKeyTypes
107112

@@ -116,4 +121,5 @@ union type aliases can be used instead to reference a multitude of key types.
116121
:class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey`,
117122
:class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey`,
118123
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`,
119-
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`.
124+
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`,
125+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MlDsa44PrivateKey`.
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
.. hazmat::
2+
3+
ML-DSA-44 signing
4+
=================
5+
6+
.. currentmodule:: cryptography.hazmat.primitives.asymmetric.mldsa
7+
8+
9+
ML-DSA-44 is a post-quantum digital signature algorithm based on module
10+
lattices, standardized in `FIPS 204`_. It provides NIST security level 2
11+
(comparable to 128-bit security) and is suitable for applications where smaller
12+
key and signature sizes are important. ML-DSA-44 is designed to be secure
13+
against attacks from both classical and quantum computers.
14+
15+
Signing & Verification
16+
~~~~~~~~~~~~~~~~~~~~~~~
17+
18+
.. doctest::
19+
20+
>>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa44PrivateKey
21+
>>> private_key = MlDsa44PrivateKey.generate()
22+
>>> signature = private_key.sign(b"my authenticated message")
23+
>>> public_key = private_key.public_key()
24+
>>> # Raises InvalidSignature if verification fails
25+
>>> public_key.verify(signature, b"my authenticated message")
26+
27+
Context-based Signing & Verification
28+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29+
30+
ML-DSA-44 supports context strings to bind additional information to signatures.
31+
The context can be up to 255 bytes and is used to differentiate signatures in
32+
different contexts or protocols. This is useful for domain separation and
33+
preventing cross-protocol attacks.
34+
35+
.. doctest::
36+
37+
>>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa44PrivateKey
38+
>>> private_key = MlDsa44PrivateKey.generate()
39+
>>> context = b"email-signature-v1"
40+
>>> signature = private_key.sign_with_context(b"my authenticated message", context)
41+
>>> public_key = private_key.public_key()
42+
>>> # Verification requires the same context
43+
>>> public_key.verify_with_context(signature, b"my authenticated message", context)
44+
45+
X.509 Certificate Usage
46+
~~~~~~~~~~~~~~~~~~~~~~~~
47+
48+
ML-DSA-44 can be used to create and sign X.509 certificates. When signing
49+
certificates with ML-DSA, the ``hash_algorithm`` parameter must be ``None``
50+
as ML-DSA uses pure signature mode without pre-hashing.
51+
52+
.. doctest::
53+
54+
>>> import datetime
55+
>>> from cryptography import x509
56+
>>> from cryptography.x509.oid import NameOID
57+
>>> from cryptography.hazmat.primitives.asymmetric import mldsa
58+
>>> from cryptography.hazmat.primitives import serialization
59+
>>> # Generate ML-DSA-44 key
60+
>>> private_key = mldsa.MlDsa44PrivateKey.generate()
61+
>>> public_key = private_key.public_key()
62+
>>> # Create a self-signed certificate
63+
>>> subject = issuer = x509.Name([
64+
... x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
65+
... x509.NameAttribute(NameOID.ORGANIZATION_NAME, "My Organization"),
66+
... x509.NameAttribute(NameOID.COMMON_NAME, "example.com"),
67+
... ])
68+
>>> cert = (
69+
... x509.CertificateBuilder()
70+
... .subject_name(subject)
71+
... .issuer_name(issuer)
72+
... .public_key(public_key)
73+
... .serial_number(x509.random_serial_number())
74+
... .not_valid_before(datetime.datetime.now(datetime.timezone.utc))
75+
... .not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=365))
76+
... .sign(private_key, None) # hash_algorithm must be None for ML-DSA
77+
... )
78+
>>> # Verify the certificate signature
79+
>>> cert_public_key = cert.public_key()
80+
>>> cert_public_key.verify(cert.signature, cert.tbs_certificate_bytes)
81+
82+
CMS/PKCS#7 Signed Data
83+
~~~~~~~~~~~~~~~~~~~~~~~
84+
85+
ML-DSA-44 can be used to create CMS (Cryptographic Message Syntax) signed
86+
messages, commonly used for S/MIME email and document signing.
87+
88+
.. doctest::
89+
90+
>>> from cryptography.hazmat.primitives.serialization import pkcs7
91+
>>> # Create a signed message
92+
>>> message = b"Important document content"
93+
>>> builder = (
94+
... pkcs7.PKCS7SignatureBuilder()
95+
... .set_data(message)
96+
... .add_signer(cert, private_key, None) # hash_algorithm must be None for ML-DSA
97+
... )
98+
>>> # Sign and serialize as PEM
99+
>>> signed_data = builder.sign(serialization.Encoding.PEM, [])
100+
>>> # The signed_data can now be transmitted and verified by recipients
101+
102+
.. note::
103+
When using ML-DSA with CMS, the ``hash_algorithm`` parameter must be
104+
``None``. This is required by RFC 9882. The digestAlgorithm field in
105+
the CMS structure will automatically use SHA-512 for compliance with
106+
the standard.
107+
108+
Key interfaces
109+
~~~~~~~~~~~~~~
110+
111+
.. class:: MlDsa44PrivateKey
112+
113+
.. versionadded:: 47.0
114+
115+
.. classmethod:: generate()
116+
117+
Generate an ML-DSA-44 private key.
118+
119+
:returns: :class:`MlDsa44PrivateKey`
120+
121+
:raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-44 is
122+
not supported by the OpenSSL version ``cryptography`` is using.
123+
124+
.. classmethod:: from_seed_bytes(data)
125+
126+
A class method for deterministically generating an ML-DSA-44 private key
127+
from seed bytes. This is used for deterministic key generation, not for
128+
loading serialized keys. To load serialized private keys, use
129+
:func:`~cryptography.hazmat.primitives.serialization.load_pem_private_key`
130+
or :func:`~cryptography.hazmat.primitives.serialization.load_der_private_key`.
131+
132+
:param data: 32 byte seed.
133+
:type data: :term:`bytes-like`
134+
135+
:returns: :class:`MlDsa44PrivateKey`
136+
137+
:raises ValueError: If the seed is not 32 bytes.
138+
139+
:raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-44 is
140+
not supported by the OpenSSL version ``cryptography`` is using.
141+
142+
.. doctest::
143+
144+
>>> from cryptography.hazmat.primitives import serialization
145+
>>> from cryptography.hazmat.primitives.asymmetric import mldsa
146+
>>> private_key = mldsa.MlDsa44PrivateKey.generate()
147+
>>> # Serialize to PEM
148+
>>> pem = private_key.private_bytes(
149+
... encoding=serialization.Encoding.PEM,
150+
... format=serialization.PrivateFormat.PKCS8,
151+
... encryption_algorithm=serialization.NoEncryption()
152+
... )
153+
>>> # Load from PEM
154+
>>> loaded_private_key = serialization.load_pem_private_key(pem, password=None)
155+
156+
157+
.. method:: public_key()
158+
159+
:returns: :class:`MlDsa44PublicKey`
160+
161+
.. method:: sign(data)
162+
163+
Sign the data using ML-DSA-44.
164+
165+
:param data: The data to sign.
166+
:type data: :term:`bytes-like`
167+
168+
:returns bytes: The signature (2420 bytes).
169+
170+
.. method:: sign_with_context(data, context)
171+
172+
Sign the data using ML-DSA-44 with an additional context string.
173+
The context is used for domain separation and preventing cross-protocol
174+
attacks.
175+
176+
:param data: The data to sign.
177+
:type data: :term:`bytes-like`
178+
179+
:param context: The context string (up to 255 bytes).
180+
:type context: :term:`bytes-like`
181+
182+
:returns bytes: The signature (2420 bytes).
183+
184+
:raises ValueError: If the context is longer than 255 bytes.
185+
186+
.. method:: private_bytes(encoding, format, encryption_algorithm)
187+
188+
Allows serialization of the key to bytes. Encoding (
189+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`,
190+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`, or
191+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`) and
192+
format (
193+
:attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8`
194+
or
195+
:attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.Raw`
196+
) are chosen to define the exact serialization.
197+
198+
:param encoding: A value from the
199+
:class:`~cryptography.hazmat.primitives.serialization.Encoding` enum.
200+
201+
:param format: A value from the
202+
:class:`~cryptography.hazmat.primitives.serialization.PrivateFormat`
203+
enum. If the ``encoding`` is
204+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`
205+
then ``format`` must be
206+
:attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.Raw`
207+
, otherwise it must be
208+
:attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8`.
209+
210+
:param encryption_algorithm: An instance of an object conforming to the
211+
:class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryption`
212+
interface.
213+
214+
:return bytes: Serialized key.
215+
216+
.. method:: seed_bytes()
217+
218+
Returns the 32-byte seed used to generate this private key. This seed
219+
can be used with :meth:`from_seed_bytes` to deterministically recreate
220+
the same private key.
221+
222+
:return bytes: 32-byte seed.
223+
224+
.. doctest::
225+
226+
>>> from cryptography.hazmat.primitives.asymmetric import mldsa
227+
>>> private_key = mldsa.MlDsa44PrivateKey.generate()
228+
>>> seed = private_key.seed_bytes()
229+
>>> len(seed)
230+
32
231+
>>> # Recreate the same key from the seed
232+
>>> recreated_key = mldsa.MlDsa44PrivateKey.from_seed_bytes(seed)
233+
234+
.. class:: MlDsa44PublicKey
235+
236+
.. versionadded:: 47.0
237+
238+
.. classmethod:: from_public_bytes(data)
239+
240+
:param bytes data: 1312 byte public key.
241+
242+
:returns: :class:`MlDsa44PublicKey`
243+
244+
:raises ValueError: If the public key is not 1312 bytes.
245+
246+
:raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-44 is
247+
not supported by the OpenSSL version ``cryptography`` is using.
248+
249+
.. doctest::
250+
251+
>>> from cryptography.hazmat.primitives import serialization
252+
>>> from cryptography.hazmat.primitives.asymmetric import mldsa
253+
>>> private_key = mldsa.MlDsa44PrivateKey.generate()
254+
>>> public_key = private_key.public_key()
255+
>>> public_bytes = public_key.public_bytes(
256+
... encoding=serialization.Encoding.Raw,
257+
... format=serialization.PublicFormat.Raw
258+
... )
259+
>>> loaded_public_key = mldsa.MlDsa44PublicKey.from_public_bytes(public_bytes)
260+
261+
.. method:: public_bytes(encoding, format)
262+
263+
Allows serialization of the key to bytes. Encoding (
264+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`,
265+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`, or
266+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`) and
267+
format (
268+
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo`
269+
or
270+
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw`
271+
) are chosen to define the exact serialization.
272+
273+
:param encoding: A value from the
274+
:class:`~cryptography.hazmat.primitives.serialization.Encoding` enum.
275+
276+
:param format: A value from the
277+
:class:`~cryptography.hazmat.primitives.serialization.PublicFormat`
278+
enum. If the ``encoding`` is
279+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`
280+
then ``format`` must be
281+
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw`
282+
, otherwise it must be
283+
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo`.
284+
285+
:returns bytes: The public key bytes.
286+
287+
.. method:: public_bytes_raw()
288+
289+
Allows serialization of the key to raw bytes. This method is a
290+
convenience shortcut for calling :meth:`public_bytes` with
291+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`
292+
encoding and
293+
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw`
294+
format.
295+
296+
:return bytes: 1312-byte raw public key.
297+
298+
.. method:: verify(signature, data)
299+
300+
Verify a signature using ML-DSA-44.
301+
302+
:param signature: The signature to verify.
303+
:type signature: :term:`bytes-like`
304+
305+
:param data: The data to verify.
306+
:type data: :term:`bytes-like`
307+
308+
:returns: None
309+
:raises cryptography.exceptions.InvalidSignature: Raised when the
310+
signature cannot be verified.
311+
312+
.. method:: verify_with_context(signature, data, context)
313+
314+
Verify a signature using ML-DSA-44 with the context string that was used
315+
during signing. The same context must be provided for verification to
316+
succeed.
317+
318+
:param signature: The signature to verify.
319+
:type signature: :term:`bytes-like`
320+
321+
:param data: The data to verify.
322+
:type data: :term:`bytes-like`
323+
324+
:param context: The context string (up to 255 bytes) that was used during signing.
325+
:type context: :term:`bytes-like`
326+
327+
:returns: None
328+
:raises cryptography.exceptions.InvalidSignature: Raised when the
329+
signature cannot be verified.
330+
:raises ValueError: If the context is longer than 255 bytes.
331+
332+
333+
.. _`FIPS 204`: https://csrc.nist.gov/pubs/fips/204/final

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Deserialization
5050
deserializing
5151
Diffie
5252
Diffie
53+
digestAlgorithm
5354
disambiguating
5455
Django
5556
Docstrings

0 commit comments

Comments
 (0)