require "openssl" # One method for both curves: PKey#derive (openssl gem 3.0+) works for X25519 and EC keysdef agree(private_key_file, peer_public_key_file) private_key = OpenSSL::PKey.read(File.read(private_key_file)) peer_key = OpenSSL::PKey.read(File.read(peer_public_key_file)) private_key.derive(peer_key)end def derive_key(shared_secret) # salt: is a required keyword; an empty string means the RFC 5869 default OpenSSL::KDF.hkdf(shared_secret, salt: "", info: "AES-256-GCM key v1", length: 32, hash: "SHA256")end { "X25519" => "x25519", "P-256" => "p256" }.each do |curve, name| secret = agree("alice-#{name}.pem", "bob-#{name}.pub.pem") puts "#{curve} shared secret: #{secret.unpack1("H*")}" puts "#{curve} derived key: #{derive_key(secret).unpack1("H*")}"endrequire "openssl" # In a live exchange each side makes a fresh key pair and sends only its public keyalice = OpenSSL::PKey.generate_key("X25519") # or OpenSSL::PKey::EC.generate("prime256v1")bob = OpenSSL::PKey.generate_key("X25519") from_alice = alice.public_to_pem # what travels over the wirefrom_bob = bob.public_to_pem alice_secret = alice.derive(OpenSSL::PKey.read(from_bob))bob_secret = bob.derive(OpenSSL::PKey.read(from_alice))puts "both sides agree: #{alice_secret == bob_secret}"How it works
OpenSSL::PKey.readloads both the private key and the peer's public key; X25519 keys come back as genericOpenSSL::PKey::PKey, P-256 keys asOpenSSL::PKey::EC.private_key.derive(peer_key)returns the raw shared secret for either type.OpenSSL::KDF.hkdf(secret, salt: "", info: …, length: 32, hash: "SHA256")derives the key; the empty salt is the RFC 5869 default.- For a live exchange,
OpenSSL::PKey.generate_key("X25519")creates a key pair andpublic_to_pemgives the part that is sent.
Gotchas
- All keywords of
OpenSSL::KDF.hkdfare required: leaving outsalt:raisesArgumentError: missing keyword: :salt. Pass""for the default. - Older code uses
ec_key.dh_compute_key(peer.public_key), which works only for EC keys and takes a point, not a key.deriveworks for both curves and is the method to use. - Keys of different types fail with
OpenSSL::PKey::PKeyError: EVP_PKEY_derive_set_peer: operation not supported for this keytype. - A malicious all-zero X25519 peer key raises
OpenSSL::PKey::PKeyError: EVP_PKEY_derive: failed during derivation; rescue it and reject the peer.