Skip to content

SSH Key Formats and Encoding

The confusion around SSH keys usually comes from mixing up three independent layers:

  1. Encoding — how binary data is serialised (DER, PEM/Base64)
  2. Schema — what ASN.1 structure wraps the key material (PKCS#1, PKCS#8, PKCS#12)
  3. Wire format — the application-level framing (OpenSSH, RFC 4716 / SSH2, PuTTY PPK)

These layers combine freely, which is why the same RSA key can appear in half a dozen superficially different files.

Encoding layer

DER

Distinguished Encoding Rules — a binary serialisation of ASN.1 structures. All PEM files are DER underneath; DER files are not human-readable.

PEM

Privacy-Enhanced Mail — DER bytes encoded as Base64, wrapped in -----BEGIN <label>----- / -----END <label>----- markers. The label identifies the schema, not the encoding:

-----BEGIN RSA PUBLIC KEY-----   ← PKCS#1, RSA-specific
-----BEGIN PUBLIC KEY-----       ← PKCS#8 SubjectPublicKeyInfo, algorithm-agnostic
-----BEGIN CERTIFICATE-----      ← X.509 certificate
-----BEGIN PRIVATE KEY-----      ← PKCS#8 unencrypted private key
-----BEGIN ENCRYPTED PRIVATE KEY----- ← PKCS#8 encrypted private key
-----BEGIN RSA PRIVATE KEY-----  ← PKCS#1 private key (legacy OpenSSL "traditional" format)

The five-dash delimiter (-----) is mandatory. The RFC 4716 SSH2 format uses four dashes (---- BEGIN SSH2 PUBLIC KEY ----) and is not PEM — they are not interchangeable.

Schema layer

PKCS#1 — RSA-specific

Defined in RFC 3447. Carries only RSA keys.

RSAPublicKey ::= SEQUENCE {
    modulus           INTEGER,  -- n
    publicExponent    INTEGER   -- e
}

PEM marker: -----BEGIN RSA PUBLIC KEY-----

PKCS#8 — algorithm-agnostic

Defined in RFC 5958. Wraps any key type by prepending an AlgorithmIdentifier (OID + optional params) before the key bytes. This is the modern default for private keys and the format openssl rsa -pubout produces for public keys.

SubjectPublicKeyInfo ::= SEQUENCE {
    algorithm   AlgorithmIdentifier,
    subjectPublicKeyBit BIT STRING   -- DER-encoded RSAPublicKey inside
}

PEM marker: -----BEGIN PUBLIC KEY-----

The extra wrapper is why SubjectPublicKeyInfo is 24 bytes longer than the equivalent PKCS#1 blob for the same RSA key.

PKCS#12 — certificate + key bundle

A binary container (.p12 / .pfx) that bundles a certificate chain and its private key, typically password-protected. Not used for bare public keys; common for importing into browsers and Windows certificate stores.

DER vs PEM — same schema, different encoding

openssl rsa -pubin -in key.pub -outform DER -out key.pub.der   # PEM → DER
openssl rsa -pubin -in key.pub.der -inform DER -out key.pub    # DER → PEM

OpenSSH wire format

OpenSSH uses its own framing, defined in RFC 4253 §6.6 and RFC 4251 §5.

A public key line in ~/.ssh/authorized_keys or id_rsa.pub:

ssh-rsa <base64-blob> optional-comment

The Base64 blob is not DER/ASN.1. It is a sequence of length-prefixed fields (4-byte big-endian length, then data):

[00 00 00 07] [73 73 68 2d 72 73 61]   ← length=7, "ssh-rsa"
[00 00 00 03] [01 00 01]               ← length=3, exponent e = 65537
[00 00 01 01] [00 fb ...]              ← length=257, modulus n (leading 0x00 if MSB set)

The leading 0x00 on the modulus is not padding — it signals that the integer is positive (same convention as DER, but applied manually here).

RFC 4716 / SSH2 format

A different public key container used by some tools (notably PuTTYgen "Export OpenSSH key"):

---- BEGIN SSH2 PUBLIC KEY ----
Comment: "rsa-key-20240101"
AAAA...base64...
---- END SSH2 PUBLIC KEY ----

Four dashes, space before/after the label. The Base64 payload is the same OpenSSH blob as above. ssh-keygen -i -m RFC4716 converts it to the one-liner format.

PuTTY PPK format (Windows)

PuTTY stores keys in its own .ppk text format (v2 or v3). It is not PEM and not OpenSSH. This is the root cause of the Windows ↔ Linux friction.

PuTTY-User-Key-File-3: ssh-rsa
Encryption: none
Comment: rsa-key-20240101
Public-Lines: 6
AAAA...
Private-Lines: 14
AAAA...
Private-MAC: ...

Format map

flowchart TD
    A[RSA key material\nn, e] --> B[PKCS#1 DER\nRSAPublicKey]
    A --> C[PKCS#8 DER\nSubjectPublicKeyInfo]
    B --> D["PEM\n-----BEGIN RSA PUBLIC KEY-----"]
    C --> E["PEM\n-----BEGIN PUBLIC KEY-----"]
    A --> F[OpenSSH blob\nlength-prefixed fields]
    F --> G["one-liner\nssh-rsa AAAA... comment"]
    F --> H["RFC 4716\n---- BEGIN SSH2 PUBLIC KEY ----"]
    F --> I[PuTTY PPK\nPuTTY-User-Key-File-3]

Conversion cheat sheet

All conversions use ssh-keygen (OpenSSH ≥ 5.6) or openssl.

OpenSSH → PKCS#8 PEM (Linux → most tools)

ssh-keygen -f id_rsa.pub -e -m pkcs8
# -----BEGIN PUBLIC KEY-----

OpenSSH → PKCS#1 PEM

ssh-keygen -f id_rsa.pub -e -m pem
# -----BEGIN RSA PUBLIC KEY-----

PKCS#8 PEM → OpenSSH

ssh-keygen -i -m pkcs8 -f key_pkcs8.pub
# ssh-rsa AAAA...

PuTTY PPK → OpenSSH (Windows → Linux)

In PuTTYgen: Conversions → Export OpenSSH key.

Or on Linux with putty-tools:

puttygen key.ppk -O public-openssh -o id_rsa.pub
puttygen key.ppk -O private-openssh -o id_rsa

OpenSSH → PuTTY PPK (Linux → Windows)

puttygen id_rsa -o key.ppk

Inspect any PEM/DER structure

openssl asn1parse -in key.pub -inform PEM
openssl asn1parse -in key.pub.der -inform DER

Windows ↔ Linux

Git Bash bundles OpenSSH, so ssh-keygen and ssh behave identically to Linux. Keys generated on either side are directly compatible — no conversion needed.

# same command, same output format, on both platforms
ssh-keygen -t ed25519 -C "your@email.com"

PuTTY (legacy / GUI workflows)

PuTTY defaults to its own .ppk format, which sshd and ssh-keygen cannot read directly. This is the source of friction when mixing PuTTY with Linux servers or Git Bash.

  • Windows tools that default to PPK: PuTTY, WinSCP, FileZilla.
  • Linux sshd and ssh-keygen default to OpenSSH one-liner for public keys and OpenSSH new private key format (-----BEGIN OPENSSH PRIVATE KEY-----) for private keys since OpenSSH 7.8 (2018).
  • The OpenSSH new private key format is not PKCS#1 or PKCS#8 — it is a custom binary container. openssl cannot read it directly; use ssh-keygen -p -m PEM to convert back to the legacy PKCS#1 format if a tool requires it.

Sources