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 →