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:

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 becomes 666f6f20626172E275D725D72A604B . 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 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: