JWS と JWE の違いは、JOSE トークンが何を保護するかの違いです。JSON Web Signature(RFC 7515)は、読める状態のままのペイロードに完全性と真正性を付与し、JSON Web Encryption(RFC 7516)は機密性を付与します。目にする JWT のほぼすべては JWS であり、クレームは秘密にされているものと思っている人はこれに驚きます。
比較表
| JWS | JWE | |
|---|---|---|
| 仕様 | RFC 7515 | RFC 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 で実行したもので、同じクレームを両方の方法で保護します。
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 向けに標準化されしだい選択肢になります。
移行時の注意点
- クレームを分類します:既存の JWS トークンについて、保有者に見えてはならないものは、トークンから外すか JWE に移します。
- ネストしたトークンを段階的に導入します:復号できるクライアントには
cty: JWTの JWE を発行し、内側の JWS は変更せずに残して、検証ロジックを再利用できるようにします。 - 暗号化鍵は署名鍵とは別に配布します:JWKS の各エントリには、
use: encまたはuse: sigと、それぞれ異なるkidの値を設定すべきです。 - 送信側と受信側の両方でアルゴリズムを固定し、
RSA1_5、反復回数に上限のないPBES2、圧縮を拒否します。 - ログ出力を見直します:トークンを暗号化したら、トークンそのものは記録されていなかった場所に、復号したクレームが記録されていないことを確認してください。JWE インスペクターは、JWE を復号せずにヘッダーと構造を表示します。