from cryptography.exceptions import InvalidSignaturefrom 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 bytespublic_key.verify(signature, message) # raises InvalidSignature on failure, returns Noneprint(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 purposessigned_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") from cryptography.exceptions import InvalidSignaturefrom cryptography.hazmat.primitives import serializationfrom 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
mldsa.MLDSA65PrivateKey.generate()creates a key;public_key().public_bytes_raw()is the 1952-byte public key andsign()returns a 3309-byte signature.verify(signature, message)returnsNoneand raisesInvalidSignatureon failure — wrap it intrywhen you need a boolean, asverify_with_pemdoes.load_pem_public_keyreturns whatever key the file holds, soverify_with_pemchecks forMLDSA65PublicKeyfirst.- Both
signandverifytake an optionalcontext(up to 255 bytes) that must match on both sides. serialization.load_pem_public_keyreads the SPKI that OpenSSL writes and returns anMLDSA65PublicKey; the run verifies OpenSSL's signature with it, andMLDSA65PrivateKey.from_seed_byteswith 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 withopenssl pkey -provparam ml-dsa.output_formats=seed-only. - A signature made without a context fails when verified with one, and vice versa (
InvalidSignature).