If you administer a server or use Git over SSH, your key is literally the key to your digital house. Even so, most developers generated their pair once with ssh-keygen and never understood what is inside. This article closes that gap: which algorithm to choose, how a key serializes into the OpenSSH format, and how to generate valid pairs without leaving your browser.
Ed25519 vs RSA: the choice has been made
Two families dominate the SSH world:
RSA (1977) bases its security on the difficulty of factoring huge numbers. At 2048 bits it remains acceptable; at 4096, more robust but noticeably slower on every handshake. Its public keys are long and generation is computationally expensive.
Ed25519 (2011) uses elliptic-curve cryptography (Edwards curve with EdDSA signatures over Curve25519). With only 256 bits it offers security equivalent to RSA-3072, faster signatures, short public keys (68 encoded characters), and determinism that eliminates an entire class of implementation errors (no random nonce per signature — historically the source of ECDSA private key leaks).
OpenSSH has supported Ed25519 since version 6.5 (2014). Any reasonably maintained server accepts it today. The modern recommendation is simple: Ed25519 by default, RSA-4096 only when a legacy system demands RSA.
Anatomy of the public key
An authorized_keys line has three fields:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... miguel@laptop
The type (ssh-ed25519), the Base64 blob and a free-form comment. The blob hides a binary structure defined by RFC 4253/4251 — it is not a raw dump of the key:
string "ssh-ed25519" ← algorithm name
string public key ← 32 raw bytes
Each string is preceded by its length as a 32-bit big-endian integer. For RSA the order matters and is specific: first ssh-rsa, then the exponent e followed by the modulus n (that inverted order trips up intuition). Integers use the mpint format, which prepends a 0x00 byte when the most significant byte has its high bit set — the protocol's way of saying "this is positive".
You can verify this yourself: decode any public key's Base64 blob with a converter and you will see the ASCII bytes of ssh-ed25519 right at the start.
The private key: the openssh-key-v1 container
The old PEM format (-----BEGIN RSA PRIVATE KEY-----) was abandoned because it enabled hardware-accelerated brute-force attacks. Since OpenSSH 6.5, the native format is a binary container whose Base64 header begins with the magic string openssh-key-v1\0. Its structure:
string "openssh-key-v1"
string ciphername ← "none" if no passphrase
string kdfname ← "none"
string kdfoptions ← empty
uint32 number of keys ← 1
string public blob
string private section:
uint32 checkint ← random...
uint32 checkint ← ...written twice
string key type
string private fields
string comment
padding ← bytes 1,2,3... up to multiple of 8
The duplicated checkint is an elegant integrity mechanism: when generating, the client picks a random uint32 and writes it twice; when reading, it verifies both copies match. If a wrong-passphrase decryption produces garbage bytes, the check fails before attempting to parse nonsense.
The deterministic padding (incremental bytes 1, 2, 3...) guarantees the block always ends aligned to 8 bytes and also detects corruption at the end. When you encrypt with a passphrase, ciphername and kdfname move from none to aes256-ctr and bcrypt, and the entire private section gets encrypted.
Generating pairs inside the browser with WebCrypto
The interesting part: no browser exposes "serialize to OpenSSH format", but they do provide the primitives to build it. The Ed25519 flow:
const pair = await crypto.subtle.generateKey(
{ name: "Ed25519" },
true,
["sign", "verify"]
);
const rawPub = await crypto.subtle.exportKey("raw", pair.publicKey);
const pkcs8 = await crypto.subtle.exportKey("pkcs8", pair.privateKey);
A practical detail emerges from the PKCS#8 private key: the Ed25519 seed is simply the last 32 bytes of the DER — the ASN.1 structure holds it as the final OCTET STRING, so a slice(-32) extracts it without parsing anything. An Ed25519 private key is the seed: with those 32 bytes and the public key (derived from it) you can reconstruct the whole container.
For RSA the path goes through JWK:
const jwkPriv = await crypto.subtle.exportKey("jwk", pair.privateKey);
// { n, e, d, p, q, qi, ... } in base64url
With those BigInt components you reassemble the mpints of the public blob (e before n) and of the private section (n, e, d, iqmp, p, q — OpenSSH's canonical order).
The advantage of doing this in the browser: the key never exists outside your machine. It does not travel over the network, never touches a shared disk, never lands in shell history. Randomness comes from the browser's CSPRNG (crypto.getRandomValues) — the same one used for TLS.
Fingerprint and verification
The modern OpenSSH fingerprint is SHA-256 over the complete public blob, Base64-encoded without padding and prefixed with SHA256:. It is what ssh-keygen -lf displays and what you see when first connecting to a server ("key fingerprint is SHA256:..."). Comparing that hash over a trusted channel is your defense against man-in-the-middle on first connection.
Minimum key hygiene
- One key per device and context. Rotating when selling or retiring a machine should be trivial.
- Permissions
600on the private key (chmod 600 ~/.ssh/id_ed25519). OpenSSH refuses keys readable by other users. - Passphrase whenever your threat model includes laptop theft. Without one, whoever copies the file is you.
- In
authorized_keys, restrict scope where possible:from="10.0.0.*",no-port-forwarding ssh-ed25519 AAAA...limits each key's origin and capabilities.
Generate your pair right now
Our SSH Key Generator runs this entire process in your browser: Ed25519, RSA-2048 or RSA-4096, building both the authorized_keys line and the full openssh-key-v1 container byte by byte, and showing the SHA-256 fingerprint. Copy, save into ~/.ssh/ and add the public half to your server.
FAQ
Can I reuse the same key for GitHub and my VPS? Technically yes; for hygiene, keep them separate. If one gets compromised, you don't lose both accesses simultaneously.
Does my browser support Ed25519? Modern Chromium-based browsers and Firefox support it in SubtleCrypto; Safari shipped later and may fail depending on version. If your browser lacks it, the correct fallback is RSA.
Are browser-generated keys as secure as ssh-keygen's? Yes: generation uses the same OS CSPRNG and the same algorithms. The real difference is that ssh-keygen can protect the private key with an encrypted passphrase; if you need that locally, generate here and then protect it with ssh-keygen -p -f id_ed25519.
Generate your Ed25519 or RSA pair with the SSH Key Generator, 100% in your browser with the key never leaving your device.