# An ML-DSA-65 key pair (OpenSSL 3.5 or later)openssl genpkey -algorithm ML-DSA-65 -out key.pemopenssl pkey -in key.pem -pubout -out pub.pem # -rawin: the whole message goes in, ML-DSA hashes it internallyopenssl pkeyutl -sign -inkey key.pem -rawin -in message.txt -out message.sigopenssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in message.txt -sigfile message.sig# A context string binds the signature to one purpose; verify must pass the same oneopenssl pkeyutl -sign -inkey key.pem -rawin -in message.txt -pkeyopt context-string:invoice-v1 -out ctx.sigopenssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in message.txt -sigfile ctx.sig -pkeyopt context-string:invoice-v1 # Deterministic signing and a key from a known 32-byte seed: the same signature every timeopenssl genpkey -algorithm ML-DSA-65 -pkeyopt hexseed:$SEED_HEX -out seeded.pemopenssl pkeyutl -sign -inkey seeded.pem -rawin -in message.txt -pkeyopt deterministic:1 -out det.sigHow it works
openssl genpkey -algorithm ML-DSA-65creates the private key;openssl pkey -puboutwrites the public key as SubjectPublicKeyInfo (BEGIN PUBLIC KEY, 1974 bytes of DER).openssl pkeyutl -sign -rawinpasses the whole message to ML-DSA, which hashes it internally; the signature is always 3309 bytes for ML-DSA-65.-pkeyopt context-string:…binds the signature to a purpose (FIPS 204 allows up to 255 bytes); verification must pass the same context.-pkeyopt hexseed:derives the key from a 32-byte seed and-pkeyopt deterministic:1turns off the signing randomness: the run reproduces the shared test signature (SHA-2569456b955…68a6) exactly, and verifies it with the shared public key.
Gotchas
- Default signing is randomized ("hedged"): two signatures of the same message differ, and both verify. Do not compare signatures to check them; compare only with deterministic signing on both sides.
- OpenSSL writes the private key as seed plus expanded key by default (4098 bytes of DER). Go and pyca/cryptography refuse that form;
openssl pkey -provparam ml-dsa.output_formats=seed-onlywrites the 54-byte seed-only PKCS#8 every library reads. - ML-DSA takes no separate digest:
openssl dgst -sha256 -sign key.pemfails witherror:1C80007A:Provider routines:ml_dsa_digest_signverify_init:invalid digest. Usepkeyutl -rawin. - A signature made with a context string fails without it (
Signature Verification Failure), as does a tampered message.