Peter Lyons

OpenSSH ed25519 private key file format

December 01, 2017

Today I finished understanding the openssh private key format for ed25519 keys. Yesterday's analysis had a few remaining mysteries that a fellow RCer helped me solve plus a pair of mistakes that threw off some fields. So here for the record is a "complete" byte-by-byte analysis of what's inside an openssh ed25519 private key (both with and without a passphrase). This was done with OpenSSH_7.5p1, LibreSSL 2.5.4 on macOS 10.13. I say "complete" in quotes because you could go one layer deeper into the actual key payloads but for our purposes those details don't have specific meaning.

First, let's generate a test keypair (hit ENTER for no passphrase when prompted):

mkdir /tmp/test-keys
cd /tmp/test-keys
ssh-keygen -t ed25519 -f ssh-ed25519-private-key.pem
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ssh-ed25519-private-key.pem.
Your public key has been saved in ssh-ed25519-private-key.pem.pub.
The key fingerprint is:
SHA256:qXcj+K4QZHxJ712c/M1PPutkyxT/Hn+1RKBIHLlypY4 plyons@avi
The key's randomart image is:
+--[ED25519256]--+
|      . ..o      |
|   . . o + + o   |
|    + o o = * .  |
|   o . o B o . + |
|    .   S .   o.+|
|     . E .     +=|
|    . o o o   .**|
|     . o o .  =.O|
|      .oo     .*=|
+----[SHA256]-----+

Now let's get at those tasty bytes of payload as a hex dump:

grep -v - ssh-ed25519-private-key.pem | base64 -D | xxd -p                  
6f70656e7373682d6b65792d763100000000046e6f6e65000000046e6f6e
650000000000000001000000330000000b7373682d656432353531390000
00203cfe2afb025f46582e502b97f7dfa5a08dea09f87abfa8d5bfcaabf2
9fbb369500000090a2224bbaa2224bba0000000b7373682d656432353531
39000000203cfe2afb025f46582e502b97f7dfa5a08dea09f87abfa8d5bf
caabf29fbb36950000004002a1965d1a2684d50d29f2be0efd8e2fae3c5b
b013d06f7818416333955271a53cfe2afb025f46582e502b97f7dfa5a08d
ea09f87abfa8d5bfcaabf29fbb36950000000a706c796f6e734061766901
0203

OK here's how it breaks down.

# ASCII magic "openssh-key-v1" plus null byte
6f70656e7373682d6b65792d7631 00
00000004 int length = 4
6e6f6e65 string cipher = none
00000004 int length = 4
6e6f6e65 string kdfname = none
00000000 int length = 0
# zero-length kdfoptions placeholder here
00000001 int number of public keys = 1
00000033 int length first public key = 51 (4 + 11 + 4 + 32)
0000000b int length = 11
7373682d65643235353139 string key type = ssh-ed25519
00000020 int length = 32
# public key payload 32 bytes
# probably encoding a point on the ed25519 curve
3cfe2afb025f46582e502b97f7dfa5a0
8dea09f87abfa8d5bfcaabf29fbb3695

00000090 int length = 144 size of remaining payload
# 8 + 4 + 11 + 4 + 32 + 4 + 64 + 4 + 10 + 3
a2224bbaa2224bba iv/salt? (Not sure about these 8 bytes)

# Here's a repeat of the public key (part of the private key pair)
0000000b int length = 11
7373682d656432353531 39 string key type = ssh-ed25519
00000020  int length = 32
# public key payload 32 bytes
# probably encoding a point on the ed25519 curve
3cfe2afb025f46582e502b97f7dfa5a0
8dea09f87abfa8d5bfcaabf29fbb3695

00000040 int length = 64
# 32 bytes private key payload 1
02a1965d1a2684d50d29f2be0efd8e2f
ae3c5bb013d06f7818416333955271a5
# 32 bytes is the public/point again
3cfe2afb025f46582e502b97f7dfa5a0
8dea09f87abfa8d5bfcaabf29fbb3695

0000000a int length = 10
706c796f6e7340617669 private key payload 2
010203 padding 3 bytes incrementing integers

