JWT ハンドブック

著者: Sebastian Peyrott

JWT ハンドブック
Sebastián E. Peyrott、Auth0 Inc.
バージョン 0.14.1、2016〜2018⽬次
⽬次 ........................................................................................................................................................................................ 1
謝辞 ........................................................................................................................................................................................ 4
はじめに ................................................................................................................................................................................ 5
1.1
JSON Web Token とは ........................................................................................................................................ 5
1.2
JWT が解決する問題 ......................................................................................................................................... 6
1.3
JWT の簡単な歴史 ............................................................................................................................................. 6
実際の利⽤例 ........................................................................................................................................................................ 7
2.1
クライアント側セッション/ステートレス セッション ................................................................................ 7
2.1.1 セキュリティに関する考慮事項 ..........................................................................................................8
2.1.1.1 署名の削除 ............................................................................................................................ 8
2.1.1.2 クロスサイト リクエスト フォージェリ (CSRF) ......................................................... 8
2.1.1.3 クロスサイト スクリプティング (XSS) .......................................................................... 9
2.1.2 クライアント側セッションの有⽤性 ................................................................................................ 11
2.1.3 例 ............................................................................................................................................................ 11
2.2
フェデレーション アイデンティティ ........................................................................................................... 14
2.2.1 アクセス トークンとリフレッシュ トークン ................................................................................15
2.2.2 JWT と OAuth2 ....................................................................................................................................16
2.2.3 JWT と OpenID Connect......................................................................................................................16
2.2.3.1 OpenID Connect のフローと JWT ................................................................................... 16
2.2.4 例 ............................................................................................................................................................16
2.2.4.1 Node.js アプリのための Auth0 Lock のセットアップ ................................................ 17

JSON Web Token の詳細 .................................................................................................................................................. 19
3.1
ヘッダー ............................................................................................................................................................. 20
3.2
ペイロード ......................................................................................................................................................... 20
3.2.1 登録済みクレーム ................................................................................................................................21
3.2.2 パブリック クレームとプライベート クレーム ............................................................................21
3.3
セキュリティ保護のない JWT ....................................................................................................................... 23
3.4
セキュリティ保護のない JWT の作成 ......................................................................................................... 23
3.4.1 サンプル コード ..................................................................................................................................24
3.5
セキュリティ保護のない JWT の解析 ......................................................................................................... 24
3.5.1 サンプル コード ..................................................................................................................................24

1

JSON Web Signatures ........................................................................................................................................................ 26
4.1
署名付き JWT の構造 ..................................................................................................................................... 26
4.1.1 コンパクト シリアライゼーションに使⽤されるアルゴリズムの概要 ......................................27
4.1.2 署名アルゴリズムの実際的な側⾯ ....................................................................................................28
4.1.3 JWS のヘッダー クレーム .................................................................................................................30
4.1.4 JWS JSON シリアライゼーション ....................................................................................................31
4.1.4.1 フラット化 JWS JSON シリアライゼーション ............................................................ 32
4.2
トークンの署名と検証 ..................................................................................................................................... 32
4.2.1 HS256: HMAC + SHA-256 ....................................................................................................................33
4.2.2 RS256: RSASSA + SHA256 ...................................................................................................................33
4.2.3 ES256: P-256 および SHA-256 を使⽤した ECDSA ......................................................................33
JSON Web Encryption (JWE) .......................................................................................................................................... 35
5.1
暗号化された JWT の構造 ............................................................................................................................. 37
5.1.1 鍵暗号化アルゴリズム ........................................................................................................................37
5.1.1.1 鍵管理モード ...................................................................................................................... 38
5.1.1.2 コンテンツ暗号化鍵 (CEK) と JWE 暗号化鍵 .......................................................... 39
5.1.2 コンテンツ暗号化アルゴリズム ........................................................................................................40
5.1.3 ヘッダー ................................................................................................................................................40
5.1.4 コンパクト シリアライゼーションに使⽤されるアルゴリズムの概要 ......................................41
5.1.5 JWE JSON シリアライゼーション ....................................................................................................41
5.1.5.1 フラット化 JWE JSON シリアライゼーション............................................................ 43
5.2
トークンの暗号化と復号 ................................................................................................................................. 43
5.2.1 導⼊: node-jose を使⽤した鍵の管理 .................................................................................................43
5.2.2 AES-128 Key Wrap (鍵) + AES-128 GCM (コンテンツ) ....................................................................44
5.2.3 RSAES-OAEP (鍵) + AES-128 CBC + SHA-256 (コンテンツ)..........................................................45
5.2.4 ECDH-ES P-256 (鍵) + AES-128 GCM (コンテンツ) .........................................................................45
5.2.5 ネストされた JWT: P-256 および SHA-256 を使⽤した ECDSA (署名) + RSAES-OAEP (暗号化鍵) + AES-128 CBC + SHA-256 (暗号化コンテンツ) ....................................................................45
5.2.6 復号 ........................................................................................................................................................46
JSON Web Key (JWK) ...................................................................................................................................................... 48
6.1
JSON Web Key の構造 ...................................................................................................................................... 49
6.1.1 JSON Web Key Set .................................................................................................................................50
JSON Web Algorithms ....................................................................................................................................................... 51
7.1
⼀般的なアルゴリズム ..................................................................................................................................... 51
7.1.1 Base64 .....................................................................................................................................................51
7.1.1.1 Base64-URL ......................................................................................................................... 53
7.1.1.2 サンプル コード ................................................................................................................ 53
7.1.2 SHA ........................................................................................................................................................54
7.2
署名アルゴリズム ............................................................................................................................................. 57
7.2.1 HMAC ....................................................................................................................................................57
7.2.1.1 HMAC + SHA256 (HS256) ................................................................................................. 59
7.2.2 RSA .........................................................................................................................................................61
7.2.2.1 e、d、n の選択 .................................................................................................................. 62
7.2.2.2 基本的な署名⽅法 .............................................................................................................. 63
7.2.2.3 RS256: SHA-256 を使⽤した RSASSA PKCS1 v1.5 ...................................................... 63
7.2.2.3.1
アルゴリズム ...................................................................................................... 63
7.2.2.3.1.1
EMSA-PKCS1-v1_5 プリミティブ......................................................... 65
7.2.2.3.1.2
OS2IP プリミティブ ................................................................................ 65
7.2.2.3.1.3
RSASP1 プリミティブ ............................................................................ 66
2

7.3

7.2.2.3.1.4
RSAVP1 プリミティブ ............................................................................ 66
7.2.2.3.1.5
I2OSP プリミティブ ................................................................................ 66
7.2.2.3.2
サンプル コード ................................................................................................ 66
7.2.2.4 PS256: SHA-256 および MGF1 と SHA-256 を使⽤した RSASSA-PSS ................. 71
7.2.2.4.1
アルゴリズム ...................................................................................................... 71
7.2.2.4.1.1
MGF1: マスク⽣成関数........................................................................... 72
7.2.2.4.1.2
EMSA-PSS-ENCODE プリミティブ ...................................................... 72
7.2.2.4.1.3
EMSA-PSS-VERIFY プリミティブ........................................................ 73
7.2.2.4.2
サンプル コード ................................................................................................ 75
7.2.3 楕円曲線 ................................................................................................................................................76
7.2.3.1 楕円曲線の演算 .................................................................................................................. 78
7.2.3.1.1
点の加算 .............................................................................................................. 78
7.2.3.1.2
点の 2 倍算 ........................................................................................................ 78
7.2.3.1.3
スカラー倍算 ...................................................................................................... 79
7.2.3.2 楕円曲線デジタル署名アルゴリズム (ECDSA) ............................................................ 79
7.2.3.2.1
楕円曲線のドメイン パラメーター ................................................................ 81
7.2.3.2.2
公開鍵と秘密鍵 .................................................................................................. 81
7.2.3.2.2.1
離散対数問題 ............................................................................................ 82
7.2.3.2.3
ES256: P-256 および SHA-256 を使⽤した ECDSA.................................... 82
今後の更新 ......................................................................................................................................................... 84

付録 A: 現在のベスト プラクティス ............................................................................................................................ 85
8.1
隠れた危険とよく⾒られる攻撃 ..................................................................................................................... 85
8.1.1 "alg: none" 攻撃 ....................................................................................................................................86
8.1.2 RS256 公開鍵を HS256 共有シークレットとして使⽤する攻撃 ................................................87
8.1.3 脆弱な HMAC 鍵 ................................................................................................................................88
8.1.4 暗号化と署名検証の併⽤に関する誤解 ............................................................................................89
8.1.5 無効な楕円曲線による攻撃 ................................................................................................................90
8.1.6 置換攻撃 ................................................................................................................................................91
8.1.6.1 受信者が異なる場合 .......................................................................................................... 91
8.1.6.2 受信者が同じ場合/Cross JWT ........................................................................................... 92
8.2
対策とベストプラクティス ............................................................................................................................. 93
8.2.1 アルゴリズムの検証を常に実⾏する ................................................................................................93
8.2.2 適切なアルゴリズムを使⽤する ........................................................................................................94
8.2.3 常にすべての検証を実⾏する ............................................................................................................94
8.2.4 暗号の⼊⼒を必ず検証する ................................................................................................................94
8.2.5 強⼒な鍵を選択する ............................................................................................................................94
8.2.6 使⽤可能なすべてのクレームを検証する ........................................................................................94
8.2.7 typ クレームを使⽤してトークンの種類を区別する ...................................................................95
8.2.8 トークンごとに異なる検証ルールを使⽤する ................................................................................95
8.3
まとめ ................................................................................................................................................................. 953

謝辞
第 2 章のフェデレーション アイデンティティの例を提供してくれた Prosper Otemuyiwa ⽒、本書のレビューを担当
し、私の執筆時間を確保してくれた Diego Poza ⽒、本書の難しい箇所をレビューしてくれた Matías Woloski ⽒、私のリクエストに応じてくれた Martín Gontovnikas ⽒、全体の体裁を整えてくれた Bárbara Mercedes Muñoz Cruzado ⽒、本書の配布に関する前⼯程と後⼯程の業務を担当してくれた Alejo Fernández ⽒と Víctor Fernández ⽒、全⼒でチームメイトを⽀援してくれた Sergio Fruto ⽒、プロジェクトを進⾏させながらも各関係者にヒアリングする時間を確保してくれた Federico Jack ⽒に、⼼から感謝を申し上げます (順不同)。

4

第 1 章はじめにJSON Web Token (略称 JWT、「ジョット」と発⾳) は、スペースに制約のある環境でクレームを安全に渡すために利⽤される標準仕様です。JWT は多くの1 主要2 Web3 フレームワーク4で利⽤されています。このアーキテクチャの主な特徴
は、シンプルかつコンパクトで使いやすいことです。JWT よりもはるかに複雑な⽅式5 もまだ使⽤されていますが、JWTはさまざまな形で利⽤されています。本書では、JWT のバイナリ表現や JWT の構成に使⽤されるアルゴリズムなど、
JWT のアーキテクチャの重要な特徴について説明するほか、JWT が業界で⼀般にどのように使⽤されているかを紹介し
ます。

1.1

JSON Web Token とはJSON Web Token (JWT) は次のようなものです (⾒やすいように改⾏を挿⼊しています)。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
この状態では何が書かれているのかさっぱりわかりませんが、JWT は⾮常にコンパクトかつ表⽰可能な形で⼀連のクレームを表現し、信頼性を検証するための署名も使⽤できるという特徴があります。

{"alg": "HS256","typ": "JWT"}{"sub": "1234567890","name": "John Doe","admin": true
https://github.com/auth0/express-jwt
https://github.com/nsarno/knock
3 https://github.com/tymondesigns/jwt-auth4 https://github.com/jpadilla/django-jwt-auth5 https://ja.wikipedia.org/wiki/Security_Assertion_Markup_Language
1
2

5

}
クレームとは、特定の当事者またはオブジェクトに関する定義または表明です。クレームとその意味は、⼀部が JWT の仕様として定義されています。その他のクレームはユーザーにより定義されます。JWT が優れている点は、⼀般的な処理でよく使⽤されるクレームが標準化されていることです。⼀般的な処理とは、たとえば、特定の当事者のアイデンティ
ティを確⽴することです。そのため JWT には、標準クレームの 1 つとして sub (「subject (主題)」を表す) クレームが⽤意されています。標準クレームについては、第 3 章で詳しく説明します。
JWT のもう 1 つの重要な特徴は、JSON Web Signatures (JWS、RFC75156) を使⽤して署名したり、JSON Web Encryption(JWE、RFC75167) を使⽤して暗号化したりできることです。JWT を JWS や JWE と組み合わせることで、さまざまな問題を解決する強⼒でセキュアなソリューションを実現できます。

1.2

JWT が解決する問題

JWT の主な⽤途は⼆者間でクレームをやり取りすることですが、この⽤途における JWT の最も重要な特徴は、検証や暗号化にも対応できるシンプルなコンテナー形式を利⽤した標準化の取り組みであることだと⾔えるでしょう。従来は、個⼈や組織がこの問題に対して個別に解決策を実施してきました。特定の当事者に関するクレームの設定には、従来型の標準8 を使⽤することもできますが、JWT の登場により、シンプルかつ便利で標準的なコンテナー形式を利⽤できるよ
うになりました。
ここまでの説明は少し抽象的でしたが、JWT の⽤途は、ログイン システムなど、容易に想像がつくものです。実際の利⽤例については、第 2 章で詳しく説明します。それ以外の利⽤例としては次のものが挙げられます。

• 認証
• 認可
• フェデレーション アイデンティティ• クライアント側セッション ("ステートレス" セッション)• クライアント側シークレット

1.3

JWT の簡単な歴史

2011 年、JSON Object Signing and Encryption (JOSE) グループが結成されました9。このグループの⽬的は、JSON を使⽤するプロトコルのセキュリティ サービスの相互運⽤性を実現するために、整合性保護 (署名および MAC) と暗号化のメカニズム、および鍵とアルゴリズム識別⼦の形式を標準化することでした。2013 年までに、グループが考案したアイ
デアのさまざまな使⽤例を⽰したクックブックなどを含め、⼀連の草稿が公開されました。これらの草稿は、後に JWT、JWS、JWE、JWK、JWA の RFC となりました。2016 年の時点では、これらの RFC はインターネット標準化過程にあります。また、正誤表は発⾏されていません。現在、グループは活動休⽌中です。
主な仕様の著者は、Mike Jones ⽒10、Nat Sakimura ⽒11、John Bradley ⽒12 Joe Hildebrand ⽒13です。

https://tools.ietf.org/html/rfc7515
https://tools.ietf.org/html/rfc7516
8 https://ja.wikipedia.org/wiki/Security_Assertion_Markup_Language
9 https://datatracker.ietf.org/wg/jose/history/
10 http://self-issued.info/
11 https://nat.sakimura.org/
12 https://www.linkedin.com/in/ve7jtb
13 https://www.linkedin.com/in/hildjj
6
7

6

第 2 章実際の利⽤例JWT の構造や構築について詳しく説明する前に、実際の利⽤例を⾒てみましょう。この章では、今⽇業界で使⽤されている⼀般的な JWT ベースのソリューションについて、その複雑さ (簡潔さ) に焦点を当てながら説明します。すべてのコードが公開リポジトリ1 で提供されているため、⾃由に参照できます。以下の例は、本番環境を想定したものではありませんのでご注意ください。本番環境で使⽤するには、テスト ケース、ロギング、セキュリティ ベスト プラクティスが必要です。以下の例は説明⽤のものであり、要点だけを簡潔に⽰しています。

2.1

クライアント側セッション/ステートレス セッション

いわゆるステートレス セッションは、実際にはクライアント側のデータにすぎません。この利⽤例で重要なポイントは、
セッションの内容を認証および保護するために署名を使⽤し、さらに多くの場合は暗号化も使⽤する点です。クライアン
ト側のデータは改ざんされるおそれがあるため、バックエンドで慎重に処理する必要があります。
JWT では、JWS と JWE を利⽤することで、さまざまな種類の署名と暗号化を使⽤できます。署名を使⽤すると、デー
タが改ざんされているかどうかを検証できます。暗号化を使⽤すると、第三者に読み取られないようにデータを保護することができます。
ほとんどの場合、セッションに必要なものは署名だけです。セッションに格納されているデータが第三者に読み取られても、セキュリティやプライバシーの問題が発⽣しないためです。たとえば sub クレーム (サブジェクト クレーム) は、
通常は第三者に読み取られても安全なクレームです。サブジェクト クレームは⼀般的に、⼀⽅の当事者が他⽅の当事者を識別するために使⽤するものです (ユーザー ID やメール アドレスなどを思い浮かべてください)。このクレームは⼀意にする必要はありません。⾔い換えると、ユーザーを⼀意に識別するために別のクレームが必要になる場合があります。これは利⽤者が決めることができます。
ショッピング カートを表す items (アイテム) クレームなどは、第三者のアクセスを許すべきではないクレームです。
ショッピング カートには、ユーザーが購⼊しようとしている、つまりユーザーのセッションに関連付けられているアイ
テムが含まれる可能性があります。暗号化されていない JWT にアイテムが保存されている場合、第三者 (クライアント
側スクリプト) によってアイテムが収集される可能性があるため、プライバシーの問題が発⽣するおそれがあります。

1

https://github.com/auth0/jwt-handbook-samples

7

サーバー

ユーザー

タ

ー

デ
い

な

い

変
改

れ
さ

て

データ
できない

データを改変インターネット
クラウド

署名済みデータ
秘密鍵またはシークレット

悪意のある
ユーザー

改

変

さ

れ

て
い

な
い
デ

ー

タ

ユーザー

署名がチェックに合格した場合、サーバーはデータが改変されていないとみなし、署名付きデータを信頼します。

図 2.1: クライアント側の署名付きのデータ

2.1.1

セキュリティに関する考慮事項

2.1.1.1

署名の削除

署名付きの JWT に対する攻撃⼿法としてよく⾒られるのは、署名を単純に削除するというものです。署名付きの JWTは、ヘッダー、ペイロード、署名という 3 つの異なる要素で構成されます。3 つの部分は別々にエンコードされます。
そのため、署名を削除してからヘッダーを改変することで、署名付きでない JWT であると宣⾔することが可能です。特定の JWT 検証ライブラリを不⽤意に使⽤すると、署名付きでないトークンが有効なトークンとみなされ、攻撃者の都合のいいようにペイロードが改変されるおそれがあります。この問題は、検証を実⾏するアプリケーションで、署名付きでない JWT を無効とみなすことで簡単に解決できます。

サーバー

悪意のある
ユーザー

設定が不適切なサーバーが署名のない JWT を受け⼊れる

JWT 内のデータが改変されている

図 2.2: 署名の削除

2.1.1.2

クロスサイト リクエスト フォージェリ (CSRF)

クロスサイト リクエスト フォージェリとは、ユーザーのブラウザーを欺いて別のサイトから要求を送信させることに
8より、ユーザーがログインしているサイトに対して要求を実⾏しようとする攻撃⼿法です。この攻撃を⾏うには、特別に細⼯したサイト (またはアイテム) に攻撃ターゲットへの URL を埋め込む必要があります。よく⾒られる例は、src 属性で攻撃ターゲットを指定した <img> タグを不正なページに埋め込む⼿法です。以下はその例です。

<!-- This is embedded in another domain's site --><img src="http://target.site.com/add-user?user=name&grant=admin">上記の <img> タグにより、このタグを含むページがロードされるたびに target.site.com に要求が送信されます。
ユーザーが以前に target.site.com にログインしていて、このサイトが Cookie を使⽤してセッションをアクティブ
状態に保っている場合は、その Cookie も送信されます。ターゲット サイトに CSRF 対策が実装されていない場合、この要求はユーザーからの有効な要求として処理されます。JWT は、他のクライアント側データと同様に、Cookie として保管することができます。

な

不正

取得
グを
タ
g>
<im

侵害されたサーバー

ユーザー

不正

なタ
グが

要求
を

送信

安全でないサーバー

図 2.3: クロスサイト リクエスト フォージェリ

このケースでは、JWT の有効期間を短く設定することが効果的です。⼀般的な CSRF 対策としては、セッション Cookie
ごと、およびリクエスト トークンごとに適切な要求元から要求が実⾏された場合にのみ、特殊なヘッダーを要求に追加する⽅法があります。JWT (およびセッション データ) が Cookie として保存されていなければ、CSRF 攻撃は不可能です。ただしそれでも、クロスサイト スクリプティング攻撃は可能です。

2.1.1.3

クロスサイト スクリプティング (XSS)

クロスサイト スクリプティング (XSS) とは、信頼できるサイトに JavaScript を挿⼊しようとする攻撃⼿法です。挿⼊
された JavaScript は Cookie やローカル ストレージからトークンを盗み取ります。有効期限が切れていないアクセス
トークンが漏えいすると、悪意のあるユーザーがそれを使⽤して保護されたリソースにアクセスするおそれがあります。
XSS 攻撃は多くの場合、バックエンドに渡されたデータの検証の不備が原因で発⽣します (SQL インジェクション攻撃と似た⼿法です)。
XSS 攻撃の例としては、⼀般向けサイトのコメント セクションを利⽤したものが挙げられます。ユーザーがコメントを追加するたびに、そのコメントがバックエンドに保存され、コメント セクションをロードしたユーザーに表⽰されます。
バックエンドでコメントをサニタイズしていない場合、悪意のあるユーザーが、ブラウザーに <script> タグとして解釈させることを狙ったコメントを書き込むことができます。悪意のあるユーザーが任意の JavaScript コードを挿⼊して各ユーザーのブラウザーで実⾏させることができるため、Cookie として保存されている資格情報やローカル ストレージに保存されている資格情報が盗まれるおそれがあります。

9任サ
ユー意の ーバ
ザー Java ーにに送 Scr
信さipt をせる

悪意のあるユーザー

挿⼊された
JavaScript が収集したデータを送信脆弱なサーバーが ジ
pt レー らir
Sc ト かva ス eJa ル ooki 収集
カ C をー
ロ たは 情報
ま 資格侵害されたサーバー

図 2.4: 永続的なクロスサイト スクリプティング

サーバーが脆弱なサーバーに接続
不正なサーバーユーザー

任

挿⼊された JavaScript が収集したデータを送信

悪意のあるユーザー

意 脆
の 弱
な
Ja サ
va ー
Sc バ
rip ー
t が
を
提
供

特別に細⼯したリンクにより、
任意の JavaScript をサーバーからユーザーに送信させる

脆弱なサーバー

図 2.5: 反射型クロスサイト スクリプティング

対策は、バックエンドに渡されるすべてのデータを適切に検証することです。特に、クライアントから受信したデータは常にサニタイズする必要があります。Cookie を使⽤する場合は、HttpOnly フラグ2 を設定することで、JavaScript によるアクセスから保護できます。HttpOnly フラグは便利ですが、CSRF 攻撃から Cookie を保護するためには役⽴ちません。

2https://www.owasp.org/index.php/HttpOnly

10

2.1.2

クライアント側セッションの有⽤性

どのようなアプローチにも⻑所と短所がありますが、クライアント側セッションも例外ではありません3。アプリケーショ
ンによっては、⼤きなセッションが必要になる場合があります。要求 (または要求のグループ) ごとにセッションのステート情報をやり取りするため、ほとんどの場合、バックエンドでの通信が削減されるというメリットが失われます。そのため、バックエンドでのクライアント側データ検索とデータベース検索の間である程度のバランスを取る必要があります。その⽅法は、アプリケーションのデータ モデルによって異なります。アプリケーションによっては、クライアン
ト側セッションに適切にマッピングされないものもあります。また、クライアント側データに完全に依存しているアプリ
ケーションもあります。この問題に関しては、皆様⾃⾝が最終的な判断を⾏ってください。ベンチマークを実⾏し、特定のステート情報をクライアント側に保持するメリットを検討しましょう。JWT のサイズは⼤きすぎないか? データ転送量に影響が及ばないか? データ転送量の増加により、バックエンドでの遅延の減少効果が相殺されないか? 複数の⼩さな要求を 1 つの⼤きな要求にまとめることはできないか? 要求を 1 つにまとめても、⼤規模なデータベースの検索が必要になるか? これらの問題を検討することで、適切なアプローチを決定できるでしょう。

2.1.3

例

この例では、簡単なショッピング アプリケーションを作成します。ユーザーのショッピング カートはクライアント側に保存されます。この例では、複数の JWT を使⽤しています。ショッピング カートもそのうちの 1 つです。

• ID トークン⽤の JWT。このトークンにユーザーのプロファイル情報が保持され、UI に使⽤されます。
• API のバックエンドとやり取りするための JWT (アクセス トークン)。
• クライアント側のステート⽤の JWT (ショッピング カート)。
デコード状態のショッピング カートは次のとおりです。

{"items": [
0,
2,
4
],
"iat": 1493139659,"exp": 1493143259}
各アイテムは数値 ID で識別されます。エンコードして署名した JWT は次のようになります。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpdGVtcyI6WzAsMiw0XSwiaWF0IjoxNDkzMTM5NjU5LCJleHAiOjE0OTMxND
MyNTl9. 932ZxtZzy1qhLXs932hd04J58Ihbg5_g_rIrj-Z16Js
カート内のアイテムを表⽰するには、フロントエンドで Cookie からアイテムを取得するだけです。

function populateCart() {const cartElem = $('#cart');cartElem.empty();
const cartToken = Cookies.get('cart');if(!cartToken) {return;}const cart = jwt_decode(cartToken).items;
3

https://auth0.com/blog/stateless-auth-for-stateful-minds/

11

}

cart.forEach(itemId => {const name = items.find(item => item.id ==itemId).name; cartElem.append(`<li>${name}</li>`);});

フロントエンドでは署名がチェックされないことにご注意ください。JWT の内容を表⽰できるように、JWT がデコードされるだけです。チェック⾃体はバックエンドで実⾏されます。すべての JWT が検証されます。
次に、Express ミドルウェアとして実装したカート⽤ JWT の有効性をバックエンドでチェックする⽅法を⽰します。

function cartValidator(req, res, next) {if(!req.cookies.cart){ req.cart = { items: [] };} else {try {req.cart = {items: jwt.verify(req.cookies.cart,
process.env.AUTH0_CART_SECRET,
cartVerifyJwtOptions).items};} catch(e) {req.cart = { items: [] };}}next();}
アイテムが追加されると、新しいアイテムと署名が含まれた新しい JWT がバックエンドで作成されます。

app.get('/protected/add_item', idValidator, cartValidator, (req, res) => {req.cart.items.push(parseInt(req.query.id));
const newCart = jwt.sign(req.cart,
process.env.AUTH0_CART_SECRET,
cartSignJwtOptions);
res.cookie('cart', newCart,{ maxAge: 1000 * 60 * 60});
res.end();});

console.log(`Item ID ${req.query.id} added to cart.`);接頭辞 /protected が付いている場所は、API アクセス トークンによっても保護されます。これは express-jwt を使⽤して設定します。

app.use('/protected', expressJwt({
secret:jwksClient.expressJwtSecret(jwksOpts),
issuer: process.env.AUTH0_API_ISSUER,
audience: process.env.AUTH0_API_AUDIENCE,
requestProperty: 'accessToken',getToken: req => {12

return req.cookies['access_token'];}}));
つまり、/protected/add_item エンドポイントがアクセス トークン検証ステップを通過した後でカートを検証する必要があります。⼀⽅のトークンは API へのアクセス (認可) を検証し、もう⼀⽅のトークンはクライアント側データ(カート) の整合性を検証します。
アクセス トークンと ID トークンは、Auth0 によってアプリケーションに割り当てられます。そのためには、Auth0 ダッシュボードを使⽤してクライアント4 と API エンドポイント5 を設定する必要があります6。これらはフロントエンドから呼び出される Auth0 JavaScript ライブラリを使って取得されます。

//Auth0 Client ID
const clientId = "t42WY87weXzepAdUlwMiHYRBQj9qWVAT";//Auth0 Domain
const domain = "speyrott.auth0.com";const auth0 = new window.auth0.WebAuth({domain: domain,clientID: clientId,
audience: '/protected',scope: 'openid profile purchase',
responseType: 'id_token token',redirectUri: 'http://localhost:3000/auth/',responseMode: 'form_post'});//(...)$('#login-button').on('click', function(event){ auth0.authorize();});
audience クレームは、Auth0 ダッシュボードを使⽤して API エンドポイントに対して設定したクレームと⼀致している必要があります。
Auth0 認証/認可サーバーは、前述の設定を使⽤してログイン画⾯を表⽰した後、要求したトークンを使⽤して特定のパスにあるアプリケーションにリダイレクトします。これはバックエンドによって処理され、Cookie として設定されます。

