本文へスキップ
ecdsa.com

HS256 と RS256 の比較|JWT は共有秘密か公開鍵か

HS256 は HMAC なので、どの検証者もトークンを発行できてしまいます。RS256 は署名なので、検証者が持つのは公開鍵だけです。JWT の設計ミスの多くは、この違いに気づかないことから始まります。
比較対象HS256RS256

HS256 と RS256 のどちらを使うかは、JWT を発行するときに最初に決めることです。2 つの alg の値は似ていますが、HS256 は共有秘密(HMAC-SHA256)でトークンを認証し、RS256 は RSA の秘密鍵(SHA-256 を使う RSASSA-PKCS1-v1_5)でトークンに署名します。この違いによって、誰がトークンを作成できるか、鍵をどう配布するか、鍵が漏洩したときに何が起きるかが変わります。

比較表

HS256RS256
アルゴリズムSHA-256 を使う HMAC(RFC 7518 §3.2)SHA-256 を使う RSASSA-PKCS1-v1_5(§3.3)
鍵1 つの共有秘密、256 ビット以上RSA の鍵ペア、2048 ビット以上
検証できる者シークレットの保有者公開鍵を持っていれば誰でも
トークンを発行できる者シークレットの保有者全員(検証者を含む)秘密鍵の保有者のみ
鍵の配布すべての検証者に秘密裏にコピーを配布公開の JWKS(jwks_uri)
署名部分32 バイト、base64url で 43 文字256 バイト、342 文字(RSA-2048)
トークンの例(後述の実測値)187 文字508 文字
速度(Apple M4 Max の 1 コア)毎秒数百万回の HMAC毎秒約 3,000 回の署名、114,000 回の検証
JWA の要件レベル必須(Required)推奨(Recommended)
主な破綻のしかた弱いシークレットや漏洩したシークレット:誰でもトークンを発行できるチェックの甘い検証者でのアルゴリズム混同、鍵の取り扱い

信頼モデル

HS256 では、検証鍵と署名鍵はまったく同じバイト列です。トークンを検証するサービスはどれも、任意のユーザー、スコープ、オーディエンス向けの有効なトークンを作成でき、そのうちどれか 1 つからシークレットが漏洩すれば(ログに残った環境変数、フロントエンドのバンドル、モバイルアプリ)、誰でもトークンを発行できるようになります。自前のセッショントークンを発行・検証するモノリスならこれで問題ありませんが、1 つのトークン発行者が多数の API やチームにトークンを提供する場合は深刻な弱点になります。

RS256 では、発行者が秘密鍵を(理想的には HSM や KMS に)保管し、メタデータに記載した URL で公開鍵を JWKS として公開します。検証者は鍵セットを取得して kid で鍵を選択します。検証者の追加や削除も、シークレットを共有せずに行えます。乗っ取られた検証者でもトークンを読むことはできますが、偽造はできません。OpenID Connect プロバイダーがデフォルトで RS256 を使って ID トークンに署名するのも、JWT のベストプラクティスを定めた RFC(RFC 8725)が複数の当事者が関わる環境に非対称アルゴリズムを勧めているのも、このためです。

なお OpenID Connect では、コンフィデンシャルクライアント向けに、クライアントシークレットを鍵とする HS256 の ID トークンも認められています。これが成り立つのは、各クライアントが自分宛てに発行されたトークンだけを検証するからです。

シークレットの強度とオフラインでのクラッキング

HS256 のトークンは、オフラインで推測を確かめられる完璧なオラクルです。トークンを 1 つ見た者は誰でも、サーバーと一切通信することなく、自分のハードウェアが HMAC-SHA256 を計算できる速さで候補のシークレットを試せます。短いシークレット、人が考えたシークレット、デフォルトのシークレット(「secret」、プロジェクト名、チュートリアルの例)は、一般的なパスワードクラッキングツールで数秒のうちに破られます。RFC 7518 はハッシュの出力以上の長さ(HS256 では 256 ビット)の鍵を要求しており、RFC 8725 §3.5 は鍵に十分なエントロピーがなければならないとしています。シークレットは CSPRNG で生成し(openssl rand -base64 32)、パスワードから導出してはいけません。

RS256 には秘密鍵に対する同等のオフライン攻撃はありませんが、RFC 7518 は 2048 ビット未満の RSA 鍵を認めておらず、鍵ファイルは他の秘密鍵と同じように保護しなければなりません。

HS256 と RS256 のアルゴリズム混同

