Перейти к содержимому
ecdsa.com
Node.js · WebCrypto

jose (panva)

jose Филипа Скокана — реализация JOSE без зависимостей (JWS, JWE, JWK, JWKS, JWT) поверх WebCrypto, которая без изменений работает в Node.js, браузерах, Deno, Bun и edge-средах выполнения, с поддержкой EdDSA и ML-DSA, но без secp256k1.
Лицензия
MIT
Проверенная версия
6.2.13 ·
Исходный код
github.com/panva/jose
Документация
github.com/panva/jose/tree/main/docs

Совместимость вкратце

JOSE и JWT

Алгоритмы JWS
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSAEd25519ML-DSA-44ML-DSA-65ML-DSA-87

jose охватывает всё семейство JOSE — подписанные и зашифрованные токены, импорт и экспорт JWK, локальные и удалённые наборы JWK — с одним API в любой среде выполнения JavaScript. Начиная с версии 6 вся криптография выполняется через WebCrypto, поэтому ключи — это объекты CryptoKey (экземпляры KeyObject из Node.js по-прежнему принимаются), а библиотека наследует список кривых WebCrypto: ES256, ES384 и ES512, EdDSA и Ed25519, а в версии 6.2.13 — алгоритмы ML-DSA там, где их предоставляет среда выполнения. ES256K и Ed448 отклоняются с ошибкой ERR_JOSE_NOT_SUPPORTED.

Там, где это важно, настройки по умолчанию строгие. jwtVerify() никогда не принимает alg: none (неподписанные токены можно прочитать только через отдельный класс UnsecuredJWT), а ключ привязан к одному семейству алгоритмов, поэтому токен HS256, предъявленный с открытым ключом EC, или токен RS256 с ключом EC не проходит проверку: возникает ошибка TypeError. ES256 также не принимает ключ P-384. И всё же передавайте algorithms: когда в наборе ключей есть ключи нескольких типов, именно этот параметр не даёт издателю незаметно сменить алгоритм.

Две настройки по умолчанию противоположны по строгости. clockTolerance равен нулю, и это строго, а exp необязателен, и это нестрого: токен без срока действия проходит проверку, если не указать это утверждение в requiredClaims, как это делает пример. JWS, подпись ECDSA в котором пришла в DER (типичная ситуация, когда токен собирают вручную с помощью облачного KMS или openssl dgst), не проходит проверку с общей ошибкой signature verification failed, и ничто не указывает на кодировку; отладчик JWT сразу показывает длину подписи, а конвертер DER ⇄ raw исправляет кодировку.

Импорт ключей явный: importSPKI и importPKCS8 принимают строки PEM (байты DER отклоняются с ошибкой "spki" must be SPKI formatted string), importX509 — сертификат PEM, а importJWK — JWK. importPKCS8 не принимает файлы SEC1 EC PRIVATE KEY, которые OpenSSL записывает по умолчанию; сначала преобразуйте их в PKCS#8, например с помощью конвертера JWK.

Для проверки токенов от поставщика удостоверений используйте createRemoteJWKSet с jwks_uri поставщика: эта функция кеширует ключи, загружает набор заново при неизвестном kid и применяет к повторным загрузкам ограничение частоты запросов. Что на самом деле публикует поставщик, покажет проверка JWKS.

Факты и проверки, на которых они основаны

Ничего из приведённого ниже не скопировано из документации. 20 октября 2026 г. проверка content/libraries/verify/jose/run.sh запустила jose (panva) 6.2.13 в среде Node.js 24.22.0 в образе контейнера node:24.22.0-bookworm-slim; содержимое страницы должно совпадать с выводом проверки, а у каждого факта приведены соответствующие строки вывода.

  • Алгоритмы JWS

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSAEd25519ML-DSA-44ML-DSA-65ML-DSA-87

    HS, RS, PS и ES на 256/384/512 бит, EdDSA и Ed25519, а также ML-DSA-44/65/87 (в Node.js 24.22): в ходе проверки подпись и проверка подписи прошли успешно; ES256K и Ed448 вызвали ошибку ERR_JOSE_NOT_SUPPORTED.

    Что записала проверка
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA, Ed25519, ML-DSA-44, ML-DSA-65, ML-DSA-87
    evidence jws-algorithms: ES256K: ERR_JOSE_NOT_SUPPORTED: Invalid or unsupported "alg" (Algorithm) value
    evidence jws-algorithms: Ed448: ERR_JOSE_NOT_SUPPORTED: Invalid or unsupported "alg" (Algorithm) value
  • Кодировка подписи ES256

    raw

    Подписи ES256 в компактных токенах — 64 байта в формате raw r‖s, которого требует RFC 7518.

    Что записала проверка
    fact jws-ecdsa-encoding = raw
  • Подпись в DER внутри JWS

    rejected

    Токен, подпись ES256 которого перекодировали в DER, не проходит проверку с ошибкой ERR_JWS_SIGNATURE_VERIFICATION_FAILED — той же, что и при поддельной подписи.

    Что записала проверка
    fact jws-der-signature = rejected
    evidence jws-der-signature: ERR_JWS_SIGNATURE_VERIFICATION_FAILED: signature verification failed
  • Список разрешённых алгоритмов

    from-key

    Без algorithms разрешён любой алгоритм, который может выполнить ключ; проверка токена RS256 ключом EC завершилась ошибкой CryptoKey does not support this operation, its algorithm.name must be RSASSA-PKCS1-v1_5.

    Что записала проверка
    fact jwt-algorithm-allowlist = from-key
    evidence jwt-algorithm-allowlist: RS256 token against an EC key: TypeError: CryptoKey does not support this operation, its algorithm.name must be RSASSA-PKCS1-v1_5
  • Неподписанные токены (alg none)

    rejected

    jwtVerify() отклоняет alg: none, даже если none указан в algorithms; незащищённые токены можно декодировать только через UnsecuredJWT.decode().

    Что записала проверка
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: jwtVerify: ERR_JOSE_NOT_SUPPORTED: alg none is not supported either by JOSE or your javascript runtime
    evidence jwt-alg-none: jwtVerify with algorithms ["none"]: ERR_JOSE_NOT_SUPPORTED: alg none is not supported either by JOSE or your javascript runtime
    evidence jwt-alg-none: UnsecuredJWT.decode: returned sub fact-check
  • Открытый ключ в роли секрета HMAC

    rejected

    Токен HS256, ключом HMAC для которого служит текст PEM открытого ключа, не проходит проверку этим открытым ключом с ошибкой CryptoKey instances must be of type "secret" for the HS256 algorithm — как со списком алгоритмов, так и без него.

    Что записала проверка
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: no list: TypeError: CryptoKey instances must be of type "secret" for the HS256 algorithm
    evidence jwt-hmac-public-key: algorithms ["HS256"]: TypeError: CryptoKey instances must be of type "secret" for the HS256 algorithm
  • Сверка кривой с alg

    enforced

    При подписи ES256 ключом P-384 возникает ошибка its algorithm.namedCurve must be P-256.

    Что записала проверка
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: TypeError: CryptoKey does not support this operation, its algorithm.namedCurve must be P-256
  • Допустимое расхождение часов по умолчанию

    0s

    По умолчанию расхождение часов не допускается: токен, срок действия которого истёк 3 секунды назад, отклонён; чтобы учесть расхождение, задайте clockTolerance.

    Что записала проверка
    fact jwt-clock-skew = 0s
  • Обязательное утверждение о сроке действия

    no

    Токен без exp прошёл проверку; чтобы сделать это утверждение обязательным, добавьте requiredClaims: ["exp"].

    Что записала проверка
    fact jwt-exp-required = no
  • Форматы открытых ключей

    spki-pemx509jwk

    importSPKI (строка PEM), importX509 (сертификат PEM) и importJWK сработали; байты DER, переданные в importSPKI, были отклонены. Экземпляры KeyObject из Node.js тоже принимались напрямую.

    Что записала проверка
    fact public-key-formats = spki-pem, x509, jwk
    evidence public-key-formats: spki-der: TypeError: "spki" must be SPKI formatted string
    evidence public-key-formats: KeyObject from node:crypto accepted as a key: true
  • Форматы закрытых ключей

    pkcs8-pemjwk

    importPKCS8 принимает строку PEM в формате PKCS#8, а importJWK — JWK с d; ключ SEC1 EC PRIVATE KEY в PEM отклонён с ошибкой "pkcs8" must be PKCS#8 formatted string.

    Что записала проверка
    fact private-key-formats = pkcs8-pem, jwk
    evidence private-key-formats: sec1-pem: TypeError: "pkcs8" must be PKCS#8 formatted string

Использование

Выпустите токен ES256 с идентификатором ключа, а затем проверьте его через набор JWK, обязательно сверяя алгоритм, издателя, аудиторию и срок действия.
import * as jose from "jose";

const { privateKey, publicKey } = await jose.generateKeyPair("ES256");
const jwk = { ...(await jose.exportJWK(publicKey)), kid: "2026-10", alg: "ES256" };

const token = await new jose.SignJWT({ sub: "user-42" })
  .setProtectedHeader({ alg: "ES256", kid: jwk.kid })
  .setIssuer("https://auth.example.com")
  .setAudience("api")
  .setExpirationTime("10m")
  .sign(privateKey);

// The key set picks the key by kid; createRemoteJWKSet(new URL(...)) does the same over HTTPS.
const keys = jose.createLocalJWKSet({ keys: [jwk] });
const { payload } = await jose.jwtVerify(token, keys, {
  algorithms: ["ES256"],
  issuer: "https://auth.example.com",
  audience: "api",
  requiredClaims: ["exp"],
});

console.log("sub:", payload.sub);

Тестовый стенд для фрагментов кода запускает каждый пример без изменений в образе контейнера node:24.22.0-bookworm-slim, и запуск завершается ошибкой, если вывод не совпадает.

Подготовка
npm init -y >/dev/null && npm install --no-audit --no-fund jose@6.2.13
Команда
node usage.mjs
Вывод должен совпадать (регулярное выражение)
sub: user-42

Библиотеки