42,84
рейтинг
7 мая 2015 в 15:30

Разработка → Реализуем ещё более безопасный VPN-протокол

Эта публикация является продолжением ранее написанной в нашем блоге: «Реализуем безопасный 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 пара ключей, он отправляет проверяльщик администратору сервера. Как побочный эффект мы получаем увеличение трафика рукопожатия на длину подписи, и трату ресурсов процессора клиента на создание подписи, а сервера на её проверку.

Конечный протокол рукопожатия стал выглядеть так:


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
Автор: @stargrave2

Комментарии (27)

  • 0
    Elligator взяли референсную реализацию или свою? Если свою, то можно глянуть?
    • 0
      Реализация используется вот эта: github.com/agl/ed25519/blob/master/extra25519/extra25519.go#L93
      Автор говорит что просто под копирку реализовал это на Go гляда на reference C: www.imperialviolet.org/2013/12/25/elligator.html
      • 0
        Еще есть неплохая библиотека от Mike Hamburg для таких операций. Подробнее про нее и практическое применение elligator тут: www.mail-archive.com/curves@moderncrypto.org/msg00412.html
      • 0
        у вас на Go? Потому что по второй ссылке референс кода на С не очень много…
        • 0
          Да, на Go. По ссылке ведёт на статью блога автора кода. Говорит что основано на SUPERCOP C реализации, собственно упоминаемого на сайте Ed25519 сразу же: ed25519.cr.yp.to/software.html. Я сравнивал только то что тестовые векторы совпадают и тесты проходят.
          • 0
            Ясно, просто я кагбэ больше на c# и немного офигеваю от объема кода, который надо переносить. А тестовые вектора не подскажете где лежат? Интересно допилить
  • 0
    Не знаю, что вы так привязались к «закрытым проприетарным» системам. Задача создания безопасных соединений всегда очерчивается моделью угроз. Если вы используете «открытый» ГПСЧ (хотя бы тот же fortuna от FreeBSD), то это все равно не дает гарантий, что вы защищены, скажем, от закладок в оборудовании или чего-то подобного Dual EC DRBG (как видите, открытость все равно не исключает закладок в коде). В таком случае, если мы исключаем collocated adversary из модели угроз, то можно взять хотя бы тот же RDRAND + какие-то шумы от пользователя, скормить их фортуне или же взять тот же PRF на базе чачи. И это будет все равно лучше, чем упорствовать, что работать в закрытых проприетарных системах не может быть безопасным (потому что в вашей модели угроз безопасности все равно не существует).
  • –1
    1. Когда будет референс на сях?

    2. Чему равен «1 экран»?

    3. 366 мегабит против 100 предыдущей версии это результат оптимизаций в го или просто хеон был очень старым? Или что то ещё поменялось?
    • +1
      1. Референс чего на C? Реализации GoVPN на C? Не будет. Код должен быть reviewable, одна из целей этого проекта.
      2. Само собой 1 экран я написал только для прикидки. В моём случае это ровно 45 строк.
      3. Это результат оптимизации кода в последних коммитах, где уменьшено кол-во вновь создаваемых переменных, за счёт чего для garbage collector осталось значительно меньше работы.
  • 0
    Портировал код Elligator на c#, даже тесты проходят. Правда беспокоит пара моментов:

    1) Открытый ключ, полученный при помощи Elligator, не такой же как при обычном преобразовании ed25519
    2) ed25519 вроде как для подписи, а нам нужно согласование ключей
    • 0
      1) Обычное преобразование сделает точку на кривой. Elligator сделает нечто похожее на шум.
      2) Elligator в GoVPN и применяется для Curve25519 Диффи-Хельман ключей. Ed25519 Elligator-ом не затрагивается. Ed25519 публичные ключи это verifier для проверки пароля, лежат на жёстком диске сервера, по сети не передаются.
      • 0
        1) Я имел в виду, что если обернуть Elligator, то получившийся ключ будет не таким же, как если бы мы получали его из закрытого обычным способом и это довольно странно.
        2) Curve25519 от Ed25519 отличается форматом кривой, при этом тот код, который вы привели, работает для Ed25519, а не для Curve25519. Вы это учитываете у себя в коде?
        • 0
          1) Таков алгоритм уж.
          2) Документация библиотеки говорит об обратном. Надо будет проверить. Хотя странно как бы оно работало если бы пыталось применить алгоритм к разноформатным кривым и не падать.
          • 0
            Я был не прав, эллигатор все правильно генерит именно для Curve25519 (а я смотрел на ключи для ed25519) Меня просто смутило, что код лежит в файле d25519/extra25519/extra25519.go и все методы используются от Ed25519 (edwards25519.FeMul(&t0, &t0, &u) ...)
            Так что, всё гуд, я спокоен )
  • 0
    Теперь, после того как я сам реализовал Elligator, у меня возник вопрос: А нафига он нам нужен? Почему, например, не шифровать открытый ключ каким то быстрым шифром с известным ключом (по сути, ксорить с заранее выбранной псевдослучайной последовательностью)? Проще же, и быстрее, судя по объему вычислений
    • 0
      Ну во-первых в GoVPN публичный ключ шифруется при передаче. В качестве случайной последовательности можно выбрать некий высокоэнтропийный PSK ключ — изначально в GoVPN так и было. Однако хочется чтобы можно было вместо выскокоэнтропийных PSK использовать пароль. Пароль эта штука которая уязвима к атакам по словарю, его можно пробовать перебирать. Мы перехватываем зашифрованный публичный DH ключ и перебираем пароли, пытаясь его дешифровать. Без Elligator мы внезапно сможем увидеть что при дешифровании получили не случайный непонятный шум, а как бы 25519 точку на кривой — после этого мы понимаем что пароль подобрали успешно. С Elligator-ом мы не знаем правильно ли мы дешифровали или нет (zero-knowledge).
      • 0
        Почему не знаете? Просто каждый раз пытаетесь обернуть его, вот и всё. Просто еще один шаг который надо выполнить
        • 0
          Если смогли преобразовать из Elligator, то и в него назад сможем же. Или я не понял вас.
  • 0
    Без Elligator мы внезапно сможем увидеть что при дешифровании получили не случайный непонятный шум, а как бы 25519 точку на кривой


    Зная, что может использоваться Elligator, мы этот шум превратим в точку автоматически. Это просто еще одно преобразование, которое нужно принимать во внимание. Чем оно лучше шифрования по известному ключу?
    • +1
      Elligator *любой* шум превратит в точку. В итоге мы при каждом дешифровании получим точки (валидные публичные DH ключи). Мы не знаем какой конкретно DH ключ (точка) используется на деле. Без Elligator мы получим при дешифровании: либо то что не является точкой, либо точку. Здесь мы везде получаем то что Elligator сможет преобразовать в точки, то есть получаем точки всегда.
      • 0
        О, вот щас понятно стало, спасибо
  • +1
    Конструкции
    if err != nil {
        panic(err)
    } 
    

    Можно заменить на
    func must(err) {
       if err != nil {
            log.Fatal(err)
       }
    }
    // ...
    must(err)
    

    Ну или хотя бы не вызывать panic() там, где нужен log.Fatal(), потому что стек-трейсы не нужны в таком случае (ошибка конфигурации, например).

    Было бы приятно видеть проект на github (хотя бы зеркало) :)
    Огромное спасибо за проделанную работу!
    • +1
      Да, согласен насчёт того что panic() вреден. Собственно сейчас это у меня первое в списке что надо исправить, так как оно не красиво и пугает пользователей. Но я за использование без func() — чтобы меньше overhead был.

      На github он имеется с самого начала: github.com/stargrave/govpn
      • 0
        Overhead не имеет значения для проверки конфигурации, например, тут. В основном цикле простой if оправдан, да и обычно там таких проверок не так много/они не подряд идут.

        Спасибо за ссылку, упустил, что он уже есть на гитхабе.
        А можно сделать как-нибудь проект «go-gettable»? Я вижу, что используются конкретные коммиты для внешних зависимостей, это можно решить с помощью утилиты из PackageManagementTools.
        Хотя это не очень важно, учитывая простоту развертывания проектов на go.
        • +1
          Да, насчёт конфигурации полностью согласен. Беру на заметку, скоро закоммичу.

          В самом начале govpn ещё можно было установить через go get, сейчас нет. Мне не нравится идея впиливать какие-то зависимости и инструменты для задач которые уже могут быть решены имеющимся инструментариев в виде Makefile-ов.

          go get не делает криптографические проверки кода который скачался. HTTPS сертификат для Github это зависимость от PKI. Я не доверяю PKI для таких вещей. Отдельные tarball-ы с PGP-подписями: это старый добрый проверенный вариант.

          Кроме того через go get не будет документации видно Texinfo-вской. Делать доку исключительно внутри Go кода: как вариант, но она не такая красивая, структурированная и пригодная для выкладки как статичный HTML-сайт. Можно ещё коммитить готовые .info/.html файлы, но это уж совсем не правильно: репозиторий это для разработки и не стоит в нём хранить генерируемые вещи (не исходники).

          Release tarball содержат все требуемые библиотеки, собранную документацию, подпись не зависящую от PKI.
          • 0
            Минус вашего подхода в том, что вы используете import «govpn» в своем коде.
            Например, если у кого-нибудь возникнет идея использовать код govpn для своего проекта (вот мне как раз не хватало vpn для проекта обхода блокировок РКМ), то это добавит сложностей. Я считаю, что выбор способа проверки кода/бинарников/etc лучше оставить конечному пользователю, как вы это частично и сделали уже (сайт доступен по HTTP).
            По поводу красивости доков — readthedocs в этом смысле предпочтительней. Доки внутри go кода обычно используют для godoc, у них немного предназначение другое.

            • 0
              А можно поподробней каких сложностей? Если речь про namespace и то что govpn может коррелировать с другими названиями — да, тут не спорю что может возникнуть фигня. Но а какие пути решения с namespace-ом? Классический подход привязываться к github.com и прочему мне не нравится тем, что сегодня это github, завтра какой-то другой. Если кто-то делает fork (в терминах github) библиотеки где прописаны «github.com/stargrave», то тут тоже возникнут проблемы.

              Способ аутентификации кода: иметь выбор это безусловно полезно. Но я, как разработчик, тоже должен оградить себя от возможных нападок со стороны пользователей связанных с тем что они или не проверяют что скачали или полагаются на информацию официально разработчиком не подтверждаемую. И тут похоже мне надо поднять HTTPS сервер и предоставить его данные о сертификате, но технически мне кажется что это куда более геморройнее (указывать всяким git clone, wget, go get, whatever сертификат).

              Да, я про godoc как-раз говорил. Они для разработчика всё же заточены сильно. Вы имеете в виду чтобы readthedocs.org использовать пользователям для чтения документации? Иметь зависимость от подключения к Интернету? Опять же отсутствие проверки что информация которую они выведут будет достоверна (аутентифицирована). Online ресурсы — никогда не вариант. Только разве что для ознакомления и первого взгляда, а для этого есть собственно просто сайт проекта.

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

Самое читаемое Разработка