Skip to content

SSH Key Generator — Ruby source

Generate Ed25519 or RSA SSH key pairs in your browser. Copy or download the public and private keys. No server involved.

This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.

# SSH Key Generator — Ed25519 / RSA SSH key pair generation via OpenSSL.
#
# Language: Ruby (3.1+, standard library only)
# Source:   CosmoDev polyglot showcase port of the SSH Key Generator tool,
#           ported from src/lib/ssh-keygen.ts (the canonical TypeScript
#           implementation).
# License:  display source — part of CosmoDev's polyglot tool pages.
#
# Formats produced:
# - Ed25519 public key : `ssh-ed25519 <base64(blob)> <comment>` where blob is
#   the OpenSSH wire format (string "ssh-ed25519" + string 32-byte key).
# - Ed25519 private key: `-----BEGIN OPENSSH PRIVATE KEY-----` PEM containing
#   the unencrypted "openssh-key-v1" structure (cipher "none", kdf "none").
# - RSA public key     : `ssh-rsa <base64(blob)> <comment>` where blob is
#   string "ssh-rsa" + mpint e + mpint n.
# - RSA private key    : PKCS#8 PEM (`-----BEGIN PRIVATE KEY-----`).
# - Fingerprint        : SHA-256 of the raw public key blob, base64-encoded
#   without padding, prefixed "SHA256:" (matches OpenSSH).

require 'openssl'
require 'securerandom'
require 'digest/sha2'

module SSHKeygen
  SSHKeyPair = Struct.new(:public_key, :private_key, :fingerprint,
                          keyword_init: true)

  MAGIC = 'openssh-key-v1'.dup.force_encoding('BINARY') + "\x00".b

  class << self
    # Generate an SSH key pair in OpenSSH format. options:
    #   algorithm: 'ed25519' | 'rsa-2048' | 'rsa-4096'
    #   comment:   optional key comment, conventionally `user@host`
    def generate_ssh_key_pair(options = {})
      algorithm = options[:algorithm]
      comment = options[:comment].to_s.strip
      suffix = comment.empty? ? '' : " #{comment}"

      case algorithm
      when 'ed25519' then ed25519_pair(suffix, comment)
      when 'rsa-2048', 'rsa-4096'
        rsa_pair(algorithm == 'rsa-4096' ? 4096 : 2048, suffix)
      else
        raise ArgumentError, "Unsupported algorithm: #{algorithm}"
      end
    end

    private

    # ---- binary helpers ----------------------------------------------------

    def concat(*parts)
      parts.join.b
    end

    def u32(n)
      [n].pack('N')
    end

    # SSH "string": 4-byte big-endian length + raw bytes.
    def ssh_string(data)
      data = data.b unless data.encoding == Encoding::BINARY
      u32(data.bytesize) + data
    end

    # SSH "mpint": big-endian integer, minimal, with a leading zero when the
    # high bit is set. +bytes+ is a big-endian binary String (BN#to_s(2)).
    def mpint(bytes)
      bytes = bytes.sub(/\A\x00+/o, '') || bytes # strip redundant leading zeros
      bytes = "\x00".b + bytes unless bytes.empty? || (bytes.getbyte(0) & 0x80).zero?
      ssh_string(bytes)
    end

    def to_base64(bytes)
      [bytes].pack('m0')
    end

    def pem_wrap(base64, label, width)
      lines = base64.scan(/.{1,#{width}}/)
      "-----BEGIN #{label}-----\n#{lines.join("\n")}\n-----END #{label}-----\n"
    end

    # "SHA256:<base64 digest of the public blob, unpadded>" - the OpenSSH
    # fingerprint format.
    def ssh_fingerprint(pub_blob)
      "SHA256:#{to_base64(Digest::SHA256.digest(pub_blob)).sub(/=+\z/, '')}"
    end

    # ---- Ed25519 -----------------------------------------------------------

    def ed25519_pair(suffix, comment)
      pkey = OpenSSL::PKey::Ed25519.generate
      # The SPKI wrapper is <...header...> 03 21 00 <32-byte key> and the
      # PKCS#8 wrapper is <...header...> 04 20 <32-byte seed>; the raw values
      # are always the trailing 32 bytes of either DER.
      pub = pkey.public_to_der.bytes.last(32).pack('C*')
      raise "Unexpected Ed25519 public key length: #{pub.bytesize}." if pub.bytesize != 32

      pkcs8 = pkey.private_to_der
      raise 'Unexpected Ed25519 private key encoding.' if pkcs8.bytesize < 34

      seed = pkcs8.bytes.last(32).pack('C*')
      pub_blob = ssh_string('ssh-ed25519') + ssh_string(pub)
      SSHKeyPair.new(
        public_key: "ssh-ed25519 #{to_base64(pub_blob)}#{suffix}",
        private_key: open_ssh_private_pem(pub_blob, seed + pub, comment),
        fingerprint: ssh_fingerprint(pub_blob)
      )
    end

    # ---- RSA ---------------------------------------------------------------

    def rsa_pair(bits, suffix)
      rsa = OpenSSL::PKey::RSA.generate(bits)
      pub_blob = concat(
        ssh_string('ssh-rsa'),
        mpint(rsa.e.to_s(2)), # public exponent e (big-endian)
        mpint(rsa.n.to_s(2))  # modulus n
      )
      pkcs8 = rsa.private_to_der
      SSHKeyPair.new(
        public_key: "ssh-rsa #{to_base64(pub_blob)}#{suffix}",
        private_key: pem_wrap(to_base64(pkcs8), 'PRIVATE KEY', 64),
        fingerprint: ssh_fingerprint(pub_blob)
      )
    end

    # ---- openssh-key-v1 ----------------------------------------------------

    # Build the unencrypted "openssh-key-v1" private key structure for
    # Ed25519 (cipher "none", kdf "none"), wrapped in an OPENSSH PRIVATE KEY
    # PEM.
    def open_ssh_private_pem(pub_blob, private_key_bytes, comment)
      check = SecureRandom.random_bytes(4)
      inner = concat(check, check, pub_blob, ssh_string(private_key_bytes),
                     ssh_string(comment))
      # Pad to a multiple of the block size (8 for cipher "none") with 1,2,3,...
      pad_len = (8 - (inner.bytesize % 8)) % 8
      inner = concat(inner, (1..pad_len).to_a.pack('C*'))
      outer = concat(
        MAGIC,
        ssh_string('none'),          # ciphername
        ssh_string('none'),          # kdfname
        ssh_string(''.b),            # kdfoptions
        u32(1),                      # number of keys
        ssh_string(pub_blob),
        ssh_string(inner)
      )
      pem_wrap(to_base64(outer), 'OPENSSH PRIVATE KEY', 70)
    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 →