Skip to content
ecdsa.com

Ed25519 sign & verify · PHP

How to sign and verify Ed25519 signatures in PHP

PHP can sign and verify Ed25519 signatures with two bundled extensions: ext-openssl reads PEM keys and supports Ed25519 since PHP 8.4, while ext-sodium (libsodium) works with raw keys. Both produce the same deterministic RFC 8032 signature, as the run below shows.

Tested with PHP 8.5.11 (ext-openssl (OpenSSL 3.5.7), ext-sodium).

openssl.php — PEM keys with ext-openssl (PHP 8.4+)
<?php
// PEM keys through ext-openssl (Ed25519 support since PHP 8.4)
$privateKey = openssl_pkey_get_private(file_get_contents('key.pem'));
$publicKey = openssl_pkey_get_public(file_get_contents('pub.pem'));
$message = file_get_contents('message.txt');
// Algorithm 0: no digest. The default (SHA-1) fails with "invalid digest".
if (!openssl_sign($message, $signature, $privateKey, 0)) {
throw new RuntimeException(openssl_error_string());
}
file_put_contents('sig.bin', $signature);
echo 'signature: ', bin2hex($signature), PHP_EOL;
// openssl_verify returns 1 (valid), 0 (invalid) or -1/false (error)
echo 'valid: ', var_export(openssl_verify($message, $signature, $publicKey, 0) === 1, true), PHP_EOL;
echo 'tampered message valid: ', var_export(openssl_verify('hello ed25520', $signature, $publicKey, 0) === 1, true), PHP_EOL;
sodium.php — the same signature with ext-sodium and raw keys
<?php
// The same key with ext-sodium, which works on raw keys: a 32-byte seed
// (what PKCS#8 stores) expands to libsodium's 64-byte secret key.
$details = openssl_pkey_get_details(openssl_pkey_get_private(file_get_contents('key.pem')));
$seed = $details['ed25519']['priv_key'];
$keyPair = sodium_crypto_sign_seed_keypair($seed);
$message = file_get_contents('message.txt');
$signature = sodium_crypto_sign_detached($message, sodium_crypto_sign_secretkey($keyPair));
echo 'sodium signature: ', bin2hex($signature), PHP_EOL;
$publicKey = sodium_crypto_sign_publickey($keyPair); // 32 bytes
echo 'sodium valid: ', var_export(sodium_crypto_sign_verify_detached($signature, $message, $publicKey), true), PHP_EOL;

How it works

  1. openssl_pkey_get_private and openssl_pkey_get_public accept the PEM text directly; the key type comes from the PEM itself.
  2. The fourth argument of openssl_sign is 0, meaning no digest: Ed25519 hashes internally, and every OPENSSL_ALGO_* constant is rejected.
  3. openssl_verify returns 1 (valid), 0 (invalid) or -1/false (error), so the result is compared with === 1.
  4. ext-sodium wants a 64-byte secret key: sodium_crypto_sign_seed_keypair expands the 32-byte seed that PKCS#8 stores, read here from openssl_pkey_get_details(…)['ed25519']['priv_key'].

Gotchas

  • Without the algorithm argument, openssl_sign uses its default OPENSSL_ALGO_SHA1 and fails with error:1C80007A:Provider routines::invalid digest. It returns false rather than throwing, so check the return value.
  • if (openssl_verify(…)) treats -1 — an error such as a malformed key — as success, because -1 is truthy. Always compare with === 1.
  • Before PHP 8.4, ext-openssl cannot sign or verify with Ed25519 keys. On those versions, use ext-sodium: the 32-byte seed is the last 32 bytes of the 48-byte PKCS#8 DER that OpenSSL writes.
  • sodium_crypto_sign without _detached returns the signature followed by the message. Use the _detached functions when the signature travels separately, as it does in JWS, SSH and most APIs.

Related recipes