【実務・中級編】 JWTの署名アルゴリズムRS256とHS256の使い分け – Web APIアーキテクチャ・データ連携実践ガイド

JWTの署名アルゴリズム「RS256」と「HS256」:現場で迷わないためのセキュリティ選定と実装ガイド

こんにちは。インフラとネットワークの現場を渡り歩き、数々のパケットキャプチャと深夜の障害対応に立ち会ってきたシニアアーキテクトの私だ。

Web APIの設計やマイクロサービスの認証基盤を構築する際、必ずといっていいほど直面するのがJWT(JSON Web Token)の署名アルゴリズム選定だ。特に、多くのエンジニアが頭を悩ませるのが、対称鍵暗号方式を使う HS256(HMAC using SHA-256) と、非対称鍵暗号方式(公開鍵暗号)を使う RS256(RSA Signature with SHA-256) のどちらを採用すべきかという問いである。

「とりあえずチュートリアルに書いてあったからHS256で」「セキュリティが強そうだから全部RS256で」――そんな理由で設計していないだろうか?

プロトコルの背後にあるRFC(今回の場合はRFC 7519およびRFC 7518)の仕様を紐解き、パケットの往来や鍵のライフサイクルを正しく理解していなければ、システムが成長した段階で致命的な脆弱性やスケール限界の壁にぶつかることになる。今回は、この2つのアルゴリズムの本質的な違いと、実務における正しい使い分けについて、現場の知見を交えて徹底的に解説しよう。

—

1. そもそもJWTの署名とは何か?(RFC 7519の基本)

JWTは、Header、Payload、Signature の3つのパートがドット(.)で連結された文字列だ。よく「JWTは暗号化されている」と誤解している人がいるが、それは大きな間違いだ。Payload部分は単なるBase64URLエンコードであり、誰でもデコードして中身(ユーザーIDや権限など)を読むことができる。

では、署名(Signature)は何のためにあるのか? それは 「データの改ざん検知」と「発行元の証明(完全性・信頼性の担保)」 である。

サーバーは、受け取ったJWTの Header と Payload を結合し、あらかじめ共有された秘密鍵または秘密の署名方式を用いてハッシュ値を計算する。それがJWTに含まれる Signature と一致するかを検証することで、「このトークンは途中で書き換えられていない正規のものである」と確信できるのだ。

この署名を生み出すエンジンの部分が、今回取り上げる HS256 と RS256 である。

—

2. HS256 vs RS256:セキュリティ特性と構造の決定的な違い

まずは両者の仕様上の違いを整理しておこう。

| 特性項目 | HS256 (HMAC-SHA256) | RS256 (RSASSA-PKCS1-v1_5 + SHA-256) |
| :— | :— | :— |
| 暗号方式 | 対称鍵暗号(Symmetric) | 非対称鍵暗号(Asymmetric / PKI) |
| 使用する鍵 | 共通の秘密鍵(Secret Key)が1つ | 秘密鍵(Private Key)と公開鍵(Public Key)のペア |
| 署名と検証 | 同じ鍵で署名も検証も行う | 秘密鍵で署名し、公開鍵で検証する |
| 鍵共有のリスク | 検証側にも秘密鍵を渡す必要がある | 検証側には公開鍵だけを配ればよい(安全) |
| パフォーマンス | 高速(CPU負荷が非常に低い) | 比較的低速(RSAの数学的処理のため負荷高) |
| 主な用途 | モノリスなシステム、単一のバックエンド | マイクロサービス、OAuth 2.0 / OIDCの外部検証 |

HS256:シンプルだが「鍵の共有」に爆弾を抱える方式

HS256は、トークンを発行するサーバーも、それを検証するAPIサーバーも、まったく同じ「1つの秘密鍵」を持つ。処理が非常に高速で、実装もシンプルだ。

