Skip to content
ecdsa.com

Verify a signature · Ruby

How to verify an ECDSA signature in Ruby

To verify an ECDSA signature in Ruby, use the openssl library that ships with Ruby: OpenSSL::PKey.read loads the public key and verify checks an ASN.1 DER signature. Raw r‖s signatures from JWS or WebCrypto are not DER and make verify raise rather than return false, so the second snippet re-encodes them with OpenSSL::ASN1 first.

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

verify.rb — verify a DER signature
require "openssl"
public_key = OpenSSL::PKey.read(File.read("pub.pem")) # "BEGIN PUBLIC KEY" (SPKI)
message = File.binread("message.txt")
signature = File.binread(ARGV.fetch(0, "sig.der")) # ASN.1 DER, as OpenSSL produces it
begin
# verify(digest, signature, data) hashes the data itself
puts public_key.verify("SHA256", signature, message) ? "valid" : "INVALID"
rescue OpenSSL::PKey::PKeyError => e
puts "malformed signature (#{e.message})" # bytes that are not DER raise instead of returning false
end
raw r‖s signatures: wrap the halves as ASN.1 INTEGERs, then verify
# r‖s from a JWS or WebCrypto: wrap both halves as ASN.1 INTEGERs, then verify as DER
raw = File.binread("sig.raw") # 64 bytes on P-256
half = raw.bytesize / 2
r, s = [raw.byteslice(0, half), raw.byteslice(half, half)].map { |bytes| OpenSSL::BN.new(bytes, 2) }
der = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer(r), OpenSSL::ASN1::Integer(s)]).to_der
puts public_key.verify("SHA256", der, message) ? "valid" : "INVALID"

How it works

  1. OpenSSL::PKey.read detects the PEM type and returns an OpenSSL::PKey::EC for an EC public key; the curve comes from the key.
  2. verify("SHA256", signature, data) takes the digest name, the signature and the original data — the order differs from most other languages — and hashes the data itself.
  3. It returns true or false for a well-formed signature and raises OpenSSL::PKey::PKeyError when the bytes are not DER at all; the run shows both outcomes.
  4. OpenSSL::BN.new(bytes, 2) reads a binary string as a big-endian unsigned integer, and OpenSSL::ASN1::Sequence with two Integers builds the DER encoding, sign bytes included.

Gotchas

  • A raw 64-byte signature raises OpenSSL::PKey::PKeyError: EVP_DigestVerify instead of returning false. Rescue it where you verify, or a malformed client signature becomes a 500 error.
  • A public key stored in an environment variable usually arrives with literal \n sequences instead of line breaks, and OpenSSL::PKey.read then fails with Could not parse PKey. Restore the newlines (gsub("\\n", "\n")) before reading.
  • verify with the wrong digest name returns false like a forgery: a SHA-384 signature checked with "SHA256" fails without any hint.
  • secp256k1 keys work through the system OpenSSL, but OpenSSL accepts high-S signatures; Bitcoin-style rules that require low-S must be checked separately.

Related recipes