Skip to content
ecdsa.com

ML-DSA-65 sign/verify · PHP

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

To sign and verify with ML-DSA-65 in PHP, create the keys with OpenSSL 3.5 and use them through ext/openssl: openssl_sign and openssl_verify accept ML-DSA keys when the algorithm argument is 0 (no digest). PHP 8.5 linked against OpenSSL 3.5 handles the signing; what it cannot do is generate keys or pass a context string, as this recipe shows.

Tested with PHP 8.5.11 (ext/openssl, OpenSSL 3.5.7) and OpenSSL 3.5.7 (key generation and the context signature).

the key pair, made with OpenSSL
# PHP cannot generate ML-DSA keys; OpenSSL 3.5 does, and PHP's ext/openssl then uses them
openssl genpkey -algorithm ML-DSA-65 -out key.pem
openssl pkey -in key.pem -pubout -out pub.pem
sign.php — sign and verify with ext/openssl
<?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 itself
if (!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

  1. openssl genpkey -algorithm ML-DSA-65 creates the private key and openssl pkey -pubout the public key; PHP loads both with openssl_pkey_get_private and openssl_pkey_get_public.
  2. openssl_sign($message, $signature, $privateKey, 0) signs the whole message; ML-DSA hashes it internally, so the algorithm must be 0. The signature is 3309 bytes.
  3. openssl_verify returns 1 for a valid signature, 0 for an invalid one and -1 or false for an error — compare with === 1.
  4. 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_sign returns false with error:1C80007A:Provider routines::invalid digest.
  • ext/openssl has no way to pass a context string: a signature OpenSSL made with context-string:invoice-v1 returns 0 in PHP. Use ML-DSA without a context if PHP must verify.
  • openssl_pkey_new cannot create ML-DSA keys (it returns false), and openssl_pkey_get_details reports the type as -1 — there is no OPENSSL_KEYTYPE_ constant for it yet.

Related recipes