SSH Key Generator — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! ssh-keygen — OpenSSH key pair generation (Ed25519).
//!
//! Language: Zig 0.14 (standard library only)
//! Ported from: src/lib/ssh-keygen.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool 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").
//! - Fingerprint : SHA-256 of the raw public key blob, base64-encoded
//! without padding, prefixed "SHA256:" (matches OpenSSH).
//!
//! The TS reference also generates RSA-2048/4096 keys through the Web Crypto
//! API. Zig's standard library ships Ed25519 but no RSA key generator, so this
//! port implements the Ed25519 path natively and reports the RSA options as
//! unsupported (an RSA port would need a third-party crypto library).
const std = @import("std");
const Ed25519 = std.crypto.sign.Ed25519;
const Sha256 = std.crypto.hash.sha2.Sha256;
const B64Encoder = std.base64.standard.Encoder;
pub const SSHAlgorithm = enum {
ed25519,
rsa_2048,
rsa_4096,
};
pub const SSHKeyOptions = struct {
algorithm: SSHAlgorithm = .ed25519,
/// Optional key comment, conventionally `user@host`. Omitted when empty.
comment: []const u8 = "",
};
pub const SSHKeyPair = struct {
public_key: []u8,
private_key: []u8,
fingerprint: []u8,
};
const magic = "openssh-key-v1\x00"; // "openssh-key-v1\0"
pub const Error = error{
UnsupportedAlgorithm,
UnexpectedKeyEncoding,
OutOfMemory,
};
/// SSH "string": 4-byte big-endian length + raw bytes, appended to `list`.
fn sshString(list: *std.ArrayList(u8), data: []const u8) !void {
var len_buf: [4]u8 = undefined;
std.mem.writeInt(u32, &len_buf, @intCast(data.len), .big);
try list.appendSlice(&len_buf);
try list.appendSlice(data);
}
fn u32be(list: *std.ArrayList(u8), n: u32) !void {
var buf: [4]u8 = undefined;
std.mem.writeInt(u32, &buf, n, .big);
try list.appendSlice(&buf);
}
/// Base64-encode into a fresh caller-owned buffer.
fn toBase64(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
const out = try allocator.alloc(u8, B64Encoder.calcSize(bytes.len));
_ = B64Encoder.encode(out, bytes);
return out;
}
/// Wrap `b64` in a `-----BEGIN/END <label>-----` PEM with fixed line width.
fn pemWrap(allocator: std.mem.Allocator, b64: []const u8, label: []const u8, width: usize) ![]u8 {
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
try out.writer().print("-----BEGIN {s}-----\n", .{label});
var i: usize = 0;
while (i < b64.len) : (i += width) {
const end = @min(i + width, b64.len);
try out.appendSlice(b64[i..end]);
try out.append('\n');
}
try out.writer().print("-----END {s}-----\n", .{label});
return out.toOwnedSlice();
}
/// "SHA256:<base64 digest of the public blob, unpadded>" - the OpenSSH
/// fingerprint format. Caller owns the returned string.
fn sshFingerprint(allocator: std.mem.Allocator, pub_blob: []const u8) ![]u8 {
var digest: [Sha256.digest_length]u8 = undefined;
Sha256.hash(pub_blob, &digest, .{});
const b64 = try toBase64(allocator, &digest);
defer allocator.free(b64);
var unpadded_len: usize = b64.len;
while (unpadded_len > 0 and b64[unpadded_len - 1] == '=') unpadded_len -= 1;
return std.fmt.allocPrint(allocator, "SHA256:{s}", .{b64[0..unpadded_len]});
}
/// Build the unencrypted "openssh-key-v1" private key structure for Ed25519
/// (cipher "none", kdf "none"), wrapped in an OPENSSH PRIVATE KEY PEM.
fn openSshPrivatePem(
allocator: std.mem.Allocator,
pub_blob: []const u8,
private_key_bytes: []const u8,
comment: []const u8,
) ![]u8 {
var outer = std.ArrayList(u8).init(allocator);
defer outer.deinit();
// Two random u32 check values, the public blob, then the private key and
// comment as SSH strings; padded to a multiple of 8 with 1,2,3,...
var check: u32 = undefined;
std.crypto.random.bytes(std.mem.asBytes(&check));
try u32be(&outer, check);
try u32be(&outer, check);
try sshString(&outer, pub_blob);
try sshString(&outer, private_key_bytes);
try sshString(&outer, comment);
// Pad to a multiple of the block size (8 for cipher "none") with 1,2,3,...
const pad_len = (8 - (outer.items.len % 8)) % 8;
for (1..pad_len + 1) |i| try outer.append(@intCast(i));
var inner = std.ArrayList(u8).init(allocator);
defer inner.deinit();
try inner.appendSlice(magic);
try sshString(&inner, "none"); // ciphername
try sshString(&inner, "none"); // kdfname
try sshString(&inner, ""); // kdfoptions
try u32be(&inner, 1); // number of keys
try sshString(&inner, pub_blob);
try sshString(&inner, outer.items);
const b64 = try toBase64(allocator, inner.items);
defer allocator.free(b64);
return pemWrap(allocator, b64, "OPENSSH PRIVATE KEY", 70);
}
/// Generate an SSH key pair in OpenSSH format. Caller owns every field.
pub fn generateSSHKeyPair(
allocator: std.mem.Allocator,
options: SSHKeyOptions,
) (Error || std.mem.Allocator.Error)!SSHKeyPair {
const comment = std.mem.trim(u8, options.comment, " \t\r\n");
const suffix: []const u8 = if (comment.len > 0) " " else "";
switch (options.algorithm) {
.ed25519 => {
const kp = Ed25519.KeyPair.generate();
const pub = kp.public_key.toBytes(); // 32 raw bytes
var blob = std.ArrayList(u8).init(allocator);
defer blob.deinit();
try sshString(&blob, "ssh-ed25519");
try sshString(&blob, &pub);
// Private half: the 64-byte expanded key = seed || public key
// (Zig derives the full secret; the OpenSSH format stores
// seed || public, which is the same 64 bytes).
const expanded = kp.secret_key.toBytes();
const pub_b64 = try toBase64(allocator, blob.items);
const public_key = try std.fmt.allocPrint(
allocator,
"ssh-ed25519 {s}{s}{s}",
.{ pub_b64, suffix, comment },
);
allocator.free(pub_b64);
return .{
.public_key = public_key,
.private_key = try openSshPrivatePem(allocator, blob.items, &expanded, comment),
.fingerprint = try sshFingerprint(allocator, blob.items),
};
},
// Zig's std.crypto has Ed25519 but no RSA key generator; the RSA
// variants would require a third-party crypto library.
.rsa_2048, .rsa_4096 => return Error.UnsupportedAlgorithm,
}
}
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 →