Digital Signature — Ruby source
Sign messages with Ed25519 or ECDSA and verify signatures. Prove authorship without revealing your private key.
This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# Digital Signature — Ed25519 / ECDSA P-256 message signing and verification
# via OpenSSL.
#
# Language: Ruby (3.1+, standard library only)
# Source: CosmoDev polyglot showcase port of the Digital Signature tool,
# ported from src/lib/digital-signature.ts (the canonical
# TypeScript implementation).
# License: display source — part of CosmoDev's polyglot tool pages.
#
# Key encoding: keys are exchanged as hex of the standard DER structures -
# SPKI (SubjectPublicKeyInfo) for public keys, PKCS8 for private keys - so
# they interoperate with OpenSSL, SSH, JOSE tooling and any other Web Crypto
# implementation. Signatures are hex of the raw signature bytes: 64 bytes for
# both algorithms (Ed25519 R||S; ECDSA P-256 in IEEE P1363 r||s layout, NOT
# ASN.1 DER - OpenSSL emits DER, so this port converts). ECDSA signs the
# SHA-256 digest of the message.
require 'openssl'
module DigitalSignature
KeyPair = Struct.new(:public_key, :private_key, keyword_init: true)
HEX_RE = /\A(?:[0-9a-fA-F]{2})+\z/
class << self
# Generate a fresh signing key pair, exported as hex SPKI / PKCS8 keys.
# +algorithm+ is 'ed25519' or 'ecdsa-p256'.
def generate_key_pair(algorithm)
assert_algorithm(algorithm)
pkey =
if algorithm == 'ed25519'
OpenSSL::PKey::Ed25519.generate
else
OpenSSL::PKey::EC.generate('prime256v1')
end
KeyPair.new(
public_key: to_hex(pkey.public_to_der),
private_key: to_hex(pkey.private_to_der)
)
end
# Sign +message+ with a hex PKCS8 private key. Returns the hex signature.
def sign_message(message, private_key_hex, algorithm)
assert_algorithm(algorithm)
raise ArgumentError, 'Message must not be empty.' if message.empty?
key = import_private_key(private_key_hex, algorithm)
signature =
if algorithm == 'ed25519'
# Ed25519 signs the message itself - no separate digest.
key.sign(nil, message)
else
# ECDSA signs the SHA-256 digest; OpenSSL returns ASN.1 DER, the
# shared format is IEEE P1363 r||s.
der_to_p1363(key.sign(OpenSSL::Digest::SHA256.new, message))
end
to_hex(signature)
end
# Verify +signature_hex+ against +message+ with a hex SPKI public key.
# Returns false when the signature, message or key simply do not match;
# raises only for malformed input (non-hex / wrong key type).
def verify_signature(message, signature_hex, public_key_hex, algorithm)
assert_algorithm(algorithm)
signature = hex_to_bytes(signature_hex, 'Signature')
key = import_public_key(public_key_hex, algorithm)
if algorithm == 'ed25519'
key.public_key.verify(nil, signature, message)
else
der = p1363_to_der(signature)
key.public_key.verify(OpenSSL::Digest::SHA256.new, der, message)
end
end
private
def assert_algorithm(algorithm)
return if %w[ed25519 ecdsa-p256].include?(algorithm)
raise ArgumentError, 'Unknown algorithm. Use "ed25519" or "ecdsa-p256".'
end
def label(algorithm)
algorithm == 'ed25519' ? 'Ed25519' : 'ECDSA P-256'
end
def hex_to_bytes(hex, what)
if hex.nil? || hex.empty? || !hex.match?(HEX_RE)
raise ArgumentError,
"#{what} must be a non-empty hex string (pairs of 0-9 / a-f digits)."
end
[hex].pack('H*')
end
def to_hex(bytes)
bytes.unpack1('H*')
end
def import_private_key(private_key_hex, algorithm)
bytes = hex_to_bytes(private_key_hex, 'Private key')
OpenSSL::PKey.read(bytes)
rescue OpenSSL::PKey::PKeyError
raise ArgumentError,
"Invalid private key for #{label(algorithm)} (expected hex PKCS8)."
end
def import_public_key(public_key_hex, algorithm)
bytes = hex_to_bytes(public_key_hex, 'Public key')
OpenSSL::PKey.read(bytes)
rescue OpenSSL::PKey::PKeyError
raise ArgumentError,
"Invalid public key for #{label(algorithm)} (expected hex SPKI)."
end
# ASN.1 DER (SEQUENCE { INTEGER r, INTEGER s }) -> 64-byte IEEE P1363
# r||s, each half left-padded with zeros to the field size.
def der_to_p1363(der)
seq = OpenSSL::ASN1.decode(der)
r, s = seq.value.map { |int| int.value.to_i }
hex = [r, s].map { |v| v.to_s(16).rjust(64, '0') }.join
[hex].pack('H*')
end
# 64-byte IEEE P1363 r||s -> ASN.1 DER (for OpenSSL's verifier).
def p1363_to_der(raw)
raise ArgumentError, 'Signature must be 64 bytes (r||s).' if raw.bytesize != 64
r, s = raw.unpack1('H*').scan(/.{64}/).map { |h| OpenSSL::BN.new(h, 16) }
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer(r),
OpenSSL::ASN1::Integer(s)]).to_der
end
end
end
Also available in 8 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →