<?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;<?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 bytesecho 'sodium valid: ', var_export(sodium_crypto_sign_verify_detached($signature, $message, $publicKey), true), PHP_EOL;How it works
openssl_pkey_get_privateandopenssl_pkey_get_publicaccept the PEM text directly; the key type comes from the PEM itself.- The fourth argument of
openssl_signis0, meaning no digest: Ed25519 hashes internally, and everyOPENSSL_ALGO_*constant is rejected. openssl_verifyreturns1(valid),0(invalid) or-1/false(error), so the result is compared with=== 1.- ext-sodium wants a 64-byte secret key:
sodium_crypto_sign_seed_keypairexpands the 32-byte seed that PKCS#8 stores, read here fromopenssl_pkey_get_details(…)['ed25519']['priv_key'].
Gotchas
- Without the algorithm argument,
openssl_signuses its defaultOPENSSL_ALGO_SHA1and fails witherror:1C80007A:Provider routines::invalid digest. It returnsfalserather than throwing, so check the return value. if (openssl_verify(…))treats-1— an error such as a malformed key — as success, because-1is 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_signwithout_detachedreturns the signature followed by the message. Use the_detachedfunctions when the signature travels separately, as it does in JWS, SSH and most APIs.