Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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,
) -> 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()).
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.

Same Q here about seed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(replied about this one in public_bytes_raw comment)

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.

I wonder if we should be using the noun seed here, rather than raw -- wdyt @reaperhulk ?

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.

@reaperhulk still pending :-)

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.

Hmm, well a seed only key can have two forms. The raw/bare seed or one wrapped in a PKCS8 structure. This parses the raw seed so I think I'm fine with it being called raw?


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
33 changes: 33 additions & 0 deletions src/rust/cryptography-key-parsing/src/pkcs8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ pub struct PrivateKeyInfo<'a> {
pub attributes: Option<Attributes<'a>>,
}

// RFC 9881 Section 6.5
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub enum MlDsaPrivateKey {
#[implicit(0)]
Seed([u8; 32]),
}

/// Extract the 32-byte ML-DSA-65 seed from a private key.
///
/// AWS-LC's `raw_private_key()` returns the expanded key, not the seed.
/// This function round-trips through the native PKCS#8 encoding to extract it.
/// https://github.com/aws/aws-lc/issues/3072
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
pub fn mldsa_seed_from_pkey(
pkey: &openssl::pkey::PKeyRef<openssl::pkey::Private>,
) -> Result<MlDsaPrivateKey, openssl::error::ErrorStack> {
let pkcs8_der = pkey.private_key_to_pkcs8()?;
let pki = asn1::parse_single::<PrivateKeyInfo<'_>>(&pkcs8_der).unwrap();
Ok(asn1::parse_single::<MlDsaPrivateKey>(pki.private_key).unwrap())
}

pub fn parse_private_key(
data: &[u8],
) -> KeyParsingResult<openssl::pkey::PKey<openssl::pkey::Private>> {
Expand Down Expand Up @@ -108,6 +130,12 @@ pub fn parse_private_key(
)?)
}

#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
AlgorithmParameters::MlDsa65 => {
let MlDsaPrivateKey::Seed(seed) = asn1::parse_single::<MlDsaPrivateKey>(k.private_key)?;
Ok(cryptography_openssl::mldsa::new_raw_private_key(&seed)?)
}

_ => Err(KeyParsingError::UnsupportedKeyType(
k.algorithm.oid().clone(),
)),
Expand Down Expand Up @@ -443,6 +471,11 @@ pub fn serialize_private_key(

(params, private_key_der)
}
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
cryptography_openssl::mldsa::PKEY_ID => {
let private_key_der = asn1::write_single(&mldsa_seed_from_pkey(pkey)?)?;
(AlgorithmParameters::MlDsa65, private_key_der)
}
_ => {
unimplemented!("Unknown key type");
}
Expand Down
15 changes: 15 additions & 0 deletions src/rust/cryptography-key-parsing/src/spki.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ pub fn parse_public_key(

Ok(openssl::pkey::PKey::from_dh(dh)?)
}
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
AlgorithmParameters::MlDsa65 => Ok(cryptography_openssl::mldsa::new_raw_public_key(
k.subject_public_key.as_bytes(),
)
.map_err(|_| KeyParsingError::InvalidKey)?),

_ => Err(KeyParsingError::UnsupportedKeyType(
k.algorithm.oid().clone(),
)),
Expand Down Expand Up @@ -214,6 +220,15 @@ pub fn serialize_public_key(

(params, pub_key_der)
}
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
cryptography_openssl::mldsa::PKEY_ID => {
let raw_bytes = pkey.raw_public_key()?;
assert_eq!(
raw_bytes.len(),
cryptography_openssl::mldsa::MLDSA65_PUBLIC_KEY_BYTES
);
(AlgorithmParameters::MlDsa65, raw_bytes)
}
_ => {
unimplemented!("Unknown key type");
}
Expand Down
2 changes: 2 additions & 0 deletions src/rust/cryptography-openssl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub mod aead;
pub mod cmac;
pub mod fips;
pub mod hmac;
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
pub mod mldsa;
#[cfg(any(
CRYPTOGRAPHY_IS_BORINGSSL,
CRYPTOGRAPHY_IS_LIBRESSL,
Expand Down
Loading