OpenSSH Keys: A Walkthrough

I recently finished a Go package to generate and handle OpenSSH keys. For posterity and the sake of having notes on how to do this, I’m going to describe how the keys are stored, and talk about converting between SSH keys and the original key formats. The Go package can be found on Github, and I’ve written a pair of programs that serve as proof-of-concepts:

One of the ideas I’ve also been tossing around is using Github’s public key API to provide a way to sign PGP keys using Github SSH keys. I have much of the groundwork laid out, but I need to actually code everything up.

RSA

When most people think of SSH keys, they probably think of RSA if they’re aware of the underlying cryptography. Until recently, they would be right: RSA has been a mainstay of public key cryptography for some time now, and although it’s on the way out for new protocols and systems, it will be around for quite some time.

Note: I generated a fresh set of keys for this blog post. You can get them from here (currently unavailable).

First, let’s look at unprotected keys, as they will be the most straight-forward. Here is our demonstration private key, found in rsa_2048_nopass/ssh_rsa_2048:

$ cat ssh_rsa_2048
-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEAoGjw0eeJLluMPgbZdSrM+uTNPs9YlZoiuF7xetVMTZ1Y7CGf
lxFwWFSTEvlozlnQqTifWRILv1jXnUni3ZFkJawM8gc/2j6O0n5DHbxUKcX4YQ+t
4aZ/+DuXYca6sdi9gmALL1vIoFvd7YcfiV9hYdPWHJJYfgqHZiw2u6aV4pKElSku
My60qbzyTio51b0N9bFrAIc2hmdcg0x+Ta/j3Xki8YJZji+WG1MYwTVi9hyAALqM
hfVCqDLBxOFRek+6TH4jceAT4+gTCWX4+tUxfiOlYP2rBDKBgfl1OsmUl/N6vmJz
Tceb13lkUC39HHCZuUCb0Ts/HpBvUj003AAHqQIDAQABAoIBAQCY65HwuWbIv9OR
ahwym4vv/uE/aJGNhPRmiXRx4heswjz8Vw16CdDtFCtlYkkstui5+dXHJvH2B279
bmuNSEaNt1hb/tc7annjZyT6mwgtDqK7fSQJwx2p+r1VJAvk8bewK3leO4Smgw2t
nCxPXJNMnJM4h7c+6TCtEadX+vZWmE7XRMrZTmSWMj/ZHVYhpKKG76uZgqJAmIGg
nCRd3ORPtnEJtQN5mdFQOlhx/dxZvnBCvDdQAlpzytYcCrH45ZtveExBrsZDtEv9
sWbDs0pQUpyYbfGavuXBs39StWf7ph7RTQ65eiL09BdPW/oS/fNvObS3InKRagqO
Vz79MbY1AoGBAMpwteJeygCt2nL5A4YUGL8k3Vck4YPW/f5wjWU0MGfgyrcw+tcu
4BbYysyi878onXeZ+HFy62VLuK4dck9FRfpBLU79GG4yFaLf925o+OQDDl28B45W
jyI3EMdJUS3v8B9+I63r8g4Wnq+Mms0F3SWi2iY7hh952zoK61yXg557AoGBAMrZ
gzbsfFibaehVeFPe5zDYHwXcNByoebpYMz8AZDkPWcBDNwzDW7IcGnC9hnj06kTI
FI0wZPqlQW/0etiky5TYB0CIOPCAOCoDNnX+6l6NdwBs6MuEH8+9/pv2aK8pVpZT
vNB53NAPEDGjRN7Si4LAc0jl5SOxOjJryt0DxWsrAoGBAMPxuHs1mHxzyn+Ce2Cp
zxIkUoFo10dPL2W594I/s6K4OD58kC771jcG+7R6/UbHvzLmu0zEGQhg9I7DPcNw
n70MnRhZbe4rWDngYpRh0paQRrV/rCifq8dIWVsrogG+vkMdSterCw2L42izxZow
1M77BAABmV6aChHyQ8HJfcJFAoGBAK0KM/bMcZ6cpRG+p3DUe1+dXYmAOSwhRAYE
a2LZEKXkRGnQbMuEc1pSwvNdmbLhKl8WVwHCQMHX6yR357ubiNcmGbmg+wGeP0sH
hpPNq1yRTOyd+1BxGzn6F5Iv90lE+EowkKc+7XDHCMdvQbba4IvfY/jRtFBoRP7y
GRHEv8oVAoGBAINw9cFvGwyFEcT+Q+UEzNJwZ+JVEK8rsiqcNAZEVkaIkcxOC1Fq
a+Zy+fMse1TS7Km1QXOyAIzcYQZy0JLjAvOUZ2ubvT1ifQ+2VCXSQywuOh1JMXMo
5OQ6J/Hmzj7+zPcgdRl0VeO7Al35YybIHdFQtFIWBCcnyjGQjKh2cxCi
-----END RSA PRIVATE KEY-----

This is a PEM-encoded RSA private key suitable for use in any application that accepts PEM-encoded RSA private keys. The public key (found in rsa_2048_nopass/ssh_rsa_2048.pub) is a different story:

