SRP-6: аутентификация без передачи пароля

  • Tutorial
Как и было обещано в соседней теме, где рассказывался велосипед, выкладываю описание алгоритма SRP RFC2945 — способе регистрации и аутентификации пользователей безопасным образом по небезопасному каналу. Вот только в процессе подготовки статьи я обнаружил более свежую версию протокола, SRP-6, вместе с реализацией, в связи с чем решил выбросить свои архаичные наработки по SRP-3, и просто дать ссылки на имплементацию новой версии.

Secure Remote Password (SRP) протокол описывает установление безопасного соединения, используя для аутентификации пару логина и пароля, при которой пароль используется только на этапе регистрации и аутентификации и только на стороне клиента. Благодаря использованию схемы близкой к Диффи-Хеллману, SRP обеспечивает безопасную аутентификацию пользователя по паролю без необходимости передачи этого пароля, и даже имея полную запись обмена по каналу мы не получаем информации достаточной, для аутентификации данным пользователем. Так же, если сервер будет скомпрометирован и утечёт «в народ» вся база логинов со стороны сервера, злоумышленник не сможет аутентифицироваться ни одним из пользователей.x

Итак, общая схема аутентификации пользователя используя сокращенный по числу обменов SRP выглядит так:
  • Выбирается для работы «поле безопасности»:
    H = односторонняя функция хеширования, достаточно безопасная на текущий момент.
    N = "безопасное простое
    " = 2*q+1 простое, где q тоже простое число.
    g = генератор по модулю N => для любого 0 < X < N существует и единственный x такой, что g^x % N = X.
    k = параметр-множитель. В SRP-6a это H(N, g); в SRP-6 это 3
  • При регистрации, клиент вычисляет следующее:
    s = random_string()
    x = H(s, p)
    v = g^x % N

    И присылает серверу три поля:
    I = username
    s = salt
    v = password verifier

    При правильном выборе g и N, передача этих данных достаточно безопасна — из них вычислительно трудно получить p.
    Безопасность обеспечивается сложностью операции дискретного логарифмирования — получения x из v=g^x%N зная v, g, N.
    Именно за счет этого, даже если с сервера сольют базу пользователей (привет, Sony!), получить пароли, или представиться пользователем будет невозможно.
    Впрочем, следует учесть, что если база пользователей слита, то протокол перестаёт быть защищенным от представления злоумышленника _сервером_. То есть данный метод никак не гарантирует клиенту, что сервер тот, за кого себя выдаёт. Однако Man-In-The-Middle атака остаётся невозможной — злоумышленник не может (вычислительно сложно) получить у себя общий с сервером ключ сессии K.
  • Пользователь:
    I = username
    a = random()
    A = g^a % N

    На сервер передаётся пара из логина I и вычисленного A.
    Сервер должен проверить и убедиться, что A != 0.
  • Сервер достаёт из своих загашников два числа, соответствующих логину:
    s = соль, созданная при регистрации
    v = верификатор пароля, вычисленный при регистрации
    После чего генерирует случайное число b, и вычисляет
    B=k*v + g^b % N
    Клиенту присылается пара из соли s и вычисленного B.
    Клиент в этот момент должен проверить, что B != 0.
  • Обе стороны вычисляют скремблер:
    u = H(A, B).
    Если u == 0, соединение прерывается.
  • Клиент вводит свой пароль p, на основе которого вычисляется общий ключ сессии:
    x = H(s, p)
    S = ((B - k*(g^x % N)) ^ (a + u*x)) % N
    K = H(S)
  • Сервер со своей стороны так же вычисляет общий ключ сессии:
    S = ((A*(v^u % N)) ^ B) % N
    K = H(S)
  • Начиная с этого момента, если клиент и сервер оба _действительно_ знают друг друга, есть идентичное число K у обоих.
    Чтобы доказать друг дружке, что они сами себе злобные буратины, происходит второй раунд обмена.
    Клиент генерирует подтверждение:
    M = H( H(N) XOR H(g), H(I), s, A, B, K)
    и отсылает серверу.
    Сервер у себя вычисляет M используя свою копию K, и проверяет равенство. Если они равны, значит у клиента точно такой же K, и, следовательно, клиент тот, за кого себя выдаёт. Если не равны — соединение прерывается, БЕЗ отсылки клиенту R.
    Если же они равны, то чтобы доказать теперь клиенту, что он не верблюд, он отсылает ему в ответ свою подтверждалку:
    R = H( A, M, K )
    Клиент аналогично вычисляет у себя R используя вычисленную у себя K, и сравнивет с полученной от сервера.

