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(), ¶ms) != 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 →