Согласование ключей между клиентом и сервером

Модель безопасного обмена ключами

В клиент-серверных системах симметричное шифрование требует предварительного согласования общего секрета, который не должен передаваться по сети в явном виде. В SJCL (Stanford Javascript Crypto Library) для этого используются схемы согласования ключей на основе асимметричной криптографии, чаще всего Elliptic Curve Diffie–Hellman (ECDH).

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

  • приватный ключ остаётся на стороне владельца
  • публичный ключ может свободно передаваться по сети

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


ECDH-реализация в SJCL

В SJCL реализация ECC находится в пространстве sjcl.ecc. Для схемы Diffie–Hellman на эллиптических кривых используется механизм, основанный на умножении точки генератора на случайное число (приватный ключ).

Ключевые компоненты:

  • sjcl.ecc.elGamal.generateKeys() — генерация пары ключей
  • sec.get() — получение приватного ключа
  • pub — публичный ключ
  • multiply() — операция ECDH для получения общего секрета

Пример генерации ключей:

const keys = sjcl.ecc.elGamal.generateKeys();

const privateKey = keys.sec;
const publicKey = keys.pub;

Процесс согласования ключей между клиентом и сервером

Обмен ключами проходит в несколько этапов:

Генерация ключевых пар

Клиент и сервер независимо создают свои ECC ключи:

// Клиент
const clientKeys = sjcl.ecc.elGamal.generateKeys();

// Сервер
const serverKeys = sjcl.ecc.elGamal.generateKeys();

Обмен публичными ключами

Публичные ключи сериализуются и передаются по сети:

const clientPub = clientKeys.pub;
const serverPub = serverKeys.pub;

Вычисление общего секрета

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

// Клиент вычисляет общий секрет
const clientSecret = clientKeys.sec.derive(serverPub);

// Сервер вычисляет общий секрет
const serverSecret = serverKeys.sec.derive(clientPub);

В результате clientSecret и serverSecret совпадают, хотя приватные ключи никогда не покидали устройства.


Преобразование общего секрета в симметричный ключ

Полученный общий секрет нельзя напрямую использовать как AES-ключ. Он представляет собой структуру bitArray, которую необходимо привести к фиксированной длине через KDF (Key Derivation Function).

В SJCL часто используется:

  • sjcl.hash.sha256 — хеширование
  • sjcl.misc.pbkdf2 — если требуется усиление пароля
  • прямое хеширование секрета

Пример преобразования:

const sharedBits = clientKeys.sec.derive(serverPub);

const sessionKey = sjcl.hash.sha256.hash(sharedBits);

Далее sessionKey может использоваться в симметричных алгоритмах:

const aesKey = new sjcl.cipher.aes(sessionKey);

Обеспечение согласованного формата данных

Ключевой проблемой при межсистемной совместимости является согласование:

  • формата публичных ключей
  • сериализации bitArray
  • кривой (curve selection)
  • кодирования (base64, hex)

В SJCL ECC ключи обычно сериализуются в формате JSON:

const pubJSON = sjcl.codec.base64.fromBits(publicKey.get());

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


Использование KDF для формирования сессионных ключей

Общий секрет не должен использоваться напрямую, поскольку:

  • имеет неоднородную структуру
  • может содержать статистические утечки
  • не имеет фиксированной длины

Для приведения к криптографически стойкому виду применяется схема:

  1. получение ECDH секрета
  2. добавление соли (salt)
  3. хеширование SHA-256 или SHA-512
  4. разбиение на ключи (encryption / mac / iv)

Пример:

const shared = clientKeys.sec.derive(serverPub);

const masterKey = sjcl.hash.sha256.hash(shared);

const encKey = masterKey.slice(0, 4);
const macKey = masterKey.slice(4, 8);

Привязка ключа к сессии

Для предотвращения повторного использования ключей в разных сессиях вводится контекст:

  • nonce клиента
  • nonce сервера
  • идентификатор сессии
  • временная метка

Комбинированный вход в KDF:

const context = sjcl.bitArray.concat(
  sharedSecret,
  sjcl.codec.utf8String.toBits(sessionId)
);

const sessionKey = sjcl.hash.sha256.hash(context);

Аутентификация в процессе согласования

ECDH сам по себе не защищает от атак типа “man-in-the-middle”. Для устранения этой проблемы используется дополнительная аутентификация:

  • подпись публичных ключей (ECDSA)
  • проверка сертификатов
  • заранее разделённые ключи (PSK)
  • протоколы SRP или STS

Пример проверки подписи:

const isValid = sjcl.ecc.verify(serverPub, signature, serverCertPub);

Типовые ошибки при реализации

Несогласованная кривая

Использование разных параметров ECC приводит к несовместимости секрета.

Повторное использование ключевых пар

Снижает стойкость и нарушает forward secrecy.

Прямое использование ECDH секрета

Без KDF приводит к предсказуемым AES ключам.

Неправильная сериализация bitArray

Любое изменение порядка байтов приводит к расхождению результата.


Схема полного обмена

  1. Генерация ECC ключей на клиенте и сервере
  2. Обмен публичными ключами
  3. Вычисление ECDH секрета
  4. Прогон через SHA-256 / KDF
  5. Получение симметричных ключей
  6. Использование AES для защиты данных канала
  7. Привязка ключей к сессии и контексту

Использование SJCL для защищённого канала

После согласования ключей формируется симметричный канал:

const aes = new sjcl.cipher.aes(sessionKey);

const ciphertext = sjcl.mode.gcm.encrypt(aes, plaintext, iv);
const plaintext = sjcl.mode.gcm.decrypt(aes, ciphertext, iv);

Особенности работы в клиент-серверной архитектуре

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

Контроль согласованности ключей

Для диагностики используют:

  • хеш общего секрета на обеих сторонах
  • сравнение отпечатков ключей
  • логирование промежуточных bitArray значений
const fingerprint = sjcl.codec.hex.fromBits(
  sjcl.hash.sha256.hash(sharedSecret)
);

Совпадение отпечатков подтверждает корректное согласование ключей между клиентом и сервером.