Практика: ECDH-рукопожатие

Алгоритм ECDH (Elliptic Curve Diffie–Hellman) реализует механизм согласования общего секрета между двумя сторонами через небезопасный канал. Основан на свойствах эллиптических кривых, где вычисление скалярного произведения точки считается простой операцией, а обратная задача (нахождение множителя) — вычислительно сложной.

Каждая сторона:

  • генерирует приватный ключ (случайное число),
  • вычисляет публичный ключ (точка на кривой),
  • обменивается публичными ключами,
  • вычисляет общий секрет, используя свой приватный ключ и чужой публичный.

Ключевое свойство:

PrivA * PubB = PrivB * PubA = SharedSecret

Поддержка ECDH в jsrsasign

Библиотека jsrsasign предоставляет низкоуровневые и высокоуровневые API для работы с эллиптическими кривыми:

  • генерация ключей: KEYUTIL.generateKeypair
  • работа с кривыми: KJUR.crypto.ECDSA
  • вычисление общего секрета: ECDH через методы работы с ключами

Поддерживаются популярные кривые:

  • secp256r1 (P-256)
  • secp384r1 (P-384)
  • secp521r1 (P-521)

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

Для каждой стороны создаётся независимая пара ключей.

const kpA = KEYUTIL.generateKeypair("EC", "secp256r1");
const kpB = KEYUTIL.generateKeypair("EC", "secp256r1");

Результат:

  • kpA.prvKeyObj — приватный ключ A
  • kpA.pubKeyObj — публичный ключ A
  • аналогично для B

Извлечение координат публичного ключа

Публичный ключ на эллиптической кривой представлен точкой (x, y):

const pubA = kpA.pubKeyObj.pubKeyHex;
const pubB = kpB.pubKeyObj.pubKeyHex;

Этот формат используется для передачи между сторонами.

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

jsrsasign не имеет отдельного высокоуровневого метода computeSecret, но позволяет выполнить вычисление через внутренние структуры.

Пример вычисления секрета стороной A:

const ecA = new KJUR.crypto.ECDSA({curve: "secp256r1"});
ecA.setPrivateKeyHex(kpA.prvKeyObj.prvKeyHex);

const sharedA = ecA.ecdh(kpB.pubKeyObj.pubKeyHex);

Аналогично для стороны B:

const ecB = new KJUR.crypto.ECDSA({curve: "secp256r1"});
ecB.setPrivateKeyHex(kpB.prvKeyObj.prvKeyHex);

const sharedB = ecB.ecdh(kpA.pubKeyObj.pubKeyHex);

Проверка совпадения секрета

console.log(sharedA === sharedB); // true

Совпадение подтверждает корректность рукопожатия.

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

Полученный общий секрет — это сырые данные. Для практического применения требуется преобразование через KDF (Key Derivation Function).

Пример с SHA-256:

const hashA = KJUR.crypto.Util.hashHex(sharedA, "sha256");

Результат:

  • фиксированная длина (256 бит)
  • пригоден для использования в AES

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

После получения ключа можно применить симметричное шифрование:

const key = hashA.substring(0, 32); // 128 бит для AES

const encrypted = KJUR.crypto.Cipher.encrypt(
  "Secret message",
  key,
  "aes128-CBC"
);

Расшифрование:

const decrypted = KJUR.crypto.Cipher.decrypt(
  encrypted,
  key,
  "aes128-CBC"
);

Полный сценарий ECDH-рукопожатия

Шаг 1: генерация ключей

  • сторона A создаёт (PrivA, PubA)
  • сторона B создаёт (PrivB, PubB)

Шаг 2: обмен публичными ключами

  • A отправляет PubA
  • B отправляет PubB

Шаг 3: вычисление секрета

  • A: Shared = PrivA * PubB
  • B: Shared = PrivB * PubA

Шаг 4: derivation

  • общий секрет пропускается через KDF

Шаг 5: использование

  • симметричное шифрование
  • HMAC
  • генерация ключей сессии

Форматы ключей

jsrsasign поддерживает несколько форматов:

  • HEX (внутренний)
  • PEM
  • DER

Экспорт публичного ключа:

const pemPub = KEYUTIL.getPEM(kpA.pubKeyObj);

Импорт:

const pubKeyObj = KEYUTIL.getKey(pemPub);

Выбор кривой

Кривая влияет на:

  • уровень безопасности
  • производительность

Рекомендации:

  • secp256r1 — баланс
  • secp384r1 — повышенная безопасность
  • secp521r1 — максимальная защита

Безопасность реализации

Генерация случайных чисел

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

Проверка публичных ключей

Перед использованием необходимо убедиться:

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

Аутентификация

ECDH сам по себе не защищает от MITM-атаки. Требуется:

  • подпись ключей (ECDSA)
  • сертификаты (X.509)

Комбинация ECDH + ECDSA

Типичная схема:

  1. ECDH — согласование секрета
  2. ECDSA — подтверждение подлинности

Пример:

const sig = new KJUR.crypto.Signature({alg: "SHA256withECDSA"});
sig.init(kpA.prvKeyObj);
sig.updateString(pubA);
const signature = sig.sign();

Проверка:

const verifier = new KJUR.crypto.Signature({alg: "SHA256withECDSA"});
verifier.init(kpA.pubKeyObj);
verifier.updateString(pubA);
verifier.verify(signature);

Практическое применение

ECDH используется в:

  • TLS (HTTPS)
  • WebRTC
  • защищённых мессенджерах
  • VPN-протоколах

Ограничения jsrsasign

  • отсутствие высокоуровневого API для ECDH
  • необходимость ручной работы с ключами
  • отсутствие встроенного HKDF

Оптимизация

  • повторное использование объектов ECDSA снижает накладные расходы
  • минимизация конверсий HEX ↔︎ PEM
  • выбор подходящей длины ключа

Ошибки и отладка

Типичные проблемы:

Несовпадение секрета

  • разные кривые
  • неправильный формат ключа

Ошибка при вычислении

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

Пустой результат

  • ошибка инициализации приватного ключа

Расширение: HKDF

Для более корректного derivation:

function hkdf(hash, salt, ikm, info, length) {
  // реализация HKDF поверх HMAC
}

Использование HKDF позволяет:

  • разделять ключи (encryption / authentication)
  • улучшить криптографическую стойкость

Сравнение с классическим DH

Параметр DH ECDH
Размер ключа большой компактный
Производительность ниже выше
Безопасность высокая выше при равных размерах

Роль в современных протоколах

ECDH стал стандартом благодаря:

  • компактности
  • высокой скорости
  • устойчивости к современным атакам

Особенно широко используется в варианте ECDHE (ephemeral), где ключи генерируются на каждую сессию, обеспечивая Perfect Forward Secrecy.

Итоговая схема реализации в jsrsasign

  1. generateKeypair
  2. обмен pubKeyHex
  3. ecdh()
  4. hashHex()
  5. использование результата

Такая последовательность формирует полноценное криптографическое рукопожатие на основе эллиптических кривых.