Как и было обещано в соседней теме, где рассказывался велосипед, выкладываю описание алгоритма 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
- Алгоритм Диффи-Хеллмана
- Дискретное логарифмирование