Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
176e34b
MLDSA65 support for AWS-LC
DarkaMaul Mar 2, 2026
65ab941
Improve coverage
DarkaMaul Mar 3, 2026
4fab32f
Initial review
DarkaMaul Mar 4, 2026
2450b41
First round of review
DarkaMaul Mar 5, 2026
ef4345f
Clean tests
DarkaMaul Mar 5, 2026
2f6b313
Revert spurious formatting
DarkaMaul Mar 6, 2026
4fe06c7
Incorporate review
DarkaMaul Mar 6, 2026
ed6d9de
Rename from mldsa65 to mldsa
DarkaMaul Mar 6, 2026
b60d88b
Improve serialization/deserialization
DarkaMaul Mar 6, 2026
39c9db3
Fix coverage
DarkaMaul Mar 9, 2026
db6d98d
Use ASN1 struct to parse the private key
DarkaMaul Mar 9, 2026
c78a527
Merge branch 'pyca:main' into dm/mldsa65-aws-lc
DarkaMaul Mar 13, 2026
7bde7d5
Use Enum instead of Struct
DarkaMaul Mar 13, 2026
a8083a4
Inline function
DarkaMaul Mar 13, 2026
f849147
Change match arm
DarkaMaul Mar 13, 2026
7284fec
Remove duplicate comment
DarkaMaul Mar 13, 2026
16bd0d3
Change unimplemented by assert
DarkaMaul Mar 13, 2026
b66fdca
More review fix
DarkaMaul Mar 13, 2026
67c5374
Merge branch 'pyca:main' into dm/mldsa65-aws-lc
DarkaMaul Mar 23, 2026
e457a27
Fix Wycheproof vectors handling
DarkaMaul Mar 23, 2026
8cb4c7e
Add NO-COVERAGE marker
DarkaMaul Mar 23, 2026
a532164
Merge remote-tracking branch 'origin/main' into dm/mldsa65-aws-lc
DarkaMaul Mar 23, 2026
c56210c
Remove vectors
DarkaMaul Mar 24, 2026
b2cd7dd
Merge remote-tracking branch 'origin/main' into dm/mldsa65-aws-lc
DarkaMaul Mar 25, 2026
f492ca3
Review comments
DarkaMaul Mar 25, 2026
d517b35
Remove markers
DarkaMaul Mar 25, 2026
530aa92
Merge branch 'pyca:main' into dm/mldsa65-aws-lc
DarkaMaul Mar 26, 2026
4b8430c
Merge branch 'pyca:main' into dm/mldsa65-aws-lc
DarkaMaul Mar 26, 2026
1f1303a
Additional round of review
DarkaMaul Mar 27, 2026
7a6f3a2
Merge branch 'pyca:main' into dm/mldsa65-aws-lc
DarkaMaul Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/development/custom-vectors/mldsa.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
ML-DSA vector creation
======================

This page documents the code that was used to generate the ML-DSA test
vectors. These vectors are used to verify:

* Unsupported ML-DSA variants (i.e. variants other than ML-DSA-65) are
correctly rejected when loading keys.
* ML-DSA-65 private keys without a seed are correctly rejected.

The following Python script was run to generate the vector files.

.. literalinclude:: /development/custom-vectors/mldsa/generate_mldsa.py

Download link: :download:`generate_mldsa.py
</development/custom-vectors/mldsa/generate_mldsa.py>`

ML-DSA-44 public key
--------------------

The public key was derived from the private key using the OpenSSL CLI
(requires OpenSSL 3.5+):

.. code-block:: console

$ openssl pkey -in mldsa44_priv.der -inform DER -pubout -outform DER -out mldsa44_pub.der
74 changes: 74 additions & 0 deletions docs/development/custom-vectors/mldsa/generate_mldsa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

import os

from cryptography import x509
from cryptography.hazmat import asn1


@asn1.sequence
class AlgorithmIdentifier:
algorithm: x509.ObjectIdentifier


@asn1.sequence
class OneAsymmetricKey:
version: int
algorithm: AlgorithmIdentifier
private_key: bytes


# ML-DSA-PrivateKey ::= CHOICE {
# seed [0] IMPLICIT OCTET STRING (SIZE (32)),
# expandedKey OCTET STRING,
# both SEQUENCE { seed, expandedKey }
# }
MLDSA_SEED_BYTES = 32


def generate_mldsa44_unsupported_variant(output_dir: str) -> None:
seed = b"\x2a" * MLDSA_SEED_BYTES
# [0] IMPLICIT OCTET STRING: tag 0x80, length 0x20
seed_only_privkey = b"\x80\x20" + seed

