5. Proof of Possession

このビデオについて

このビデオは、2020 年 4 月 22 日に開催した弊社勉強会プレゼンテーション録画のパート 5 です。 Proof of Possession (PoP) のしくみと、PoP の仕様である MTLS (RFC 8705) と DPoP について、 Authlete の川崎貴彦がお話しします。

文字起こし(ログを元に再構成)

Proof of Possetion (PoP) とは

Proof of Possession の話をします。 これはけっこう重要なトピックです。

まず、電車の切符の話をします。 自分が落としてしまった電車の切符を誰かが拾った場合、 その拾った人がそのまま、自分に代わって切符を使えてしまいます。

一方、国際線の航空券の場合、自分がその航空券を落としてしまって、 誰かが拾ったとしても、 それは使えません。 使うときに、チケットが本来の所有者のものかどうかの再確認を求められるからです。 よって、盗んでも使えません。

従来のアクセストークンは、電車の切符みたいなものです。 一度盗まれると、悪い人にアクセストークンを使われてしまいます。

そういう状況はあまりよろしくないということで、 アクセストークンを使うときにも、 再度、そのクライアントがそのアクセストークンを使える人かどうかを確認すれば、 アクセストークンの不正利用を減らせるよね、 というアイディアが生まれました。

PoP の 2 つの手法

アクセストークンの正当な所有者であるという証明のことを、 Proof of Possession と言います。PoP と略されたりします。

この手法がいくつか考えられています。 いま生き残ってる仕様のうちの 2 つがこれです。

ひとつは RFC 8705 になった、 OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens、 MTLS と略されたりします。 ぼくはよく Certificate Binding と呼んだりします。

あとは DPoP という仕様があります。 これは RFC にはまだなってはいないんですけれども、 アプリケーションレイヤーで PoP を実現する手法です。

基本的に MTLS を使ったほうがいいと思いますが、 それを使えないようなケース、 Web ブラウザ内で動くシングルページアプリケーションとかの場合、 DPoP という方法で PoP を実現します。

RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate Bound Access Tokens

Certificate Bound Access Token、RFC 8705 の方法が、 どのように実現されているかは、こんな感じです。

トークンリクエスト (MTLS)

クライアントがトークンリクエストを認可サーバーに投げます。

トークンリクエストの TLS コネクションを Mutual TLS で実現する場合、 認可サーバーのトークンエンドポイントは、クライアントに、 クライアント証明書を提示しなさいよと要求します。

クライアントはクライアント証明書を、 TLS コネクションに入れます。

その結果、認可サーバー側にクライアント証明書が渡ってきます。 このクライアント証明書を使います。

認可サーバーはアクセストークンを作って、そのアクセストークンと、 トークンリクエストのときに使ったクライアント証明書をひもづけます。

ひもづけて覚えた上で、 アクセストークンをクライアントに発行します。

API コール (MTLS)

クライアントはこのアクセストークンを API コールに使います。 API コールにアクセストークンを含めます。

この API コールが Mutual TLS の場合、 トークンリクエストのときに使用したのと同じクライアント証明書を、 その API コールのときにも使用します。

こうすると、リソースサーバー側にも、 アクセストークンとクライアント証明書の、 両方が渡ります。

この 2 つを受け取ったリソースサーバーは、 クライアント証明書とアクセストークンとのひもづけが、 正しいかどうかをチェックします。

ここで何を確認するかというと、 アクセストークンの発行を受けたクライアントが、 その API コールをしているクライアントと同一かどうかです。

たとえば、アクセストークンを誰か悪い人が盗んでしまっても、 トークンリクエストのときに使ったクライアント証明書(鍵ペア)を持ってないと、 その API コールは成功しません。

アクセストークンとクライアント証明書(鍵ペア)の両方が盗まれてしまうと、 このしくみは動きませんが、それはちょっとハードルが高いです。

アクセストークンとクライアント証明書のひもづけ

アクセストークンとクライアント証明書をひもづけるという話が出てきました。 具体的にどうひもづけるか、という話です。

これは、クライアント証明書のハッシュ値を、 アクセストークンにひもづけるかたちになります。

このハッシュ値は、証明書のフィンガープリントと呼ばれているものです。 X.509 証明書 DER 表現の SHA-256 ハッシュを取ったものです。

この Certificate Bound Access Tokens をサポートする認可サーバーは、 イントロスペクションレスポンスや、 JWT アクセストークンのペイロード部分に、 ハッシュ値を BASE64URL で表現した値を含めます。 それを cnf クレームの下の x5t#S256 という値として含めます。

これがどういう風に見えるかというと、 これは仕様書に書かれているイントロスペクションレスポンスの例です。

アクセストークンの情報を返しています。 アクセストークンがアクティブかどうかとか、 サブジェクトは何か、という情報があります。

それと同時に cnf クレームがあって、そのさらに下に x5t#S256 があります。 x5t#S256 は、 そのアクセストークンにひもづいているクライアント証明書の、 フィンガープリントを表現しています。

リソースサーバーがイントロスペクションして、 ハッシュ値をゲットして、その値が、 受け取ってるクライアント証明書のハッシュと等しいかどうかを確認します。

OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP)

次に、別のもうひとつの PoP の手法です。 DPoP の話です。

鍵ペアの生成

