We increase the security of private ssh keys

Original author: Martin Kleppmann
  • Transfer
Have you ever been interested in how ssh keys work? Or by how safe they are?

I use ssh every day many times - when I run it, git fetchor git pushwhen I deploy code or log in to the server. Not so long ago, I realized that for me ssh has become a magic that I used to use without understanding the principles of its work. I did not like it very much - I like to understand the tools that I use. So I did a little research and share the results with you.

In the course of presentation, there will be many abbreviations. They will not help to understand the ideas, but will be useful if you decide to google the details.

So, if you have ever resorted to key authentication, then you most likely have a file ~/.ssh/id_rsaor~/.ssh/id_dsain the home directory. This is a private (aka private) RSA / DSA key, ~/.ssh/id_rsa.pubor it ~/.ssh/id_dsa.pubis a public (aka public) key. The server on which you want to log in must have a copy of the public key in ~/.ssh/authorized_keys. When you try to log in, the ssh client confirms that you have a private key using a digital signature; the server checks that the signature is valid and ~/.ssh/authorized_keysthere is a public key, and you get access.

What is stored inside the private key?

Unencrypted Private Key Format


It is recommended to protect the private key with a password (passphrase), otherwise an attacker who managed to steal your private key from you will be able to log into your server without problems. First, let's take a look at the unencrypted file format, but we'll deal with the encrypted one later.

An unencrypted key looks something like this:

-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEArCQG213utzqE5YVjTVF5exGRCkE9OuM7LCp/FOuPdoHrFUXk
y2MQcwf29J3A4i8zxpES9RdSEU6iIEsow98wIi0x1/Lnfx6jG5Y0/iQsG1NRlNCC
aydGvGaC+PwwWiwYRc7PtBgV4KOAVXMZdMB5nFRaekQ1ksdH/360KCGgljPtzTNl
09e97QBwHFIZ3ea5Eih/HireTrRSnvF+ywmwuxX4ubDr0ZeSceuF2S5WLXH2+TV0
   ... и так далее ...
-----END RSA PRIVATE KEY-----

The private key contains data in ASN.1 format , presented as a sequence of bytes according to the X.690 standard and encoded in Base64. Roughly speaking, ASN.1 can be compared to JSON (it supports various data types, such as INTEGER, BOOLEAN, strings and sequences that can form a tree structure). ASN.1 is widespread in cryptography, although it has slightly gone out of fashion with the advent of the web (I don’t know why - it looks like a pretty decent format ( this can be argued - approx. Per. )) We will

generate a test RSA key without a password, using ssh-keygen, and decode it using asn1parse(or use an ASN.1 decoder written in JavaScript ):

$ ssh-keygen -t rsa -N '' -f test_rsa_key
$ openssl asn1parse -in test_rsa_key
    0:d=0  hl=4 l=1189 cons: SEQUENCE
    4:d=1  hl=2 l=   1 prim: INTEGER           :00
    7:d=1  hl=4 l= 257 prim: INTEGER           :C36EB2429D429C7768AD9D879F98C...
  268:d=1  hl=2 l=   3 prim: INTEGER           :010001
  273:d=1  hl=4 l= 257 prim: INTEGER           :A27759F60AEA1F4D1D56878901E27...
  534:d=1  hl=3 l= 129 prim: INTEGER           :F9D23EF31A387694F03AD0D050265...
  666:d=1  hl=3 l= 129 prim: INTEGER           :C84415C26A468934F1037F99B6D14...
  798:d=1  hl=3 l= 129 prim: INTEGER           :D0ACED4635B5CA5FB896F88BB9177...
  930:d=1  hl=3 l= 128 prim: INTEGER           :511810DF9AFD590E11126397310A6...
 1061:d=1  hl=3 l= 129 prim: INTEGER           :E3A296AE14E7CAF32F7E493FDF474...

The data structure in ASN.1 is quite simple: it is a sequence of nine integers. Their purpose is defined in RFC2313 . The first and third numbers are the version number (0) and the open exponent e . The second and fourth numbers (2048 bits long) are the module n and the secret exponent d . These numbers are RSA key parameters . The remaining five can be obtained by knowing n and d - they are cached in the file to speed up some operations.

The structure of DSA keys is similar and includes six numbers:

$ ssh-keygen -t dsa -N '' -f test_dsa_key
$ openssl asn1parse -in test_dsa_key
    0:d=0  hl=4 l= 444 cons: SEQUENCE
    4:d=1  hl=2 l=   1 prim: INTEGER           :00
    7:d=1  hl=3 l= 129 prim: INTEGER           :E497DFBFB5610906D18BCFB4C3CCD...
  139:d=1  hl=2 l=  21 prim: INTEGER           :CF2478A96A941FB440C38A86F22CF...
  162:d=1  hl=3 l= 129 prim: INTEGER           :83218C0CA49BA8F11BE40EE1A7C72...
  294:d=1  hl=3 l= 128 prim: INTEGER           :16953EA4012988E914B466B9C37CB...
  425:d=1  hl=2 l=  21 prim: INTEGER           :89A356E922688EDEB1D388258C825...