app.post('/auth', (req, res) =>{ res.cookie('access_token', req.body.access_token, {httpOnly: true,
maxAge: req.body.expires_in * 1000});
res.cookie('id_token', req.body.id_token, {maxAge: req.body.expires_in * 1000});
res.redirect('/');});CSRF 対策の実装は皆様の課題とし、本書では省略します。完全なコード サンプルは、samples/statelesssessions ディレクトリに納められています。

https://manage.auth0.com/#/clients
https://manage.auth0.com/#/apis
6 https://manage.auth0.com
4
5

13

2.2

フェデレーション アイデンティティ

フェデレーション アイデンティティ7 システムを使⽤すると、異なる (さらに無関係の) 当事者間で認証および認可サー
ビスを共有することができます。⾔い換えるならば、ユーザーのアイデンティティを⼀元化できるということです。フェデレーション アイデンティティの管理ソリューションはいくつかありますが、その中でも SAML8 と OpenID Connect9は最も⼀般的なものです。認証と認可を⼀元化する特殊な製品も販売されています。前述の標準を実装している製品もあ
れば、まったく異なるものを使⽤している製品もあります。中には⼀元化のために JWT を使⽤している製品もあります。
認証と認可の⼀元化のために JWT を使⽤するかどうかは製品によって異なりますが、認可プロセスの基本的な流れは次のようになります。

アイデンティティ
プロバイダー

ユーザー

認可サーバーサーバー

リソース

図 2.6: ⼀般的なフェデレーション アイデンティティの処理の流れ
1. ユーザーがサーバーによって制御されているリソースへのアクセスを試みる。
2. ユーザーがリソースにアクセスするための適切な資格情報を持っていないため、サーバーがユーザーを認可サー
バーにリダイレクトする。認可サーバーは、アイデンティティ プロバイダーによって管理されている資格情報を使⽤してユーザーがログインできるように構成されている。
3. ユーザーが認可サーバーによってアイデンティティ プロバイダーのログイン画⾯にリダイレクトされる。
4. ユーザーが正常にログインし、認可サーバーにリダイレクトされる。認可サーバーは、アイデンティティ プロバ
イダーが提供する資格情報を使⽤して、リソース サーバーが要求する資格情報にアクセスする。
5. ユーザーが認可サーバーによってリソース サーバーにリダイレクトされる。この要求にはリソースへのアクセスに必要な正しい資格情報が含まれている。
6. ユーザーがリソースに正常にアクセスする。
サーバー間で渡されるすべてのデータは、リダイレクト要求に (通常は URL の⼀部として) 埋め込まれてユーザーを通過します。そのため、トランスポート セキュリティ (TLS) とデータ セキュリティが必要になります。
認可サーバーからユーザーに返される資格情報は、JWT としてエンコードできます。この例のように、認可サーバーがアイデンティティ プロバイダーを介してログインを許可している場合、認可サーバーは、統⼀されたインターフェイスと統⼀されたデータ (JWT) をユーザーに提供していると⾔えるでしょう。
本節で後ほど取り上げる例では、Auth0 を認可サーバーとして使⽤し、Twitter、Facebook、および⼀般的なユーザー デー
タベースからのログインを処理します。

https://auth0.com/blog/2015/09/23/what-is-and-how-does-single-sign-on-work/
http://saml.xml.org/saml-specifications
9 https://openid.net/connect/
7814

2.2.1

アクセス トークンとリフレッシュ トークン

アクセス トークンとリフレッシュ トークンは、各種のフェデレーション アイデンティティ ソリューションを解析するとよく⾒られるトークンです。ここでは、それぞれがどのようなものであり、認証プロセスや認可プロセスとの関連でどのように役⽴つかを簡単に説明します。
通常、どちらのトークンも OAuth2 仕様に基づいて実装されます10。OAuth2 仕様では、所有権からアクセス権を分離する形で、リソースへのアクセス権を提供するために必要な⼀連のステップを定義しています (つまり、異なるアクセス
レベルを持つ複数の当事者が同じリソースにアクセスできます)。これらのステップの⼀部は実装で定義されます。そのため、複数の OAuth2 実装が競合している場合、相互運⽤できなくなる可能性があります。たとえば、トークンの実際のバイナリ形式は指定されません。トークンの⽬的と機能は指定されます。
アクセス トークンは、そのトークンを持つユーザーに保護リソースへのアクセス権を与えるトークンです。通常、この種のトークンは⼀時的なものであり、有効期限が埋め込まれている場合があります。また、追加情報 (要求が許可される
IP アドレスなど) を保持していたり、関連付けられたりしている場合もあります。この追加情報は実装で定義されます。

⼀⽅、リフレッシュ トークンは、新しいアクセス トークンの要求をクライアントに許可するトークンです。たとえば、アクセス トークンの有効期限が切れた後に、クライアントが認可サーバーに対して新しいアクセス トークンを要求する場合があります。この要求を実⾏するには、リフレッシュ トークンが必要です。アクセス トークンとは対照的に、通常、リフレッシュ トークンは⻑期間有効です。

リフレッシュ トークンを送信
ユーザー

認可サーバー

アクセス トークンを
使⽤

アクセス トークンを受信

サーバー

リソース

図 2.7: リフレッシュ トークンとアクセス トークン

アクセス トークンとリフレッシュ トークンを分離する主な⽬的は、アクセス トークンの検証を容易にすることにあります。署名を含むアクセス トークン (署名付き JWT など) は、リソース サーバーが単独で検証できます。検証のために認可サーバーにアクセスする必要はありません。
⼀⽅、リフレッシュ トークンについては認可サーバーにアクセスする必要があります。検証を認可サーバーへのクエリから分離しておくことで、遅延を改善し、アクセス パターンの複雑さを軽減することが可能になります。トークンの漏えいに備えて適切なセキュリティ対策を講じる場合は、アクセス トークンの有効期間をできる限り短くして、追加のチェック (クライアント チェックなど) を組み込みます。
リフレッシュ トークンは有効期間が⻑いため、漏えいから保護する必要があります。漏えいが発⽣した場合は、サーバーのブラックリストに登録する必要があります (アクセス トークンの有効期間が短いと最終的にリフレッシュ トークン
10

https://tools.ietf.org/html/rfc6749#section-1.4

15

が強制的に使⽤されるため、リフレッシュ トークンがブラックリストに登録されて、すべてのアクセス トークンが期限切れになった後でリソースが保護されます)。
注: アクセス トークンとリフレッシュ トークンの概念は OAuth2 で導⼊されました。OAuth 1.0 および 1.0a では、トークンという⾔葉は違う意味で使⽤されています。

2.2.2

JWT と OAuth2

OAuth2 ではトークンの形式は特に定められていませんが、JWT は OAuth2 の要件に適しています。署名付きの JWT は、
リソースへのアクセス レベルを区別するのに必要なすべてのデータをエンコードしたり、有効期限を保持したり、署名により認可サーバーに対する検証クエリを回避したりできるため、アクセス トークンとして優れています。⼀部のフェ
デレーション アイデンティティ プロバイダーは、JWT 形式のアクセス トークンを発⾏しています。
JWT はリフレッシュ トークンにも使⽤されますが、リフレッシュ トークンに JWT を使⽤する必要性はあまりありま
せん。リフレッシュ トークンに必要なのは認可サーバーへのアクセスであり、ほとんどの場合は単純な UUID を使⽤するだけで⼗分であるため、リフレッシュ トークンにペイロードを含める必要はありません (ただし、署名をすることはあります)。

2.2.3

JWT と OpenID Connect

OpenID Connect11 は、OAuth2 の⼀般的なユース ケースを、明確に定義された共通の仕様に準拠させることを⽬的とした標準化の取り組みです。OAuth2 を構成する多くの要素は実装者が⾃由に選択できるため、OpenID Connect では、不⾜している部分に対して適切な定義を提供することを⽬指しています。具体的には、OpenID Connect は OAuth2 認可フローを実⾏するための API とデータ形式を定義します。また、このフローに基づいて構築できる認証レイヤーも提供します。
その⼀部のデータ形式として選ばれているのが、JSON Web Token です。特に、ID トークン12 は、認証済みユーザーに関する情報を伝える特殊な種類のトークンです。

2.2.3.1

OpenID Connect のフローと JWTOpenID Connect では、さまざまな⽅法でデータを返すフローがいくつか定義されています。このデータの中には、JWT形式のものもあります。

• 認可フロー: クライアントがエンドポイント (/authorize) に認可コードを要求します。このコードをトークン
エンドポイント (/token) に対して使⽤すると、ID トークン (JWT 形式)、アクセス トークン、またはリフレッ
シュ トークンを要求することができます。

• 暗黙的フロー: クライアントが認可エンドポイント (/authorize) から直接トークンを要求します。このトークンは要求で指定されます。ID トークンが要求されると、トークンは JWT 形式で返されます。

• ハイブリッド フロー: クライアントが認可コードと特定のトークンの両⽅を認可エンドポイント (/authorize)から要求します。ID トークンが要求されると、トークンは JWT 形式で返されます。ID トークンがこのステップで要求されなかった場合、後でトークン エンドポイント (/token) から直接要求されることがあります。

2.2.4

例

この例では、認可サーバーとして Auth013 を使⽤します。Auth0 では、さまざまなアイデンティティ プロバイダーを動的に設定できます。つまり、ユーザーがログインを試みる際に、認可サーバーで変更を⾏うことで、ユーザーがさまざまなアイデンティティ プロバイダー (Twitter や Facebook など) を使⽤してログインできるようにすることが可能です。
デプロイ後にアプリケーションを特定のプロバイダーに委ねる必要はありません。そのため、この例は⾮常にシンプルです。すべてのサンプル サーバーで、Auth0.js14 ライブラリを使⽤して Auth0 ログイン画⾯を設定しました。ユーザーは
1 つのサーバーにログインすれば、相互接続されていない他のサーバーにもアクセスできるようになります。

https://openid.net/connect/
http://openid.net/specs/openid-connect-core-1_0.html#IDToken
13 https://auth0.com
14 https://github.com/auth0/auth0.js
11
12

16

リダイレクト
リダイレクト
すべてのデータがリダイレクトによりユーザーを経由
Auth0/jp

ユーザー(Facebook、Twitter など)

リダイレクトリダイレクト

アイデンティティ
プロバイダー

App1.com/jp

App2.com/jp

App3.com/jp

図 2.8: Auth0 を認可サーバーとして使⽤

2.2.4.1

Node.js アプリのための Auth0 Lock のセットアップ

Auth0 ライブラリ15 の設定は次のように⾏います。ここでもステートレス セッションの例で使⽤したサンプルと同じものを使⽤します。

const auth0 = new window.auth0.WebAuth({domain: domain,clientID: clientId,
audience: 'app1.com/protected',scope: 'openid profile
purchase', responseType:'id_token token',redirectUri: 'http://app1.com:3000/auth/',responseMode: 'form_post'});// (...)$('#login-button').on('click', function(event) {auth0.authorize({
prompt: 'none'});});
authorize 呼び出しで prompt: 'none' パラメーターを使⽤していることに注意してください。authorize 呼び出
しによって、ユーザーを認可サーバーにリダイレクトします。ユーザーが保護リソースへのアクセスに⾃分の資格情報を使⽤する認可をアプリケーションに既に与えている場合、none パラメーターを指定すると、認可サーバーは単純にユーザーをアプリケーションにリダイレクトします。ユーザーからは、既にアプリケーションにログインしているように⾒えます。
この例では、app1.com と app2.com という 2 つのアプリケーションがあります。ユーザーが両⽅のアプリケーションを認可してから (認可は初回ログイン時に⼀度だけ⾏います)、どちらかのアプリケーションにログインすると、もう⼀⽅のアプリケーションでもログイン画⾯が表⽰されないままログインできるようになります。
15

https://github.com/auth0/auth0.js

17

こ れ を テ ス ト す る に は 、 samples/single-sign-on-federated-identity デ ィ レ ク ト リ に あ る サ ン プ ル の
README ファイルを参照して、両⽅のアプリケーションをセットアップして実⾏してください。両⽅のアプリケーショ
ンを実⾏した後で、app1.com:300016 と app2.com:300117 に移動してログインします。両⽅のアプリケーションからログアウトしたら、どちらかのアプリケーションにログインします。続いてもう⼀⽅のアプリケーションに戻り、ログインします。すると、どちらのアプリケーションでもログイン画⾯が表⽰されなくなります。認可サーバーは以前のログインを記憶しており、どちらかのアプリケーションによって要求された場合に新しいアクセス トークンを発⾏できます。したがって、ユーザーが認可サーバーのセッションを保持している限り、そのユーザーは両⽅のアプリケーションにログイン済みということになります。
CSRF 対策の実装は皆様の課題とし、本書では省略します。

16
17

http://app1.com:3000
http://app2.com:3001

18

第 3 章JSON Web Token の詳細
第 1 章で説明したように、どの JWT も、ヘッダー、ペイロード、署名/暗号化データという 3 つの異なる要素で構成されます。ヘッダーとペイロードは、⼀定の構造を持つ JSON オブジェクトです。署名/暗号化データは、署名または暗号化に使⽤されるアルゴリズムに依存します。また、暗号化されない JWT の場合は省略されます。JWT は、JWS/JWE
コンパクト シリアライゼーションと呼ばれるコンパクトな表現にエンコードできます。
JWS および JWE の仕様では、JSON シリアライゼーションという 3 つ⽬のシリアライゼーション形式も定義されています。これは、1 つの JWT 内で複数の署名または受信者を使⽤できるコンパクトでない表現形式です。詳細は第 4 章と第 5 章で説明します。
コンパクト シリアライゼーションとは、最初の 2 つの JSON 要素 (ヘッダーとペイロード) の UTF-81 バイト配列と、必要に応じて署名または暗号化⽤のデータ (この要素は JSON オブジェクトではありません) を、URL セーフな
Base642 でエンコードする⽅式です。署名/暗号化データも Base64-URL でエンコードされます。この 3 つの要素はドット (.) で区切られます。
JWT では、URL が含まれていても問題のない Base64 エンコード⽅式を使⽤します。このエンコード⽅式では、"+" と "/" をそれぞれ "-" と "_" に置き換えます。パディングも削除されます。この種のエンコード⽅式はbase64url3 と呼ばれています。本書における Base64 エンコード⽅式とは、すべてこの⽅式を指すとお考えください。
結果は次のような表⽰可能な⽂字列になります (⾒やすいように改⾏を挿⼊しています)。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
ご覧のとおり、JWT の 3 つの要素がドットで区切られています (ヘッダー、ペイロード、署名の順)。
この例では、デコードされたヘッダーは次のようになります。

{"alg": "HS256","typ": "JWT"
https://ja.wikipedia.org/wiki/UTF-8
https://ja.wikipedia.org/wiki/Base64
3 https://tools.ietf.org/html/rfc4648#section-5
1
2

19

}
デコードされたペイロードは次のようになります。

{"sub": "1234567890","name": "John Doe","admin": true}
署名の検証に必要なシークレットは secret です。
JWT.io4 では、JWT についてインタラクティブ形式で詳しく学ぶことができます。上記のトークンをコピーし、編集するとどのような結果になるか試してみてください。

3.1

ヘッダー

すべての JWT のヘッダー (JOSE ヘッダー) には、その JWT ⾃体に関するクレームが含まれています。このクレームによって、その JWT が署名付きか暗号化されているかにかかわらず、使⽤するアルゴリズムが設定されます。また⼀般
的には、その JWT の残りの部分を解析する⽅法も設定されています。
JWT の種類によっては、その他の必須クレームがヘッダーに含まれている場合があります。たとえば、暗号化された JWTは、鍵の暗号化とコンテンツの暗号化に使⽤する暗号アルゴリズムに関する情報を保持しています。これらのフィールド
は、暗号化されていない JWT にはありません。
暗号化されない JWT ヘッダーの必須クレームは、alg クレームだけです。

• alg: この JWT の署名や復号化に使⽤される主なアルゴリズム。
暗号化されない JWT の場合、このクレームの値を none に設定する必要があります。
オプションのヘッダー クレームとして、typ クレームと cty クレームがあります。

• typ: その JWT のメディア タイプ5。このパラメーターは、JWT が JOSE ヘッダーを含む他のオブジェクトと混在する可能性がある場合に、参照⽤としてのみ使⽤されます。そのような状況は、実際にはほとんど発⽣しません。そのような状況が⽣じた場合には、このクレームの値を JWT に設定する必要があります。

• cty: コンテンツ タイプ。ほとんどの JWT はペイロードに特定のクレームと任意のデータを保持しています。その場合、cty クレームを設定しないでください。ペイロードも JWT である場合 (ネストされた JWT)、このクレームを追加し、値を JWT に設定する必要があります。これにより、ネストされた JWT の処理が必要であることが通知されます。JWT がネストされることは⾮常に少ないため、cty クレームがヘッダーに含められることはまずありません。
したがって、暗号化されていない JWT の場合、ヘッダーは次のようになります。

{"alg": "none"}
これをエンコードすると、次のようになります。

eyJhbGciOiJub25lIn0
ヘッダーにはさらにユーザー定義のクレームを追加することもできます。ただし、暗号化された JWT で復号化前に特定のユーザー固有のメタデータが必要になる場合を除き、ユーザー定義のクレームが追加されることは⼀般的ではありません。

3.2
4
5

ペイロード

https://jwt.io
http://www.iana.org/assignments/media-types/media-types.xhtml

20

{"sub": "1234567890","name": "John Doe","admin": true}
ペイロードには、通常、すべての有⽤なユーザー データが追加されます。また、仕様で定義されている⼀部のクレームも含まれることがあります。ヘッダーと同様に、ペイロードは JSON オブジェクトです。必須クレームはありませんが、明確な意味を有する特定のクレームが存在します。JWT の仕様では、実装によって認識されないクレームは無視すべきであると規定されています。特定の意味が付与されているクレームは、登録済みクレームと呼ばれています。

3.2.1

登録済みクレーム• iss: issuer (発⾏者) を表します。JWT の発⾏者を⼀意に識別する、⼤⽂字⼩⽂字の区別がある⽂字列または URI です。このクレームの解釈はアプリケーション固有です (発⾏者を管理する中央機関はありません)。

• sub: subject (主題) を表します。この JWT に情報が含まれている当事者を⼀意に識別する、⼤⽂字⼩⽂字の区別がある⽂字列または URI です。つまり、この JWT に含まれるクレームは、この当事者に関する記述ということになります。JWT の仕様では、このクレームは発⾏者のコンテキストにおいて⼀意でなければならず、それが不可能な場合にはグローバルに⼀意でなければならないと規定されています。このクレームの処理はアプリケーション固有です。

• aud: audience (受信者) を表します。⼤⽂字⼩⽂字の区別がある単⼀の⽂字列/URI、またはこの JWT が対象とする受信者を⼀意に識別する値の配列のどちらかです。つまり、このクレームがある場合、この JWT 内のデータを読み取る当事者は、aud クレームに⾃⾝に関する情報があることを認識するか、JWT 内に含まれるデータを無視しなければなりません。iss クレームや sub クレームの場合と同様に、このクレームの処理はアプリケーション固有です。

• exp: expiration (有効期限) を表します。POSIX6 で定義されている形式である "エポックからの経過秒数" によって特定の⽇時を表す数値です。このクレームにより、この JWT が無効とみなされる時点を正確に設定します。実装
によっては、クロック間における⼀定のスキューを許容する場合があります (この JWT が有効期限後も数分間有効であるとみなします)。

• nbf : not before (開始⽇時) を表します。これは exp クレームの反対です。POSIX7 で定義されている形式である "エポックからの経過秒数" によって特定の⽇時を表す数値です。このクレームにより、この JWT が有効とみなされる時点を正確に設定します。この⽇時には、現在以降の⽇時を指定する必要があります。実装によっては、⼀定のスキューが許容されます。

• iat: issued at (発⾏⽇時) を表します。この JWT が発⾏された特定の⽇時 (exp および nbf と同じ形式) を表す数値です。

• jti: JWT ID を表します。この JWT の⼀意の ID を表す⽂字列です。このクレームは、(リプレイを防⽌する⽬的などで) JWT を他の類似するコンテンツと区別するために使⽤できます。⼀意性を保証するのは、実装側の責任です。
ご覧のように、どのクレームも短い名前が使⽤されています。これは JWT をできる限りコンパクトにするという JWTの設計要件に沿っています。
⽂字列または URI: JWT の仕様によると、URI は : を含む任意の⽂字列として解釈されます。有効な値を提供す
るのは、実装側の責任です。

3.2.2

パブリック クレームとプライベート クレーム

「登録済みクレーム」の節で挙げられていないクレームは、プライベート プレームまたはパブリック クレームのどちらかに該当します。

67

http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_15
http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_15

21

• プライベート クレームとは、JWT のユーザー (利⽤者および作成者) によって定義されたクレームです。これら
は特定の場合に使⽤される⼀時的なクレームです。そのため、名前の衝突を防ぐために注意を払う必要があります。

• パブリック クレームとは、IANA JSON Web Token Claims レジストリ8 (ユーザーが衝突を防ぐために独⾃のクレームを登録できるレジストリ) に登録されたクレーム、または衝突しにくい名前 (たとえば先頭に名前空間を付加した名前) を使⽤して命名されたクレームを指します。
実際のところ、ほとんどのクレームは登録済みクレームかパブリック クレームのどちらかです。⼀般的に、ほとんどの
JWT は特定の⽬的と明確な⼀連の潜在的ユーザーを想定して発⾏されます。その場合、衝突しにくい名前を選択しやす
くなります。
JSON 解析ルールの場合と同様に、重複するクレーム (重複する JSON 鍵) は、最後のクレームだけを有効なものとして保持する形で処理されます。また、JWT の仕様では、実装において重複するクレームを含む JWT を無効とみなすことが可能です。JWT を処理する実装についてよく知らない場合は、重複するクレームを避けてください。

8

https://tools.ietf.org/html/rfc7519#section-10.1

22

3.3

セキュリティ保護のない JWT

これまでに学習したことで、セキュリティ保護のない JWT を構築することができます。次に⽰すのは最もシンプルな
JWT です。単純な (通常は静的) ヘッダーは次のようなものです。

{"alg": "none"}
ユーザー定義のペイロードは、次のようになります。

{}"sub": "user123","session": "ch72gsb320000udocl363eofy","name": "Pretty Name","lastpage": "/views/settings"

この JWT には署名も暗号化もないため、2 つの要素だけがエンコードされます (⾒やすいように改⾏を挿⼊しています)。

eyJhbGciOiJub25lIn0.
eyJzdWIiOiJ1c2VyMTIzIiwic2Vzc2lvbiI6ImNoNzJnc2IzMjAwMDB1ZG9jbDM2M
2VvZnkiLCJuYW1lIjoiUHJldHR5IE5hbWUiLCJsYXN0cGFnZSI6Ii92aWV3cy9zZXR0aW5ncyJ9.
このようなセキュリティ保護のない JWT は、クライアント側での使⽤に適しています。たとえば、セッション ID が推測しにくい数値であり、その他のデータがクライアントによってビューの構築のためだけに使⽤される場合、署名を使⽤する必要はありません。このようなデータは、単⼀ページの Web アプリケーションで、ユーザーが最後に訪れたページにリダイレクトされている間に、バックエンドにアクセスすることなくユーザー名を含むビューを構築するために使⽤されることがあります。悪意のあるユーザーがこのデータを改変しても、何も得るものはないでしょう。
エンコード後の末尾のドット (.) に注⽬してください。署名が付いていないため、⽂字列は空になっていますが、ドットは残ります。
しかし実際には、セキュリティ保護のない JWT はほとんど使⽤されません。

3.4

セキュリティ保護のない JWT の作成

JSON 形式のヘッダーとペイロードをコンパクトな表現に変換するには、次の⼿順を実⾏します。
1. ヘッダーを、UTF-8 表現のバイト配列として取得します。JWT の仕様では、エンコードの前に JSON を圧縮した
り、無意味な⽂字 (空⽩など) を取り除いたりする必要はありません。
2. Base64-URL アルゴリズムを使⽤してバイト配列をエンコードし、末尾の等号 (=) を削除します。
3. ペイロードの UTF-8 表現のバイト配列を取得します。JWT の仕様では、エンコードの前に JSON を圧縮したり、無意味な⽂字 (空⽩など) を取り除いたりする必要はありません。
4. Base64-URL アルゴリズムを使⽤してバイト配列をエンコードし、末尾の等号 (=) を削除します。
5. ⽣成された 2 つの⽂字列を連結します。ヘッダー、"."、ペイロードの順につなげます。
エンコードの前に、ヘッダーとペイロードを検証 (必須クレームが存在するか、各クレームが正しく使⽤されているか)する必要があります。

23

Base64
エンコード
Base64
エンコード

図 3.1: セキュリティ保護のない JWT のコンパクトな表現の作成

3.4.1

