Повышаем безопасность закрытых ssh-ключей

Original author: Martin Kleppmann
  • Translation
Вы когда-нибудь интересовались механизмом работы ssh-ключей? Или тем, насколько безопасно они хранятся?

Я использую ssh каждый день много раз — когда запускаю git fetch или git push, когда развертываю код или логинюсь на сервере. Не так давно я осознал, что для меня ssh стал магией, которой я привык пользоваться без понимация принципов ее работы. Мне это не сильно понравилось — я люблю разбираться в инструментах, которые использую. Поэтому я провел небольшое исследование и делюсь с вами результатами.

По ходу изложения встретится много аббревиатур. Они не помогут понять идеи, но будут полезны в том случае, если вы решите погуглить подробности.

Итак, если вам доводилось прибегать к аутентификации по ключу, то у вас, скорее всего, есть файл ~/.ssh/id_rsa или ~/.ssh/id_dsa в домашнем каталоге. Это закрытый (он же приватный) RSA/DSA ключ, а ~/.ssh/id_rsa.pub или ~/.ssh/id_dsa.pub — открытый (он же публичный) ключ. На сервере, на котором вы хотите залогиниться, должна быть копия открытого ключа в ~/.ssh/authorized_keys. Когда вы пытаетесь залогиниться, ssh-клиент подтвержает, что у вас есть закрытый ключ, используя цифровую подпись; сервер проверяет, что подпись действительна и в ~/.ssh/authorized_keys есть открытый ключ, и вы получаете доступ.

Что же хранится внутри закрытого ключа?

Незашифрованный формат закрытого ключа


Закрытый ключ рекомендуется защищать паролем (парольной фразой), иначе злоумышленник, которому удалось украсть у вас закрытый ключ, сможет без проблем залогиниться на вашем сервере. Сначала взглянем на незашифрованный формат файла, а с зашифрованным разберемся позже.

Незашифрованный ключ выглядит примерно так:

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

Закрытый ключ содержит данные в формате ASN.1, представленные в виде последовательности байт согласно стандарту X.690 и закодированные в Base64. Грубо говоря, ASN.1 можно сравнить с JSON (он поддерживает различные типы данных, такие как INTEGER, BOOLEAN, строки и последовательности, которые могут формировать древовидную структуру). ASN.1 широко распространен в криптографии, хотя слегка вышел из моды с пришествием веба (я не знаю почему — он выглядит как вполне достойный формат (с этим можно поспорить — прим. пер.))

Сгенерируем тестовый RSA-ключ без пароля, используя ssh-keygen, и декодируем его с помощью asn1parse (или воспользуемся написанным на JavaScript ASN.1-декодером):

$ 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...

Структура данных в ASN.1 довольно проста: это последовательность из девяти целых чисел. Их назначение определено в RFC2313. Первое и третье числа — это номер версии (0) и открытая экспонента e. Второе и четвертое числа (длиной 2048 бит) — это модуль n и секретная экспонента d. Эти числа являются параметрами ключа RSA. Остальные пять можно получить, зная n и d — они кэшированы в файле для ускорения некоторых операций.

Структура DSA-ключей похожа и включает шесть чисел:

$ 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...

Формат закрытого ключа, защищенного паролем


Теперь усложним жизнь потенциальному злоумышленнику, который смог украсть закрытый ключ — защитим его паролем. Что произошло с файлом?

$ 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-----

Заметим, что добавились две строки с заголовками, а результат декодирования Base64-строки больше не является валидным ASN.1. Дело в том, что структура ASN.1. зашифрована. Из заголовков узнаем, какой алгоритм использовался для шифрования: AES-128 в режиме CBC. 128-битная шестнадцатеричная строка в заголовке DEK-Info — это вектор инициализации (IV). Ничего необычного здесь нет, все распространенные криптографические библиотеки умеют работать с используемыми здесь алгоритмами.

Но как из пароля получается ключ AES? Я не нашел этого в документации и поэтому был вынужден разбираться в исходниках OpenSSL. Вот что я выяснил насчет получения ключа шифрования:

  1. Первые 8 байт вектора инициализации дописываются к паролю (по сути, являются солью).
  2. От полученной строки один раз берется MD5-хеш.

Для проверки расшифруем закрытый ключ, взяв вектор инициализации из заголовка 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

Эта команда выведет параметры ключа RSA. Если вы хотите просто увидеть ключ, есть способ и проще:

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

