# PHP cannot generate ML-DSA keys; OpenSSL 3.5 does, and PHP's ext/openssl then uses themopenssl genpkey -algorithm ML-DSA-65 -out key.pemopenssl pkey -in key.pem -pubout -out pub.pem<?php$privateKey = openssl_pkey_get_private(file_get_contents('key.pem'));$publicKey = openssl_pkey_get_public(file_get_contents('pub.pem'));$message = 'hello, post-quantum world'; // Algorithm 0: no digest — ML-DSA processes the whole message itselfif (!openssl_sign($message, $signature, $privateKey, 0)) { throw new RuntimeException('signing failed: ' . openssl_error_string());}echo 'signature: ', strlen($signature), " bytes\n";// openssl_verify returns 1 (valid), 0 (invalid) or -1/false (error)echo 'verified: ', openssl_verify($message, $signature, $publicKey, 0), "\n";echo 'tampered message: ', openssl_verify($message . '!', $signature, $publicKey, 0), "\n"; How it works
openssl genpkey -algorithm ML-DSA-65creates the private key andopenssl pkey -puboutthe public key; PHP loads both withopenssl_pkey_get_privateandopenssl_pkey_get_public.openssl_sign($message, $signature, $privateKey, 0)signs the whole message; ML-DSA hashes it internally, so the algorithm must be0. The signature is 3309 bytes.openssl_verifyreturns1for a valid signature,0for an invalid one and-1orfalsefor an error — compare with=== 1.- The run verifies the shared signature OpenSSL made with the shared public key.
Gotchas
- Without the fourth argument PHP uses SHA-1 as the digest, and signing fails:
openssl_signreturnsfalsewitherror:1C80007A:Provider routines::invalid digest. - ext/openssl has no way to pass a context string: a signature OpenSSL made with
context-string:invoice-v1returns0in PHP. Use ML-DSA without a context if PHP must verify. openssl_pkey_newcannot create ML-DSA keys (it returnsfalse), andopenssl_pkey_get_detailsreports the type as-1— there is noOPENSSL_KEYTYPE_constant for it yet.