Привет, %username%!


Что может быть проще, чем отправить зашифрованное сообщение собеседнику?

Берём его публичный ключ, свой приватный, считаем Diffie-Hellman, загоняем после KDF результат в какой-нибудь AES, и готово. Но, если один из приватных ключей украдут, всё, весь перехваченный до этого трафик будет без проблем расшифрован.

Мы расскажем как с этим борются современные мессенджеры и транспортные протоколы, а так же дадим вам инструмент для внедрения PFS в свои продукты.

Для понимания PFS ключевым моментом является эфемерная ключевая пара, далее для простоты публичный ключ эфемерной пары я буду просто называть эфемерный ключ. Эфемерная ключевая пара генерируется непосредственно перед установкой сессии, живёт только до окончания процесса установки соединения (handshake) и уничтожается сразу после использования. Этим достигается уникальность сессионных симметричных ключей, использующихся после хэндшейка. О том, как асиметричные ключи превращаются в симметричные, немного позже.

Сразу же возникает несколько вопросов по поводу использования эфемерных ключей:

  1. Как доверять эфемерному ключу?
  2. Как передать собеседнику свой эфемерный ключ?

Рассмотрим их подробнее.

Доверие к эфемерному ключу


Абы какой ключ принимать нельзя, ведь он никак не привязан к собеседнику на том конце провода. Мы должны аутентифицировать ключ и можем сделать это несколькими способами:

  1. Подписать эфемерный ключ. Так делает TLS.
  2. Использовать несколько операций Diffie Hellman между разными ключами. Так делает Noise protocol.
  3. Комбинированный подход (Signal, Whatsapp).

В первом случае всё понятно, перед соединением мы генерируем эфемерную ключевую пару и подписываем её приватным ключом, например, сертификата. Клиент убеждается, что ключ «откуда надо» и использует его.

В некоторых случаях можно обойтись и без цифровой подписи. Noise Protocol в зависимости от конкретного паттерна «подмешивает» результаты DH между эфемерными и статическими ключами к общему состоянию. Таким образом достигается связанность между главным статическим ключом и эфемерным.

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



называется X3DH.

В ней используются несколько типов ключей:

  • IK — Identity ключи. Это постоянные статические ключи, которые генерируются один раз.
  • EK — эфемерный ключ, который генерируется отправителем перед началом сессии.
  • SPk — подписанный Identity ключом эфемерный ключ со среднем временем жизни около недели.
  • OPK — одноразовый эфемерный ключ.

Так же мы видим 4 операции DH, одна из которых, с одноразовым ключом, опциональная.
DH проводятся между ключами разных типов, как эфемерных так и статических. Комбинация различных DH позволяет добиться как взаимной аутентификации, так защиты от последствий кражи главных статических (Identity) приватных ключей.

Далее поговорим зачем нужны аж 2 вида эфемерных ключей.

Доставка эфемерных ключей


Самый простой способ — онлайн соединение, когда мы можем передать эфемерный ключ прямо во время хэндшейка. TLS, NoiseSocket именно так и делают. Но что если наш собеседник оффлайн, а послать сообщение все равно нужно?

Можно попробовать использовать схему ECIES,



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

Нужен надёжный механизм доставки отправителям эфемерных ключей получателей вне зависимости от их нахождения в сети!

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

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

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

Раньше так делали Signal и WhatsApp. А теперь может делать кто угодно.

Virgil PFS


Нам понравилась идея с предзагрузкой одноразовых ключей на сервер и было решено сделать для всех универсальный PFS, похожий на тот, что есть в Signal. Поскольку у нас уже есть Virgil Cards, сделать это было не трудно. Из каких же частей состоит наш PFS?

Virgil Cards Service


Это наш обычный сервис карточек. На нём хранятся долговременные Identity карты.

PFS Service


Отвечает за хранение и выдачу одноразовых и многоразовых эфемерных карточек, подписанных приватными ключами Identity карт.

PFS Протокол


Описанный выше X3DH + формирование сессионных симметричных ключей + формирование ключей для шифрования индивидуальных сообщений.

SDK


Поддерживаются IOS, Android и C# (+ Xamarin). SDK ответственен за управление жизненным циклом эфемерных ключей, сессий, шифрование и расшифровку сообщений. Все SDK совместимы между собой.

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

Virgil PFS Protocol