JWT で最もよく知られた脆弱性は、この 2 つを取り違えさせるものです。あるサービスが、PEM 形式の RSA 公開鍵を渡してライブラリを呼び出し、RS256 のトークンを検証しているとします。そのライブラリがアルゴリズムをトークンのヘッダーから読み取り、しかも HS256 にも対応している場合、攻撃者は、公開鍵の PEM のバイト列をシークレットとして HMAC を計算した "alg": "HS256" のトークンを送ります。ライブラリも同じバイト列を使うため、偽造したトークンの検証が通ってしまいます。Tim McLean は 2015年に "alg": "none" とあわせてこの問題を報告し、RFC 8725 §2.1 と §3.1 がその対策を明文化しました。つまり、各鍵をどのアルゴリズムで使ってよいかを決めるのは、トークンではなく検証者です。

最近のライブラリでは、この攻撃は成立しにくくなっています。jsonwebtoken は公開鍵を HMAC シークレットとして使うことを拒否し(設定で両者を取り違えると secretOrPublicKey must be an asymmetric key というエラーが出ます)、PyJWT は algorithms 引数を必須とし、jose はアルゴリズムを鍵の種類に紐付けます。それでも、想定するアルゴリズムだけを正確に指定した許可リストを、常に明示的に渡してください。

サイズと速度

以下のスクリプトは Node.js 26.10 で実行したもので、node:crypto だけを使い、同じクレームから HS256 と RS256 のトークンを作成します。

Node.js:同じペイロードに対する HS256 と RS256 のトークン
import { createHmac, generateKeyPairSync, randomBytes, sign } from "node:crypto";

const b64url = (value) => Buffer.from(value).toString("base64url");
const payload = b64url(JSON.stringify({ iss: "https://auth.example.com", sub: "user-42", aud: "api", exp: 1792500000 }));

// HS256: HMAC-SHA256 with a shared 256-bit secret.
const secret = randomBytes(32);
const hsInput = `${b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }))}.${payload}`;
const hs256 = `${hsInput}.${b64url(createHmac("sha256", secret).update(hsInput).digest())}`;

// RS256: RSASSA-PKCS1-v1_5 with SHA-256 and a 2048-bit key.
const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
const rsInput = `${b64url(JSON.stringify({ alg: "RS256", typ: "JWT", kid: "2026-10" }))}.${payload}`;
const rs256 = `${rsInput}.${b64url(sign("sha256", Buffer.from(rsInput), privateKey))}`;

console.log("HS256 token:", hs256.length, "characters, signature part:", hs256.split(".")[2].length);
console.log("RS256 token:", rs256.length, "characters, signature part:", rs256.split(".")[2].length);
出力
HS256 token: 187 characters, signature part: 43
RS256 token: 508 characters, signature part: 342

RS256 のヘッダーには kid も含まれています。検証者が JWKS から鍵を選ぶのに必要だからです。本番環境では、自作のトークンではなく、保守されている JWT ライブラリを使ってください。ここで示したいのは署名部分のサイズです。同じトレードオフを ECDSA で比較した内容は、ES256 と RS256 の比較を参照してください。

それぞれの用途

  • 自前のセッショントークンを発行・検証する単一のアプリケーション:32 バイトのランダムなシークレットで HS256 を使い、短い重複期間を設けてシークレットをローテーションします。
  • 複数の API がトークンを検証する認可サーバー:RS256、PS256、ES256 のいずれかを使い、鍵を JWKS で公開します。
  • 第三者、モバイルアプリ、ブラウザーが検証するトークン:非対称アルゴリズムだけを使います。クライアントに配布したシークレットは、公開したのと同じです。
  • コンフィデンシャルクライアント向けの OpenID Connect の ID トークン:どちらでも使えます。デフォルトは非対称で、運用もより簡単です。
  • 個々の顧客への webhook:顧客ごとに専用のシークレットがあるため、HMAC 方式が一般的です。
  • コンプライアンス要件に基づく鍵の保管(HSM、KMS):非対称鍵を使います。サービスは署名用の鍵素材にアクセスしなくても検証できるからです。

移行時の注意点

一斉に切り替える日(フラグデー)を設けずに HS256 から RS256 または ES256 へ移行するには、次のようにします。

  1. KMS または HSM で鍵ペアを作成し、新しい kid と alg を付けて公開鍵を JWKS で公開します。
  2. 検証者を両方に対応させる:kid で鍵を選択し、各鍵を 1 つのアルゴリズムに紐付けます(共有秘密は HS256、新しい鍵は RS256)。1 回の検証呼び出しで、同じ鍵素材に対して両方のアルゴリズムを受け付けるようにしてはいけません。
  3. 新しい kid で非対称のトークンの発行を開始します。既存の HS256 トークンは、期限切れになるまで引き続き検証を通ります。
  4. HS256 トークンの最長の有効期間が過ぎたら、すべての検証者から共有秘密を削除します。セキュリティが実際に向上するのは、このステップです。
  5. 漏洩した可能性が少しでもあれば、いずれ削除する予定でも古いシークレットをローテーションします。それを持つ攻撃者は、検証者が受け付けなくなるまで HS256 トークンを発行し続けられるからです。

参考資料

すべての比較