Skip to content
ecdsa.com

ECDH key agreement · OpenSSL

How to derive a shared key with ECDH and HKDF with OpenSSL

The OpenSSL command line derives a shared key with ECDH in two commands: openssl pkeyutl -derive computes the X25519 or P-256 shared secret from your private key and the peer's public key, and openssl kdf HKDF turns that secret into a key. The test keys are the published RFC 7748 and RFC 5903 ones.

Tested with OpenSSL 3.5.7 (Debian 13).

generate key pairs for X25519 and P-256 and extract the public halves
# Each side: a key pair on the agreed curve; only the .pub.pem file is sent
openssl genpkey -algorithm X25519 -out my-x25519.pem
openssl pkey -in my-x25519.pem -pubout -out my-x25519.pub.pem
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out my-p256.pem
openssl pkey -in my-p256.pem -pubout -out my-p256.pub.pem
ECDH with pkeyutl -derive, then HKDF-SHA256 with openssl kdf
for curve in x25519 p256; do
# ECDH: my private key + the peer's public key -> raw shared secret (32 bytes)
openssl pkeyutl -derive -inkey alice-$curve.pem -peerkey bob-$curve.pub.pem -out $curve.secret
secret=$(od -An -v -tx1 $curve.secret | tr -d ' \n')
# HKDF-SHA256 turns the secret into a 32-byte key (no salt option = empty salt)
key=$(openssl kdf -keylen 32 -kdfopt digest:SHA256 -kdfopt hexkey:$secret \
-kdfopt info:"AES-256-GCM key v1" -binary HKDF | od -An -v -tx1 | tr -d ' \n')
echo "$curve shared secret: $secret"
echo "$curve derived key: $key"
done

How it works

  1. genpkey -algorithm X25519 and genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 create the private keys; pkey -pubout extracts the public keys that are exchanged.
  2. pkeyutl -derive -inkey mine.pem -peerkey theirs.pub.pem writes the raw 32-byte secret. Both sides run the same command with the roles swapped and get the same bytes.
  3. openssl kdf … HKDF takes the secret as hexkey:, the purpose string as info:, and -keylen 32. Leaving out salt gives the RFC 5869 default salt.
  4. -binary makes kdf write raw bytes, which od turns into the lowercase hex the other recipes print.

Gotchas

  • Keys of different types fail with Type of peer public key: X25519 does not match type of private key: EC, followed by pkeyutl: Error setting up peer key.
  • Pass the secret as hexkey: (or key: only for text). key: takes the characters themselves, so key:4a5d… derives from the ASCII of the hex string and silently gives a different key.
  • Without -binary, openssl kdf prints uppercase hex with colons (4E:CA:29:…). That is fine for reading, but strip the colons before comparing with other tools.
  • A malicious all-zero X25519 peer key makes pkeyutl -derive fail with Key derivation failed (ossl_ecx_compute_key:failed during derivation) — check the exit code in scripts.

Related recipes