Техники безопасной парольной авторизации в Web +1



Приветствую, друзья! В прошлой статье я предлагал на обсуждение какой-то бред, за который теперь стыдно черновик нового протокола аутентификации на сайтах. И хотя сейчас я значительно его переработал (с учетом ваших замечаний) и готовлю новую версию, я решил что стоит предварительно опубликовать несколько статей, которые будут раскрывать мотивы технических решений, закладываемых в новый протокол. А эта статья посвящается проблемам и техникам безопасной передачи и хранения сайтами ваших паролей.

Я приведу несколько реально используемых схем на практике. Буду приводить схемы авторизации в порядке от простого к сложному, показывая их преимущества и недостатки и обосновывая выбранные технические решения.

Теперь кратко о себе: Я опасно некомпетентен в криптографии. Это всё, что вы должны обо мне знать.

Я думаю, любой здесь web-разработчик, что со стороны front-, что со стороны back-, хотя бы раз реализовывал механизм парольной аутентификации на одном из своих проектов.

Что может быть проще? Два поля ввода. Кнопка "Войти". Передаем на сервер, "пробиваем" по базе. Есть совпадение – "Welcome", нет – увы. На заре веб-разработки некоторые даже не проверяли корректность переданных аргументов, и получали эпические sql-инъекции:

скажите, вашего сына и правда зовут «Вася, брось таблицу»?

0. TLS спасет мир

С тех пор много воды утекло. Разработчики стали опытнее, а взломщики – изворотливее. Массовое распространение получил HTTPS. Сейчас он продавливается так агрессивно, что кажется, будто вскоре вообще не окажется веб-приложений, не охваченных им. Правда это только кажется.

Ну а раз так, то зачем изобретать велосипеды? Передаем пароль как есть, и все дела. А о безопасности позаботится нижележащий слой TLS. Зря его что ли придумывали?

Кто-то удивится, но некоторые так и поступают. Пароль пользователя хранится в БД «как есть». Или передается «как есть» (в конце статьи приводятся примеры). А качестве бонуса разработчик получает возможность при смене пароля проверять, что он не совпадает с предыдущими версиями. И для пущей безопасности, заставим пользователя менять пароль каждые три месяца... Хочется сказать: "спасибо, что заботитесь о нас".

Вы можете не соглашаться, но факт есть факт: массовые утечки нехешированных паролей даже с крупных сайтов случаются. Точнее: именно благодаря таким утечкам мы и узнаем, что крупный сайт даже не удосужился захешировать наши пароли.

Кстати, почему только сайты. Многие аппаратные устройства хранят пароли в открытом виде.

ВРЕЗКА

…Ozon о каких-либо утечках или взломах никогда не сообщал. Однако в декабре 2018 года технический директор ретейлера Анатолий Орлов неожиданно заявил, что компания изменила систему восстановления паролей, добавив в нее дополнительное шифрование. «В 2018 году пришла новая команда, которая сделала серьезную работу для усиления безопасности. Сейчас все пароли пользователей хранятся в хешированном виде («закодированные необратимым образом»): от пароля вычисляется необратимая функция, в результате проверить его можно, вычислив от него такую же функцию, но восстановить его невозможно. Лучше всего оберегаются те секреты, которые ты сам не знаешь», — говорил Орлов изданию TJ. Источник

Обратите внимание, что технический директор де-факто признался, что пароли ранее ozon хранил в открытом виде. Шел 2018 год…

Кто же может покушаться на процесс авторизации? Концептуально таких злоумышленников три класса:

  1. «Человек-по-середине». Он же MitM, «сидящий на канале» клиент-сервер. Будем полагать, что у нас всегда активный MitM.

  2. Взломщик БД, получивший в свое распоряжение дамп сайта и учётных записей его пользователей.

  3. Атакующий на клиенте: всевозможные вирусы, трояны, клавиатурные шпионы, троянские скрипты на Web-странице от «партнерских» сайтов, административный ресурс.

Мы будем далее рассматривать только классы 1 и 2. Что касается атакующего на клиенте – то у него гораздо больше возможностей, чем у первых двух. И защититься от него в рамках нашей статьи не получится вовсе.

Также, давайте пока не будем обращать своё внимание на повсеместное распространение https. К нему много вопросов. И это тема отдельной статьи. Будем полагать, чего его нет вовсе. Тем более, что такое вполне может быть.

1. Хешируем

Ну хорошо. Попробуем усилить элементарную схему. Первое, что приходит на ум: будем передавать не пароль P, а его хэш H. И сервер будет хранить не исходный пароль, а его хеш. Теперь исходный пароль пользователей не сможет узнать ни MitM, ни взломщик БД. Кроме того, в качестве бонуса, даем возможность пользователю использовать пароль любой длины и из любых символов. Всё равно всё свернется в короткий хэш. А значит, поле для хранения пароля в БД можно сделать фиксированной длины, а не varchar().

Для надежности, возьмём в качестве хеширующей функции SHA2 или SHA3. 256-битов на текущий момент вполне достаточно. Но если хотите – обе функции поддерживают режим 512 бит. Хотя признаться, такое количество бит потребует сделать в БД поле размером в 64 байта!

Хорошо. Вот только злоумышленнику MitM не нужно уже знать пароль – ему достаточно узнать его хэш. Мало того, если пользователь использует такой же пароль на другом сайте, и тот сайт использует подобную "защиту", то MitM и там может "поживиться" (ему уже не обязательно взламывать канал, достаточно зайти "легально").

Что касается взломщика БД, то он сразу же может вычислить пользователей, имеющих несколько учетных записей (двойников), по совпадению у них хешей. А логины этих пользователей, зачастую, могут многое рассказать о его владельце и о сайтах, которые он посещает. Ну и радужные таблицы от популярных паролей тоже принесут свои плоды. Хотя, такой взломщик и без всяких таблиц уже может попытать удачи в легальной аутентификации на других сайтах с такой же схемой защиты.

Значит такая схема не лучше первой. Что ж. Усиливаем дальше.

2. Солим

В терминах криптографии, "соль" – это последовательность случайных бит, которые добавляются к хэшируемой (или шифруемой) последовательности для "рандомизации" результата. Добавляя соли к паролям при хешировании, мы будем получать разные хэши для одинаковых паролей. Ну при условии, конечно же, что соль используется разная:

hash(password,salt_1) \ne hash(password, salt_2)

В качестве соли можно использовать (почти) что угодно: временную метку создания учетной записи, идентификатор или логин пользователя. Но лучше – случайную последовательность бит, не короче длины блока хэш-функции. И если вы используете в качестве hash функцию SHA2-256, то соль лучше брать 256-битную.

Схема взаимодействия чуть усложняется. Теперь сервер клиенту должен предварительно сообщать его соль, чтобы тот смог вычислить правильный хэш от своего пароля. Сам сервер не хранит ни пароль пользователя, ни прямой хэш от него. Он хранит предвычисленное значение hash(password, salt), которое ему сообщает пользователь один единственный раз в момент регистрации. При логине, сервер сравнивает полученный hash(password, salt) от пользователя с тем значением, что хранится у него в БД:

  1. Клиент посылает серверу логин login

  2. Сервер посылает клиенту соль salt для учетной записи login

  3. Клиент вычисляет H = hash( password, salt ) и посылает серверу.

  4. Сервер сравнивает H со значением, хранимым в БД.

Что мы имеем? Теперь взломщик БД столкнется с серьезным препятствием в деле восстановления исходных паролей пользователей. Радужные таблицы для всех возможных солей заранее не рассчитать. Особенно если у вас соли 256-битные. Также он не сможет использовать полученные хеши на других сайтах – они уникальны (при условии, что в качестве соли используется случайный набор бит, а не логин пользователя). И ещё – не сможет вычислить пользователей-"двойников".

Также заметим, что соль хоть и разная для разных пользователей, но не меняется со временем. А значит, злоумышленник MitM перехвативший однажды hash(password, salt), может использовать это значение для легального входа от имени жертвы. Тоже самое сможет сделать и взломщик БД.

3. scrypt’им

В принципе, описанная выше схема уже надежна в некоторой степени. И уж более надежна чем предыдущие. Но тут есть одна засада.

Производительность компьютеров растет. И хотя закон Мура давно как не действует (по слухам, с 2006 года), и рост уже не экспоненциальный, на "плато" мы ещё не вышли. То что ранее считалось невозможным, теперь доступно многим. Например, аренда на время вычислительных кластеров с фантастической мощностью. Специализированных кластеров, заметим. Наточенных на число-дробильные операции, включая вычисление хешей. Спасибо за это криптовалютам.

поговаривают, что

количество хешей, которое обсчитывается в сети Bitcoin каждые три секунды, достаточно, чтобы найти коллизию для SHA-1. Источник.

Теперь взломщик БД может попытаться восстановить пароли интересующих его учетных записей, даже если они захешированы с солью (все восстанавливать нет смысла и не хватит времени).

Как? В начале по таблице известных паролей: синтетических qwerty, популярных 123 и когда-то угнанных реальных. Затем, путём тупого перебора символов из заданного алфавита. Алгоритм концептуально прост: берем пароль-кандидат, вычисляем hash( password, salt ), сравниваем результат. Соль учетной записи известна, ибо хранится рядом с хешем пароля.

На нашу беду, процесс перебора хорошо параллелится. А значит, в теории рост производительности не ограничен (правда только в теории; практика – вещь куда более суровая).

Вот для примера расчеты

Допустим (гипотетически), что вы используете сложный 8-ми символьный пароль, состоящий из символов латинского алфавита разного регистра, цифр и спец.символов следующего алфавита: ~!@#$%^&. Общий алфавит составит: 26 + 26 + 10 + 8 = 70 символов. 8-символьный пароль на таком алфавите имеет емкость 70^8 вариантов.

Представим, что злоумышленник имеет специализированный кластер, способный вычислять наши хеши из пароля со скоростью 10^12 вариантов в секунду. Тогда для взлома ему потребуется 70^8 / 10^12 = ~576 сек. Т.е. ваш пароль будет гарантировано подобран за ~10 минут. А в среднем такой системе будет достаточно потратить лишь 5 минут на подбор пароля от известного токена. 10-символьный пароль потребует уже в среднем 15 суток. Но повысив производительность системы в 10 раз, мы сократим это время до 1-2 суток. А такая производительность теперь есть и легко доступна благодаря облачным сервисам, распределенным вычислениям и всего того наследия майнинговых фрем.

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

Функция scrypt делает почти тоже самое, что и хэш-функция (на самом деле использует в качестве базы SHA2-256), но создана таким образом, чтобы усложнить атаку перебором при помощи ПЛИС: FPGA, ASIC и подобных. Она заставляет использовать в алгоритме много циклов и ветвлений (чего суперскаллярные процессоры крайне не любят), к тому же, требует слишком много памяти на одно ядро.

