Как и было обещано в соседней теме, где рассказывался велосипед, выкладываю описание алгоритма SRP RFC2945 — способе регистрации и аутентификации пользователей безопасным образом по небезопасному каналу. Вот только в процессе подготовки статьи я обнаружил более свежую версию протокола, SRP-6, вместе с реализацией, в связи с чем решил выбросить свои архаичные наработки по SRP-3, и просто дать ссылки на имплементацию новой версии.
Secure Remote Password (SRP) протокол описывает установление безопасного соединения, используя для аутентификации пару логина и пароля, при которой пароль используется только на этапе регистрации и аутентификации и только на стороне клиента. Благодаря использованию схемы близкой к Диффи-Хеллману, SRP обеспечивает безопасную аутентификацию пользователя по паролю без необходимости передачи этого пароля, и даже имея полную запись обмена по каналу мы не получаем информации достаточной, для аутентификации данным пользователем. Так же, если сервер будет скомпрометирован и утечёт «в народ» вся база логинов со стороны сервера, злоумышленник не сможет аутентифицироваться ни одним из пользователей.x
Итак, общая схема аутентификации пользователя используя сокращенный по числу обменов SRP выглядит так:
В результате, процесс аутентификации требует 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 можно найти по ссылкам:
Почитать еще по теме можно и нужно:
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) % 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 можно найти по ссылкам:
- SRP JavaScript Demo реализация обеих сторон на JS, для исследования «что творится». Использует SHA1, который, разумеется, уже нелья использовать в живых системах.
- SRP-JS on GoogleCode с бэкендом к Django
- Javascript Crypto Library
Почитать еще по теме можно и нужно:
- The Stanford SRP Homepage
- SRP in Wikipedia
- RFC2945, описывающий SRP-3 (устаревший)
- IEEE P1363.2: Password-Based Public-Key Cryptography, в числе которого описывается SRP
- Алгоритм Диффи-Хеллмана
- Дискретное логарифмирование