В результате, процесс аутентификации требует 2 обращения к серверу и наличие какого-либо программного клиента на стороне пользователя. Еще раз обращаю внимание, на в первую очередь защиту со стороны сервера. Сервер отдаёт как можно меньше информации, производя в первую очередь проверки у себя. Поэтому злоумышленник, имеющий у себя s и v может представиться сервером, но не сможет быть посредником.

Для реализации входа, можно использовать классическую форму логин+пароль, на onSubmit которой повешен аутентификатор.
В случае отсутствия на стороне клиента яваскрипта, форма отправится «как есть», что передаст по небезопасному каналу пароль, но позволит аутентифицироваться даже с «тупых» клиентов (путем симуляции всех действий «пользователя» на сервере).
В случае присутствия JS, аутентификация реализуется путем отправки двух спец-форм используя AJAX методики, производя вычисления между ними.

Впрочем, если сайт не требуется чтобы работал без JS (например, fallback вообще не возможен, слишком сложен, «не тянем» по времени и силам разработчиков), то мне кажется более разумным реализация такой схемы: поля вообще не находятся в форме, после выхода из поля логина поле пароля блокируется на некоторое время, и рисуется рядом ajax спиннер. После получения с сервера соли для юзернейма, поле пароля разблокируется и можно ввести туда пароль.
Смысл в разделении необходим для того, чтобы дать JS время на вычисления. Всё-таки там довольно много математики с большими числами. Когда я исследовал этот вопрос на SRP-3 используя SHA1 (лет пять эдак назад), использовать 512битные ключи оказалось слишком медленным — до 30 секунд на вычисления для подтверждения пароля. При этом весь UI браузера [особенно IE6 ;)] блокируется на всё время вычисления. Решением может быть вычисления используя континуации (запуск вычисления кусочками через setTimeout(), сохраняя контекст между вызовами) или вынося вычисления во flash / java. Причем java предпочтительнее с точки зрения скорости вычисления, а flash — с точки зрения вероятности доступности у пользователя.

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

Еще очень важно понимать, что алгоритм никак не защищает от фишинга. Поддельный сайт вполне может выдать идентично выглядящую страницу, на которой будут так же расположенные поля логина и пароля, вплоть до симуляции вычислений вычисляя, например, сумму ряда чисел от 1 до 1e12 через однострочник, на самом деле передавая на сервер злоумышленника пароль «как есть». Именно поэтому мне кажется использование подписанного Java аутентификатора более привлекательным — при подделке надо будет симулировать еще и подпись на ява-апплете авторизаторе… Что, в общем-то, используя человеческую лень и невнимательность по-прежнему возможно.

Готовые реализации SRP на JS можно найти по ссылкам:

