本文へスキップ
ecdsa.com

JWS と JWE の比較|JOSE トークンは署名か暗号化か

JWS はペイロードを誰が作成したかを証明しますが、内容は誰でも読めます。JWE はペイロードを受信者以外の誰からも隠しますが、公開鍵アルゴリズムを使う場合、誰が作成したかについては何も示しません。

JWS と JWE の違いは、JOSE トークンが何を保護するかの違いです。JSON Web Signature(RFC 7515)は、読める状態のままのペイロードに完全性と真正性を付与し、JSON Web Encryption(RFC 7516)は機密性を付与します。目にする JWT のほぼすべては JWS であり、クレームは秘密にされているものと思っている人はこれに驚きます。

比較表

JWSJWE
仕様RFC 7515RFC 7516
保護するもの完全性と作成元機密性(と AEAD による完全性)
コンパクト形式header.payload.signature(3 つの部分)header.encrypted_key.iv.ciphertext.tag(5 つの部分)
ヘッダーのアルゴリズムalg(たとえば ES256、RS256、HS256、Ed25519)鍵管理用の alg(ECDH-ES、ECDH-ES+A256KW、RSA-OAEP-256、A256KW、dir)と、コンテンツ用の enc(A256GCM、A128CBC-HS256)
ペイロードが見えるか見える(base64url でエンコードしただけ)見えない
送信者の認証あり(署名または MAC)共通鍵または認証付きの鍵共有を使う場合のみ。ECDH-ES や RSA-OAEP 単独では不可
70 バイトのクレームセットのサイズ(後述の実測値)186 文字(ES256)405 文字(ECDH-ES+A256KW と A256GCM)
主な用途JWT のアクセストークンと ID トークン、DPoP、署名付きリクエスト、SD-JWT暗号化した ID トークン、クライアントサイドに保存するトークン、一部の FAPI と EUDI のレスポンス

それぞれが保護するもの

JWS は base64url(header) + "." + base64url(payload) に署名します。トークンを持っていれば誰でもクレームをデコードして読むことができ、JWT デバッガーが行っているのもまさにそれです。署名は、トークンが鍵の保有者から発行され、改ざんされていないことを証明します。API がクレームを信頼するために必要なのは、この保証です。

JWE は、ランダムなコンテンツ暗号化鍵(CEK)を生成し、enc で指定した AEAD でペイロードを暗号化したうえで、alg で指定した鍵管理アルゴリズムによって CEK を受信者に届けます。その方法は、RSA-OAEP でラップする、ECDH-ES による鍵共有の後に AES 鍵ラップでラップする、ECDH-ES から直接導出する、事前に共有しておく(dir)のいずれかです。保護ヘッダーは関連データとして認証されます。ペイロードを読めるのは受信者だけです。

重要なのは、ここにある非対称性です。公開鍵による鍵管理では、受信者の公開鍵を持っていれば誰でも有効な JWE を作成できます。復号に成功しても、送信者については何も証明されません。そのため、信頼される必要もある機密性の高いトークンはネストします。署名済みの JWT(JWS)を cty: JWT を指定した JWE の中に入れ、復号した後に検証します。

実際のサイズ

次のスクリプトは Node.js 26.10 上の jose 6.2.13 で実行したもので、同じクレームを両方の方法で保護します。

jose:同じクレームを JWS と JWE で保護
import { CompactEncrypt, CompactSign, compactDecrypt, compactVerify, generateKeyPair } from "jose";

const claims = new TextEncoder().encode(JSON.stringify({ sub: "user-42", email: "alice@example.com", plan: "pro" }));

// JWS: integrity and origin; the payload is only base64url-encoded.
const signer = await generateKeyPair("ES256");
const jws = await new CompactSign(claims).setProtectedHeader({ alg: "ES256" }).sign(signer.privateKey);
await compactVerify(jws, signer.publicKey);

// JWE: confidentiality for the holder of the recipient's private key.
const recipient = await generateKeyPair("ECDH-ES+A256KW");
const jwe = await new CompactEncrypt(claims).setProtectedHeader({ alg: "ECDH-ES+A256KW", enc: "A256GCM" }).encrypt(recipient.publicKey);
const { plaintext } = await compactDecrypt(jwe, recipient.privateKey);

console.log("JWS parts:", jws.split(".").length, "length:", jws.length, "payload readable:", Buffer.from(jws.split(".")[1], "base64url").toString().includes("alice"));
console.log("JWE parts:", jwe.split(".").length, "length:", jwe.length, "decrypts:", new TextDecoder().decode(plaintext).includes("alice"));
出力
JWS parts: 3 length: 186 payload readable: true
JWE parts: 5 length: 405 decrypts: true

JWE のオーバーヘッドの大半は、ヘッダー内の一時的な公開鍵(epk)、ラップされた鍵、12 バイトの IV、16 バイトのタグです。署名してから暗号化するネストされたトークンは、JWS 全体が JWE の平文になるため、さらに大きくなります。

落とし穴

  • JWS のクレームを秘密として扱う:署名しただけのトークンに、個人情報、公開したくない内部の識別子、シークレットを入れてはいけません。そうしたトークンは、ログやブラウザーのストレージ、サポートチケットに残ることになります。
  • 署名のない JWE を信頼する:ECDH-ES や RSA-OAEP による復号で証明されるのは、そのトークンが自分宛てに暗号化されたことだけです。
  • 弱いアルゴリズムや旧来の JWE アルゴリズム:RSA1_5 はパディングオラクル攻撃に対して脆弱で、p2c の反復回数が極端に大きい PBES2 はサービス拒否に悪用される可能性があります。また、曲線上にない点を受け入れてしまう ECDH-ES の実装からは、2017年に秘密鍵が漏洩しました(RFC 8725 §2.5)。ECDH-ES+A256KW か RSA-OAEP-256 を A256GCM と組み合わせて使うことを推奨します。PBES2 に対応しなければならない場合は、反復回数に上限を設けてください。
  • トークンによるアルゴリズムの選択:JWS と同様に、受信者は鍵ごとに想定している alg と enc の値だけを受け入れるべきです。
  • 圧縮:JWE の zip: DEF は暗号化の前に圧縮を行うため、長さを通じて平文に関する情報が漏れる可能性があります(RFC 8725 §3.6)。使用は避けてください。

どちらを選ぶべきか

  • 自分の API が検証するアクセストークン:JWS を使います。クレームは最小限にし、読まれてはならない場合は、イントロスペクションと組み合わせた不透明なトークンを使ってください。
  • 個人情報を含み、ブラウザーやモバイルアプリが保存する ID トークン:通常は JWS を使います。脅威モデルにクライアント環境が含まれる場合は、ネストした JWE を使います。
  • 信頼できない仲介者を経由して渡されるトークン(第三者を経由するリダイレクト、URL や QR コードに埋め込まれたトークン):JWS を JWE の中にネストして使います。
  • 相互認証された TLS 上のマシン間メッセージ:多くの場合、どちらも不要です。TLS がすでに通信経路上の機密性と完全性を提供しており、JWS が意味を持つのは、保存や転送の後もメッセージを検証できる状態に保つ必要がある場合だけです。
  • 既知の受信者に宛てたエンドツーエンド暗号化のペイロード:ECDH-ES+A256KW と A256GCM による JWE を使います。HPKE ベースの形式も、JOSE 向けに標準化されしだい選択肢になります。

移行時の注意点

  1. クレームを分類します:既存の JWS トークンについて、保有者に見えてはならないものは、トークンから外すか JWE に移します。
  2. ネストしたトークンを段階的に導入します:復号できるクライアントには cty: JWT の JWE を発行し、内側の JWS は変更せずに残して、検証ロジックを再利用できるようにします。
  3. 暗号化鍵は署名鍵とは別に配布します:JWKS の各エントリには、use: enc または use: sig と、それぞれ異なる kid の値を設定すべきです。
  4. 送信側と受信側の両方でアルゴリズムを固定し、RSA1_5、反復回数に上限のない PBES2、圧縮を拒否します。
  5. ログ出力を見直します:トークンを暗号化したら、トークンそのものは記録されていなかった場所に、復号したクレームが記録されていないことを確認してください。JWE インスペクターは、JWE を復号せずにヘッダーと構造を表示します。

参考資料

すべての比較