Now let's generate one and encrypt with a passphrase

ssh-keygen -t ed25519 -f ssh-ed25519-passphrase-private-key.pem
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ssh-ed25519-passphrase-private-key.pem.
Your public key has been saved in ssh-ed25519-passphrase-private-key.pem.pub.
The key fingerprint is:
SHA256:QfPM5uIVVM6yT5sXeCvSjETCcP5zmTH+Rt7wY/91Ik8 plyons@avi
The key's randomart image is:
+--[ED25519256]--+
|      . + ...    |
|       * * o     |
|        = O =    |
|         B = *   |
|        S B B =  |
|       . + O B * |
|        . o O Eo=|
|           . B..=|
|              . +|
+----[SHA256]-----+

Here's the dump

grep -v - ssh-ed25519-passphrase-private-key.pem | base64 -D | xxd -p
6f70656e7373682d6b65792d7631000000000a6165733235362d63626300
0000066263727970740000001800000010d08f6b8fd17593f246db4ac6c4
5a11930000001000000001000000330000000b7373682d65643235353139
0000002062837be86c63712896b8e0e7543e367c3abd0c0b5ad3e764ea0e
4f8ddd7d00ef000000901e60c56ef30d0ff02e07b57bf14645076c32c86c
88ecad545ca28424e4739aff5895bebd6778e70b6c54b309b9fdb0c94102
bf8cef5b97d3d75636967e67e4b9c1ee72ae81074b0ce0f7e540e051d569
05da263af3e383342cc75b3145242abb75257586a119c9d3673dfb7eabe4
696350904e7c7af3cd77f28bea10374e15bc6536c2e1029438fdd3930bee
bbc5ac30

Here's the annotated structure:

#ASCII magic "openssh-key-v1" plus null byte
6f70656e7373682d6b65792d7631 00
0000000a int length = 10
6165733235362d636263 string cipher = aes256-cbc
00000006 int length = 6
626372797074 string kdfname = bcrypt
00000018 int length = 24 (kdfoptions)
00000010 int length = 16
d08f6b8fd17593f246db4ac6c45a1193 salt/iv for bcrypt
00000010 int work factor = 16
00000001 int number of public keys = 1
00000033 int length = 51 public key 1 size (4 + 11 + 4 + 32)

# Public key 1
0000000b int length = 11
7373682d65643235353139 string key type = ssh-ed25519
00000020 int length = 32
# public key payload 32 bytes
# probably encoding a point on the ed25519 curve
62837be86c63712896b8e0e7543e367c
3abd0c0b5ad3e764ea0e4f8ddd7d00ef

00000090 int length = 144 encrypted aes256-cbc output (16x9)
1e60c56ef30d0ff02e07b57bf1464507
6c32c86c88ecad545ca28424e4739aff
5895bebd6778e70b6c54b309b9fdb0c9
4102bf8cef5b97d3d75636967e67e4b9
c1ee72ae81074b0ce0f7e540e051d569
05da263af3e383342cc75b3145242abb
75257586a119c9d3673dfb7eabe46963
50904e7c7af3cd77f28bea10374e15bc
6536c2e1029438fdd3930beebbc5ac30

So there you have it. You could dig even deeper into the keys themselves to extract the various specific numbers (usually they have 1-letter names from the math equations they belong to like e,n,p,q etc) but I didn't bother for this since for this key format all I want to know is that it's an ed25519 private key and whether or not it has a passphrase.

Other bits I learned while pairing are that ed25519 is the name of a specific curve in elliptic curve cryptography and the numbers represent ((2^255) - 19) as a Mersenne prime (out of my depth here, could be wrong). Part of it is the x coordinate and then you can compute the y bit if you know the sign or something like that.

Also in the openssh source code they explicitly error out if the number of keys field isn't 1 so only 1 key is supported.

For further reading check out How do Ed5519 keys work? by Brian Warner.


This post is technically my Recurse Center 15 post but it was suggested that this post would make a handy reference so I didn't want to muck up the clarity/SEO of the title.