Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Changelog
* 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.
support for ML-DSA signing and verification with the AWS-LC backend.

.. _v46-0-6:

Expand Down
186 changes: 186 additions & 0 deletions docs/hazmat/primitives/asymmetric/mldsa.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,192 @@ different contexts or protocols.
Key interfaces
~~~~~~~~~~~~~~

.. class:: MlDsa44PrivateKey

.. versionadded:: 47.0

.. classmethod:: generate()

Generate an ML-DSA-44 private key.

:returns: :class:`MlDsa44PrivateKey`

:raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-44 is
not supported by the backend ``cryptography`` is using.

.. classmethod:: from_seed_bytes(data)

Load an ML-DSA-44 private key from seed bytes.

:param data: 32 byte seed.
:type data: :term:`bytes-like`

:returns: :class:`MlDsa44PrivateKey`

:raises ValueError: If the seed is not 32 bytes.

:raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-44 is
not supported by the backend ``cryptography`` is using.

.. doctest::
:skipif: not _backend.mldsa_supported()

>>> from cryptography.hazmat.primitives.asymmetric import mldsa
>>> private_key = mldsa.MlDsa44PrivateKey.generate()
>>> seed = private_key.private_bytes_raw()
>>> same_key = mldsa.MlDsa44PrivateKey.from_seed_bytes(seed)

.. method:: public_key()

:returns: :class:`MlDsa44PublicKey`

.. method:: sign(data, context=None)

Sign the data using ML-DSA-44. 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 (2420 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:: MlDsa44PublicKey

.. versionadded:: 47.0

.. classmethod:: from_public_bytes(data)

:param bytes data: 1312 byte public key.

:returns: :class:`MlDsa44PublicKey`

:raises ValueError: If the public key is not 1312 bytes.

:raises cryptography.exceptions.UnsupportedAlgorithm: If ML-DSA-44 is
not supported by the backend ``cryptography`` is using.

.. doctest::
:skipif: not _backend.mldsa_supported()

>>> from cryptography.hazmat.primitives import serialization
>>> from cryptography.hazmat.primitives.asymmetric import mldsa
>>> private_key = mldsa.MlDsa44PrivateKey.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.MlDsa44PublicKey.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: 1312-byte raw public key.

.. method:: verify(signature, data, context=None)

Verify a signature using ML-DSA-44. 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.

.. class:: MlDsa65PrivateKey

.. versionadded:: 47.0
Expand Down
5 changes: 5 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/openssl/mldsa.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
from cryptography.hazmat.primitives.asymmetric import mldsa
from cryptography.utils import Buffer

class MlDsa44PrivateKey: ...
class MlDsa44PublicKey: ...
class MlDsa65PrivateKey: ...
class MlDsa65PublicKey: ...

def generate_mldsa44_key() -> mldsa.MlDsa44PrivateKey: ...
def from_mldsa44_public_bytes(data: bytes) -> mldsa.MlDsa44PublicKey: ...
def from_mldsa44_seed_bytes(data: Buffer) -> mldsa.MlDsa44PrivateKey: ...
def generate_mldsa65_key() -> mldsa.MlDsa65PrivateKey: ...
def from_mldsa65_public_bytes(data: bytes) -> mldsa.MlDsa65PublicKey: ...
def from_mldsa65_seed_bytes(data: Buffer) -> mldsa.MlDsa65PrivateKey: ...
143 changes: 143 additions & 0 deletions src/cryptography/hazmat/primitives/asymmetric/mldsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,149 @@
from cryptography.utils import Buffer


class MlDsa44PublicKey(metaclass=abc.ABCMeta):
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 know we already did this for 65, but do we want to use this casing for the Python APIs? Historically we've (at least mostly) done all caps for initialisms (e.g., SECP256R1) for the Python side.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ugh, no, we don't. Follow up PR though?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

#14596 (claude will rebase this once that merges)

@classmethod
def from_public_bytes(cls, data: bytes) -> MlDsa44PublicKey:
from cryptography.hazmat.backends.openssl.backend import backend

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

return rust_openssl.mldsa.from_mldsa44_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,312 bytes for MLDSA-44.
"""

@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) -> MlDsa44PublicKey:
"""
Returns a copy.
"""

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


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


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

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

return rust_openssl.mldsa.generate_mldsa44_key()

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

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

return rust_openssl.mldsa.from_mldsa44_seed_bytes(data)

@abc.abstractmethod
def public_key(self) -> MlDsa44PublicKey:
"""
The MlDsa44PublicKey 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()).

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) -> MlDsa44PrivateKey:
"""
Returns a copy.
"""

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


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


class MlDsa65PublicKey(metaclass=abc.ABCMeta):
@classmethod
def from_public_bytes(cls, data: bytes) -> MlDsa65PublicKey:
Expand Down
17 changes: 16 additions & 1 deletion src/rust/cryptography-key-parsing/src/pkcs8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@ pub fn parse_private_key(data: &[u8]) -> KeyParsingResult<ParsedPrivateKey> {
Ok(ParsedPrivateKey::Pkey(pkey))
}

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

#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
AlgorithmParameters::MlDsa65 => {
let MlDsaPrivateKey::Seed(seed) = asn1::parse_single::<MlDsaPrivateKey>(k.private_key)?;
Expand Down Expand Up @@ -481,7 +492,11 @@ pub fn serialize_private_key(
#[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)
let params = match cryptography_openssl::mldsa::MlDsaVariant::from_pkey(pkey) {
cryptography_openssl::mldsa::MlDsaVariant::MlDsa44 => AlgorithmParameters::MlDsa44,
cryptography_openssl::mldsa::MlDsaVariant::MlDsa65 => AlgorithmParameters::MlDsa65,
};
(params, private_key_der)
}
_ => {
unimplemented!("Unknown key type");
Expand Down
Loading