Password Protected Private Key Format


Now we complicate the life of a potential attacker who was able to steal the private key - we will protect it with a password. What happened to the file?

$ ssh-keygen -t rsa -N 'super secret passphrase' -f test_rsa_key
$ cat test_rsa_key
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,D54228DB5838E32589695E83A22595C7
3+Mz0A4wqbMuyzrvBIHx1HNc2ZUZU2cPPRagDc3M+rv+XnGJ6PpThbOeMawz4Cbu
lQX/Ahbx+UadJZOFrTx8aEWyZoI0ltBh9O5+ODov+vc25Hia3jtayE51McVWwSXg
wYeg2L6U7iZBk78yg+sIKFVijxiWnpA7W2dj2B9QV0X3ILQPxbU/cRAVTd7AVrKT
    ... и так далее ...
-----END RSA PRIVATE KEY-----

Note that two header lines have been added, and the result of decoding a Base64 string is no longer a valid ASN.1. The fact is that the structure of ASN.1. encrypted. From the headers we find out which algorithm was used for encryption: AES-128 in CBC mode. The 128-bit hexadecimal string in the header DEK-Infois the initialization vector (IV). There is nothing unusual here, all common cryptographic libraries can work with the algorithms used here.

But how is the AES key obtained from the password? I did not find this in the documentation and therefore was forced to understand the OpenSSL source code. Here's what I found out about getting the encryption key:

  1. The first 8 bytes of the initialization vector are appended to the password (in fact, they are salt).
  2. An MD5 hash is taken from the received string once.

To verify, we decrypt the private key, taking the initialization vector from the header DEK-Info:

$ tail -n +4 test_rsa_key | grep -v 'END ' | base64 -d | 
  openssl aes-128-cbc -d -iv D54228DB5838E32589695E83A22595C7 -K $(
    ruby -rdigest/md5 -e 'puts Digest::MD5.hexdigest(["super secret passphrase",0xD5,0x42,0x28,0xDB,0x58,0x38,0xE3,0x25].pack("a*cccccccc"))'
  ) |
  openssl asn1parse -inform DER

This command will output the RSA key parameters. If you want to just see the key, there is a simpler way:

$ openssl rsa -text -in test_rsa_key -passin 'pass:super secret passphrase'

But I wanted to show exactly how the AES key is obtained from the password in order to pay attention to two vulnerabilities:

  1. The use of MD5 is registered in the code, which means that without changing the format it is impossible to switch to another hash function (for example, SHA-1). If it turns out that MD5 is not secure enough, there will be problems. ( Actually no, see comments - approx. Per. )
  2. The hash function is applied only once. Since MD5 and AES are quick to compute, a short password is easy to guess.

If the ssh key falls into bad hands, for example, someone steals your laptop or hard drive with backups, an attacker will be able to try out a large number of passwords, even with little computing power. If you set a dictionary password, you can pick it up in seconds.

This is bad news: password protection of a key is not as good as one might have guessed. But there is good news: you can switch to a more secure private key format.

Enhancing key security using PKCS # 8


So, we need an algorithm to get a symmetric encryption key from a password, which would work slowly, so that an attacker needed more computational time to guess the password.

There are several standards with awkward names for ssh keys:

  • The PKCS # 5 (RFC 2898) defined algorithm PBKDF2 (Password-Based Key Derivation Function 2) obtaining the encryption key from the password by repeatedly applying the hash function. It also defines the PBES2 (Password-Based Encryption Scheme 2) encryption scheme, which includes the use of a key generated by PBKDF2 and a symmetric cipher.
  • The PKCS # 8 (RFC 5208) defined store encrypted private key with PBKDF2 format support. OpenSSL supports private keys in PKCS # 8 format, and OpenSSH uses OpenSSL, so if you use OpenSSH, you can switch from the traditional ssh key file format to PKCS # 8 format.

I do not know why ssh-keygenit still generates keys in the traditional format, despite the fact that for many years there have been better alternatives. It's not about compatibility with server software: private keys never leave your computer. Fortunately, existing keys are fairly easy to convert to PKCS # 8:

$ mv test_rsa_key test_rsa_key.old
$ openssl pkcs8 -topk8 -v2 des3 \
    -in test_rsa_key.old -passin 'pass:super secret passphrase' \
    -out test_rsa_key -passout 'pass:super secret passphrase'

If you try to use the new key file in PKCS # 8 format, you may find that everything works the same as before. Let's see what is now inside the file.

