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:
sshbox
allows you to use SSH keys to encrypt and decrypt files.sshkeygen is a pure-Go implementation of the core-functionality found in
ssh-keygen(1)
.
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:
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 becomes666f6f20626172E275D725D72A604B
.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.We decrypt the body of the certificate with AES-128 in CBC mode with this key.
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 be0x05
(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... c5="" fc="" e6="" e2="" fa="" c9="" b9="" db="" d8="" f3="" e1="" fe="" ff="" a8="" c2="" ab="" e7="" d0="" a3="" c4="" cc="" cd="" ee="" f2="" b8="" f7="" a0="" c0="" ad="" d6="" e9="" ce="" c1="" ae="" dc="" b4="" d2="" b0="" de="" a6="" c7="" aa="" c6="" fb="" ea="" bc="" a4="" b1="" bb="" f5="" ca="" b7="" ac="" d7="" dd="" b5="" c3="" af="" f1="" b6="" f0="" d4="" ec="" a2="" e0="" a5="" a9="" cb="" .="" b2="" f8="" a7="" e3="" cf="" readability="118.2868611488">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 curvenistp256
– 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:
a 19-byte field containing the text “ecdsa-sha2-nistp256” (once again, echoing the RSA public key material).
an 8-byte field containing “nistp256” – the curve specifier for our EC key.
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.