Вы когда-нибудь интересовались механизмом работы ssh-ключей? Или тем, насколько безопасно они хранятся?
Я использую ssh каждый день много раз — когда запускаю
По ходу изложения встретится много аббревиатур. Они не помогут понять идеи, но будут полезны в том случае, если вы решите погуглить подробности.
Итак, если вам доводилось прибегать к аутентификации по ключу, то у вас, скорее всего, есть файл
Что же хранится внутри закрытого ключа?
Закрытый ключ рекомендуется защищать паролем (парольной фразой), иначе злоумышленник, которому удалось украсть у вас закрытый ключ, сможет без проблем залогиниться на вашем сервере. Сначала взглянем на незашифрованный формат файла, а с зашифрованным разберемся позже.
Незашифрованный ключ выглядит примерно так:
Закрытый ключ содержит данные в формате ASN.1, представленные в виде последовательности байт согласно стандарту X.690 и закодированные в Base64. Грубо говоря, ASN.1 можно сравнить с JSON (он поддерживает различные типы данных, такие как INTEGER, BOOLEAN, строки и последовательности, которые могут формировать древовидную структуру). ASN.1 широко распространен в криптографии, хотя слегка вышел из моды с пришествием веба (я не знаю почему — он выглядит как вполне достойный формат (с этим можно поспорить — прим. пер.))
Сгенерируем тестовый RSA-ключ без пароля, используя
Структура данных в ASN.1 довольно проста: это последовательность из девяти целых чисел. Их назначение определено в RFC2313. Первое и третье числа — это номер версии (0) и открытая экспонента e. Второе и четвертое числа (длиной 2048 бит) — это модуль n и секретная экспонента d. Эти числа являются параметрами ключа RSA. Остальные пять можно получить, зная n и d — они кэшированы в файле для ускорения некоторых операций.
Структура DSA-ключей похожа и включает шесть чисел:
Теперь усложним жизнь потенциальному злоумышленнику, который смог украсть закрытый ключ — защитим его паролем. Что произошло с файлом?
Заметим, что добавились две строки с заголовками, а результат декодирования Base64-строки больше не является валидным ASN.1. Дело в том, что структура ASN.1. зашифрована. Из заголовков узнаем, какой алгоритм использовался для шифрования: AES-128 в режиме CBC. 128-битная шестнадцатеричная строка в заголовке
Но как из пароля получается ключ AES? Я не нашел этого в документации и поэтому был вынужден разбираться в исходниках OpenSSL. Вот что я выяснил насчет получения ключа шифрования:
Для проверки расшифруем закрытый ключ, взяв вектор инициализации из заголовка
Эта команда выведет параметры ключа RSA. Если вы хотите просто увидеть ключ, есть способ и проще:
Но я хотел показать, как именно ключ AES получается из пароля, чтобы обратить внимание на два уязвимых места:
Если ssh-ключ попадет в недобрые руки, например, кто-нибудь украдет ваш ноутбук или жеский диск с бэкапами, злоумышленник сможет перебрать большое количество паролей, даже обладая небольшой вычислительной мощностью. Если вы установили словарный пароль, его можно подобрать за секунды.
Это плохая новость: защита ключа паролем не так хороша, как можно было предположить. Но есть и хорошая новость: вы можете перейти на более надежный формат закрытого ключа.
Итак, нам нужен алгоритм получения симметричного ключа шифрования из пароля, который работал бы медленно, чтобы злоумышленнику потребовалось больше вычислительного времени для подбора пароля.
Для ssh-ключей существует несколько стандартов с неуклюжими названиями:
Я не знаю, почему
Если попробовать использовать новый файл ключа в формате PKCS#8, можно обнаружить, что все работает так же как и раньше. Посмотрим, что теперь находится внутри файла.
Обратите внимание на то, что первая и последняя строки изменились (
Воспользуемся JavaScript-декодером, чтобы рассмотреть структуру ASN.1:
Здесь упоминаются OID (Object identifier) — глобально-уникальные цифровые идентификаторы. По ним мы узнаем, что используется схема шифрования pkcs5PBES2, функция получения ключа PBKDF2 и алгоритм шифрования des-ede3-cbc. Функция хеширования не указана явно, значит, по умолчаниюиспользуется hMAC-SHA1.
Хранение OID в файле хорошо тем, что ключи можно обновить без смены формата контейнера (если, например, будет изобретен лучший алгоритм шифрования).
Также мы видим, что в ходе получения ключа шифрования выполняется 2048 итераций. Это гораздо лучше, чем однократное применение хеш-функции при использовании традиционного формата ssh-ключей — перебор паролей потребует больше времени. В настоящий момент количество итераций прописано в коде OpenSSL, я надеюсь, в будущем его можно будет настраивать.
Если вы установили сложный пароль на закрытый ключ, то преобразование его из традиционного формата в PKCS#8 можно сравнить с увеличением длины пароля на пару символов. Если же вы используете слабый пароль, PKCS#8 сделает его подбор заметно сложнее.
Поменять формат ключей очень просто:
Команда
Не весь софт может читать формат PKCS#8, но в этом нет ничего страшного — доступ к закрытому ssh-ключу нужен только 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
есть открытый ключ, и вы получаете доступ.Что же хранится внутри закрытого ключа?
Незашифрованный формат закрытого ключа
Закрытый ключ рекомендуется защищать паролем (парольной фразой), иначе злоумышленник, которому удалось украсть у вас закрытый ключ, сможет без проблем залогиниться на вашем сервере. Сначала взглянем на незашифрованный формат файла, а с зашифрованным разберемся позже.
Незашифрованный ключ выглядит примерно так:
Закрытый ключ содержит данные в формате ASN.1, представленные в виде последовательности байт согласно стандарту X.690 и закодированные в Base64. Грубо говоря, ASN.1 можно сравнить с JSON (он поддерживает различные типы данных, такие как INTEGER, BOOLEAN, строки и последовательности, которые могут формировать древовидную структуру). ASN.1 широко распространен в криптографии, хотя слегка вышел из моды с пришествием веба (я не знаю почему — он выглядит как вполне достойный формат (с этим можно поспорить — прим. пер.))
Сгенерируем тестовый RSA-ключ без пароля, используя
ssh-keygen
, и декодируем его с помощью asn1parse
(или воспользуемся написанным на JavaScript ASN.1-декодером):Структура данных в ASN.1 довольно проста: это последовательность из девяти целых чисел. Их назначение определено в RFC2313. Первое и третье числа — это номер версии (0) и открытая экспонента e. Второе и четвертое числа (длиной 2048 бит) — это модуль n и секретная экспонента d. Эти числа являются параметрами ключа RSA. Остальные пять можно получить, зная n и d — они кэшированы в файле для ускорения некоторых операций.
Структура DSA-ключей похожа и включает шесть чисел:
Формат закрытого ключа, защищенного паролем
Теперь усложним жизнь потенциальному злоумышленнику, который смог украсть закрытый ключ — защитим его паролем. Что произошло с файлом?
Заметим, что добавились две строки с заголовками, а результат декодирования Base64-строки больше не является валидным ASN.1. Дело в том, что структура ASN.1. зашифрована. Из заголовков узнаем, какой алгоритм использовался для шифрования: AES-128 в режиме CBC. 128-битная шестнадцатеричная строка в заголовке
DEK-Info
— это вектор инициализации (IV). Ничего необычного здесь нет, все распространенные криптографические библиотеки умеют работать с используемыми здесь алгоритмами.Но как из пароля получается ключ AES? Я не нашел этого в документации и поэтому был вынужден разбираться в исходниках OpenSSL. Вот что я выяснил насчет получения ключа шифрования:
- Первые 8 байт вектора инициализации дописываются к паролю (по сути, являются солью).
- От полученной строки один раз берется MD5-хеш.
Для проверки расшифруем закрытый ключ, взяв вектор инициализации из заголовка
DEK-Info
:Эта команда выведет параметры ключа RSA. Если вы хотите просто увидеть ключ, есть способ и проще:
Но я хотел показать, как именно ключ AES получается из пароля, чтобы обратить внимание на два уязвимых места:
- Использование MD5 прописано в коде, а это значит, что без изменения формата невозможно перейти на другую хеш-функцию (например, SHA-1). Если выяснится, что MD5 недостаточно безопасен, будут проблемы. (На самом деле нет, см. комментарии — прим. пер.)
- Хеш-функция применяется только один раз. Так как 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:Если попробовать использовать новый файл ключа в формате PKCS#8, можно обнаружить, что все работает так же как и раньше. Посмотрим, что теперь находится внутри файла.
Обратите внимание на то, что первая и последняя строки изменились (
BEGIN ENCRYPTED PRIVATE KEY
вместо BEGIN RSA PRIVATE KEY
), а заголовки Proc-Type
и DEK-Info
исчезли. Фактически, в файле хранятся данные во все том же формате ASN.1:Воспользуемся JavaScript-декодером, чтобы рассмотреть структуру ASN.1:
Здесь упоминаются OID (Object identifier) — глобально-уникальные цифровые идентификаторы. По ним мы узнаем, что используется схема шифрования pkcs5PBES2, функция получения ключа PBKDF2 и алгоритм шифрования des-ede3-cbc. Функция хеширования не указана явно, значит, по умолчаниюиспользуется hMAC-SHA1.
Хранение OID в файле хорошо тем, что ключи можно обновить без смены формата контейнера (если, например, будет изобретен лучший алгоритм шифрования).
Также мы видим, что в ходе получения ключа шифрования выполняется 2048 итераций. Это гораздо лучше, чем однократное применение хеш-функции при использовании традиционного формата ssh-ключей — перебор паролей потребует больше времени. В настоящий момент количество итераций прописано в коде OpenSSL, я надеюсь, в будущем его можно будет настраивать.
Заключение
Если вы установили сложный пароль на закрытый ключ, то преобразование его из традиционного формата в PKCS#8 можно сравнить с увеличением длины пароля на пару символов. Если же вы используете слабый пароль, PKCS#8 сделает его подбор заметно сложнее.
Поменять формат ключей очень просто:
Команда
openssl pkcs8
запрашивает пароль три раза: один раз для разблокировки существующего ключа и два раза при создании нового файла ключа. Вы можете придумать новый пароль или использовать старый, это не имеет никакого значения.Не весь софт может читать формат PKCS#8, но в этом нет ничего страшного — доступ к закрытому ssh-ключу нужен только ssh-клиенту. С точки зрения сервера, хранение закрытого ключа в другом формате вообще ничего не меняет.
Комментариев нет:
Отправить комментарий