しかし、ここに大きな罠がある。「検証を行うすべてのサーバーに、秘密鍵を配らなければならない」 という点だ。もしAPIサーバーの1台が不正アクセスを受け、メモリダンプや設定ファイルから秘密鍵が漏洩したらどうなるか? 攻撃者は「自由に偽の管理者権限トークンを発行できるマスターキー」を手に入れたことになり、システム全体が完全に乗っ取られる。

RS256:公開鍵インフラ(PKI)による堅牢な信頼モデル

一方、RS256は非対称鍵暗号を使用する。
1. 認証サーバー(IdP): 秘密鍵(Private Key)を厳重に保持し、これを使ってJWTに署名(発行)する。
2. APIサーバー等(リソースサーバー): 公開鍵(Public Key)のみを保持し、受け取ったJWTの署名を検証する。

ここで重要なのは、APIサーバー側に秘密鍵が存在しないという点だ。仮にAPIサーバーの1台がハッキングされて公開鍵が盗まれても、攻撃者は署名(偽造)を行うことができない。公開鍵は文字通り「公開するもの」だからだ。これが、大規模な分散システムや外部連携においてRS256がデファクトスタンダードである理由である。

—

3. 通信・検証フローの比較(シーケンス)

百聞は一見にしかず。それぞれのアーキテクチャにおけるデータの流れを確認しよう。

HS256の通信フロー(モノリス・閉じた環境)

[クライアント]                   [APIサーバー / 認証局 (同一)]
      |                                      |
      | --- 1. 認証リクエスト (ID/PW) ----> |
      |                                      | (秘密鍵でHS256署名を作成)
      | <--- 2. JWT (HS256署名付き) --------- |
      |                                      |
      | --- 3. APIリクエスト + JWT --------> |
                                             | (同じ秘密鍵で署名を検証)

すべてが同一の境界内(あるいは単一のアプリケーション)で完結する場合、HS256はオーバーヘッドが少なく非常に効率的だ。

RS256の通信フロー(マイクロサービス・外部連携)

[クライアント]       [認証サーバー (IdP)]        [APIサーバー A]       [APIサーバー B]
      |                     |                         |                     |
      | --- 1. 認証 -------> |                         |                     |
      |                     | (秘密鍵でRS256署名)     |                     |
      | <--- 2. JWT ------- |                         |                     |
      |                     |                         |                     |
      | --- 3. リクエスト + JWT -------------------> |                     |
      |                     |                         | (JWKSエンドポイント等から公開鍵を取得)
      |                     | <--- 4. 公開鍵取得 ----- |                     |
      |                     |                         | (公開鍵で署名検証)  |
      |                     |                                               |
      | --- 5. リクエスト + JWT -----------------------------------------> |
      |                                             (キャッシュした公開鍵で検証)

RS256を採用した場合、APIサーバー群は認証サーバーの「公開鍵」さえ持っていればよいため、疎結合なマイクロサービスアーキテクチャに完璧にフィットする。現代のOAuth 2.0 / OIDC(OpenID Connect)環境では、認証サーバーが公開鍵をJSON形式で配信する /.well-known/jwks.json(JWKS)という標準エンドポイントを用意するのが定石だ。

—

4. 実務で役立つ実装とデバッグの作法

ここからは、実際にコードや設定を通じて、両者の違いとトラブルシューティングの勘所を見ていこう。

Python(PyJWT)を用いた検証の実装例

現場のインフラやバックエンドでよく使われるPythonの PyJWT ライブラリを例に取る。

HS256のコード例

import jwt

# 共通の秘密鍵(絶対にソースコードにハードコードせず、環境変数から読み込むこと!)
SECRET_KEY = "super-secret-key-that-must-be-kept-safe"

# トークンの生成(発行側)
payload = {"sub": "1234567890", "name": "Taro Network", "iat": 1516239022}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
print(f"Generated HS256 Token: {token}")

# トークンの検証(検証側:同じ鍵を使用)
try:
    decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    print("HS256 Verification Succeeded:", decoded)
except jwt.PyJWTError as e:
    print("HS256 Verification Failed:", e)

RS256のコード例