$ cat ssh_rsa_2048.pub

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCgaPDR54kuW4w+Btl1Ksz65M0+z1iVmi K4XvF61UxNnVjsIZ+XEXBYVJMS+WjOWdCpOJ9ZEgu/WNedSeLdkWQlrAzyBz/aPo7SfkMd vFQpxfhhD63hpn/4O5dhxrqx2L2CYAsvW8igW93thx+JX2Fh09Ycklh+CodmLDa7ppXiko SVKS4zLrSpvPJOKjnVvQ31sWsAhzaGZ1yDTH5Nr+PdeSLxglmOL5YbUxjBNWL2HIAAuoyF 9UKoMsHE4VF6T7pMfiNx4BPj6BMJZfj61TF+I6Vg/asEMoGB+XU6yZSX83q+YnNNx5vXeW RQLf0ccJm5QJvROz8ekG9SPTTcAAep kyle@localhost

I’ve added newlines so it would fit, but this is actually all one line. How do we make sense of this? First, let’s split this line into three elements, separated by spaces. The first element, ssh_rsa, tells us this is an SSH RSA key. (Actually, it specifically refers to an SSH protocol version 2 RSA key; version 1 public keys have a different format.) The third element is a comment, and can contain any text. By default, ssh-keygen(1) uses user@hostname as the comment. That leaves us with the middle part; astute readers might recognise this as a base64-encoded string. A useful tool when analysing binary data is hexdump(1): if we pass the base64-decoded middle element (which I’ve stored in /tmp/key.bin), we get

$ hexdump -C /tmp/key.bin 
0000  00 00 00 07 73 73 68 2d  72 73 61 00 00 00 03 01  |....ssh-rsa.....|
0010  00 01 00 00 01 01 00 a0  68 f0 d1 e7 89 2e 5b 8c  |........h.....[.|
0020  3e 06 d9 75 2a cc fa e4  cd 3e cf 58 95 9a 22 b8  |>..u*....>.X..".|
0030  5e f1 7a d5 4c 4d 9d 58  ec 21 9f 97 11 70 58 54  |^.z.LM.X.!...pXT|
0040  93 12 f9 68 ce 59 d0 a9  38 9f 59 12 0b bf 58 d7  |...h.Y..8.Y...X.|
0050  9d 49 e2 dd 91 64 25 ac  0c f2 07 3f da 3e 8e d2  |.I...d%....?.>..|
0060  7e 43 1d bc 54 29 c5 f8  61 0f ad e1 a6 7f f8 3b  |~C..T)..a......;|
0070  97 61 c6 ba b1 d8 bd 82  60 0b 2f 5b c8 a0 5b dd  |.a......`./[..[.|
0080  ed 87 1f 89 5f 61 61 d3  d6 1c 92 58 7e 0a 87 66  |...._aa....X~..f|
0090  2c 36 bb a6 95 e2 92 84  95 29 2e 33 2e b4 a9 bc  |,6.......).3....|
00a0  f2 4e 2a 39 d5 bd 0d f5  b1 6b 00 87 36 86 67 5c  |.N*9.....k..6.g\|
00b0  83 4c 7e 4d af e3 dd 79  22 f1 82 59 8e 2f 96 1b  |.L~M...y"..Y./..|
00c0  53 18 c1 35 62 f6 1c 80  00 ba 8c 85 f5 42 a8 32  |S..5b........B.2|
00d0  c1 c4 e1 51 7a 4f ba 4c  7e 23 71 e0 13 e3 e8 13  |...QzO.L~#q.....|
00e0  09 65 f8 fa d5 31 7e 23  a5 60 fd ab 04 32 81 81  |.e...1~#.`...2..|
00f0  f9 75 3a c9 94 97 f3 7a  be 62 73 4d c7 9b d7 79  |.u:....z.bsM...y|
0100  64 50 2d fd 1c 70 99 b9  40 9b d1 3b 3f 1e 90 6f  |dP-..p..@..;?..o|
0110  52 3d 34 dc 00 07 a9                              |R=4....|
0117

Looking at the ASCII section, we see another ssh-rsa string, preceded by the four bytes 0x00000007. This is the big-endian encoded length of the element: “ssh-rsa” contains seven bytes. Immediately after the “a” (or the 12th byte – 4 bytes of length and 7 bytes of data), we see the four bytes 0x00000003. Let’s look at next three bytes: 0x010001. If you’ve seen the low level parts of RSA keys, you’ll immediately recognise this. Otherwise, convert it to an unsigned integer and you get 65537 – a common RSA modulus. RSA public keys require two pieces: the modulus and the public exponent. Let’s take a guess that the next piece is going to be the public exponent. We can tell from the length field that we need to read 0x00000101 (or 257) bytes; 257 * 8 = 2056 bytes, which is in the range for a 2048-byte RSA key (with some of the bits going unused). Indeed, reading 257 bytes takes us to the end of the file, and if we import both this public key and our previous PEM-encoded private key, we’d see the public keys for both match. If we convert this public key to a PEM-encoded public key (such as might be expected in other applications), we’d get}

