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-keygen — Ed25519 / RSA SSH key pair generation in OpenSSH format.
*
* Language: C (C11, standard library + OpenSSL 3.x libcrypto — C has no crypto
* in its standard library; libcrypto is the de-facto native choice)
* 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 — byte-identical to the TS reference:
* Ed25519 public : `ssh-ed25519 <base64(blob)> <comment>`, blob = SSH string
* "ssh-ed25519" + SSH string of the 32-byte key.
* Ed25519 private : `-----BEGIN OPENSSH PRIVATE KEY-----` PEM holding the
* unencrypted "openssh-key-v1" structure (cipher "none",
* kdf "none").
* RSA public : `ssh-rsa <base64(blob)>`, blob = string "ssh-rsa" +
* mpint e + mpint n.
* RSA private : PKCS#8 PEM (`-----BEGIN PRIVATE KEY-----`).
* Fingerprint : "SHA256:" + unpadded base64 of SHA-256 over the public
* blob (the OpenSSH fingerprint format).
*
* The private key is generated locally and never leaves the process — the TS
* reference makes the same guarantee in the browser via Web Crypto.
*
* Build: cc -std=c11 ssh-keygen.c -lcrypto
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/bn.h>
#include <openssl/core_names.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
/* --------------------------------------------------------------- constants --- */
/* "openssh-key-v1\0" — the magic that opens every OpenSSH private key blob. */
static const uint8_t OPENSSH_MAGIC[15] = {
0x6f, 0x70, 0x65, 0x6e, 0x73, 0x73, 0x68, 0x2d,
0x6b, 0x65, 0x79, 0x2d, 0x76, 0x31, 0x00
};
enum {
ED25519_KEY_LEN = 32,
/* Cipher "none" still pads the inner blob to an 8-byte boundary. */
NONE_BLOCK_SIZE = 8,
/* Line widths the TS reference wraps at, per key type. */
OPENSSH_PEM_WIDTH = 70,
PKCS8_PEM_WIDTH = 64
};
/** Mirrors the TS SSHAlgorithm union. */
typedef enum {
SSH_ED25519,
SSH_RSA_2048,
SSH_RSA_4096
} ssh_algorithm;
/** Mirrors the TS SSHKeyPair interface. All three strings are heap-owned. */
typedef struct {
char *public_key;
char *private_key;
char *fingerprint;
} ssh_keypair;
/* Error reporting mirrors the TS `throw new Error(...)` messages: every entry
* point writes one into `err` and returns false rather than aborting. */
typedef struct {
char message[160];
} ssh_err;
static bool ssh_fail(ssh_err *err, const char *msg)
{
if (err != NULL) {
snprintf(err->message, sizeof err->message, "%s", msg);
}
return false;
}
/* ------------------------------------------------------------- byte buffer --- */
/* A growable byte buffer — the C stand-in for the TS concat() helper. */
typedef struct {
uint8_t *data;
size_t len;
size_t cap;
} ssh_buf;
static void ssh_buf_free(ssh_buf *b)
{
if (b == NULL || b->data == NULL) {
return;
}
/* Private key material passes through here — wipe before release. */
OPENSSL_cleanse(b->data, b->cap);
free(b->data);
b->data = NULL;
b->len = 0;
b->cap = 0;
}
static bool ssh_buf_append(ssh_buf *b, const void *bytes, size_t n)
{
if (b->len + n > b->cap) {
size_t cap = (b->cap != 0) ? b->cap : 64;
uint8_t *grown;
while (cap < b->len + n) {
cap *= 2;
}
/* Grow by hand rather than realloc() so the old bytes can be wiped. */
grown = malloc(cap);
if (grown == NULL) {
return false;
}
if (b->data != NULL) {
memcpy(grown, b->data, b->len);
OPENSSL_cleanse(b->data, b->cap);
free(b->data);
}
b->data = grown;
b->cap = cap;
}
if (n != 0) {
memcpy(b->data + b->len, bytes, n);
}
b->len += n;
return true;
}
/* ------------------------------------------------------- ssh wire encoding --- */
/* 4-byte big-endian length prefix — the TS u32(). */
static bool ssh_put_u32(ssh_buf *b, uint32_t n)
{
uint8_t be[4] = {
(uint8_t) (n >> 24), (uint8_t) (n >> 16),
(uint8_t) (n >> 8), (uint8_t) n
};
return ssh_buf_append(b, be, sizeof be);
}
/* SSH "string": 4-byte big-endian length + raw bytes. */
static bool ssh_put_string(ssh_buf *b, const void *bytes, size_t n)
{
return ssh_put_u32(b, (uint32_t) n) && ssh_buf_append(b, bytes, n);
}
static bool ssh_put_cstr(ssh_buf *b, const char *s)
{
return ssh_put_string(b, s, strlen(s));
}
/*
* SSH "mpint": big-endian integer, minimal-length, with a leading zero byte
* when the high bit is set (so it never reads as negative).
*/
static bool ssh_put_mpint(ssh_buf *b, const uint8_t *bytes, size_t n)
{
size_t i = 0;
static const uint8_t zero = 0;
while (i + 1 < n && bytes[i] == 0) {
i++; /* strip leading zeros, keeping at least one byte */
}
bytes += i;
n -= i;
if (n > 0 && (bytes[0] & 0x80) != 0) {
return ssh_put_u32(b, (uint32_t) (n + 1)) &&
ssh_buf_append(b, &zero, 1) &&
ssh_buf_append(b, bytes, n);
}
return ssh_put_string(b, bytes, n);
}
/* ------------------------------------------------------------------ base64 --- */
static const char B64[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/* Standard padded base64. Returns a heap string, or NULL on allocation failure. */
static char *ssh_base64(const uint8_t *in, size_t n)
{
size_t out_len = 4 * ((n + 2) / 3);
char *out = malloc(out_len + 1);
size_t o = 0;
size_t i;
if (out == NULL) {
return NULL;
}
for (i = 0; i + 2 < n; i += 3) {
uint32_t v = ((uint32_t) in[i] << 16) | ((uint32_t) in[i + 1] << 8) | in[i + 2];
out[o++] = B64[(v >> 18) & 0x3F];
out[o++] = B64[(v >> 12) & 0x3F];
out[o++] = B64[(v >> 6) & 0x3F];
out[o++] = B64[v & 0x3F];
}
if (i < n) {
uint32_t v = (uint32_t) in[i] << 16;
bool two = (i + 1 < n);
if (two) {
v |= (uint32_t) in[i + 1] << 8;
}
out[o++] = B64[(v >> 18) & 0x3F];
out[o++] = B64[(v >> 12) & 0x3F];
out[o++] = two ? B64[(v >> 6) & 0x3F] : '=';
out[o++] = '=';
}
out[o] = '\0';
return out;
}
/* Wrap base64 at `width` columns between BEGIN/END lines — the TS pemWrap(). */
static char *ssh_pem_wrap(const char *base64, const char *label, size_t width)
{
size_t b64_len = strlen(base64);
size_t lines = (b64_len + width - 1) / width;
/* "-----BEGIN " + label + "-----\n" ... "-----END " + label + "-----\n" */
size_t size = b64_len + lines + 2 * strlen(label) + 64;
char *out = malloc(size);
size_t o, i;
if (out == NULL) {
return NULL;
}
o = (size_t) snprintf(out, size, "-----BEGIN %s-----\n", label);
for (i = 0; i < b64_len; i += width) {
size_t n = (b64_len - i < width) ? b64_len - i : width;
memcpy(out + o, base64 + i, n);
o += n;
out[o++] = '\n';
}
snprintf(out + o, size - o, "-----END %s-----\n", label);
return out;
}
/* "SHA256:<unpadded base64 of SHA-256(pubBlob)>" — the OpenSSH fingerprint. */
static char *ssh_fingerprint(const uint8_t *pub_blob, size_t n)
{
uint8_t digest[SHA256_DIGEST_LENGTH];
char *b64;
char *out;
size_t len;
SHA256(pub_blob, n, digest);
b64 = ssh_base64(digest, sizeof digest);
if (b64 == NULL) {
return NULL;
}
len = strlen(b64);
while (len > 0 && b64[len - 1] == '=') {
b64[--len] = '\0'; /* OpenSSH prints the digest unpadded */
}
len += sizeof "SHA256:";
out = malloc(len);
if (out != NULL) {
snprintf(out, len, "SHA256:%s", b64);
}
free(b64);
return out;
}
/* ------------------------------------------------------- openssh-key-v1 pem --- */
/*
* Build the unencrypted "openssh-key-v1" private key structure for Ed25519
* (cipher "none", kdf "none") and wrap it in an OPENSSH PRIVATE KEY PEM.
* `private_key_bytes` is the 64-byte seed||public concatenation OpenSSH stores.
*/
static char *openssh_private_pem(const uint8_t *pub_blob, size_t pub_len,
const uint8_t *private_key_bytes, size_t priv_len,
const char *comment)
{
ssh_buf inner = {0};
ssh_buf outer = {0};
uint8_t check[4];
uint8_t pad;
char *b64 = NULL;
char *pem = NULL;
bool ok;
/* Two identical random words: a decryptor checks they match to confirm the
* passphrase. With cipher "none" they are decorative but still required. */
if (RAND_bytes(check, sizeof check) != 1) {
return NULL;
}
ok = ssh_buf_append(&inner, check, sizeof check) &&
ssh_buf_append(&inner, check, sizeof check) &&
ssh_buf_append(&inner, pub_blob, pub_len) &&
ssh_put_string(&inner, private_key_bytes, priv_len) &&
ssh_put_cstr(&inner, comment);
/* Pad to the cipher block size with the bytes 1, 2, 3, ... */
for (pad = 1; ok && (inner.len % NONE_BLOCK_SIZE) != 0; pad++) {
ok = ssh_buf_append(&inner, &pad, 1);
}
if (!ok) {
goto done;
}
ok = ssh_buf_append(&outer, OPENSSH_MAGIC, sizeof OPENSSH_MAGIC) &&
ssh_put_cstr(&outer, "none") && /* ciphername */
ssh_put_cstr(&outer, "none") && /* kdfname */
ssh_put_string(&outer, "", 0) && /* kdfoptions */
ssh_put_u32(&outer, 1) && /* number of keys */
ssh_put_string(&outer, pub_blob, pub_len) &&
ssh_put_string(&outer, inner.data, inner.len);
if (!ok) {
goto done;
}
b64 = ssh_base64(outer.data, outer.len);
if (b64 != NULL) {
pem = ssh_pem_wrap(b64, "OPENSSH PRIVATE KEY", OPENSSH_PEM_WIDTH);
OPENSSL_cleanse(b64, strlen(b64));
free(b64);
}
done:
ssh_buf_free(&inner);
ssh_buf_free(&outer);
return pem;
}
/* --------------------------------------------------------------- key types --- */
/* `ssh-ed25519 <base64> [comment]` + the matching OPENSSH PRIVATE KEY PEM. */
static bool generate_ed25519(const char *comment, ssh_keypair *out, ssh_err *err)
{
EVP_PKEY *pkey = EVP_PKEY_Q_keygen(NULL, NULL, "ED25519");
uint8_t pub[ED25519_KEY_LEN];
uint8_t seed[ED25519_KEY_LEN];
uint8_t priv_bytes[2 * ED25519_KEY_LEN];
size_t pub_len = sizeof pub;
size_t seed_len = sizeof seed;
ssh_buf pub_blob = {0};
char *b64 = NULL;
bool ok = false;
if (pkey == NULL) {
return ssh_fail(err, "Failed to generate the Ed25519 key pair.");
}
if (EVP_PKEY_get_raw_public_key(pkey, pub, &pub_len) != 1 ||
pub_len != ED25519_KEY_LEN) {
ssh_fail(err, "Unexpected Ed25519 public key length.");
goto done;
}
/* OpenSSL's "raw private key" is the 32-byte seed — the same value the TS
* reference slices off the tail of the PKCS#8 encoding. */
if (EVP_PKEY_get_raw_private_key(pkey, seed, &seed_len) != 1 ||
seed_len != ED25519_KEY_LEN) {
ssh_fail(err, "Unexpected Ed25519 private key encoding.");
goto done;
}
if (!ssh_put_cstr(&pub_blob, "ssh-ed25519") ||
!ssh_put_string(&pub_blob, pub, pub_len)) {
ssh_fail(err, "Out of memory.");
goto done;
}
b64 = ssh_base64(pub_blob.data, pub_blob.len);
if (b64 == NULL) {
ssh_fail(err, "Out of memory.");
goto done;
}
out->public_key = malloc(strlen(b64) + strlen(comment) + sizeof "ssh-ed25519 ");
if (out->public_key == NULL) {
ssh_fail(err, "Out of memory.");
goto done;
}
/* The comment is appended only when non-empty, as in the TS `suffix`. */
sprintf(out->public_key, "ssh-ed25519 %s%s%s",
b64, (comment[0] != '\0') ? " " : "", comment);
/* OpenSSH stores seed || public as the Ed25519 "private key" string. */
memcpy(priv_bytes, seed, ED25519_KEY_LEN);
memcpy(priv_bytes + ED25519_KEY_LEN, pub, ED25519_KEY_LEN);
out->private_key = openssh_private_pem(pub_blob.data, pub_blob.len,
priv_bytes, sizeof priv_bytes, comment);
out->fingerprint = ssh_fingerprint(pub_blob.data, pub_blob.len);
OPENSSL_cleanse(priv_bytes, sizeof priv_bytes);
OPENSSL_cleanse(seed, sizeof seed);
if (out->private_key == NULL || out->fingerprint == NULL) {
ssh_fail(err, "Failed to encode the Ed25519 private key.");
goto done;
}
ok = true;
done:
free(b64);
ssh_buf_free(&pub_blob);
EVP_PKEY_free(pkey);
return ok;
}
/* `ssh-rsa <base64>` + a PKCS#8 PRIVATE KEY PEM, for RSA-2048 / RSA-4096. */
static bool generate_rsa(unsigned bits, const char *comment, ssh_keypair *out, ssh_err *err)
{
EVP_PKEY *pkey = EVP_PKEY_Q_keygen(NULL, NULL, "RSA", (size_t) bits);
BIGNUM *n = NULL;
BIGNUM *e = NULL;
uint8_t *n_bytes = NULL;
uint8_t *e_bytes = NULL;
int n_len, e_len;
ssh_buf pub_blob = {0};
char *b64 = NULL;
BIO *bio = NULL;
bool ok = false;
if (pkey == NULL) {
return ssh_fail(err, "Failed to generate the RSA key pair.");
}
if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_N, &n) != 1 ||
EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_E, &e) != 1) {
ssh_fail(err, "Failed to export the RSA public key.");
goto done;
}
n_len = BN_num_bytes(n);
e_len = BN_num_bytes(e);
n_bytes = malloc((size_t) n_len);
e_bytes = malloc((size_t) e_len);
if (n_bytes == NULL || e_bytes == NULL) {
ssh_fail(err, "Out of memory.");
goto done;
}
BN_bn2bin(n, n_bytes);
BN_bn2bin(e, e_bytes);
/* Wire order is exponent first, then modulus. */
if (!ssh_put_cstr(&pub_blob, "ssh-rsa") ||
!ssh_put_mpint(&pub_blob, e_bytes, (size_t) e_len) ||
!ssh_put_mpint(&pub_blob, n_bytes, (size_t) n_len)) {
ssh_fail(err, "Out of memory.");
goto done;
}
b64 = ssh_base64(pub_blob.data, pub_blob.len);
if (b64 == NULL) {
ssh_fail(err, "Out of memory.");
goto done;
}
out->public_key = malloc(strlen(b64) + strlen(comment) + sizeof "ssh-rsa ");
if (out->public_key == NULL) {
ssh_fail(err, "Out of memory.");
goto done;
}
sprintf(out->public_key, "ssh-rsa %s%s%s",
b64, (comment[0] != '\0') ? " " : "", comment);
/* PKCS#8, unencrypted — PEM_write_bio_PrivateKey emits the same
* `-----BEGIN PRIVATE KEY-----` block the TS reference builds by hand. */
bio = BIO_new(BIO_s_mem());
if (bio == NULL ||
PEM_write_bio_PrivateKey(bio, pkey, NULL, NULL, 0, NULL, NULL) != 1) {
ssh_fail(err, "Failed to encode the RSA private key.");
goto done;
}
{
char *pem_data = NULL;
long pem_len = BIO_get_mem_data(bio, &pem_data);
out->private_key = malloc((size_t) pem_len + 1);
if (out->private_key == NULL) {
ssh_fail(err, "Out of memory.");
goto done;
}
memcpy(out->private_key, pem_data, (size_t) pem_len);
out->private_key[pem_len] = '\0';
}
out->fingerprint = ssh_fingerprint(pub_blob.data, pub_blob.len);
if (out->fingerprint == NULL) {
ssh_fail(err, "Out of memory.");
goto done;
}
ok = true;
done:
free(b64);
free(n_bytes);
free(e_bytes);
BN_free(n);
BN_free(e);
BIO_free(bio);
ssh_buf_free(&pub_blob);
EVP_PKEY_free(pkey);
return ok;
}
/* --------------------------------------------------------------- public api --- */
void ssh_keypair_free(ssh_keypair *kp)
{
if (kp == NULL) {
return;
}
if (kp->private_key != NULL) {
OPENSSL_cleanse(kp->private_key, strlen(kp->private_key));
}
free(kp->public_key);
free(kp->private_key);
free(kp->fingerprint);
kp->public_key = NULL;
kp->private_key = NULL;
kp->fingerprint = NULL;
}
/*
* Generate an SSH key pair in OpenSSH format — the entry point matching the TS
* generateSSHKeyPair(). `comment` may be NULL; surrounding whitespace is
* trimmed and an empty comment is omitted from the public key line.
*/
bool generate_ssh_keypair(ssh_algorithm algorithm, const char *comment,
ssh_keypair *out, ssh_err *err)
{
char trimmed[256];
size_t start = 0;
size_t end;
if (out == NULL) {
return ssh_fail(err, "Output parameter must not be NULL.");
}
memset(out, 0, sizeof *out);
/* comment?.trim() ?? '' */
snprintf(trimmed, sizeof trimmed, "%s", (comment != NULL) ? comment : "");
end = strlen(trimmed);
while (start < end && (unsigned char) trimmed[start] <= ' ') {
start++;
}
while (end > start && (unsigned char) trimmed[end - 1] <= ' ') {
end--;
}
trimmed[end] = '\0';
memmove(trimmed, trimmed + start, end - start + 1);
switch (algorithm) {
case SSH_ED25519:
return generate_ed25519(trimmed, out, err);
case SSH_RSA_2048:
return generate_rsa(2048, trimmed, out, err);
case SSH_RSA_4096:
return generate_rsa(4096, trimmed, out, err);
default:
return ssh_fail(err, "Unsupported algorithm.");
}
}
/* -------------------------------------------------------------------- demo --- */
int main(void)
{
ssh_keypair kp;
ssh_err err = {0};
if (!generate_ssh_keypair(SSH_ED25519, " user@host ", &kp, &err)) {
fprintf(stderr, "ssh-keygen: %s\n", err.message);
return 1;
}
printf("%s\n\n%s\n%s\n", kp.public_key, kp.private_key, kp.fingerprint);
ssh_keypair_free(&kp);
if (!generate_ssh_keypair(SSH_RSA_2048, "", &kp, &err)) {
fprintf(stderr, "ssh-keygen: %s\n", err.message);
return 1;
}
printf("\n%s\n%s\n", kp.public_key, kp.fingerprint);
ssh_keypair_free(&kp);
return 0;
}
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 →