Skip to content

Commit 8a18d58

Browse files
committed
Add ML-DSA-44 support alongside existing ML-DSA-65
Extend the ML-DSA module to support the ML-DSA-44 parameter set (FIPS 204 security level 2), following the MlDsaVariant enum and parameterized test patterns established on main. - MlDsaVariant enum: add MlDsa44 with NID, public key size, sign/verify FFI - X.509: ML-DSA-44 OID (2.16.840.1.101.3.4.3.17) and AlgorithmParameters - Key parsing: PKCS#8 and SPKI support using MlDsaVariant::from_pkey dispatch - Python API: MlDsa44PrivateKey/MlDsa44PublicKey with generate_mldsa44_key, from_mldsa44_seed_bytes, from_mldsa44_public_bytes barrier functions - Tests: ML-DSA-44 added to parameterized ML_DSA_VARIANTS, KAT vectors, and wycheproof tests - Docs: complete API documentation for both variants https://claude.ai/code/session_01GaM5UZrNWJvnptDHvFNBWg
1 parent dc2128c commit 8a18d58

File tree

13 files changed

+763
-80
lines changed

13 files changed

+763
-80
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Changelog
9090
* Added :doc:`/hazmat/primitives/hpke` support implementing :rfc:`9180` for
9191
hybrid authenticated encryption.
9292
* Added new :doc:`/hazmat/primitives/asymmetric/mldsa` module with
93-
support for ML-DSA-65 signing and verification with the AWS-LC backend.
93+
support for ML-DSA signing and verification with the AWS-LC backend.
9494

9595
.. _v46-0-6:
9696

docs/hazmat/primitives/asymmetric/mldsa.rst

Lines changed: 190 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ Signing & Verification
1414
.. doctest::
1515
:skipif: not _backend.mldsa_supported()
1616

17-
>>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa65PrivateKey
18-
>>> private_key = MlDsa65PrivateKey.generate()
17+
>>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa44PrivateKey
18+
>>> private_key = MlDsa44PrivateKey.generate()
1919
>>> signature = private_key.sign(b"my authenticated message")
2020
>>> public_key = private_key.public_key()
2121
>>> public_key.verify(signature, b"my authenticated message")
@@ -30,8 +30,8 @@ different contexts or protocols.
3030
.. doctest::
3131
:skipif: not _backend.mldsa_supported()
3232

33-
>>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa65PrivateKey
34-
>>> private_key = MlDsa65PrivateKey.generate()
33+
>>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa44PrivateKey
34+
>>> private_key = MlDsa44PrivateKey.generate()
3535
>>> context = b"email-signature-v1"
3636
>>> signature = private_key.sign(b"my authenticated message", context)
3737
>>> public_key = private_key.public_key()
@@ -41,6 +41,192 @@ different contexts or protocols.
4141
Key interfaces
4242
~~~~~~~~~~~~~~
4343

44+
.. class:: MlDsa44PrivateKey
45+
46+
.. versionadded:: 47.0
47+
48+
.. classmethod:: generate()
49+
50+
Generate an ML-DSA-44 private key.
51+
52+
:returns: :class:`MlDsa44PrivateKey`
53+
54+
:raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-44 is
55+
not supported by the backend ``cryptography`` is using.
56+
57+
.. classmethod:: from_seed_bytes(data)
58+
59+
Load an ML-DSA-44 private key from seed bytes.
60+
61+
:param data: 32 byte seed.
62+
:type data: :term:`bytes-like`
63+
64+
:returns: :class:`MlDsa44PrivateKey`
65+
66+
:raises ValueError: If the seed is not 32 bytes.
67+
68+
:raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-44 is
69+
not supported by the backend ``cryptography`` is using.
70+
71+
.. doctest::
72+
:skipif: not _backend.mldsa_supported()
73+
74+
>>> from cryptography.hazmat.primitives.asymmetric import mldsa
75+
>>> private_key = mldsa.MlDsa44PrivateKey.generate()
76+
>>> seed = private_key.private_bytes_raw()
77+
>>> same_key = mldsa.MlDsa44PrivateKey.from_seed_bytes(seed)
78+
79+
.. method:: public_key()
80+
81+
:returns: :class:`MlDsa44PublicKey`
82+
83+
.. method:: sign(data, context=None)
84+
85+
Sign the data using ML-DSA-44. An optional context string can be
86+
provided.
87+
88+
:param data: The data to sign.
89+
:type data: :term:`bytes-like`
90+
91+
:param context: An optional context string (up to 255 bytes).
92+
:type context: :term:`bytes-like` or ``None``
93+
94+
:returns bytes: The signature (2420 bytes).
95+
96+
:raises ValueError: If the context is longer than 255 bytes.
97+
98+
.. method:: private_bytes(encoding, format, encryption_algorithm)
99+
100+
Allows serialization of the key to bytes. Encoding (
101+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`,
102+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`, or
103+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`) and
104+
format (
105+
:attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8`
106+
or
107+
:attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.Raw`
108+
) are chosen to define the exact serialization.
109+
110+
This method only returns the serialization of the seed form of the
111+
private key, never the expanded one.
112+
113+
:param encoding: A value from the
114+
:class:`~cryptography.hazmat.primitives.serialization.Encoding` enum.
115+
116+
:param format: A value from the
117+
:class:`~cryptography.hazmat.primitives.serialization.PrivateFormat`
118+
enum. If the ``encoding`` is
119+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`
120+
then ``format`` must be
121+
:attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.Raw`
122+
, otherwise it must be
123+
:attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8`.
124+
125+
:param encryption_algorithm: An instance of an object conforming to the
126+
:class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryption`
127+
interface.
128+
129+
:return bytes: Serialized key.
130+
131+
.. method:: private_bytes_raw()
132+
133+
Allows serialization of the key to raw bytes. This method is a
134+
convenience shortcut for calling :meth:`private_bytes` with
135+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`
136+
encoding,
137+
:attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.Raw`
138+
format, and
139+
:class:`~cryptography.hazmat.primitives.serialization.NoEncryption`.
140+
141+
This method only returns the seed form of the private key (32 bytes).
142+
143+
:return bytes: Raw key (32-byte seed).
144+
145+
.. class:: MlDsa44PublicKey
146+
147+
.. versionadded:: 47.0
148+
149+
.. classmethod:: from_public_bytes(data)
150+
151+
:param bytes data: 1312 byte public key.
152+
153+
:returns: :class:`MlDsa44PublicKey`
154+
155+
:raises ValueError: If the public key is not 1312 bytes.
156+
157+
:raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-44 is
158+
not supported by the backend ``cryptography`` is using.
159+
160+
.. doctest::
161+
:skipif: not _backend.mldsa_supported()
162+
163+
>>> from cryptography.hazmat.primitives import serialization
164+
>>> from cryptography.hazmat.primitives.asymmetric import mldsa
165+
>>> private_key = mldsa.MlDsa44PrivateKey.generate()
166+
>>> public_key = private_key.public_key()
167+
>>> public_bytes = public_key.public_bytes(
168+
... encoding=serialization.Encoding.Raw,
169+
... format=serialization.PublicFormat.Raw
170+
... )
171+
>>> loaded_public_key = mldsa.MlDsa44PublicKey.from_public_bytes(public_bytes)
172+
173+
.. method:: public_bytes(encoding, format)
174+
175+
Allows serialization of the key to bytes. Encoding (
176+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`,
177+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`, or
178+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`) and
179+
format (
180+
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo`
181+
or
182+
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw`
183+
) are chosen to define the exact serialization.
184+
185+
:param encoding: A value from the
186+
:class:`~cryptography.hazmat.primitives.serialization.Encoding` enum.
187+
188+
:param format: A value from the
189+
:class:`~cryptography.hazmat.primitives.serialization.PublicFormat`
190+
enum. If the ``encoding`` is
191+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`
192+
then ``format`` must be
193+
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw`
194+
, otherwise it must be
195+
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo`.
196+
197+
:returns bytes: The public key bytes.
198+
199+
.. method:: public_bytes_raw()
200+
201+
Allows serialization of the key to raw bytes. This method is a
202+
convenience shortcut for calling :meth:`public_bytes` with
203+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`
204+
encoding and
205+
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw`
206+
format.
207+
208+
:return bytes: 1312-byte raw public key.
209+
210+
.. method:: verify(signature, data, context=None)
211+
212+
Verify a signature using ML-DSA-44. If a context string was used during
213+
signing, the same context must be provided for verification to succeed.
214+
215+
:param signature: The signature to verify.
216+
:type signature: :term:`bytes-like`
217+
218+
:param data: The data to verify.
219+
:type data: :term:`bytes-like`
220+
221+
:param context: An optional context string (up to 255 bytes) that was
222+
used during signing.
223+
:type context: :term:`bytes-like` or ``None``
224+
225+
:returns: None
226+
:raises cryptography.exceptions.InvalidSignature: Raised when the
227+
signature cannot be verified.
228+
:raises ValueError: If the context is longer than 255 bytes.
229+
44230
.. class:: MlDsa65PrivateKey
45231

46232
.. versionadded:: 47.0

src/cryptography/hazmat/bindings/_rust/openssl/mldsa.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
from cryptography.hazmat.primitives.asymmetric import mldsa
66
from cryptography.utils import Buffer
77

8+
class MlDsa44PrivateKey: ...
9+
class MlDsa44PublicKey: ...
810
class MlDsa65PrivateKey: ...
911
class MlDsa65PublicKey: ...
1012

13+
def generate_mldsa44_key() -> mldsa.MlDsa44PrivateKey: ...
14+
def from_mldsa44_public_bytes(data: bytes) -> mldsa.MlDsa44PublicKey: ...
15+
def from_mldsa44_seed_bytes(data: Buffer) -> mldsa.MlDsa44PrivateKey: ...
1116
def generate_mldsa65_key() -> mldsa.MlDsa65PrivateKey: ...
1217
def from_mldsa65_public_bytes(data: bytes) -> mldsa.MlDsa65PublicKey: ...
1318
def from_mldsa65_seed_bytes(data: Buffer) -> mldsa.MlDsa65PrivateKey: ...

src/cryptography/hazmat/primitives/asymmetric/mldsa.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,149 @@
1212
from cryptography.utils import Buffer
1313

1414

15+
class MlDsa44PublicKey(metaclass=abc.ABCMeta):
16+
@classmethod
17+
def from_public_bytes(cls, data: bytes) -> MlDsa44PublicKey:
18+
from cryptography.hazmat.backends.openssl.backend import backend
19+
20+
if not backend.mldsa_supported():
21+
raise UnsupportedAlgorithm(
22+
"ML-DSA-44 is not supported by this backend.",
23+
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM,
24+
)
25+
26+
return rust_openssl.mldsa.from_mldsa44_public_bytes(data)
27+
28+
@abc.abstractmethod
29+
def public_bytes(
30+
self,
31+
encoding: _serialization.Encoding,
32+
format: _serialization.PublicFormat,
33+
) -> bytes:
34+
"""
35+
The serialized bytes of the public key.
36+
"""
37+
38+
@abc.abstractmethod
39+
def public_bytes_raw(self) -> bytes:
40+
"""
41+
The raw bytes of the public key.
42+
Equivalent to public_bytes(Raw, Raw).
43+
44+
The public key is 1,312 bytes for MLDSA-44.
45+
"""
46+
47+
@abc.abstractmethod
48+
def verify(
49+
self,
50+
signature: Buffer,
51+
data: Buffer,
52+
context: Buffer | None = None,
53+
) -> None:
54+
"""
55+
Verify the signature.
56+
"""
57+
58+
@abc.abstractmethod
59+
def __eq__(self, other: object) -> bool:
60+
"""
61+
Checks equality.
62+
"""
63+
64+
@abc.abstractmethod
65+
def __copy__(self) -> MlDsa44PublicKey:
66+
"""
67+
Returns a copy.
68+
"""
69+
70+
@abc.abstractmethod
71+
def __deepcopy__(self, memo: dict) -> MlDsa44PublicKey:
72+
"""
73+
Returns a deep copy.
74+
"""
75+
76+
77+
if hasattr(rust_openssl, "mldsa"):
78+
MlDsa44PublicKey.register(rust_openssl.mldsa.MlDsa44PublicKey)
79+
80+
81+
class MlDsa44PrivateKey(metaclass=abc.ABCMeta):
82+
@classmethod
83+
def generate(cls) -> MlDsa44PrivateKey:
84+
from cryptography.hazmat.backends.openssl.backend import backend
85+
86+
if not backend.mldsa_supported():
87+
raise UnsupportedAlgorithm(
88+
"ML-DSA-44 is not supported by this backend.",
89+
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM,
90+
)
91+
92+
return rust_openssl.mldsa.generate_mldsa44_key()
93+
94+
@classmethod
95+
def from_seed_bytes(cls, data: Buffer) -> MlDsa44PrivateKey:
96+
from cryptography.hazmat.backends.openssl.backend import backend
97+
98+
if not backend.mldsa_supported():
99+
raise UnsupportedAlgorithm(
100+
"ML-DSA-44 is not supported by this backend.",
101+
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM,
102+
)
103+
104+
return rust_openssl.mldsa.from_mldsa44_seed_bytes(data)
105+
106+
@abc.abstractmethod
107+
def public_key(self) -> MlDsa44PublicKey:
108+
"""
109+
The MlDsa44PublicKey derived from the private key.
110+
"""
111+
112+
@abc.abstractmethod
113+
def private_bytes(
114+
self,
115+
encoding: _serialization.Encoding,
116+
format: _serialization.PrivateFormat,
117+
encryption_algorithm: _serialization.KeySerializationEncryption,
118+
) -> bytes:
119+
"""
120+
The serialized bytes of the private key.
121+
122+
This method only returns the serialization of the seed form of the
123+
private key, never the expanded one.
124+
"""
125+
126+
@abc.abstractmethod
127+
def private_bytes_raw(self) -> bytes:
128+
"""
129+
The raw bytes of the private key.
130+
Equivalent to private_bytes(Raw, Raw, NoEncryption()).
131+
132+
This method only returns the seed form of the private key (32 bytes).
133+
"""
134+
135+
@abc.abstractmethod
136+
def sign(self, data: Buffer, context: Buffer | None = None) -> bytes:
137+
"""
138+
Signs the data.
139+
"""
140+
141+
@abc.abstractmethod
142+
def __copy__(self) -> MlDsa44PrivateKey:
143+
"""
144+
Returns a copy.
145+
"""
146+
147+
@abc.abstractmethod
148+
def __deepcopy__(self, memo: dict) -> MlDsa44PrivateKey:
149+
"""
150+
Returns a deep copy.
151+
"""
152+
153+
154+
if hasattr(rust_openssl, "mldsa"):
155+
MlDsa44PrivateKey.register(rust_openssl.mldsa.MlDsa44PrivateKey)
156+
157+
15158
class MlDsa65PublicKey(metaclass=abc.ABCMeta):
16159
@classmethod
17160
def from_public_bytes(cls, data: bytes) -> MlDsa65PublicKey:

0 commit comments

Comments
 (0)