Но я хотел показать, как именно ключ AES получается из пароля, чтобы обратить внимание на два уязвимых места:

  1. Использование MD5 прописано в коде, а это значит, что без изменения формата невозможно перейти на другую хеш-функцию (например, SHA-1). Если выяснится, что MD5 недостаточно безопасен, будут проблемы. (На самом деле нет, см. комментарии — прим. пер.)
  2. Хеш-функция применяется только один раз. Так как MD5 и AES быстро вычисляемы, короткий пароль легко подобрать перебором.

Если ssh-ключ попадет в недобрые руки, например, кто-нибудь украдет ваш ноутбук или жеский диск с бэкапами, злоумышленник сможет перебрать большое количество паролей, даже обладая небольшой вычислительной мощностью. Если вы установили словарный пароль, его можно подобрать за секунды.

Это плохая новость: защита ключа паролем не так хороша, как можно было предположить. Но есть и хорошая новость: вы можете перейти на более надежный формат закрытого ключа.

Повышаем защиту ключа с использованием PKCS#8


Итак, нам нужен алгоритм получения симметричного ключа шифрования из пароля, который работал бы медленно, чтобы злоумышленнику потребовалось больше вычислительного времени для подбора пароля.

Для ssh-ключей существует несколько стандартов с неуклюжими названиями:

  • В PKCS #5 (RFC 2898) определен алгоритм PBKDF2 (Password-Based Key Derivation Function 2) получения ключа шифрования из пароля путем многократного применения хеш-функции. Там же определена схема шифрования PBES2 (Password-Based Encryption Scheme 2), которая включает использование ключа, сгенерированного по PBKDF2, и симметричного шифра.
  • В PKCS #8 (RFC 5208) определен формат хранения зашифрованных закрытых ключей с поддержкой PBKDF2. OpenSSL поддерживает закрытые ключи в формате PKCS#8, а OpenSSH использует OpenSSL, так что если вы пользуетесь OpenSSH, то можете переключиться с традиционного формата файлов ssh-ключей на формат PKCS#8.

Я не знаю, почему ssh-keygen до сих пор генерирует ключи в традиционном формате, несмотря на то, что уже много лет существуют лучшие альтернативы. Дело не в совместимости с серверным софтом: закрытые ключи никогда не покидают пределы вашего компьютера. К счастью, существующие ключи достаточно легко преобразовать в формат 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'

Если попробовать использовать новый файл ключа в формате PKCS#8, можно обнаружить, что все работает так же как и раньше. Посмотрим, что теперь находится внутри файла.

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

Обратите внимание на то, что первая и последняя строки изменились (BEGIN ENCRYPTED PRIVATE KEY вместо BEGIN RSA PRIVATE KEY), а заголовки Proc-Type и DEK-Info исчезли. Фактически, в файле хранятся данные во все том же формате ASN.1:

$ 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...

Воспользуемся JavaScript-декодером, чтобы рассмотреть структуру 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) — глобально-уникальные цифровые идентификаторы. По ним мы узнаем, что используется схема шифрования pkcs5PBES2, функция получения ключа PBKDF2 и алгоритм шифрования des-ede3-cbc. Функция хеширования не указана явно, значит, по умолчанию используется hMAC-SHA1.

Хранение OID в файле хорошо тем, что ключи можно обновить без смены формата контейнера (если, например, будет изобретен лучший алгоритм шифрования).

Также мы видим, что в ходе получения ключа шифрования выполняется 2048 итераций. Это гораздо лучше, чем однократное применение хеш-функции при использовании традиционного формата ssh-ключей — перебор паролей потребует больше времени. В настоящий момент количество итераций прописано в коде OpenSSL, я надеюсь, в будущем его можно будет настраивать.

Заключение


Если вы установили сложный пароль на закрытый ключ, то преобразование его из традиционного формата в PKCS#8 можно сравнить с увеличением длины пароля на пару символов. Если же вы используете слабый пароль, PKCS#8 сделает его подбор заметно сложнее.

Поменять формат ключей очень просто:

$ 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

Команда openssl pkcs8 запрашивает пароль три раза: один раз для разблокировки существующего ключа и два раза при создании нового файла ключа. Вы можете придумать новый пароль или использовать старый, это не имеет никакого значения.

Не весь софт может читать формат PKCS#8, но в этом нет ничего страшного — доступ к закрытому ssh-ключу нужен только ssh-клиенту. С точки зрения сервера, хранение закрытого ключа в другом формате вообще ничего не меняет.

Переводчик будет рад услышать замечания и конструктивную критику.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 22

    +6
    Кстати, небольшой оффтоп:
    лично у меня файл с закрытым ключом отсутствует вообще:
    mva@note ~ % ls ~/.ssh
    authorized_keys config known_hosts
    mva@note ~ % ssh router
    router ~ %

    Секрет в использовании GPG-ключа сгенерённого ещё не релизнутой (и хз когда релизнущейся) версией 2.1 (ибо текущая пока не умеет делать то, что надо) + gpg-agent'а в режиме эмуляции ssh-агента.

    Правда, после Вашей статьи-перевода я задумался о том, безопасней ли это на самом деле, или не особо… :)
      0
      И ssh-copy-id у вас работает? Очень интересный способ, надо что-то поподробнее почитать.
        0
        да, работает (а уведомление о Вашем комменте я, почему-то, не получил на почту. прошу прощения) :)
        Да и публичный ключ можно посмотреть с помощью ssh-add -L ;)
          0
          Тут секрет прост. PGP ключ использует RSA/DSA пару для подписей. Собственно закрытый и открытый ключи RSA/DSA, которые используются SSH'ем является частью PGP ключа.
          Интересен gpg-agent'а, предоставляющий ключи для SSH по надобности.
            0
            тут скорее наоборот: сгенерировать нужный закрытый PGP-ключ может только не релизнутая (и, судя по всему, не скоро релизнущаяся) версия gnupg. В то время, как даже текущая версия gpg-agent'a из коробки того же gnupg текущей версии спокойно подхватывает нужный gpg-ключ и добавляет его в псево-ssh-агент если агент запущен с --enable-ssh-support.

            У меня, например:
            mva PID 0.0 0.0 16948 1568? Ss июл14 0:10 /usr/bin/gpg-agent --daemon --write-env-file /home/mva/.gpg-agent-info --enable-ssh-support --no-use-standard-socket --allow-mark-trusted


            (А если быть точнее, то в /etc/kde/startup/agent-startup.sh)
            if [[ -z $(pgrep gpg-agent -U $UID) ]]; then
                    if [[ -x /usr/bin/gpg-agent ]]; then
                            eval "$(/usr/bin/gpg-agent --daemon --write-env-file ${HOME}/.gpg-agent-info --enable-ssh-support --no-use-standard-socket --allow-mark-trusted)"
                    fi;
            else
                    if [[ -f "${HOME}/.gpg-agent-info" ]]; then
                            source "${HOME}/.gpg-agent-info"
                            export GPG_AGENT_INFO
                            export SSH_AUTH_SOCK
                            export SSH_AGENT_PID
                            ln -sf "${GPG_AGENT_INFO/:*/}" "${HOME}/.gnupg/S.gpg-agent"
                            GPG_TTY=${TTY}
                            export GPG_TTY
                    fi
            fi
            

      0
      Спасибо за статью, весьма неожиданно с паролем получилось.

      На всякий случай напомню пользователям ssh-ключей, что не стоит пренебрегать возможностью ограничивать варианты использования ключа настройкой публичной части (секция AUTHORIZED_KEYS FILE FORMAT в man sshd), в частности, крайне рекомендую указывать в опции from="" список хостов, с которых разрешено логиниться данному ключу, например
      from="1.2.3.4,5.6.7.8" ssh-dss AAAAB3NzaC...bla-bla...WWng==
      Если ключ только для git (или других очень узких целей), то можно и список разрешенных команд настроить.
        +3
        Весьма сомнительная рекомендация, по-моему. Это нужно быть на 100% уверенным, что никогда не понадобится зайти не со своего компа и что провайдер адрес не поменяет.
          0
          Я же не говорил, что пароли отменяются )
          +3
          Ограничение по хостам почти полностью нивелирует преимущества от использования персональных ключей. Те ключи, по которым одни сервера общаются с другими — другое дело.
          0
          А еще openssh уже достаточно давно умеет ecdsa, который афаик надежнее rsa. Кстати, а что надежнее dsa или rsa?
            0
              0
              по поводу «более надёжный» можно ещё и поспорить. Ибо это как сравнивать самолёт и вертолёт. Они просто разные. Конечно, люди проводят аналогии в сравнениях, но самое главное, что мне однажды удалось нарыть:

              It all comes down to key size. A 384 bit elliptical curve key is equivalent to a 7680 bit RSA key in terms of security, and at some point (I don't know exactly where), ECC starts outperforming RSA for the same level of security.

              The advantage to RSA is simplicity. RSA can encrypt arbitrary data, so it can be used directly for both signing and key exchange, and can even encrypt data directly.
              ECC cannot be used to encrypt data directly, and requires separate algorithms to derive keys and generate/verify signatures.

              The biggest problem with ECC is it's tangled in a web of patents, which makes implementing it risky unless you really want to spend a lot of time reading patents.

              Прошу обратить особое внимание на последнее предложение и вчитаться в то, что посередине.
                +1
                У DSA проблема в другом — атака на рандом и вскрытие ключа таким образом.
                  0
                  ЧТо такое ECC в данном контексте? Если ECDSA, то знаю, что это самый надежный из алгоритмов в линейке: RSA, DSA, ECDSA. Просто у меня телефон и планшет его не умеют. Поэтому выбираю между RSA и простым DSA.
                    +2
                    ECC — вероятно, elliptic curve cryptography, как и ecdsa — dsa на эллиптических кривых
                      0
                      и таки, повторюсь, я бы воздержался от высказываний а-ля «знаю, что самый надёжный». И опять повторюсь, вы сравниваете не очень сравниваемые вещи. Прочитайте, пожалуйста, оригинал статьи.

                      Да, «матиматика эллиптических кривых» в криптографии показывает лучше результаты, чем теория простых чисел. Но сравнивать ECС и RSA как нечто одинаковое — не очень правильно.

                      К слову, да, libssh пока что не очень хорошо умеет в ecdsa в текущих релизнутых версиях (в ещё не релизнутой 0.6 уже добавили). Как следствие — куча софта для работы с тем же sftp колбасится при виде соответствующего ключа.
                  0
                  Круто конечно, но у меня в Ubuntu Seahorse не понимает, что это именно SSH ключ.
                    0
                    Пардон, после того, как удалил и снова импортировал ключ через интерфейс Seahorse всё заработало.
                    +2
                    Познавательно, спасибо.
                    Однако вот это:
                    Если выяснится, что MD5 недостаточно безопасен, будут проблемы.

                    неверно (если я правильно понял алгоритм, конечно).

                    В данном случае хэш-функция используется только для того, чтобы сделать ключ шифрования более «псевдослучайным» — при этом злоумышленник даже в случае компрометации MD5 ничего сделать не может, ибо не видит результат хэширования. В случае применения вместо AES идеальной PRP хэширование можно вообще не применять с тем же результатом.
                      +1
                      эта конкретно строчка да, ни ап чом
                      а вот проблема с AES есть — нам надо только один первый раунд. так как мы знаем, что после расшифровки будет ASN.1 — мы можем даже атаку на известный открытый текст проводить.
                      –2
                      Неделя криптобезопасности на хабре. Прирост SSH-ключей увеличился вдвое.
                        +1
                        Тем, кто так же попадает из поисковой системы сюда, то в 2016 имеются улучшения для повышения безопасности хранения ssh-ключей:

                        • Вместо des3 можно использовать aes-256-cfb или aes-256-cbc:
                          openssl pkcs8 -in id_rsa -topk8 -v2 aes-256-cfb -out id_rsa_cfb
                          

                        • С помощью ssh-keygen приватный ключ можно хранить в новом формате [-o], который позволяет указывать количество итераций хэширования [-a]. Ключ, полученный в следующем примере, на Intel i5 проверяется при подключении на удаленный сервер около 10 секунд (я в свое окно укладываюсь, но можно и снизить паранойю)
                          ssh-keygen -o -a 2048 -t rsa -b 8192
                          # новый формат хранения для имеющегося ключа (перед этим сделать его копию):
                          ssh-keygen -o -a 2048 -p -f id_rsa
                          

                          Что внутри ключа:
                          -----BEGIN OPENSSH PRIVATE KEY-----
                          b3BlbnNzaC1rZXktdjEAAA....
                          ....bla-bla....
                          -----END OPENSSH PRIVATE KEY-----
                          


                        Однако ни один из способов не работает при использовании PuTTY, да и вообще на виндовых клиентах, которые пробовал. На Debian 8.6 Stable все ок.

                        Only users with full accounts can post comments. Log in, please.