<?php// ext-openssl reads EC and (since PHP 8.4) Curve25519 keys; the key type comes from the PEMfunction agree(string $privateKeyFile, string $peerPublicKeyFile): string{ $privateKey = openssl_pkey_get_private(file_get_contents($privateKeyFile)); $peerKey = openssl_pkey_get_public(file_get_contents($peerPublicKeyFile)); $secret = openssl_pkey_derive($peerKey, $privateKey); // peer's public key comes first if ($secret === false) { throw new RuntimeException(openssl_error_string() ?: 'key agreement failed'); } return $secret;} foreach (['X25519' => 'x25519', 'P-256' => 'p256'] as $curve => $name) { $secret = agree("alice-$name.pem", "bob-$name.pub.pem"); // hash_hkdf(algo, key material, length, info, salt) — info comes before salt $key = hash_hkdf('sha256', $secret, 32, 'AES-256-GCM key v1', ''); echo "$curve shared secret: ", bin2hex($secret), PHP_EOL; echo "$curve derived key: ", bin2hex($key), PHP_EOL;}<?php// ext-sodium works on raw 32-byte X25519 keys$alice = openssl_pkey_get_details(openssl_pkey_get_private(file_get_contents('alice-x25519.pem')));$bob = openssl_pkey_get_details(openssl_pkey_get_public(file_get_contents('bob-x25519.pub.pem'))); $secret = sodium_crypto_scalarmult($alice['x25519']['priv_key'], $bob['x25519']['pub_key']);echo 'sodium X25519 shared secret: ', bin2hex($secret), PHP_EOL;How it works
openssl_pkey_derive($peerPublicKey, $privateKey)takes the peer's public key first and returns the raw shared secret, orfalse.hash_hkdf('sha256', $secret, 32, $info, $salt)derives the key. An empty salt is the RFC 5869 default.- The same function covers both curves, because the key type comes from the PEM files.
- For ext-sodium,
openssl_pkey_get_detailsexposes the raw X25519 bytes (['x25519']['priv_key'],['pub_key'], PHP 8.4+), andsodium_crypto_scalarmultcomputes the same secret from them.
Gotchas
- The argument order is public key first. Swapped arguments return
falseand leaveopenssl_error_string()empty, so the failure carries no message at all. hash_hkdftakesinfobeforesalt— the reverse of RFC 5869's order and of most other libraries. Passing the purpose string as the fifth argument makes it the salt and derives a different key.- Curves must match: a P-256 public key with an X25519 private key fails with
error:03000096:digital envelope routines::operation not supported for this keytype. - A malicious all-zero X25519 peer key makes
openssl_pkey_derivereturnfalse(failed during derivation) and makessodium_crypto_scalarmultthrowSodiumException: internal error. Both mean: reject the peer.