この場合は何をするかというと、 まずクライアントは、 公開鍵と秘密鍵のペアを作ります。

次に、その公開鍵を含むヘッダーと、 トークンエンドポイントにリクエストするためのペイロードを用意します。

この用意したデータに対して秘密鍵を用いて署名します。 3 つできたのを JWT 形式で包みます。 これを DPoP proof JWT と呼びます。

トークンリクエスト (DPoP)

この JWT を用意した上でトークンリクエストを行います。

そのトークンリクエストに DPoP proof JWT を含めます。

トークンエンドポイント実装は、 トークンリクエストの中から、 DPoP proof JWT を抽出します。

抽出し、そこに含まれている公開鍵で、署名を検証します。 署名を検証するとはどういうことかというと、 公開鍵に対応する秘密鍵をクライアントが持っているかの確認です。

確認した上でアクセストークンを生成し、 その後にその生成したアクセストークンと、 その公開鍵をひもづけます。

このひもづけを覚えておいて、 アクセストークンをクライアントに発行します。

API コール (DPoP)

クライアントはアクセストークンを使って、 API にアクセスしたいんですけど、 その前にちょっと一手間かかります。

まず、さっき作った公開鍵です。 公開鍵を含むヘッダーと、ペイロードを用意します。 ペイロードは、そのリソースサーバーの API にアクセスするための、 専用のものです。

これに対して、さっき作った秘密鍵で署名します。 この署名を JWT 形式で包んで、DPoP proof JWT になります。

この状態にして API コールします。 その API コールにはアクセストークンと DPoP proof JWT を含めます。

API(リソースサーバー)の実装は、 まずリクエストの中から、 アクセストークンと DPoP proof JWT を取り出します。

そして、含まれている公開鍵で署名を検証します。 本当に公開鍵に対応する秘密鍵を、 クライアント(API コーラー)が持っているかどうかの確認ができます。

それを確認した上で、 公開鍵とアクセストークンとのひもづけをチェックします。 このアクセストークンにひもづいているべき公開鍵が、 提示された公開鍵と一緒であれば、 OK というしくみです。

この確認によって、 アクセストークンの発行を受けたクライアントと、 API をコールしているクライアントが、 同一であると確認できる、という手順になっています。

アクセストークンと公開鍵のひもづけ

アクセストークンと公開鍵をひもづけると書いてあります。 具体的には、公開鍵のハッシュ値をアクセストークンとひもづけます。

では、そのハッシュ値とは何なのか。 これは JWK SHA-256 Thumbprint です。 RFC 7638 という仕様で決められている、 フィンガープリント(サムプリント)です。

DPoP をサポートする認可サーバーは、 イントロスペクションレスポンスや、 JWT アクセストークンのペイロード部分に、 ハッシュ値を含めます。

具体的にはこのように含めます。 アクセストークンが JWT 形式の場合、 そのペイロード部分に cnf クレーム、 さらにその下に jkt クレームを置いて、 そこに公開鍵のハッシュ値を書いておきます。

DPoP proof JWT の生成

これが DPoP proof JWT のヘッダー部分とペイロードの例です。 上がヘッダー、下がペイロードです。

ヘッダーを見ると、まず typ(タイプ)です。 これは固定文字列(dpop+jwt)を入れてください、と仕様で決まっています。

次に署名アルゴリズムです。 ここでは ES256 になっています。 アルゴリズムは非対称鍵系のみ、というルールです。

次に jwk というフィールドがあります。 この中に情報がいろいろ書かれています。これが公開鍵です。 ヘッダー部に公開鍵を含める、とはこういうことです。

つぎにペイロード部分です。 jti があります。

htm という特別なものがあります。 これがリクエストの HTTP メソッドです。 たとえばトークンエンドポイントにアクセスする場合、 必ず POST になるので、 htm は POST になります。 一方、リソースサーバーの API を呼ぶとき、 API に GET リクエストでアクセスする場合は、 ここが GET という文字列になります。

次に、htu は、リクエストの URL です。 たとえばトークンリクエストの場合は、トークンエンドポイントの URL が入ります。 リソースサーバーの API にアクセスする場合には、その API の URL が入ります。

あと iat です。発行日時が入ります。

これらを DPoP proof JWT のヘッダーとペイロードとして用意し、 署名します。

DPoP proof JWT の利用

DPoP proof JWT を伴うトークンリクエストは次のようになります。

DPoP proof JWT をそのトークンリクエストに含めなくてはなりません。 どうやるかというと、DPoP という HTTP ヘッダーに DPoP proof JWT を含めます。 あとはいままでのトークンリクエストと一緒です。

認可サーバーによっては、DPoP ヘッダーがリクエストに含まれていても、 全く認識しないケースもあります。

DPoP をサポートする認可サーバーの場合は、それを解釈し、 トークンレスポンスに含まれる token_type の値が DPoP になります。 Bearer とかではなく、DPoP です。 クライアントは、レスポンスの中の値から、 その認可サーバーが DPoP をサポートしているとわかります。

今度は DPoP proof JWT を使うリソースアクセスの例です。 このようになります。

Authorization: DPoP の後に続くのは、 DPoP-bound なアクセストークンです。

下が DPoP ヘッダーです。 これは DPoP proof JWT です。 トークンリクエストと同様に、API コールのときも、 DPoP proof JWT を含めなくてはなりません。