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

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


  1. kostja
    14.03.2017 18:57
    +3

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


  1. xalkin
    15.03.2017 11:05

    Про велосипеды ранних 90-е еще можно понять, но сейчас их городить? Разве что потому, что SRP not invented here.


    1. petropavel
      15.03.2017 11:10

      Нет, просто он неудобный. Все схемы с солью добавляют еще один round-trip к аутентификации. А этот SRP, похоже, даже два. Мы же, наоборот, старается уменьшить число round-trip-ов в протоколе, так что раздувать протокол не хотелось бы.


      1. xalkin
        15.03.2017 14:18

        Он совершенно также дает плюс один round-trip, как и эти велосипеды. Но кроме всего прочего позволяет клиенту удостовериться что сервер тоже знает пароль. Да, последнее не стойко к компрометации серверной базы, но как минимум стойко к прослушке.


        1. petropavel
          15.03.2017 14:44

          Можно посчитать.

          В нашем протоколе так:
          1. сервер>клиенту: «привет, вот scramble»
          2. клиент>серверу: «username такой-то, пароль такой-то»
          3. сервер>клиенту: «ok» (или «пошел на фиг»)

          причем шаги 1 и 3 обязательны, плагин на них не может повлиять.

          В SRP будет так:
          1. сервер>клиенту: «привет»
          2. клиент>серверу: "I and A"
          3. сервер>клиенту: "s and B"
          4. клиент>серверу: «вот мой M1»
          5. сервер>клиенту: «ok, вот мой M2»

          да, в плюс один можно уложиться.

          Идентификация сервера — это хорошо, но в задачу не входило. Для этого можно включать SSL. Может потом сделаем и с идентификацией сервера.


          1. xalkin
            15.03.2017 17:57

            По количеству запросов SRP практически такой же, единственная разница что в вашем случае серверу не нужно ждать первого авторизационного пакета, он может сразу выдать scramble. Честно говоря сомнительное ускорение. При этом SRP хорошо проанализирован, стандартизован и проверен. В нем учтены всевозможные векторы атаки. Его разрабатывали специалисты, его анализировали специалисты. Зачем городить самопал?

            Первое правило криптографии: не изобретайте собственных алгоритмов.


  1. den_crane
    15.03.2017 11:10
    +1

    А что djb думает по поводу того что вместо случайный 32 случайных байт ключ всего от силы 10 неслучайных?


    1. petropavel
      15.03.2017 11:15

      Я, конечно, не спрашивал. Ну а что тут думать — плохо это, подобрать 10 неслучайных байт проще, чем 32 случайных. Но если без соли, то за случайность отвечает пользователь, мы тут ничего улучшить не можем. А соль увеличивает число round-trip-ов, поэтому ее пока что избегаем.

      Кстати, вначале я использовал немодифицированный ed25519 и получал 32 «случайных» байта как SHA256(password). Потом решил, что лишний SHA256 случайности не добавит, и убрал :)


  1. FreeMind2000
    15.03.2017 14:20

    «Man-in-the-middle отдыхает»

    Вопрос1:
    А каким образом при регистрации пароль юзера первый раз передается на сервер?

    Вопрос2,3:

    — После авторизации, чем шифруется весь трафик между клиентом — сервером?

    — Если MITM получит подписанный юзером scramble и переправит на сервер, после этого разве он не сможет выполнять любые команды от имени юзера? (актуально если нет шифрования трафика после авторизации или необходимости подписи всех команд отправляемых на сервер юзером)


    1. petropavel
      15.03.2017 14:50

      1. Есть варианты. Обычно он передается открытым текстом и на сервере хешируется. Это не есть хорошо. Но можно хеш посчитать и локально, тогда на сервер попадет только хеш.

      2. Или SSL или ничем, смотря как настроено.

      3. Если SSL — то не сможет, если нет SSL — то сможет. В протоколе после аутентификации никакой защиты нет. Велосипедить свой собственный аналог OpenSSL не хочется, да и не нужно это никому — в смысле никто никогда за 20 лет это не просил.


      1. FreeMind2000
        16.03.2017 20:02

        Хм, тогда какой же здесь «Man-in-the-middle отдыхает»? :)

        Если используется SSL то, вообще зачем нужен какой-то дополнительный протокол, просто клиент через SSL открыто (для сервера) посылает захешированный пароль и сервер пишет его в базу, а при авторизации тоже самое, клиент по SSL открыто (для сервера) отправляет хешированный пароль и сервер сравнивает его с хешем из БД. При взломе сервера пароли никто не получит. Это просто и надежно.

        Если без SSL, то от MITM у вас защиты никакой нет, есть только защита от снифера.

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


        1. petropavel
          16.03.2017 22:24

          1. Ну как. Это протокол аутентификации. Как он работает — определяет плагин. Но он инкапсулирован в MySQL client-server protocol, и все равно все происходит по правилам этого протокола, который несколько шире, чем аутентификация, и плагин на него влиять не может (пока что. хотим это менять).

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

          В приведенном новом протоколе аутентификации MitM-у ловить нечего. За его пределами — да, можно. Но тут уже всю систему менять надо, то есть сам клиент-серверный протокол. Это задача совсем другого уровня, в 10.1 (да и в 10.2) это сделать невозможно.

          2. Неее. Это как раз то, что мы исправляли в начале двухтысячных. При взломе сервера «кто-то» получит все хэши. И потом сможет их по SSL слать, пароли ему не нужны.

          3. Защита он снифера у нас такая же как и от MitM. Во время собственно аутентификации —- ни MitM, ни сниффер ничего не получат. После — обоим путь открыт, если не использовать SSL.

          4.Заранее распространенный файл от MitM-а не поможет, если сессия не шифруется. А она шифруется только, если включить SSL. Так что тут заранее распространенный файл бесполезен от слова совсем. Он нужен не для этого, а для аутентификации сервера клиенту. Чтоб клиент знал, что он подключается к правильному серверу. Но в том же вышеупомянутом SRP эта задача решена гораздо элегантнее, без всяких файлов. И через официальный дистрибутив ключи нельзя распространять, в том же весь смысл, чтоб у разных серверов были разные ключи.


          1. FreeMind2000
            17.03.2017 23:06

            2. Неее. Это как раз то, что мы исправляли в начале двухтысячных. При взломе сервера «кто-то» получит все хэши. И потом сможет их по SSL слать, пароли ему не нужны.
            — Тут согласен

            3. Защита он снифера у нас такая же как и от MitM. Во время собственно аутентификации —- ни MitM, ни сниффер ничего не получат. После — обоим путь открыт, если не использовать SSL.
            — Тут ошибка. Защиты от MITM нет, так как если между клиентом и сервером стоит ПК, который во время аутентификации перехватывает пакеты и просто дублирует их для клиента и сервера, то в момент получения от клиента подписанного scramble и переправки его на сервер, злоумышленник полностью получает доступ с правами юзера. А например юзеру, он перешлет «ошибку подключения к серверу».
            От снифера есть, т.к. пароль в открытом виде не передается, и подсмотренный подписанный юзером scramble ему не пригодится, так как при попытке авторизации scramble уже будет выслан другой.

            4.Заранее распространенный файл от MitM-а не поможет, если сессия не шифруется.
            — :) Так он же как раз и распространяется для шифрования сессии SSL при авторизации и защиты от MITM (подмены сервера).

            5. И через официальный дистрибутив ключи нельзя распространять, в том же весь смысл, чтоб у разных серверов были разные ключи
            — Вы видимо не поняли, «например через официальный дистрибутив клиента». Никто не мешает создать свой сертификат сервера, для своего клиент-серверного приложения и распространять этот сертификат со своим клиентским приложением, что бы клиент всегда подключался только к легальному серверу. Тут можно использовать даже самоподписанные сертификаты ибо ваш клиент — это по сути браузер, который сертификат нужного сервера уже считает доверенным. Создание сертификата даже можно вынести в отдельную ф-цию приложения, что бы, например, администраторы в разных компаниях, могли создавать свои корпоративные сертификаты для своих серверов и распространять их в дистрибутивах своих корпоративных клиентов.

            Что бы ваш протокол авторизации был по надежности сравним с SRP, надо решить проблему указанную выше в пункте 3.


            1. petropavel
              18.03.2017 13:30

              3. Согласен, но это то же, что и я говорил. Во время аутентификации ни снифер ни MitM не могут узнать пароль (или «информацию, эквивалентную паролю», как в примере с хэшем). После аутентификации MitM может посылать от имени юзера команды. А снифер может видеть все данные, что запрашивает юзер — без SSL после аутентификации защиты нет, тут надо весь протокол менять.

              Вот, например, в новом sha256 протоколе в 5.7 если заранее не дать клиенту открытый ключ, он его запросит с сервера. MitM может этот ключ подменить своим и узнать пароль клиента, plain-text. Тут сама аутентификация уязвима к MitM. То же самое в SSL, если у клиента нет возможности проверить сертификат.

              4. Нет, в sha256 плагине, файл используется только для шифрования пароля, не всей сессии. Аутентификационный плагин в принципе не может повлиять на всю сессию, он используется только для аутентификации. Так устроен протокол. Если использовать SSL (и проверять валидность сертификатов) то неважно как аутентифицировать, все равно SSL все зашифрует и обеспечит.

              5. Да, если свой дистрибутив со своим сертификатом ­— то конечно, согласен.

              6. Нет, SRP тут не причем. Если использовать SRP в рамках теперешнего протокола (и без SSL), то сессия все равно будет не зашифрованной и не защищенной от MitM. Вообще неважно насколько сложный, крутой и непробиваемый будет протокол аутентификации, если основной клиент-серверный протокол останется, какой есть. Надо сам протокол переделывать, плагином это не исправить.


              1. FreeMind2000
                18.03.2017 18:47

                Собственно мы говорим разными словами об одном и том же :)

                «После аутентификации MitM может посылать от имени юзера команды.»
                — А разве кто-то вообще может посылать команды до аутентификации? Просто перехват (компрометация) выполняется именно в момент отправки юзером подписанного scramble, т.е. в процессе аутентификации ибо scramble только для этого процесса и нужен. А команды естественно уже выполняются потом. Пароль MITM в процессе аутентификации юзера не узнал, но доступ получил.

                Кстати, интересно, по вашему клиент-серверному протоколу, если после нормальной аутентификации юзера, хакер просто включит снифер и увидит ID авторизованной сессии юзера, он сможет по этому ID выполнять свои команды?

                6. Нет, SRP тут не причем
                — Ну, я имел в виду именно сравнение авторизации через SRP, и тем вариантом авторизации, который предложен в статье. В этом смысле отличие SRP в том, что он от MITM защищен. Безопасная работа уже с самой авторизованной сессией должна обеспечиваться протоколом передачи данных/команд.