# ML-DSA-44 OID: 2.16.840.1.101.3.4.3.17
obj = OneAsymmetricKey(
version=0,
algorithm=AlgorithmIdentifier(
algorithm=x509.ObjectIdentifier("2.16.840.1.101.3.4.3.17"),
),
private_key=seed_only_privkey,
)
with open(os.path.join(output_dir, "mldsa44_priv.der"), "wb") as f:
f.write(asn1.encode_der(obj))


def generate_mldsa65_noseed(output_dir: str) -> None:
# ML-DSA-65 OID: 2.16.840.1.101.3.4.3.18
# Generate an ML-DSA-65 PKCS#8 key whose inner privateKey is an
# empty SEQUENCE (0x30 0x00) — i.e. the "both" SEQUENCE form with
# no seed present. This exercises the InvalidKey error path in the
# Rust parser when seed is None.
obj = OneAsymmetricKey(
version=0,
algorithm=AlgorithmIdentifier(
algorithm=x509.ObjectIdentifier("2.16.840.1.101.3.4.3.18"),
),
private_key=b"\x30\x00",
)
with open(os.path.join(output_dir, "mldsa65_noseed_priv.der"), "wb") as f:
f.write(asn1.encode_der(obj))


def main():
output_dir = os.path.join(
"vectors", "cryptography_vectors", "asymmetric", "MLDSA"
)
generate_mldsa44_unsupported_variant(output_dir)
generate_mldsa65_noseed(output_dir)


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Asymmetric ciphers
* ``asymmetric/PKCS8/ed25519-scrypt.pem`` a PKCS8 encoded Ed25519 key from
RustCrypto using scrypt as the KDF. The password is ``hunter42``.
* FIPS 204 ML-DSA-{44,65,87} KAT vectors from `post-quantum-cryptography/KAT`_.
* ML-DSA-44 PKCS#8 and SPKI DER test vectors generated by this project.
See :doc:`/development/custom-vectors/mldsa`

Custom asymmetric vectors
~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -74,6 +76,7 @@ Custom asymmetric vectors

custom-vectors/secp256k1
custom-vectors/rsa-oaep-sha2
custom-vectors/mldsa

* ``asymmetric/PEM_Serialization/ec_private_key.pem`` and
``asymmetric/DER_Serialization/ec_private_key.der`` - Contains an Elliptic
Expand Down Expand Up @@ -1212,6 +1215,7 @@ Created Vectors
custom-vectors/idea
custom-vectors/seed
custom-vectors/hkdf
custom-vectors/mldsa
custom-vectors/rc2


Expand Down
3 changes: 3 additions & 0 deletions src/cryptography/hazmat/backends/openssl/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ def x448_supported(self) -> bool:
and not rust_openssl.CRYPTOGRAPHY_IS_AWSLC
)

def mldsa_supported(self) -> bool:
return rust_openssl.CRYPTOGRAPHY_IS_AWSLC

def ed25519_supported(self) -> bool:
return not self._fips_enabled

Expand Down
2 changes: 2 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ from cryptography.hazmat.bindings._rust.openssl import (
hpke,
kdf,
keys,
mldsa,
poly1305,
rsa,
x448,
Expand All @@ -38,6 +39,7 @@ __all__ = [
"hpke",
"kdf",
"keys",
"mldsa",
"openssl_version",
"openssl_version_text",
"poly1305",
Expand Down
13 changes: 13 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/openssl/mldsa.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

from cryptography.hazmat.primitives.asymmetric import mldsa
from cryptography.utils import Buffer

class MlDsa65PrivateKey: ...
class MlDsa65PublicKey: ...

def generate_key() -> mldsa.MlDsa65PrivateKey: ...
def from_public_bytes(data: bytes) -> mldsa.MlDsa65PublicKey: ...
def from_seed_bytes(data: Buffer) -> mldsa.MlDsa65PrivateKey: ...
155 changes: 155 additions & 0 deletions src/cryptography/hazmat/primitives/asymmetric/mldsa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

from __future__ import annotations

import abc

from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
from cryptography.hazmat.bindings._rust import openssl as rust_openssl
from cryptography.hazmat.primitives import _serialization
from cryptography.utils import Buffer


class MlDsa65PublicKey(metaclass=abc.ABCMeta):
@classmethod
def from_public_bytes(cls, data: bytes) -> MlDsa65PublicKey:
from cryptography.hazmat.backends.openssl.backend import backend

if not backend.mldsa_supported():
raise UnsupportedAlgorithm(
"ML-DSA-65 is not supported by this backend.",
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM,
)

return rust_openssl.mldsa.from_public_bytes(data)

@abc.abstractmethod
def public_bytes(
self,
encoding: _serialization.Encoding,
format: _serialization.PublicFormat,
) -> bytes:
"""
The serialized bytes of the public key.
"""

@abc.abstractmethod
def public_bytes_raw(self) -> bytes:
"""
The raw bytes of the public key.
Equivalent to public_bytes(Raw, Raw).

The public key is 1,952 bytes for MLDSA-65.
"""

@abc.abstractmethod
def verify(
self,
signature: Buffer,
data: Buffer,
context: Buffer | None = None,
) -> None:
"""
Verify the signature.
"""

@abc.abstractmethod
def __eq__(self, other: object) -> bool:
"""
Checks equality.
"""

@abc.abstractmethod
def __copy__(self) -> MlDsa65PublicKey:
"""
Returns a copy.
"""

@abc.abstractmethod
def __deepcopy__(self, memo: dict) -> MlDsa65PublicKey:
"""
Returns a deep copy.
"""


if hasattr(rust_openssl, "mldsa"):
MlDsa65PublicKey.register(rust_openssl.mldsa.MlDsa65PublicKey)


class MlDsa65PrivateKey(metaclass=abc.ABCMeta):
@classmethod
def generate(cls) -> MlDsa65PrivateKey:
from cryptography.hazmat.backends.openssl.backend import backend

if not backend.mldsa_supported():
raise UnsupportedAlgorithm(
"ML-DSA-65 is not supported by this backend.",
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM,
)

return rust_openssl.mldsa.generate_key()

@classmethod
def from_seed_bytes(cls, data: Buffer) -> MlDsa65PrivateKey:
from cryptography.hazmat.backends.openssl.backend import backend

if not backend.mldsa_supported():
raise UnsupportedAlgorithm(
"ML-DSA-65 is not supported by this backend.",
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM,
)

return rust_openssl.mldsa.from_seed_bytes(data)

@abc.abstractmethod
def public_key(self) -> MlDsa65PublicKey:
"""
The MlDsa65PublicKey derived from the private key.
"""

@abc.abstractmethod
def private_bytes(
self,
encoding: _serialization.Encoding,
format: _serialization.PrivateFormat,
encryption_algorithm: (_serialization.KeySerializationEncryption),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
encryption_algorithm: (_serialization.KeySerializationEncryption),
encryption_algorithm: _serialization.KeySerializationEncryption,

) -> bytes:
"""
The serialized bytes of the private key.

This method only returns the serialization of the seed form of the
private key, never the expanded one.
"""

@abc.abstractmethod
def private_bytes_raw(self) -> bytes:
"""
The raw bytes of the private key.
Equivalent to private_bytes(Raw, Raw, NoEncryption()).

This method only returns the seed form of the private key (32 bytes).
"""

@abc.abstractmethod
def sign(self, data: Buffer, context: Buffer | None = None) -> bytes:
"""
Signs the data.
"""

@abc.abstractmethod
def __copy__(self) -> MlDsa65PrivateKey:
"""
Returns a copy.
"""

@abc.abstractmethod
def __deepcopy__(self, memo: dict) -> MlDsa65PrivateKey:
"""
Returns a deep copy.
"""


if hasattr(rust_openssl, "mldsa"):
MlDsa65PrivateKey.register(rust_openssl.mldsa.MlDsa65PrivateKey)
3 changes: 3 additions & 0 deletions src/cryptography/hazmat/primitives/asymmetric/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
ec,
ed448,
ed25519,
mldsa,
rsa,
x448,
x25519,
Expand All @@ -26,6 +27,7 @@
ec.EllipticCurvePublicKey,
ed25519.Ed25519PublicKey,
ed448.Ed448PublicKey,
mldsa.MlDsa65PublicKey,
x25519.X25519PublicKey,
x448.X448PublicKey,
]
Expand All @@ -42,6 +44,7 @@
dh.DHPrivateKey,
ed25519.Ed25519PrivateKey,
ed448.Ed448PrivateKey,
mldsa.MlDsa65PrivateKey,
rsa.RSAPrivateKey,
dsa.DSAPrivateKey,
ec.EllipticCurvePrivateKey,
Expand Down
Loading