RFC 8725 JSON Web Token Best Current Practices をざっくり解説する

ritou です。

今回は [RFC 8725 JSON Web Token Best Current Practices](https://www.rfc-editor.org/rfc/rfc8725.html) を紹介します。
みんな大好き JWT (JSON Web Token) の BCP ときたらチェックせずにはいられないでしょう。

## 概要

* JWTは **署名/暗号化が可能な一連のクレームを含む、URLセーフなJSONベースのセキュリティトークン** です
* JWTは、デジタルアイデンティティの分野および他のアプリケーション分野の両方の多数のプロトコルおよびアプリケーションにて、シンプルなセキュリティトークンフォーマットとして広く使用/展開されています
* このBCPの目的は、JWTの確実な導入と展開につながる実行可能なガイダンスを提供することです

ということで、何かのフレームワークでもプロトコルでもなければJWTを使ったユースケース考えたよって話でもなく、JWTを導入する上で基本的な部分の安全補強のためのドキュメントと言えそうです。

## 対象読者

* JWTライブラリの実装者
* ライブラリを利用するコードの実装者
* IETF内外でJWTに依存する仕様策定者

JWT触るやつらはみんな読んでおけという感じですね。

## 脅威と脆弱性

内容は2章の脅威と脆弱性の一覧に対して3章で対策の一覧が書いてあります。

### 2.1. Weak Signatures and Insufficient Signature Validation / 弱い署名と不十分な署名検証

これは JWT Header の `alg` パラメータにて定義されるアルゴリズムに関連した攻撃です。

* 攻撃者が `none` に書き換え、検証側がそれを信用して署名検証をスキップ : ライブラリが JWT Header の alg の値を信用して署名検証をスキップしてしまうお話です
* 攻撃者が `RS256` を `HS256` に書き換え、検証側は RSA 公開鍵を HMAC の共有鍵として署名検証 : こちらも JWT Header の alg の値を信用し、署名検証用の関数の引数として指定したRSA公開鍵を共有鍵として扱ってしまうお話です

「ここがダメだよ JWT」みたいな記事で言及されることが多いやつですが、JWTの署名検証なんてのは1から作らず、ライブラリに任せるべきでしょう。
ライブラリ開発者は次のような対策をとり、利用する側もそれを検証できると良いでしょう。

* 3.1. Perform Algorithm Verification / アルゴリズムをちゃんと検証する
* 3.2. Use Appropriate Algorithms / 適切なアルゴリズムを使用する

### 2.2. Weak Symmetric Keys / 弱い対称鍵

これは、人間が記憶可能なパスワードのような弱い共有鍵 + `HS256` のような MAC アルゴリズムの組み合わせを使うとオフラインの総当たり攻撃や辞書攻撃を受ける可能性があるというお話です。

これはライブラリ開発者というよりは、ライブラリを利用する側が気をつける必要がありますし、プロトコルにJWT利用する場合も鍵の使い方で注意が必要です。

対策は 3.5. Ensure Cryptographic Keys Have Sufficient Entropy / 暗号化のキーに十分なエントロピーを確保する となります。

### 2.3. Incorrect Composition of Encryption and Signature / 暗号化と署名の正しくない構成

何かに JWS で署名をつけて、それを JWE で暗号化した JWT をライブラリが扱う場合、どこかの検証に漏れが発生する可能性があります。
対策は 3.3. Validate All Cryptographic Operations / 全ての暗号処理をしっかりやる となります。JWTがネストしてるような場合に気をつけましょう。

### 2.4. Plaintext Leakage through Analysis of Ciphertext Length / 暗号文の長さの解析によるプレーンテキストの漏洩

これは完全に暗号の世界のお話ですね。
暗号化する前に圧縮するとプレーンテキストに関する情報が漏えいする。CRIMEやBREACHって覚えてますか...あれのJWT版といったところです。
参考文献っぽいのが載っています。

* [Compression and Information Leakage of Plaintext](https://link.springer.com/chapter/10.1007%2F3-540-45661-9_21)
* [Protecting Encrypted Cookies from Compression Side-Channel Attacks](https://link.springer.com/chapter/10.1007%2F978-3-662-47854-7_6)

対策は 3.6. Avoid Compression of Encryption Inputs / 暗号化入力値の圧縮を避ける となります。

### 2.5. Insecure Use of Elliptic Curve Encryption / 楕円曲線暗号の安全でない利用

[Critical Vulnerability Uncovered in JSON Encryption](https://blogs.adobe.com/security/2017/03/critical-vulnerability-uncovered-in-json-encryption.html) によると、いくつかの JOSE ライブラリは楕円曲線ディフィー・ヘルマン鍵共有("ECDH-ES") の検証時に誤って "成功" として扱ってしまうバグを含んでいます。
JWEのコンテンツ暗号化のための鍵については [JSON Web Encryption (JWE) の解説](https://qiita.com/ono_matope/items/938a98fb111a297b68b9) で説明されています。
攻撃者は "invalid curve points" を利用して検証した結果のクリアテキストを知ることで、JWT 受信者のコンテンツ暗号化のための秘密鍵を知ることができます。

対策としては、 3.4. Validate Cryptographic Inputs / 入力された暗号鍵の検証 に書かれている、公開鍵の有効性の検証をする必要があります。

### 2.6. Multiplicity of JSON Encodings / JSON エンコーディングの多様性

JSON の仕様が RFC 7159 から RFC 8259 に置き換わったところで、(クローズなエコシステムな場合を除いて)エンコードに UTF-8 を使うようになりました。

> JSON text exchanged between systems that are not part of a closed ecosystem MUST be encoded using UTF-8.

とはいえ、古い環境やクローズなエコシステムの中では複数のエンコーディングが混在することで受信者が誤った解釈をすることがあり、攻撃者に検証処理をバイパスされたりする可能性があります。

なので対策としては 3.7. Use UTF-8 / UTF-8 を利用 となります。

### 2.7. Substitution Attacks / 置換攻撃 

発行者の意図した受信者に与えられた JWT が意図しない別の受信者で利用される攻撃です。

例として OAuth 2.0 の例が挙げられています。

* JWT形式のアクセストークンを利用してリソースサーバー1にアクセスする
* リソースサーバー1は受け取ったアクセストークンをリソースサーバー2に提示してリソースを取得しようとする
* リソースサーバー2は受け取ったアクセストークンがリソースサーバー1を利用するためのものだと言うことを検証せずにリソースを提供してしまう
* 攻撃者は意図しないリソース(リソースサーバー2)にアクセスできる

こんなのもはやJWTの問題でもない気もしますが、JWTでできる対策としてはクレームをちゃんと検証しようと言うものです。

* 3.8. Validate Issuer and Subject / 発行者、対象者の検証
* 3.9. Use and Validate Audience / JWTの利用者の利用と検証

OAuth 2.0 の Implicit Grant における Token Substitution Attack の方がわかりやすい気がします。

### 2.8. Cross-JWT Confusion / JWT間の取り違え

JWTは色々なところで使われているため、目的外の JWT の利用を防ぐことが重要です。
ある目的で発行された JWT トークンを別の目的で利用することは、特定の種類の置換攻撃です。
JWTの混同が起こりうるアプリケーションコンテキストでは、このような置換攻撃を防ぐための対策が必要です。

2.7. とこれで似ていますが、

* 2.7. Substitution Attacks : ある用途、受信者に向けて発行された JWT を同じ用途で **別の受信者** に利用する
* 2.8. Cross-JWT Confusion : ある用途で発行された JWT を **別の用途** で利用する

と言う整理がされているようです。

対策としては各クレームの値の検証と、それぞれで排他的な検証ルールを用意することです。

* 3.8. Validate Issuer and Subject / ”iss”, "sub" クレームの検証
* 3.9. Use and Validate Audience / ”aud” クレームの利用と検証
* 3.11. Use Explicit Typing / 明確な種類の利用
* 3.12. Use Mutually Exclusive Validation Rules for Different Kinds of JWTs / 様々な JWT のための相互排他的な検証ルールの利用

ライブラリを利用する側が意識して必要なクレームを含み、検証することが重要ですね。

### 2.9. Indirect Attacks on the Server / サーバーへの間接的攻撃

JWTの受信者は、JWTのクレームの値を利用してDBやLDAPの検索などをしたり、サーバーにより検索されるURLが含まれていたりします。
このようなリクエストにおいて、攻撃者は JWT のクレームの値をインジェクション攻撃やSSRFなどの媒体として利用できます。

対策としては 3.10. Do Not Trust Received Claims / 受け取ったクレームを信用しない があります。
例として "kid", "jku", "x5u" が挙げられていますが、どんな値でも検索のキーに利用しないとか、とりあえずURLにアクセスしない方が良いねというのが書かれています。

実際は鍵周りのクレームに限らない話なので、ライブラリ開発者というよりもライブラリを利用する側が気をつけるべき点と言えるでしょう。
「キーになりうる値はサニタイズしろ」みたいな流れ、なんか懐かしいですね。

## ベストプラクティス

上にちょこちょこ書いたのでざっくりしか書いていません。

### 3.1. Perform Algorithm Verification / アルゴリズムをちゃんと検証する

ずっと前、とあるPerlのライブラリでアルゴリズムに関する脆弱性への対応が行われ、アップデートしてくれ！みたいな流れを見かけたことがあります。

* ライブラリは検証処理を呼び出す側がサポートするアルゴリズムのセットを指定できるようにして、それ以外のアルゴリズムを利用しない
* "alg", "enc" ヘッダーの値が暗号処理で利用されたものと同一であることを保証する
* それぞれの鍵はアルゴリズムに一意に紐つくようにして、それを暗号化処理で利用する

OSSなライブラリであれば、検証の関数の引数などを見ればこの辺りが対応されているかどうかわかると思います。

### 3.2. Use Appropriate Algorithms / 適切なアルゴリズムを使用する

アプリケーションのセキュリティ要件を満たすアルゴリズムのみの利用を許可する必要があります。
このアルゴリズムのセットは新しいのが追加されたり古いのが削除されたり時間によって変わるので、アプリケーションも暗号処理の切り替えを可能とする設計にする必要があります。
あとは alg=none の扱い方とアルゴリズム固有の推奨事項に気をつけることなどが書かれています。

### 3.3. Validate All Cryptographic Operations / 全ての暗号処理をしっかりやる

ネストされた JWT でもちゃんと検証しましょう。

### 3.4. Validate Cryptographic Inputs / 入力された暗号鍵の検証

ECDH-ES などの一部の暗号処理は、無効な入力を受けとる可能性があるので、ちゃんと検証するかちゃんと検証するライブラリを利用しましょう。

### 3.5. Ensure Cryptographic Keys Have Sufficient Entropy / 暗号化のキーに十分なエントロピーを確保する

これもこれまでの知見が整理されています。

* [RFC7515 : 10.1.  Key Entropy and Random Values](https://www.rfc-editor.org/rfc/rfc7515.html#section-10.1) 
* [RFC7518 : 8.8.  Password Considerations](https://www.rfc-editor.org/rfc/rfc7518.html#section-8.8)
* 人間が記憶できるパスワードはMAC系アルゴリズムの鍵として直接利用してはいけない(MUST NOT)
* パスワードはコンテンツの暗号化ではなくキーの暗号化のために利用する。 [RFC7518 : 4.8.  Key Encryption with PBES2](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8)その場合でもブルートフォース攻撃の対象となることを意識する

### 3.6. Avoid Compression of Encryption Inputs

データの圧縮は暗号化の前にしてはいけません。

### 3.7. Use UTF-8

エンコーディングの混在によるリスクを回避するため、とにかくUTF-8 を使いましょう。

### 3.8. Validate Issuer and Subject / "iss", "sub" クレームの検証

JWT が "iss" クレームを含む場合、暗号化処理で利用される鍵が発行者のものであることを検証する必要があります。
鍵の検証方法は OpenID Connect の jwks_uri の値を使ったり、アプリケーションによります。
同様に "sub" クレームを含む場合、その値もしくは "iss" との組み合わせで表現される対象者がアプリケーションで有効であることを検証する必要があります。

"iss" はわかりますが、"sub" の検証までできるかというとどうでしょうね。
OAuth 2.0 でクライアントからアクセストークンを受け取ったリソースサーバーが、トークンインストロペクションAPI を使ってそれぞれが有効であることを検証するみたいな話かもしれません。

### 3.9. Use and Validate Audience / ”aud” クレームの利用と検証

同じ発行者から複数のリライングパーティー、アプリケーションによる利用を意図した JWT を発行できる場合、"aud" クレームを利用します。
リライングパーティー、アプリケーションが "aud" クレームの値を検証することで、攻撃者によって意図しないパーティー向けのものに置き換えられていないことを判定できます。

### 3.10. Do Not Trust Received Claims / 受け取ったクレームを信用しない

検索のキーとなる値を JWT のクレームとして受け取る際は、検証/サニタイズにより SQL/LDAP インジェクションなどを防ぎましょう。
"jku", "x5u" といった任意のURLを扱う場合、SSRFの攻撃を受けるリスクがあるので、URLをホワイトリストで検証したり、GETリクエストに何もCookieなどを含まないようにして対策しましょう。

### 3.11. Use Explicit Typing / 明確な種類の利用

JWTの混同を避けるため、JWTに型(種類)を導入し、それをチェックする検証ルールを用意しましょうという話です。

JWTのヘッダーパラメーターである "typ" に JWT の種類を表す値を指定することで実現可能です。
例として [RFC 8417 Security Event Token (SET)](https://tools.ietf.org/html/rfc8417) では明確に分類を行うために "application/secevent+jwt" というメディアタイプが定義されていますが、これを JWT の "typ" で表現する場合は "secevent+jwt" を指定すべきです。

あとは、ネストされたJWTの場合もちゃんと "typ" をつけようというのと、古くて "typ" 使ってない JWT は当然扱えないので曖昧さを解消できない場合があるというところです。
新しく JWT を利用する際、 "typ" の利用が推奨されます。

### 3.12. Use Mutually Exclusive Validation Rules for Different Kinds of JWTs / 様々な JWT のための相互排他的な検証ルールの利用

JWT を扱うアプリケーションは、必須/任意なクレームや検証ルールなどをプロファイルとして定義する必要があります。
1つの発行者が複数の JWT を発行する場合、それらの検証ルールは異なる種類の JWT を拒否して相互に排他的にならないといけません。
この手の分類には、いくつかやり方があります。

* "typ" で明示的に分類する
* 必須のクレームもしくはクレームのセットで分類する
* 必須のヘッダパラメータやヘッダパラメータのセットで分類する
* 署名作成、検証に使うキーで分類する
* "aud" で分類する
* "iss" で分類する

私が同様のことを考えたときは、キーによる分類を入れることで署名検証と同時に行うやり方を選びました。(参考: [JSON Web Signature導入における鍵周りの基本的な考え方](https://qiita.com/ritou/items/a861e952ccea8ba9e68c))
これはこれで、"kid" のプレフィックスに用途を入れて分類しているので若干無理矢理感があります。
3.11. の内容とも重なりますが、BCP的には新しく JWT を利用するアプリケーションでは "typ" の利用を推奨しているとのことなので参考にするのが良さそうです。

## まとめ

9個の脅威、脆弱性と12個のベストプラクティスを紹介しました。
どうしてもJWS/JWEでは暗号化処理がキモなので、その辺りとクレームの扱い方が書かれていました。

昨年までと同様、今年もたくさん「JWTは実装に脆弱性を生み出しやすいので危ねぇ」 なんていう記事を見かけることでしょう。
そんな時、これを読んでいればいちいち心を揺さぶられることなく手元にあるあなたのプロダクトでeyJの生成を始められるかもしれません(???)

![animal_chara_neko_police.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10192/d8240779-482a-b563-cc0f-df270443060d.png)

ではまた。