Почитать еще по теме можно и нужно:
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 28
    0
    Спасибо за статью!
    Как раз подумываю применить SRP в одном проекте. Как думаете, возможна ли такая схема работы:
    Веб-приложение общается с сервером практически целиком через JSON, т.к. после аутентификации у сервера и клиента будет общий ключ K, то мы будем его использовать для шифрования JSON'а, чем обеспечим и аутентификацию и безопасность обмена данными.
    Или это уже извращение?
      +3
      А зачем нужно? Это фактически замена SSL с ключами на SSL с паролем :) Может, стоит на ключи таки поглядет?
        +1
        В основном просто интересно. :)
        Плюс к тому же раз уж у нас при применении SRP всё равно получается ключик, то почему бы его не использовать? Да и с сертификатами возиться не хочется…
          +1
          Единственная проблема — скорость шифрования.
          Ради интересу советую на JS закодировать/раскодировать простенький запрос на 15-20К текста…
            +1
            Наверно вы правы.
            Но тогда получается если мы будем HTTPS использовать для всего, то разумно и регистрацию через него же проводить, а значит SRP можно и не применять.
              +4
              SRP имеет смысл оставить для защиты от утечки паролей при взломе бд.
                +1
                Любой уважающий себя проект уже давно не хранит пароли в открытом виде, на мой взгляд это самая банальная и первичная мера безопасности.
                Забудем на минутку о Sony. :)
                  +1
                  Хранение просто хеша от пароля тоже не радостный вариант, разве что солёный.
                  Я лично не раз использовал md5(pass) из чужой бд для получения pass :)

                  И еще один момент — SRP позволят аутентифицировать пользователя безопасно и просто, в то время как SSL требует для этого сертификаты (во всяком случае, пока OpenSSL+SRP не используются браузерами).

                  То есть я за выбор SRP+SSL с Fallback до простой формы логина (поверх всё того же SSL) если требуется.
                    0
                    даже имея хеш пароля — легко залогинится
                    (имея косвенный доступ к исходникам)

                    в данном случае, гарантией является сессионный ключ
            +2
            Думаю ответ в том что для популярного сайта делать всё через SSL(https) может быть расточительно по ресурсам сервера. Используя SRP вы остаётесь в http но довольно эффективно и безопасно авторизируете пользователя сайта и без опасения перехвата пароля.
              +1
              SSL, реализованный в сильно оптимизированном C коде меньше нагрузит сервер, чем ручное кодирование на скриптовом языке.
                0
                В теории при использовании простой key-value базы на серере и толстого клиента на стороне пользователя, на сервер можно засылать шифрованные данные и не париться на сервере с их расшифровкой.
                  0
                  Это мы рассматриваем клинический случай, когда от сервера осталась только база, да еще и по которой не требуется даже поиска делать?
                    0
                    Именно так :)
          +1
          SRP JavaScript Demo, как кажется отрабатывает практически мгновенно, даже при выборе 1024 бит. Я ожидал больших тормозов. Или там какое-то не полноценное демо?

          И да, спасибо, что опубликовали этот материал.
            0
            Сильно ускорились JS движки + поддержка Binary в последних версиях, без которых всё было очень и очень тоскливо.
              0
              И да, еще сильно зависит от хеш-функции. SHA-512 работает ощутимо медленнее чем SHA1
                +1
                А мне вот не удалось проверить. Оказывается в Google Chrome есть клёвая бага, которой уже почти 3 года.
                code.google.com/p/chromium/issues/detail?id=580
                  0
                  А при чём тут Java? Там же первый пример без явы, только на js.
              +1
              так вот зачем нужен Native Client
                0
                NC нужен за любой сложной логикой на стороне клиента. JS всё-таки убоговат всё еще по скорости.
                  0
                  хм, я бы не был столь категоричен
                  bellard.org/jslinux/
                    0
                    Я видел это, потому и говорю «убоговат» а не «совсем убог» :) Всё-таки, это еще не совсем «оно». По сравнению с нативным кодом отстаёт на несколько десятичных порядков.
                +1
                Спасибо за информацию, нашел ее очень полезной
                хотелось бы протестировать поближе к практике:
                на реальных цифрах, чтоб JS не тормозил

                что посоветуете?
                  0
                  Судя по всему, сейчас оно не так сильно и тормозит, всё-таки компы шустрее стали и js движки тоже.
                  Стоит просто взять и попробовать. Оптимальным будет килобит и SHA256.
                  Если будут тормоза — тогда уже посмотреть на полкилобита и SHA224.
                  Опускаться ниже полукилобита не имеет смысла из соображений безопасности.
                  0
                  Спасибо за статью, как раз искал что-то в этом роде.
                  Вроде всё понятно кроме одного момента, правильно ли я понимаю что N генериться один раз, и пишется куда-то как константа, и потом используется при регистрации и аутентикации пользователей?

                  Если генерить N при каждой новой регистрации и потом для каждой новой аутентикации то у меня никак не получается получить тот-же K но обеих сторонах.

                  Поясните пожалуйста что делать с N.
                  Спасибо
                    0
                    g и N выбираются и используются в течение всей коммуникации — это выбор поля, на котором работаем.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое