Skip to content
ecdsa.com

Ed25519 sign & verify · Ruby

How to sign and verify Ed25519 signatures in Ruby

Ruby signs and verifies Ed25519 signatures with the openssl default gem: OpenSSL::PKey.read loads the PEM keys, and sign and verify take nil as the digest, because Ed25519 (RFC 8032) hashes the message itself. The 64-byte signature is deterministic and matches every other language for the same key.

Tested with Ruby 4.0.7 (openssl gem 4.0.2, OpenSSL 3.5.7).

sign.rb — load a PKCS#8 key and sign message.txt
require "openssl"
private_key = OpenSSL::PKey.read(File.read("key.pem")) # an OpenSSL::PKey::PKey with oid "ED25519"
raise "expected an Ed25519 key, got #{private_key.oid}" unless private_key.oid == "ED25519"
message = File.binread("message.txt")
signature = private_key.sign(nil, message) # digest nil: Ed25519 hashes internally
File.binwrite("sig.bin", signature)
puts "signature: #{signature.unpack1("H*")}"
verify.rb — verify with the SPKI public key
require "openssl"
public_key = OpenSSL::PKey.read(File.read("pub.pem"))
message = File.binread("message.txt")
signature = File.binread("sig.bin")
puts "valid: #{public_key.verify(nil, signature, message)}"
puts "tampered message valid: #{public_key.verify(nil, signature, "hello ed25520")}"

How it works

  1. OpenSSL::PKey.read returns a generic OpenSSL::PKey::PKey for Ed25519 — there is no dedicated class like OpenSSL::PKey::EC — so the code checks oid == "ED25519".
  2. sign(nil, message) signs the raw message; the nil digest is mandatory for Ed25519.
  3. verify(nil, signature, message) returns true or false. Note the order: signature before data.
  4. File.binread and File.binwrite keep the bytes untouched by any encoding conversion.

Gotchas

  • A digest name fails: key.sign("SHA256", data) raises OpenSSL::PKey::PKeyError: EVP_DigestSignInit: invalid digest (Explicit digest not allowed with EdDSA operations).
  • OpenSSL::PKey::EC.new(pem) does not read Ed25519 keys and raises OpenSSL::PKey::PKeyError: incorrect pkey type: ED25519. Use OpenSSL::PKey.read.
  • Raw 32-byte keys load with OpenSSL::PKey.new_raw_private_key("ED25519", seed) and new_raw_public_key, and export with raw_private_key and raw_public_key — available from the openssl gem 3.2, bundled since Ruby 3.3.
  • Keys and signatures move freely between this code and the ed25519 or RbNaCl gems, but those work with raw 32-byte keys, not PEM — convert at the boundary with the raw-key methods above.

Related recipes