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-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 →