今さら人に聞けないエンコードと暗号化とJWTの話
一言まとめ: JWTの中から欲しい文字列を取り出すことは簡単だ!
JWTを使ったクレームベースアクセスコントロールの実装の話をしていてなんか話が伝わりにくいなーと思うことが時々あり、最近になって「あれは相手の頭の中でエンコードと暗号化がごっちゃになってたのか!」と気がつきました。
実装方法だけわかっていれば使えるんですが、どう成り立っているかよくわからないものを使いたくない気持ちはよく分かるので解説してみます。
エンコードと暗号化
正式な定義は別の信頼できるリソースにお任せし、ここでは差をわかりやすく解説するとしましょう。
文字列を一定のルールに従って置き換えるという点においてエンコードと暗号化は同じようなものです。
ただし目的と、文字列を元に戻して人の目に読みやすい状態に戻す時の難易度が大きく違います。
エンコード:
一定の環境下で文字列を扱いやすくするために文字列を「一定のルール」で変換する
「一定のルール」は公開されているため、誰でも元に戻せる (デコード)
暗号化:
本来の文字列の悪用を防止するため、「一定のルール」で変換する
「一定のルール」あるいはルールの一部は秘匿されているため、暗号化をした人にルールを確認しないと元に戻せない (暗号化を解除して元に戻すことを復号と呼ぶ)
エンコード例: URLエンコード
暗号化とは違いの見えやすいエンコードの一つがURLエンコードだと思います。
URLエンコードとはURLとして扱うことができない文字をURLの中で扱わなければいけなくなった時に、間違いなく扱うことができるよう対象文字を置き換えるものです。
例えば扱えない文字とは日本語とか、スペースとか、イコールなどがあります。
具体的に確認してみます。
Googleで検索し、検索結果が表示された時にURLバーでエンコード結果を見てみます。
Googleの検索画面を表示し、検索フィールドに以下のように入力します。
スペースは半角でお願いします。
私のPCでこのようにURLが変化しました。
[q=] 以降が検索文字列部分です。firefoxとchromeの間のスペースが [+] に変換されました。
URLエンコードをデコードできるツールを使って検索文字列部分をデコードしてみると、最初に自分が指定した文字列が出てきます。
例えばデコードはこういうツールで試すことができます。
ではもう一つ、スペースを全角にして同じようにGoogleで検索し、検索文字列部分を確認します。
結果。
今度は [%E3%80%80] に変換されました。
これもデコードして確認してみてください。
このようにエンコード結果をデコーダーで簡単にデコードできるのは、変換のための「一定のルール」が公開されており、デコーダーがルールに基づき機械的に置き換えできるからです。
上記の例では
半角スペース = +
全角スペース = %E3%80%80
の置き換えルールがあるので、Googleは文字列を機械的に置き換えることでエンコードし、デコーダーは機械的に置き換えることでデコードしました。
暗号化例: RSA暗号
これは自分で確認するのは難しいので、確認してくれた方の苦労を動画と動画解説記事で見てみてください。
動画で雰囲気だけ掴んでいただければよく、ここで理解していただきたいのは次の2点だけです。
計算方法はわかっている
ただしとんでもなく大変
動画。
解説記事。
真面目なRSA暗号の解説はこちら。
動画と一緒に見ると、各設問で何をしていたかがわかります。
動画の問2で設問の一部として与えられるp, q, eは秘密情報 (公開されていない情報) であり、暗号化する人と復号する人しか持っていないものです。
このp, q, eを共有するための方法が秘密鍵と公開鍵 (証明書) です。
動画の問3、4は複合用の証明書 (秘密鍵/公開鍵) を持っていない場合の復号をシミュレートしたものです。
つまりRSA暗号は復号のための「一定のルール」のうち計算方法は公開されているものの、計算に必要ないくつかの値が秘匿されているため、普通にやったら復号できないわけです。
JWT
APIのシナリオではアイデンティティプロバイダ、つまりユーザーの認証 (ユーザーの本人確認) ができるシステム、あるいはそれに準じる機能を持つものがJWTを生成します。
例えばOktaやAuth0などのIDaaS, ソーシャルログオンに使うFacebookやTwitter, あるいはそこが発行したトークンを精査して自分の持つディレクトリと付き合わせ、その内容を信頼できると判定できるもの、例えばAPI Gatewayなどです。
JWTのRFCはこちら。
3.1 のところにJWTのサンプルがあるのでこれを使って解説しましょう。
わかりやすいように区切り文字 (ピリオド) 前後のスペースもそのままコピー、ピリオドで改行しました。
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9 .
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ .
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
ピリオドを境界線にして3つのパーツから構成されています。
それぞれのパーツはBase64でエンコードされています。
エンコードということはデコーダーを使うと機械的に読めるものに変換できるということです。
デコーダーは例えばこちら。
https://tool-taro.com/base64_decode/
最初のパーツ: ヘッダー
最初のピリオドの前、[…J9] までをデコードしてみます。
{"typ":"JWT",
"alg":"HS256"}
これがJWTであることと、アルゴリズムが書いてあります。
アルゴリズムは3つのパーツのうち最後のパーツで使います。
真ん中のパーツ: ペイロード
本文部分です。デコードすると次のようになります。
{"iss":"joe",
"exp":1300819380,
"http://example.com/is_root":true}
このiss、expなどをクレームと呼びます。
このペイロードにはいろいろ情報を格納してよいので、必要に応じてこれ以外の情報も格納します。
どんな情報を格納してもいいかというと基本的に何でもありです。
ただしみんなが好き勝手にクレームを作ると受け取り側は格納された情報をどう解釈していいかわからなくなります。
そういうことが起こらないよう、基本情報は「パブリッククレーム」として決まっています。
ざっと見ると例えばname, family_name, email, gender, phone_numberといったような個人にまつわる情報や、at_user_nbrなどのトークンの取り扱いにまつわる情報を意味するクレームなどさまざまです。
これ以外に発行側が必要に応じて自由にクレームを作ることができます。
プライベートクレームと呼びます。
最後のパーツ: 署名
最後は電子署名です。
さすがにこれはBase64でそのままデコードしても読めません。
署名部分の仕組みはJWSの仕様を参照してください。A-1のところにJWTのRFCで使ったサンプルを暗号化して署名とする仕組みが解説されています。
暗号化のアルゴリズムとしてヘッダーに指定されたアルゴリズムが使用されます。
暗号化する対象はヘッダーとペイロード部分です。
ここで理解しておきたいのは、暗号化の内容や復号の仕組みやルールではありません。
署名を使って改ざん対策をしているということが理解できればOKです。
使ったことがある方はJWTはよくHTTPヘッダーを使って受け渡しされることをご存知だと思います。
このHTTPヘッダーは改ざんが簡単です。
例えばcURLやPostman、SoapUIなどを使ったことがある方は、ヘッダー部分に何らかの文字列を使ってデータの送受信をしたことがありますよね。サーバー側が受け付けるかどうかは別として、HTTPを送信する側は基本的にはHTTPヘッダーには自分の好きな情報を格納できます。
ということは、サーバーが受け入れてくれそうな文字列を自分で考えてHTTPヘッダーに格納することも簡単です。
上記の説明を見れば、誰でも「なるほど、JWTのヘッダー部分と本文部分は自分でも書けるな」ということがわかると思います。
では「自分以外の誰かの情報を入れてみよう!」も簡単です。これでは書いてある内容は信頼できません。
そこでJWTの発行元はヘッダーやペイロードの部分を暗号化して電子署名を作成し、受け取り側のサーバーでは受け取って良いJWTの発行元の証明書を信頼する設定をすることで次を実現しています。
信頼する発行元のJWTしか信じない仕組み
改ざんを検知できる仕組み
JWTからクレームを取り出して処理に引き渡すのは簡単だ
APIを使った実装の話をしていてJWTの話をしなければいけないのは、API Gatewayを使って認証した結果をどうやって裏側のバックエンドAPI側に渡したらいいかです。
クレームベースアクセスコントロールという実装パターンがあります。ヘッダーにJWTを格納してバックエンドに渡すことで、アイデンティティ情報をバックエンドAPIが利用できるようにする仕組みのことです。
ではバックエンドAPI側でJWTを受け取ったら何をしなければならないか。
JWTの検証 (電子署名の検証など)
Base64で書かれたペイロード部分のデコード
ペイロード部分のJSONをパースしてクレームを取り出す
それぞれライブラリにお任せできる領域なので難しくありません。
Kubernetes上で実施するならJWTの検証の部分はサービスメッシュにお任せできます。
理屈がわかってしまえばとても簡単です。ご活用ください。