Skip to content
ecdsa.com
Ruby

Ruby OpenSSL (openssl gem)

Ruby's openssl gem, the binding every Ruby crypto and JWT library sits on: DER signatures, exceptions instead of false for malformed input, immutable keys on OpenSSL 3, and ML-DSA through the generic PKey API.
License
Ruby OR BSD-2-Clause
Checked version
4.0.2 ·
Source
github.com/ruby/openssl
Documentation
docs.ruby-lang.org/en/master/OpenSSL.html

Compatibility at a glance

ECDSAEdDSAPost-quantum

ECDSA curves
P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed448
Public key formats
spki-pemspki-der
Private key formats
pkcs8-pempkcs8-derencrypted-pkcs8sec1-pem

The openssl gem ships with Ruby and wraps the system OpenSSL — 3.5.7 in the ruby:4.0.7 image, with gem version 4.0.2. OpenSSL::PKey::EC#sign(digest, data) produces ASN.1 DER with a random nonce and no low-S normalization, and verify accepts high-S. dsa_sign_asn1 signs a digest you already computed.

Malformed signatures raise rather than return false: a 64-byte raw r‖s signature or a 3-byte blob raised OpenSSL::PKey::PKeyError: EVP_DigestVerify, while a well-formed signature over another message returned false. Code that only checks the boolean crashes on a JWS-style signature, so either rescue PKeyError or convert first, as the sample does; the DER ⇄ raw converter shows the two forms side by side.

Keys are immutable on OpenSSL 3: assigning private_key on an existing EC key raised pkeys are immutable on OpenSSL 3.0, which breaks older tutorials that build a key by setting the point or scalar. Construct keys from DER or PEM instead — a bare SEC1 point has to be wrapped in an SPKI structure first. Note which method writes what: to_pem on a private key writes SEC1 EC PRIVATE KEY including the secret, private_to_pem writes PKCS#8, and public_to_pem is the one to publish.

Curve coverage follows OpenSSL: P-224 to P-521, secp256k1 and the Brainpool r1 curves, plus Ed25519 and Ed448 through OpenSSL::PKey.generate_key. The same generic API generated, signed and verified ML-DSA-44/65/87 and SLH-DSA keys; ML-KEM keys can be generated, but PKey offers no encapsulation method in 4.0.2. For JWTs, ruby-jwt builds on this gem.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/ruby-openssl/run.sh ran Ruby OpenSSL (openssl gem) 4.0.2 on Ruby 4.0.7, OpenSSL 3.5.7 9 Jun 2026 in the container image ruby:4.0.7-slim-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    der

    sign(digest, data) returns ASN.1 DER.

    Recorded by the check
    fact ecdsa-signature-encoding = der
  • ECDSA nonce

    random

    Random nonce: two signatures of the same message differ.

    Recorded by the check
    fact ecdsa-nonce = random
  • Low-S when signing

    either

    s is not normalized: high-S values appeared among 64 signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = either
  • High-S when verifying

    accepts-high-s

    verify accepted the signature with s replaced by n − s.

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
  • What the signing call hashes

    hashes-message-or-digest

    sign hashes with the given digest; dsa_sign_asn1 signs a precomputed digest, and that signature verified against the message.

    Recorded by the check
    fact ecdsa-prehash = hashes-message-or-digest
  • Malformed ECDSA signature

    throws

    A raw r‖s signature and a 3-byte blob raise OpenSSL::PKey::PKeyError: EVP_DigestVerify; a valid signature over another message returns false.

    Recorded by the check
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: raw r||s: OpenSSL::PKey::PKeyError: EVP_DigestVerify
    evidence ecdsa-malformed-signature: 3 bytes: OpenSSL::PKey::PKeyError: EVP_DigestVerify
    evidence ecdsa-malformed-signature: wrong message: false
  • ECDSA curves

    P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-224, P-256, P-384, P-521, secp256k1 and brainpoolP256r1/P384r1/P512r1 signed and verified.

    Recorded by the check
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed448

    Ed25519 and Ed448 through OpenSSL::PKey.generate_key.

    Recorded by the check
    fact eddsa = Ed25519, Ed448
  • Post-quantum algorithms

    ML-DSASLH-DSA

    ML-DSA-44/65/87 and SLH-DSA-SHA2-128s generated, signed and verified through the generic PKey API; ML-KEM keys generate, but there is no encapsulation method.

    Recorded by the check
    fact pqc = ML-DSA, SLH-DSA
    evidence pqc: ML-KEM-768 key generation, and whether PKey offers encapsulate: false
  • Public key formats

    spki-pemspki-der

    SPKI PEM and DER through OpenSSL::PKey.read; a bare SEC1 point loaded only after wrapping it in SPKI.

    Recorded by the check
    fact public-key-formats = spki-pem, spki-der
    evidence public-key-formats: a 65-byte SEC1 point wrapped in an SPKI structure by hand, then loaded: ok
  • Private key formats

    pkcs8-pempkcs8-derencrypted-pkcs8sec1-pem

    PKCS#8 PEM and DER, encrypted PKCS#8 and SEC1 PEM; to_pem writes EC PRIVATE KEY, private_to_pem writes PKCS#8.

    Recorded by the check
    fact private-key-formats = pkcs8-pem, pkcs8-der, encrypted-pkcs8, sec1-pem
    evidence private-key-formats: key.to_pem writes -----BEGIN EC PRIVATE KEY-----; key.private_to_pem writes -----BEGIN PRIVATE KEY-----
  • EC key objects mutable

    no

    Assigning private_key on an existing key raised pkeys are immutable on OpenSSL 3.0.

    Recorded by the check
    fact ec-key-mutable = no
    evidence ec-key-mutable: assigning private_key on an existing EC key: OpenSSL::PKey::PKeyError: pkeys are immutable on OpenSSL 3.0

Usage

Sign as DER, convert to raw r‖s, publish only the public key, and see that verify raises on a raw signature.
require "openssl"

key = OpenSSL::PKey::EC.generate("prime256v1")
digest = OpenSSL::Digest.new("SHA256")
message = "order #1042 approved"

der = key.sign(digest, message) # ASN.1 DER

# Raw r||s for JWS or WebCrypto: take the two INTEGERs and pad each to 32 bytes.
r, s = OpenSSL::ASN1.decode(der).value.map(&:value)
raw = r.to_s(2).rjust(32, "\0") + s.to_s(2).rjust(32, "\0")

# Publish only the public half: public_to_pem, never to_pem (which writes the private key).
public_key = OpenSSL::PKey.read(key.public_to_pem)
puts "verified: #{public_key.verify(digest, der, message)}, raw bytes: #{raw.bytesize}"

# A raw signature is not "false" here: verify raises OpenSSL::PKey::PKeyError.
begin
  public_key.verify(digest, raw, message)
rescue OpenSSL::PKey::PKeyError => e
  puts "raw signature: #{e.class}"
end

The snippet harness runs every sample verbatim in the container image ruby:4.0.7-slim-trixie, and the run fails unless the output matches.

Command
ruby usage.rb
Output must match (regular expression)
verified: true, raw bytes: 64\nraw signature: OpenSSL::PKey::PKeyError

Libraries