次に非対称鍵を用いた実装だ。事前にOpenSSL等で秘密鍵(private_key.pem)と公開鍵(public_key.pem)を生成しておく必要がある。

import jwt

# 秘密鍵の読み込み(認証サーバー側が保持)
with open("private_key.pem", "r") as f:
    private_key = f.read()

# 公開鍵の読み込み(APIサーバー側が保持)
with open("public_key.pem", "r") as f:
    public_key = f.read()

payload = {"sub": "1234567890", "name": "Taro Network", "iat": 1516239022}

# 秘密鍵で署名(発行)
token = jwt.encode(payload, private_key, algorithm="RS256")
print(f"Generated RS256 Token: {token}")

# 公開鍵で検証(検証側)
try:
    # 注意: 検証側には公開鍵のみを渡す
    decoded = jwt.decode(token, public_key, algorithms=["RS256"])
    print("RS256 Verification Succeeded:", decoded)
except jwt.PyJWTError as e:
    print("RS256 Verification Failed:", e)

—

5. 現場で遭遇しがちなしびれるトラブルとTips

最後に、私がこれまで現場の現場で踏んできた地雷と、その回避策をシェアしておこう。

トラブル1:「HS256とRS256の混同」による脆弱性(CVE-2015-9235など)

過去に多くのJWTライブラリで問題になったのが、「公開鍵を秘密鍵(HS256のシークレット)として扱って検証してしまう」 という恐ろしい脆弱性だ。
攻撃者が、RS256で署名されたトークンのヘッダーを {"alg": "HS256"} に書き換え、APIサーバーが公開している「公開鍵の文字列そのもの」をHS256の秘密鍵として署名を作り直して送りつけると、古いライブラリでは「検証成功」と判定されてしまっていた。

【対策】
jwt.decode() を呼び出す際は、必ず algorithms=["RS256"] のように、受け入れるアルゴリズムを明示的にホワイトリスト形式で指定すること。アルゴリズムを動的にトークンヘッダーから自動選択させるような実装は絶対に避けるべきだ。

トラブル2:公開鍵のローテーションとキャッシュ戦略

RS256を採用した場合、認証サーバー側でセキュリティ上の理由から定期的に秘密鍵・公開鍵のペアを更新(ローテーション)する必要がある。
このとき、APIサーバー側が毎回認証サーバーへ公開鍵を問い合わせていると、認証サーバーに負荷が集中し、レイテンシの悪化を招く。

【現場のベストプラクティス】

  • APIサーバーは、JWKSエンドポイントから取得した公開鍵をメモリ上(あるいはRedisなどのローカルキャッシュ)に一定時間(例: 1時間〜24時間)キャッシュする。
  • JWTのヘッダーに含まれる kid(Key ID)クレームを確認し、キャッシュ内に該当するキーが存在しない場合のみ、例外的にJWKSエンドポイントへ再取得(フェッチ)に行く設計にする。これにより、通常リクエスト時のネットワークラウンドトリップを完全に排除できる。

—

まとめ:どう使い分けるべきか?

ここまで解説した特性を踏まえ、実務での選択基準をまとめよう。

1. HS256を選ぶべきケース

  • システムがモノリスであり、トークンの発行と検証を同一のアプリケーション・閉じた信頼境界内で行う場合。
  • マイクロサービスであっても、内部通信専用のセキュアなVPNやサービスメッシュ内で、鍵の管理が厳格に一元化できる場合。

2. RS256を選ぶべきケース

  • トークンを発行する認証基盤(IdP)と、それを検証するAPIサーバー群が完全に分離・分散している場合(OAuth 2.0 / OIDCを採用するほぼすべてのモダンなシステム)。
  • サードパーティのクライアントや外部サービスに対してAPIを公開し、検証用の公開鍵のみを安全に配りたい場合。

プロトコルの仕様を深く理解し、システムのトポロジーに合わせた正しいアルゴリズムを選択することが、強靭で美しいアーキテクチャへの第一歩だ。あなたの次の設計の参考になれば幸いである。

コメント

タイトルとURLをコピーしました