Аппаратные ключи и HSM: интеграция через WebCrypto

Jose строится вокруг стандартных Web Crypto API и принципа строгого разделения криптографических операций и представления ключей. Основная идея заключается в том, что библиотека не реализует собственные примитивы шифрования, а использует нативные возможности окружения: crypto.subtle в браузере или crypto.webcrypto в Node.js. Это делает возможной работу с аппаратными ключами и HSM без изменения уровня прикладного кода.

Ключевая абстракция — CryptoKey. В отличие от «сырого» представления ключа (PEM, JWK, DER), объект CryptoKey может быть:

  • неэкспортируемым (extractable: false)
  • привязанным к аппаратному модулю (TPM, Secure Enclave, HSM)
  • ограниченным по использованию (keyUsages: sign, verify, encrypt, decrypt)

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

Пример создания ключа через WebCrypto:

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

Здесь false означает, что приватный ключ не может быть экспортирован, что критично для аппаратных сценариев.


Подпись JWT с использованием аппаратного ключа

Jose предоставляет высокоуровневый API для JWS (JSON Web Signature), скрывая детали криптографии.

import { SignJWT } from 'jose';

const jwt = await new SignJWT({ role: "admin" })
  .setProtectedHeader({ alg: "ES256" })
  .setIssuedAt()
  .setIssuer("auth-service")
  .setExpirationTime("2h")
  .sign(privateKey);

privateKey здесь может быть:

  • обычным CryptoKey
  • ключом из HSM
  • ключом из WebAuthn/TPM (в зависимости от окружения)

Jose не делает различий — важен только интерфейс WebCrypto.


Аппаратные ключи и их поведение

Аппаратные ключи отличаются тем, что:

  • приватный материал недоступен приложению
  • операции sign выполняются внутри защищённого модуля
  • экспорт невозможен даже через обходные механизмы JavaScript

Типичный сценарий — CryptoKey с extractable: false:

{
  type: "private",
  extractable: false,
  algorithm: { name: "ECDSA", namedCurve: "P-256" },
  usages: ["sign"]
}

При использовании Jose это означает, что библиотека передаёт данные в crypto.subtle.sign, а реальное вычисление выполняется в аппаратном слое.


WebCrypto как слой абстракции над HSM

WebCrypto не предоставляет прямого доступа к HSM, но служит универсальным интерфейсом. Поддержка аппаратных модулей реализуется через провайдеры:

  • TPM (Trusted Platform Module)
  • Secure Enclave (Apple)
  • Windows CNG / KSP
  • PKCS#11 токены (YubiKey, Nitrokey)
  • облачные HSM (AWS CloudHSM, Azure Key Vault, GCP KMS)

С точки зрения Jose это выглядит одинаково: доступен CryptoKey, а реализация скрыта.


Интеграция через PKCS#11 (Node.js)

В серверной среде аппаратные модули чаще всего подключаются через PKCS#11. Node.js WebCrypto напрямую этого не делает, поэтому используется промежуточный слой.

Пример архитектуры:

Jose → WebCrypto → PKCS#11 provider → HSM

Библиотеки-адаптеры:

  • node-webcrypto-p11
  • pkcs11js
  • интеграция через OpenSSL engine

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

import { WebCrypto } from "node-webcrypto-p11";

const crypto = new WebCrypto({
  library: "/usr/lib/softhsm/libsofthsm2.so",
  slot: 0,
  pin: "1234"
});

После этого Jose может использовать crypto как стандартный WebCrypto провайдер.


Использование HSM для подписи JWS

После подключения HSM ключ выглядит как обычный CryptoKey, но фактически операция выполняется аппаратно.

import { SignJWT } from "jose";

const token = await new SignJWT({ sub: "user-123" })
  .setProtectedHeader({ alg: "RS256" })
  .setIssuedAt()
  .sign(hsmPrivateKey);

Важный момент: даже при высокой нагрузке приватный ключ не покидает HSM, а Jose работает как тонкий слой оркестрации.


Верификация и публичные ключи

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

import { jwtVerify } from "jose";

const { payload } = await jwtVerify(token, publicKey);

Публичный ключ может быть получен из:

  • HSM (экспорт разрешён только public part)
  • JWKS endpoint
  • статической конфигурации

Импорт ключей и работа с JWK

Jose активно использует формат JWK для обмена ключами.

import { importJWK } from "jose";

const key = await importJWK({
  kty: "EC",
  crv: "P-256",
  x: "...",
  y: "..."
}, "ES256");

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


Ограничения WebCrypto при работе с HSM

Несмотря на универсальность, WebCrypto накладывает ряд ограничений:

  • отсутствует контроль над жизненным циклом ключа внутри HSM
  • нет стандартного API для перечисления аппаратных слотов
  • ограниченная поддержка алгоритмов (зависит от провайдера)
  • невозможность тонкой настройки операций подписи

Jose не обходит эти ограничения, а принимает их как часть модели безопасности.


Архитектура безопасного хранения ключей

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

Приложение (Jose)
        ↓
WebCrypto API
        ↓
HSM / TPM / Secure Enclave
        ↓
Аппаратная операция подписи

В такой архитектуре:

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

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

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

  • локальный CryptoKey: микросекунды–миллисекунды
  • HSM: миллисекунды–десятки миллисекунд
  • облачные KMS: десятки–сотни миллисекунд

Jose не кэширует результат подписи, поэтому оптимизация обычно выполняется на уровне архитектуры:

  • батчинг запросов
  • кэширование JWT с коротким TTL
  • выделенные signing service

Частые ошибки при интеграции

Попытка экспортировать приватный ключ

await crypto.subtle.exportKey("pkcs8", privateKey);

Для аппаратных ключей это невозможно и приводит к ошибке InvalidAccessError.

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

HSM может поддерживать только RSA или ECDSA, но не EdDSA.

Смешивание провайдеров WebCrypto

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


Разделение ответственности в Jose

Jose не управляет ключами, не хранит их и не знает о физическом устройстве. Его задача:

  • формирование структуры JWS/JWE
  • передача данных в WebCrypto
  • обработка результата

Это делает библиотеку совместимой с любыми аппаратными реализациями без изменений кода прикладного уровня.


Browser-сценарии с аппаратными ключами

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

  • Secure Enclave через Safari
  • TPM через Windows Hello
  • WebAuthn (FIDO2 устройства)

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

const credential = await navigator.credentials.get({
  publicKey: options
});

// credential.response содержит signature, но ключ остаётся в устройстве

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


WebAuthn и косвенная интеграция

Хотя WebAuthn не является частью WebCrypto, он часто используется вместе с Jose для сценариев:

  • аутентификация пользователей
  • аппаратная подпись challenge-response
  • привязка JWT к устройству

В таких системах подпись не выполняется через Jose напрямую, но результат интегрируется в JWT-потоки.


Криптографическая изоляция в прикладных системах

Использование Jose с HSM позволяет строить модель, где:

  • приложение полностью доверяет WebCrypto
  • ключи изолированы аппаратно
  • компрометация runtime не раскрывает секреты
  • подпись становится сервисной функцией, а не операцией в памяти

Это особенно важно в системах:

  • финансовых транзакций
  • OAuth/OIDC провайдеров
  • распределённых API-шлюзов
  • инфраструктуры с нулевым доверием