Have you ever been interested in the mechanism of ssh-keys? Or how safe they are?
I use ssh every day many times - when I run
git fetch
or
git push
, when I deploy code or log in to the server. Not so long ago, I realized that for me ssh became the magic that I used to use without understanding the principles of its work. I didn’t like it a lot - I love to understand the tools I use. Therefore, I did a little research and share with you the results.
In the course of the presentation will meet many abbreviations. They do not help to understand the ideas, but will be useful if you decide to google the details.
')
So, if you had to resort to key authentication, then you most likely have a file
~/.ssh/id_rsa
or
~/.ssh/id_dsa
in your home directory. This is a private (aka private) RSA / DSA key, and
~/.ssh/id_rsa.pub
or
~/.ssh/id_dsa.pub
is an open (or public) key. On the server on which you want to log in, there should be 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 verifies that the signature is valid and there is a public key in
~/.ssh/authorized_keys
, and you are gaining 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 the private key from you will be able to log in to your server without any problems. First, take a look at the unencrypted file format, and deal with the encrypted file later.
The unencrypted key looks 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 the
ASN.1 format, represented 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 widely used in cryptography, although it is slightly out of fashion with the advent of the web (I don’t know why - it looks like quite a decent format (
you can argue with that )
. )
Generate a test RSA key without a password using
ssh-keygen
and decode it with
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 fairly 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 modulus
n and the secret exponent
d . These numbers are
RSA key parameters. The remaining five can be obtained, 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 let's make life difficult for a potential intruder who could steal a private key - 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 strings have been added with headers, and the result of decoding the Base64 strings is no longer 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
DEK-Info
header is the initialization vector (IV). There is nothing unusual here; all common cryptographic libraries can work with the algorithms used here.
But how does the AES key come from the password? I did not find this in the documentation and therefore I was forced to understand the source code of OpenSSL. Here is what I found out about getting the encryption key:
- The first 8 bytes of the initialization vector are added to the password (in fact, they are salt).
- From the received string, the MD5 hash is taken once.
To verify, decrypt the private key by taking the initialization vector from the
DEK-Info
header:
$ 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 just want to 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:
- Using MD5 is written 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 safe enough, there will be problems. ( Actually, no, see comments - approx. Lane. )
- The hash function is applied only once. Since MD5 and AES are quickly calculated, it is easy to select a short password by searching.
If the ssh-key falls into unkind hands, for example, someone steals your laptop or hard drive with backups, the attacker will be able to sort through a large number of passwords, even with a small computing power. If you set a vocabulary password, you can pick it up in seconds.
This is bad news: password protection is not as good as you might expect. But there is good news: you can switch to a more reliable private key format.
Enhance Key Protection with PKCS # 8
So, we need an algorithm for obtaining a symmetric encryption key from a password that would work slowly so that the attacker took more computational time to find the password.
For ssh-keys, there are several standards with awkward names:
- In PKCS # 5 (RFC 2898) , the PBKDF2 (Password-Based Key Derivation Function 2) algorithm is defined to derive an encryption key from a password by repeatedly using a hash function. The PBES2 encryption scheme (Password-Based Encryption Scheme 2) is also defined there, which includes the use of a key generated by PBKDF2 and a symmetric cipher.
- PKCS # 8 (RFC 5208) defines the storage format for encrypted private keys with PBKDF2 support. OpenSSL supports private keys in the PKCS # 8 format, and OpenSSH uses OpenSSL, so if you use OpenSSH, you can switch from the traditional ssh key file format to the PKCS # 8 format.
I do not know why
ssh-keygen
still generates keys in the traditional format, despite the fact that for many years there are better alternatives. It's not about compatibility with server software: private keys never leave your computer. Fortunately, the existing keys are fairly easy to convert to the PKCS # 8 format:
$ 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 a new key file in the 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-----
Notice that the first and last lines have changed (
BEGIN ENCRYPTED PRIVATE KEY
instead of
BEGIN RSA PRIVATE KEY
), and the
Proc-Type
and
DEK-Info
headings have disappeared. 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...
Let's use a
JavaScript decoder to look at 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
OID (Object identifier) ​​- globally unique digital identifiers are mentioned here. From them, we learn that the
pkcs5PBES2 encryption
scheme , the
PBKDF2 key obtaining function, and the
des-ede3-cbc encryption algorithm are used. The hashing function is not explicitly specified, which means that
the default is
hMAC-SHA1 .
Storing the OID in the file is good because the keys can be updated without changing the container format (if, for example, the best encryption algorithm is invented).
We also see that in the course of obtaining the encryption key, 2048 iterations are performed. This is much better than a single use of the hash function when using the traditional format of ssh-keys - brute force will take more time. At the moment, the number of iterations is written in the OpenSSL code, I hope it can be customized in the future.
Conclusion
If you set a complex password to a private key, then converting it from the traditional format to PKCS # 8 can be compared with increasing the length of the password 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
The
openssl pkcs8
prompts for a password three times: once to unlock an existing key and twice 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 the PKCS # 8 format, but there’s nothing to worry about - only the ssh client needs access to the private ssh-key. From the server’s point of view, storing a private key in a different format does not change anything at all.
The translator will be happy to hear comments and constructive criticism.