Skip to content

SSH Key Generator — C++ 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 C++ 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: C++17 (standard library + OpenSSL EVP)
// Ported from src/lib/ssh-keygen.ts (the canonical TypeScript implementation,
// which uses the browser's Web Crypto API). display source — part of
// CosmoDev's polyglot tool pages.
//
// Keys are created in-process and never leave it — the C++ analogue of the TS
// guarantee that generation happens entirely client-side.
//
// Formats produced (identical to the TS reference):
// - 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).
//
// Build: c++ -std=c++17 ssh-keygen.cpp -lcrypto

#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

#include <openssl/bn.h>
#include <openssl/core_names.h>
#include <openssl/evp.h>
#include <openssl/rand.h>

namespace sshkeygen {

using Bytes = std::vector<uint8_t>;

enum class SSHAlgorithm { Ed25519, RSA2048, RSA4096 };

struct SSHKeyOptions {
  SSHAlgorithm algorithm = SSHAlgorithm::Ed25519;
  /** Optional key comment, conventionally `user@host`. Omitted when empty. */
  std::string comment;
};

struct SSHKeyPair {
  std::string publicKey;
  std::string privateKey;
  std::string fingerprint;
};

const Bytes MAGIC = {'o', 'p', 'e', 'n', 's', 's', 'h', '-', 'k', 'e', 'y', '-', 'v', '1', 0};

// ── small OpenSSL RAII helpers ─────────────────────────────────────────────

struct EVPMDeleter {
  void operator()(EVP_MD_CTX* ctx) const { EVP_MD_CTX_free(ctx); }
  void operator()(EVP_PKEY_CTX* ctx) const { EVP_PKEY_CTX_free(ctx); }
};
using MDContext = std::unique_ptr<EVP_MD_CTX, EVPMDeleter>;
using PKEYContext = std::unique_ptr<EVP_PKEY_CTX, EVPMDeleter>;

struct PKEYDeleter {
  void operator()(EVP_PKEY* key) const { EVP_PKEY_free(key); }
};
using PKey = std::unique_ptr<EVP_PKEY, PKEYDeleter>;

// ── byte/string helpers ────────────────────────────────────────────────────

Bytes concat(const std::vector<Bytes>& parts) {
  size_t total = 0;
  for (const auto& p : parts) total += p.size();
  Bytes out;
  out.reserve(total);
  for (const auto& p : parts) out.insert(out.end(), p.begin(), p.end());
  return out;
}

Bytes utf8(const std::string& s) { return Bytes(s.begin(), s.end()); }

/** 4-byte big-endian length. */
Bytes u32(uint32_t n) {
  return {static_cast<uint8_t>(n >> 24), static_cast<uint8_t>(n >> 16),
          static_cast<uint8_t>(n >> 8), static_cast<uint8_t>(n)};
}

/** SSH "string": 4-byte big-endian length + raw bytes. */
Bytes sshString(const Bytes& data) {
  Bytes out = u32(static_cast<uint32_t>(data.size()));
  out.insert(out.end(), data.begin(), data.end());
  return out;
}
Bytes sshString(const std::string& s) { return sshString(utf8(s)); }

/** SSH "mpint": big-endian integer, minimal, leading zero when high bit set. */
Bytes mpint(const Bytes& bytes) {
  size_t i = 0;
  while (i + 1 < bytes.size() && bytes[i] == 0) i++;
  Bytes v(bytes.begin() + i, bytes.end());
  if (!v.empty() && (v[0] & 0x80) != 0) return sshString(concat({Bytes{0}, v}));
  return sshString(v);
}

/** Standard Base64 with padding. */
std::string toBase64(const Bytes& bytes) {
  static const char* CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  std::string out;
  out.reserve((bytes.size() + 2) / 3 * 4);
  for (size_t i = 0; i < bytes.size(); i += 3) {
    const uint8_t b0 = bytes[i];
    const uint8_t b1 = i + 1 < bytes.size() ? bytes[i + 1] : 0;
    const uint8_t b2 = i + 2 < bytes.size() ? bytes[i + 2] : 0;
    out += CHARS[b0 >> 2];
    out += CHARS[((b0 & 0x03) << 4) | (b1 >> 4)];
    out += i + 1 < bytes.size() ? CHARS[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=';
    out += i + 2 < bytes.size() ? CHARS[b2 & 0x3f] : '=';
  }
  return out;
}

/** PEM-wrap a Base64 body: marker lines + fixed-width body lines. */
std::string pemWrap(const std::string& base64, const std::string& label, size_t width) {
  std::string out = "-----BEGIN " + label + "-----\n";
  for (size_t i = 0; i < base64.size(); i += width) {
    out += base64.substr(i, width);
    out += '\n';
  }
  out += "-----END " + label + "-----\n";
  return out;
}

/** SHA-256 digest via the OpenSSL EVP interface. */
Bytes sha256(const Bytes& data) {
  MDContext ctx(EVP_MD_CTX_new());
  if (!ctx || EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr) != 1 ||
      EVP_DigestUpdate(ctx.get(), data.data(), data.size()) != 1) {
    throw std::runtime_error("SHA-256 digest failed.");
  }
  Bytes digest(32);
  unsigned len = 0;
  if (EVP_DigestFinal_ex(ctx.get(), digest.data(), &len) != 1 || len != 32) {
    throw std::runtime_error("SHA-256 digest failed.");
  }
  return digest;
}

/** "SHA256:<base64 digest of the public blob, unpadded>" — OpenSSH's format. */
std::string sshFingerprint(const Bytes& pubBlob) {
  std::string b64 = toBase64(sha256(pubBlob));
  const size_t last = b64.find_last_not_of('=');
  b64.erase(last + 1);
  return "SHA256:" + b64;
}

// ── key material extraction ────────────────────────────────────────────────

/** Raw SPKI (SubjectPublicKeyInfo) DER of an EVP_PKEY. */
Bytes exportSPKI(EVP_PKEY* key) {
  int len = i2d_PUBKEY(key, nullptr);
  if (len <= 0) throw std::runtime_error("Failed to export the public key.");
  Bytes out(static_cast<size_t>(len));
  uint8_t* cursor = out.data();
  if (i2d_PUBKEY(key, &cursor) <= 0) throw std::runtime_error("Failed to export the public key.");
  return out;
}

/** Raw PKCS#8 DER of an EVP_PKEY. */
Bytes exportPKCS8(EVP_PKEY* key) {
  int len = i2d_PKCS8_PRIV_KEY_INFO(EVP_PKEY_get0_PKCS8(key), nullptr);
  if (len <= 0) throw std::runtime_error("Failed to export the private key.");
  Bytes out(static_cast<size_t>(len));
  uint8_t* cursor = out.data();
  if (i2d_PKCS8_PRIV_KEY_INFO(EVP_PKEY_get0_PKCS8(key), &cursor) <= 0) {
    throw std::runtime_error("Failed to export the private key.");
  }
  return out;
}

/** Big-endian minimal bytes of a BIGNUM. */
Bytes bnBytes(const BIGNUM* bn) {
  const int len = BN_num_bytes(bn);
  Bytes out(static_cast<size_t>(len));
  if (len > 0) BN_bn2bin(bn, out.data());
  return out;
}

struct BNDeleter {
  void operator()(BIGNUM* bn) const { BN_free(bn); }
};
using BigNum = std::unique_ptr<BIGNUM, BNDeleter>;

// ── openssh-key-v1 private key container ───────────────────────────────────

/**
 * Build the unencrypted "openssh-key-v1" private key structure for Ed25519
 * (cipher "none", kdf "none"), wrapped in an OPENSSH PRIVATE KEY PEM.
 */
std::string openSshPrivatePem(const Bytes& pubBlob, const Bytes& privateKeyBytes,
                              const std::string& comment) {
  uint32_t checkValue = 0;
  if (RAND_bytes(reinterpret_cast<uint8_t*>(&checkValue), sizeof(checkValue)) != 1) {
    throw std::runtime_error("CSPRNG failure while building the private key.");
  }
  const Bytes check = u32(checkValue);
  Bytes inner = concat({check, check, pubBlob, sshString(privateKeyBytes), sshString(comment)});
  // Pad to a multiple of the block size (8 for cipher "none") with 1,2,3,...
  const size_t padLen = (8 - (inner.size() % 8)) % 8;
  for (size_t i = 1; i <= padLen; i++) inner.push_back(static_cast<uint8_t>(i));
  const Bytes outer =
      concat({MAGIC, sshString("none"), sshString("none"), sshString(Bytes{}), u32(1),
              sshString(pubBlob), sshString(inner)});
  return pemWrap(toBase64(outer), "OPENSSH PRIVATE KEY", 70);
}

// ── generation ─────────────────────────────────────────────────────────────

/** Run EVP_PKEY_keygen for a built-in algorithm id (Ed25519) or RSA bits. */
static PKey keygen(int algorithmId, int rsaBits) {
  EVP_PKEY* params = nullptr;
  PKEYContext ctx(rsaBits == 0 ? EVP_PKEY_CTX_new_id(algorithmId, nullptr)
                               : EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr));
  if (!ctx || EVP_PKEY_keygen_init(ctx.get()) != 1) {
    throw std::runtime_error("Key generation context setup failed.");
  }
  if (rsaBits != 0 &&
      EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), rsaBits) <= 0) {
    throw std::runtime_error("RSA key size configuration failed.");
  }
  if (EVP_PKEY_keygen(ctx.get(), &params) != 1) {
    throw std::runtime_error("Key generation failed.");
  }
  return PKey(params);
}