Поэтому, вместо стандартной хеширующей функции, мы будем использовать scrypt. WebCryptApi пока её не поддерживает, но для javascript-разработчиков есть уже реализованные библиотеки. В остальном схема взаимодействия пока остается прежней. И вроде бы, надежной.

Но защитит ли она от пассивного MitM? Нет. Ибо мы всё ещё посылаем на сервер всегда один и тот же хеш. А значит, перехватив передачу итогового хеша на сервер, злоумышленник может в последствии легально его использовать для входа под чужой учеткой.

Некоторые скажут, что MitM вообще можно было бы исключить из рассмотрения, ибо скоро нагрянет HTTPS. Но к этому протоколу большие вопросы. Использование https можно обойти и организационно. А надежность TLS – тема отдельной статьи. Да и к тому же, проектировать какие-либо защитные механизмы, надеясь на работу других средств безопасности, – наивно. Практика показывает, что более чем наивно.

4. Добавляем challenge

Ну хорошо. Давайте усложним жизнь и для MitM. Попытаемся добиться того, чтобы хеш от пароля на сервер передавался всегда разным. Как? А давайте «посолим» уже сам хэш! Тот самый, что нам дает функция scrypt( password, salt ). Для этого сервер сгенерирует и пошлет клиенту случайную последовательность байт (длина которой равна длине блока хеширующей функции), которую мы (по традиции) назовем challenge. Теперь схема взаимодействия клиента и сервера такова:

  1. Клиент посылает серверу логин login

  2. Сервер посылает клиенту соль salt для login и случайный challenge

  3. Клиент вычисляет H = scrypt(password, salt) ; затем вычисляет Hs = hash(H, challenge); и отправляет серверу HS.

  4. Сервер хранит оригинальный H клиента; он также вычисляет свою версию HS = hash(H,challenge) и сравнивает с присланной HS.

Разработчик этой схемы должен не забывать очищать на своей стороне challenge, который он использовал «только что», не давая вероятному MitM повторить вход пользователя «по горячим следам».

Теперь MitM точно не сможет узнать передаваемый хеш. Ну если только мы честно генерируем действительно случайный challenge, всегда разный для каждой аутентификации. Что касается взломщика БД: он всё ещё может воспользоваться угнанными хешами для авторизации под чужими учётками. Но только на этом сайте. Впрочем, за него мы «возьмёмся» позже.

5. Обоюдный challenge

Казалось бы где тут подвох? Да почти нигде. Вот только, что если наш злоумышленник MitM вклинится в момент аутентификации и поменяет challenge от сервера на более простой (например – все нулевые биты, или вообще пустой)? Зачем? А чтобы попытаться облегчить себе задачу обратного восстановления H из Hs.

Защититься от такой потенциальной атаки можно, если клиент будет генерировать свой challenge и использовать его для хеширования пароля. Теперь на шаге 2, клиент вычисляет Hs = hash(H,server-challenge,client-challenge) и отправляет Hs вместе со своей версией challenge. Как результат, даже если сервер или клиент (или кто-то другой за них) случайно или намеренно решит поменять challenge на более примитивный, это не снизит уровень защиты исходного H, который, в свою очередь, защищает пароль.

6. Финальный штрих

Некоторые, возможно, уже заметили, что даже такая усложненная схема авторизации не защищает нас от взломщика БД. Последний может использовать компрометированные хеши пользователей и соли для легального входа от имени пользователей. Можно ли как-то от этого защититься?

Можно. И даже более того. Фактически мы можем сделать так, что угон аутентификационных данных пользователей будет бессмысленным. Компрометация этих сведений не принесет взломщику ничего (ну правда, он может «поживиться» куда более ценными сведениями; например данными сохраненных кредитных карт и cvc-кодами для них; но мы ведь защищаем процессы авторизации, а не проектируем PCI DSS).

Чтобы защититься от компрометации на стороне сервера, клиенту необходимо иметь некий секрет и алгоритм доказательства серверу, что клиент знает этот секрет. Тут на ум сразу же приходит криптография на ассиметричных ключах. Попробуем вначале обойтись хешированием или симметричными шифрами. И посмотрим как это можно реализовать.

КРАЙНЕ ВАЖНОЕ ЗАМЕЧАНИЕ: если вдруг кому-то покажется интересной какая-то из двух рассмотренных далее схем, ни в коем случае не спешите внедрять. Внимательно изучите сравнительную таблицу недостатков и уязвимостей. Опять же, не забывайте про опасность использования «учебных» криптопротоколов. Я предупредил.

6.А Схема защиты на симметричных ключах

Пусть H – это тот самый хеш клиента, по которому сервер его аутентифицирует. H хранится на сервере. H может быть потенциально стать доступным взломщику БД.

Пусть клиент имеет некий ключ K, которым он зашифровывает свой исходный аутентификационный хеш H: V = EK(H), где E – функция шифрования на ключе K. Итоговый шифр V вместе с H сервер хранит в своей БД. Если алгоритм шифра выбран надежный, то из пары (V, H) восстановить исходный ключ почти невозможно.

Теперь в процессе аутентификации нам остается передать защищенным образом пару (H, V) серверу. Тот сверит H, расшифрует V и убедится, что мы действительно знаем ключ К, а не украли H при взломе БД. 

Это был концепт. Теперь конкретика. 

Определим функцию hmac как функцию HMAC-SHA2-256. Salt и challenge должны быть равны длине блока хеширующей функции. В нашем случае – 256 бит. Определим функцию E – как функцию шифрования, а функцию D – как функцию расшифровывания на базе симметричного шифра AES-256.  Наконец, определим функцию scrypt, как функцию, возвращающую 256-битный хеш от пароля и его соли.

На стороне клиента производятся предварительные вычисления:

  1. Защитим исходный пароль пользователя, вычислив H_p=scrypt(password,salt).

  2. Сформируем аутентификационный хеш H=hmac_{H_p}(login).

  3. Сформируем код подтверждения K=hmac_{H_p}(salt).

  4. Сформируем шифр аутентификационного хеша V = E_K (H).

При регистрации клиент посылает серверу пару (H, V) в открытом виде. Заметим, что сервер не знает ни исходный пароль пользователя, ни его первичный хеш Hp.

Алгоритм авторизации клиента теперь такой:

  1. Делаем запрос на сервер с нашим логином.

  2. Сервер посылает нам соль salt учетной записи, свое значение challenge Cs.

  3. Клиент вычисляет Hp, H, K на базе пароля, логина и соли, аналогично п. 1-3 выше.

  4. Формирует свой challenge Сс, вычисляет C = Cs^Cc.(xor-ит его с серверным, получая итоговые challenge).

  5. Вычисляет Ks = hmacH(C).

  6. Гаммирует код подтверждения Kv = K ^ Ks.

  7. Передает серверу пару (Kv,Cc).

  8. Сервер, аналогично клиенту вычисляет Ks (п. 5), восстанавливает K = Kv ^ Ks, расшифровывает H' = DK(V), сравнивает с хранимым H.

  9. Аутентификация считается пройденной, если Ks, присланная клиентом совпадает с вычисленной сервером, а расшифрованное значение H' эквивалентно хранимому H.

Не смотрите косо на операцию XOR (^) при вычислении Kv. Если challenge сформирован действительно случайным образом (а для гарантии этого его генерируют обе стороны взаимодействия) KS не поможет криптоаналитику восстановить K из множества пар (Kv, KS). Подобная методика используется в AEAD-схеме Chiphertext translation.

Если взломщик БД получит доступ к аутентификационным сведениям пользователей (login, salt, H, V), то у него нет никакого шанса восстановить недостающие данные об password и K. Мало того, воспользоваться полученными сведениями для осуществления «легального» входа от имени жертвы злоумышленник тоже не сможет. Число Kv, которое помогло бы злоумышленнику восстановить K, доступно только в момент аутентификации. Поэтому, чтобы взломать эту схему окончательно злоумышленник должен взломать БД и перехватить все процессы аутентификации.

Такое вполне может быть. Например, если взломщик – это сотрудник/владелец ЦОД. Вот тут нас может выручить уже криптография на ассиметричных ключах.

Схема авторизации на ассиметричных ключах и эллиптических кривых

Пусть в процессе аутентификации клиент подпишет случайную строку message своим закрытым ключом e. А сервер проверит подпись клиента, зная его открытый ключ D. Но если клиент может таким образом доказать, что ключ D принадлежит ему, и только ему, то нам уже не нужно «заморачиваться» с хранением хэша от пароля!

Для реализации подписи будем использовать алгоритм ECDSA. Эллиптические кривые удобны тем, что здесь не потребуется генерация очень больших простых чисел. А в качестве закрытого ключа мы можем использовать любое целое число, и конечно же большое. И, значит, можем воспользоваться хешем из всё той же пары логин-пароль.

Возьмём, например, эллиптическую кривую spec256k1 группы SECG («Standards for Efficient Cryptography Group», основанной Certicom). Она же используется в Bitcoin.

Параметры этой кривой

p = 0xffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe fffffc2f

a = 0, b = 7, h = 1

Gx = 0x79be667e f9dcbbac 55a06295 ce870b07 029bfcdb 2dce28d9 59f2815b 16f81798

Gy = 0x483ada77 26a3c465 5da4fbfc 0e1108a8 fd17b448 a6855419 9c47d08f fb10d4b8

n = 0xffffffff ffffffff ffffffff fffffffe baaedce6 af48a03b bfd25e8c d0364141

где, p – простое, задающее размер конечного поля; a и b – коэффициенты уравнения эллиптической кривой y2=x3+ax2+b; h – кофактор подгруппы; (GX, GY) – декартовы координаты базовой точки G, генерирующей подгруппу; n – порядок подгруппы (определяет размер ключей и подписываемых данных).

Т.к. порядок подгруппы n этой кривой почти 128-битный, открытый и закрытый ключи, а также message будут 128-битными. Для формирования хешей по прежнему используем 256-битные SHA2 или SHA3.

Этап регистрации пароля пользователя в системе

  1. Вычислим хеш от пароля пользователя H=scrypt(password,login).

  2. Сформируем закрытый ключ подтверждения e=hmac_H (salt); сжимаем значение, чтобы удовлетворить условию e \in \{1,...,n-1\} (можно операцией XOR старшей и младшей частей).

  3. Сформируем открытый ключ подтверждения D = eG, где G – базовая точка подгруппы.

  4. Посылаем серверу открытый ключ подтверждения D, вместе с login, salt.

Эта же операция производится и при смене пароля пользователем. Но можно менять не пароль, а соль. Пароль также может быть установлен администратором системы. Здесь проблем нет.

Этап аутентификации пользователя в системе

  1. Делаем запрос на сервер с логином login.

  2. Сервер посылает нам соль salt учетной записи login и 128-битное значение challenge Cs.

  3. Клиент вычисляет свой закрытый ключ e на базе пароля, логина и соли, аналогично пп. 1-2 этапа регистрации.

  4. Генерирует свое 128-битное значение challenge Cc.

  5. Формирует сообщение message = Cc ^ Cs.

  6. Делает подпись сообщения по алгоритму ECDSA (r, s) = Fe(message).

  7. Передает серверу пару чисел (r, s) и свой challengeCc.

  8. Сервер формирует message аналогично клиенту п. 5.

  9. Сервер по алгоритму ECDSA вычисляет точку эллиптической кривой P = FD(r, s, message). Авторизация пройдена, только если r = Px mod n, где Px - координата x точки P.

Подробности реализации функции Fe(message)

Здесь e – секретный ключ клиента.

  1. Клиент генерирует случайное натуральное k \in \{1,...,n-1\}, где n – порядок подгруппы;

  2. Вычисляет точку P = kG (где G – базовая точка подгруппы);

  3. Вычисляет r = Px mod n, где Px – x-координата точки P; если r = 0, возвращаемся к п. 1;

  4. Вычисляет s = k-1(message + re), где e – закрытый ключ клиента, а k-1 – мультипликативная инверсия k по модулю n. Если s = 0, то возвращаемся к п. 1.

  5. Пара чисел (r, s) образует подпись.

Подробности реализации функции Fd(r, s, message)

Здесь D – открытый ключ.

  1. Сервер вычисляет u1=s-1message (mod⁡ n);

  2. Сервер вычисляет u2=s-1 r (mod⁡ n);

  3. Сервер вычисляет точку P=u1G + u2D.

Предлагаемая схема может быть применена не только в Web/HTTP, но и в других протоколах: SMTP/IMAP/POP3, SOAP, SNMP. Правда потребует принятия соответствующих под-стандартов. Эта же схема может быть применена для  парольной авторизации любых ваших приложений, построенных на архитектуре клиент-сервер. Как пример, клиентский агент администрирования антивируса и консоль управления агентами.

В чем преимущества предложенной схемы? Во-первых, клиент вообще никак не передает свой пароль. Никогда и никому. А, значит, и скомпрометировать его будет невозможно. Закрытый ключ клиента e, и секретное число k, никогда не передаются. Взломщик БД (или MitM на этапе регистрации) могут попробовать найти e за какое-то время по известному D (текущий рекорд для кривой со 109-битным n равен 17 месяцам перебора при помощи po-метода Полларда для дискретного логарифмирования [источник]). Но это слишком сложно. И это только на одного пользователя. Самому серверу искать e пользователя нет смысла.

Но уязвимость этого алгоритма в том, что клиент может повторно использовать ранее выбранное k или использовать слабый генератор случайных чисел. И если наш MitM умудрится перехватить две подписи от клиента, с одинаковым k, то он сможет определить закрытый ключ!

В вероятность этого стремится к нулю по многим причинам. Но даже, если такое произошло, смена salt пользователя мигом решит проблему. Фактически вместо классической смены пароля пользователя тут можно предложить обновление его salt и, как следствие, открытого ключа. На мой взгляд, очень удобно. Сравните с классической схемой, когда при взломе сайта, вы вынуждены (не по вашей вине) менять свой пароль. Другое дело, когда у вас лично увели пароль (клавиатурным шпионом). Но тут, как говорится, «против лома нет приёма».

Ложка дёгтя в бочке мёда.

Есть одна проблема – математическая. Эллиптические кривые используются в криптографии относительно недавно и ещё слабо изучены. Даже сами математики говорят, что теория эллиптических кривых только развивается. Не случайно, что самые крупные достижения в математике последних лет (теорема о модулярности эллиптических кривых, приведшая к доказательству известного Диафантова уравнения,  [всё ещё спорное] доказательство ABC-гипотезы) тесно связаны с эллиптическими кривыми.

Известно также, что существуют классы эллиптических кривых, которые являются слабыми. Однако на сегодняшний день не известно методик, которые бы однозначно могли оценить надёжность кривых. А вот методики, которые позволяют генерировать слабые кривые, замаскированные под сильные, есть. Поэтому всегда остается вероятность, что выбранный вами для предложенной схемы класс эллиптических кривых, уязвим к атакующему «по середине».

Обнадёживает тут только то, что даже если эллиптическая кривая «подведёт», пароль пользователя всё ещё находится под надёжной защитой благодаря scrypt и hmac.

Схема на базе протокола SRP

SRP — протокол парольной аутентификации, устойчивый к прослушиванию и MITM-атаке и не требующий третьей доверенной стороны. Имеет свой RFC2945. Есть производные от него стандарты для протоколов Telnet, TLS. Единственный из представленных, который проводит окончательную взаимную авторизацию обеих сторон. Хорошо описан как в Википедии, так и на Хабре пользователем @datacompboy.

Тем не менее, для генерации секретного ключа x протокол использует устаревшие подходы: (уже  не криптостойкую) функцию SHA-1 и  простую схему хеширования: x = hash(salt | hash(login | ':' | pass). В свете современных тенденций к взлому паролей брутфорсом офлайн на специализированных ПЛИС или арендованных кластерах, данный подход должен быть улучшен, использованием scrypt.

Для реализации схемы нам понадобятся: большое простое число N, генератор g мультипликативной группы Z*N и параметр k = hash(N, g). В качестве g и N стороны используют числа, определенные в RFC-5054. Так, например, g = 2, а «мультипликативная группа» – это просто степени двойки; точнее их остатки по модулю N.

Процесс регистрации пользователя в системе:

  1. Клиент отправляет серверу свой login и свою 256-битную версию соли saltC.

  2. Сервер генерирует свою 256-битную версию соли saltS для пользователя login.

  3. Клиент формирует общую 256-битную соль: salt = saltC ^ saltS.

  4. Клиент вычисляет секретный 256-битный ключ x = scrypt(password, salt) от своего пароля

  5. Клиент вычисляет верификатор пароля V = gx (mod N).

  6. Клиент посылает на сервер пару (saltC, V).

  7. Сервер вычисляет общую соль salt аналогично клиенту в п. 3.

  8. Сервер хранит учетные данные пользователя в форме: login, salt, V.

Процесс авторизации пользователя в системе:

  1. Клиент формирует случайное 256-битное число a и вычисляет 2048-битное A = ga (mod N).

  2. Клиент делает запрос на сервер со своим login, и числом А.

  3. Сервер формирует случайное 256-битное число b и вычисляет 2048-битное B = (kv + gb) (mod N).

  4. Сервер посылает клиенту пару (salt, B).

  5. Обе стороны вычисляют u = hash(A | B). В оригинальном RFC указано, что параметр u – 32-битное беззнаковое, получаемое от результата примененной хеш-функции hash(A | B).

  6. Пользователь вычисляет свой ключ x на основе пароля и соли, аналогично пп. 4 процесса регистрации.

  7. Пользователь вычисляет сессионный ключ K следующим образом: S = (B – kgx)a + ux (mod N); K = hash(S).

  8. Сервер вычисляет сессионный ключ K следующим образом: S = (Avu)b (mod N); K = hash(S).

  9. Пользователь вычисляет M1 = hash( hash(N) ^ hash(g) | hash(login) | salt | A | B | K ) и отправляет на сервер.

  10. Сервер дождавшись от пользователя M1, проверяет его, производя аналогичные п. 9 вычисления.

  11. Сервер вычисляет M2 = hash( A | M1 | K ) и отправляет клиенту.

  12. Клиент проверяет M2 сервера. На этом этапе аутентификация считается завершенной.

В функции hash операция | означает конкатенацию аргументов. Если предполагается, что ключ K не будет использоваться в дальнейшем взаимодействии (например, для шифрования или аутентификации пакетов), то пп. 9-12 к исполнению обязательны. А если будет, то проверка излишняя, т.к. несогласованный ключ не позволит проводить корректное взаимодействие. Но я бы всё-таки оставил эти пункты обязательными.

Согласно спецификации, пользователь должен прервать аутентификацию, если получил B = 0 (mod N) или u = 0. (Полагаю, что B не должно быть также равно –kV, т.к. в этом случае клиент получит S = 0a+ux (mod N) = 0).

Сервер должен прервать аутентификацию, если получил A = 0 (mod N). Клиент должен первым доказать, что получил тот же ключ K. Если клиенту это не удалось, сервер должен прервать аутентификацию, без своего доказательства K.

Немного воды или, что же автор хотел всем этим сказать?

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

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

Возникает закономерный вопрос? Если эта схема такая крутая и древняя, почему она не получила до сих пор широкого распространения.

Я думаю, что этот протокол просто опередил свое время. Тогда аутентификацию предполагалось выполнять на клиенте в браузере. А javascript находился в зачаточном состоянии и мог, в лучшем случае вывести алертом «hello word». А нужна была арифметика на больших числах. Сейчас уже есть, но время упущено.

И самое главное, веб сообщество до сих пор не осознало, что процесс аутентификации необходимо выносить за рамки прямого взаимодействия сервер–браузер (равно как и аутентификацию в иных системах). В самом деле, ведь никто в здравом уме не хотел бы реализовывать вручную TLS  (на javascipt/PHP/JAVA)? Для этого есть специальные системные библиотеки. Так и аутентификацию необходимо давно снять с плеч javascript, браузера и бэкенда.

И если это осознать, то не трудно видеть, что гораздо более правильным решением, поступить также с аутентификацией: создать протокол более низкого уровня, заточить его на использование парольных менеджеров (как сейчас менеджеры сертификатов и криптопровайдеры). А там уже подтянется и более профессиональная криптография. Именно по этому пути идет ваш покорный слуга, но это тема отдельной статьи.

А кстати, почему это SRP вдруг крут?

Чтобы понять это, необходимо вначале глужбе его изучить. Сравнить с другими методами и протоколами.

Злоумышленник не сможет сделать ничего, даже если взломал сервер и активно контролирует канал. Это очень круто.

Мало того, это единственный протокол, который требует взаимной аутентификации сторон (а значит уходит «фишинг», как класс сетевых атак). Даже HTTPS вам такого не гарантирует (последний лишь гарантирует, что в данный момент вы взаимодействуете с сервером, обладающим доменным именем, которое у вас светится в адресной строке). А вот с этим протоколом, клиент точно может быть уверены, что это именно тот сервер, который знает ваш V. Иначе стороны не получат общий сессионный ключ, и пункты 9-12 потерпят неудачу.

А в качестве подобного эффекта от такой аутентификации – стороны получают совместный сессионный ключ, который может быть использован для последующих целей. Например, для отделения сервером запросов пользователя от запросов другого пользователя; для «подписи» запросов и т.п.

Единственная слабость – отправка в открытом виде верификатора пользователя V во время первичной регистрации.

Если злоумышленник контролирует канал на стороне сервера с самого его основания и всё время (оператор ЦОД), то он может подменять верификатор пользователя на свой, и выполнять прокси-авторизацию. В современных реалиях (когда модны облачные вычисления, аренда серверов, всякие «co-location») данный вид атаки более чем реален. Но стоит хотя бы раз «соскочить» с контроля канала (при смене оператора) – пользователи больше никогда не смогут авторизоваться. Возникнут паника, скандалы, расследования и прочий alarm.

Напротив, контролировать же все каналы пользователя – нереально: пользователи слишком мобильны.

В прочем, подобной слабости подвержены все рассмотренные схемы. Защиты от такого рода атаки нет. Возможно для защиты первичной регистрации клиентов может помочь открытая сеть на базе блокчейн технологии. Но это вопрос будущих исследований. А пока…

7. Подведём итоги

Параметр / Схема

1. Симметричная на базе HASH

2. На базе ECDSA

3. SRP-6a

Скорость

Проверка на сервере реализуется быстрее

Проверка подписи на стороне сервера в 2 раза медленнее клиента

Медленная, требует длинной арифметики и 4 пакета для завершения аутентификации

Технические сложности

Отсутствуют; может быть реализована на нативном js; работает достаточно быстро.

Отсутствует поддержка произвольной кривой в WebCryptoApi на клиенте

Уже никаких

Потенциальные уязвимости

Уязвима к компрометации БД и последующему контролю канала сервера; уязвима в процессе регистрации пользователя

Существуют классы уязвимых эллиптических кривых. Методики оценки безопасности кривых ещё не разработано. Вероятность, что выбрана «слабая» кривая, всегда остается.

Задача решения дискретного логарифма может быть решена теоретически; надежность базируется на сложности факторизации N; числа N, g, k известны всем.

Обратить особое внимание на

Случайное число k, генерируемое клиентом в процессе подписи, не должно использоваться дважды!

Числа A, B не должны быть равны 0 и 0, –vk соответственно.

Защита от пассивного MitM

да (но только не в момент регистрации)

да

да

Защита от активного MitM

да (только, если учетные данные не компрометированы)

да

да

Защита пароля при компрометации БД

да

да

да

Защита после компрометации БД и прослушивании канала

нет

да

да

Препятствие «легальному» доступу злоумышленника после компрометации БД

да

да

да

Взаимная аутентификация сторон

нет

нет

да

Защита от фишинга (попытка злоумышленника выдать себя за легальный сервер)

нет

нет

да

Требует наличие какого-либо доп. защищенного канала при регистрации клиента?

да

нет

нет

Защита слабых паролей в момент авторизации

нет

да

да

Защита от владельцев ЦОД

нет

нет

нет

8. Вместо заключения

Напоследок, приведу примеры авторизаций разных сайтов. Не ручаюсь сказать, кто из них хранит пароли в открытом виде. Но косвенно это можно выяснить, пробуя сайту скармливать сверхдлинные пароли. Если сайт хеширует ваш пароль, то ему всё равно какая длина и какие символы в наборе. А вот если сайт запрещает использовать некоторые символы и ограничивает длину пароля (gosuslugi.ru) – это намёк на нарушение.

Среди исследованных оказались такие:  yandex.ru, mail.ru, gosuslugi.ru, sberbank.ru, vk.com, google.com. В тестах использовались реальные учётные данные. Результат огорчил. Можете проверить.

Ссылки на иные статьи:

Вы опасно некомпетентны в криптографии

Защита пароля при передаче по открытому каналу (часть 2)

Опасность использования «учебных» криптопротоколов

Аутентификация на базе ЭЦП

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

Доступно о криптографии на эллиптических кривых

Курс MIT «Безопасность компьютерных систем». Лекция 17: «Аутентификация пользователя», часть 1

 




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

  1. newpavlov
    /#23797099

    Вместо SRP лучше использовать современные PAKE алгоритмы на основе эллиптических кривых, например, OPAQUE или SPAKE2.

    >Эллиптические кривые используются в криптографии относительно недавно и ещё слабо изучены.

    Лихое, однако, утверждение касающееся львиной доли современной асимметричной криптографии вы озвучили.

    >Однако на сегодняшний день не известно методик, которые бы однозначно могли оценить надёжность кривых.

    Не углубляясь в математику, надёжность secp256k1, как минимум, оценена рыночной капитализацией Биткойна. Чего уж говорить об остальных кривых, обеспечивающих безопасность обмена данных, в т.ч. финансовых сообщений на много-много денег.

    • ifap
      /#23798585

      Погодите, мсье хочет оспорить выкладки Бернштейна и Ко, не надо его демотивировать!

      • KaminskyIlya
        /#23801329

        Ох как хитро завернули, коллега! Я так и не понял, кто мсье, кто хочет оспорить, и кому посвящена ваша реплика. Но меня точно не стоит демотивировать)

        Но судя по тому, что в приведенной Вами ссылке кривая Биткоина называется небезопасной, думаю,

        надёжность secp256k1, как минимум, переоценена

        Или я не прав?

        • ifap
          /#23801367

          Да, двусмысленно вышло, но по идее дискутирую с вами обоими ;) И с тем, что надежность курв неизучена, и с тем, что они вот прям надежны-надежны. Грубо говоря, Бернштейн показал, что для некоторых популярных курв задача дискретного логарифмирования в группе точек эллиптической кривой не всегда решается лишь полным перебором. Т.е. 256 в имени secp256k1 можно назвать смелым утверждением ;) При этом следует помнить, что Бернштейн «автор» Curve25519 и тут присутствует определенный конфликт интересов.

    • KaminskyIlya
      /#23801341

      Лихое, однако, утверждение

      Утверждение, что "теория эллиптических кривых ещё только-только развивается", я не раз слышал от самих математиков на лекциях. Хотя теория такая, что приходится вгрызаться зубами, чтобы хоть что-то понять. Знал бы, что придется держать ответ, собирал бы пруфы.

      Математики вообще скромные ребята: "мы только-только начертили дорожную карту, чтобы понять, куда идти дальше". Сравните со "здание физики уже построено, остались лишь детали".

      Опасения о том, что "не все кривые одинаково полезны", тоже не на пустом месте родились. Особенно после известных в узком кругу скандалов.

      Так что я тут "за что купил, за то и продаю") Не судите строго.

    • KaminskyIlya
      /#23801361

      А Вы не могли бы вкратце пояснить, чем именно лучше? Я сейчас без подвохов спрашиваю.

      Я погрузился в черновик стандарта OPAQUE, но так и не понял, выполняются ли нужные мне требования: взаимная аутентификация сторон, защита клиента даже после взлома сервера при последующем активном вмешательстве в канал (защита от проксирования, навязывания ложной информации).

      Не увидел там формул вообще. А только поверхностное описание протокола при очень подробном описании используемых структур данных. Может не туда смотрел, каюсь заранее.

  2. FSA
    /#23797173

    Не понимаю зачем городить огород для веб. Используешь https, который уже предполагает защиту, передаёшь пароль на сервер в открытом виде. На сервере всё хранится в виде солёного хеша. В той же php есть операторы password_hash и password_verify для подобных операций. Даже если сольют базу, то будут вынуждены искать пароли для каждой учётной записи отдельно. На Python есть аналогичный функционал, как я понял, когда смотрел базу matrix-synapse. К тому же на вебе передать пароль в открытом виде можно через обычную html форму, а для предложенных алгоритмов нужно использовать js.

    • Dotarev
      /#23797187 / +2

      Чтобы понять намек, поищите по фразе "Национальный сертификат безопасности Казахстана"

      • FSA
        /#23797393 / +1

        Слышал о таком. Но если пользователь своими руками даёт способ расшифровывать свой трафик, то чья это вина? К тому же, кто мешает в таком незащищённом соединении модифицировать ваши скрипты и передать пароль в открытом виде на сторону? Да, сложнее, но тоже возможно.

        • Vamp
          /#23800601 / +1

          У среднего пользователя недостаточно квалификации чтобы понять это. Особенно если государство будет продвигать свой СА под соусом "государственной безопасности".

          Да и необязательно заставлять пользователей делать это самостоятельно. Можно просто продавить микрософт, чтобы сертификат "государственной безопасности" прилетел в автоматическом обновлении всем гражданам данного государства. В таком случае пользователь уж точно не будет винить себя.

        • KaminskyIlya
          /#23801369

          Так если бы своими руками давал. Вот в школах интернет "фильтруют" в принудительном порядке. Ровно таким методом.

        • KaminskyIlya
          /#23801375

          К тому же, кто мешает в таком незащищённом соединении модифицировать ваши скрипты и передать пароль в открытом виде на сторону?

          Вот поэтому я и предлагаю в новом протоколе (на базе SRP), вынести процессы авторизации (включая сопутствующие) за рамки "браузер-сервер", как сейчас работает TLS.

      • KaminskyIlya
        /#23801253

        Ага. Это полбеды. А ещё есть такая штука, как StoneGate, у которой заявлено "инспекция HTTPS-трафика со скоростью 40Мб/с". И она вскрывает трафик без всяких предварительных установок сертификатов. Автору самому "больно щелкнули по носу", когда он усомнился в таком. Так что я сейчас немного настороженно отношусь к т.н. гражданской криптографии.

        И кстати, интернет в российских школах некоторых регионов тоже также фильтруется - через установку корневого сертификата. Ключевые слова: InsideSystems и Ростелеком.

        Это в дополнение к Вашему комментарию.

  3. thedrnic
    /#23797277 / +1

    А как «фишинг» уйдёт, если создать реплику сайта с похожим доменом?
    в такой атаке нет необходимости во взаимодействии с оригинальными клиент / сервер.
    останется вопрос как туда траффик нагнать, но это уже другая история

    • KaminskyIlya
      /#23801315

      Если я правильно понял Ваш вопрос.

      В протоколе, который проектирую, само явление "как ввод логина/пароля" на веб-сайте уходит "в отставку". Аутентификацию предлагается проводить через дополнительные HTTP-заголовки по протоколу SRP.

      Сервер себя идентифицирует не по доменному имени (как это принято в SSL), а по идентификатору, который выбирает сам. Как результат, появляется понятие не сайта, а "приложения", которое может быть распределено по нескольким доменам, или, наоборот, несколько приложений (даже от разных вендоров) сконцентрироваться в пределах одного домена.

      Конечно, в такой схеме любой сайт, заманивший пользователя к себе, может попытаться использовать чужой идентификатор и потребовать "авторизоваться" (аналог фишина в моей версии протокола). Но т.к. SRP требует от сторон взаимной проверки, сервер злоумышленника не сможет прикинуться легальным, и клиент это выявит (пп. 9-12 протокола).

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

      Кроме того, в дальнейшем полученный сессионный ключ предлагается использовать для подписи запросов клиента и сервера. Тем самым мешая проксирующему серверу "навязать" неверные данные.

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