From fda75f8a0fe88c7732688e75e8df052003970728 Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 12 Mar 2026 12:03:58 -0400 Subject: [PATCH 1/6] Add doc for MLDSA --- docs/hazmat/primitives/asymmetric/index.rst | 1 + docs/hazmat/primitives/asymmetric/mldsa.rst | 227 ++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 docs/hazmat/primitives/asymmetric/mldsa.rst diff --git a/docs/hazmat/primitives/asymmetric/index.rst b/docs/hazmat/primitives/asymmetric/index.rst index b0dd09aff24f..c9be024e659e 100644 --- a/docs/hazmat/primitives/asymmetric/index.rst +++ b/docs/hazmat/primitives/asymmetric/index.rst @@ -31,6 +31,7 @@ private key is able to decrypt it. rsa dh dsa + mldsa serialization utils cloudhsm diff --git a/docs/hazmat/primitives/asymmetric/mldsa.rst b/docs/hazmat/primitives/asymmetric/mldsa.rst new file mode 100644 index 000000000000..e67e025796d4 --- /dev/null +++ b/docs/hazmat/primitives/asymmetric/mldsa.rst @@ -0,0 +1,227 @@ +.. hazmat:: + +ML-DSA-65 signing +================= + +.. currentmodule:: cryptography.hazmat.primitives.asymmetric.mldsa + +ML-DSA-65 is a post-quantum digital signature algorithm based on module +lattices, standardized in `FIPS 204`_. + +Signing & Verification +~~~~~~~~~~~~~~~~~~~~~~~ + +.. doctest:: + + >>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa65PrivateKey + >>> private_key = MlDsa65PrivateKey.generate() + >>> signature = private_key.sign(b"my authenticated message") + >>> public_key = private_key.public_key() + >>> public_key.verify(signature, b"my authenticated message") + +Context-based Signing & Verification +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +ML-DSA-65 supports context strings to bind additional information to signatures. +The context can be up to 255 bytes and is used to differentiate signatures in +different contexts or protocols. + +.. doctest:: + + >>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa65PrivateKey + >>> private_key = MlDsa65PrivateKey.generate() + >>> context = b"email-signature-v1" + >>> signature = private_key.sign(b"my authenticated message", context) + >>> public_key = private_key.public_key() + >>> # Verification requires the same context + >>> public_key.verify(signature, b"my authenticated message", context) + +Key interfaces +~~~~~~~~~~~~~~ + +.. class:: MlDsa65PrivateKey + + .. versionadded:: 47.0 + + .. classmethod:: generate() + + Generate an ML-DSA-65 private key. + + :returns: :class:`MlDsa65PrivateKey` + + :raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-65 is + not supported by the backend ``cryptography`` is using. + + .. classmethod:: from_seed_bytes(data) + + Load an ML-DSA-65 private key from seed bytes. + + :param data: 32 byte seed. + :type data: :term:`bytes-like` + + :returns: :class:`MlDsa65PrivateKey` + + :raises ValueError: If the seed is not 32 bytes. + + :raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-65 is + not supported by the backend ``cryptography`` is using. + + .. doctest:: + + >>> from cryptography.hazmat.primitives.asymmetric import mldsa + >>> private_key = mldsa.MlDsa65PrivateKey.generate() + >>> seed = private_key.private_bytes_raw() + >>> same_key = mldsa.MlDsa65PrivateKey.from_seed_bytes(seed) + + .. method:: public_key() + + :returns: :class:`MlDsa65PublicKey` + + .. method:: sign(data, context=None) + + Sign the data using ML-DSA-65. An optional context string can be + provided. + + :param data: The data to sign. + :type data: :term:`bytes-like` + + :param context: An optional context string (up to 255 bytes). + :type context: :term:`bytes-like` or ``None`` + + :returns bytes: The signature (3309 bytes). + + :raises ValueError: If the context is longer than 255 bytes. + + .. method:: private_bytes(encoding, format, encryption_algorithm) + + Allows serialization of the key to bytes. Encoding ( + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`, + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`, or + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`) and + format ( + :attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8` + or + :attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.Raw` + ) are chosen to define the exact serialization. + + This method only returns the serialization of the seed form of the + private key, never the expanded one. + + :param encoding: A value from the + :class:`~cryptography.hazmat.primitives.serialization.Encoding` enum. + + :param format: A value from the + :class:`~cryptography.hazmat.primitives.serialization.PrivateFormat` + enum. If the ``encoding`` is + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw` + then ``format`` must be + :attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.Raw` + , otherwise it must be + :attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8`. + + :param encryption_algorithm: An instance of an object conforming to the + :class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryption` + interface. + + :return bytes: Serialized key. + + .. method:: private_bytes_raw() + + Allows serialization of the key to raw bytes. This method is a + convenience shortcut for calling :meth:`private_bytes` with + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw` + encoding, + :attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.Raw` + format, and + :class:`~cryptography.hazmat.primitives.serialization.NoEncryption`. + + This method only returns the seed form of the private key (32 bytes). + + :return bytes: Raw key (32-byte seed). + +.. class:: MlDsa65PublicKey + + .. versionadded:: 47.0 + + .. classmethod:: from_public_bytes(data) + + :param bytes data: 1952 byte public key. + + :returns: :class:`MlDsa65PublicKey` + + :raises ValueError: If the public key is not 1952 bytes. + + :raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-65 is + not supported by the backend ``cryptography`` is using. + + .. doctest:: + + >>> from cryptography.hazmat.primitives import serialization + >>> from cryptography.hazmat.primitives.asymmetric import mldsa + >>> private_key = mldsa.MlDsa65PrivateKey.generate() + >>> public_key = private_key.public_key() + >>> public_bytes = public_key.public_bytes( + ... encoding=serialization.Encoding.Raw, + ... format=serialization.PublicFormat.Raw + ... ) + >>> loaded_public_key = mldsa.MlDsa65PublicKey.from_public_bytes(public_bytes) + + .. method:: public_bytes(encoding, format) + + Allows serialization of the key to bytes. Encoding ( + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`, + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`, or + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`) and + format ( + :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo` + or + :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw` + ) are chosen to define the exact serialization. + + :param encoding: A value from the + :class:`~cryptography.hazmat.primitives.serialization.Encoding` enum. + + :param format: A value from the + :class:`~cryptography.hazmat.primitives.serialization.PublicFormat` + enum. If the ``encoding`` is + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw` + then ``format`` must be + :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw` + , otherwise it must be + :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo`. + + :returns bytes: The public key bytes. + + .. method:: public_bytes_raw() + + Allows serialization of the key to raw bytes. This method is a + convenience shortcut for calling :meth:`public_bytes` with + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw` + encoding and + :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw` + format. + + :return bytes: 1952-byte raw public key. + + .. method:: verify(signature, data, context=None) + + Verify a signature using ML-DSA-65. If a context string was used during + signing, the same context must be provided for verification to succeed. + + :param signature: The signature to verify. + :type signature: :term:`bytes-like` + + :param data: The data to verify. + :type data: :term:`bytes-like` + + :param context: An optional context string (up to 255 bytes) that was + used during signing. + :type context: :term:`bytes-like` or ``None`` + + :returns: None + :raises cryptography.exceptions.InvalidSignature: Raised when the + signature cannot be verified. + :raises ValueError: If the context is longer than 255 bytes. + + +.. _`FIPS 204`: https://csrc.nist.gov/pubs/fips/204/final From 6a8da4807d7edf15cbce2129e12fbc7bc018d8bf Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Tue, 31 Mar 2026 11:01:49 +0200 Subject: [PATCH 2/6] Skip ML-DSA-65 doctests when backend is not AWS-LC Signed-off-by: Facundo Tuesca --- docs/conf.py | 5 +++++ docs/hazmat/primitives/asymmetric/mldsa.rst | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index e44d94b46d84..cbb333d942b1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,6 +50,11 @@ "sphinx_inline_tabs", ] +doctest_global_setup = """ +from cryptography.hazmat.bindings._rust import openssl as _rust_openssl +SKIP_MLDSA = not _rust_openssl.CRYPTOGRAPHY_IS_AWSLC +""" + if spelling is not None: extensions.append("sphinxcontrib.spelling") diff --git a/docs/hazmat/primitives/asymmetric/mldsa.rst b/docs/hazmat/primitives/asymmetric/mldsa.rst index e67e025796d4..dc404055d7b4 100644 --- a/docs/hazmat/primitives/asymmetric/mldsa.rst +++ b/docs/hazmat/primitives/asymmetric/mldsa.rst @@ -12,6 +12,7 @@ Signing & Verification ~~~~~~~~~~~~~~~~~~~~~~~ .. doctest:: + :skipif: SKIP_MLDSA >>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa65PrivateKey >>> private_key = MlDsa65PrivateKey.generate() @@ -27,6 +28,7 @@ The context can be up to 255 bytes and is used to differentiate signatures in different contexts or protocols. .. doctest:: + :skipif: SKIP_MLDSA >>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa65PrivateKey >>> private_key = MlDsa65PrivateKey.generate() @@ -67,6 +69,7 @@ Key interfaces not supported by the backend ``cryptography`` is using. .. doctest:: + :skipif: SKIP_MLDSA >>> from cryptography.hazmat.primitives.asymmetric import mldsa >>> private_key = mldsa.MlDsa65PrivateKey.generate() @@ -155,6 +158,7 @@ Key interfaces not supported by the backend ``cryptography`` is using. .. doctest:: + :skipif: SKIP_MLDSA >>> from cryptography.hazmat.primitives import serialization >>> from cryptography.hazmat.primitives.asymmetric import mldsa From d72afe2cc88df3eea5747a7854915208bbe4d909 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Wed, 1 Apr 2026 22:20:20 +0200 Subject: [PATCH 3/6] Use ML-DSA instead of ML-DSA-65 in docs Signed-off-by: Facundo Tuesca --- docs/hazmat/primitives/asymmetric/mldsa.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hazmat/primitives/asymmetric/mldsa.rst b/docs/hazmat/primitives/asymmetric/mldsa.rst index dc404055d7b4..75e8323dcefa 100644 --- a/docs/hazmat/primitives/asymmetric/mldsa.rst +++ b/docs/hazmat/primitives/asymmetric/mldsa.rst @@ -1,11 +1,11 @@ .. hazmat:: -ML-DSA-65 signing +ML-DSA signing ================= .. currentmodule:: cryptography.hazmat.primitives.asymmetric.mldsa -ML-DSA-65 is a post-quantum digital signature algorithm based on module +ML-DSA is a post-quantum digital signature algorithm based on module lattices, standardized in `FIPS 204`_. Signing & Verification @@ -23,7 +23,7 @@ Signing & Verification Context-based Signing & Verification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -ML-DSA-65 supports context strings to bind additional information to signatures. +ML-DSA supports context strings to bind additional information to signatures. The context can be up to 255 bytes and is used to differentiate signatures in different contexts or protocols. From bc2be778e9c898b2415b56b8361b0649634015a2 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Wed, 1 Apr 2026 22:21:06 +0200 Subject: [PATCH 4/6] Put ML-DSA first in docs Signed-off-by: Facundo Tuesca --- docs/hazmat/primitives/asymmetric/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hazmat/primitives/asymmetric/index.rst b/docs/hazmat/primitives/asymmetric/index.rst index c9be024e659e..57197281dc4a 100644 --- a/docs/hazmat/primitives/asymmetric/index.rst +++ b/docs/hazmat/primitives/asymmetric/index.rst @@ -23,6 +23,7 @@ private key is able to decrypt it. .. toctree:: :maxdepth: 1 + mldsa ed25519 x25519 ed448 @@ -31,7 +32,6 @@ private key is able to decrypt it. rsa dh dsa - mldsa serialization utils cloudhsm From 565fa5f31cf66a9dd61f4c12949007f6e019a12a Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Wed, 1 Apr 2026 22:26:36 +0200 Subject: [PATCH 5/6] Use backend method to check if ML-DSA is supported Signed-off-by: Facundo Tuesca --- docs/conf.py | 3 +-- docs/hazmat/primitives/asymmetric/mldsa.rst | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cbb333d942b1..e277d0f2c1c9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,8 +51,7 @@ ] doctest_global_setup = """ -from cryptography.hazmat.bindings._rust import openssl as _rust_openssl -SKIP_MLDSA = not _rust_openssl.CRYPTOGRAPHY_IS_AWSLC +from cryptography.hazmat.backends.openssl.backend import backend as _backend """ if spelling is not None: diff --git a/docs/hazmat/primitives/asymmetric/mldsa.rst b/docs/hazmat/primitives/asymmetric/mldsa.rst index 75e8323dcefa..9d78e6e2787f 100644 --- a/docs/hazmat/primitives/asymmetric/mldsa.rst +++ b/docs/hazmat/primitives/asymmetric/mldsa.rst @@ -12,7 +12,7 @@ Signing & Verification ~~~~~~~~~~~~~~~~~~~~~~~ .. doctest:: - :skipif: SKIP_MLDSA + :skipif: not _backend.mldsa_supported() >>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa65PrivateKey >>> private_key = MlDsa65PrivateKey.generate() @@ -28,7 +28,7 @@ The context can be up to 255 bytes and is used to differentiate signatures in different contexts or protocols. .. doctest:: - :skipif: SKIP_MLDSA + :skipif: not _backend.mldsa_supported() >>> from cryptography.hazmat.primitives.asymmetric.mldsa import MlDsa65PrivateKey >>> private_key = MlDsa65PrivateKey.generate() @@ -69,7 +69,7 @@ Key interfaces not supported by the backend ``cryptography`` is using. .. doctest:: - :skipif: SKIP_MLDSA + :skipif: not _backend.mldsa_supported() >>> from cryptography.hazmat.primitives.asymmetric import mldsa >>> private_key = mldsa.MlDsa65PrivateKey.generate() @@ -158,7 +158,7 @@ Key interfaces not supported by the backend ``cryptography`` is using. .. doctest:: - :skipif: SKIP_MLDSA + :skipif: not _backend.mldsa_supported() >>> from cryptography.hazmat.primitives import serialization >>> from cryptography.hazmat.primitives.asymmetric import mldsa From 489dd56697fa0406ba432d4b6440d48b4044e1e8 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Wed, 1 Apr 2026 22:32:12 +0200 Subject: [PATCH 6/6] Add CHANGELOG entry Signed-off-by: Facundo Tuesca --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 53356693788c..5c6decfc3488 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -89,6 +89,8 @@ Changelog method for computing hashes. * Added :doc:`/hazmat/primitives/hpke` support implementing :rfc:`9180` for hybrid authenticated encryption. +* Added new :doc:`/hazmat/primitives/asymmetric/mldsa` module with + support for ML-DSA-65 signing and verification with the AWS-LC backend. .. _v46-0-6: