В далекие времена, до фейсбука и гугла, когда 32 мегабайта RAM было дофига как много, security была тоже… немножко наивной. Вирусы выдвигали лоток CD-ROM-а и играли Янки Дудль. Статья «Smashing the stack for fun and profit» уже была задумана, но еще не написана. Все пользовались telnet и ftp, и только особо продвинутые параноики знали про ssh.
Вот примерно в это время, плюс-минус год, родился MySQL и были в нем юзеры, которых надо было не пускать к чужим данным, но пускать к своим.
Michael Widenius (или просто Monty) явно был знаком с параноидальными безопасниками не понаслышке, чего стоит один такой момент (из исходников, global.h
):
/* Paranoid settings. Define I_AM_PARANOID if you are paranoid */
#ifdef I_AM_PARANOID
#define DONT_ALLOW_USER_CHANGE 1
#define DONT_USE_MYSQL_PWD 1
#endif
Так что неудивительно, что пароли в MySQL открытым текстом не передавались никогда. Передавались случайные строки на основе хешей. А конкретно, первый протокол аутентификации (цитируется по mysql-3.20, 1996) работал так:
- Сервер хранил хеш от пароля. Впрочем хеш был совсем простенький, примерно вот такой:
for (; *password ; password++) { tmp1 = *password; hash ^= (((hash & 63) + tmp2) * tmp1) + (hash << 8); tmp2 += tmp1; }
Это, на минуточку, 32 бита всего.
- Для аутентификации сервер слал клиенту случайную строку из восьми букв.
- Клиент считал хеш (тот что выше) этой строки и хеш же пароля. Потом XOR-ом этих двух получившихся 32-битных чисел инициализировал генератор случайных чисел, генерировал восемь «случайных» байт и отсылал их серверу.
- Сервер, в свою очередь, брал хеш пароля (который он знал) и XOR-ил его с хешем той случайной строки (которую он сам же и сгенерировал, то есть ее он тоже знал). Ну и запускал генератор случайных чисел на своей стороне и сравнивал с тем, что клиент прислал.
Плюсы этого решения были очевидны. Пароль никогда не пересылался в открытом виде. И не хранился в открытом виде тоже. Но, право, 32 бита на хеш — это несерьезно даже в 1996. Поэтому уже в следующем мажорном релизе (mysql-3.21) хеш был 64-битный. И в таком виде под именем «old mysql authentication» этот протокол живет и сейчас. Из MySQL-5.7 его выпилили, но в 5.6 он еще был, а в MariaDB есть даже и в 10.2. Искренне надеюсь, что им никто сейчас не пользуется.
* * *
Главная проблема этой схемы, как мы осознали где-то в районе двухтысячных, в том, что пароль хранится, внезапно, открытым текстом. Да, да. То есть хранится как бы хеш от пароля, но клиенту пароль и не нужен — для аутентификации используется хеш. То есть достаточно утащить табличку mysql.user
с хешами паролей и после легкой модификацией клиентской библиотеки можно коннектиться как кто угодно.
Ну и эта самопальная хеш-функция была зело подозрительна. В итоге, кстати, ее сломали (sqlhack.com), но у нас к тому времени уже был новый протокол.
Придумывали мы его тогда (а «мы» это были я, kostja, Петр Зайцев, и еще несколько товарищей) с такими целями:
- То что хранится на сервере — недостаточно для аутентификации
- То что пересылается по проводу — недостаточно для аутентификации
- Бонус — использовать нормальную крипто-хеш функцию, хватит самодеятельности
И получился следующий «двойной-SHA1» протокол, который вошел в MySQL-4.1 и в неизменном виде используется до сих пор:
- Сервер хранит
SHA1(SHA1(password)).
- Для аутентификации сервер по-прежнему шлет клиенту случайную строку (20 букв) — которая исторически называется «scramble».
- Клиент шлет серверу вот такую штуку:
SHA1( scramble || SHA1( SHA1( password ) ) ) ⊕ SHA1( password )
где ⊕ — XOR, а || — конкатенация строк.
- Соответственно, сервер не знает
SHA1(password)
, но он знает scramble иSHA1(SHA1(password))
, а значит может посчитать первый операнд в этой клиентской конструкции. Потом XOR-ом он получает второй, то естьSHA1(password)
. И считая от него SHA1 может, наконец-то, сравнить его с тем, что хранится в таблице для этого юзера. Уфф.
Протокол получился удачный, все цели были достигнуты. Прослушивать аутентификацию — бесполезно, утянуть хеши паролей — бесполезно. Но ложка дегтя все-таки была, если бы кому-то удалось утянуть таблицу mysql.user
с хешами паролей и прослушать аутентификацию — вот тогда он смог бы повторить то, что делает сервер, и восстановить SHA1(password)
, чтобы в дальнейшем притворяться соответствующим юзером. Это мы не закрыли, и у меня есть сильное подозрение, что без криптографии с открытым ключом оно не закрывается в принципе. Впрочем, эта ложка дегтя совсем небольшая, если уж есть хеши, пароли зачастую проще по словарю подобрать.
* * *
Все было хорошо, но прогресс, увы, не стоит на месте. MySQL перешла под крыло Оракла, MariaDB отпочковалась и зажила своей жизнью, и, независимо от этих пертурбаций, надежность SHA1 падала с каждым годом. Первыми засуетились в Оракле. Разработку нового протокола поручили товарищу Кристоферу Петерсону. Я к тому времени был уже в MariaDB, так что могу только догадываться, что он думал и с кем советовался. Впрочем, основное понятно — цель была перейти на SHA2 и убрать эту маленькую оставшуюся ложку дегтя. Он правильно сообразил, что нужна криптография с открытым ключом. Так что новый протокол в MySQL-5.7 использует SHA256 (256-битный вариант SHA2) и RSA. И работает все это так:
- На сервере хранится
SHA256(password)
- Сервер, как и раньше, посылает клиенту 20-буквенный scramble
- Клиент читает открытый RSA-ключ сервера из заранее припасенного файла
- Клиент XOR-ит пароль полученным scramble-ом (если пароль длиннее, scramble повторяется в цикле), шифрует ключом сервера и отсылает
- Сервер, соответственно, расшифровывает своим секретным ключом, XOR-ит обратно, получает пароль в исходном открытом виде, считает от него SHA256 и сравнивает
Все довольно просто. Минус, с моей точки зрения, один, но большой — чертовски неудобно раздавать заранее всем клиентам серверный открытый ключ. А серверов-то еще может быть много, и одному клиенту может быть надо подключаться ко всем по очереди. Для этого, наверно, в MySQL и сделали, что клиент может запросить открытый ключ с сервера. Но этот вариант в наше беспокойное время серьезно рассматривать нельзя — ну в самом деле, с чего бы клиенту верить, что какой-то набор байт, которые ему кто-то прислал — это действительно открытый ключ сервера? Man-in-the-middle еще никто не отменял. И еще, как-то нехорошо, что сервер получает пароль в открытом виде, мало ли что. Мелочь, а неприятно.
Впрочем в MariaDB не было даже этого, только SHA1. В принципе, хватало, хотя банки и ворчали. Но в свете последних новостей мы тоже стали искать замену хешу-ветерану. Вообще-то ничего страшного пока не произошло — ну научились искать коллизии, ну сможет кто-то сгенерировать два пароля с одинаковым хешем, а дальше-то что? Но каждому юзеру это не объяснишь. Да и вдруг завтра еще чего для SHA1 найдут?
* * *
Новый протокол я тоже строил на основе криптографии с открытым ключом. Так чтобы ни mysql.user
, ни перехват трафика, ни и то и другое вместе, ни даже полная компрометация сервера не смогли бы открыть пароль. И, конечно, с точки зрения пользователя все должно было работать как раньше — ввел пароль, получил доступ. Никаких файлов, которые надо распространять заранее. Протокол получился на основе ed25519, это крипто-подпись с использованием эллиптической кривой, которую придумал легендарный Daniel J. Bernstein (или просто djb). Он же написал несколько готовых к использованию реализаций, одна из них используется в OpenSSH. Кстати, название происходит от типа используемой кривой (Edwards curve) и порядка поля 2255–19. Обычно (в openssh да и везде) ed25519 работает так (опуская математику):
- Генерируется 32 случайных байта — это будет секретный ключ (почти).
- От них считается SHA512, потом происходит всякая математическая магия и получается открытый ключ.
- Текст подписывается секретным ключом. Подпись можно проверить открытым ключом
Вот на основе этого и работает новый протокол аутентификации в MariaDB:
- Вместо случайных 32-х байт мы просто берем пароль пользователя (то есть пароль фактически является секретным ключом), а дальше — SHA512 и вычисление открытого ключа, как обычно.
- На сервере в качестве пароля в
mysql.user
хранится открытый ключ (43 байта в base64) - Для аутентификации сервер шлет случайный 32-байтный scramble
- Клиент его подписывает
- Сервер проверяет подпись
Все! Даже проще, чем с SHA1. Недостатков, собственно, пока не видно — пароль на сервере не хранится, не пересылается, сервер его вообще ни в какой момент не видит и восстановить не может. Man-in-the-middle отдыхает. Файлов с ключами никаких нет. Естественно, пароли можно брутфорсить, ну тут уж поделать ничего нельзя, только отказываться от паролей совсем.
Этот новый протокол впервые появился в MariaDB-10.1.22 в виде отдельного плагина, и в 10.2 или 10.3 будет поплотнее интегрирован в сервер.
Автор: petropavel