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 falseend # r‖s from a JWS or WebCrypto: wrap both halves as ASN.1 INTEGERs, then verify as DERraw = File.binread("sig.raw") # 64 bytes on P-256half = raw.bytesize / 2r, 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_derputs public_key.verify("SHA256", der, message) ? "valid" : "INVALID" How it works
OpenSSL::PKey.readdetects the PEM type and returns anOpenSSL::PKey::ECfor an EC public key; the curve comes from the key.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.- It returns
trueorfalsefor a well-formed signature and raisesOpenSSL::PKey::PKeyErrorwhen the bytes are not DER at all; the run shows both outcomes. OpenSSL::BN.new(bytes, 2)reads a binary string as a big-endian unsigned integer, andOpenSSL::ASN1::Sequencewith twoIntegers builds the DER encoding, sign bytes included.
Gotchas
- A raw 64-byte signature raises
OpenSSL::PKey::PKeyError: EVP_DigestVerifyinstead of returningfalse. 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
\nsequences instead of line breaks, andOpenSSL::PKey.readthen fails withCould not parse PKey. Restore the newlines (gsub("\\n", "\n")) before reading. verifywith the wrong digest name returnsfalselike 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.