Аутентифицированный обмен ключами по схеме X3DH

X3DH (Extended Triple Diffie-Hellman) строится вокруг идеи установления общего секретного ключа между двумя сторонами, которые могут никогда не встречаться одновременно, при этом обеспечивая аутентификацию и защиту от атак типа man-in-the-middle. В экосистеме JavaScript криптографии, где используется TweetNaCl.js или его более низкоуровневый аналог nacl.js, реализация этой схемы опирается на операции над кривой Curve25519, а также на корректное управление набором предварительно созданных ключей (prekeys).


Библиотека TweetNaCl.js предоставляет минималистичный набор примитивов, необходимых для построения протоколов обмена ключами и шифрования:

  • nacl.box.keyPair() — генерация ключевой пары Curve25519 (X25519 для обмена ключами)
  • nacl.scalarMult() — выполнение Diffie-Hellman на Curve25519
  • nacl.sign.keyPair() — генерация ключей Ed25519 для цифровой подписи
  • nacl.hash() — криптографическое хэширование (SHA-512-эквивалент в реализации NaCl)
  • nacl.secretbox() — симметричное шифрование после установления ключа

Важно понимать, что X3DH не реализован в библиотеке напрямую — он строится поверх этих примитивов.


Архитектура X3DH

Протокол X3DH предполагает участие двух сторон:

  • Инициатор (Alice)
  • Получатель (Bob)

Bob заранее публикует набор публичных ключей:

  • Identity Key (IK_B)
  • Signed PreKey (SPK_B)
  • One-Time PreKeys (OPK_B[0..n])

Alice использует эти данные для вычисления общего секрета без необходимости синхронного взаимодействия.


Набор ключей получателя

У Bob формируются следующие ключевые элементы:

  1. Identity Key (IK_B) Постоянная ключевая пара Ed25519/X25519, идентифицирующая пользователя.

  2. Signed PreKey (SPK_B) Временный ключ Curve25519, подписанный приватным Identity Key для подтверждения подлинности.

  3. One-Time PreKeys (OPK_B) Дополнительные одноразовые ключи для повышения безопасности первого соединения.

Подпись SPK_B выполняется через Ed25519:

const signed = nacl.sign.detached(spkPublicKey, identitySecretKey);

Генерация ключей в nacl.js

Identity Key и PreKey создаются через:

const ik = nacl.sign.keyPair();     // Ed25519
const spk = nacl.box.keyPair();     // Curve25519

В реальных реализациях identity key часто используется в двух пространствах: для подписи и для Diffie-Hellman (через преобразование Ed25519 → X25519).


Подготовка инициатора

Alice формирует собственную пару ключей:

const aliceIK = nacl.box.keyPair();
const aliceEK = nacl.box.keyPair(); // ephemeral key

Эфемерный ключ (EK) критически важен: он обеспечивает forward secrecy, поскольку используется только в рамках одной сессии.


Обмен параметрами

Alice получает от сервера:

  • IK_B (public identity key Bob)
  • SPK_B (signed prekey)
  • OPK_B (one-time prekey, если доступен)

Также Alice проверяет подпись SPK_B:

const valid = nacl.sign.detached.verify(
  spkPublicKey,
  spkSignature,
  bobIdentityPublicKey
);

Если проверка не проходит, соединение должно быть отклонено.


Диффи-Хеллман вычисления

X3DH использует несколько DH-операций:

  1. DH1 = DH(IK_A, SPK_B)
  2. DH2 = DH(EK_A, IK_B)
  3. DH3 = DH(EK_A, SPK_B)
  4. DH4 = DH(EK_A, OPK_B) — опционально

В nacl.js это реализуется через:

const dh1 = nacl.scalarMult(aliceIK.secretKey, spkPublicKey);
const dh2 = nacl.scalarMult(aliceEK.secretKey, bobIKPublicKey);
const dh3 = nacl.scalarMult(aliceEK.secretKey, spkPublicKey);

Каждый результат представляет собой 32-байтовый shared secret.


Формирование общего секрета

Все DH-значения конкатенируются:

SK_input = DH1 || DH2 || DH3 || (DH4 если есть)

Далее применяется KDF (обычно HKDF-SHA256, реализуемый отдельно, так как TweetNaCl не включает полноценный HKDF):

const masterSecret = hkdf(
  concat(dh1, dh2, dh3),
  salt,
  info,
  32
);

Результатом является корневой ключ сессии.


Роль хэширования и KDF

TweetNaCl.js не предоставляет HKDF напрямую, поэтому используется комбинация:

  • nacl.hash() как базовый строительный блок
  • PBKDF2 или внешняя реализация HKDF

Ключевой момент: raw output DH нельзя использовать напрямую как ключ шифрования.


Аутентификация через X3DH

Аутентификация достигается за счёт:

  • проверки подписи Signed PreKey
  • использования Identity Key Bob в DH1 и DH2
  • связывания ephemeral key Alice с постоянной идентичностью Bob

Таким образом, даже если атакующий подменит SPK_B, он не сможет корректно подписать его без приватного IK_B.


Связь с Double Ratchet

X3DH используется только для начального установления секретов. После этого включается Double Ratchet Algorithm:

  • симметрическое обновление ключей
  • forward secrecy на каждом сообщении
  • пост-обфускация ключей

Начальный masterSecret становится root key для ratchet:

rootKey = KDF(masterSecret, "root");

Практическая структура сессии

После выполнения X3DH формируются:

  • Root Key
  • Chain Key (sending)
  • Chain Key (receiving)

Инициализация:

session = {
  rootKey: masterSecret,
  sendChain: null,
  recvChain: null,
  dhPair: aliceEK
};

Особенности реализации в JavaScript

Работа с TweetNaCl.js накладывает ограничения:

  • отсутствие высокоуровневых протоколов
  • необходимость ручного управления сериализацией ключей
  • Uint8Array как единственный формат данных

Типичная ошибка — смешивание строк и бинарных данных:

// неправильно
const bad = "key" + dh1;

// правильно
const good = concatUint8Arrays(dh1, dh2);

Управление One-Time PreKeys

One-Time PreKeys используются только один раз. После использования сервер обязан их удалить.

Если OPK отсутствует, протокол продолжает работу, но снижается уровень forward secrecy.


Безопасность эфемерных ключей

Эфемерный ключ Alice (EK_A) должен:

  • генерироваться заново для каждой сессии
  • не храниться после использования
  • не повторно использоваться между соединениями

Любая повторная генерация EK_A с тем же значением нарушает модель безопасности X3DH.


Ошибки интеграции с nacl.js

Типовые проблемы при реализации:

  • использование Ed25519 ключей напрямую в scalarMult без конвертации
  • отсутствие проверки подписи SPK_B
  • неправильное объединение DH-секретов
  • использование небезопасного RNG вместо nacl.randomBytes

Корректная генерация случайных данных:

const random = nacl.randomBytes(32);

Конвертация Ed25519 ↔︎ Curve25519

TweetNaCl.js разделяет:

  • Ed25519 (подпись)
  • Curve25519 (DH)

Identity Key часто требуется в обеих формах. В низкоуровневых реализациях используется преобразование ключей через алгоритм Montgomery ladder representation.


Структура сообщений после установления ключа

После X3DH дальнейшая коммуникация происходит через:

ciphertext = nacl.secretbox(message, nonce, symmetricKey);

Где symmetricKey выводится из chain key Double Ratchet.

Nonce должен быть уникальным:

nacl.randomBytes(24);

Связь X3DH и модель доверия

X3DH не требует централизованного доверенного канала во время соединения, но предполагает доверие к:

  • серверу публикации prekeys (ограниченное)
  • корректности публичного Identity Key

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


Производительность в JavaScript среде

TweetNaCl.js оптимизирован под WebAssembly-отсутствие и работает в чистом JS:

  • операции scalarMult ~ миллисекунды
  • генерация ключей дешёвая
  • основная нагрузка — KDF и сериализация

Типовая последовательность X3DH

  1. Bob публикует IK_B, SPK_B, OPK_B
  2. Alice получает набор ключей
  3. Alice проверяет подпись SPK_B
  4. Alice генерирует EK_A
  5. Alice вычисляет DH1–DH3
  6. Alice формирует masterSecret через KDF
  7. Alice отправляет initial message с EK_A
  8. Bob выполняет симметричные DH-вычисления
  9. Обе стороны получают одинаковый root key

Интеграционные паттерны в приложениях

В реальных системах поверх nacl.js обычно строится слой:

  • key store (IndexedDB / secure storage)
  • prekey manager
  • session manager
  • message encryption layer

Каждый компонент строго отделяет криптографию от логики передачи данных.


Модель угроз

X3DH защищает от:

  • MITM при отсутствии компрометации ключей
  • replay атак (при использовании ratchet)
  • пассивного наблюдения

Не защищает от:

  • компрометации устройства
  • подмены сервера prekeys без проверки identity fingerprint
  • утечки приватных ключей

Использование в связке с протоколами мессенджеров

X3DH лежит в основе современных end-to-end систем, где TweetNaCl.js часто используется как лёгкая реализация криптографии в браузере или Node.js окружении, заменяя более тяжёлые библиотеки.