А ты хто такой? Эволюция протоколов аутентификации MySQL и MariaDB в лицах


замок на старом сейфе В далекие времена, до фейсбука и гугла, когда 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 будет поплотнее интегрирован в сервер.

Комментарии (1)

  • 14 марта 2017 в 18:57

    +1

    Сергей, спасибо что включаешь меня в соавторы, но по правде сказать я был тогда такой зелёный, что разве что мог послужить «подопытным кроликом». Шаги протокола несколько раз прокрутил в голове в процессе реализации, уязвимостей придумать так и не смог. А ещё мне до сих пор стыдно за CVE с nil-terminated string в MYSQL CHANGE USER который я в своё время проморгал.

© Habrahabr.ru