JWTの署名アルゴリズム、RS256とHS256の境界線。インフラアーキテクトが語る「信頼」の数学的証明
ネットワークの深淵を覗き込むとき、我々は常に「信頼」の担保に頭を悩ませる。REST APIのエンドポイントを設計し、Authorization ヘッダーに忍ばせるJWT(JSON Web Token)は、単なる文字列ではない。それは、クライアントとサーバーの間で交わされる「改ざん不能な証明書」であるべきだ。
今日は、多くのエンジニアが「なんとなく」で選んでいるJWTの署名アルゴリズム、HS256とRS256の深淵に切り込む。なぜインフラアーキテクトは、よりオーバーヘッドの大きいRS256を愛するのか。その裏側にあるプロトコルスタックの挙動と、パフォーマンスチューニングの真髄を解き明かそう。
—
1. 対称鍵(HS256)と非対称鍵(RS256)の「信頼」の数学
HS256(HMAC with SHA-256)は、共有秘密鍵を用いた対称暗号だ。一見すると高速で軽量だが、インフラ的な観点からは「秘密鍵の共有」という致命的な弱点を抱えている。
一方、RS256(RSA Signature with SHA-256)は非対称暗号を用いる。秘密鍵で署名し、公開鍵で検証する。この構造が意味するのは、「検証権限の分離」だ。認証サーバー(AS)だけが署名権を持ち、リソースサーバー(RS)は公開鍵を持つだけで検証が可能になる。
なぜインフラ屋はRS256を推すのか
マイクロサービスアーキテクチャでは、多数のリソースサーバーが独立して動作する。HS256を採用すれば、全てのサービスに同じ共有秘密鍵を配る必要がある。一度でも鍵が漏洩すれば、全システムの認証が崩壊する。RS256であれば、リソースサーバーは公開鍵の公開鍵セット(JWKS)をキャッシュするだけで済み、権限の分離が厳格に守られるのだ。
—
2. パフォーマンスの代償と最適化の哲学
「RS256は計算コストが高い」という言説は、現代のCPU性能において半分は迷信だ。確かにRSAの署名検証はHMACに比べてCPUサイクルを消費するが、ボトルネックは計算そのものよりも、ネットワークのRTT(Round Trip Time)と、TLSハンドシェイクの効率にある。
TLSハンドシェイクとJWTの相互作用
API通信において、JWTを検証するたびに公開鍵を取りに行くのは愚策だ。
1. JWKSキャッシュの最適化: 公開鍵をメモリ内に保持し、Cache-Control ヘッダーを適切に解釈してリフレッシュする。
2. TLSセッション再利用: TLS 1.3の「0-RTT」を有効にし、クライアントとサーバー間のハンドシェイクを極限まで削減する。
# NginxでTLS 1.3とセッション再利用を有効にする設定
ssl_protocols TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 0-RTTを許可(リプレイ攻撃対策を別途実装すること)
ssl_early_data on;
—
3. パケットレベルの最適化:ヘッダー圧縮とMTUの考慮
JWTはトークン長が長くなりがちだ。特にクレーム(claims)を詰め込むと、HTTPリクエストヘッダーが肥大化し、TCPの初期輻輳ウィンドウ(initcwnd)を超える可能性がある。
TCPバッファチューニングの指針
initcwndをデフォルトの10から増やすことで、最初のACKが返る前に、より多くのパケットを流し込める。
# Linuxカーネルでの初期輻輳ウィンドウの調整
ip route change default via 192.168.1.1 dev eth0 initcwnd 20
また、HTTP/2やHTTP/3(QUIC)を採用することで、HPACKやQPACKといったヘッダー圧縮アルゴリズムを活用せよ。これにより、JWTの冗長な文字列は動的テーブルによって効率的に圧縮される。
—
4. 実装におけるセキュリティの罠:アルゴリズム「None」攻撃
最後に、最も恐ろしい脆弱性に触れておく。JWTのライブラリの中には、ヘッダーの alg を none に書き換えるだけで署名検証をスキップしてしまう実装が存在した。
# 脆弱な実装例(絶対に行わないこと)
# ライブラリの検証を明示せず、JWTのヘッダーを盲信している
import jwt
token = request.headers.get("Authorization")
# algをチェックせずにデコードしてはいけない
payload = jwt.decode(token, options={"verify_signature": False})
必ず、使用するアルゴリズムを厳格に指定すること。
# 堅牢な実装例
# 期待するアルゴリズムをRS256に固定し、鍵は公開鍵のみを使用する
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"] # ここでアルゴリズムをホワイトリスト化する
)
—
結論:アーキテクトの視点
ネットワークプロトコルにおいて「完全無欠」など存在しない。あるのは「トレードオフの最適解」だけだ。
RS256を選択することは、単なるセキュリティの向上ではない。それは、システム全体を疎結合に保ち、認証基盤を堅牢なものへ昇華させるためのインフラ的な投資である。パケットの深淵に触れるエンジニア諸君には、ぜひJWTを単なる文字列としてではなく、暗号学的信頼の結晶として扱ってほしい。
JWTの検証を CPU の負荷として捉えるか、信頼のコスト として捉えるか。その視点の違いが、あなたの設計するAPIを一段上のレベルへ引き上げるはずだ。
コメント