$ cat rsa_2048.pub
-----BEGIN RSA PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoGjw0eeJLluMPgbZdSrM
+uTNPs9YlZoiuF7xetVMTZ1Y7CGflxFwWFSTEvlozlnQqTifWRILv1jXnUni3ZFk
JawM8gc/2j6O0n5DHbxUKcX4YQ+t4aZ/+DuXYca6sdi9gmALL1vIoFvd7YcfiV9h
YdPWHJJYfgqHZiw2u6aV4pKElSkuMy60qbzyTio51b0N9bFrAIc2hmdcg0x+Ta/j
3Xki8YJZji+WG1MYwTVi9hyAALqMhfVCqDLBxOFRek+6TH4jceAT4+gTCWX4+tUx
fiOlYP2rBDKBgfl1OsmUl/N6vmJzTceb13lkUC39HHCZuUCb0Ts/HpBvUj003AAH
qQIDAQAB
-----END RSA PUBLIC KEY-----

Fairly straight-forward.

With protected keys, it’s a little different. The public key format will stay the same, but the private key will be different. I’ve used the password ‘foo bar’ in ssh-keygen(1) for this key, which can be found in rsa_2048_foobar/ssh_rsa_2048:

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,E275D725D72A604BFEEFA91F4E976FBD
 
2WRvLWPz3Q3Hse8vRN17+Eh7A/l9eUPKidzc7SBaZBHIUoAnTx8JGpjIRQN/O+Ne
LZuBgoc4uwbXlUbCWAs9L0laEYKrHTMzJql5OFu7mwr1/ZRG3BDS5Mrrq8krc5XA
2j/ky+VYOGVQ2CNBHJjNAoSdcfCfd11jsCPMsCbOCfrIX8jqsX3JZUjp+f86T7d3
0J+GyE3bnvrjwMKdqY7jI/cOTYZMYCrbIp4iPY5Z1Fs4WCYOz0JhxqDwObnOS78/
LZh4c83yF+rEA6eEk8UEReHQ22iWU05hnlCitcr/uwffHqNBFLHQlKO8oEsV/0G2
n+aSp76GIczcGtDk9SFIUwNS0N1leF5mN+sUZpzcwrk8v0hE+hpQVxLtw6hptssJ
6yFc3GSBdYEKbhxuv3Wd+/P+2MmPdQRAZkjewjg5TaSdGnu1p/a2uEssT1odVR9j
uRgUzKR/nhZ46KDyw7wtBGd76WArF8bfw3VwKlTWIuSQmLmA14ANxZg6KdM6TKu/
BOQIv3GEC7YkPO6MOymNQXw17ForBN16QuNGR7RgLQn+wCxrmyQtSkx3Cyktq87B
G1CFKm/lwFHxU+AjtidAmywJM/+FDGLVm2p9/Uqun3SyhxUwe6dIBF71Gv4RUT0j
pzxfH7aSk5l0uke6bN+fIT3RrRHEknOJViEeLP9XTBDYYX7OWH7rMgL3R+m1fo1c
1cX8SH5QOW5tsRizmp5AESRgWl8McRw+DUfvQ2yICfUx5YtdzEUBEr1p+3iwXit8
cvQz0h/w60kRkp0UiIsaor9lgsVTZgsRVKAbOyifrVhibFGsuKRiQuXeMMCqWKKa
D35DXxFLSNm4GubZX/+6O5cnyzAaZ48m/K6FuMN0AYCY+pK7TvUz7D7lp7C7fY3p
h15cLgSUvbubNY4wOqQuTEy1yIfBA1kW+VCUaBnbPNvNO5fdLOwuPaZsAAnT1Hye
4u3C/rVU3/Zai1E5L/pTkRkGjEoWPbUnFGNcoMtm0ykZEOxxLnew/Pq7CGUEJ/Kp
ycgi44SJj1G6fxT0cQtxjhkAPFiBKemEEaU8kLmB97rgQAiyY+ySss7BRyO5Zrer
U6Fp7C002Aaq+qGkhxvKl5Hn6Nh3RX0eu6VmtPrWSQyoZybSB5cQklG07BdvTnwr
A0kJleze6s/aW3KQlFFY4uhnD92FNOwWLced7HGToOIolx1YCg80CIv3j+/WoTkD
c9rSWeltuQuj35+imfHltNO8xkOdFNCnUIaly4epewSYXZkw8YRpm5pJ35JjoSAG
q5Ry3nDQX7wpNU7HUe+zBTQCv9fn1TEsSadt1TMiK9ljs3xxpVWz6OIFkvojQA8R
W9jERPSmkKLyTpGjwhGWR0ooQVKwMztAvrl8rzgc9yjMFDNWpHpz/nbPwxTk6cHb
0oYXHik53qCCIjceP3duaasAO2z6/6OOldqyXdPSYVWMVVxpg8oC1EpZkENXrmCZ
gdWFpt8XMFYU6z7Icfc12BKpW1T2u7sa+FDc/6SGNDk6s0kqxRZViWxgW8ec1SqA
IZoJieieK/SPI9U1bSuci223FNaTeLiNKliiLaofI/xIohcPik7WQsHX+V6jH26c
-----END RSA PRIVATE KEY-----

