Traditionally OpenSSH has used the OpenSSL-compatible formats PKCS#1 (for RSA) and SEC1 (for EC) for Private keys.

This week I discovered that it now has its own format too, which is the default output format for some installations of ssh-keygen.

After peeking at the binary I found, much to my dismay - and very much unlike the ssh public key format (RFC 4253) - that OpenSSH private key format is not intuitively obvious, I headed to les googles.

I searched high and low (or at least past page 2, which is a distinguished mark of true dedication), but found no useful information to assauge my curiosity (and habit).

In lieu of the docs I turned to the source. With a combination of the concentrated efforts of my best code sluething and reverse engineering skills, I believe I have (here below) produced the most complete documentation the Internet has to offer on the subject.

So, without further ado...

OpenSSH Private Keys

On the outside it's PEM encoded. It looks like this:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR9WZPeBSvixkhjQOh9yCXXlEx5CN9M
yh94CJJ1rigf8693gc90HmahIR5oMGHwlqMoS7kKrRw+4KpxqsF7LGvxAAAAqJZtgRuWbY
EbAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH1Zk94FK+LGSGNA
6H3IJdeUTHkI30zKH3gIknWuKB/zr3eBz3QeZqEhHmgwYfCWoyhLuQqtHD7gqnGqwXssa/
EAAAAgBzKpRmMyXZ4jnSt3ARz0ul6R79AXAr5gQqDAmoFeEKwAAAAOYWpAYm93aWUubG9j
YWwBAg==
-----END OPENSSH PRIVATE KEY-----

But, unlike most PEMs, there's no DER inside.

Instead it's the "proprietary" OpenSSH format, which looks like this:

"openssh-key-v1"0x00    # NULL-terminated "Auth Magic" string
32-bit length, "none"   # ciphername length and string
32-bit length, "none"   # kdfname length and string
32-bit length, nil      # kdf (0 length, no kdf)
32-bit 0x01             # number of keys, hard-coded to 1 (no length)
32-bit length, sshpub   # public key in ssh format
    32-bit length, keytype
    32-bit length, pub0
    32-bit length, pub1
32-bit length for rnd+prv+comment+pad
    64-bit dummy checksum?  # a random 32-bit int, repeated
    32-bit length, keytype  # the private key (including public)
    32-bit length, pub0     # Public Key parts
    32-bit length, pub1
    32-bit length, prv0     # Private Key parts
    ...                     # (number varies by type)
    32-bit length, comment  # comment string
    padding bytes 0x010203  # pad to blocksize (see notes below)

As you can see (maybe) there's

  • A format ID prefix
  • Encryption headers
  • An unused number for number of keys in the block
  • An rfc4253-style ssh public key
  • An private key somewhat modeled after the rfc4253 style
  • A comment
  • Padding for aligning private key to the blocksize

Note that the blocksize is 8 (for unencrypted keys, at least).

The RFC 4253 SSH Public Key format, is used for both the embedded public key and embedded private key key, with the caveat that the private key has a header and footer that must be sliced:

  • RSA private keys swap e and n for n and e.
  • 8 bytes of unused checksum bytes as a header
  • n bytes (between 0 and 7) of padding
    • bytes > 0x00 and < 0x08 must be trimmed (from the right)
    • the padding must be a (right-trimmed) substring of 0x01020304050607
    • (that includes the empty substring)
    • if the last byte isn't padding, it's part of the comment (0x21 to 0x7e)

Reference Material

The canonical source code is only available via tarball (.tar.gz).

However, there's also a well-maintained fork (Portable OpenSSH) which has perfectly linkable source code and among them I found this to be the file of greatest interest:

https://github.com/openssh/openssh-portable/blob/master/sshkey.c

Even more particularly, these were the most interesting functions:

  • sshkey_parse_private2
  • sshkey_private_deserialize
  • sshkey_private_serialize_opt

I don't quite remember where, but another piece of information I discovered is that when the key isn't encrypted (cipher and kdf values are "none" and "none") the blocksize is 8 bytes and the value of CLFLAG_NONE is also 8:

blocksize = 8
CFLAG_NONE = 00001000

By AJ ONeal

If you loved this and want more like it, sign up!


Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )