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.
// C# 12 / .NET 8 — ported from src/lib/ssh-keygen.ts (the canonical
// TypeScript implementation). Display source for CosmoDev's polyglot 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).
//
// .NET note: the RSA path runs fully on the standard library. .NET 8 ships no
// Ed25519, so BuildEd25519KeyPair takes the raw 32-byte public key + 32-byte
// seed from an Ed25519 library (NSec, BouncyCastle) and assembles the exact
// same OpenSSH wire formats around them.

using System.Security.Cryptography;
using System.Text;

/// <summary>The three key algorithms offered in the UI.</summary>
public enum SshAlgorithm
{
    Ed25519,
    Rsa2048,
    Rsa4096,
}

/// <summary>Key generation options.</summary>
/// <param name="Algorithm">Which key algorithm to generate.</param>
/// <param name="Comment">Optional key comment, conventionally user@host. Omitted when empty.</param>
public sealed record SshKeyOptions(SshAlgorithm Algorithm, string? Comment = null);

/// <summary>A generated key pair in OpenSSH formats.</summary>
/// <param name="PublicKey">The one-line authorized_keys entry.</param>
/// <param name="PrivateKey">The PEM private key block.</param>
/// <param name="Fingerprint">SHA256:<base64> public-key fingerprint, OpenSSH-style.</param>
public sealed record SshKeyPair(string PublicKey, string PrivateKey, string Fingerprint);

public static class SshKeygen
{
    private static ReadOnlySpan<byte> Magic =>
        "openssh-key-v1\0"u8; // the format's leading magic + NUL

    private static byte[] Concat(params ReadOnlySpan<byte>[] parts)
    {
        var output = new byte[parts.Sum(p => p.Length)];
        var offset = 0;
        foreach (var p in parts)
        {
            p.CopyTo(output.AsSpan(offset));
            offset += p.Length;
        }
        return output;
    }

    private static byte[] U32(uint n)
    {
        var b = new byte[4];
        BitConverter.GetBytes(n).CopyTo(b, 0);
        if (BitConverter.IsLittleEndian) Array.Reverse(b);
        return b;
    }

    /// <summary>SSH "string": 4-byte big-endian length + raw bytes.</summary>
    private static byte[] SshString(ReadOnlySpan<byte> data) =>
        Concat(U32((uint)data.Length), data);

    private static byte[] SshString(string text) => SshString(Encoding.UTF8.GetBytes(text));

    /// <summary>SSH "mpint": big-endian integer, minimal, with a leading zero when the high bit is set.</summary>
    private static byte[] Mpint(byte[] bytes)
    {
        var i = 0;
        while (i < bytes.Length - 1 && bytes[i] == 0) i++;
        var v = bytes[i..];
        return v.Length > 0 && (v[0] & 0x80) != 0
            ? SshString(new byte[] { 0 }.Concat(v).ToArray())
            : SshString(v);
    }

    /// <summary>Wrap base64 in BEGIN/END markers, <paramref name="width"/> characters per line.</summary>
    private static string PemWrap(byte[] der, string label, int width)
    {
        var base64 = Convert.ToBase64String(der);
        var lines = Enumerable.Range(0, (base64.Length + width - 1) / width)
            .Select(i => base64.Substring(i * width, Math.Min(width, base64.Length - i * width)));
        return $"-----BEGIN {label}-----\n{string.Join('\n', lines)}\n-----END {label}-----\n";
    }

    /// <summary>"SHA256:<base64 digest of the public blob, unpadded>" — the OpenSSH fingerprint format.</summary>
    private static string SshFingerprint(byte[] pubBlob) =>
        $"SHA256:{Convert.ToBase64String(SHA256.HashData(pubBlob)).TrimEnd('=')}";