Right away, we can tell this isn’t a vanilla PEM-encoded private key, as there are a couple of extra headers in our certificate. The “Proc-Type” header tells us that this is an SSH encrypted key; the “DEK-Info” tells us it was encrypted with AES-128 in CBC mode with an initialisation vector of E275D725D72A604BFEEFA91F4E976FBD. How do we get the decryption key, though? We have to use the following key derivation function:

  1. The first eight bytes of the IV are appended to the passphrase: “foo bar” in hex is 666f6f20626172, and with the first eight bytes of the IV, this becomes 666f6f20626172E275D725D72A604B.

  2. This new byte string is then passed to MD5, which gives us the digest 1b407f2387eb48705dc78ed2b03a532b. This is 16 bytes long, which gives us our AES key.

  3. We decrypt the body of the certificate with AES-128 in CBC mode with this key.

  4. The key uses the PKCS#5 padding scheme: the last byte contains the number of padding bytes; e.g. if there are 5 bytes of padding, it contains 0x05. The last five bytes of the plaintext should then be 0x05 (something you should validate if you are decrypting the key yourself). If you decrypt the key above, you’ll see the last eight bytes are, in fact, 0x08.

Here’s the decrypted key (in rsa_2048_foobar/key.bin) with the padding stripped:

$ hexdump -C rsa_2048_foobar/key.bin
0000  9a 55 c4 8e 5f 65 b6 0b  bd 39 51 41 2d f5 7f de  |.U.._e...9QA-...|
0010  65 15 f8 7d 99 50 fc 21  b0 45 c3 ed b0 7e b9 6d  |e....P.!.E...~.m|
0020  a9 e2 f1 30 b9 f3 a5 cf  b6 91 94 f2 b5 d4 ec 19  |...0............|
0030  53 9a e5 ab 62 8d 3b 6f  a0 39 d0 a6 81 18 d2 f3  |S...b.;o.9......|
0040  6d 80 9d fd 33 d3 8d 51  ed 19 f9 5b 0f 26 da fa  |m...3..Q...[.&..|
0050  ea df f3 fb d6 54 c2 01  26 6b 87 56 6e 07 81 0b  |.....T..&k.Vn...|
0060  e2 cf 8e fd fd 00 40 2c  d4 a0 f5 97 80 64 1c 97  |......@,.....d..|
0070  e5 f1 b1 cd 11 a3 01 09  89 8c 53 78 4f 43 39 e1  |..........SxOC9.|
0080  e4 e6 71 31 9d f8 f1 12  1d 34 9e 2f 91 d4 da 29  |..q1.....4./...)|
0090  27 fe b8 6a 20 73 f9 b0  71 cd 2c b6 54 3b e4 74  |'..j s..q.,.T;.t|
00a0  0a 4b 54 6e 2f c0 3d c4  40 87 44 d7 9b e5 e6 db  |.KTn/.=.@.D.....|
00b0  88 84 8b 5f 1b d4 44 69  4b 04 b6 f8 a7 54 4d 01  |..._..DiK....TM.|
00c0  8a 15 79 f3 d7 48 13 f9  7e 1b c9 f0 4c 0f 98 d5  |..y..H..~...L...|
00d0  0a a6 15 16 cf cf 80 ee  cf 1e da a8 05 c2 5d bf  |..............].|
00e0  fa ae fb c2 82 f3 6e 69  9c 65 64 60 1c 44 54 48  |......ni.ed`.DTH|
00f0  30 8f 9d 3f 59 22 bd 97  eb bd eb 17 ba 07 d3 24  |0..?Y".........$|
0100  1b b7 42 e7 dd f7 83 5c  e4 ad a3 75 02 03 01 00  |..B....\...u....|
0110  01 02 82 01 00 3a 4e 09  08 bd 0a c1 a0 f1 ae a9  |.....:N.........|
0120  20 5f 2f 52 a0 8d 84 65  10 45 94 3b 30 62 32 dd  | _/R...e.E.;0b2.|
0130  f7 a3 ba 47 1e 99 5b 2c  44 a3 a1 58 8d e8 25 15  |...G..[,D..X..%.|
0140  6a c3 62 9f c3 bf 78 d0  74 f1 a7 32 b3 be 5e 07  |j.b...x.t..2..^.|
0150  01 58 cb 34 4f 93 b0 56  46 08 9c db 5f 5d d5 80  |.X.4O..VF..._]..|
0160  8a 2e a4 05 73 75 92 8a  51 1a 5b 7a 59 8e a7 e3  |....su..Q.[zY...|
0170  c8 70 83 6c c0 7e 20 54  d4 e1 ce aa e7 8d bf 0d  |.p.l.~ T........|
0180  12 61 7a 82 d1 ff 58 42  91 1c ef 7c 3b e6 38 c0  |.az...XB...|;.8.|
0190  74 97 2a 17 fb 93 e3 d9  e5 bd 95 7d 72 21 42 44  |t.*.........r!BD|
01a0  0f fe f0 75 86 91 fb ba  ee 88 89 17 14 f3 1c cc  |...u............|
01b0  34 04 e7 1c b5 ae 8e 34  b2 41 90 37 f6 44 a4 d3  |4......4.A.7.D..|
01c0  bc 86 e9 51 77 8e 93 0e  43 c4 9d 95 71 13 60 cf  |...Qw...C...q.`.|
01d0  ad 22 71 4f c1 76 7d 43  bd f9 b6 48 37 ae 1a 03  |."qO.v.C...H7...|
01e0  7d 9f 2b c2 29 47 88 de  75 f7 19 63 d6 a2 04 82  |..+.)G..u..c....|
01f0  d5 7b a9 7e 43 c9 36 b9  2d 56 35 d2 3e 1a 1a 40  |...~C.6.-V5.>..@|
0200  9e e9 c6 7f e8 66 e0 49  9a fb 3a 68 da a9 96 41  |.....f.I..:h...A|
0210  ce 1d 54 30 a5 02 81 81  00 ee 55 b7 ed d3 e2 d5  |..T0......U.....|
0220  84 58 7f 27 8c 33 af 0e  28 93 f8 54 be 86 79 dc  |.X.'.3..(..T..y.|
0230  c4 de 6c 91 e3 13 13 7b  69 15 ae 02 a5 2e 87 85  |..l.....i.......|
0240  8d 0f e4 88 3b d3 32 7b  46 f5 fa 1b 8f c8 1e fd  |....;.2.F.......|
0250  a9 7d 40 1d 20 6d 86 78  16 9a 52 02 f8 c8 04 17  |..@. m.x..R.....|
0260  dc 48 10 a1 2c df 6e 32  18 22 a4 c3 ca fc 5a 9a  |.H..,.n2."....Z.|
0270  54 f2 5a f6 78 d2 c0 00  42 1f fb c0 c8 72 ee 06  |T.Z.x...B....r..|
0280  da 50 cd a3 e4 05 e6 bf  8a 89 48 e8 5a fe f8 15  |.P........H.Z...|
0290  49 ef 0f bb 0e 6d 18 32  5f 02 81 81 00 ce c1 e6  |I....m.2_.......|
02a0  41 e2 ee 14 3e 2c b2 03  da 42 01 a3 89 a4 bd 64  |A...>,...B.....d|
02b0  45 8b 8f 8c 31 01 f7 b7  e3 61 a4 19 bc fe 55 75  |E...1....a....Uu|
02c0  7b 0b aa 14 cf f6 69 64  49 4a 17 fe fe 6f d1 c7  |......idIJ...o..|
02d0  d0 9c 1f 07 b0 42 40 90  7e 11 8b 73 a0 cb 2c d6  |.....B@.~..s..,.|
02e0  3c 08 98 dc 12 c4 7d 75  1f e6 c6 23 97 12 ff 31  |<......u...#...1|
02f0  48 1d 46 8e 89 c5 38 fc  e6 31 90 1d 0e 60 03 e2  |H.F...8..1...`..|
0300  fa 0a c9 50 7f b9 14 db  10 79 40 0e d8 9c 04 f3  |...P.....y@.....|
0310  13 05 97 e1 fe 50 14 10  00 ff a8 c2 ab 02 81 81  |.....P..........|
0320  00 e7 24 3c d0 2b 69 57  58 16 17 a3 7c 26 77 c4  |..$<.+iWX...|&w.|
0330  cc 77 cd 24 2a ee f2 6a  b8 87 5a 56 16 1a 5f 0f  |.w.$*..j..ZV.._.|
0340  95 f7 8f 9e 43 22 a0 0e  56 a3 2e 29 2d 94 02 e6  |....C"..V..)-...|
0350  6a 08 8e 7a 82 74 3c 12  18 79 3f 5f c0 1e 58 86  |j..z.t<..y?_..X.|
0360  48 ad 7b 92 24 42 9f 2d  a0 d6 47 42 78 e9 6b ce  |H...$B.-..GBx.k.|
0370  ab 77 95 c1 c9 2f fa 92  a8 85 ae d0 50 97 5b dc  |.w.../......P.[.|
0380  24 45 2e 7c 66 2a 88 4c  1f 18 b4 87 18 d2 dc 59  |$E.|f*.L.......Y|
0390  b0 fa b9 b9 96 de a6 2c  c7 5a 51 30 41 aa 6e 08  |.......,.ZQ0A.n.|
03a0  13 02 81 81 00 c6 1c fb  92 54 6c c6 8e d0 ea fe  |.........Tl.....|
03b0  50 0b 81 60 7f 8a 90 43  5d 74 1d e7 3a 2a 65 80  |P..`...C]t..:*e.|
03c0  19 6b bc ff 40 9d ff ab  23 6d 08 db 0a 6e 55 8c  |.k..@...#m...nU.|
03d0  57 c1 9c 7e a4 b4 e6 b1  6d 0d bb 99 f5 65 76 52  |W..~....m....evR|
03e0  3d ca 77 f2 22 b7 ac 4d  d7 96 71 4a 26 dd 8a 4d  |=.w."..M..qJ&..M|
03f0  49 75 2a 94 7d b5 21 3b  ca 9a ea b9 b7 ff 77 dc  |Iu*...!;......w.|
0400  b4 81 f7 52 30 2d 06 6c  5e 25 29 c3 6d af 1d f1  |...R0-.l^%).m...|
0410  ce b6 30 10 49 b4 2d f5  f0 5b 22 ae d4 ad 4c 9c  |..0.I.-..["...L.|
0420  0d 28 ec 3f 23 02 81 80  0c 97 3a 00 a2 29 0b d7  |.(.?#.....:..)..|
0430  4b d6 a4 c5 89 d7 75 ce  c6 ea 7e 03 55 a4 8c 79  |K.....u...~.U..y|
0440  79 02 db 8b f0 af 02 ca  b0 98 16 15 e0 a5 42 0f  |y.............B.|
0450  dd 7d b6 22 90 16 2d b7  b9 dc 7a 06 61 b6 5b a9  |..."..-...z.a.[.|
0460  fc 2f ec 21 8a cb 25 80  1c 4e e2 f3 33 80 20 94  |./.!..%..N..3. .|
0470  c9 a5 e2 8d 97 a4 16 c7  ce 68 75 89 fa ab 8f 21  |.........hu....!|
0480  b2 f8 1c 16 b4 70 06 f8  01 d0 2e 54 88 70 60 30  |.....p.....T.p`0|
0490  1f d6 6c 3a 0d bb 8e a7  7d e3 cf 39 f2 a2 01 bc  |..l:.......9....|
04a0  72 7e 44 46 d0 80 9e 16                           |r~DF....|

Let’s base64-encode this, and split the lines at 64 characters (such as is done with PEM).

MIIEpAIBAAKCAQEAwH1+/2UV+H2ZUPwhsEXD7bB+uW2p4vEwufOlz7aRlPK11OwZ
U5rlq2KNO2+gOdCmgRjS822Anf0z041R7Rn5Ww8m2vrq3/P71lTCASZrh1ZuB4EL
4s+O/f0AQCzUoPWXgGQcl+Xxsc0RowEJiYxTeE9DOeHk5nExnfjxEh00ni+R1Nop
J/64aiBz+bBxzSy2VDvkdApLVG4vwD3EQIdE15vl5tuIhItfG9REaUsEtvinVE0B
ihV589dIE/l+G8nwTA+Y1QqmFRbPz4Duzx7aqAXCXb/6rvvCgvNuaZxlZGAcRFRI
MI+dP1kivZfrvesXugfTJBu3Qufd94Nc5K2jdQIDAQABAoIBADpOCQi9CsGg8a6p
IF8vUqCNhGUQRZQ7MGIy3fejukcemVssRKOhWI3oJRVqw2Kfw7940HTxpzKzvl4H
AVjLNE+TsFZGCJzbX13VgIoupAVzdZKKURpbelmOp+PIcINswH4gVNThzqrnjb8N
EmF6gtH/WEKRHO98O+Y4wHSXKhf7k+PZ5b2VfXIhQkQP/vB1hpH7uu6IiRcU8xzM
NATnHLWujjSyQZA39kSk07yG6VF3jpMOQ8SdlXETYM+tInFPwXZ9Q735tkg3rhoD
fZ8rwilHiN519xlj1qIEgtV7qX5DyTa5LVY10j4aGkCe6cZ/6GbgSZr7OmjaqZZB
zh1UMKUCgYEA7lW37dPi1YRYfyeMM68OKJP4VL6GedzE3myR4xMTe2kVrgKlLoeF
jQ/kiDvTMntG9fobj8ge/al9QB0gbYZ4FppSAvjIBBfcSBChLN9uMhgipMPK/Fqa
VPJa9njSwABCH/vAyHLuBtpQzaPkBea/iolI6Fr++BVJ7w+7Dm0YMl8CgYEAzsHm
QeLuFD4ssgPaQgGjiaS9ZEWLj4wxAfe342GkGbz+VXV7C6oUz/ZpZElKF/7+b9HH
0JwfB7BCQJB+EYtzoMss1jwImNwSxH11H+bGI5cS/zFIHUaOicU4/OYxkB0OYAPi
+grJUH+5FNsQeUAO2JwE8xMFl+H+UBQQAP+owqsCgYEA5yQ80CtpV1gWF6N8JnfE
zHfNJCru8mq4h1pWFhpfD5X3j55DIqAOVqMuKS2UAuZqCI56gnQ8Ehh5P1/AHliG
SK17kiRCny2g1kdCeOlrzqt3lcHJL/qSqIWu0FCXW9wkRS58ZiqITB8YtIcY0txZ
sPq5uZbepizHWlEwQapuCBMCgYEAxhz7klRsxo7Q6v5QC4Fgf4qQQ110Hec6KmWA
GWu8/0Cd/6sjbQjbCm5VjFfBnH6ktOaxbQ27mfVldlI9ynfyIresTdeWcUom3YpN
SXUqlH21ITvKmuq5t/933LSB91IwLQZsXiUpw22vHfHOtjAQSbQt9fBbIq7UrUyc
DSjsPyMCgYAMlzoAoikL10vWpMWJ13XOxup+A1WkjHl5AtuL8K8CyrCYFhXgpUIP
3X22IpAWLbe53HoGYbZbqfwv7CGKyyWAHE7i8zOAIJTJpeKNl6QWx85odYn6q48h
svgcFrRwBvgB0C5UiHBgMB/WbDoNu46nfePPOfKiAbxyfkRG0ICeFg==

Now, let’s wrap this with PEM markers:

-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAwH1+/2UV+H2ZUPwhsEXD7bB+uW2p4vEwufOlz7aRlPK11OwZ
U5rlq2KNO2+gOdCmgRjS822Anf0z041R7Rn5Ww8m2vrq3/P71lTCASZrh1ZuB4EL
4s+O/f0AQCzUoPWXgGQcl+Xxsc0RowEJiYxTeE9DOeHk5nExnfjxEh00ni+R1Nop
J/64aiBz+bBxzSy2VDvkdApLVG4vwD3EQIdE15vl5tuIhItfG9REaUsEtvinVE0B
ihV589dIE/l+G8nwTA+Y1QqmFRbPz4Duzx7aqAXCXb/6rvvCgvNuaZxlZGAcRFRI
MI+dP1kivZfrvesXugfTJBu3Qufd94Nc5K2jdQIDAQABAoIBADpOCQi9CsGg8a6p
IF8vUqCNhGUQRZQ7MGIy3fejukcemVssRKOhWI3oJRVqw2Kfw7940HTxpzKzvl4H
AVjLNE+TsFZGCJzbX13VgIoupAVzdZKKURpbelmOp+PIcINswH4gVNThzqrnjb8N
EmF6gtH/WEKRHO98O+Y4wHSXKhf7k+PZ5b2VfXIhQkQP/vB1hpH7uu6IiRcU8xzM
NATnHLWujjSyQZA39kSk07yG6VF3jpMOQ8SdlXETYM+tInFPwXZ9Q735tkg3rhoD
fZ8rwilHiN519xlj1qIEgtV7qX5DyTa5LVY10j4aGkCe6cZ/6GbgSZr7OmjaqZZB
zh1UMKUCgYEA7lW37dPi1YRYfyeMM68OKJP4VL6GedzE3myR4xMTe2kVrgKlLoeF
jQ/kiDvTMntG9fobj8ge/al9QB0gbYZ4FppSAvjIBBfcSBChLN9uMhgipMPK/Fqa
VPJa9njSwABCH/vAyHLuBtpQzaPkBea/iolI6Fr++BVJ7w+7Dm0YMl8CgYEAzsHm
QeLuFD4ssgPaQgGjiaS9ZEWLj4wxAfe342GkGbz+VXV7C6oUz/ZpZElKF/7+b9HH
0JwfB7BCQJB+EYtzoMss1jwImNwSxH11H+bGI5cS/zFIHUaOicU4/OYxkB0OYAPi
+grJUH+5FNsQeUAO2JwE8xMFl+H+UBQQAP+owqsCgYEA5yQ80CtpV1gWF6N8JnfE
zHfNJCru8mq4h1pWFhpfD5X3j55DIqAOVqMuKS2UAuZqCI56gnQ8Ehh5P1/AHliG
SK17kiRCny2g1kdCeOlrzqt3lcHJL/qSqIWu0FCXW9wkRS58ZiqITB8YtIcY0txZ
sPq5uZbepizHWlEwQapuCBMCgYEAxhz7klRsxo7Q6v5QC4Fgf4qQQ110Hec6KmWA
GWu8/0Cd/6sjbQjbCm5VjFfBnH6ktOaxbQ27mfVldlI9ynfyIresTdeWcUom3YpN
SXUqlH21ITvKmuq5t/933LSB91IwLQZsXiUpw22vHfHOtjAQSbQt9fBbIq7UrUyc
DSjsPyMCgYAMlzoAoikL10vWpMWJ13XOxup+A1WkjHl5AtuL8K8CyrCYFhXgpUIP
3X22IpAWLbe53HoGYbZbqfwv7CGKyyWAHE7i8zOAIJTJpeKNl6QWx85odYn6q48h
svgcFrRwBvgB0C5UiHBgMB/WbDoNu46nfePPOfKiAbxyfkRG0ICeFg==
-----END RSA PRIVATE KEY-----

This is the unprotected RSA private key. Easy enough, right?

Let’s talk elliptic curves now.

Elliptic curve keys

Elliptic curves are the new hotness (and both actually not very new as well as the future of public-key cryptography, but that’s another post). Recent versions of OpenSSH support the use of ECDSA (elliptic curve keys for the digital signature algorithm), although if you’re on a Mac, you’re likely to be out of luck. Hopefully, OS X catches up on the crypto game soon. Update: I’ve heard reports that OS X 10.8.4 now has an updated version of OpenSSH, but I have not been able to confirm this.

The encrypted key format is exactly the same, so I’ll focus on the unencrypted keys.

OpenSSH supports three curves: the NIST 256-bit, 384-bit, and 521-bit (that’s actually 521 bits, and not a typo for 512) curves. The 256-bit curves provide equivalent 128-bit symmetric key security, and correspond to 3072-bit RSA keys. Let’s see what a 256-bit key looks like:

$ cat ecdsa_256/ecdsa_256
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDrzGO4D3FhCGRPthZi7mO/VWmAdIiak8LyWbYddnA20oAoGCCqGSM49
AwEHoUQDQgAEzNls59RMcoe1sHsb3m/nOMT2tbdPAqMH9I1B3MiWmIF96MzazkMm
xjjkKRmFmLLfCdlHjIGjDK1gNeDyKg/hCA==
-----END EC PRIVATE KEY-----

That’s all there is to it. As with RSA keys, this is the “vanilla” key, in a format that’s as widely applicable as possible. Let’s look at the public key:

$ cat ecdsa_256/ecdsa_256.pub 
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAA
ABBBMzZbOfUTHKHtbB7G95v5zjE9rW3TwKjB/SNQdzIlpiBfejM2s5DJsY45CkZhZiy3w
nZR4yBowytYDXg8ioP4Qg= kyle@localhost

Once again, we have our three space-delimited elements: key type, the key material, and a comment. The key type is ecdsa-sha2-nistp256: elliptic curve Digital Signature Algorithm, using the SHA2 family of hash functions, and the key is of the curve nistp256 – the NIST curve over a curve defined on a prime field of 256 bits.}

Let’s decode the key material and see what’s in there:

$ hexdump -C /tmp/key.bin 
0000  00 00 00 13 65 63 64 73  61 2d 73 68 61 32 2d 6e  |....ecdsa-sha2-n|
0010  69 73 74 70 32 35 36 00  00 00 08 6e 69 73 74 70  |istp256....nistp|
0020  32 35 36 00 00 00 41 04  cc d9 6c e7 d4 4c 72 87  |256...A...l..Lr.|
0030  b5 b0 7b 1b de 6f e7 38  c4 f6 b5 b7 4f 02 a3 07  |..{..o.8....O...|
0040  f4 8d 41 dc c8 96 98 81  7d e8 cc da ce 43 26 c6  |..A.....}....C&.|
0050  38 e4 29 19 85 98 b2 df  09 d9 47 8c 81 a3 0c ad  |8.).......G.....|
0060  60 35 e0 f2 2a 0f e1 08                           |`5..*...|

Looks pretty similar? We have three fields, as before, each preceded by a length field. Our three fields are:

  1. a 19-byte field containing the text “ecdsa-sha2-nistp256” (once again, echoing the RSA public key material).

  2. an 8-byte field containing “nistp256” – the curve specifier for our EC key.

  3. a 65-byte field containing the actual EC public key. This key is in the format specified by SEC 1, the standard specifying elliptic curve cryptography.

We can base64 field #3 and put it in a PEM-encoded certificate to create a generic EC public key file:

-----BEGIN EC PUBLIC KEY-----
BMzZbOfUTHKHtbB7G95v5zjE9rW3TwKjB/SNQdzIlpiBfejM2s5DJsY45CkZhZiy
3wnZR4yBowytYDXg8ioP4Qg=
-----END EC PUBLIC KEY-----

sshkeyconvert

This is a utility tool I wrote to convert SSH keys into vanilla RSA keys. It builds on a few other Go packages I’ve written, namely the previously-mentioned SSH key package, as well as an implementation of the Elliptic Curve Integrated Encryption scheme (which has a number of useful key-related functions). It is released under the ISC license.}

package main
 
import (
    "crypto/ecdsa"
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "flag"
    "fmt"
    "github.com/gokyle/ecies"
    "github.com/gokyle/sshkey"
    "io/ioutil"
    "os"
)
 
func loadKey(in, out string) (err error) {
    key, keytype, err := sshkey.LoadPrivateKeyFile(in)
    if err != nil {
        return
    }
 
    switch keytype {
    case sshkey.KEY_RSA:
        rsaKey := key.(*rsa.PrivateKey)
        var block pem.Block
        block.Type = "RSA PRIVATE KEY"
        block.Bytes = x509.MarshalPKCS1PrivateKey(rsaKey)
        privOut := pem.EncodeToMemory(&block)
        err = ioutil.WriteFile(out+".key", privOut, 0600)
        if err != nil {
            return
        }
 
        block.Type = "RSA PUBLIC KEY"
        block.Bytes, err = x509.MarshalPKIXPublicKey(&rsaKey.PublicKey)
        if err != nil {
            return
        }
        pubOut := pem.EncodeToMemory(&block)
        err = ioutil.WriteFile(out+".pub", pubOut, 0644)
        return
    case sshkey.KEY_ECDSA:
        var privOut, pubOut []byte
        eckey := ecies.ImportECDSA(key.(*ecdsa.PrivateKey))
        privOut, err = ecies.ExportPrivatePEM(eckey)
        if err != nil {
            return
        }
        pubOut, err = ecies.ExportPublicPEM(&eckey.PublicKey)
        if err != nil {
            return
        }
        err = ioutil.WriteFile(out+".key", privOut, 0600)
        if err != nil {
            return
        }
        err = ioutil.WriteFile(out+".pub", pubOut, 0600)
        if err != nil {
            return
        }
        return
    default:
        err = fmt.Errorf("sshkeyconvert: invalid key format")
        return
    }
}
 
func main() {
    inFile := flag.String("in", "", "source SSH private key")
    outFile := flag.String("out", "", "destination file basename")
    flag.Parse()
 
    if *inFile == "" {
        fmt.Println("no source file specified.")
        os.Exit(1)
    } else if *outFile == "" {
        fmt.Println("no destination file basename specified")
        os.Exit(1)
    }
    err := loadKey(*inFile, *outFile)
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(1)
    }
    os.Exit(0)
}

References

I spent a lot of time reading the source for OpenSSH, and I also found Martin Kleppmann’s post “Improving the Security of SSH Private Keys” rather helpful.