/** Generate an SSH key pair in OpenSSH format using OpenSSL. */
SSHKeyPair generateSSHKeyPair(const SSHKeyOptions& options = SSHKeyOptions{}) {
  const std::string comment = [&] {
    const size_t first = options.comment.find_first_not_of(" \t\r\n");
    if (first == std::string::npos) return std::string{};
    const size_t last = options.comment.find_last_not_of(" \t\r\n");
    return options.comment.substr(first, last - first + 1);
  }();
  const std::string suffix = comment.empty() ? "" : " " + comment;

  if (options.algorithm == SSHAlgorithm::Ed25519) {
    const PKey kp = keygen(EVP_PKEY_ED25519, 0);

    size_t pubLen = 32;
    Bytes pub(32);
    if (EVP_PKEY_get_raw_public_key(kp.get(), pub.data(), &pubLen) != 1 || pubLen != 32) {
      throw std::runtime_error("Unexpected Ed25519 public key length: " + std::to_string(pubLen) +
                               ".");
    }
    // The OpenSSH private "key" blob is the 32-byte seed followed by the
    // 32-byte public key — exactly the concat(seed, pub) the TS reference
    // extracts from the PKCS#8 wrapper.
    size_t seedLen = 32;
    Bytes seed(32);
    if (EVP_PKEY_get_raw_private_key(kp.get(), seed.data(), &seedLen) != 1 || seedLen != 32) {
      throw std::runtime_error("Unexpected Ed25519 private key encoding.");
    }

    const Bytes pubBlob = concat({sshString("ssh-ed25519"), sshString(pub)});
    return {
        "ssh-ed25519 " + toBase64(pubBlob) + suffix,
        openSshPrivatePem(pubBlob, concat({seed, pub}), comment),
        sshFingerprint(pubBlob),
    };
  }

  if (options.algorithm == SSHAlgorithm::RSA2048 || options.algorithm == SSHAlgorithm::RSA4096) {
    const int bits = options.algorithm == SSHAlgorithm::RSA4096 ? 4096 : 2048;
    const PKey kp = keygen(EVP_PKEY_RSA, bits);

    // ssh-rsa blob: string "ssh-rsa" + mpint e + mpint n (from the RSA key).
    BigNum n, e;
    {
      BIGNUM* nRaw = nullptr;
      BIGNUM* eRaw = nullptr;
      if (EVP_PKEY_get_bn_param(kp.get(), OSSL_PKEY_PARAM_RSA_N, &nRaw) != 1 ||
          EVP_PKEY_get_bn_param(kp.get(), OSSL_PKEY_PARAM_RSA_E, &eRaw) != 1 || eRaw == nullptr) {
        BN_free(nRaw);
        BN_free(eRaw);
        throw std::runtime_error("Failed to export the RSA public key.");
      }
      n.reset(nRaw);
      e.reset(eRaw);
    }
    const Bytes pubBlob =
        concat({sshString("ssh-rsa"), mpint(bnBytes(e.get())), mpint(bnBytes(n.get()))});
    return {
        "ssh-rsa " + toBase64(pubBlob) + suffix,
        pemWrap(toBase64(exportPKCS8(kp.get())), "PRIVATE KEY", 64),
        sshFingerprint(pubBlob),
    };
  }

  throw std::runtime_error("Unsupported algorithm.");
}

} // namespace sshkeygen

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 →