    /// <summary>
    /// Build the unencrypted "openssh-key-v1" private key structure for
    /// Ed25519 (cipher "none", kdf "none"), wrapped in an OPENSSH PRIVATE KEY PEM.
    /// </summary>
    private static string OpenSshPrivatePem(byte[] pubBlob, byte[] privateKeyBytes, string comment)
    {
        var check = U32(BitConverter.ToUInt32(RandomNumberGenerator.GetBytes(4)));
        Span<byte> pad = stackalloc byte[(8 - (8 + 4 + 4 + pubBlob.Length +
            4 + privateKeyBytes.Length + 4 + Encoding.UTF8.GetByteCount(comment)) % 8) % 8];
        for (var i = 0; i < pad.Length; i++) pad[i] = (byte)(i + 1); // 1,2,3,... filler
        var inner = Concat(check, check, pubBlob, SshString(privateKeyBytes), SshString(comment), pad);
        var outer = Concat(
            Magic.ToArray(),
            SshString("none"),          // ciphername
            SshString("none"),          // kdfname
            SshString(Array.Empty<byte>()), // kdfoptions
            U32(1),                     // number of keys
            SshString(pubBlob),
            SshString(inner));
        return PemWrap(outer, "OPENSSH PRIVATE KEY", 70);
    }

    /// <summary>
    /// Ed25519 assembly: takes the raw 32-byte public key and 32-byte seed
    /// from any Ed25519 implementation and produces the OpenSSH key pair.
    /// The private "key" blob carries seed || public (64 bytes), per OpenSSH.
    /// </summary>
    public static SshKeyPair BuildEd25519KeyPair(byte[] publicKey, byte[] seed, string? comment = null)
    {
        if (publicKey.Length != 32) throw new ArgumentException("Ed25519 public key must be 32 bytes.");
        if (seed.Length != 32) throw new ArgumentException("Ed25519 seed must be 32 bytes.");
        var trimmed = (comment ?? "").Trim();
        var suffix = trimmed.Length > 0 ? $" {trimmed}" : "";
        var pubBlob = Concat(SshString("ssh-ed25519"), SshString(publicKey));
        return new SshKeyPair(
            $"ssh-ed25519 {Convert.ToBase64String(pubBlob)}{suffix}",
            OpenSshPrivatePem(pubBlob, Concat(seed, publicKey), trimmed),
            SshFingerprint(pubBlob));
    }

    /// <summary>Generate an RSA SSH key pair in OpenSSH format.</summary>
    public static SshKeyPair GenerateRsaKeyPair(int modulusBits, string? comment = null)
    {
        using var rsa = RSA.Create(modulusBits);
        var p = rsa.ExportParameters(false);
        if (p.Modulus is null || p.Exponent is null)
        {
            throw new InvalidOperationException("Failed to export the RSA public key.");
        }
        var trimmed = (comment ?? "").Trim();
        var suffix = trimmed.Length > 0 ? $" {trimmed}" : "";
        var pubBlob = Concat(SshString("ssh-rsa"), Mpint(p.Exponent), Mpint(p.Modulus));
        return new SshKeyPair(
            $"ssh-rsa {Convert.ToBase64String(pubBlob)}{suffix}",
            rsa.ExportPkcs8PrivateKeyPem().Replace("\r\n", "\n"),
            SshFingerprint(pubBlob));
    }

    /// <summary>Generate an SSH key pair in OpenSSH format.</summary>
    public static SshKeyPair Generate(SshKeyOptions options)
    {
        return options.Algorithm switch
        {
            // Feed a library's Ed25519 key material through the OpenSSH assembler.
            SshAlgorithm.Ed25519 => throw new NotSupportedException(
                "Use BuildEd25519KeyPair with an Ed25519 library - .NET 8 has no built-in Ed25519."),
            SshAlgorithm.Rsa2048 => GenerateRsaKeyPair(2048, options.Comment),
            SshAlgorithm.Rsa4096 => GenerateRsaKeyPair(4096, options.Comment),
            _ => throw new NotSupportedException($"Unsupported algorithm: {options.Algorithm}"),
        };
    }
}

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 →