Начинается он с X3DH по тому же принципу, что и в Signal. После выполнения DH уничтожаем соответствующие приватные эфемерные ключи.



Отличий два.

  1. Используются не просто голые ключи, а Virgil Cards
  2. Все эфемерные ключи подписаны приватным Identity ключом

Соответствующие Virgil Cards:

  • IC — Identity Card. Главная карта пользователя или устройства.
  • OTC — One Time Card. Одноразовая эфемерная карта.
  • LTC — Long Term Card, многоразовая эфемерная карта.
  • EK — Эфемерный ключ, который передаётся в первом сообщении.

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

Установка сессии


Алиса хочет пообщаться с Бобом. Её действия:

  1. Получить с Virgil Cards Service Identity карту Боба, проверить цифровую подпись приложения. Это делается один раз.
  2. Получить с PFS сервиса бандл эфемерных карт. Либо LTC+OTC, либо только LTC если нет OTC.
  3. Сгенерировать эфемерную ключевую пару.
  4. Посчитать все DH по схеме выше.
  5. Превратить эти DH в сессию.

После этого Алиса отправляет Бобу первое сообщение, содержащее следующие данные:

  • Identity Card ID Алисы
  • Identity Card ID Боба (у Боба может быть несколько Identity Cards)
  • Long Term Card ID Боба
  • One Time Card ID Боба (если есть)
  • Эфемерный ключ
  • Подпись Эфемерного ключа

Боб, получив это сообщение, достаёт соответствующие приватные OTC, LTC, Identity ключи и повторяет DH по схеме выше. Сразу после этого приватный ключ OTC уничтожается.

Далее, мы не можем напрямую использовать результаты DH, это небезопасно. Поэтому мы прогоняем их через Key Derivation, а именно HKDF. Это специальная функция, которая превращает некоторую «слабую ключевую информацию», в данном случае, разультаты DH, в «сильную ключевую информацию» путём использования HMAC-Hash.

Из первого HKDF мы получаем 4 значения, каждое по 32 байта:

  1. Секрет отправителя SKa
  2. Секрет получателя SKb
  3. Session ID key
  4. Additional data key

Затем мы получаем Session ID и Additional Data, прогнав Session ID key и Additional data key каждый еще раз через HKDF. Это нужно, чтобы убрать зависимость ключевых данных от дополнительных служебных.

Таким образом, любая сессия состоит из четырёх значений:

2 разных секрета для отправки и получения сообщений, уникальный ID сессии и дополнительные 32 байта, которые будут участвовать в качестве Additional data в AEAD схеме шифрования при непосредственном шифровании сообщений.

Как я уже говорил, Virgil не занимается передачей сообщений. Поэтому в каждое из них мы должны добавлять Session ID, чтобы наш SDK понял каким ключом какое сообщение расшифровывать.

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

  1. Генерируем 16 случайных байт соли.
  2. Получаем индивидуальный ключ и nonce для шифрования сообщения, прогнав соответствующий Секрет с солью через HKDF.
  3. Шифруем сообщение полученным ключом, nonce. В качестве Additional data передаем значение из сессии.
  4. Отправляем Session ID, соль, шифротекст.

Таким образом, каждое сообщение шифруется своим уникальным ключом.

Менеджмент эфемерных ключей и сессий


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

Проблем было две. Первая — что делать с эфемерными карточками, которые взяли но не использовали. Мы удаляем их не сразу, а через некоторое время после того, как узнали что их больше нет на сервере. Это даёт шанс тем сообщениям, которые еще не пришли но скоро придут.

Вторая — обновление сессий. Для пущей безопасности есть необходимость периодически создавать новые сессии с одним и тем же собеседником и дропать старые. Если сессия «утечет», то у атакующего будет возможность расшифровать лишь небольшой объем данных. Чтобы всё происходило прозрачно мы даём некоторое время старым сессиям перед тем как дропнуть их навсегда.

Код!


Мы сделали замечательные обучающие материалы, которые помогут вам освоить PFS и внедрить его в свои приложения.



Надеюсь, вам понравилось. Вопросы?

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


  1. den_admin
    07.09.2017 11:49

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


    1. Scratch Автор
      07.09.2017 12:16

      Строгих доказательств стойкости X3DH ко всем видам атак, включая подмену эфемерных ключей по пути, пока нет. Мы решили перестраховаться на самый крайний случай