Skip to content
ecdsa.com

ML-DSA-65 sign/verify · Python

How to sign and verify with ML-DSA-65 in Python

To sign and verify with ML-DSA-65 in Python, use the mldsa module of the cryptography package (version 50): MLDSA65PrivateKey.generate(), sign(message) and public_key().verify(signature, message), with an optional context string. This recipe also loads an OpenSSL 3.5 public key from PEM and verifies a signature OpenSSL made.

Tested with cryptography 50.0.2 (Python 3.14.8, wheel with OpenSSL 4.0.3) and OpenSSL 3.5.7 (shared test vector and the default key file).

mldsa_demo.py — generate, sign, verify, with and without a context string
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric import mldsa
private_key = mldsa.MLDSA65PrivateKey.generate()
public_key = private_key.public_key()
message = b"hello, post-quantum world"
signature = private_key.sign(message) # randomized ("hedged") signing, 3309 bytes
public_key.verify(signature, message) # raises InvalidSignature on failure, returns None
print(f"public key {len(public_key.public_bytes_raw())} bytes, signature {len(signature)} bytes")
try:
public_key.verify(signature, message + b"!")
except InvalidSignature:
print("tampered message: InvalidSignature")
# A context string (0-255 bytes) separates signatures made for different purposes
signed_invoice = private_key.sign(message, b"invoice-v1")
public_key.verify(signed_invoice, message, b"invoice-v1")
try:
public_key.verify(signed_invoice, message)
except InvalidSignature:
print("context signature verified without the context: InvalidSignature")
verify_pem.py — verify with an SPKI public key file, e.g. from OpenSSL
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import mldsa
def verify_with_pem(public_key_path: str, message: bytes, signature: bytes) -> bool:
"""Verifies with an SPKI ("BEGIN PUBLIC KEY") file, e.g. one written by OpenSSL 3.5."""
with open(public_key_path, "rb") as f:
public_key = serialization.load_pem_public_key(f.read())
if not isinstance(public_key, mldsa.MLDSA65PublicKey):
raise TypeError(f"expected an ML-DSA-65 key, got {type(public_key).__name__}")
try:
public_key.verify(signature, message)
return True
except InvalidSignature:
return False

How it works

  1. mldsa.MLDSA65PrivateKey.generate() creates a key; public_key().public_bytes_raw() is the 1952-byte public key and sign() returns a 3309-byte signature.
  2. verify(signature, message) returns None and raises InvalidSignature on failure — wrap it in try when you need a boolean, as verify_with_pem does. load_pem_public_key returns whatever key the file holds, so verify_with_pem checks for MLDSA65PublicKey first.
  3. Both sign and verify take an optional context (up to 255 bytes) that must match on both sides.
  4. serialization.load_pem_public_key reads the SPKI that OpenSSL writes and returns an MLDSA65PublicKey; the run verifies OpenSSL's signature with it, and MLDSA65PrivateKey.from_seed_bytes with the shared 32-byte seed gives OpenSSL's exact key.

Gotchas

  • Signing is randomized: two signatures of the same message are not equal, and both verify.
  • cryptography 50.0.2 reads PKCS#8 keys that hold only the 32-byte seed, but not OpenSSL's default private key file (seed plus expanded key): it fails with Could not deserialize key data… ASN.1 parsing error: unexpected tag. Convert with openssl pkey -provparam ml-dsa.output_formats=seed-only.
  • A signature made without a context fails when verified with one, and vice versa (InvalidSignature).

Related recipes