本文へスキップ
ecdsa.com
Node.js · WebCrypto

jose (panva)

Filip Skokan による jose は、WebCrypto の上に構築された、依存関係のない JOSE 実装(JWS、JWE、JWK、JWKS、JWT)です。Node.js、ブラウザー、Deno、Bun、エッジランタイムでそのまま動作し、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
公開鍵の形式
spki-pemx509jwk
秘密鍵の形式
pkcs8-pemjwk

jose は、署名付きトークンと暗号化トークン、JWK のインポートとエクスポート、ローカルとリモートの JWK セットといった JOSE ファミリー全体を、あらゆる JavaScript ランタイムで同じ API によって扱えます。バージョン 6 以降はすべての暗号処理を WebCrypto で行うため、鍵は CryptoKey オブジェクトです(Node.js の KeyObject インスタンスも引き続き受け入れられます)。また、ライブラリは WebCrypto の曲線の一覧をそのまま引き継ぎます。ES256、ES384、ES512、EdDSA と Ed25519、そして 6.2.13 ではランタイムが提供していれば ML-DSA のアルゴリズムにも対応します。ES256K と Ed448 は ERR_JOSE_NOT_SUPPORTED で拒否されます。

重要な部分のデフォルトは厳格です。jwtVerify() は alg: none を決して受け入れず(署名のないトークンは別の UnsecuredJWT クラスでしか読めません)、鍵は 1 つのアルゴリズムファミリーに結び付けられます。そのため、EC 公開鍵で検証する HS256 トークンや、EC 鍵で検証する RS256 トークンは、検証を通らずに TypeError で失敗します。ES256 は P-384 の鍵も拒否します。それでも algorithms は指定してください。複数の種類の鍵を含む鍵セットでは、発行者が黙ってアルゴリズムを切り替えても受け入れないようにするのが、この指定の役割です。

2 つのデフォルトは、厳しい方向と緩い方向に分かれています。clockTolerance はゼロで厳格ですが、exp は任意で厳格ではありません。サンプルのように requiredClaims に列挙しない限り、有効期限のないトークンも検証を通ります。ECDSA 署名が DER で届いた JWS(クラウドの KMS や openssl dgst を使ってトークンを手作業で組み立てた場合によくあります)は、汎用的な signature verification failed で失敗し、エンコーディングが原因だと示すものは何もありません。JWT デバッガーなら署名の長さがひと目でわかり、DER ⇄ 生形式の変換ツールで修正できます。

鍵のインポートは明示的です。importSPKI と importPKCS8 は PEM 文字列を受け取り(DER のバイト列は "spki" must be SPKI formatted string で拒否されます)、importX509 は PEM 形式の証明書を、importJWK は JWK を受け取ります。OpenSSL がデフォルトで書き出す SEC1 の EC PRIVATE KEY ファイルは importPKCS8 で拒否されるため、たとえば JWK 変換ツールを使って、先に PKCS#8 に変換してください。

ID プロバイダーが発行したトークンを検証するには、プロバイダーの jwks_uri を指定して createRemoteJWKSet を使ってください。鍵をキャッシュし、未知の kid を受け取ると再取得し、再取得にはレート制限をかけます。プロバイダーが実際に公開している内容は JWKS チェッカーで確認してください。

事実とその根拠となるチェック

以下の内容はドキュメントからの転記ではありません。2026年10月20日に、チェック content/libraries/verify/jose/run.sh がコンテナイメージ node:24.22.0-bookworm-slim 内で jose (panva) 6.2.13 を Node.js 24.22.0 で実行しました。このページの内容はその出力と一致していなければならず、各事実には該当する出力行を示しています。

  • JWS アルゴリズム

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

    チェックでは、256/384/512 ビットの HS、RS、PS、ES、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 署名は、RFC 7518 が求める形式である 64 バイトの生の r‖s です。

    チェックによる記録
    fact jws-ecdsa-encoding = raw
  • JWS 内の DER 署名

    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 を指定しない場合、鍵で実行できるアルゴリズムはすべて許可されます。EC 鍵に対して RS256 トークンを検証すると、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() は、algorithms に none を列挙していても alg: none を拒否します。署名のないトークンは 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

    公開鍵の PEM テキストを鍵とした HS256 トークンは、アルゴリズムの一覧の有無にかかわらず、その公開鍵に対して 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

    P-384 の鍵で ES256 の署名を行うと 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 に渡すと拒否されました。Node.js の KeyObject インスタンスもそのまま受け入れられました。

    チェックによる記録
    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 は PKCS#8 の PEM 文字列を、importJWK は d を含む JWK を受け取ります。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

使い方

鍵 ID を付けて 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

ライブラリ