$ cat test_rsa_key
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIOu/S2/v547MCAggA
MBQGCCqGSIb3DQMHBAh4q+o4ELaHnwSCBMjA+ho9K816gN1h9MAof4stq0akPoO0
CNvXdtqLudIxBq0dNxX0AxvEW6exWxz45bUdLOjQ5miO6Bko0lFoNUrOeOo/Gq4H
dMyI7Ot1vL9UvZRqLNj51cj/7B/bmfa4msfJXeuFs8jMtDz9J19k6uuCLUGlJscP
    ... десу-десу ...
-----END ENCRYPTED PRIVATE KEY-----

Note that the first and last lines changed ( BEGIN ENCRYPTED PRIVATE KEYinstead BEGIN RSA PRIVATE KEY), and headers Proc-Typeand DEK-Infodisappeared. In fact, the file stores data in the same ASN.1 format:

$ openssl asn1parse -in test_rsa_key
    0:d=0  hl=4 l=1294 cons: SEQUENCE
    4:d=1  hl=2 l=  64 cons: SEQUENCE
    6:d=2  hl=2 l=   9 prim: OBJECT            :PBES2
   17:d=2  hl=2 l=  51 cons: SEQUENCE
   19:d=3  hl=2 l=  27 cons: SEQUENCE
   21:d=4  hl=2 l=   9 prim: OBJECT            :PBKDF2
   32:d=4  hl=2 l=  14 cons: SEQUENCE
   34:d=5  hl=2 l=   8 prim: OCTET STRING      [HEX DUMP]:3AEFD2DBFBF9E3B3
   44:d=5  hl=2 l=   2 prim: INTEGER           :0800
   48:d=3  hl=2 l=  20 cons: SEQUENCE
   50:d=4  hl=2 l=   8 prim: OBJECT            :des-ede3-cbc
   60:d=4  hl=2 l=   8 prim: OCTET STRING      [HEX DUMP]:78ABEA3810B6879F
   70:d=1  hl=4 l=1224 prim: OCTET STRING      [HEX DUMP]:C0FA1A3D2BCD7A80DD61F4C0287F8B2D...

We will use a JavaScript decoder to consider the structure of ASN.1:

Sequence (2 elements)
|- Sequence (2 elements)
|  |- Object identifier: 1.2.840.113549.1.5.13            // using PBES2 from PKCS#5
|  `- Sequence (2 elements)
|     |- Sequence (2 elements)
|     |  |- Object identifier: 1.2.840.113549.1.5.12      // using PBKDF2 — yay! :)
|     |  `- Sequence (2 elements)
|     |     |- Byte string (8 bytes): 3AEFD2DBFBF9E3B3    // salt
|     |     `- Integer: 2048                              // iteration count
|     `- Sequence (2 elements)
|          Object identifier: 1.2.840.113549.3.7          // encrypted with Triple DES, CBC
|          Byte string (8 bytes): 78ABEA3810B6879F        // initialization vector
`- Byte string (1224 bytes): C0FA1A3D2BCD7A80DD61F4C0287F8B2DAB46A43E...  // encrypted key blob

Mentioned here are OID (Object identifier) ​​- globally unique digital identifiers. From them we learn that the encryption scheme pkcs5PBES2 , the key retrieval function PBKDF2 and the encryption algorithm des-ede3-cbc are used . The hash function is not explicitly specified, which means that hMAC-SHA1 is used by default . Storing the OID in a file is good because the keys can be updated without changing the container format (if, for example, a better encryption algorithm is invented).



We also see that in the process of obtaining the encryption key, 2048 iterations are performed. This is much better than a one-time use of the hash function when using the traditional ssh-key format - enumerating passwords will take longer. Currently, the number of iterations is registered in the OpenSSL code, I hope it will be possible to configure it in the future.

Conclusion


If you set a complex password for the private key, then converting it from the traditional format to PKCS # 8 can be compared with increasing the password length by a couple of characters. If you use a weak password, PKCS # 8 will make its selection much more difficult.

Changing the key format is very simple:

$ mv ~/.ssh/id_rsa ~/.ssh/id_rsa.old
$ openssl pkcs8 -topk8 -v2 des3 -in ~/.ssh/id_rsa.old -out ~/.ssh/id_rsa
$ chmod 600 ~/.ssh/id_rsa
# проверьте, что новый ключ работает. если работает, старый можно удалить
$ rm ~/.ssh/id_rsa.old

The command openssl pkcs8asks for a password three times: once to unlock an existing key and two times when creating a new key file. You can come up with a new password or use the old one, it does not matter.

Not all software can read PKCS # 8 format, but there is nothing to worry about - only an ssh client needs access to the private ssh key. From the server point of view, storing the private key in a different format does not change anything at all.

The translator will be happy to hear comments and constructive criticism.

Also popular now: