Эта публикация является продолжением ранее написанной в нашем блоге: «Реализуем безопасный VPN-протокол». В этой статье мы не переделываем и не переписываем протокол, а только чуть дорабатываем его дальше. Реализация всего нижеописанного уже присутствует в версии GoVPN 3.1.
Для создания шума немного изменён транспортный протокол. Для аугментации рукопожатия и усиления паролей изменён протокол рукопожатия. Более подробно обо всём этом под катом.
В конце предыдущей статьи я заметил, что мы обеспечиваем конфиденциальность содержимого передаваемых данных, но не скрываем размера пакетов и факта их отправки. Иногда даже сам факт (период возникновения пакета) может косвенно с большой вероятностью сказать, что сейчас по шифрованному каналу работает, например, DHCP: вроде и зашифровано, а мы всё равно знаем, какие процессы внутри. Или можно отслеживать корреляцию между поступающим трафиком от одного клиента к исходящему в другом месте, и тем самым его деанонимизировать.
Данную проблему решаем довольно просто, хотя и несколько накладно по ресурсам: добавляем шум к трафику.
В транспортном протоколе после nonce добавлено два байта (которые будут шифроваться), содержащих размер полезной нагрузки. Он может равняться и нулю, что удобно использовать для heartbeat-пакетов, чтобы показать, что клиент/сервер ещё «жив» и в сети. Как побочный эффект: мы уменьшаем MTU виртуального TAP-интерфейса на эти два байта.
Каждый пакет дополняется перед шифрованием нулями для того, чтобы увеличить его размер до максимально возможного отправляемого GoVPN. После шифрования он становится шумом, в котором нельзя понять, где полезная нагрузка, а где бесполезные данные.
Так мы скрыли размер сообщения, но не факт возникновения сообщений в сети. Эта проблема решается просто созданием константного по скорости трафика (constant packet rate). Технически сделано просто: включается генератор «тиков». На каждый тик проверяется, есть ли пакет для отправки. Если нет, то отправляется пустой пакет. Все пакеты дополняются до максимального размера шумом.
Схема формирования пакета транспортного уровня выглядит так:
Как верно заметил пользователь cebka в комментариях к предыдущей публикации, 256-бит публичный ключ Curve25519 является не случайным набором байт, а точкой на эллиптической кривой. Поэтому при попытке его дешифрования мы увидим, что получили не случайные данные, а, собственно, точку, и, тем самым, мы поймём, что успешно подобрали (нашли) общий ключ аутентификации.
Общий ключ аутентификации в предыдущей реализации GoVPN даже в примерах предполагается, что генерировался не из пароля, а из PRNG. Так что на практике, конечно же, просто так перебрать ключ не получилось бы. Однако если мы хотим использовать пароли, то тут это станет проблемой, так как пароли имеют куда меньшую энтропию и поддаются атакам перебора по словарю.
Почему мы хотим использовать пароли? Потому, что в любом случае общий ключ аутентификации должен быть как-то защищён. Либо он хранится на диске, к которому применяется полнодисковое шифрование, либо шифруется, например, PGP и при использовании его дешифрованная версия помещается в оперативную память (временный диск). И диск и PGP, в свою очередь, защищаются парольными фразами. Почему бы не использовать эти парольные фразы напрямую в GoVPN-протоколе, чтобы иметь меньше зависимостей программного обеспечения и векторов для атак?
Небольшое отступление: использовать следует именно парольные фразы, а не пароли. Технически между ними может и не быть разницы для вычислительной машины, но для человека она существенна: пароль это, как правило, короткая строчка высокоэнтропийных (случайных) символов, а парольная фраза – это длинная строчка низкоэнтропийных. Низкая энтропия означает простоту запоминания человеком. Считается, что обычный английский текст содержит 1-2 бита энтропии на символ. Однако если взять сотню символов, то суммарно мы получим сотню бит, как правило, легко запоминаемых. Единственное «но» с технической точки зрения: если пароль ещё можно сохранить в БД (не надо так делать, конечно же), то парольная фраза не удобна для такого и от неё сохраняют хэш.
Чтобы протокол аутентификации мог называться «сильным», то он должен быть безопасен для использования даже со слабыми паролями. В нашем случае пароль «foobar» будет быстро подобран по словарю и дешифрование публичного ключа в момент рукопожатия выдаст, что пароль подобран успешно. То есть это ещё не zero-knowledge-протокол.
Исправить это можно, применив специальное кодирование точек кривых Elligator. Оно позволяет закодировать их так, что они становятся неотличимы от шума. Этого будет достаточно, чтобы протокол стал zero-knowledge и смог использовать даже слабые пароли, при этом назывался «сильным протоколом аутентификации». Elligator применяется к публичному ключу на одной стороне перед шифрованием и инвертируется на противоположной после дешифрования.
Elligator можно применить не ко всем парам ключей Curve25519: в среднем примерно половина точек не может быть закодирована в случайную строку. При генерировании Curve25519 пары ключей мы пробуем закодировать публичный, проверяя получится ли. Если нет, то повторяем процедуру. Получаем неприятный побочный эффект: при генерировании ключей Curve25519 на каждой стороне нам в среднем понадобится в два раза больше энтропии и вычислительных ресурсов.
Протокол после применения Elligator становится zero-knowledge и пригодным для аутентификации со слабыми паролями. Но аутентификационные данные сохраняются на сервере и клиенте. На жёстком диске клиента может быть и нет, так как парольная фраза вводится руками, но на сервере это будет отдельный файл. Компрометация содержимого жёсткого диска сервера, утечка базы данных аутентификационных ключей позволит перебирать пароль, атаковать по словарю. Это очень мощная атака, которая в состоянии восстановить огромное количество используемых людьми паролей и даже парольных фраз.
Если на сервере мы сохраним хэш от пароля (так как его удобно хранить), то злоумышленник просто будет высчитывать хэши от перебираемых паролей и сравнивать с тем, что имеется на жёстком диске. Хэши считаются быстро. Поэтому всегда и везде хранимые пароли или парольные фразы нужно усиливать.
Распространённые методы усиления паролей: PBKDF2, bcrypt, scrypt. Особо не будем вдаваться в описания этих алгоритмов, так как статей на эту тему огромное количество (потому, что до сих пор люди умудряются не использовать ничего из этого, абсолютно не ценя секреты пользователей).
Лично я не рассматриваю bcrypt как вариант, так как штатно длина парольных фраз на вход ограничена 72 символами (особенность Blowfish), что мало (лично у меня все парольные фразы имеют длину 90-110 символов). Да и основной аргумент в пользу bcrypt – это то, что его функция медленнее. Верно, но что мешает увеличить количество итераций PBKDF2? Разница между ними в целом получается очень размытой: суть одна, просто чуть другие инструменты применяются.
Scrypt интересен, но против него тоже есть много доводов, пускай и спорных. О нём можно было бы задуматься плотнее, если бы не финал конкурса Password Hashing Competition, призванного сделать качественную хорошую функцию усиления паролей, учитывающую нагрузку и на память, и временные атаки по сторонним каналам (side channel attack). Там действительно очень интересные идеи, реализации и отлично разбирающиеся в теме «судьи». Но пока финалист не выбран, в GoVPN используется PBKDF2-SHA512.
Как правило, любое усиление заключается в увеличении энтропии паролей и некой дорогостоящей операции. Увеличение энтропии нужно, чтобы, как минимум, усиленные одинаковые пароли не совпали и для этого добавляют так называемую «соль». Дорогая операция в случае PBKDF2 это много (тысячи) итераций хэш-функции. Кроме того, дополнительная энтропия защищает от создания заранее рассчитанных хэш-значений.
В GoVPN в качестве соли, которая не является секретной (не требуется прятать), используется уже имеющийся 128-бит идентификатор клиента.
На сервере сохраняется уже усиленная версия. Она же используется и в протоколе рукопожатия. Перед началом соединения пользователь вводит парольную фразу, она усиливается, используя в качестве соли идентификатор пользователя, и этот результат уже используется как аутентификационный ключ при рукопожатии с сервером. Операция усиления дорогая, но производится только на клиенте в момент запуска демона.
При компрометации базы аутентификационных ключей клиентов на сервере мы вряд ли сможем легко узнать пароли пользователей. Но на руках у нас имеется результат их усиления, используемый для аутентификации сторон. Если эти данные утекут к злоумышленнику, то он сможет представляться клиентом, сможет подключаться к VPN-серверу.
Если мы сможем хранить на сервере нечто, что только сможет удостоверять подлинность аутентификационных данных, но не сможет быть использовано в их качестве, то эта проблема будет решена. Процесс общепринято называется аугментацией («augmentation») и применительно к EKE описан в статье. Вместо паролей на стороне сервера находятся так называемые «проверяльщики» (verifiers).
Вариантов решения этой задачи множество. Мы применим основанный на алгоритмах асимметричных подписей. Конкретно Ed25519 от автора уже используемых нами Curve25519, Salsa20 и Poly1305. Это простой в реализации, быстрый, надёжный (хорошие криптоанализы) алгоритм генерирования и проверки подписей. Кроме того, он не требует дополнительной энтропии при создании подписей.
Суть аугментации в этом случае сводится к тому, что в качестве проверяльщика используется публичный ключ Ed25519 пары, сгенерированной из усиленного пароля. Вместо усиленного пароля для шифрования публичных ключей Диффи-Хельмана используется этот проверяльщик. Клиент дополнительно в конце рукопожатия подписывает используемый общий ключ K, полученный после Диффи-Хельмана и отправляет эту подпись серверу. Так как проверяльщик это просто публичный ключ, то сервер сможет им проверить подпись и убедиться, что клиент действительно имеет приватную часть ключа, которую можно получить, зная только пароль в открытом виде. Злоумышленник не сможет создать подпись и представиться клиентом.
Проверяльщик создаётся на стороне клиента заранее, используя включённую в GoVPN утилиту. После ввода своего идентификатора (который может быть создан на чьей угодно стороне) и парольной фразы, на основе которой создаётся усиленная версия и Ed25519 пара ключей, он отправляет проверяльщик администратору сервера. Как побочный эффект мы получаем увеличение трафика рукопожатия на длину подписи, и трату ресурсов процессора клиента на создание подписи, а сервера на её проверку.
Конечный протокол рукопожатия стал выглядеть так:
Зависимость от качественного PRNG никуда не исчезла и безопасное применение GoVPN под закрытыми проприетарными операционными системами технически невозможно. Исправить это можно только поменяв ОС/платформу по хорошему. Исправлено в версии 3.4: можно использовать сторонние EGD-совместимые PRNG источники.
Единственное, по чему косвенно можно понять, что трафик является GoVPN-специфичным, так это то, что в начале (когда происходит рукопожатие) идёт обмен пакетами всегда чётко заданных размеров и только потом включается «шум». Сообщения рукопожатия неотличимы от шума, не выдают идентификатор клиента, но размер не скрывается. Исправлено в версии 4.0: сообщения рукопожатия можно зашумлять.
Небольшая статистика не текущий момент:
Всего доброго, не переключайтесь!
Сергей Матвеев, Python и Go-разработчик ivi.ru
Наши предыдущие публикации:
» Реализуем безопасный VPN-протокол
» Лишние элементы или как мы балансируем между серверами
» Blowfish на страже ivi
» Неперсонализированные рекомендации: метод ассоциаций
» По городам и весям или как мы балансируем между узлами CDN
» I am Groot. Делаем свою аналитику на событиях
» Все на одного или как мы построили CDN
Для создания шума немного изменён транспортный протокол. Для аугментации рукопожатия и усиления паролей изменён протокол рукопожатия. Более подробно обо всём этом под катом.
Скрытие размера и времени отправки полезной нагрузки
В конце предыдущей статьи я заметил, что мы обеспечиваем конфиденциальность содержимого передаваемых данных, но не скрываем размера пакетов и факта их отправки. Иногда даже сам факт (период возникновения пакета) может косвенно с большой вероятностью сказать, что сейчас по шифрованному каналу работает, например, DHCP: вроде и зашифровано, а мы всё равно знаем, какие процессы внутри. Или можно отслеживать корреляцию между поступающим трафиком от одного клиента к исходящему в другом месте, и тем самым его деанонимизировать.
Данную проблему решаем довольно просто, хотя и несколько накладно по ресурсам: добавляем шум к трафику.
В транспортном протоколе после nonce добавлено два байта (которые будут шифроваться), содержащих размер полезной нагрузки. Он может равняться и нулю, что удобно использовать для heartbeat-пакетов, чтобы показать, что клиент/сервер ещё «жив» и в сети. Как побочный эффект: мы уменьшаем MTU виртуального TAP-интерфейса на эти два байта.
Каждый пакет дополняется перед шифрованием нулями для того, чтобы увеличить его размер до максимально возможного отправляемого GoVPN. После шифрования он становится шумом, в котором нельзя понять, где полезная нагрузка, а где бесполезные данные.
Так мы скрыли размер сообщения, но не факт возникновения сообщений в сети. Эта проблема решается просто созданием константного по скорости трафика (constant packet rate). Технически сделано просто: включается генератор «тиков». На каждый тик проверяется, есть ли пакет для отправки. Если нет, то отправляется пустой пакет. Все пакеты дополняются до максимального размера шумом.
Схема формирования пакета транспортного уровня выглядит так:
Сильный протокол аутентификации по паролю
Как верно заметил пользователь cebka в комментариях к предыдущей публикации, 256-бит публичный ключ Curve25519 является не случайным набором байт, а точкой на эллиптической кривой. Поэтому при попытке его дешифрования мы увидим, что получили не случайные данные, а, собственно, точку, и, тем самым, мы поймём, что успешно подобрали (нашли) общий ключ аутентификации.
Общий ключ аутентификации в предыдущей реализации GoVPN даже в примерах предполагается, что генерировался не из пароля, а из PRNG. Так что на практике, конечно же, просто так перебрать ключ не получилось бы. Однако если мы хотим использовать пароли, то тут это станет проблемой, так как пароли имеют куда меньшую энтропию и поддаются атакам перебора по словарю.
Почему мы хотим использовать пароли? Потому, что в любом случае общий ключ аутентификации должен быть как-то защищён. Либо он хранится на диске, к которому применяется полнодисковое шифрование, либо шифруется, например, PGP и при использовании его дешифрованная версия помещается в оперативную память (временный диск). И диск и PGP, в свою очередь, защищаются парольными фразами. Почему бы не использовать эти парольные фразы напрямую в GoVPN-протоколе, чтобы иметь меньше зависимостей программного обеспечения и векторов для атак?
Небольшое отступление: использовать следует именно парольные фразы, а не пароли. Технически между ними может и не быть разницы для вычислительной машины, но для человека она существенна: пароль это, как правило, короткая строчка высокоэнтропийных (случайных) символов, а парольная фраза – это длинная строчка низкоэнтропийных. Низкая энтропия означает простоту запоминания человеком. Считается, что обычный английский текст содержит 1-2 бита энтропии на символ. Однако если взять сотню символов, то суммарно мы получим сотню бит, как правило, легко запоминаемых. Единственное «но» с технической точки зрения: если пароль ещё можно сохранить в БД (не надо так делать, конечно же), то парольная фраза не удобна для такого и от неё сохраняют хэш.
Чтобы протокол аутентификации мог называться «сильным», то он должен быть безопасен для использования даже со слабыми паролями. В нашем случае пароль «foobar» будет быстро подобран по словарю и дешифрование публичного ключа в момент рукопожатия выдаст, что пароль подобран успешно. То есть это ещё не zero-knowledge-протокол.
Исправить это можно, применив специальное кодирование точек кривых Elligator. Оно позволяет закодировать их так, что они становятся неотличимы от шума. Этого будет достаточно, чтобы протокол стал zero-knowledge и смог использовать даже слабые пароли, при этом назывался «сильным протоколом аутентификации». Elligator применяется к публичному ключу на одной стороне перед шифрованием и инвертируется на противоположной после дешифрования.
Elligator можно применить не ко всем парам ключей Curve25519: в среднем примерно половина точек не может быть закодирована в случайную строку. При генерировании Curve25519 пары ключей мы пробуем закодировать публичный, проверяя получится ли. Если нет, то повторяем процедуру. Получаем неприятный побочный эффект: при генерировании ключей Curve25519 на каждой стороне нам в среднем понадобится в два раза больше энтропии и вычислительных ресурсов.
Усиление паролей
Протокол после применения Elligator становится zero-knowledge и пригодным для аутентификации со слабыми паролями. Но аутентификационные данные сохраняются на сервере и клиенте. На жёстком диске клиента может быть и нет, так как парольная фраза вводится руками, но на сервере это будет отдельный файл. Компрометация содержимого жёсткого диска сервера, утечка базы данных аутентификационных ключей позволит перебирать пароль, атаковать по словарю. Это очень мощная атака, которая в состоянии восстановить огромное количество используемых людьми паролей и даже парольных фраз.
Если на сервере мы сохраним хэш от пароля (так как его удобно хранить), то злоумышленник просто будет высчитывать хэши от перебираемых паролей и сравнивать с тем, что имеется на жёстком диске. Хэши считаются быстро. Поэтому всегда и везде хранимые пароли или парольные фразы нужно усиливать.
Распространённые методы усиления паролей: PBKDF2, bcrypt, scrypt. Особо не будем вдаваться в описания этих алгоритмов, так как статей на эту тему огромное количество (потому, что до сих пор люди умудряются не использовать ничего из этого, абсолютно не ценя секреты пользователей).
Лично я не рассматриваю bcrypt как вариант, так как штатно длина парольных фраз на вход ограничена 72 символами (особенность Blowfish), что мало (лично у меня все парольные фразы имеют длину 90-110 символов). Да и основной аргумент в пользу bcrypt – это то, что его функция медленнее. Верно, но что мешает увеличить количество итераций PBKDF2? Разница между ними в целом получается очень размытой: суть одна, просто чуть другие инструменты применяются.
Scrypt интересен, но против него тоже есть много доводов, пускай и спорных. О нём можно было бы задуматься плотнее, если бы не финал конкурса Password Hashing Competition, призванного сделать качественную хорошую функцию усиления паролей, учитывающую нагрузку и на память, и временные атаки по сторонним каналам (side channel attack). Там действительно очень интересные идеи, реализации и отлично разбирающиеся в теме «судьи». Но пока финалист не выбран, в GoVPN используется PBKDF2-SHA512.
Как правило, любое усиление заключается в увеличении энтропии паролей и некой дорогостоящей операции. Увеличение энтропии нужно, чтобы, как минимум, усиленные одинаковые пароли не совпали и для этого добавляют так называемую «соль». Дорогая операция в случае PBKDF2 это много (тысячи) итераций хэш-функции. Кроме того, дополнительная энтропия защищает от создания заранее рассчитанных хэш-значений.
В GoVPN в качестве соли, которая не является секретной (не требуется прятать), используется уже имеющийся 128-бит идентификатор клиента.
На сервере сохраняется уже усиленная версия. Она же используется и в протоколе рукопожатия. Перед началом соединения пользователь вводит парольную фразу, она усиливается, используя в качестве соли идентификатор пользователя, и этот результат уже используется как аутентификационный ключ при рукопожатии с сервером. Операция усиления дорогая, но производится только на клиенте в момент запуска демона.
Аугментация аутентификации
При компрометации базы аутентификационных ключей клиентов на сервере мы вряд ли сможем легко узнать пароли пользователей. Но на руках у нас имеется результат их усиления, используемый для аутентификации сторон. Если эти данные утекут к злоумышленнику, то он сможет представляться клиентом, сможет подключаться к VPN-серверу.
Если мы сможем хранить на сервере нечто, что только сможет удостоверять подлинность аутентификационных данных, но не сможет быть использовано в их качестве, то эта проблема будет решена. Процесс общепринято называется аугментацией («augmentation») и применительно к EKE описан в статье. Вместо паролей на стороне сервера находятся так называемые «проверяльщики» (verifiers).
Вариантов решения этой задачи множество. Мы применим основанный на алгоритмах асимметричных подписей. Конкретно Ed25519 от автора уже используемых нами Curve25519, Salsa20 и Poly1305. Это простой в реализации, быстрый, надёжный (хорошие криптоанализы) алгоритм генерирования и проверки подписей. Кроме того, он не требует дополнительной энтропии при создании подписей.
Суть аугментации в этом случае сводится к тому, что в качестве проверяльщика используется публичный ключ Ed25519 пары, сгенерированной из усиленного пароля. Вместо усиленного пароля для шифрования публичных ключей Диффи-Хельмана используется этот проверяльщик. Клиент дополнительно в конце рукопожатия подписывает используемый общий ключ K, полученный после Диффи-Хельмана и отправляет эту подпись серверу. Так как проверяльщик это просто публичный ключ, то сервер сможет им проверить подпись и убедиться, что клиент действительно имеет приватную часть ключа, которую можно получить, зная только пароль в открытом виде. Злоумышленник не сможет создать подпись и представиться клиентом.
Проверяльщик создаётся на стороне клиента заранее, используя включённую в GoVPN утилиту. После ввода своего идентификатора (который может быть создан на чьей угодно стороне) и парольной фразы, на основе которой создаётся усиленная версия и Ed25519 пара ключей, он отправляет проверяльщик администратору сервера. Как побочный эффект мы получаем увеличение трафика рукопожатия на длину подписи, и трату ресурсов процессора клиента на создание подписи, а сервера на её проверку.
Конечный протокол рукопожатия стал выглядеть так:
rand(Xbit) | чтение X бит из PRNG |
CDHPriv | приватный Диффи-Хельман ключ клиента |
SDHPriv | приватный Диффи-Хельман ключ сервера |
CDHPub | публичный Диффи-Хельман ключ клиента |
SDHPub | публичный Диффи-Хельман ключ сервера |
enc(K, N, D) | Salsa20 шифрование ключом K, nonce N, данных D |
H() | хэш-функция HSalsa20. Не принципиально какая тут. Могла бы быть SHA2 |
El() | функция кодирования точки кривой Elligator, а также инвертирование этого действия |
DSAPub | Ed25519 публичный ключ клиента, сгенерированный на основе его пароля |
DSAPriv | Ed25519 приватный ключ клиента, сгенерированный на основе его пароля |
Sign(K, D) | генерирование Ed25519-подписи приватным ключом K данных D |
Verify(K, D) | проверка Ed25519 подписи публичным ключом K данных D |
Что ещё стоит сделать или исправить?
Зависимость от качественного PRNG никуда не исчезла и безопасное применение GoVPN под закрытыми проприетарными операционными системами технически невозможно. Исправить это можно только поменяв ОС/платформу по хорошему. Исправлено в версии 3.4: можно использовать сторонние EGD-совместимые PRNG источники.
Единственное, по чему косвенно можно понять, что трафик является GoVPN-специфичным, так это то, что в начале (когда происходит рукопожатие) идёт обмен пакетами всегда чётко заданных размеров и только потом включается «шум». Сообщения рукопожатия неотличимы от шума, не выдают идентификатор клиента, но размер не скрывается. Исправлено в версии 4.0: сообщения рукопожатия можно зашумлять.
Небольшая статистика не текущий момент:
Overhead транспортного протокола | 26 байт на Ethernet пакет TAP интерфейса |
Overhead протокола рукопожатия | 264 байт, 2 пакета от клиента, 2 от сервера |
Пропуск IPv4 TCP трафика | 786 Mbps на amd64 FreeBSD 10.1, Intel i5-2450M CPU 2.5 GHz, Go 1.5.1, загружено демоном одно ядро |
Размер кода ф-ии (де)шифрования транспортного протокола | 1 экран, 1 экран |
Размер кода серверной, клиентской части протокола рукопожатия | 2 экрана, 1.5 экрана |
Поддерживаемые платформы | i386/amd64 GNU/Linux и FreeBSD |
Доступно в виде пакетов в | Arch Linux, FreeBSD |
Сергей Матвеев, Python и Go-разработчик ivi.ru
Наши предыдущие публикации:
» Реализуем безопасный VPN-протокол
» Лишние элементы или как мы балансируем между серверами
» Blowfish на страже ivi
» Неперсонализированные рекомендации: метод ассоциаций
» По городам и весям или как мы балансируем между узлами CDN
» I am Groot. Делаем свою аналитику на событиях
» Все на одного или как мы построили CDN