サンプル コード// URL-safe variant of Base64
function b64(str) {return new Buffer(str).toString('base64').replace(/=/g, '').replace(/¥+/g, '-')
.replace(/¥//g, '_');}
function encode(h, p) {const headerEnc = b64(JSON.stringify(h));
const payloadEnc = b64(JSON.stringify(p));
return `${headerEnc}.${payloadEnc}`;}
完全なサンプルは、付属のサンプル コードの coding.js ファイルにあります。

3.5

セキュリティ保護のない JWT の解析

コンパクト シリアライゼーション形式から JSON 表現に変換するには、次の⼿順を実⾏します。

1. 最初のピリオド "." を検索します。その前の⽂字列を取得します (ピリオドを含まない)。
2. Base64-URL アルゴリズムを使⽤して⽂字列をデコードします。その結果が JWT ヘッダーです。
3. ⼿順 1 のピリオドよりも後の⽂字列を取得します。
4. Base64-URL アルゴリズムを使⽤して⽂字列をデコードします。その結果が JWT ペイロードです。
⽣成された JSON ⽂字列に必要に応じて空⽩を追加することで、体裁を整えることができます。

3.5.1

サンプル コード

function decode(jwt) {const [headerB64, payloadB64] = jwt.split('.');// These supports parsing the URL safe variant of Base64 as well.
const headerStr = new Buffer(headerB64, 'base64').toString();
const payloadStr = new Buffer(payloadB64, 'base64').toString();
return {header: JSON.parse(headerStr),
payload: JSON.parse(payloadStr)
24

}

};

完全なサンプルは、付属のサンプル コードの coding.js ファイルにあります。

25

第 4 章JSON Web Signatures
JSON Web Signature は、おそらく JWT の最も有⽤な機能の 1 つです。シンプルなデータ形式と、明確に定義された⼀連の署名アルゴリズムを組み合わせることで、JWT は急速に発展し、クライアントと仲介者の間でデータを安全に共有するのに最適なフォーマットとなりつつあります。
署名の⽬的は、1 つ以上の当事者が JWT の信頼性を確⽴できるようにすることです。ここで⾔う信頼性とは、JWT に含まれるデータが改ざんされていないことを指します。つまり、当事者が署名チェックを実⾏できれば、JWT によって提供されるコンテンツを信頼できるということになります。ただし、署名を⾏っても、他の当事者が JWT 内のコンテンツを読み取ることを防ぐことはできません。暗号化はそのために存在します。暗号化の⽬的については第 5 章で説明します。
JWT の署名をチェックするプロセスは、トークンの検証と呼ばれます。トークンは、そのヘッダーおよびペイロードで指定された制約がすべて満たされた場合に有効とみなされます。これは JWT の⾮常に重要な特徴です。JWT のヘッダーとペイロード (およびユーザーが求める要件) で指定された範囲まで、実装で JWT をチェックする必要があるからです。
そのため、署名がなくても JWT は有効であるとみなされる場合があります (ヘッダーの alg クレームが none に設定されている場合)。さらに、JWT に有効な署名が含まれていても、他の理由 (exp クレームによって有効期限切れになった場合など) で無効とみなされることもあります。署名付き JWT を対象とした攻撃では、署名を取り除き、ヘッダーを変更してセキュアでない JWT に改変する⼿法がよく取られます。JWT がユーザー独⾃の要件に従って検証されていることを確認するのは、ユーザーの責任です。
署名付き JWT は、JSON Web Signature の仕様、RFC 75151 で定義されています。

4.1

署名付き JWT の構造

JWT の構造については、第 3 章で説明しました。本節では署名の要素に注⽬しながら、その構造を再確認します。
署名付き JWT は、ヘッダー、ペイロード、署名の 3 つの要素で構成されます (⾒やすいように改⾏を挿⼊しています)。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
1https://tools.ietf.org/html/rfc7515

26最初の 2 つの要素 (ヘッダーとペイロード) のデコード プロセスは、セキュリティ保護のない JWT の場合と同じです。
アルゴリズムとサンプル コードは第 3 章の最後にあります。

{"alg": "HS256","typ": "JWT"}{"sub": "1234567890","name": "John Doe","admin": true}
しかし、署名付き JWT には署名というもう 1 つの要素があります。コンパクト シリアライゼーション形式において、この要素は最後のピリオド (.) の後に表記されます。
JWS の仕様に従って使⽤できる署名アルゴリズムにはいくつかの種類があるため、この要素はさまざまな⽅法で解釈されます。JWS の仕様では、仕様に準拠するすべての実装で次のアルゴリズムをサポートすることを要件としています。

• SHA-256 を使⽤した HMAC。JWA 仕様では HS256 と呼ばれています。
また、仕様では推奨アルゴリズムも明⽰しています。

• SHA-256 を使⽤した RSASSA PKCS1 v1.5。JWA 仕様では RS256 と呼ばれています。
• P-256 および SHA-256 を使⽤した ECDSA。JWA 仕様では ES256 と呼ばれています。
JWA とは、JSON Web Algorithm 仕様、RFC 75182 を指します。
これらのアルゴリズムについては、第 7 章で詳しく説明します。本章では、アルゴリズムの使⽤の実際的な側⾯を取り上げます。
JWS では、次に⽰すアルゴリズムもオプションとして使⽤できます。

• HS384、HS512: HS256 アルゴリズムのバリエーションで、SHA-256 をそれぞれ SHA-384 と SHA-512 に変えたもの。

• RS384、RS512: RS256 アルゴリズムのバリエーションで、SHA-256 をそれぞれ SHA-384 と SHA-512 に変えたもの。

• ES384、ES512: ES256 アルゴリズムのバリエーションで、SHA-256 をそれぞれ SHA-384 と SHA-512 に変えたもの。

• PS256、PS384、PS512: RSASSA-PSS + MGF1 のバリエーションで、それぞれ SHA256/384/512 を使⽤したもの。
これらは基本的に、先に挙げた 3 つの必須アルゴリズムと推奨アルゴリズムのバリエーションです。これらの略称の意
味は第 7 章で詳しく説明します。

4.1.1

コンパクト シリアライゼーションに使⽤されるアルゴリズムの概要

これらのアルゴリズムの概要を説明するにあたり、まず JavaScript 2015 環境のいくつかの関数の意味を説明します。

• base64: オクテット配列を受け取り、Base64-URL アルゴリズムを使⽤して新しいオクテット配列を返す関数。
• utf8: 任意のエンコーディングのテキストを受け取り、UTF-8 エンコーディングのオクテット配列を返す関数。
• JSON.stringify: JavaScript オブジェクトを受け取り、それを⽂字列形式 (JSON) にシリアライズする関数。
• sha256: オクテット配列を受け取り、SHA-256 アルゴリズムを使⽤して新しいオクテット配列を返す関数。
• hmac: SHA 関数、オクテット配列、およびシークレットを受け取り、HMAC アルゴリズムを使⽤して新しいオクテット配列を返す関数。

• rsassa: SHA 関数、オクテットの配列、および秘密鍵を受け取り、RSASSA アルゴリズムを使⽤して新しいオクテッ
ト配列を返す関数。
2

https://tools.ietf.org/html/rfc7518

27

HMAC ベースの署名アルゴリズムの場合:

const encodedHeader = base64(utf8(JSON.stringify(header)));const encodedPayload = base64(utf8(JSON.stringify(payload)));const signature = base64(hmac(`${encodedHeader}.${encodedPayload}`,secret, sha256));
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;
公開鍵署名アルゴリズムの場合:

const encodedHeader = base64(utf8(JSON.stringify(header)));const encodedPayload = base64(utf8(JSON.stringify(payload)));const signature = base64(rsassa(`${encodedHeader}.${encodedPayload}`,privateKey, sha256));
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;Base64
エンコード

Base64
エンコード

共有シークレットまたは秘密鍵

署名アルゴリズム署名

Base64エンコード

図 4.1: JWS コンパクト シリアライゼーション
これらのアルゴリズムの詳細については、第 7 章で説明します。

4.1.2

署名アルゴリズムの実際的な側⾯

どの署名アルゴリズムも⽬的は同じです。つまり、JWT に含まれるデータの信頼性を確⽴することです。しかし、その⽅法はそれぞれ異なります。
Keyed-Hash Message Authentication Code (HMAC) は、暗号学的ハッシュ関数3 を使⽤して特定のペイロードとシークレットを組み合わせるアルゴリズムです。⽣成されるコードは、⽣成した側と検証する側の両⽅がシークレットを知っている
場合にのみ、メッセージの検証に使⽤できます。つまり、HMAC は共有シークレットによってメッセージの検証を可能にするアルゴリズムであると⾔えます。
JWT の最も⼀般的な署名アルゴリズムである HS256 で使⽤される暗号学的ハッシュ関数は、SHA-256 です。SHA-256
については、第 7 章で詳しく説明します。暗号学的ハッシュ関数は、任意の⻑さのメッセージを受け取り、固定⻑の出⼒を⽣成します。同じメッセージは常に同じ出⼒を⽣成します。暗号学的な性質を持つハッシュ関数を使⽤した場合、関数の出⼒から元のメッセージを復元することは、数理的に不可能です。そのため、暗号学的ハッシュ関数は⼀⽅向性関数とされており、実際にメッセージを共有することなくメッセージを識別するために使⽤できます。メッセージを少し変え

3

https://ja.wikipedia.org/wiki/%E6%9A%97%E5%8F%B7%E5%AD%A6%E7%9A%84%E3%83%8F%E3%83%83%E3%82%B7%E3%8
3%A5%E9%96%A2%E6%95%B028

ると (たとえば 1 バイト変えるだけでも)、出⼒がまったく違ったものになります。
RSASSA は、署名⽤に改良された RSA アルゴリズム4 (第 7 章で説明) の⼀種です。RSA は公開鍵アルゴリズムです。
公開鍵アルゴリズムは、分割鍵 (1 つの公開鍵と 1 つの秘密鍵) を⽣成します。この種のアルゴリズムでは、秘密鍵を
署名付きのメッセージの作成とその信頼性の検証の両⽅に使⽤できます。⼀⽅、公開鍵はメッセージの信頼性の検証にのみ使⽤できます。そのため、このアルゴリズムを使⽤すると、1 対多のメッセージを安全に配信できます。受信者は、メッセージに関連付けられた公開鍵のコピーを持っていれば、メッセージの信頼性を検証することはできますが、それを使⽤しても新しいメッセージを作成することはできません。したがって、シークレットを共有する署名⽅式 (HMAC など) とは異なる使⽤シナリオが可能になります。HMAC と SHA-256 を使⽤する⽅式の場合、メッセージを検証できる当事者が、新しいメッセージを作成することもできます。たとえば、正規のユーザーが攻撃者になった場合、他の当事者に気づかれることなくメッセージを改変することができます。公開鍵⽅式では、ユーザーが攻撃者になったとしても公開鍵しか⼊⼿できないため、新しい署名付きメッセージを作成することはできません。
利⽤者

エンティティ 1

公開鍵

作成者

エンティティ 2

公開鍵

データ
署名
アルゴリズムエンティティ 3

秘密鍵
公開鍵

エンティティ 4

公開鍵

図 4.2: 1 対多の署名
公開鍵暗号5 ⽅式を使⽤すると、別の利⽤シナリオが可能になります。たとえば、前述の RSA アルゴリズムの⼀種を使⽤し、公開鍵を使ってメッセージを暗号化することができます。暗号化したメッセージは、秘密鍵を使⽤しない限り復号化できません。そのため、多対 1 のセキュアな通信チャネルを構築できます。この⽅式は、第 5 章で説明する暗号化された JWT に使⽤されます。

45

https://ja.wikipedia.org/wiki/RSA%E6%9A%97%E5%8F%B7
https://ja.wikipedia.org/wiki/%E5%85%AC%E9%96%8B%E9%8D%B5%E6%9A%97%E5%8F%B7

29

作成者
エンティティ 1
公開鍵 +
データ

利⽤者

エンティティ 2
公開鍵 +
データ

秘密鍵
暗号化
アルゴリズムエンティティ 3

復号
アルゴリズム

公開鍵 +データ
エンティティ 4
公開鍵 +データ

図 4.3: 多対 1 の暗号化
楕円曲線 DSA (ECDSA)6 は、RSA に代わるアルゴリズムです。このアルゴリズムでも公開鍵と秘密鍵のペアが⽣成されますが、計算⽅法に違いがあります。この違いのおかげで、RSA を使⽤する場合よりも低いハードウェア要件で同様のセキュリティ保証を実現できます。
これらのアルゴリズムについては、第 7 章で詳しく説明します。

4.1.3

JWS のヘッダー クレーム

JWS では、多くのクレームをヘッダーに強制的に含める特殊な使⽤⽅法が認められています。たとえば、公開鍵署名ア
ルゴリズムの場合、URL をクレームとして公開鍵に埋め込むことができます。以下に⽰したのは、JWS トークンで使⽤可能な登録済みヘッダー クレームです。これらのクレームはすべて、セキュリティ保護のない JWT で使⽤可能なクレームに加えて使⽤できるものであり、署名付きの JWT の使⽤⽅法に応じてオプションとして追加できます。

• jku: JSON Web Key (JWK) Set の URL。この JWT に署名するために使⽤する、JSON 形式にエンコードされた公
開鍵のセットを指す URI です。この鍵を取得するには、トランスポート セキュリティ (HTTP の TLS など) を使⽤する必要があります。鍵の形式は JWK Set (第 6 章を参照) です。

• jwk: JSON Web Key。この JWT に JSON Web Key 形式で署名するために使⽤する鍵です (第 6 章を参照)。
• kid: 鍵 ID。この JWT に署名するために使⽤する単⼀の鍵を表すユーザー定義の⽂字列です。このクレームは、受信者に鍵の署名の変更を通知するために使⽤されます (複数の鍵を使⽤する場合)。

• x5u: X.509 URL。PEM 形式にエンコードされた X.509 (証明書形式の標準の 1 つ) 公開証明書のセットを指す
URI。セット内の最初の証明書は、この JWT に署名するために使⽤する証明書である必要があります。後続の証明書はそれぞれ先⾏する証明書に署名します。これにより証明書チェーンが形成されます。X.509 は RFC 52807 で
6
7

https://ja.wikipedia.org/wiki/%E6%A5%95%E5%86%86%E6%9B%B2%E7%B7%9ADSA
https://tools.ietf.org/html/rfc5280

30定義されています。証明書の転送には、トランスポート セキュリティが必要です。

• x5c: X.509 証明書チェーン。この JWS に署名するために使⽤する X.509 証明書の JSON 配列です。各証明書は、
その DER PKIX 表現の Base64 エンコード値である必要があります。配列内の最初の証明書は、この JWT に署名するために使⽤される証明書である必要があります。以降に、証明書チェーン内のその他の証明書が続きます。

• x5t: X.509 証明書の SHA-1 フィンガープリント。この JWT に署名するために使⽤する、X.509 DER にエンコードされた証明書の SHA-1 フィンガープリントです。

• x5t#S256: x5t と同じですが、SHA-1 の代わりに SHA-256 を使⽤します。
• typ: 暗号化なしの JWT の typ の値と同じですが、"JOSE" と "JOSE+JSON" という値を追加で使⽤できます。それぞれ、コンパクト シリアライゼーション形式と JSON シリアライゼーション形式を⽰すために使⽤されます。
このクレームは、1 つのコンテナー内に、JOSE ヘッダーを持つ類似したオブジェクトがこの JWT と混在する場
合にのみ使⽤されます。

• crit: critical (必須) を表します。この JWT のパーサーで処理する必要がある実装定義の拡張として同じヘッダー
内に⽰されているクレームの名前が含まれた⽂字列配列です。クレーム名が含まれていない⽂字列配列は指定できません (空の配列は無効値です)。

4.1.4

JWS JSON シリアライゼーション

JWS の仕様では、コンパクトではない別の種類のシリアライゼーション形式が定義されています。この形式では、同⼀の署名付き JWT で複数の署名を使⽤できます。これは JWS JSON シリアライゼーションと呼ばれています。
JWS JSON シリアライゼーション形式では、署名付き JWT は JSON 形式 (ブラウザーで JSON.stringify を呼び出すと得られる形式) の表⽰可能なテキストとして表現されます。最上位の JSON オブジェクトに、以下の鍵と値のペアを含める必要があります。

• payload: 実際の JWT ペイロード オブジェクトを Base64 エンコードした⽂字列。
• signatures: 署名を保持する JSON オブジェクトの配列。このオブジェクトは以下のように定義されます。
signatures 配列内の各 JSON オブジェクトには、次の鍵と値のペアが含まれている必要があります。

• protected: JWS ヘッダーを Base64 でエンコードした⽂字列。このヘッダーに含まれるクレームは、署名により保護されます。このヘッダーは、保護されていないヘッダーがない場合にのみ必要です。保護されていないヘッダーがある場合は、このヘッダーはあってもなくても構いません。

• header: ヘッダー クレームを含んでいる JSON オブジェクト。このヘッダーは署名によって保護されません。保
護されないヘッダーが存在しない場合、この要素は必須です。保護されたヘッダーが存在する場合、この要素はオプションです。

• signature: JWS の署名を Base64 でエンコードした⽂字列。
コンパクト シリアライゼーション形式 (保護されたヘッダーのみが存在する場合) とは異なり、JSON シリアライゼー
ションでは、protected (保護されたヘッダー) と unprotected (保護されないヘッダー) の 2 種類のヘッダーを使⽤できます。保護されたヘッダーは署名によって検証されます。保護されないヘッダーは署名によって検証されません。どのク
レームを含めるかは、実装側またはユーザーの⾃由に委ねられています。必ずどちらかのヘッダーが存在していなければなりません。両⽅とも存在していても構いません。
保護されたヘッダーと保護されないヘッダーの両⽅が存在する場合、実際の JOSE ヘッダーは、両⽅のヘッダーの要素を結合して作成されます。クレームの重複は認められません。
次の例は、JWS RFC8 から引⽤したものです。

{"payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogIm
h0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ","signatures": [{8https://tools.ietf.org/html/rfc7515#appendix-A.6

31

"protected": "eyJhbGciOiJSUzI1NiJ9","header": { "kid": "2010-12-29" },"signature":"cC4hiUPoj9Eetdgtv3hF80EGrhuB dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AA
uHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAyn
RFdiuB--f_nZLgrnbyTyWzO5vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6
IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlU
PQGe77Rw"},{"protected": "eyJhbGciOiJFUzI1NiJ9","header": { "kid": "e9bc097a-ce51-4036-9562-d2ade882db0d" },"signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDx
w5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"]}}
この例では、同⼀のペイロードに対して 2 つの署名 (RS256 署名と ES256 署名) をエンコードしています。

4.1.4.1

フラット化 JWS JSON シリアライゼーション

JWS JSON シリアライゼーションには、JWT の署名が 1 つしかない場合に使⽤できる簡略化された形式があります。この形式は、フラット化 JWS JSON シリアライゼーションと呼ばれています。フラット化シリアライゼーションでは、
signatures 配列を削除し、payload 要素と同じレベルに 1 つの署名要素を置きます。
たとえば、上記の例から署名の 1 つを削除すると、次のようなフラット化 JSON シリアライゼーション オブジェクトになります。

{}"payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQog
Imh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ","protected": "eyJhbGciOiJFUzI1NiJ9","header": { "kid": "e9bc097a-ce51-4036-9562-d2ade882db0d" },"signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFC
gfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"

4.2

トークンの署名と検証

トークンの署名と検証に使⽤されるアルゴリズムについては、第 7 章で詳しく説明します。署名付き JWT の使⽤⽅法
は実際にはシンプルであり、これまでに説明した概念を適⽤するだけで効果的に使⽤できます。また、JWT の実装に便利な優れたライブラリも利⽤できます。ここでは、最も⼀般的な JavaScript ライブラリを使⽤する必須アルゴリズムと推奨アルゴリズムについて説明します。その他の⼀般的な⾔語やライブラリの例については、付属のコードを参照してく
ださい。
以下の例では、いずれも⼀般的に使われている jsonwebtoken JavaScript ライブラリを利⽤しています。

import jwt from 'jsonwebtoken'; //var jwt = require('jsonwebtoken');const payload = {sub: "1234567890",
name: "John Doe",admin: true};32

4.2.1

HS256: HMAC + SHA-256

HMAC 署名には共有シークレットが必要です。任意の⽂字列を使⽤できます。

const secret = 'my-secret';const signed = jwt.sign(payload, secret, {algorithm: 'HS256',
expiresIn: '5s' // if ommited, the token will not expire});
トークンの検証も同様に簡単です。

const decoded = jwt.verify(signed, secret, {// Never forget to make this explicit to prevent// signature stripping attacks
algorithms: ['HS256'],});
jsonwebtoken ライブラリは、署名と有効期限に基づいてトークンの有効性をチェックします。この例では、トークンが作成されてから 5 秒後にチェックした場合、トークンは無効と⾒なされ、例外がスローされます。

4.2.2

RS256: RSASSA + SHA256

RS256 で署名されたトークンの署名と検証も同様に簡単です。共有シークレットではなく、秘密鍵と公開鍵のペアを使う点だけ異なります。RSA 鍵を作成する⽅法は多数あります。OpenSSL は、鍵の作成と管理に使⽤される最も⼀般的なライブラリの 1 つです。

# Generate a private key
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048# Derive the public key from the private key
openssl rsa -pubout -in private_key.pem -out public_key.pem
どちらの PEM ファイルも単純なテキスト ファイルです。これらのファイルの内容を JavaScript ソース ファイルにコピーして貼り付けて、jsonwebtoken ライブラリに渡すことができます。

// You can get this from private_key.pem above.
const privateRsaKey = `<YOUR-PRIVATE-RSA-KEY>`;
const signed = jwt.sign(payload, privateRsaKey, {algorithm: 'RS256',
expiresIn: '5s'});// You can get this from public_key.pem above.
const publicRsaKey = `<YOUR-PUBLIC-RSA-KEY>`;
const decoded = jwt.verify(signed, publicRsaKey, {// Never forget to make this explicit to prevent// signature stripping attacks.
algorithms: ['RS256'],
});

4.2.3

ES256: P-256 および SHA-256 を使⽤した ECDSAECDSA アルゴリズムでも公開鍵を利⽤します。ただし、アルゴリズムの計算⽅法が異なるため、鍵を⽣成する⼿順も異33

なります。このアルゴリズムの名前 "P-256" は、使⽤されるアルゴリズムのバージョンを⽰しています (詳細は第 7 章
を参照)。OpenSSL を使⽤して鍵を⽣成することもできます。

# Generate a private key (prime256v1 is the name of the parameters used
# to generate the key, this is the same as P-256 in the JWA spec).
openssl ecparam -name prime256v1 -genkey -noout -out ecdsa_private_key.pem# Derive the public key from the private key
openssl ec -in ecdsa_private_key.pem -pubout -out ecdsa_public_key.pem
これらのファイルを開くと、データが⾮常に少ないことがわかります。これは、RSA と⽐べた場合の ECDSA のメリットの 1 つです (詳細は第 7 章を参照)。⽣成されるファイルも PEM 形式であるため、ソースに貼り付けるだけで使⽤できます。

// You can get this from private_key.pem above.
const privateEcdsaKey = `<YOUR-PRIVATE-ECDSA-KEY>`;
const signed = jwt.sign(payload, privateEcdsaKey, {algorithm: 'ES256',
expiresIn: '5s'});// You can get this from public_key.pem above.
const publicEcdsaKey = `<YOUR-PUBLIC-ECDSA-KEY>`;
const decoded = jwt.verify(signed, publicEcdsaKey, {// Never forget to make this explicit to prevent// signature stripping attacks.
algorithms: ['ES256'],});JWT でのこれらのアルゴリズムの実際の利⽤例については、第 2 章を参照してください。

34

第 5 章JSON Web Encryption (JWE)
JSON Web Signature (JWS) はデータを検証する⼿段を提供しますが、JSON Web Encryption (JWE) はデータを第三者に対して不透明にする⼿段を提供します。この場合、不透明とは読み取れないことを意味します。暗号化されたトークンを第三者が調べることはできません。この機能は、さまざまな⽤途に活⽤できます。
暗号化によって検証と同じ保証が得られるように⾒えますが、暗号化にはデータを読み取り不可にする機能もあるため、必ずしも同じではありません。その理由を理解するにはまず、JWE では、JWS と同様に、2 つの⽅式 (共有シークレッ
ト⽅式と公開鍵/秘密鍵⽅式) を利⽤できることに注⽬しましょう。
共有シークレット⽅式では、すべての当事者に共有シークレットを提供します。共有シークレットを持つ各当事者は、情報の暗号化と復号の両⽅を実⾏できます。これは JWS で共有シークレットを使⽤する場合と似ています。シークレット
を持っている当事者は、署名付きトークンの検証と⽣成の両⽅を⾏うことができます。
⼀⽅、公開鍵/秘密鍵⽅式の仕組みは異なります。JWS では、秘密鍵を持っている当事者はトークンに署名して検証できますが、公開鍵を持っている当事者はトークンを検証することしかできません。⼀⽅ JWE では、秘密鍵を持っている当事者のみがトークンを復号できます。つまり、公開鍵の所有者はデータを暗号化できますが、そのデータを復号 (および暗号化) できるのは、秘密鍵の所有者だけです。具体的には、JWE の場合、公開鍵を持っている当事者が新しいデータを取り込んで交換することができます。⼀⽅ JWS の場合には、公開鍵を持っている当事者はデータを検証できるだけで、新しいデータを取り込むことができません。要するに、JWE は JWS と同じ保証を提供しないため、トークンの交換において JWS に代わる役割を果たすことはできません。公開鍵/秘密鍵⽅式を使⽤している場合、JWS と JWE は互いに補完する関係にあります。
このことを分かりやすくするために、作成者と利⽤者の観点から考えてみましょう。作成者がデータの署名または暗号化
を⾏い、利⽤者がデータを検証または復号します。JWT 署名の場合、秘密鍵は JWT 署名のために使⽤され、公開鍵はそれを検証するために使⽤されます。作成者は秘密鍵を保有し、利⽤者は公開鍵を保有します。データは秘密鍵の保有者
から公開鍵の保有者にのみ送られます。⼀⽅、JWT 暗号化の場合は、公開鍵を使⽤してデータを暗号化し、秘密鍵を使⽤してデータを復号します。この場合、データは公開鍵の保有者から秘密鍵の保有者にのみ送られます。公開鍵の保有者
は作成者であり、秘密鍵の保有者は利⽤者です。

JWS

JWE

作成者

秘密鍵

公開鍵

利⽤者

公開鍵

秘密鍵

35

作成者

利⽤者

署名

検証

秘密鍵

公開鍵

署名
アルゴリズム検証
アルゴリズム署名

有効または無効

データ

作成者

利⽤者

暗号化

復号

公開鍵

秘密鍵

暗号化
アルゴリズム

復号
アルゴリズム

暗号化データデータ

図 5.1: 公開鍵暗号⽅式を使⽤した署名と暗号化の⽐較

ここで、次のような疑問が浮かぶかもしれません。
JWE の場合、利⽤者にデータを送るすべての当事者に、秘密鍵を配布することはできないのでしょうか。利⽤者
がデータを復号できれば、その利⽤者は同時にデータが有効であることも確認できます (復号できないデータは変更できないため)。
これは技術的には可能ですが、意味がありません。秘密鍵を共有することは、シークレットを共有することと同じです。
したがって、秘密鍵を共有すると、公開鍵暗号⽅式は実質的に秘密鍵共有⽅式となり、公開鍵のメリットも失われます(公開鍵は秘密鍵から導出することができます)。
このような理由により、暗号化された JWT がネストされる、つまり暗号化された JWT が署名付き JWT のコンテナーとなる場合があります。そうすることで、両⽅の⽅式のメリットが得られるためです。
36なお、以上の説明が当てはまるのは、利⽤者と作成者が別々である場合です。作成者がデータの利⽤者でもある場
合、共有シークレットで暗号化された JWT は、暗号化および署名された JWT と同じ保証を提供します。
JWE で暗号化された JWT は、ネストされた署名付き JWT を含んでいるかどうかにかかわらず、認証タグを保持しています。このタグを使⽤すると、JWE 形式の JWT を検証できます。ただし、前述の問題により、この署名はJWS の署名と同じ⽤途では利⽤できません。このタグの⽬的は、パディング オラクル攻撃1 や暗号⽂の操作を防ぐことです。

5.1

暗号化された JWT の構造

暗号化された JWT には、署名付きのセキュリティ保護のない JWT とは異なるコンパクトな表現があります (⾒やすいように改⾏を挿⼊しています)。

eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.
UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_
i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKxYxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8Otv
zlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTPcFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.
AxY8DCtDaGlsbGljb3RoZQ.
KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.
9hH0vgRfYgPnAHOd8stkvw
上記の例ではわかりにくいかもしれませんが、JWE コンパクト シリアライゼーションには 5 つの要素があります。JWSの場合と同様に、各要素がドットで区切られ、それぞれに含まれるデータが Base64 でエンコードされています。
コンパクト表現の 5 つの要素を順に説明します。
1. 保護されたヘッダー: JWS ヘッダーと同様のヘッダー。
2. 暗号化鍵: 暗号⽂やその他の暗号化されたデータを暗号化するために使⽤された対称鍵。この鍵は、ユーザーが指定した実際の暗号化⽤の鍵から導出されたものであるため、その鍵によって暗号化されます。
3. 初期化ベクトル: ⼀部の暗号化アルゴリズムでは、追加の (通常、ランダムの) データが必要です。
4. 暗号化されたデータ (暗号⽂): 暗号化されている実際のデータ。
5. 認証タグ: アルゴリズムによって⽣成される追加データで、暗号⽂の内容が改ざんされていないかを検証するために使⽤できます。
署名が 1 つの JWS コンパクト シリアライゼーション形式の場合と同様に、JWE のコンパクト形式では、1 つの暗号化鍵を使⽤できます。
⾮対称暗号化 (公開鍵/秘密鍵暗号化) を使⽤する場合、⼀般的に、対称鍵を使⽤して実際の暗号化処理を⾏います。
通常、⾮対称暗号化アルゴリズムは計算が⾮常に複雑であるため、⻑いデータ シーケンス (暗号⽂) の暗号化には最適ではありません。⾼速な対称暗号化と⾮対称暗号化の両⽅のメリットを活かす⽅法として、対称暗号化アルゴ
リズム⽤のランダム鍵を⽣成してから、その鍵を⾮対称暗号化アルゴリズムで暗号化する⽅法があります。これが上記の 2 番⽬の要素である、暗号化鍵です。
暗号化アルゴリズムの中には、渡されたすべてのデータを処理できるものもあります。そのようなアルゴリズムでは、暗号⽂が変更されていても (復号されていなくても)、暗号⽂を処理できます。認証タグを使⽤すると、これを防ぐことができます。そのため、認証タグは実質的に署名として機能します。とはいえ、前述のネストされた JWTの必要性がなくなるわけではありません。

5.1.1

鍵暗号化アルゴリズム

暗号化鍵が暗号化されるということは、同じ JWT に 2 つの暗号化アルゴリズムがあることを意味します。鍵の暗号化に使⽤できる暗号化アルゴリズムは次のとおりです。

• RSA の変種: RSAES PKCS #1 v1.5 (RSAES-PKCS1-v1_5)、RSAES OAEP、OAEP
1

https://en.wikipedia.org/wiki/Padding_oracle_attack37• + MGF1 + SHA-256。
• AES の変種: 128 ビットから 256 ビットまでの AES 鍵ラップ、128 ビットから 256 ビットまでの AES Galois
Counter Mode (GCM)。

• 楕円曲線暗号の変種: Concat KDF を使⽤した Elliptic Curve Diffie-Hellman Ephemeral Static (ECDH-ES) 鍵共有、および上記のいずれかの⾮ GCM の AES 変種を使⽤して鍵を事前にラップする変種。

• PKCS#5 の変種: PBES2 (パスワードベースの暗号化) + HMAC ( SHA-256 〜 512) + 128 〜 256 ビットの⾮ GCMの AES 変種。

• 直接: 暗号化鍵の暗号化なし (CEK を直接使⽤)。
これらのアルゴリズムは、いずれも JWA の仕様では必須とされていません。以下は、仕様で (実装するよう) 推奨されているアルゴリズムです。

• RSAES-PKCS1-v1_5 (今後推奨から除外される予定)• 既定の設定の RSAES-OAEP (今後必須化される予定)• AES-128 鍵ラップ
• AES-256 鍵ラップ
• Concat KDF を使⽤した ECDH-ES (今後必須化される予定)• ECDH-ES + AES-128 鍵ラップ
• ECDH-ES + AES-256 鍵ラップ
これらのアルゴリズムの中には、追加のヘッダー パラメーターが必要なものもあります。

5.1.1.1

鍵管理モードJWE の仕様では、さまざまな鍵管理モードが定義されています。鍵管理モードとは、ペイロードを暗号化するために使⽤する鍵を決定する⽅法です。JWE の仕様では、特に次の鍵管理モードについて説明されています。

• 鍵ラッピング: 対称暗号化アルゴリズムを使⽤し、対象受信者⽤のコンテンツ暗号化鍵 (CEK) を暗号化します。
CEK

共有鍵

対称暗号化
アルゴリズム

ラップされた鍵

図 5.2: 鍵ラッピング• 鍵暗号化: ⾮対称暗号化アルゴリズムを使⽤し、対象受信者⽤の CEK を暗号化します。

38

CEK

⾮対称暗号化
アルゴリズム

公開証明書

暗号化鍵

図 5.3: 鍵暗号化

• 直接鍵共有: 鍵共有アルゴリズムを使⽤して CEK を選択します。
鍵共有
アルゴリズムCEK

図 5.4: 直接鍵共有• 鍵共有と鍵ラッピング: 対称暗号化アルゴリズムを使⽤して暗号化された対称 CEK を、鍵共有アルゴリズムを使⽤して選択します。

鍵共有
アルゴリズム

CEK

共有鍵

対称暗号化
アルゴリズム

ラップされた鍵

図 5.5: 鍵共有と鍵ラッピング• 直接暗号化: ユーザー定義の対称共有鍵を CEK として使⽤します (鍵の派⽣または⽣成なし)。

選択した鍵

そのまま
使⽤

CEK

図 5.6: 直接暗号化

これは⽤語の問題になりますが、各管理モードの違いを理解し、それぞれに適切な名前を付けることが重要です。

5.1.1.2

コンテンツ暗号化鍵 (CEK) と JWE 暗号化鍵39

また、CEK と JWE 暗号化鍵の違いを理解することも重要です。CEK は、ペイロードの暗号化に使⽤される実際の鍵です。暗号化アルゴリズムは、CEK と平⽂を受け取り、暗号⽂を⽣成します。⼀⽅、JWE 暗号化鍵は、暗号化された形のCEK か、空のオクテット列のどちらかになります (選択したアルゴリズムによって異なります)。JWE 暗号化鍵が空の場合、アルゴリズムでは外部から提供された鍵を使⽤して、データを直接復号するか (直接暗号化)、実際の CEK を計算します (直接鍵共有)。

5.1.2

コンテンツ暗号化アルゴリズム

ペイロードを実際に暗号化するために使⽤されるコンテンツ暗号化アルゴリズムは次のとおりです。

• AES CBC + HMAC SHA: 暗号ブロック連鎖 (CBC) モードの AES 128 〜 256 ビットと、検証⽤の HMAC + SHA256 〜 512。

• AES GCM: GCM を使⽤した AES 128 〜 256。
これらのうち必須とされているのは、AES-128 CBC + HMAC SHA-256 と AES-256 CBC + HMAC SHA-512 の 2 つだけです。GCM を使⽤した AES-128 および AES-256 の変種は、推奨アルゴリズムです。
これらのアルゴリズムについては、第 7 章で詳しく説明します。

5.1.3

ヘッダー

JWS やセキュリティ保護のない JWT のヘッダーと同様に、ヘッダーには JWT をライブラリによって正しく処理するために必要な情報がすべて含まれています。JWE の仕様では、JWS で定義されている登録済みクレームの意味がその⽤途に合わせて変更されているほか、いくつかのクレームが追加されています。以下に新しいクレームと変更されているクレームを⽰します。

• alg: JWS と同じですが、CEK の暗号化と復号に使⽤されるアルゴリズムを定義するものである点が異なります。
つまり、このアルゴリズムは、後でコンテンツの暗号化に使⽤される鍵を暗号化するために使⽤されます。

• enc: CEK を使⽤してコンテンツを暗号化するために使⽤されるアルゴリズムの名前。
• zip: 暗号化の前に、暗号化するデータに適⽤される圧縮アルゴリズム。このパラメーターはオプションです。指定しなかった場合、圧縮が実⾏されません。通常は DEF (⼀般的な deflate アルゴリズム2) がこの値に使⽤されます。

• jku: JWS と同じですが、CEK を暗号化するために使⽤する公開鍵を指す点が異なります。
• jkw: JWS と同じですが、CEK を暗号化するために使⽤する公開鍵を指す点が異なります。
• kid: JWS と同じですが、CEK を暗号化するために使⽤する公開鍵を指す点が異なります。
• x5u: JWS と同じですが、CEK を暗号化するために使⽤する公開鍵を指す点が異なります。
• x5c: JWS と同じですが、CEK を暗号化するために使⽤する公開鍵を指す点が異なります。
• x5t: JWS と同じですが、CEK を暗号化するために使⽤する公開鍵を指す点が異なります。
• x5t#S256: JWS と同じですが、CEK を暗号化するために使⽤する公開鍵を指す点が異なります。
• typ: JWS と同じです。
• cty: JWS と同じですが、暗号化されたコンテンツの種類である点が異なります。
• crit: JWS と同じですが、このヘッダーのパラメーターを参照する点が異なります。
使⽤している暗号化アルゴリズムによっては、追加のパラメーターが必要になる場合があります。これについては、各ア
ルゴリズムについて取り上げる節で説明します。

2https://tools.ietf.org/html/rfc1951

40

5.1.4コンパクト シリアライゼーションに使⽤されるアルゴリズムの概要

本章の冒頭で、JWE コンパクト シリアライゼーションについて簡単に触れました。これは基本的に、表⽰可能なテキス
ト形式にエンコードされ、ドット (.) で区切られた 5 つの要素で構成されます。JWE コンパクト シリアライゼーショ
ン形式の JWT を構築する基本的な計算⼿順を以下に⽰します。
1. 選択したアルゴリズムで必要な場合 (alg クレーム)、必要な⻑さの乱数を⽣成します。この乱数を⽣成する際には、ランダム性に関する⼀定の暗号要件に準拠する必要があります。RFC 40863 を参照するか、暗号論的に妥当性が確認されている乱数⽣成器を使⽤してください。
2. 鍵管理モード4 に基づき、コンテンツ暗号化鍵を決定します。
• 直接鍵共有: 鍵共有アルゴリズムと乱数を使⽤して、CEK を計算します。
• 鍵共有と鍵ラッピング: 鍵共有アルゴリズムと乱数を使⽤して、CEK をラップするために使⽤する鍵を計算します。
• 直接暗号化: CEK が対称鍵です。
3. 鍵管理モードに基づき、JWE 暗号化鍵を決定します。
• 直接鍵共有および直接暗号化: JWE 暗号化鍵は空です。
• 鍵ラッピング、鍵暗号化、鍵共有と鍵ラッピング: 受信者⽤の CEK を暗号化します。これにより、JWE 暗号化鍵が⽣成されます。
4. 選択したアルゴリズムを使⽤して、必要とする⻑さの初期化ベクトル (IV) を計算します。不要な場合は、この⼿順をスキップします。
5. 必要に応じて、平⽂のコンテンツを圧縮します (zip ヘッダー クレームを使⽤)。
6. CEK、IV、追加認証データ (AAD) を使⽤してデータを暗号化します。これにより、暗号化されたコンテンツ (JWE
暗号⽂) と認証タグが⽣成されます。AAD はコンパクトでないシリアライゼーションの場合にのみ使⽤します。
7. 次のようにコンパクト表現を構築します。

base64(header) + '.' +base64(encryptedKey) + '.' +// Steps 2 and 3

base64(initializationVector) + '.' +base64(ciphertext) + '.' +base64(authenticationTag)// Step 4// Step 6// Step 65.1.5

JWE JSON シリアライゼーション

JWE では、コンパクトなシリアライゼーション形式に加えて、コンパクトでない JSON 表現も定義されています。この表現を使⽤すると、コンパクトでなくなる代わりに柔軟性が得られます。特に、同時に複数の公開鍵を使⽤し、複数の受信者⽤のコンテンツを暗号化できるという利点があります。これは、JWS JSON シリアライゼーションで使⽤できる複数の署名に似ています。
JWE JSON シリアライゼーションは、以下のメンバーを持つ JSON オブジェクトを、表⽰可能なテキスト形式にエンコードしたものです。

• protected: この JWE 形式の JWT によって保護される (検証はされるが暗号化はされない) ヘッダー クレーム
を Base64 でエンコードした JSON オブジェクト。これはオプションです。この要素か unprotected ヘッダーのどちらかが必要です。

• unprotected: JSON オブジェクトとして保護されない (検証されない) ヘッダー クレーム。この要素は Base64 でエンコードされません。これはオプションです。この要素か protected ヘッダーのどちらかが必要です。

• iv: 初期化ベクトルの Base64 ⽂字列。これはオプションです (アルゴリズムが必要とする場合にのみ使⽤します)。
• aad: 追加認証データ。暗号化アルゴリズムによって保護される (検証される) 追加データの Base64 ⽂字列。暗号化の⼿順で AAD を指定しない場合、このメンバーは不要です。

• ciphertext: 暗号化されたデータを Base64 でエンコードした⽂字列。
• tag: 暗号化アルゴリズムによって⽣成された認証タグの Base64 ⽂字列。
3
4

https://tools.ietf.org/html/rfc4086
5.1.1.141• recipients: JSON オブジェクトの JSON 配列。各オブジェクトには、各受信者による復号に必要な情報が含まれています。
recipients 配列内のオブジェクトには、次のメンバーがあります。

• header: 保護されないヘッダー クレームの JSON オブジェクト。これはオプションです。
• encrypted_key: Base64 でエンコードされた JWE 暗号化鍵。JWE 暗号化鍵が使⽤される場合にのみ使⽤します。
受信者⽤の JWE 形式の JWT を復号するのに使⽤される実際のヘッダーは、存在する各ヘッダーを結合して作成されます。クレームの重複は認められません。
暗号化鍵の形式については、第 6 章 (JSON Web Key) で説明します。
次の例は、RFC 7516 (JWE) から引⽤したものです。

{"protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0","unprotected": { "jku":"https://server.example.com/keys.jwks" },"recipients":[{"header": { "alg":"RSA1_5","kid":"2011-04-29" },"encrypted_key":"UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ deLKx
GHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3
YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPh
cCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPg
wCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A"},{"header": { "alg":"A128KW","kid":"7" },"encrypted_key": "6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ"}],"iv": "AxY8DCtDaGlsbGljb3RoZQ","ciphertext": "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY","tag": "Mz-VPPyU4RlcuYv1IwIvzw"}
この JWE JSON シリアライゼーション形式の JWT では、1 つのペイロードを 2 つの受信者に対して使⽤しています。
暗号化アルゴリズムは AES-128 CBC + SHA-256 です。これは protected ヘッダーから取得できます。

{"enc": "A128CBC-HS256"}
各受信者⽤のすべてのクレームを結合することで、各受信者⽤の最終的なヘッダーが作成されます。
最初の受信者:{"alg":"RSA1_5","kid":"2011-04-29","enc":"A128CBC-HS256","jku":"https://server.example.com/keys.jwks"}
2 番⽬の受信者:{"alg":"A128KW",
42

"kid":"7","enc":"A128CBC-HS256","jku":"https://server.example.com/keys.jwks"}
5.1.5.1

フラット化 JWE JSON シリアライゼーション

JWS と同様に、JWE ではフラットな JSON シリアライゼーション形式も定義されています。このシリアライゼーショ
ン形式は、受信者が 1 つの場合にのみ使⽤できます。この形式では、recipients 配列が header と encrypted_keyのペアに置き換えられます (つまり、recipients 配列内の 1 つのオブジェクトの鍵が使⽤されます)。
以下は、前節の例に最初の受信者だけを含めることでフラット化した表現です。

{"protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0","unprotected": { "jku":"https://server.example.com/keys.jwks" },"header": { "alg":"RSA1_5","kid":"2011-04-29" },"encrypted_key":"UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ deLKx
GHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3
YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPh
cCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPg
wCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A","iv": "AxY8DCtDaGlsbGljb3RoZQ","ciphertext": "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY","tag": "Mz-VPPyU4RlcuYv1IwIvzw"}

5.2

トークンの暗号化と復号

次の例は、広く使われている node-jose5 ライブラリを使⽤して暗号化を実⾏する⽅法を⽰しています。このライブラリ
は、jsonwebtoken (JWS の例で使⽤) よりもはるかに広い範囲を対象としているため、少し複雑です。

5.2.1

導⼊: node-jose を使⽤した鍵の管理

次の例では、さまざまな形式の暗号化鍵を使⽤する必要があります。これは、keystore (鍵ストア) を介して nodejose により管理します。keystore は鍵を管理するオブジェクトです。ここでは、鍵をいくつか⽣成して鍵ストアに追加し、後の例で使⽤できるようにしておきます。JWS の例で⾒たように、jsonwebtoken ライブラリでは、このような抽象化は必要ありませんでした。keystore による抽象化は、node-jose の実装に含まれる要素の 1 つです。他の⾔語やライブラリでも、同様の抽象化が⾒られます。
空の鍵ストアを作成し、さまざまな種類の鍵をいくつか追加するには、次の⼿順を実⾏します。

// Create an empty keystore
const keystore = jose.JWK.createKeyStore();// Generate a few keys. You may also import keys generated from external// sources.
const promises = [keystore.generate('oct', 128, { kid: 'example-1' }),keystore.generate('RSA', 2048, { kid: 'example-2' }),keystore.generate('EC', 'P-256', { kid: 'example-3' }),5https://github.com/cisco/node-jose#basics

43

];
node-jose では、鍵の⽣成は簡単です。JWE および JWS で使⽤できるすべての鍵タイプがサポートされています。
この例では、シンプルな AES 128 ビット鍵、RSA 2048 ビット鍵、曲線 P-256 を使⽤した楕円曲線鍵の 3 種類の鍵を作成します。これらの鍵は、暗号化と署名の両⽅に使⽤できます。公開鍵/秘密鍵ペアをサポートする鍵の場合、⽣成される鍵は秘密鍵です。公開鍵を取得するには、次の呼び出しを実⾏します。

var publicKey = key.toJSON();
公開鍵は JWK 形式で保存されます。既存の鍵を読み込むこともできます。

// where input is either a:// * jose.JWK.Key instance// * JSON Object representation of a JWK
jose.JWK.asKey(input).
then(function(result) {// {result} is a jose.JWK.Key// {result.keystore} is a unique jose.JWK.KeyStore});// where input is either a:// * String serialization of a JSON JWK/(base64-encoded)//
PEM/(binary-encoded) DER// * Buffer of a JSON JWK/(base64-encoded) PEM/(binary-encoded) DER// form is either a:// * "json" for a JSON stringified JWK// * "pkcs8" for a DER encoded (unencrypted!) PKCS8 private key// * "spki" for a DER encoded SPKI public key// * "pkix" for a DER encoded PKIX X.509 certificate// * "x509" for a DER encoded PKIX X.509 certificate// * "pem" for a PEM encoded of PKCS8 / SPKI / PKIX
jose.JWK.asKey(input, form).
then(function(result) {// {result} is a jose.JWK.Key// {result.keystore} is a unique jose.JWK.KeyStore
});

5.2.2

AES-128 Key Wrap (鍵) + AES-128 GCM (コンテンツ)

AES-128 Key Wrap と AES-128 GCM は、対称鍵アルゴリズムです。つまり、暗号化と復号に同じ鍵が必要なアルゴリズ
ムです。先ほど⽣成した "example-1" の鍵は、対称鍵です。AES-128 Key Wrap では、この鍵はランダムに⽣成された鍵をラップするために使⽤された後、AES-128 GCM アルゴリズムを使⽤してコンテンツを暗号化するために使⽤されます。
この鍵は直接使⽤することもできます (直接暗号化モード)。

function encrypt(key, options, plaintext) {return jose.JWE.createEncrypt(options, key)
.update(plaintext).final();}function a128gcm(compact) {const key = keystore.get('example-1');
const options = {format: compact ? 'compact' : 'general',contentAlg: 'A128GCM'};
return encrypt(key, options, JSON.stringify(payload));}44

node-jose ライブラリは、主に Promise6 を使⽤して動作します。a128gcm が返すオブジェクトは Promise です。
createEncrypt 関数は、渡された任意のコンテンツを暗号化できます。つまり、コンテンツが JWT である必要はありません (ほとんどの場合)。そのため、JSON.stringify は、この関数にデータを渡す前に呼び出す必要があります。

5.2.3

RSAES-OAEP (鍵) + AES-128 CBC + SHA-256 (コンテンツ)

createEncrypt 関数の呼び出し⽅法の違いは、この関数に渡されるオプションだけです。したがって、公開鍵/秘密鍵
ペアも簡単に使⽤できます。createEncrypt に対称鍵を渡す代わりに、公開鍵または秘密鍵を渡すだけです (暗号化に必要なのは公開鍵だけですが、公開鍵は秘密鍵から導出することができます)。読みやすくするために、ここでは秘密鍵を使⽤しますが、実際には、このステップでは公開鍵を使⽤することがほとんどです。

function encrypt(key, options, plaintext) {return jose.JWE.createEncrypt(options, key)
.update(plaintext).final();}function rsa(compact) {const key = keystore.get('example-2');
const options = {format: compact ? 'compact' : 'general',contentAlg: 'A128CBC-HS256'};
return encrypt(key, options, JSON.stringify(payload));}contentAlg では、実際の暗号化アルゴリズムを選択します。前述のように、使⽤できるのは 2 種類 (各鍵⻑の AES
CBC + HMAC SHA と AES GCM) だけです。

5.2.4

ECDH-ES P-256 (鍵) + AES-128 GCM (コンテンツ)

楕円曲線の API は RSA の API と同じです。

function encrypt(key, options, plaintext) {return jose.JWE.createEncrypt(options, key)
.update(plaintext).final();}function ecdhes(compact) {const key = keystore.get('example-3');
const options = {format: compact ? 'compact' : 'general',contentAlg: 'A128GCM'};
return encrypt(key, options, JSON.stringify(payload));
}

5.2.5

6

ネストされた JWT: P-256 および SHA-256 を使⽤した ECDSA (署名) + RSAESOAEP (暗号化鍵) + AES-128 CBC + SHA-256 (暗号化コンテンツ)

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise45ネストされた JWT では、署名付き JWT を暗号化関数に渡すことになるため、多少の⼿間がかかります。署名と暗号化の⼿順を⼿動で実⾏する必要があるためです。ここでは、署名した後で暗号化する、という順序で⾏うことに注意してください。技術的には逆の順序で⾏うことも可能ですが、最初に JWT に署名しておけば、作成したトークンが署名削除攻撃を受ける危険を排除できます。

function nested(compact) {const signingKey = keystore.get('example-3');
const encryptionKey = keystore.get('example-2');
const signingPromise = jose.JWS.createSign(signingKey).update(JSON.stringify(payload)).final();const promise = new Promise((resolve, reject) => {signingPromise.then(result => {const options = {format: compact ? 'compact' : 'general',contentAlg: 'A128CBC-HS256'};
resolve(encrypt(encryptionKey, options, JSON.stringify(result)));}, error => {reject(error);});});
return promise;}
上記の例からわかるように、node-jose は署名にも使⽤できます。署名にはその他のライブラリ (jsonwebtoken など) を使⽤することも可能です。しかし、node-jose の必要性を考えると、依存関係を増やし、別々の API を使⽤しても意味がありません。
最初に署名⼿順を実⾏できるのは、JWE では認証付き暗号を必須としているからです。つまり、暗号化アルゴリ
ズムによって署名⼿順も⾏う必要があります。JWE では認証が必要であるにもかかわらず、JWS と JWE を効果的に組み合わせることができる理由については、第 5 章の冒頭で説明しました。他の⽅式 (⼀般的な暗号化 + 署名) では、通常、暗号化の後で署名を⾏います。これは、暗号化攻撃の原因となる暗号⽂の操作を防ぐためです。
このことは、JWE で認証タグが必須とされている理由でもあります。

5.2.6

復号

復号も暗号化と同じくシンプルです。暗号化と同様に、異なるデータ形式のペイロードを明⽰的に変換する必要があります。

// Decryption test
a128gcm(true).then(result => {jose.JWE.createDecrypt(keystore.get('example-1')).decrypt(result).then(decrypted => {decrypted.payload = JSON.parse(decrypted.payload);
console.log(`Decrypted result: ${JSON.stringify(decrypted)}`);}, error => {console.log(error);});}, error => {console.log(error);46});
RSA アルゴリズムと楕円曲線アルゴリズムの復号は類似しており、対称鍵ではなく秘密鍵を使⽤します。適切な kid ク
レームを持つ鍵ストアがある場合は、その鍵ストアを createDecrypt 関数に渡すだけで、適切な鍵を検索させることができます。したがって、上記の例では、まったく同じコードを使⽤して復号することができます。

jose.JWE.createDecrypt(keystore) //just pass the keystore here
.decrypt(result).then(decrypted => {decrypted.payload = JSON.parse(decrypted.payload);
console.log(`Decrypted result: ${JSON.stringify(decrypted)}`);}, error => {console.log(error);});47

第 6 章JSON Web Key (JWK)
では、JWT、JWS、JWE の全体像を把握するために、JSON Web Key (JWK) の仕様を⾒ていきましょう。この仕様では、署名や暗号化に使⽤される鍵のさまざまな表現が取り上げられています。どの鍵にも確⽴された表現がありますが、JWK仕様では、JSON Web Algorithms (JWA) 仕様でサポートされているすべての鍵に対して統⼀された表現を提供することを⽬的としています。鍵の表現形式を統⼀することで、共有を容易化するとともに、他の鍵交換形式の複雑さの影響を排除できます。
JWS および JWE は、X.509 証明書という別の種類の鍵形式をサポートしています。これは⾮常に⼀般的で、JWK よりも多くの情報を保持することができます。X.509 証明書は JWK に埋め込むことができ、X.509 証明書から JWK を構築することができます。
鍵はさまざまなヘッダー クレームで指定されます。JWK のリテラルは jwk クレームに記述されます。⼀⽅、jku ク
レームは、URL で指定される場所に保管されている鍵のセットを参照することができます。これらのクレームは両⽅とも JWK 形式です。
JWK の例を以下に⽰します。

{}"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM","d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE","use":"enc","kid":"1"

48

6.1

JSON Web Key の構造

JSON Web Key は、鍵に必要なパラメーターを記述する⼀連の値を保持する JSON オブジェクトです。パラメーターは鍵の種類によって異なります。共通のパラメーターを以下に⽰します。

• kty: "key type" の略です。鍵の種類を識別するためのクレームです。サポートされている種類は、楕円曲線鍵を表す EC、RSA 鍵⽤の RSA、対称鍵を表す oct です。このクレームは必須です。

• use: 鍵の使⽤⽬的を指定します。指定できる使⽤⽬的は、sig (署名⽤) とenc (暗号化⽤) の 2 つです。このク
レームはオプションです。同じ鍵を暗号化と署名に使⽤する場合、このメンバーを含めることはできません。

• key_ops: 鍵の詳細な使⽤⽅法を指定する⽂字列値の配列。指定できる値は、sign、verify、encrypt、decrypt、
wrapKey、unwrapKey、deriveKey、deriveBits です。中には、組み合わせて使⽤できない値もあります。たとえば、sign と verify は同じ鍵に使⽤できますが、sign と encrypt は同時に使⽤できません。このクレームはオプションです。さらに、use クレームと同時に使⽤することはできません。両⽅のクレームを使⽤する場合、内容に⼀貫性があることが必要です。

• alg: "algorithm" の略です。この鍵とともに使⽤するアルゴリズムを表します。JWE または JWS の処理で使⽤できる任意のアルゴリズムを指定できます。このクレームはオプションです。

• kid: "key id" の略です。この鍵の⼀意の識別⼦です。これを使⽤して、JWE ヘッダーまたは JWS ヘッダー内のkid クレームと鍵を照合したり、アプリケーションのロジックに基づいて鍵のセットから鍵を選択したりできます。このクレームはオプションです。同じ鍵セット内の 2 つの鍵が同じ kid を持つことができるのは、それらのkty クレームが異なり、使⽤⽬的が同じである場合だけです。

• x5u: PEM エンコード形式の X.509 公開鍵証明書または証明書チェーンを指す URL。他にもオプションのクレームが含まれている場合、そのクレームの内容に証明書の内容との⼀貫性があることが必要です。このクレームはオプションです。

• x5c: Base64-URL でエンコードされた X.509 DER 証明書または証明書チェーン。証明書チェーンはこの証明書の配列として表されます。最初の証明書は、この JWK が参照する証明書である必要があります。この JWK 内に存在する他のすべてのクレームに、最初の証明書の値との⼀貫性があることが必要です。このクレームはオプションです。

• x5t: X.509 証明書の DER エンコードの SHA-1 サムプリント/フィンガープリントを Base64-URL エンコードしたもの。このサムプリントが参照する証明書に、この JWK のクレームの内容との⼀貫性があることが必要です。
このクレームはオプションです。

• x5t#S256: x5t クレームと同じですが、X.509 証明書の SHA-256 サムプリントが含まれる点が異なります。
x、y、d (本章の冒頭の例を参照) など、その他のパラメーターは、鍵アルゴリズムに固有のものです。⼀⽅、RSA 鍵は、n、e、dp などのパラメーターを持ちます。これらのパラメーターの意味については、各鍵アルゴリズムについて詳しく説明している第 7 章で解説します。

49

6.1.1

JSON Web Key Set

JWK の仕様では、鍵のグループが認められています。これを "JWK Set" と呼びます。このセットに複数の鍵が含まれています。グループとしての鍵の意味と、鍵の順序の意味は、ユーザーが定義します。
JSON Web Key Set は、keys メンバーを持つ JSON オブジェクトです。keys メンバーは JWK の JSON 配列です。
JWK Set の例:{"keys": [{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM","use":"enc","kid":"1"},{"kty":"RSA","n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx
4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs
tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2
QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI
SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb
w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw","e":"AQAB","alg":"RS256","kid":"2011-04-29"}]}
この例には、2 つの公開鍵があります。最初のものは楕円曲線鍵であり、use クレームによって暗号化処理での使⽤に制限されています。2 番⽬のものは RSA 鍵であり、alg クレームによって特定のアルゴリズム (RS256) に関連付けられています。つまり、この 2 番⽬の鍵は署名に使⽤されます。

50

第 7 章JSON Web Algorithms
お気づきかもしれませんが、本書にはこの章への参照が数多くあります。その理由は、JWT を⽀える有⽤な機能の多くが、JWT で使⽤されるアルゴリズムを基盤としているからです。JWT の構造も重要ですが、これまで説明してきたさま
ざまな興味深い使⽤例は、アルゴリズムなしには不可能です。この章では、現在 JWT で使⽤されている最も重要なアルゴリズムについて説明します。JWT は深く理解していなくても⼗分効果的に使⽤できます。そのためこの章は、パズルの最後のピースを知りたいという好奇⼼のある⽅を対象としています。

7.1

⼀般的なアルゴリズム

以下のアルゴリズムは、JWT、JWS、JWE の仕様内で、さまざまな形で利⽤されます。Base64-URL などのアルゴリズム
は、コンパクト シリアライゼーション形式とコンパクトでないシリアライゼーション形式で使⽤されます。SHA-256 などのその他のアルゴリズムは、署名、暗号化、鍵のフィンガープリントに使⽤されます。

7.1.1

Base64

Base64 はバイナリからテキストへのエンコードを⾏うアルゴリズムです。このアルゴリズムの主な⽬的は、オクテット
列を表⽰可能な⼀連の⽂字列に変換することです。その代償として、データ サイズは増加します。数学⽤語で⾔えば、
Base64 は基数 (Radix) 256 の数列を基数 64 の数列に変換します。Base64 という名前は、Radix の代わりに Base という⽤語を使⽤できることに由来します。
注: Base64 は JWT 仕様では使⽤されていません。JWT で使⽤されるのは、後ほど本章で説明する Base64-URL という変種です。
Base64 で⼀連の任意の数字をテキストに変換する⽅法を理解するには、まずテキスト エンコード⽅式について知る必要があります。テキスト エンコード⽅式では、数字を⽂字にマップします。このマッピングは任意であり、Base64 エン
コーディングの場合は実装で定義できますが、Base64 の事実上の標準は RFC 46481 です。

1https://tools.ietf.org/rfc/rfc4648.txt

51

0A
1B
2C
3D
4E
5F
6G
7H
8I
9J
10 K
11 L
12 M
13 N
14 O

17 R
18 S
19 T
20 U
21 V
22 W
23 X
24 Y
25 Z
26 a
27 b
28 c
29 d
30 e
31 f

34 i
35 j
36 k
37 l
38 m
39 n
40 o
41 p
42 q
43 r
44 s
45 t
46 u
47 v
48 w

15 P
16 Q

32 g
33 h

49 x
50 y

51 z
52 0
53 1
54 2
55 3
56 4
57 5
58 6
59 7
60 8
61 9
62 +
63 /
(パディング⽂字) =Base64 のエンコード後の各⽂字は、元のデータの 6 ビット分に対応しています。エンコードは、4 ⽂字のグループごとに⾏います。つまり、元のデータの 24 ビット分を 1 つにまとめて、4 つの Base64 ⽂字としてエンコードします。元
のデータは 8 ビット値の数列であるため、この 24 ビット分のデータは、左から順に 3 つの 8 ビット値を連結して作ります。
Base64 エンコーディング:

8 ビット値 x 3 -> 24 ビットの連結データ -> 6 ビット⽂字 x 4
8 ビット + 8 ビット + 8 ビット = 24 ビット

6 ビット+6 ビット+6 ビット+6 ビット = 24 ビット

図 7.1: Base64 エンコーディング
⼊⼒データのオクテットの数が 3 で割り切れない場合、エンコード対象のデータの最後の部分が、24 ビットに満たない
データになります。この場合、連結した⼊⼒データにゼロを付加し、整数個の 6 ビット値のグループを作ります。このとき、次の 3 つの可能性が考えられます。
1. 24 ビットをすべて⼊⼒として使⽤できる場合は、特別な処理は⾏いません。
2. 16 ビットを⼊⼒として使⽤できる場合は、3 つの 6 ビット値を作り、最後の 6 ビット値の右にゼロを付加します。エンコード後の⽂字列には、8 ビットの⼊⼒がないことを明⽰するために、パディング⽂字の = を 1 つ追加
します。
3. 8 ビットを⼊⼒として使⽤できる場合は、2 つの 6 ビット値を作り、最後の 6 ビット値の右にゼロを付加します。
エンコード後の⽂字列には、16 ビットの⼊⼒がないことを明⽰するために、パディング⽂字の = を 2 つ追加します。
実装によっては、パディング⽂字 (=) は省略可能とされています。この⼿順を逆の順序で実⾏すると、パディング⽂字52

の有無にかかわらず、元のデータが⽣成されます。

7.1.1.1

Base64-URL

標準の Base64 変換表に含まれている⼀部の⽂字は、URL セーフではありません。Base64 は、テキスト フィールドに
任意のデータを渡すときに便利なエンコード⽅式です。URL に含めると問題が発⽣するのは 2 ⽂字だけであるため、Base64 の URL セーフな変種は容易に実装できます。+と / を - と _ にそれぞれ置き換えるだけです。

7.1.1.2

サンプル コード

次のサンプルでは、ごく限られた機能を持つ Base64-URL エンコーダーを実装しています。このサンプルは速度よりも簡潔さを考慮して書かれています。

const table = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J','K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd','e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n','o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x','y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '-', '_'];/*** @param input a Buffer, Uint8Array or Int8Array, Array* @returns a String with the encoded values*/
export function encode(input) {let result = "";for(let i = 0; i < input.length; i += 3) {const remaining = input.length - i;let concat = input[i] << 16;
result += (table[concat >>> (24 - 6)]);
if(remaining > 1) {concat |= input[i + 1] << 8;
result += table[(concat >>> (24 - 12)) & 0x3F];
if(remaining > 2) {concat |= input[i + 2];
result += table[(concat >>> (24 - 18)) & 0x3F] +table[concat & 0x3F];} else {result += table[(concat >>> (24 - 18)) & 0x3F] + "=";}
} else {result += table[(concat >>> (24 - 12)) & 0x3F] + "==";}}
return result;}53

7.1.2SHAJWT の仕様で使⽤されている Secure Hash Algorithm (SHA) は、FIPS-1802 で定義されています。SHA-13 系のアルゴリズ
ムと混同しないよう注意してください。SHA-1 系は 2010 年 に廃⽌されました。SHA-1 系と区別するために、これをSHA-2 と呼ぶことがあります。
RFC 4634 で定義されているアルゴリズムは、SHA-224、SHA-256、SHA-384、SHA-512 です。JWT にとって重要なのは、
SHA-256 と SHA-512 です。ここでは SHA-256 に焦点を当て、他の変種との違いを説明します。
多くのハッシュ アルゴリズムと同様に、SHA では、⼊⼒を固定サイズのチャンクで処理し、⼀連の算術演算を適⽤してから、直前の結果を使⽤して演算を繰り返し実⾏して結果を累積するという仕組みを採⽤しています。固定サイズの⼊⼒
チャンクがすべて処理されると、ダイジェストが計算されることになっています。
SHA 系のアルゴリズムは、衝突を回避し、⼊⼒がわずかに違うだけでもまったく異なる出⼒を⽣成するように設計されました。この理由から、SHA はセキュアであるとみなされています。さまざまな⼊⼒の衝突を発⾒したり、⽣成されたダイジェストから元の⼊⼒を計算したりすることは、計算上不可能です。
このアルゴリズムには、事前に定義された⼀連の関数が必要です。

function rotr(x, n) {return (x >>> n) | (x << (32 - n));}function ch(x, y, z) {return (x & y) ^ ((~x) & z);}
function maj(x, y, z) {return (x & y) ^ (x & z) ^ (y & z);}
function bsig0(x) {return rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22);}function bsig1(x) {return rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25);}function ssig0(x) {return rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3);}function ssig1(x) {return rotr(x, 17) ^ rotr(x, 19) ^ (x >>> 10);}
これらの関数は仕様で定義されてます。rotr 関数はビット ローテーション (右⽅向) を実⾏します。
また、このアルゴリズムでは、メッセージが所定の⻑さ (64 の倍数) である必要があります。したがって、パディング処理が必要です。パディング アルゴリズムでは次の処理が実⾏されます。
1. 元のメッセージの末尾にバイナリの 1 を 1 つ追加します。以下に例を⽰します。

Original message:
01011111 01010101 10101010 00111100
Extra 1 at the end:2
3

http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
https://ja.wikipedia.org/wiki/SHA-1

54

01011111 01010101 10101010 00111100 1
2. メッセージの⻑さが次の⽅程式の解と同じになるように、N 個のゼロを追加します。

L = Message length in bits
0 = (65 + N + L) mod 512
3. 元のメッセージのビット数を 64 ビット整数に変換して追加します。

Original message:
01011111 01010101 10101010 00111100
Extra 1 at the end:
01011111 01010101 10101010 00111100 1
N zeroes:
01011111 01010101 10101010 00111100 10000000 ...0...
Padded message:
01011111 01010101 10101010 00111100 10000000 ...0... 00000000 00100000

800 ビット (メッセージ)

パディング

メッセージの⻑さ (8 バイト)

159 個のゼロ

図 7.2: SHA のパディング

JavaScript での簡単な実装例を以下に⽰します。

function padMessage(message) {if(!(message instanceof Uint8Array) && !(message instanceof Int8Array)) {throw new Error("unsupported message container");}
const bitLength = message.length * 8;const fullLength = bitLength + 65; //Extra 1 + message size.
let paddedLength = (fullLength + (512 - fullLength % 512)) / 32;
let padded = new Uint32Array(paddedLength);for(let i = 0; i < message.length; ++i) {padded[Math.floor(i / 4)] |= (message[i] << (24 - (i % 4) * 8));}padded[Math.floor(message.length / 4)] |= (0x80 << (24 - (message.length % 4) * 8));// TODO: support messages with bitLength longer than 2^32
padded[padded.length - 1] = bitLength;
return padded;}⽣成されるパディングされたメッセージは、512 ビットのブロックで処理されます。以下の実装例は、仕様で説明されている処理⼿順に基づいて作成されています。どの処理も 32 ビット整数に対して実⾏されます。

export default function sha256(message, returnBytes) {// Initial hash values
const h_ = Uint32Array.of(
0x6a09e667,
55

0xbb67ae85,
0x3c6ef372,
0xa54ff53a,
0x510e527f,
0x9b05688c,
0x1f83d9ab,
0x5be0cd19);
const padded = padMessage(message);const w = new Uint32Array(64);for(let i = 0; i < padded.length; i += 16) {for(let t = 0; t < 16; ++t)
{ w[t] = padded[i + t];}for(let t = 16; t < 64; ++t) {w[t] = ssig1(w[t - 2]) + w[t - 7] + ssig0(w[t - 15]) + w[t - 16];}
let a = h_[0] >>> 0;
let b = h_[1] >>> 0;
let c = h_[2] >>> 0;
let d = h_[3] >>> 0;
let e = h_[4] >>> 0;
let f = h_[5] >>> 0;
let g = h_[6] >>> 0;
let h = h_[7] >>> 0;
for(let t = 0; t < 64; ++t) {let t1 = h + bsig1(e) + ch(e, f, g) + k[t] + w[t];let t2 = bsig0(a) + maj(a, b, c);
h = g;
g = f;
f = e;
e = d + t1;
d = c;
c = b;
b = a;
a = t1 + t2;}
h_[0] = (a + h_[0]) >>> 0;
h_[1] = (b + h_[1]) >>> 0;
h_[2] = (c + h_[2]) >>> 0;
h_[3] = (d + h_[3]) >>> 0;
h_[4] = (e + h_[4]) >>> 0;
h_[5] = (f + h_[5]) >>> 0;
h_[6] = (g + h_[6]) >>> 0;
h_[7] = (h + h_[7]) >>> 0;}//(...)}
変数 k は、仕様で定義されている⼀連の定数を保持します。
最終的な結果は、変数 h_[0..7] に格納されます。あとはこれを読みやすい形式で表⽰するだけです。

if(returnBytes) {56

const result = new Uint8Array(h_.length * 4);
h_.forEach((value, index) => {const i = index * 4;result[I] = (value >>> 24) & 0xFF;
result[i + 1] = (value >>> 16) & 0xFF;
result[i + 2] = (value >>> 8) & 0xFF;
result[i + 3] = (value >>> 0) & 0xFF;});
return result;} else {function toHex(n) {let str = (n >>> 0).toString(16);let result = "";for(let i = str.length; i < 8; ++i)
{ result += "0";}return result + str;}
let result = "";h_.forEach(n => {result += toHex(n);});
return result;}
これでも機能しますが、上記の実装例は最適なものではありません (232 より⻑いメッセージはサポートされません)。
SHA-2 系の他の変種 (SHA-512 など) を使⽤する場合は、各反復で処理するブロックのサイズを変更し、定数とそのサイズを変えるだけです。特に、SHA-512 を利⽤するには、64 ビット演算が必要です。上記の実装例で SHA-512 を利⽤するには、64 ビット演算を⾏うための別のライブラリが必要です (JavaScript は 32 ビットのビット演算と 64 ビットの浮動⼩数点演算しかサポートしません)。

7.2

署名アルゴリズム

7.2.1

HMAC

ハッシュベース メッセージ認証コード (HMAC)4 は、暗号学的ハッシュ関数 (前述の SHA 系などの関数) と鍵を使⽤して、特定のメッセージの認証コードを作成します。つまり HMAC ベースの認証⽅式では、⼊⼒としてハッシュ関数、メッセージ、秘密鍵を受け取り、出⼒として認証コードを⽣成します。暗号学的ハッシュ関数の特性により、秘密鍵がな
いとメッセージを変更できません。そのため、HMAC は認証とデータの完全性確認の両⽅の⽬的に使⽤できます。

4https://tools.ietf.org/html/rfc2104

57ハッシュ アルゴリズム(SHA、MD5 など)

メッセージ

HMAC
アルゴリズムHMAC

共有シークレット
図 7.3: HMAC

ハッシュ関数が脆弱な場合、悪意のあるユーザーによって認証コードの有効性が危険にさらされるおそれがあります。したがって、HMAC を使⽤する場合には、強⼒なハッシュ関数を選択する必要があります。SHA-2 系の関数は、現在の標準としては⼗分に強⼒ですが、将来的には危うくなる可能性もあります。MD5 は、過去に広く使⽤されていた別の暗号
ハッシュ関数であり、HMAC に使⽤できます。ただし、衝突攻撃やプレフィックス攻撃に対して脆弱である可能性があります。このような攻撃のおそれがあるからといって、MD5 は HMAC での使⽤に適していないわけではありません。
しかし、より強⼒なアルゴリズムを容易に利⽤できるため、その利⽤を検討することをお勧めします。
HMAC アルゴリズムは、1 ⾏に収まるほどシンプルなものです。

Let H be the cryptographic hash function
B be the block length of the hash function(how many bits are processed per iteration)
K be the secret key
K' be the actual key used by the HMAC algorithm
L be the length of the output of the hash function
ipad be the byte 0x36 repeated B times
opad be the byte 0x5C repeated B times
message be the input message|| be the concatenation function
HMAC(message) = H(K' XOR opad || H(K' XOR ipad || message))
K' は、秘密鍵 K から次のように計算されます。
K が B より短い場合、K が B の⻑さになるまでゼロを追加します。その結果が K' です。K が B より⻑い場合、H をK に適⽤します、結果は K' になります。K がちょうど B バイトである場合、そのまま使⽤します (K は K' と同値)。
JavaScript での実装例を以下に⽰します。

export default function hmac(hashFn, blockSizeBits, secret, message, returnBytes) {if(!(message instanceof Uint8Array)) {throw new Error('message must be of Uint8Array');}
const blockSizeBytes = blockSizeBits / 8;58

const ipad = new Uint8Array(blockSizeBytes);
const opad = new Uint8Array(blockSizeBytes);
ipad.fill(0x36);
opad.fill(0x5c);
const secretBytes = stringToUtf8(secret);
let paddedSecret;if(secretBytes.length <= blockSizeBytes) {const diff = blockSizeBytes - secretBytes.length;
paddedSecret = new Uint8Array(blockSizeBytes);paddedSecret.set(secretBytes);} else {paddedSecret = hashFn(secretBytes);}
const ipadSecret = ipad.map((value, index) => {return value ^ paddedSecret[index];});
const opadSecret = opad.map((value, index) => {return value ^ paddedSecret[index];});// HMAC(message) = H(K' XOR opad || H(K' XOR ipad || message))const result = hashFn(
append(opadSecret,uint32ArrayToUint8Array(hashFn(append(ipadSecret,message), true))),returnBytes);
return result;}
HMAC を使⽤してメッセージを検証する場合は、HMAC を計算し、その結果をメッセージに付属する HMAC と⽐較するだけです。これを⾏うには、すべての当事者 (メッセージの作成者とメッセージの検証のみを⾏う者) が秘密鍵を知っている必要があります。

7.2.1.1

HMAC + SHA256 (HS256)

JWS 仕様の HS256 署名アルゴリズムを実装するのに必要なものは、Base64-URL、SHA-256、HMAC の知識だけです。
その知識をもとに、これまでに作成したすべてのサンプル コードを組み合わせることで、完全な署名付き JWT を作成できます。

export default function jwtEncode(header, payload, secret) {if(typeof header !== 'object' || typeof payload !== 'object') {throw new Error('header and payload must be objects');}if(typeof secret !== 'string') {throw new Error("secret must be a string");}
header.alg = 'HS256';
const encHeader = b64(JSON.stringify(header));
const encPayload = b64(JSON.stringify(payload));const jwtUnprotected = `${encHeader}.${encPayload}`;const signature = b64(uint32ArrayToUint8Array(59

hmac(sha256, 512, secret, stringToUtf8(jwtUnprotected), true)));return `${jwtUnprotected}.${signature}`;}
この関数では (オブジェクトであるかどうかの検証を除き)、ヘッダーやペイロードは検証されないことに注意してください。この関数は、次のようにして呼び出すことができます。

console.log(jwtEncode({}, {sub: "test@test.com"}, 'secret'));⽣成された JWT を JWT.io のデバッガー5 に貼り付けて、デコードと検証の結果を確認します。
この関数は、署名アルゴリズムの例を⽰すために第 4 章で使⽤した関数と⾮常によく似ています。以下が第 4 章のものです。

const encodedHeader = base64(utf8(JSON.stringify(header)));const encodedPayload = base64(utf8(JSON.stringify(payload)));const signature = base64(hmac(`${encodedHeader}.${encodedPayload}`, secret, sha256));
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;検証も簡単です。

export function jwtVerifyAndDecode(jwt, secret) {if(!isString(jwt) || !isString(secret)) {throw new TypeError('jwt and secret must be strings');}
const split = jwt.split('.');
if(split.length !== 3) {throw new Error('Invalid JWT format');}const header = JSON.parse(unb64(split[0]));if(header.alg !== 'HS256') {throw new Error(`Wrong algorithm: ${header.alg}`);}const jwtUnprotected = `${split[0]}.${split[1]}`;const signature =b64(hmac(sha256, 512, secret, stringToUtf8(jwtUnprotected), true));return {header: header,
payload: JSON.parse(unb64(split[1])),valid: signature == split[2]};}

署名を JWT から分離し、新しい署名を計算します。新しい署名が JWT に含まれている署名と⼀致する場合、その署名は有効です。
上記の関数は、次のように使⽤できます。

const secret = 'secret';const encoded = jwtEncode({}, {sub: "test@test.com"}, secret);const decoded = jwtVerifyAndDecode(encoded, secret);
このコードは、本書に付属するサンプル6 の hs256.js ファイルにあります。

56https://jwt.io
https://github.com/auth0/jwt-handbook-samples

60

7.2.2RSA

RSA は今⽇最も広く使⽤されている暗号⽅式の 1 つです。1977 年に Ron Rivest ⽒、Adi Shamir ⽒、Leonard Adleman
⽒によって開発され、3 ⽒のイニシャルを取って RSA と命名されました。RSA の重要な特徴は、その⾮対称性にあります。つまり、暗号化に使⽤される鍵が、復号に使⽤される鍵と異なるということです。この⽅式は公開鍵暗号⽅式 (PKI)と呼ばれます。この⽅式では、公開鍵が暗号化鍵、秘密鍵が復号鍵となります。
署名に関して⾔えば、秘密鍵は情報に署名するために使⽤され、公開鍵は特定の秘密鍵によってその情報が署名されたことを検証 (情報の内容はわかりません) するために使⽤されます。
RSA アルゴリズムには、署名⽤と暗号化⽤のバリエーションがあります。ここではまず⼀般的なアルゴリズムを取り上げてから、JWT で使⽤されるさまざまなバリエーションを確認します。
多くの暗号アルゴリズム、特に RSA は、特定の数学的演算を⾏うことが⽐較的困難であることを利⽤したものです。
RSA では、主な数学的⼿法として素因数分解7 を使⽤します。素因数分解とは、複数の数値が乗算された数値から元の数値を求める数学問題です。⾔い換えると、整数の因数とは、乗算するとその整数になる⼀連の整数のペアです。

整数 = 因数_1 x 因数_2
この問題は⼀⾒、簡単に⾒えます。数が⼩さい場合は確かに簡単です。たとえば、数が 35 の場合は、次の解になります。

35 = 7 x 5
7 または 5 の乗算表を知っていれば、掛け合わせたときに 35 になる数を簡単に⾒つけることができます。ある整数の因数を⾒つける単純なアルゴリズムとして、次のようなものを考えることができます。
1. n を因数分解する数とします。
2. x を 2 以上 n / 2 以下の数とします。
3. n を x で除算し、余りが 0 かどうかを確認します。余りが 0 である場合、因数は x と商の値になります。
4. x が上限の n / 2 に達するまで、x を 1 ずつ増やしながら、⼿順 3 を繰り返し実⾏します。こうすることで、
n のすべての因数が⾒つかります。
これは基本的に総当たりで因数を⾒つける⽅法です。ご想像のとおり、このアルゴリズムは⾮常に⾮効率的です。
このアルゴリズムを改良した試⾏除算と呼ばれる⼿法では、x の条件をより厳密に設定します。具体的には、x の上限を sqrt(n) と定義し、x を 1 ずつ増加させるのではなく、x にもっと⼤きな素数を代⼊します。これらの条件を使⽤することで、なぜこのアルゴリズムを正しく効率化できるかを⽰すことは難しくありませんが、それは本書の範囲外です。
さらに効率的なアルゴリズムも存在しますが、そのような効率的なアルゴリズムや今⽇のコンピューターを使⽤しても、計算上、因数分解することが難しい数があります。そのような数を n として選択すると、因数分解問題は複雑になります。たとえば、n が 2 つの素数8 を掛け合わせた結果である場合 (その 2 つの素数だけが唯⼀可能な因数であるとします)、その因数を⾒つけることははるかに困難になります。
n が 2 つの⾮素数を掛け合わせた結果であるならば、その因数ははるかに簡単に⾒つかります。なぜなら、⾮素数は (定義上) その⾮素数以外の約数を持ち、その約数もその約数を乗算して算出された数値の約数であるからです。つまり、ある数の因数の約数は、その数の因数でもあります。別の⾔い⽅をすれば、n に素数以外の因数がある場合、n は 2 つ以上の因数を持つことになります。そのため、n が 2 つの素数を乗算した結果であれば、その因数は 2 つだけということになります (この場合、n は最⼩数の因数を持つ、それ⾃体素数ではない数です)。因数の数が少ないほど、因数を⾒つけることは困難です。
異なる⼤きな素数を 2 つ選んで乗算すると、さらに⼤きな別の数になります (これは半素数と呼ばれます)。この数には、因数分解が難しいという特殊な性質があります。⼀般数体ふるい法9 など、最も効率的な因数分解アルゴリズムを使⽤しても、⼤きな素数を掛け合わせてできる⼤きな数を現実的な時間内に因数分解することはできません。どれだけの規模かと⾔えば、たとえば 2009 年に完了した 768 ビットの数値 (232 桁の⼗進数) の因数分解10 では、コンピューター クラ
スターを使⽤して 2 年間にわたり計算が⾏われました。RSA の⼀般的な利⽤例では、2048 ビット以上の数値が使⽤されています。
https://ja.wikipedia.org/wiki/%E7%B4%A0%E5%9B%A0%E6%95%B0%E5%88%86%E8%A7%A3
https://ja.wikipedia.org/wiki/%E7%B4%A0%E6%95%B0
9 https://en.wikipedia.org/wiki/General_number_field_sieve
10 http://eprint.iacr.org/2010/006
7
861

ショアのアルゴリズム11 は特殊な因数分解アルゴリズムであり、将来⼤きな変化をもたらす可能性があります。ほとんどの因数分解アルゴリズムは、古典的な性質を持ち、標準的なコンピューターで処理されますが、ショアのア
ルゴリズムでは量⼦コンピューターを利⽤します12。量⼦コンピューターは、ある種の量⼦現象の性質を利⽤して、いくつかの古典的な演算を⾼速化できます。特に、ショアのアルゴリズムは因数分解を⾼速化し、その複雑性を (指数関数時間ではなく) 多項式時間の複雑性の問題に変換することができます。これは、現在のどの古典的アルゴリ
ズムよりもはるかに効率的です。このようなアルゴリズムが量⼦コンピューター上で実⾏可能になった場合、現在の RSA 鍵は役に⽴たなくなることが予想されます。ショアのアルゴリズムに必要な実⽤的な量⼦コンピューター
はまだ開発されていませんが、この分野は現在活発に研究されています。
現在のところ、⼤きな半素数の素因数分解は実質的に計算不能とされていますが、それが数学的に証明されているわけではありません。つまり、将来、現実的な時間内に素因数分解を解くアルゴリズムが発⾒される可能性があります。RSA についても同じことが⾔えます。
以上を念頭に、RSA アルゴリズムの説明に⼊ります。基本原理は次の式で表されます。

(me )d≡m(mod n)
図 7.4: RSA の基本式
上記の式を満たす 3 つの⾮常に⼤きな整数 e、d、n を計算で求めることは不可能です。RSA アルゴリズムでは、他の2 つの数がわかっていても d を⾒つけることが困難であることを利⽤しています。つまり、この式は⼀⽅向性関数に変換することができます。その際、d を秘密鍵、n と e を公開鍵と考えることができます。

7.2.2.1

e、d、n の選択

1. 2 つの異なる素数 p と q を選択します。
• p と q の候補を選択する際には、暗号的に安全な乱数⽣成器を使⽤する必要があります。安全な乱数⽣成器でないと、攻撃者にこれらの数を発⾒されるおそれがあります。

• 素数をランダムに⽣成する⽅法はないため、2 つの乱数を選択してから、その乱数に対して素数判定を⾏う必要があります。決定的素数判定法はコストがかかることがあるため、⼀部の実装例では確率的判定法を利⽤しています。この判定法では、擬素数が⾒つかる確率を考える必要があります。
• p と q は、⼤きさは近いが同⼀ではなく、⻑さが数桁違うものである必要があります。
2. n は p × q の結果です。これは上記の式のモジュラス値 (mod) です。n のビット数がアルゴリズムの鍵⻑になります。
3. n に対するオイラーのトーシェント関数13 を計算します。n は半素数であるため、これは n - (p + q - 1) という式で簡単に計算できます。この値をphi (n) とします。
4. 次の条件を満たす e を選択します。
• 1 < e < phi(n)• e と phi (n) は互いに素である
5. 次の式を満たす d を選択します。

d≡e−1 (mod φ (n))
図 7.5: RSA の基本式
公開鍵は値 n と e で構成されます。秘密鍵は値 n と d で構成されます。値 p、q、phi(n) は、d を⾒つける⼿がかりになるため、破棄するか⾮公開にする必要があります。
上記の式から、e と d が数学的に対称であることは明らかです。⼿順 5 の式は次のように書き換えることができます。

d . e≡1(mod φ(n))
図 7.6: e と d の対称性
https://en.wikipedia.org/wiki/Shor%27s_algorithm
https://ja.wikipedia.org/wiki/%E9%87%8F%E5%AD%90%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF
13 https://ja.wikipedia.org/wiki/%E3%82%AA%E3%82%A4%E3%83%A9%E3%83%BC%E3%81%AE%CF%86%E9%96%A2%E6%95%B0
11
12

62

ここで、値 e と n を公開しても、なぜ RSA が安全なのかと疑問に思うかもしれません。これらの値を使⽤して、d を発⾒することはできないのでしょうか。ここで重要なのは、合同算術には可能な解が複数あるということです。選択したd が上記の式を満たす限り、どの値も有効です。値が⼤きいほど、発⾒が困難になります。そのため、値 e または d のどちらか⼀⽅のみを公開すれば、RSA は機能します。また、どちらか⼀⽅の値を選択できるのであれば、できる限り⼩さい値を公開する値として選ぶことができます。そうすることで、アルゴリズムの安全性を損なうことなく計算が⾼速化されます。

7.2.2.2

基本的な署名⽅法

RSA での署名は、次の⼿順で実⾏します。
1. ハッシュ関数によって署名するメッセージからメッセージ ダイジェストを⽣成します。
2. このダイジェストを d 乗して n (秘密鍵の⼀部) で除算した余りを求めます。
3. 結果を署名としてメッセージに添付します。
公開鍵を持っている受信者がメッセージの信頼性を検証する際には、次のように逆順に処理を⾏います。
1. 署名を e 乗し、n で除算した余りを求めます。結果の値は参照ダイジェスト値です。
2. 署名⼿順と同じハッシュ関数を使⽤して、メッセージからメッセージ ダイジェストを⽣成します。
3. ⼿順 1 と 2 の結果を⽐較します。⼀致する場合、署名者は秘密鍵を所有しているはずです。
この署名/検証⽅式は "Signature Scheme with Appendix: SSA (付加物を使⽤した署名⽅式)" と呼ばれています。この⽅式では、メッセージを検証するために元のメッセージを使⽤する必要があります。つまり、署名からメッセージを復元することはできません (メッセージと署名が分離されています)。

7.2.2.3

RS256: SHA-256 を使⽤した RSASSA PKCS1 v1.5

RSA の仕組みについて基本的な概念を説明したので、次はその変種の 1 つ、SHA-256 を使⽤した PKCS#1 RSASSA v1.5
を取り上げます。これは JWA 仕様では RS256 と呼ばれています。
Public Key Cryptography Standard #1 (PKCS #1)14 仕様では、RSA アルゴリズムに基づく⼀連のプリミティブ、フォーマッ
ト、暗号化⽅式が定義されています。これらの要素を組み合わせることで、最新のコンピューティング プラットフォー
ムで利⽤できる詳細な RSA 実装を実現できます。RSASSA はこの仕様で定義されている⽅式の 1 つです。この⽅式を利⽤することで、署名で RSA を使⽤することができます。

7.2.2.3.1

アルゴリズム

署名を作成するには、次の⼿順を実⾏します。
1. EMSA-PKCS1-V1_5-ENCODE プリミティブをメッセージ (オクテット配列) に適⽤します。出⼒結果はエンコードされたメッセージです。このプリミティブはハッシュ関数 (通常、SHA-256 などの SHA 系のハッシュ関数) を使⽤します。このプリミティブは、期待されるエンコード後のメッセージの⻑さを受け取ります。ここでは、これはオクテット単位の RSA 値 n の⻑さ (鍵の⻑さ) です。
2. エンコードされたメッセージに OS2IP プリミティブを適⽤します。出⼒結果は整数メッセージ表現です。OS2IPは、Octet-String to Integer Primitive (オクテット列から整数に変換するプリミティブ) の頭字語です。
3. 秘密鍵を使⽤して、RSASP1 プリミティブを整数メッセージ表現に適⽤します。出⼒は整数署名表現です。
4. I2OSP プリミティブを適⽤して、整数署名表現をオクテット配列 (署名) に変換します。I2OSP は、Integer to OctetString Primitive (整数からオクテット列に変換するプリミティブ) の頭字語です。
以上のプリミティブに基づく JavaScript での実装例を以下に紹介します。

/*** Produces a signature for a message using the RSA algorithm as defined* in PKCS#1.
* @param {privateKey} RSA private key, an object with*three members: size (size in bits), n (the modulus) and*d (the private exponent), both bigInts*(big-integer library).
* @param {hashFn} the hash function as required by PKCS#1,
14

https://www.ietf.org/rfc/rfc3447.txt

63

*
it should take a Uint8Array and return a Uint8Array* @param {hashType} A symbol identifying the type of hash function passed.
*
For now, only "SHA-256" is supported. See the "hashTypes"*
object for possible values.
* @param {message} A String or Uint8Array with arbitrary data to sign
* @return {Uint8Array} The signature as a Uint8Array*/
export function sign(privateKey, hashFn, hashType, message) {const encodedMessage =emsaPkcs1v1_5(hashFn, hashType, privateKey.size / 8, message);const intMessage = os2ip(encodedMessage);const intSignature = rsasp1(privateKey, intMessage);const signature = i2osp(intSignature, privateKey.size / 8);
return signature;}
署名を検証するには、次の⼿順を実⾏します。
1. OS2IP プリミティブを署名 (オクテット配列) に適⽤します。出⼒結果は整数署名表現です。
2. RSAVP1 プリミティブを前の結果に適⽤します。このプリミティブは、公開鍵も⼊⼒として受け取ります。出⼒結
果は整数メッセージ表現です。
3. I2OSP プリミティブを前の結果に適⽤します。このプリミティブは、期待される⻑さを⼊⼒として受け取ります。
この⻑さは、鍵のモジュラス値の⻑さ (オクテット数) と⼀致している必要があります。出⼒結果はエンコードされたメッセージです。
4. EMSA-PKCS1-V1_5-ENCODE プリミティブを、検証するメッセージに適⽤します。出⼒結果は別のエンコードされたメッセージです。このプリミティブはハッシュ関数 (通常、SHA-256 などの SHA 系のハッシュ関数) を使⽤します。このプリミティブは、期待されるエンコード後のメッセージの⻑さを受け取ります。ここでは、これはオクテット単位の RSA 値 n の⻑さ (鍵の⻑さ) です。
5. 2 つのエンコードされたメッセージ (⼿順 3 と 4 のメッセージ) を⽐較します。⼀致する場合、署名は有効です
が、⼀致しない場合は無効です。
JavaScript での実装例は次のようになります。

/*** Verifies a signature for a message using the RSASSA algorithm as defined* in PKCS#1.
* @param {publicKey} RSA private key, an object with*three members: size (size in bits), n (the modulus) and*e (the public exponent), both bigInts*(big-integer library).
* @param {hashFn} the hash function as required by PKCS#1,*
it should take a Uint8Array and return a Uint8Array* @param {hashType} A symbol identifying the type of hash function passed.
*
For now, only "SHA-256" is supported. See the "hashTypes"*
object for possible values.
* @param {message} A String or Uint8Array with arbitrary data to verify* @param {signature} A Uint8Array with the signature* @return {Boolean} true if the signature is valid, false otherwise.
*/
export function verifyPkcs1v1_5(publicKey,
hashFn,hashType,
message,signature) {if(signature.length !== publicKey.size / 8) {throw new Error('invalid signature length');}const intSignature = os2ip(signature);64

const intVerification = rsavp1(publicKey, intSignature);
const verificationMessage = i2osp(intVerification, publicKey.size / 8);
const encodedMessage =emsaPkcs1v1_5(hashFn, hashType, publicKey.size / 8, message);
return uint8ArrayEquals(encodedMessage, verificationMessage);}
7.2.2.3.1.1

EMSA-PKCS1-v1_5 プリミティブ

このプリミティブは次の 3 つの要素を受け取ります。

• メッセージ• ⽬的とする出⼒結果の⻑さ• 使⽤するハッシュ関数 (⼿順 2 で挙げられているいずれかの関数)
1. 選択したハッシュ関数をメッセージに適⽤します。
2. 次の ASN.1 構造の DER エンコードを⽣成します。

DigestInfo ::= SEQUENCE{ digestAlgorithm DigestAlgorithm,
digest OCTET STRING}
digest は⼿順 1 の出⼒結果であり、DigestAlgorithm は次のいずれかです。

DigestAlgorithm ::=AlgorithmIdentifier { {PKCS1-v1-5DigestAlgorithms} }PKCS1-v1-5DigestAlgorithms ALGORITHM-IDENTIFIER ::= {{ OID id-md2 PARAMETERS NULL }|{ OID id-md5 PARAMETERS NULL }|{ OID id-sha1 PARAMETERS NULL }|{ OID id-sha256 PARAMETERS NULL }|{ OID id-sha384 PARAMETERS NULL }|{ OID id-sha512 PARAMETERS NULL }}3. 求められている出⼒結果の⻑さが⼿順 3 の結果に 11 を加えたものよりも短い場合 (reqLength < step2Length + 11)、
プリミティブは結果の⽣成に失敗し、エラー メッセージ "intended encoded message length too short" (⽬的とするエンコード後のメッセージ⻑が短すぎます) を出⼒します。
4. オクテット 0xFF を requested length + step2Length - 3 回繰り返して、オクテット配列を⽣成します。
このオクテット配列は PS と呼ばれます。
5. エンコードされた最終メッセージ (EM) を⽣成します (|| は連結演算⼦)。

EM = 0x00 || 0x01 || PS || 0x00 || step2Result
通常、ASN.1 OID は独⾃の仕様で定義されます。そのため、PKCS#1 の仕様に SHA-256 OID はありません。SHA1 および SHA-2 OID は RFC 356015 で定義されています。

7.2.2.3.1.2

OS2IP プリミティブ

OS2IP プリミティブはオクテット配列を受け取り、整数表現を出⼒します。

• X1、X2、...、Xn を、最初から最後までの⼊⼒オクテットとします。
• 結果を次のように計算します。

𝑟𝑒𝑠𝑢𝑙𝑡 = 𝑋! 256"#! + 𝑋$ 256"#$ + ⋯ + 𝑋"#! 256 + 𝑋"
15

https://tools.ietf.org/html/rfc3560.html

65図 7.7: OS2IP の出⼒結果

7.2.2.3.1.3

RSASP1 プリミティブ

RSASP1 プリミティブは、秘密鍵とメッセージ表現を受け取り、署名表現を⽣成します。

• n と d を秘密鍵の RSA 値とします。
• m をメッセージ表現とします。
1. メッセージ表現が 0 から n - 1 の範囲にあることを確認します。
2. 結果を次のように計算します。

𝑠 = 𝑚% 𝑚𝑜𝑑(𝑛)図 7.8: RSASP1 の出⼒結果
PKCS#1 では代替⼿段として、計算上便利な秘密鍵の格納⽅法が定義されています。この⽅法では、秘密鍵を n とd として格納するのではなく、事前計算された異なる値の組み合わせを格納し、特定の処理に使⽤します。これらの値を特定の処理で直接使⽤することで、計算を⼤幅に⾼速化できます。ほとんどの秘密鍵は、この⽅法を使⽤して格納されています。ただし、秘密鍵を n と d として格納する⽅法も適正な⽅法です。

7.2.2.3.1.4

RSAVP1 プリミティブ

RSAVP1 プリミティブは、公開鍵と整数署名表現を受け取り、整数メッセージ表現を⽣成します。

• n と e を公開鍵の RSA 値とします。
• s を整数署名表現とします。
1. メッセージ表現が 0 から n - 1 の範囲にあることを確認します。
2. 結果を次のように計算します。

𝑚 = 𝑆 & 𝑚𝑜𝑑(𝑛)図 7.9: RSAVP1 の出⼒結果

7.2.2.3.1.5

I2OSP プリミティブ

I2OSP プリミティブは整数表現を受け取り、オクテット配列を⽣成します。

• len をオクテット配列の期待される⻑さとします。
• x を整数表現とします。
1. x > 256len である場合、整数が⼤きすぎ、引数が正しくありません。
2. 整数の 256 進数表現を計算します。

χ = 𝜒! 256"#$%! + 𝜒& 256"#$%& + ⋯ + 𝜒"#$%! 256 + 𝜒"#$図 7.10: I2OSP による分解
3. それぞれの項の係数 xlen-i を順に取得します。これが結果のオクテット配列です。

7.2.2.3.2

サンプル コード

RSA は任意精度の演算が必要であるため、ここでは big-integer16 JavaScript ライブラリを使⽤します。
OS2IP プリミティブおよび I2OSP プリミティブは⾮常にシンプルです。

function os2ip(bytes) {16

https://www.npmjs.com/package/big-integer66

let result = bigInt();
bytes.forEach((b, i) => {// result += b * Math.pow(256, bytes.length - 1 - i);result = result.add( bigInt(b).multiply(
bigInt(256).pow(bytes.length - i - 1)));});
return result;}function i2osp(intRepr, expectedLength) {if(intRepr.greaterOrEquals(bigInt(256).pow(expectedLength))) {throw new Error('integer too large');}const result = new Uint8Array(expectedLength);
let remainder = bigInt(intRepr);
for(let i = expectedLength - 1; i >= 0; --i) {const position = bigInt(256).pow(i);const quotrem = remainder.divmod(position);
remainder = quotrem.remainder;
result[result.length - 1 - i] = quotrem.quotient.valueOf();}
return result;}
I2OSP プリミティブは基本的に、数を 256 進法17 の要素に分解します。
RSASP1 プリミティブは基本的に単⼀の演算であり、アルゴリズムの基礎を成しています。

function rsasp1(privateKey, intMessage) {if(intMessage.isNegative() ||intMessage.greaterOrEquals(privateKey.n)) {throw new Error("message representative out of range");}// result = intMessage ^ d (mod n)return intMessage.modPow(privateKey.d, privateKey.n);}
検証⽬的には、代わりに RSAVP1 プリミティブを使⽤します。

export function rsavp1(publicKey, intSignature) {if(intSignature.isNegative() ||intSignature.greaterOrEquals(publicKey.n)) {throw new Error("message representative out of range");}// result = intSignature ^ e (mod n)return intSignature.modPow(publicKey.e, publicKey.n);}
最後に、EMSA-PKCS1-v1_5 プリミティブでメッセージをエンコードされたパディング付き表現に変換します。演算の17

https://ja.wikipedia.org/wiki/%E4%BD%8D%E5%8F%96%E3%82%8A%E8%A8%98%E6%95%B0%E6%B3%95

67⼤部分はここで実⾏されます。

function emsaPkcs1v1_5(hashFn, hashType, expectedLength, message) {if(hashType !== hashTypes.sha256) {throw new Error("Unsupported hash type");}const digest = hashFn(message, true);// DER is a stricter set of BER, this (fortunately) works:
const berWriter = new Ber.Writer();
berWriter.startSequence();
berWriter.startSequence();// SHA-256 OID
berWriter.writeOID("2.16.840.1.101.3.4.2.1");
berWriter.writeNull();berWriter.endSequence();
berWriter.writeBuffer(Buffer.from(digest), ASN1.OctetString);
berWriter.endSequence();// T is the name of this element in RFC 3447
const t = berWriter.buffer;if(expectedLength < (t.length + 11)) {throw new Error('intended encoded message length too short');}const ps = new Uint8Array(expectedLength - t.length - 3);
ps.fill(0xff);
assert.ok(ps.length >= 8);
return Uint8Array.of(0x00, 0x01, ...ps, 0x00, ...t);}
ここでは、簡潔にするために SHA-256 のみをサポートしています。他のハッシュ関数は、適切な OID を追加するだけで追加されます。
signPkcs1v1_5 関数は、すべてのプリミティブをまとめ、署名を実⾏します。

/*** Produces a signature for a message using the RSA algorithm as defined* in PKCS#1.
* @param {privateKey} RSA private key, an object with*three members: size (size in bits), n (the modulus) and*d (the private exponent), both bigInts*(big-integer library).
* @param {hashFn} the hash function as required by PKCS#1,*
it should take a Uint8Array and return a Uint8Array* @param {hashType} A symbol identifying the type of hash function passed.
*
For now, only "SHA-256" is supported. See the "hashTypes"*
object for possible values.
* @param {message} A String or Uint8Array with arbitrary data to sign
* @return {Uint8Array} The signature as a Uint8Array*/
export function signPkcs1v1_5(privateKey, hashFn, hashType, message) {const encodedMessage =emsaPkcs1v1_5(hashFn, hashType, privateKey.size / 8, message);68const intMessage = os2ip(encodedMessage);const intSignature = rsasp1(privateKey, intMessage);const signature = i2osp(intSignature, privateKey.size / 8);
return signature;}
これを使⽤して JWT に署名するには、シンプルなラッパーが必要です。

export default function jwtEncode(header, payload, privateKey) {if(typeof header !== 'object' || typeof payload !== 'object') {throw new Error('header and payload must be objects');}header.alg = 'RS256';
const encHeader = b64(JSON.stringify(header));
const encPayload = b64(JSON.stringify(payload));const jwtUnprotected = `${encHeader}.${encPayload}`;const signature = b64(
pkcs1v1_5.sign(privateKey,
msg => sha256(msg, true),
hashTypes.sha256, stringToUtf8(jwtUnprotected)));return `${jwtUnprotected}.${signature}`;}
この関数は、HMAC の節で⽰した HS256 ⽤の jwtEncode 関数とよく似ています。検証も簡単です。

/*** Verifies a signature for a message using the RSASSA algorithm as defined* in PKCS#1.
* @param {publicKey} RSA private key, an object with*three members: size (size in bits), n (the modulus) and*e (the public exponent), both bigInts*(big-integer library).
* @param {hashFn} the hash function as required by PKCS#1,*
it should take a Uint8Array and return a Uint8Array* @param {hashType} A symbol identifying the type of hash function passed.
*
For now, only "SHA-256" is supported. See the "hashTypes"*
object for possible values.
* @param {message} A String or Uint8Array with arbitrary data to verify* @param {signature} A Uint8Array with the signature* @return {Boolean} true if the signature is valid, false otherwise.
*/
export function verifyPkcs1v1_5(publicKey,
hashFn,hashType,
message,signature) {if(signature.length !== publicKey.size / 8) {throw new Error('invalid signature length');}const intSignature = os2ip(signature);
const intVerification = rsavp1(publicKey, intSignature);
const verificationMessage = i2osp(intVerification, publicKey.size / 8);
const encodedMessage =69

emsaPkcs1v1_5(hashFn, hashType, publicKey.size / 8, message);
return uint8ArrayEquals(encodedMessage, verificationMessage);
これを使⽤して JWT を検証するには、シンプルなラッパーが必要です。

export function jwtVerifyAndDecode(jwt, publicKey) {if(!isString(jwt)) {throw new TypeError('jwt must be a string');}
const split = jwt.split('.');
if(split.length !== 3) {throw new Error('Invalid JWT format');}const header = JSON.parse(unb64(split[0]));if(header.alg !== 'RS256') {throw new Error(`Wrong algorithm: ${header.alg}`);}const jwtUnprotected = stringToUtf8(`${split[0]}.${split[1]}`);
const valid = verifyPkcs1v1_5(publicKey,
msg => sha256(msg, true),
hashTypes.sha256,
jwtUnprotected,
base64.decode(split[2]));
return {header: header,
payload: JSON.parse(unb64(split[1])),valid: valid};}

簡潔にするために、ここでは秘密鍵と公開鍵を、2 つの別々の数値を持つ JavaScript オブジェクトとして渡しています。
その 2 つの数値は、秘密鍵ではモジュラス値 (n) と秘密指数 (d)、公開鍵ではモジュラス値 (n) と公開指数 (e) となります。これは通常の PEM エンコード18 形式とは異なります。詳細については、rs256.js ファイルを参照してください。
OpenSSL を使⽤すると、PEM 鍵からこれらの数値を書き出すことができます。

openssl rsa -text -noout -in testkey.pem
OpenSSL を使⽤すると、RSA 鍵をゼロから⽣成することもできます。

openssl genrsa -out testkey.pem 2048
⽣成したら、上記のコマンドを使⽤して、PEM 形式から数値を書き出すことができます。
testkey.js フ ァ イ ル に 埋 め 込 ま れ て い る 秘 密 鍵 の 値 は 、 本 書 に 付 属 す る samples デ ィ レ ク ト リ 内 に あ る
testkey.pem ファイルに含まれているものです。対応する公開鍵は、pubtestkey.pem ファイルにあります。
rs256.js のサンプル19 を実⾏した出⼒を JWT.io20 の JWT の領域にコピーします。続いて pubtestkey.pem の内容をコピーして同じページの公開鍵の領域に貼り付けると、JWT が問題なく検証されます。

https://en.wikipedia.org/wiki/Privacy-enhanced_Electronic_Mail
https://github.com/auth0/jwt-handbook-samples/blob/master/rs256.js
20 https://jwt.io
18
19

70

7.2.2.4

PS256: SHA-256 および MGF1 と SHA-256 を使用した RSASSA-PSS

RSASSA-PSS は、もう 1 つの RSA ベースの SSA です。"PSS" は Probabilistic Signature Scheme (確率的署名⽅式) を表し、通常の決定的⼿法とはまったく異なります。この⽅式では、暗号論的に安全な乱数発⽣器を使⽤します。安全な乱数
発⽣器が利⽤できない場合も、署名処理および検証処理によって決定的⼿法と同等のレベルのセキュリティを実現できます。そのため、RSASSA-PSS は、理想的な条件のもとでは、PKCS v1.5 署名よりも優れたセキュリティを実現します。
ただし、PSS ⽅式も PKCS v 1.5 ⽅式も、実際に破られたことはありません。
RSASSA-PSS は Public Key Cryptography Standard #1 (PKCS #1)21 で定義されており、以前のバージョンの規格では使⽤できません。

7.2.2.4.1

アルゴリズム

署名を作成するには、次の⼿順を実⾏します。
1. EMSA-PSS-ENCODE プリミティブをメッセージに適⽤します。このプリミティブは、鍵のモジュラス値のビット数から 1 を引いた数のパラメーターを受け取ります。出⼒結果はエンコードされたメッセージです。
2. エンコードされたメッセージに OS2IP プリミティブを適⽤します。出⼒結果は整数メッセージ表現です。OS2IPは、Octet-String to Integer Primitive (オクテット列から整数に変換するプリミティブ) の頭字語です。
3. 秘密鍵を使⽤して、RSASP1 プリミティブを整数メッセージ表現に適⽤します。出⼒は整数署名表現です。
4. I2OSP プリミティブを適⽤して、整数署名表現をオクテット配列 (署名) に変換します。I2OSP は、Integer to OctetString Primitive (整数からオクテット列に変換するプリミティブ) の頭字語です。
以上のプリミティブに基づく JavaScript での実装例を以下に紹介します。

export function signPss(privateKey, hashFn, hashType, message) {if(hashType !== hashTypes.sha256) {throw new Error('unsupported hash type');}const encodedMessage = emsaPssEncode(hashFn,
hashType,
mgf1.bind(null, hashFn),
256 / 8, //size of hashprivateKey.size - 1,
message);const intMessage = os2ip(encodedMessage);const intSignature = rsasp1(privateKey, intMessage);const signature = i2osp(intSignature, privateKey.size / 8);
return signature;}
署名を検証するには、次の⼿順を実⾏します。
1. OS2IP プリミティブを署名 (オクテット配列) に適⽤します。出⼒結果は整数署名表現です。
2. RSAVP1 プリミティブを前の結果に適⽤します。このプリミティブは、公開鍵も⼊⼒として受け取ります。出⼒結
果は整数メッセージ表現です。
3. I2OSP プリミティブを前の結果に適⽤します。このプリミティブは、期待される⻑さを⼊⼒として受け取ります。
この⻑さは、鍵のモジュラス値の⻑さ (オクテット数) と⼀致している必要があります。出⼒結果はエンコードされたメッセージです。
4. 検証対象のメッセージと直前の⼿順の結果に EMSA-PSS-VERIFY プリミティブを適⽤します。このプリミティブ
は、署名が有効か無効かを出⼒します。このプリミティブはハッシュ関数 (通常、SHA-256 などの SHA 系のハッシュ関数) を使⽤します。このプリミティブは、鍵のモジュラス値のビット数から 1 を引いた数のパラメーターを受け取ります。

export function verifyPss(publicKey, hashFn, hashType, message, signature) {if(signature.length !== publicKey.size / 8) {throw new Error('invalid signature length');}const intSignature = os2ip(signature);21

https://www.ietf.org/rfc/rfc3447.txt

71const intVerification = rsavp1(publicKey, intSignature);
const verificationMessage =i2osp(intVerification, Math.ceil( (publicKey.size - 1) / 8 ));return emsaPssVerify(hashFn,
hashType,
mgf1.bind(null, hashFn),
256 / 8,publicKey.size - 1,
message,verificationMessage);}
7.2.2.4.1.1

MGF1: マスク⽣成関数

マスク⽣成関数は、任意の⻑さの⼊⼒を受け取り、可変⻑の出⼒を⽣成します。ハッシュ関数と同様に、これは決定的関数であり、同じ⼊⼒に対して同じ出⼒を⽣成します。ただし、ハッシュ関数とは異なり、出⼒の⻑さは可変です。Mask
Generation Function 1 (MGF1) アルゴリズムは、Public Key Cryptography Standard #1 (PKCS #1)22 で定義されています。
MGF1 は⼊⼒として、シード値と、出⼒結果の⽬標⻑さを受け取ります。出⼒の最⼤⻑は 232 と定義されます。MGF1 では内部的に、設定可能なハッシュ関数を使⽤します。PS256 では、このハッシュ関数が SHA-256 と指定されています。
1. ⽬標⻑さが 232 より⼤きい場合は、"mask too long (マスクが⻑すぎます)" というエラーが発⽣して停⽌します。
2. 0 から ceiling(intendedLength / hashLength) - 1 (⽬標⻑さをハッシュ関数の出⼒⻑で割った値を最⼩の整数に切り上げてから 1 を引いた値) までの範囲で、次の演算を反復します。
1. c = i2osp(counter, 4) とします。counter は反復カウンターの現在の値です。
2. t = t.concat(hash(seed.concat(c))) とします。t は反復間で保持されます。
hash は選択したハッシュ関数 (SHA-256)、seed は⼊⼒シード値です。
3. 関数の出⼒結果として、t の最後の値から⽬標⻑さ左端までのオクテットを出⼒します。

7.2.2.4.1.2

EMSA-PSS-ENCODE プリミティブ

このプリミティブは次の 2 つの要素を受け取ります。

• オクテット列として符号化されるメッセージ。
• ⽬的とする出⼒結果の最⼤⻑ (ビット単位)。
このプリミティブは、次の要素でパラメーター表現できます。

• ハッシュ関数。PS256 の場合、これは SHA-256 です。
• マスク⽣成関数。PS256 の場合、これは MGF1 です。
• 内部で使⽤するソルトの⽬標⻑さ。
これらのパラメーターはすべて PS256 で指定されているため、設定可能ではありません。ここでは説明上、定数とみなします。
⼊⼒として使⽤される⽬標⻑さは、ビット単位で表現されます。以下の例では、次の定義を使⽤します。

const intendedLength = Math.ceil(intendedLengthBits / 8);
1. ⼊⼒がハッシュ関数の最⼤⻑より⼤きい場合は、処理を停⽌します。そうでない場合は、メッセージにハッシュ関数を適⽤します。

const hashed1 = sha256(inputMessage);
2. メッセージの⽬標⻑さが、ハッシュの⻑さにソルトの⻑さを加えたものに 2 を加えた値よりも短い場合は、停⽌
してエラーを発⽣します。

if(intendedLength < (hashed1.length + intendedSaltLength + 2)) {throw new Error('Encoding error');}22

https://www.ietf.org/rfc/rfc3447.txt

72

3. ソルトの⻑さと同じランダムなオクテット列を⽣成します。
4. 8 個の値ゼロのオクテットを、メッセージのハッシュとソルトに連結します。

const m = [0,0,0,0,0,0,0,0, ...hashed1, ...salt];
5. ハッシュ関数を直前の⼿順の結果に適⽤します。

const hashed2 = sha256(m);6. ⽬標とする出⼒結果最⼤⻑さからソルト⻑、ハッシュ⻑、および 2 を引いた値と同じ⻑さの値ゼロのオクテット
列を⽣成します。

const ps = new Array(intendedLength - intendedSaltLength - 2).fill(0);7. 直前の⼿順の結果をオクテット 0x01 とソルトに連結します。

const db = [...ps, 0x01, ...salt];
8. ⼿順 5 の結果にマスク⽣成関数を適⽤し、この関数の⽬標⻑さを⼿順 7 の結果の⻑さに設定します (マスク⽣成
関数は指定された⻑さのパラメーターを受け取ります)。

const dbMask = mgf1(hashed2, db.length);9. ⼿順 7 と 8 の結果に XOR 演算を適⽤した結果を計算します。

const maskedDb = db.map((value, index) => {return value ^ dbMask[index];});
10. 直前の演算結果の⻑さが 8 の倍数でない場合は、減算するとその⻑さが 8 の倍数になるビット数の差を求め、このビットを左から 0 に設定します。

const zeroBits = 8 * intendedLength - intendedLengthBits;
const zeroBitsMask = 0xFF >>> zeroBits;maskedDb[0] &= zeroBitsMask;
11. 直前の⼿順の結果を⼿順 5 の結果とオクテット 0xBC に連結します。これが出⼒結果です。

const result = [...maskedDb, ...hashed2, 0xBC];
7.2.2.4.1.3

EMSA-PSS-VERIFY プリミティブ

このプリミティブは次の 3 つの要素を受け取ります。

• 検証対象のメッセージ。
• エンコードされた整数メッセージ形式の署名。
• エンコード後の整数メッセージの⽬標最⼤⻑。
このプリミティブは、次の要素でパラメーター表現できます。

• ハッシュ関数。PS256 の場合、これは SHA-256 です。
• マスク⽣成関数。PS256 の場合、これは MGF1 です。
• 内部で使⽤するソルトの⽬標⻑さ。
これらのパラメーターはすべて PS256 で指定されているため、設定可能ではありません。ここでは説明上、定数とみなします。
⼊⼒として使⽤される⽬標⻑さは、ビット単位で表現されます。以下の例では、次の定義を使⽤します。

const expectedLength = Math.ceil(expectedLengthBits / 8);
1. 選択したハッシュ関数を使⽤して、検証対象のメッセージをハッシュ化します。

const digest1 = hashFn(message, true);
2. ⽬標⻑さがハッシュ⻑ + ソルト⻑ + 2 より⼩さい場合、署名を無効とみなします。

if(expectedLength < (digest1.length + saltLength + 2)) {73

return false;}
3. 署名のエンコードされたメッセージの最後のバイトの値が 0xBC であることを確認します。

if(verificationMessage[verificationMessage.length - 1] !== 0xBC) {return false;}
4. エンコードされたメッセージを 2 つの要素に分割します。最初の要素の⻑さは expectedLength hashLength - 1 です。2 番⽬の要素は最初の要素の終わりから始まり、⻑さは hashLength です。

const maskedLength = expectedLength - digest1.length - 1;
const masked = verificationMessage.subarray(0, maskedLength);const digest2 = verificationMessage.subarray(maskedLength,
maskedLength + digest1.length);
5. masked の左端の 8 * expectedLength - expectedLengthBits (期待されるビット⻑から要求されるビッ
ト⻑を引いたもの) ビットが 0 であることを確認します。

const zeroBits = 8 * expectedLength - expectedLengthBits;
const zeroBitsMask = 0xFF >>> zeroBits;if((masked[0] & (~zeroBitsMask)) !== 0) {return false;}
6. ⼿順 4 で抽出した 2 番⽬の要素 (ダイジェスト) を、選択した MGF 関数に渡します。expectedLength hashLength - 1 の⻑さを持つ結果を要求します。

const dbMask = mgf(maskedLength, digest2);
7. ⼿順 4 (masked) で抽出した最初の要素の各バイトに対して、直前の⼿順 (dbMask) で計算した要素の対応するバイトを使⽤して、XOR 関数を適⽤します。

const db = new Uint8Array(masked.length);for(let i = 0; i < db.length; ++i)
{ db[i] = masked[i] ^ dbMask[i];}8. 直前の⼿順で計算した要素内の最初のバイトから左端の 8 * expectedLength - expectedLengthBits
ビットを 0 に設定します。

const zeroBits = 8 * expectedLength - expectedLengthBits;
const zeroBitsMask = 0xFF >>> zeroBits;db[0] &= zeroBitsMask;
9. 直前の⼿順で計算した要素の左端の expectedLength - hashLength - saltLength - 2 バイトが 0 であることを確認します。また、ゼロのグループの後に続く最初の要素が 0x01 であることを確認します。

const zeroCheckLength = expectedLength - (digest1.length + saltLength + 2);
if(!db.subarray(0, zeroCheckLength).every(v => v === 0) ||db[zeroCheckLength] !== 0x01) {return false;}
10. 直前の⼿順 (db) で計算した要素の最後の saltLength オクテットからソルトを抽出します。

const salt = db.subarray(db.length - saltLength);
11. 8 個の値ゼロのオクテット、⼿順 1 で計算したハッシュ、および直前の⼿順で抽出したソルトを連結して、新しいエンコードされたメッセージを算出します。

const m = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0, ...digest1, ...salt);
12. 直前の⼿順で計算した要素のハッシュを計算します。

const expectedDigest = hashFn(m, true);
13. 直前の⼿順で計算した要素を、⼿順 4 で抽出した 2 番⽬の要素と⽐較します。⼀致する場合、署名は有効です74

が、⼀致しない場合は無効です。

return uint8ArrayEquals(digest2, expectedDigest);
7.2.2.4.2

サンプル コード

このアルゴリズムは RSASSA の変種であるため、必要なコードの⼤部分は、RS256 の実装例で既に⽰したものです。唯⼀の違いは、EMSA-PSS-ENCODE、EMSA-PSS-VERIFY、および MGF1 プリミティブが追加される点です。

export function mgf1(hashFn, expectedLength, seed) {if(expectedLength > Math.pow(2, 32)) {throw new Error('mask too long');}
const hashSize = hashFn(Uint8Array.of(0), true).byteLength;
const count = Math.ceil(expectedLength / hashSize);
const result = new Uint8Array(hashSize * count);for(let i = 0; i < count; ++i) {const c = i2osp(bigInt(i), 4);const value = hashFn(Uint8Array.of(...seed, ...c), true);
result.set(value, i * hashSize);}
return result.subarray(0, expectedLength);}export function emsaPssEncode(hashFn,
hashType,
mgf,
saltLength,
expectedLengthBits,message) {const expectedLength = Math.ceil(expectedLengthBits / 8);
const digest1 = hashFn(message, true);if(expectedLength < (digest1.length + saltLength + 2)) {throw new Error('encoding error');}const salt = crypto.randomBytes(saltLength);const m = Uint8Array.of(...(new Uint8Array(8)),...digest1,...salt);
const digest2 = hashFn(m, true);
const ps = new Uint8Array(expectedLength - saltLength - digest2.length - 2);
const db = Uint8Array.of(...ps, 0x01, ...salt);
const dbMask = mgf(db.length, digest2);
const masked = db.map((value, index) => value ^ dbMask[index]);const zeroBits = 8 * expectedLength - expectedLengthBits;
const zeroBitsMask = 0xFF >>> zeroBits;masked[0] &= zeroBitsMask;
return Uint8Array.of(...masked, ...digest2, 0xbc);}
export function emsaPssVerify(hashFn,
hashType,
mgf,
saltLength,75

expectedLengthBits,message,verificationMessage) {const expectedLength = Math.ceil(expectedLengthBits / 8);
const digest1 = hashFn(message, true);if(expectedLength < (digest1.length + saltLength + 2)){
return false;}
if(verificationMessage.length === 0) {return false;}
if(verificationMessage[verificationMessage.length - 1] !== 0xBC) {return false;}
const maskedLength = expectedLength - digest1.length - 1;
const masked = verificationMessage.subarray(0, maskedLength);const digest2 = verificationMessage.subarray(maskedLength,
maskedLength + digest1.length);const zeroBits = 8 * expectedLength - expectedLengthBits;
const zeroBitsMask = 0xFF >>> zeroBits;if((masked[0] & (~zeroBitsMask)) !== 0) {return false;}
const dbMask = mgf(maskedLength, digest2);
const db = masked.map((value, index) => value ^ dbMask[index]);db[0] &= zeroBitsMask;
const zeroCheckLength = expectedLength - (digest1.length + saltLength + 2);
if(!db.subarray(0, zeroCheckLength).every(v => v === 0) ||db[zeroCheckLength] !== 0x01) {return false;}
const salt = db.subarray(db.length - saltLength);const m = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0, ...digest1, ...salt);
const expectedDigest = hashFn(m, true);
return uint8ArrayEquals(digest2, expectedDigest);}
完全なサンプル23 は、ps256.js、rsassa.js、pkcs.js にあります。testkey.js ファイルに埋め込まれている秘密鍵の値は、本書に付属する samples ディレクトリ内にある testkey.pem ファイルに含まれているものです。対応する公開鍵は、pubtestkey.pem ファイルにあります。鍵の作成については、RS256 の例を参照してください。

7.2.3

楕円曲線

楕円曲線 (EC) アルゴリズムは、RSA アルゴリズムと同様に、⼀定の条件下では解くことが難しい数学問題の⼀種を利⽤したものです。ここで⾔う難しさとは、⼗分なリソースがあれば解を発⾒できる可能性はあるものの、実際には解を発
23https://github.com/auth0/jwt-handbook-samples

76⾒するのが困難であるという意味です。RSA は因数分解問題24 の難しさ (⼤きな複素数の素因数を⾒つけること) を利⽤したものですが、楕円曲線アルゴリズムは楕円曲線上の離散対数問題の難しさを利⽤したものです。
楕円曲線は次の⽅程式で表されます。

y2=x3 +ax +b
図 7.11: 楕円曲線の式

a と b に異なる値を設定すると、次のような曲線が得られます。

図 7.12: 楕円曲線の例
Wikimedia から引⽤したパブリック ドメインの画像です25。
RSA とは異なり、楕円曲線アルゴリズムは特定の有限体に対して定義されます。EC 暗号で重要なのは、標数 2 の体と素体です。JWA 仕様では素体のみを使⽤しているため、本書では素体を取り上げます。
数学で⾔う「体」とは、4 つの基本的な算術演算 (減算、加算、乗算、除算) が定義されている要素の集合を指します。
"有限" とは、楕円曲線暗号で無限の実数の集合に対して演算を⾏うのではなく、有限の数の集合に対して演算を⾏うことを意味します。
素体とは、素数個 p の要素を含む体です。すべての要素と算術演算は、p (素数個の要素) を法として実⾏されます。

24
25

https://ja.wikipedia.org/wiki/%E7%B4%A0%E5%9B%A0%E6%95%B0%E5%88%86%E8%A7%A3
https://commons.wikimedia.org/wiki/File:EllipticCurveCatalog.svg

77

有限体を扱う場合、数学的演算の実⾏に使⽤する算法が変化します。特に、通常の対数の代わりに離散対数26 を使⽤する必要があります。対数は次の形の式で表されます (k の値)。

a =cloga (c)=kk

図 7.13: 指数関数

離散対数を計算できる効率的かつ汎⽤的なアルゴリズムは発⾒されていません。離散対数が暗号に最適なのは、このような限界があるためです。この限界を利⽤し、楕円曲線を暗号に使⽤することで、セキュアな⾮対称暗号化/署名処理を実現できます。
離散対数問題の求解困難性は、問題で使⽤する体のパラメーターに依存します。つまり、効果的な楕円曲線暗号を実現す
るには、特定のパラメーターを慎重に選ぶ必要があるということです。過去には楕円曲線アルゴリズムの不適切な使⽤が攻撃の標的とされたこともあります27。
楕円曲線暗号には、RSA で使⽤される⼤きな鍵と同等のセキュリティ レベルをより⼩さな鍵で実現できるという興味深い特徴があります。そのため、メモリが限られたデバイスでも暗号化を実⾏できます。⼀般に、256 ビットの楕円曲線
鍵は、3072 ビットの RSA 鍵と同等の暗号強度を持つと⾔われています。

7.2.3.1

楕円曲線の演算

楕円曲線署名を実装するには、楕円曲線演算の実装が必要です。基本的な演算は、点の加算、点の 2 倍算、点のスカラー倍算の 3 つです。3 つの演算をすべて実⾏することで、同⼀曲線上に有効な点が⽣成されます。

7.2.3.1.1

点の加算

P + Q ≡ R (mod q) (𝑃 ≠ 𝑄)
6𝑥' , 𝑦' : + 6𝑥( , 𝑦( : ≡ (𝑥) , 𝑦) ) (𝑚𝑜𝑑 𝑞)λ≡𝑦( − 𝑦'(𝑚𝑜𝑑 𝑞)𝑥( − 𝑥'𝑥) ≡ 𝜆& − 𝑥' − 𝑥' (𝑚𝑜𝑑 𝑞)𝑦) ≡ 𝜆6𝑥' − 𝑥) : − 𝑦' (𝑚𝑜𝑑 𝑞)
図 7.14: 点の加算

7.2.3.1.2

点の 2 倍算

P + Q ≡ R (mod q) (𝑃 = 𝑄)
2P ≡ R (mod q)
6𝑥' , 𝑦' : + 6𝑥' , 𝑦' : ≡ (𝑥) , 𝑦) ) (𝑚𝑜𝑑 𝑞)
2
3𝑥 + 𝑎𝑝
λ≡(𝑚𝑜𝑑 𝑞)2𝑦'𝑥) ≡ 𝜆& − 2𝑥' (𝑚𝑜𝑑 𝑞)
26
27

https://ja.wikipedia.org/wiki/%E9%9B%A2%E6%95%A3%E5%AF%BE%E6%95%B0
https://safecurves.cr.yp.to/78𝑦) ≡ 𝜆6𝑥' − 𝑥) : − 𝑦' (𝑚𝑜𝑑 𝑞)
図 7.15: 点の 2 倍算

7.2.3.1.3

スカラー倍算スカラー倍算では、係数 k を 2 進数表現に分解します。

kPR ≡ (mod q)
2

k = k0 + 2k1 + 2 k2 + … + 2mkm where [k0…km] ∈ {0,1}
図 7.16: スカラー倍算
続いて次の処理⼿順を適⽤します。
1. N を点 P とします。
2. Q を無限遠の点 (0, 0) とします。
3. i が 0 から m のまでの範囲内にある場合、以下を実⾏します。

1. k~i~ が 1 の場合、Q を、N に Q を加えた結果とします (楕円曲線加算)。
2. N を 2 倍算した結果を N とします (楕円曲線 2 倍算)。
4. Q を返します。
JavaScript での実装例を以下に⽰します。

function ecMultiply(P, k, modulus) {let N = Object.assign({}, p);
let Q = {x: bigInt(0),y: bigInt(0)};for(k = bigInt(k); !k.isZero(); k = k.shiftRight(1)) {if(k.isOdd()) {Q = ecAdd(Q, N, modulus);}
N = ecDouble(N, modulus);}
return Q;}
合同算術では、除算は分⼦と除数の逆数との乗算として⾏うことに注意してください。
これらの演算の JavaScript での実装例は、サンプル リポジトリ28 にある ecdsa.js ファイルにあります。このような単純な実装でも使⽤できますが、タイミング攻撃に対しては脆弱です。本番環境⽤の実装では、こうした攻撃を考慮して、さまざまなアルゴリズムを使⽤します。

7.2.3.2

楕円曲線デジタル署名アルゴリズム (ECDSA)

楕円曲線デジタル署名アルゴリズム (ECDSA) は、⽶国国家規格協会 (ANSI)29 の委員会によって開発されました。規格は X9.6330 です。この規格では、安全かつ適切に楕円曲線を署名に使⽤するために必要なすべてのパラメーターを規定しています。JWA 仕様では、この仕様 (および FIPS 186-431) に基づき、曲線のパラメーターの選択とアルゴリズムの指定を⾏っています。

https://github.com/auth0/jwt-handbook-samples/
https://www.ansi.org/
30 https://webstore.ansi.org/RecordDetail.aspx?sku=ANSI+X9.63-2011+(R2017)31 http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
28
29

79

JWT での使⽤に関して、JWA 仕様では、他の署名アルゴリズムと同様に、署名アルゴリズムへの⼊⼒を Base64 でエンコードされたヘッダーとペイロードとするよう定めていますが、結果は 1 つの整数ではなく 2 つの整数 r と s になります。これらの整数を、ビッグエンディアン⽅式で 32 バイト列に変換してから連結し、1 つの 64 バイトの署名を作成します。

export default function jwtEncode(header, payload, privateKey) {if(typeof header !== 'object' || typeof payload !== 'object') {throw new Error('header and payload must be objects');}header.alg = 'ES256';
const encHeader = b64(JSON.stringify(header));
const encPayload = b64(JSON.stringify(payload));const jwtUnprotected = `${encHeader}.${encPayload}`;const ecSignature = sign(privateKey, sha256,
sha256.hashType, stringToUtf8(jwtUnprotected));const ecR = i2osp(ecSignature.r, 32);
const ecS = i2osp(ecSignature.s, 32);
const signature = b64(Uint8Array.of(...ecR, ...ecS));
return `${jwtUnprotected}.${signature}`;}
このコードは、RSA および HMAC 署名で使⽤したものに似ています。主な違いは、2 つの署名値 r と s を 32 バイ
トのオクテットに変換する点です。これには、RSA でも使⽤した PKCS の i2osp 関数を使⽤できます。
署名を検証するには、パラメーター r と s を取得する必要があります。
export function jwtVerifyAndDecode(jwt, publicKey) {const header = JSON.parse(unb64(split[0]));if(header.alg !== 'ES256') {throw new Error(`Wrong algorithm: ${header.alg}`);}const jwtUnprotected = stringToUtf8(`${split[0]}.${split[1]}`);
const signature = base64.decode(split[2]);const ecR = signature.slice(0, 32);
const ecS = signature.slice(32);const ecSignature = {r: os2ip(ecR),s: os2ip(ecS)};
const valid = verify(publicKey,
sha256,sha256.hashType,jwtUnprotected,
ecSignature);
return {header: header,
payload: JSON.parse(unb64(split[1])),valid: valid};}
ここでも、署名の有効性を検証する⼿順は RSA や HMAC の場合と似ています。この場合、値 r と s は 64 バイトの80

JWT 署名から取得する必要があります。最初の 32 バイトが要素 r で、残りの 32 バイトが要素 s です。これらの値を数値に変換するには、PKCS の os2ip プリミティブを使⽤します。

7.2.3.2.1

楕円曲線のドメイン パラメーター

ECDSA で使⽤される楕円曲線演算は、いくつかの主要パラメーターに依存しています。
 p または q: 算術演算の実⾏対象の素体32 を定義するために使⽤する素数。素体の演算では合同算術を使⽤します33。
 a: 曲線⽅程式の x の係数。
 b: 曲線⽅程式の定数 (y 切⽚)。
 G: 楕円曲線演算のベース ポイントとして使⽤される有効な曲線上の点。ベース ポイントは、曲線上の他の点を得るために算術演算で使⽤されます。
 n: ベース ポイント G の位数。このパラメーターは、点 G をベース ポイントとして使⽤して構築できる曲線内の有効な点の数を表します。
楕円曲線演算を安全に実⾏するには、これらのパラメーターを慎重に選択する必要があります。JWA において、有効と⾒なされる曲線は P-256、P-384、P-521 の 3 つだけです。これらの曲線は、FIPS 186-434 やその他の関連する規格で定義されています。
コード サンプルでは、P-256 曲線を使⽤します。

const p256 = {q: bigInt('00ffffffff00000001000000000000' +'000000000000ffffffffffffffffff' +'ffffff', 16),// order of base point
n: bigInt('115792089210356248762697446949407573529996955224135760342' +'422259061068512044369'),// base point
G: {x: bigInt('6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0' +'f4a13945d898c296', 16),
y: bigInt('4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ece' +'cbb6406837bf51f5', 16)},//a: bigInt(-3)
a: bigInt('00ffffffff00000001000000000000' +'000000000000ffffffffffffffffff' +'fffffc', 16),b: bigInt('5ac635d8aa3a93e7b3ebbd55769886' +'bc651d06b0cc53b0f63bce3c3e27d2' +'604b', 16)};

7.2.3.2.2

公開鍵と秘密鍵

楕円曲線を使⽤した公開鍵と秘密鍵の作成は、⾮常に簡単です。
秘密鍵は、1 からベース ポイント G の位数 n までの範囲から乱数を選択すれば、作成できます。

const privateKey = bigInt.randBetween(1, p256.n);
たったこれだけであり、きわめて簡単です。
公開鍵は秘密鍵から計算でき、ベース ポイント G に秘密鍵を乗算して算出します。

https://ja.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E4%BD%93
https://ja.wikipedia.org/wiki/%E5%90%88%E5%90%8C%E7%AE%97%E8%A1%93
34 http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
32
33

81

// ecMultiply is the elliptic-curve scalar multiplication operation
const publicKey = ecMultiply(G, privateKey);
つまり、公開鍵は楕円曲線上の点であり、秘密鍵は単純にスカラー値です。

7.2.3.2.2.1

離散対数問題

秘密鍵からの公開鍵の導出が簡単であるならば、その逆も簡単なのではないかと思うかもしれません。ここで探す必要があるのは、G に乗算すると公開鍵 Q が得られるような数 d です。

dG≡Q (mod q)logG (Q)≡d (mod q)
図 7.17: 公開鍵の対数として表される秘密鍵
楕円曲線に対して選択した加法群35 (素体など) においては、k の計算は離散対数問題になります。これを効率的に計算できる汎⽤的なアルゴリズムは発⾒されていません。曲線 P-256 に使⽤されるような 256 ビット数を使⽤した場合、複雑すぎるため、現在の計算能⼒では計算不可能です。楕円曲線暗号の強みはそこにあります。

7.2.3.2.3

ES256: P-256 および SHA-256 を使⽤した ECDSA

署名アルゴリズム⾃体はシンプルであり、必要なものは合同算術と楕円曲線演算だけです。
1. 暗号論的に安全なハッシュ関数を使⽤して、署名対象のメッセージのダイジェストを計算します。この数値を eとします。
2. 暗号論的に安全な乱数発⽣器を使⽤して、1 から n - 1 までの範囲内にある数 k を選択します。
3. ベース ポイント Gに k (mod q) を乗算します。
4. 前の⼿順の点の x 座標を G (n) の位数で除算した余りを r とします。
5. r がゼロの場合は、ゼロ以外になるまで⼿順 2 〜 5 を繰り返します。
6. 秘密鍵を d とし、s を次の式の結果とします。

𝑠=𝑑𝑟 + 𝑒(𝑚𝑜𝑑 𝑛)𝑘

図 7.18: s
7. s がゼロの場合は、ゼロ以外になるまで⼿順 2 〜 7 を繰り返します。
署名は r と s のタプルです。JWA では、r と s は連結された 2 つの 32 バイトのオクテット列として表現します(最初に r、次に s)。
実装例を以下に⽰します。

export function sign(privateKey, hashFn, hashType, message) {if(hashType !== hashTypes.sha256) {throw new Error('unsupported hash type');}// Algorithm as described in ANS X9.62-1998, 5.3
const e = bigInt(hashFn(message), 16);
let
let
do

35

r;
s;
{
let k;
do {https://crypto.stackexchange.com/questions/15075/is-the-term-elliptic-curve-discrete-logarithm-problem-a-misnomer82

// Warning: use a secure RNG herek = bigInt.randBetween(1, p256.nMin1);
const point = ecMultiply(p256.G, k, p256.q);
r = point.x.fixedMod(p256.n);} while(r.isZero());
const dr = r.multiply(privateKey.d);const edr = dr.add(e);s = edr.multiply(k.modInv(p256.n)).fixedMod(p256.n);} while(s.isZero());
return {
r: r,
s: s
};
}
検証も簡単です。署名を (r,s) とした場合、次の⼿順を実⾏します。
1. 暗号論的に安全なハッシュ関数を使⽤して、署名対象のメッセージのダイジェストを計算します。この数値を eとします。
2. s を位数 n で除算した余りの逆数を c とします。
3. c を n で除算した余りと e を乗算した値を u1 とします。
4. c を n で除算した余りと r を乗算した値を u2 とします。
5. u1 を q で除算した余りをベース ポイント G に乗算した値を点 A とします。
6. u2 を q で除算した余りを公開鍵 Q に乗算した値を点 B とします。
7. 点 A および点 B (q を法とする) の楕円曲線加算の和を点 C とします。
8. 点 C の x 座標を n で除算した余りを v とします。
9. v が r と等しい場合、署名は有効ですが、等しくない場合は無効です。
実装例を以下に⽰します。

export function verify(publicKey, hashFn, hashType, message, signature) {if(hashType !== hashTypes.sha256) {throw new Error('unsupported hash type');}if(signature.r.compare(1) === -1 || signature.r.compare(p256.nMin1) === 1 ||signature.s.compare(1) === -1 || signature.s.compare(p256.nMin1) === 1){ return false;}// Check whether the public key is a valid curve point
if(!isValidPoint(publicKey.Q)) {return false;}// Algorithm as described in ANS X9.62-1998, 5.4
const e = bigInt(hashFn(message), 16);
const c = signature.s.modInv(p256.n);const u1 = e.multiply(c).fixedMod(p256.n);const u2 = signature.r.multiply(c).fixedMod(p256.n);const pointA = ecMultiply(p256.G, u1, p256.q);
const pointB = ecMultiply(publicKey.Q, u2, p256.q);
const point = ecAdd(pointA, pointB, p256.q);
83

const v = point.x.fixedMod(p256.n);return v.compare(signature.r) === 0;}
このアルゴリズムの重要な部分でよく⾒落とされがちなのは、公開鍵の有効性の検証です。これは過去に攻撃の原因となっています36。メッセージ署名の検証に使⽤される検証者⽤の公開鍵を攻撃者が⾃由に操作でき、かつ公開鍵が曲線上の点として検証されていない場合、攻撃者は特別な公開鍵を作成し、それを使⽤して情報を盗み取ることができます。
JWT の暗号化に使⽤される鍵共有プロトコルを考慮すると、これは特に重要です。
実装例はサンプル リポジトリ37 内の ecdsa.js ファイルにあります。

7.3

今後の更新

JWA 仕様では、他にも多くのアルゴリズムを定義しています。残りのアルゴリズムについては、本書の更新版で取り上げる予定です。

36
37

http://blogs.adobe.com/security/2017/03/critical-vulnerability-uncovered-in-json-encryption.html
https://github.com/auth0/jwt-handbook-samples/84

第 8 章

付録 A: 現在のベスト プラクティス
公開以来、JWT はさまざまな場所で使⽤されてきました。その結果、JWT やライブラリの実装は数々の攻撃にさらされてきました。この種の攻撃については、以前の章でいくつか紹介しました。本章では、JWT の利⽤に関する現在のベスト プラクティスについて説明します。
本章は、IETF OAuth Working Group1 の JWT Best Current Practices2 の草稿に基づいています。使⽤した草案のバージョン
は 00 (2017 年 7 ⽉ 19 ⽇付け) です3。

8.1

隠れた危険とよく⾒られる攻撃

攻撃の説明に進む前に、以下の攻撃の多くが JSON Web Token の設計ではなく実装に関連していることに注意することが重要です。実装に関連するといっても、危険度が下がるわけではありません。以下の攻撃が基礎的な設計を変更することによって軽減または排除できるかどうかは、確かに議論の余地があります。しかし当分の間、JWT の仕様と形式は変わらないため、⼤半の変更は実装の領域で⾏うことになります (ライブラリ、API、プログラミング⼿法、規約の変更)。
また、JWT の最も⼀般的な表現である JWS コンパクト シリアライゼーション形式の基本的な概念を理解しておくことも重要です。シリアライズ前の JWT には、header と payload という 2 つの主な JSON オブジェクトがあります。
header オブジェクトには、トークンの種類、使⽤される署名または暗号化アルゴリズム、鍵 ID など、JWT ⾃体に関する情報が含まれています。
payload オブジェクトには、トークンによって伝えられるすべての重要な情報が含まれています。sub (主題) や iat(発⾏⽇時) のような標準クレームがいくつかありますが、任意のカスタム クレームをペイロードの⼀部として含めることもできます。
これらのオブジェクトは、JWS コンパクト シリアライゼーション形式を使⽤して次のようにエンコードされます。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o

https://tools.ietf.org/wg/oauth/https://tools.ietf.org/wg/oauth/draft-ietf-oauth-jwt-bcp/
3 https://tools.ietf.org/html/draft-ietf-oauth-jwt-bcp-00
1285

これは署名付き JWT です。コンパクト形式の署名付き JWT は単純にヘッダーとペイロードであり、Base64-URL エン
コーディングを使⽤してエンコードされ、ドット (.) で区切られます。コンパクト表現の最後の部分に、署名があります。つまり、次のような形式になっています。

[Base64-URL encoded header].[Base64-URL encoded payload].[Signature]
これは署名付きトークン固有の形式です。暗号化されたトークンには別のコンパクト形式があります。その形式でも、
Base64-URL エンコーディングとドット区切りのフィールドを使⽤します。
JWT の使い⽅を試したり、JWT がどのようにエンコード/デコードされるかを確認したい場合は、JWT.io をご覧ください4。

8.1.1"alg: none" 攻撃

前述したように、JWT には重要な情報を保持する 2 つの JSON オブジェクトである header および payload があります。ヘッダーには、JWT がオブジェクトに含まれるデータを署名または暗号化するために使⽤するアルゴリズムに関する情報が含まれています。署名付き JWT では、ヘッダーとペイロードの両⽅が署名の対象ですが、暗号化された
JWT では、ペイロードだけが暗号化の対象です (ヘッダーは常に読み取り可能である必要があります)。
署名付きトークンの場合、ヘッダーとペイロードは署名によって改ざんから保護されますが、署名を使⽤したり、JWT に含まれるデータを変更したりすることなく、JWT を書き換えることができます。なぜこのようなことが可能なのでしょうか。
次のようなヘッダーとペイロードを持つ JWT を例に考えてみましょう。

header: {alg: "HS256",
typ: "JWT"},
payload: {sub: "joe"role: "user"}
ここで、このトークンを、署名と "secret" という値の署名鍵を含むコンパクト シリアライゼーション形式にエンコード
するとします。これには JWT.io5 を使⽤するとよいでしょう。結果は次のようになります (読みやいように改⾏を挿⼊しています)。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJqb2UiLCJyb2xlIjoidXNlciJ9.
vqf3WzGLAxHW-X7UP-co3bU_lSUdVjF2MKtLtSU1kzU
これを JWT.io6 に貼り付けてみてください。
これは署名付きトークンであるため、⾃由に読み取ることができます。データを少しだけ変えた同様のトークンを作成することもできますが、その場合、署名鍵を知らなければ署名することはできません。署名鍵を知らなくても、攻撃者は何かできるのでしょうか。この種の攻撃では、攻撃者は署名のないトークンを利⽤します。その⽅法を説明しましょう。
まず、攻撃者はトークンを改変します。たとえば、次のように改変します。

header: {alg: "none",typ: "JWT"},
payload: {sub: "joe"role: "admin"
https://jwt.io
https://jwt.io
6 https://jwt.io
4
5

86

}
エンコードすると次のようになります (⾒やすいように改⾏を挿⼊しています)。

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.
eyJzdWIiOiJqb2UiLCJyb2xlIjoiYWRtaW4ifQ.
ご覧のとおり、このトークンには署名が含まれておらず ("alg": "none")、またペイロードの role クレームが変更されています。攻撃者がこのトークンの利⽤に成功すると、特権エスカレーション攻撃を実⾏するおそれがあります。このような攻撃はどのように⾏われるのでしょうか。架空の JWT ライブラリを例に、この攻撃の仕組みを⾒てみましょう。次のようなデコード関数があるとします。

function jwtDecode(token, secret) {// (...)}
この関数は、エンコードされたトークンとシークレットを受け取り、トークンを検証して、デコードされたデータを返します。検証に失敗すると、例外をスローします。この関数は、適切な検証アルゴリズムを選択するために、ヘッダーのalg クレームを利⽤します。攻撃者が狙うはこの箇所です。以前は多くのライブラリが検証アルゴリズムを選択するために、このクレームを利⽤していました7。後はご想像どおりです。先ほど挙げた不正なトークンの alg クレームの値はnone (なし) です。検証アルゴリズムが指定されていないため、検証が常に成功することになります。
これは、仕様⾃体の脆弱性ではなく、特定のライブラリの API の曖昧さを利⽤した古典的な攻撃の例です。それにもかかわらず、これは実際に⾏われた攻撃であり、さまざまな実装で可能なものでした。このため、今⽇の多くのライブラリ
では、署名がない場合も "alg": "none" トークンは無効であると表明しています。この種の攻撃を回避する⽅法は他にもあります。最も重要なのは、トークンの検証を試みる前に、ヘッダーで指定されているアルゴリズムを必ず確認することです。また、alg クレームを利⽤せずに、検証関数への⼊⼒として検証アルゴリズムを要求するライブラリを使⽤する⽅法もあります。

8.1.2

RS256 公開鍵を HS256 共有シークレットとして使⽤する攻撃

この攻撃は "alg": "none" 攻撃に似ており、特定の JWT ライブラリの API の曖昧さを利⽤した攻撃です。今回の例も、先ほどの攻撃で使⽤されるトークンと似ています。ただしこの場合は、署名を削除するのではなく、多くの APIにある抜け⽳を利⽤して、検証ライブラリによって有効とみなされる署名を作成します。まず、検証関数に使⽤される
JWT ライブラリの⼀般的な関数シグネチャを考えてみます。

function jwtDecode(token, secretOrPublicKey) {// (...)}
ご覧のように、この関数は基本的に "alg": "none" 攻撃のものと同じです。検証が成功した場合、トークンはデコードされて返されます。失敗した場合は例外がスローされます。ただし、この関数は 2 番⽬の引数として公開鍵も受け取ります。これはある観点から⾒れば理にかなっていることです。公開鍵と共有シークレットは通常どちらも⽂字列かバイ
ト配列であるため、関数の引数に必要な型の観点から⾒れば、1 つの引数が公開鍵 (RS、ES、または PS アルゴリズムの場合) と共有シークレット (HS アルゴリズムの場合) の両⽅を表すことは考えられます。この種の関数シグニチャは、
多くの JWT ライブラリで⾒られます。
ここで、攻撃者が RSA 鍵ペアで署名されたエンコード済みトークンを⼊⼿したとします。トークンは次のようなものです (⾒やすいように改⾏を挿⼊しています)。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2UiLCJyb2xlIjoidXNlciJ9.
QDjcv11Kcb69THVLKMErYqzy9htWlCDtBdonVR5SX4geZa_R8StjwUuuskveUsdJVgjgXwMso7p
uAJZzoE9LEr9XCxau7SF1ddws4ONiqxSVXZbO0pSgbKm3FpkVz4Jyy4oNTsbIYyE0xf8snFlT1MbBWcG5psnuG04IEle4s
デコードすると次のようになります。

header: {"alg": "RS256",7

https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/87

"typ": "JWT"},
payload: {"sub": "joe","role": "user"}
このトークンは RSA 鍵ペアで署名されています。RSA 署名は秘密鍵で⽣成され、検証は公開鍵で⾏います。トークンの検証者はだれでも、例に挙げた jwtDecode 関数を呼び出すことができます。

const publicKey = '...';
const decoded = jwtDecode(token, publicKey);
しかし、ここで問題が発⽣します。公開鍵は名前のとおり、通常は公開されるものです。攻撃者が公開鍵を⼊⼿する可能性がありますが、それは問題ありません。しかし、攻撃者が次の⼿法で新しいトークンを作成したらどうでしょうか。まず、攻撃者はヘッダーを変更し、署名アルゴリズムとして HS256 を選択します。

header: {"alg": "HS256","typ": "JWT"}
さらに、ペイロード内の role クレームを変更して、権限をエスカレーションします。

payload: {"sub": "joe","role": "admin"}
ここがこの攻撃の核⼼部分です。攻撃者は、単純な⽂字列である公開鍵を HS256 共有シークレットとして使⽤して、新たにエンコードされた JWT を作成します。HS256 の共有シークレットは任意の⽂字列でよいため、RS256 アルゴリズ
ムの公開鍵のような⽂字列でも、共有シークレットとして使⽤できます。
ここで、先ほどの jwtDecode 関数の使⽤に戻ります。

const publicKey = '...';
const decoded = jwtDecode(token, publicKey);
これで問題がはっきりとわかるでしょう。つまり、トークンが有効とみなされてしまうのです。公開鍵は 2 番⽬の引数として jwtDecode 関数に渡されますが、この公開鍵は、RS256 アルゴリズムの公開鍵としてではなく、HS256 アルゴ
リズムの共有シークレットとして使⽤されます。これは、jwtDecode 関数がヘッダーの alg クレームに基づいて JWTの検証アルゴリズムを選択するためです。攻撃者はこのクレームを変更したのは、そのためです。

header: {"alg": "HS256", // <-- changed by the attacker from RS256"typ": "JWT"}"alg": "none" 攻撃の場合と同じように、alg クレームに加えて、不適切な API や仕組みがわかりにくい API を利⽤すると、悪意のあるユーザーによる攻撃を受けるおそれがあります。
この攻撃の対策としては、明⽰的なアルゴリズムを jwtDecode 関数に渡す、alg クレームを確認する、公開鍵アルゴ
リズムと共有シークレット アルゴリズムを分離する API を使⽤することなどが挙げられます。

8.1.3

脆弱な HMAC 鍵

HMAC アルゴリズムは、共有シークレットを利⽤して署名の⽣成と検証を⾏います。共有シークレットはパスワードに似たものだと考えている⼈がいます。確かに、秘密に保管する必要があるという点では似ています。しかし、似ているのはその点だけです。⻑さは重要な特性ですが、パスワードの場合、必要最⼩限の⻑さが他の種類のシークレットと⽐べて⽐較的短くなります。これは、適切な期間、総当たり攻撃を防げるパスワードを (ソルトとともに) 格納するために使⽤されるハッシュ アルゴリズムが原因です。
88⼀⽅、JWT で使⽤される HMAC 共有シークレットは、速度を優先して最適化されています。そのため、多くの署名/検証処理を効率的に実⾏できますが、総当たり攻撃には脆弱です8。そのため、HS256/384/512 の共有シークレットは、⻑さがきわめて重要です。実際に、JSON Web Algorithms9 では、鍵の最⼩⻑が、HMAC アルゴリズムとともに使⽤されるハッシュ関数のビット⻑さと同じに定められています。
"このアルゴリズムでは、ハッシュ出⼒と同じかそれ以上の⻑さ (HS256 の場合は 256 ビット) の鍵を使⽤しなけ
ればならない。" - JSON Web Algorithms (RFC 7518)、3.2 HMAC with SHA-2 Functions10
要するに、他の環境で使⽤できる多くのパスワードも、HMAC 署名付き JWT で使⽤するには不⼗分です。256 ビット
は 32 個の ASCII ⽂字に相当します。そのため、⼈が読めるものを使⽤している場合は、この⻑さをシークレットの最⼩⽂字数とすることをお勧めします。また、より堅牢で柔軟な RS256 やその他の公開鍵アルゴリズムに切り替えるのもよいでしょう。この攻撃は現実の脅威です。共有シークレットが短すぎる場合、HS256 に対する総当たり攻撃は容易に実⾏できることが実証されています11。

8.1.4

暗号化と署名検証の併⽤に関する誤解

署名はデータを改ざんから保護します。つまり、署名はデータを読み取り不可能にするわけではなく、データを変更できないようにします。データを変更すると、署名は無効になります。⼀⽅、暗号化を⾏うと、共有鍵または秘密鍵を知らな
い限り、データは読み取れなくなります。
多くの⽤途では、署名だけで⼗分です。しかし、機密データを扱う場合は暗号化が必要になることがあります。JWT は
署名と暗号化の両⽅をサポートします。
暗号化はあらゆるケースで改ざんに対する保護を実現する、というのは、よくある誤解です。多くの場合、この誤解の根底にあるのは、"データが読み取り不可能なら攻撃者が⾃分の都合のいいように改変できるはずがない" という認識です。
残念ながらこれは、暗号化/復号アルゴリズムに関する攻撃者の知識を過⼩評価しています。
⼀部の暗号化/復号アルゴリズムは、渡されたデータの有効性に関係なく、出⼒を⽣成します。つまり、暗号化されたデー
タが改変されている場合でも、何らかの出⼒が復号処理によって⽣じます。何の考えもなくデータを変えただけでは、無意味なデータしか出⼒されませんが、悪意のある攻撃者はそれすらも利⽤してシステムにアクセスできます。たとえば、
次のような JWT ペイロードを考えてみます。

{"sub": "joe","admin": false}
ご覧のように、admin クレームはブール型です。攻撃者が復号されたデータに変更を加え、ブール値を反転させることができれば、特権エスカレーション攻撃を実⾏できます。特に、攻撃者に⼗分な時間的余裕がある場合は、暗号化されたデータに対して必要なだけ変更を試みることができ、トークンが処理前に無効としてシステムに廃棄されることを避けられます。他にも、サニタイズされたデータが必要なサブシステムに無効なデータを送信してバグや障害を引き起こす攻撃や、他の種類の攻撃を準備する⾜がかりとなる攻撃などがあります。

このような理由から、JSON Web Algorithms12 では、データ完全性の検証も⾏う暗号化アルゴリズムのみを定義しています。つまり、使⽤する暗号化アルゴリズムが JWA で認められているアルゴリズムであれば13、署名付き JWT の上に暗号化された JWT を積み重ねる必要はありません。ただし、標準以外のアルゴリズムを使⽤して JWT を暗号化する場合
は、そのアルゴリズムによってデータ完全性を確保できることを確認する必要があります。または、データ完全性を保証するために、署名付き JWT を最も内側の JWT として使⽤して JWT をネストする必要があります。
ネストされた JWT14 は、仕様で明⽰的に定義およびサポートされています。あまり多くは⾒られないものの、ネストさhttps://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/
https://tools.ietf.org/html/rfc7518
10 https://tools.ietf.org/html/rfc7518#section-3.2
11 https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/
12 https://tools.ietf.org/html/rfc7518
13 https://tools.ietf.org/html/rfc7518
14 https://tools.ietf.org/html/rfc7519#section-2
8989

れた JWT は他のシナリオでも使⽤されることがあります。たとえば、JWT を使⽤する第三者のシステムを介して当事者が発⾏したトークンを送信する場合などです。
このようなシナリオでは、ネストされた JWT の検証に関する間違いがよく⾒られます。データ完全性を維持し、データを適切にデコードするには、JWT のすべての層が、ヘッダーで定義されているアルゴリズムに関連するすべての検証をパスする必要があります。つまり、最も外側の JWT を復号および検証できる場合でも、最も内側の JWT まですべて検証 (または復号) する必要があります。これを⾏わないと、特に最も内側の署名付き JWT が最も外側の暗号化された
JWT で伝達される場合に、未検証のデータが使⽤され、そのデータに関連するセキュリティ問題が発⽣するおそれがあります。

有効なトークン

無効な署名
無効な署名

無効なトークン

無効なトークン

図 8.1: ネストされた JWT の検証

8.1.5

無効な楕円曲線による攻撃

楕円曲線暗号は、JSON Web Algorithms15 でサポートされている公開鍵アルゴリズムの⼀種です。楕円曲線暗号は、楕円
曲線離散対数問題の難しさを利⽤した暗号です。これは使⽤する数値が⼤きいと、現実的な時間内に解くことができない数学問題です。この問題を利⽤することで、公開鍵、暗号化されたメッセージ、メッセージの平⽂からの秘密鍵の復元を防⽌できます。JSON Web Algorithms でサポートされているもう 1 つの公開鍵アルゴリズムである RSA と⽐べると、
楕円曲線暗号はより⼩さな鍵で同程度の強度を実現できます。
暗号処理に必要な楕円曲線は、有限体上で定義されます。⾔い換えると、楕円曲線は (すべての実数ではなく) 不連続な数の集合をもとに演算されます。そのため、楕円曲線に基づく暗号処理で扱われる数は、すべて整数です。
楕円曲線の数学的演算の結果は、すべて曲線上の有効な点を表します。つまり、楕円曲線の計算は無効な点が発⽣しない
ように定義されます。無効な点が作成されると、演算への⼊⼒のエラーが⽣じます。楕円曲線の主な算術演算を以下に⽰します。

• 点の加算: 同⼀曲線上の 2 つの点を加算し、その曲線上に 3 番⽬の点を作成します。
• 点の 2 倍算: 点⾃⾝に対して点を加算し、同⼀曲線上に新しい点を作成します。
• スカラー倍算: 曲線上の 1 つの点にスカラー値を乗算します。つまり、その点を k 回繰り返し加算します (kはスカラー値)。
楕円曲線に基づく暗号処理は、すべてこれらの算術演算を利⽤して⾏います。しかし、⼀部の実装例では、演算への⼊⼒を検証できていません。楕円曲線暗号において、公開鍵は楕円曲線上の点を表します。⼀⽅、秘密鍵は特殊かつ⾮常に⼤きな範囲内にある単純な数値です。演算への⼊⼒が検証されない場合、実際は有効でなくても、有効に⾒える結果が⽣成される可能性があります。このような結果を復号などの暗号処理などで使⽤して、秘密鍵を回復させることができます。
この種の攻撃は過去に実証されており16、無効曲線攻撃と呼ばれています17。優れた実装では、公開関数に渡されるすべての⼊⼒の有効性を常に確認します。その際には、公開鍵が選択した曲線の有効な楕円曲線上の点を表すことと、秘密鍵が有効な値の範囲内にあることも確認します。

https://tools.ietf.org/html/rfc7518
http://blogs.adobe.com/security/2017/03/critical-vulnerability-uncovered-in-json-encryption.html
17 http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.107.3920&rep=rep1&type=pdf
15
16

90

8.1.6置換攻撃

置換攻撃は、攻撃者が 2 つ以上の異なるトークンを傍受しようと試みる攻撃の⼀種です。攻撃者は傍受したトークンの⼀⽅または両⽅を、所定の⽬的以外の⽬的で使⽤しようとします。
置換攻撃には 2 種類あり、受信者が同じ場合 (RFC の草案で Cross-JWT Confusion と呼ばれているもの) と受信者が異なる場合があります。

8.1.6.1

受信者が異なる場合

受信者が異なる置換攻撃では、ある受信者⽤のトークンを別の受信者に送信する⼿法を使います。第三者のサービス⽤のトークンを発⾏する認可サーバーがあるとします。認可トークンは次のペイロードを持つ署名付き JWT です。

{"sub": "joe","role": "admin"}
このトークンは認証処理を実⾏する API に対して使⽤できます。また、ユーザー joe は、少なくともこのサービスに関しては管理者レベルの権限を持っています。しかし、このトークンには問題があります。対象の受信者だけでなく発⾏者も指定されていないからです。ここでこのトークンの対象受信者ではない別の API が、有効性の検証⼿段として署名のみを使⽤していたとしたらどうなるでしょうか。そのサービスまたは API のデータベースに joe というユーザーが存在する場合、攻撃者はこの別のサービスにこのトークンを送信することで、即座に管理者権限を得ることができます。
送信

秘密署名鍵

公開署名鍵
使⽤

使⽤

使⽤

認可サーバー

組織 1

通常の使⽤

の
⽤ ⾏
1 を発
織 ン
組 ク
ー
ト

有

効 組
な 織
ト 1
ー ⽤
ク の
ン
を
取

得

91

組織 2

⽤
使

不

な
正

図 8.2: 受信者が異なる JWT 置換攻撃

この攻撃を防ぐには、サービスごとの⼀意の鍵またはシークレットか、特定のクレームのどちらかを利⽤してトークンを検証する必要があります。たとえば、対象受信者を指定する aud クレームをこのトークンに含めれば、署名が有効であっても、同じ秘密鍵または署名鍵を使⽤する他のサービスでこのトークンを使⽤できなくなります。

8.1.6.2

受信者が同じ場合/Cross JWT

この攻撃は前の攻撃と似ていますが、別の受信者に対して発⾏されたトークンは利⽤しません。この場合、受信者は同⼀です。この攻撃では、攻撃者は本来のサービスとは異なる (同じ会社やサービス プロバイダー内の) 別のサービスにトークンを送信します。
次のペイロードを持つトークンがあるとしましょう。

{"sub": "joe","perms": "write","aud": "cool-company/user-database","iss": "cool-company"}
このトークンは先ほどのものよりもはるかに安全に⾒えます。発⾏者 (iss) クレーム、受信者 (aud) クレーム、権限(perms) クレームが含まれています。このトークンの発⾏対象の API は、トークンの署名が有効であっても、これらのクレームをすべて確認します。そのため、攻撃者が同じ秘密鍵またはシークレットで署名されたトークンを⼊⼿したとしても、そのトークンがそのサービスを対象としたものでなければ、それを使⽤してこのサービスを操作することはできません。
しかし、cool-company には他にも公開しているサービスがあります。そのうちの 1 つのサービスである coolcompany/item-database では、最近のアップグレードで、トークンの署名とともにクレームを確認するようになりました。しかし、アップグレード時に検証対象のクレームを選択する担当者がミスを犯し、aud クレームが正しく検証されませんでした。担当者は完全に⼀致するかどうか確認せず、cool-company という⽂字列があるかどうかしか検証
しませんでした。同社の別のサービスである cool-company/user-database では、この検証をパスするトークンを発⾏しています。つまり、攻撃者は item-database サービス⽤のトークンの代わりに、user-database サービス⽤のトークンを使⽤できます。そのため、攻撃者は user-database への書き込み権限しか持っていなくても、itemdatabase への書き込み権限を得ることができます。

92秘密署名鍵

公開署名鍵
使⽤

使⽤

使⽤

メール サーバーから有効なトークンを取得
メール サーバー

不

⽤
使

の
常

通

送信

正
な
使
⽤

メール サーバー⽤のトークンを発⾏

財務部⾨

図 8.3: 受信者が同じ JWT 置換攻撃•

8.2

対策とベストプラクティス

ここまでは JWT を使⽤した⼀般的な攻撃を⾒てきました。次は、最新のベスト プラクティスを⾒ていきましょう。前述の攻撃は、いずれも以下の推奨事項を実⾏することでうまく防ぐことができます。

8.2.1

アルゴリズムの検証を常に実⾏する"alg": "none" 攻撃や、RS256 公開鍵を HS256 共有シークレットとして使⽤する攻撃は、この対策で防⽌できます。
攻撃者に権限を奪取されないように、JWT を検証する際は、常にアルゴリズムを明⽰的に選択する必要があります。従来のライブラリでは検証⽤のアルゴリズムを選択する際に、ヘッダーの alg クレームを利⽤してきました。前述のような攻撃が実際に⾒られるようになってからは18、多くのライブラリで、ヘッダーで指定されている内容を無視し、検証⽤のアルゴリズムを明⽰的に指定する⽅法が提供されるようになりました。それでもやはり、⼀部のライブラリではヘッ
18

https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/93ダーで指定されたアルゴリズムを使⽤することができます。そのため、開発者は常に明⽰的にアルゴリズムを選択するように注意する必要があります。

8.2.2

適切なアルゴリズムを使⽤する

JSON Web Algorithms の仕様では、推奨アルゴリズムと必須アルゴリズムが⽰されていますが、個々のシナリオに合わせて適切なアルゴリズムを選択することは、依然としてユーザーの責任となっています。たとえば、1 台のサーバー上の単⼀ページの Web アプリケーションから発⾏された⼩さなトークンをユーザーのブラウザーに保存する場合は、HMAC
署名で署名された JWT を使うだけで⼗分でしょう。⼀⽅、共有シークレットのアルゴリズムは、フェデレーション ア
イデンティティを使⽤する場合は⾮常に不便です。
また、これは考え⽅を変えれば、JWT の検証アルゴリズムがアプリケーションで受け⼊れられるものでない限り、すべての JWT を無効とみなすということです。つまり、検証者がトークンの検証に必要な鍵と⼿段を持っていたとしても、検証アルゴリズムがアプリケーションにとって適切なものでない場合には、無効であるとみなすということです。これは、前述の推奨事項である "アルゴリズムの検証を常に実⾏する" ことと同じです。

8.2.3

常にすべての検証を実⾏する

ネストされたトークンの場合は、各トークンのヘッダーで宣⾔されているすべての検証⼿順を常に実⾏する必要があります。つまり、最も外側のトークンを復号または検証するだけでは不⼗分であり、内側のトークンの検証を省略すべきではありません。署名付き JWT のみ使⽤する場合も、すべての署名を検証する必要があります。不⼗分な検証に起因する
誤りは、外部から発⾏された別の JWT を伝達するために JWT を使⽤するアプリケーションでよく⾒られます。

8.2.4

暗号の⼊⼒を必ず検証する

攻撃に関する節で説明したように、⼀部の暗号処理は、その処理の範囲外の⼊⼒に対して⼗分に定義されていない場合があります。無効な⼊⼒が悪⽤されると予期しない結果が⽣成されたり、完全な侵害の⼿がかりとなる機密情報 (攻撃者が秘密鍵を⼊⼿した場合) が抽出されたりすることがあります。
先に例として挙げた楕円曲線演算では、公開鍵を使⽤する前に、必ず公開鍵を検証する必要があります (つまり、公開鍵が選択した曲線上の有効な点を表していることを確認する必要があります)。通常、この種の検証は基礎的な暗号ライブ
ラリによって処理します。開発者は、選択したライブラリでこの種の検証が実⾏されることを確認するか、アプリケー
ション レベルで検証を実⾏するために必要なコードを追加する必要があります。さもないと、秘密鍵が侵害されるおそれがあります。

8.2.5

強⼒な鍵を選択する

この推奨事項はすべての暗号鍵に該当するものであるにもかかわらず、多くの場合、無視されています。前述のように、HMAC 共有シークレットの最⼩⻑は⾒落とされがちです。しかし、共有シークレットは⼗分に⻑いだけでなく、完全にランダムなものでなければなりません。⻑い鍵でも、ランダム性のレベル (エントロピー) が低いと、総当たり攻撃や推測が可能になります。これを防ぐには、初期化の際に適切にシードが設定される暗号⽤途に適した品質の擬似乱数⽣成器(PRNG) を鍵⽣成ライブラリで使⽤する必要があります。可能であればハードウェアの乱数⽣成器を使⽤することをお勧めします。
この推奨事項は、共有鍵アルゴリズムと公開鍵アルゴリズムの両⽅に適⽤されます。さらに、共有鍵アルゴリズムの場
合、⼈が読めるパスワードでは安全性が低いと考えられ、辞書攻撃に対して脆弱です。

8.2.6

使⽤可能なすべてのクレームを検証する

94

先ほど説明した攻撃の中には、検証に関する誤った思い込みを突いたものもありました。そのような攻撃は、とりわけ検証の唯⼀の⼿段である署名検証や復号を利⽤します。また、正しく署名されたトークンや暗号化されたトークンにアクセ
スして、想定外の状況で不正な⽬的でトークンを使⽤する攻撃者も存在します。このような攻撃を防ぐには、署名と内容の両⽅が有効な場合にのみトークンを有効とみなすことが重要です。このため、sub (主題)、exp (有効期限)、iat (発⾏⽇時)、aud (受信者)、iss (発⾏者)、nbf (開始⽇時) などのクレームは最も重要であり、これらが存在する場合は必ず有効性を確認する必要があります。トークンを作成する場合は、別のコンテキストでの使⽤を防ぐために必要なだけの数のクレームを追加することをお勧めします。⼀般に、sub、iss、aud、exp は常に役に⽴つため、必ず含めることをお勧
めします。

8.2.7

typ クレームを使⽤してトークンの種類を区別する

ほとんどの場合、typ クレームの値は 1 つ (JWT) ですが、このクレームはさまざまな種類のアプリケーション固有の
JWT を区別するためにも使⽤できます。これは、システムでさまざまな種類のトークンを処理する必要がある場合に便利です。また、このクレームを使⽤して追加のクレーム検証を⾏うことで、別のコンテキストでのトークンの不正使⽤を防ぐこともできます。JWS 標準では、アプリケーション固有の typ クレームの値が明⽰的に許可されています。

8.2.8

トークンごとに異なる検証ルールを使⽤する

このプラクティスは、これまでに挙げたものの要点をまとめたものです。攻撃を防ぐためには、⾮常に明確で固有の検証
ルールを、発⾏する各トークンに設定することが重要です。そのためには、適切な場合に typ クレームを使⽤したり、iss や aud などの使⽤可能なすべてのクレームを検証したりするだけでなく、可能な場合にはトークン間の鍵の再利⽤を避けたり、さまざまなカスタム クレームやクレーム形式を活⽤したりする必要があります。そうすることで、1 つの場所で使⽤するよう設計されたトークンを要件が⾮常によく似た別のトークンで置き換えることが不可能になります。
別の⾔い⽅をすれば、すべての種類のトークンに同じ秘密鍵を使⽤するのではなく、アーキテクチャのサブシステムごとに異なる秘密鍵を使⽤することが重要です。また、⼀定の内部形式をクレームに指定することでクレームを特徴付けることもできます。たとえば、iss クレームの値を企業名ではなくそのトークンを発⾏したサブシステムの URL にすれば、トークンの再利⽤が困難になります。

8.3

まとめ

JSON Web Token は暗号を利⽤したツールです。類似するツール、特に機密情報の処理に使⽤するツールと同様に、慎重に使⽤する必要があります。⼀⾒シンプルに⾒えるため、正しい共有シークレットや公開鍵アルゴリズムを選ぶだけでJWT を使⽤できると勘違いしがちですが、これまで説明したように、残念ながらそれは間違いです。どのようなツールを利⽤する場合も、それぞれのベスト プラクティスに従うことが最も重要であり、JWT も例外ではありません。特に、実地でテストされた⾼品質のライブラリを選択すること、ペイロードとヘッダーのクレームを検証すること、適切なアルゴリズムを選択すること、強⼒な鍵が⽣成されるか確認すること、各 API の細かな特徴に注意することが重要です。これらをすべて実⾏することが難しい場合は、外部のサービス プロバイダーを利⽤して負担を軽減することをお勧めしま
す。Auth019 ならそのようなニーズにお応えすることができます。独⼒で実⾏するのであれば、本書で説明した推奨事項
を⼗分にご検討ください。また、独⾃のセキュリティ⽅式を開発するのではなく20、テスト済みのコードを利⽤することをお勧めします。

1920

https://auth0.com
https://security.stackexchange.com/questions/18197/why-shouldnt-we-roll-our-own

95