from cryptography.hazmat.primitives.asymmetric import mlkem # Receiver: a key pair; only the encapsulation (public) key is handed outprivate_key = mlkem.MLKEM768PrivateKey.generate()public_key = private_key.public_key() # Sender: a fresh 32-byte shared secret and the ciphertext that carries itshared_secret, ciphertext = public_key.encapsulate() # Receiver: the same secret from the ciphertextreceived = private_key.decapsulate(ciphertext) print(f"encapsulation key {len(public_key.public_bytes_raw())} bytes, " f"ciphertext {len(ciphertext)} bytes, shared secret {len(shared_secret)} bytes")print("shared secret agreed:", received == shared_secret) from cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.asymmetric import mlkem # Keys in the standard containers, e.g. written by OpenSSL 3.5:# the encapsulation key as SPKI ("BEGIN PUBLIC KEY"), the decapsulation key as PKCS#8with open("openssl-pub.pem", "rb") as f: encapsulation_key = serialization.load_pem_public_key(f.read())with open("seed.pem", "rb") as f: decapsulation_key = serialization.load_pem_private_key(f.read(), password=None) if not isinstance(encapsulation_key, mlkem.MLKEM768PublicKey): raise TypeError(f"expected an ML-KEM-768 key, got {type(encapsulation_key).__name__}")if not isinstance(decapsulation_key, mlkem.MLKEM768PrivateKey): raise TypeError(f"expected an ML-KEM-768 key, got {type(decapsulation_key).__name__}") How it works
mlkem.MLKEM768PrivateKey.generate()creates the decapsulation key;public_key().public_bytes_raw()is the 1184-byte encapsulation key you hand out.encapsulate()returns a tuple(shared_secret, ciphertext)— 32 and 1088 bytes — in that order.decapsulate(ciphertext)returns the same 32 bytes on the receiver's side.load_pem_public_keyandload_pem_private_keyread OpenSSL's SPKI and seed-only PKCS#8 files; the run decapsulates OpenSSL's ciphertext to the shared secretdee35810…30f9, and OpenSSL decapsulates a ciphertext Python made to the same secret Python holds.
Gotchas
- A tampered ciphertext raises nothing:
decapsulatereturns a different secret (implicit rejection, FIPS 203). The parties only notice when a key derived from the secret fails to decrypt or authenticate. - A ciphertext of the wrong length does raise:
ValueError: Invalid ML-KEM-768 ciphertext. - cryptography 50.0.2 reads seed-only PKCS#8 but not OpenSSL's default private key file (seed plus expanded key):
Could not deserialize key data… ASN.1 parsing error: unexpected tag. Convert withopenssl pkey -provparam ml-kem.output_formats=seed-only. from_seed_bytestakes the 64-byte seed (d ‖ z), not a 32-byte one as for ML-DSA; the shared seed gives OpenSSL's exact key.
Related recipes
- How to encapsulate and decapsulate a shared secret with ML-KEM-768 with OpenSSL
- How to encapsulate and decapsulate a shared secret with ML-KEM-768 in Go
- How to encapsulate and decapsulate a shared secret with ML-KEM-768 in Node.js
- How to sign and verify with ML-DSA-65 in Python
- How to derive a shared key with ECDH and HKDF in Python