sshed25519securityweb crypto

SSH keys explained: Ed25519 vs RSA and the OpenSSH format under the hood

How SSH keys work: why Ed25519 is the recommended choice, how to generate pairs in the browser with WebCrypto and what the openssh-key-v1 format contains.

August 23, 2026·8 min read

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 600 on 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.

Try it without code

SSH Key Generator

Ed25519/RSA in your browser.

Open SSH Key Generator

Built by

Miguel Ángel Colorado Marin (MACM)

Full-Stack Developer · Guadalajara, España

I develop web apps, digital tools and full projects — from design to deployment.

Contact me