Экспорт и импорт ключей ECDH и ECDSA

В Web Crypto API ключи, используемые в алгоритмах ECDH и ECDSA, всегда существуют в строго определённой форме внутри криптографического контекста браузера. Они не могут быть напрямую сериализованы через JSON.stringify или переданы как обычные объекты JavaScript. Для этого используется механизм экспорта и импорта через SubtleCrypto.

Ключевая особенность: любой ключ создаётся как объект CryptoKey, а его переносимость контролируется флагом extractable.

const keyPair = await crypto.subtle.generateKey(
  {
    name: "ECDSA",
    namedCurve: "P-256"
  },
  true,
  ["sign", "verify"]
);

Параметр true означает, что ключи можно экспортировать. Если он установлен в false, любые попытки экспорта завершатся ошибкой.


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

Web Crypto API поддерживает несколько стандартных форматов:

  • JWK (JSON Web Key) — основной формат для JavaScript-экосистемы
  • SPKI (SubjectPublicKeyInfo) — публичные ключи в бинарном DER-кодировании
  • PKCS8 (PrivateKeyInfo) — приватные ключи в DER-кодировании

ECDH и ECDSA используют разные комбинации этих форматов:

Тип ключа Экспорт Импорт
ECDSA public SPKI, JWK SPKI, JWK
ECDSA private PKCS8, JWK PKCS8, JWK
ECDH public SPKI, JWK SPKI, JWK
ECDH private PKCS8, JWK PKCS8, JWK

Экспорт ключей ECDSA

ECDSA применяется для цифровой подписи. Обычно используется пара ключей: приватный для подписи и публичный для проверки.

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

const exportedPublicKey = await crypto.subtle.exportKey(
  "spki",
  keyPair.publicKey
);

console.log(new Uint8Array(exportedPublicKey));

SPKI-формат представляет ключ в бинарном виде. Он часто используется для передачи между системами или сохранения на сервере.

Экспорт в JWK

const jwkPublic = await crypto.subtle.exportKey(
  "jwk",
  keyPair.publicKey
);

console.log(jwkPublic);

JWK удобен тем, что представляет ключ как JSON:

{
  "kty": "EC",
  "crv": "P-256",
  "x": "...",
  "y": "..."
}

Экспорт приватного ключа ECDSA

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

const exportedPrivateKey = await crypto.subtle.exportKey(
  "pkcs8",
  keyPair.privateKey
);

В бинарной форме PKCS8 ключ может быть сохранён, например, в IndexedDB или передан в зашифрованном виде.

JWK-экспорт приватного ключа:

const jwkPrivate = await crypto.subtle.exportKey(
  "jwk",
  keyPair.privateKey
);

Импорт ключей ECDSA

Импорт позволяет восстановить ключ из ранее экспортированного представления.

Импорт публичного ключа (SPKI)

const publicKey = await crypto.subtle.importKey(
  "spki",
  exportedPublicKey,
  {
    name: "ECDSA",
    namedCurve: "P-256"
  },
  true,
  ["verify"]
);

Импорт приватного ключа (PKCS8)

const privateKey = await crypto.subtle.importKey(
  "pkcs8",
  exportedPrivateKey,
  {
    name: "ECDSA",
    namedCurve: "P-256"
  },
  true,
  ["sign"]
);

Импорт через JWK

const key = await crypto.subtle.importKey(
  "jwk",
  jwkPrivate,
  {
    name: "ECDSA",
    namedCurve: "P-256"
  },
  true,
  ["sign"]
);

Особенности ECDH при экспорте и импорте

ECDH используется для установления общего секрета между двумя сторонами. Ключи ECDH почти всегда применяются в сценариях обмена ключами.

Генерация пары:

const ecdhKeys = await crypto.subtle.generateKey(
  {
    name: "ECDH",
    namedCurve: "P-256"
  },
  true,
  ["deriveKey", "deriveBits"]
);

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

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

const exportedECDHPublic = await crypto.subtle.exportKey(
  "spki",
  ecdhKeys.publicKey
);

или в JSON-формате:

const jwkECDHPublic = await crypto.subtle.exportKey(
  "jwk",
  ecdhKeys.publicKey
);

Импорт публичного ключа ECDH

const importedECDHPublic = await crypto.subtle.importKey(
  "spki",
  exportedECDHPublic,
  {
    name: "ECDH",
    namedCurve: "P-256"
  },
  true,
  []
);

Обратите внимание: публичный ключ ECDH обычно не имеет usages, потому что он используется только как вход для deriveKey или deriveBits на стороне другой пары.


Экспорт приватного ключа ECDH

const exportedECDHPrivate = await crypto.subtle.exportKey(
  "pkcs8",
  ecdhKeys.privateKey
);

Импорт приватного ключа ECDH

const importedECDHPrivate = await crypto.subtle.importKey(
  "pkcs8",
  exportedECDHPrivate,
  {
    name: "ECDH",
    namedCurve: "P-256"
  },
  true,
  ["deriveKey", "deriveBits"]
);

Взаимодействие ECDH ключей через deriveBits

После импорта ключей они используются для получения общего секрета:

const sharedSecret = await crypto.subtle.deriveBits(
  {
    name: "ECDH",
    public: importedECDHPublic
  },
  importedECDHPrivate,
  256
);

Результатом является буфер с байтами общего секрета, который далее используется как основа для симметричного шифрования (AES-GCM и др.).


JWK как основной формат обмена

JWK-формат часто оказывается наиболее удобным для веб-приложений, так как позволяет:

  • передавать ключи через JSON API
  • хранить ключи в localStorage / IndexedDB
  • легко логировать и отлаживать структуру ключа

Пример ECDSA JWK:

{
  "kty": "EC",
  "crv": "P-256",
  "x": "base64url...",
  "y": "base64url...",
  "d": "base64url..." 
}

Поле d присутствует только у приватного ключа.


Ограничения и нюансы безопасности

Флаг extractable определяет возможность экспорта:

  • true — ключ можно сериализовать
  • false — ключ остаётся внутри браузера

Практика безопасности:

  • приватные ключи ECDSA чаще делают неэкспортируемыми
  • ECDH ключи иногда экспортируются только для первоначального обмена
  • хранение PKCS8 без шифрования создаёт уязвимость
  • JWK упрощает утечку при компрометации хранилища

Несовместимости форматов между алгоритмами

ECDSA и ECDH используют одну математическую основу (эллиптические кривые), но Web Crypto различает их контекст использования:

  • ключ ECDSA нельзя использовать в ECDH и наоборот
  • импорт требует точного совпадения name и namedCurve
  • SPKI/PKCS8 содержат OID кривой, который проверяется при импорте

Типовые ошибки при импорте

Частые причины ошибок:

  • несоответствие кривой (P-256 vs P-384)
  • неверный формат (JWK вместо PKCS8)
  • попытка импортировать приватный ключ как публичный
  • отсутствие usages при импорте ECDH ключей

Пример ошибки:

DataError: algorithm is not recognized or key format is invalid

Практическая схема обмена ключами ECDH + ECDSA

В реальных протоколах часто комбинируются оба алгоритма:

  • ECDH — для согласования общего секрета
  • ECDSA — для подписи публичных ключей и проверки подлинности

Типичный поток:

  1. генерация ECDH пары
  2. экспорт публичного ключа (SPKI или JWK)
  3. обмен ключами между сторонами
  4. импорт ключей
  5. deriveBits → общий секрет
  6. использование AES-GCM поверх полученного секрета
  7. ECDSA подписывает обмен для защиты от MITM