Blog

2016.12.20

Token Bindingについて

こんにちは、エンジニアの前田です。この記事は、 HTTP2 Advent Calendar 2016 - Qiita の20日目の記事です。

IETFのtokbind Working GroupではToken Bindingというプロトコルについて検討しています。そろそろコア文書がWGLCに入り、仕様策定の最終段階になってきました。ここでToken Bindingとは何かについて書いてみたいと思います。

Bearer TokenとProof-of-possession Token

OAuth 2.0やOpenID Connect (以降OIDC)で発行されるaccess tokenについてはみなさんもうだいぶ慣れたことでしょう。このような「トークン」はOAuth 2.0やOIDCでは「Bearer Tokenと言い、このトークンを持ってきた人ならば誰でもサービスを受けられるものです。一方、特定の者に対して発行し、その同じ人が持ってこないと使えないようなトークンも世の中にはあります。これらのトークンを「Proof-of-possession Token」や「Holder-of-key Token」と呼びます。実世界のわかりやすい例としては、Bearer Tokenの例は、バスの持参人式定期券や、「ハンバーガー無料券」などがあります。一方Proof-of-possession Tokenの例としては、飛行機の搭乗券やスーパーの「ポイント2倍券」などがあります。これらは本人であることが確認できるもの(パスポートやポイントカード)を提示して、認証が成立した場合にのみ、サービスが受けられます。

Bearer Tokenは使いやすいですが、何らかの原因でトークンが漏洩すると、それを不正に入手した攻撃者でも使うことができてしまいます。これでは不都合がある場合、トークンの発行先と持参者が同一であることが確認できるようなトークンを使いたくなります。これを暗号論的に通信路にひもづけようと(bindしようと)いうのがToken Bindingのキモです。

TLSを使ったToken Binding

サーバーからクライアントにトークンを発行する際、サーバーで検証可能なIDをクライアントが用意し、トークンをそのIDに限って使えるようにします。これには、トークン内にID(またはそのハッシュ)を書き込んでおいたり、バックエンドのデータベースでトークンとIDをひもづけるなどの方法があるでしょう。クライアントは、トークン使用時に、トークンとともに、自分のIDと、自分が確かにそのIDに対応するという証拠を示します。サーバーがそれを検証し、トークンにひもづけられた(bindされた)IDと、いま提示されたIDが同一であること、そしてクライアントがそのIDの実体であることが検証できた場合、サービスを提供します。

IETFのToken Bindingプロトコルでは、クライアントが公開鍵と秘密鍵の暗号鍵ペアを生成し、その公開鍵をIDとして使います。検証の方法としては何らかのチャレンジ(サーバーとクライアント両方が知っているデータ)を用意し、クライアントがそれをIDに対応する秘密鍵を使って署名します。サーバーでは公開鍵とチャレンジを使って署名を検証することで、クライアントが確かにIDで示される実体であること(==IDに対応する秘密鍵を持っていること)がわかります。他のプロトコルでは、サーバーからクライアントにチャレンジを渡す場合もありますが、それだと、「サーバーからチャレンジを渡す」「クライアントからチャレンジを署名して返す」という1往復が必要になってしまいます。Token BindingプロトコルではチャレンジとしてTLS通信路に由来する秘密情報(master secret)を使います。TLSのハンドシェイクによってサーバー/クライアントの間で生成される情報であり、この両者が追加の送信の必要なく知っているうえ、他の誰も知り得ない情報なので都合がよいです。実際のプロトコルでは、master secretから導出される暗号論的乱数(Exported Key Material、EKM、RFC5705で規定)を使います。

Token Bindingプロトコルでは、クライアントによるID(以降Token Binding ID、TBIDと呼びます)のフォーマット、Token Bindingを使うかどうか合意するためのネゴシエーション、そしてHTTPリクエストでの署名の渡し方と検証ルールなどを定めています。IETFでは、現在これらを定めた3つのドラフトがWGLCになっており、数か月以内には仕様が標準になると思われます(以降、この略称でこれらのドラフトを参照します)。

Token Bindingとトークン発行から使用までの流れ

それではToken Bindingプロトコルでのトークン発行から使用までの流れを見てみます。

Token Binding ID

まず、クライアント(主にブラウザ)はユーザーごと、行先サーバーごとにTBIDを生成します。これは公開鍵と秘密鍵のペアとして生成し、その秘密鍵はセキュアに保存しておきます。サーバーごとと書きましたが、HTTPSTBではeTLD+1に対応したスコープで鍵を作ることが定められています。example.comやexample.co.jpが対応します。この様子を図1に示します。

図1: Token Binding ID
図1: Token Binding ID

これらの鍵ペアの暗号方式と公開鍵を連結し、TBIDとします。同一のブラウザやデバイスでも、接続先のサーバーが違うとTBIDは別になります(同じだと、トラッキングできてしまうというプライバシー上の問題になります)。

Token Binding Message

クライアントはサーバーにアクセスする度に、Token Binding Messageを送ります(図2)。

図2: Token Binding Message
図2: Token Binding Message

Token Binding Messageは、TBIDおよび署名を含みます(TBPROTO)。このTBIDはクライアントが「オレのTBIDは○○だからな」と自称しているIDということになります。署名はTBIDおよび現在使用しているTLS接続に由来する秘密情報(EKM)に対して行います。EKMはTLS接続時点でクライアントとサーバーとの間で決まっており、サーバーにとってもすでに知っている情報なので、送信する必要はありません。EKMはTLS接続のたび(TLS 1.2以前においてはリネゴシェーションのたび)に変化しますので、署名は接続ごとに計算し直します(接続が続いている間は値が変化しないので、リクエストごとに計算する必要はありません)。署名は接続ごとに変わりますが、TBIDは同一です。

サーバーは、受信したTBMからTBIDを取り出すことによって、クライアントが自称するTBIDを知ることができます。また、TBIDの中を見ると、署名に使われている暗号方式と公開鍵がわかります。サーバーはこれらの情報と、TLS接続から取り出したEKMを使って署名検証をします。EKMは暗号論的にこれら2者以外の者が知り得ない情報であるので、署名検証が通れば、確かに相手は「いまの通信路の終端にいる者」でありかつ「TBIDに対応する秘密鍵を持っている者」であることが確認できます。つまり、「いまの通信相手は確かにTBIDに対応している」ことがわかります。この時点までで何かおかしなことがあれば、サーバーは不正なアクセスであるとして接続を切ります(暗号方式が違う、TBIDとTBM内の署名が合わない、Token Bindingを使うはずなのに送ってこない、使わないはずなのにTBMを送りつけてきた、など)。

TBMは、HTTPリクエストではヘッダSec-Token-Bindingに入れて送ります(HTTPSTB)。TBMはバイナリ値ですので、Url-safe Base64エンコーディングします。ヘッダ名にSec-がついているので、JavaScriptからこれらのヘッダをいたずらすることはできないようになっています。

Sec-Token-Binding: QWR26DLF02LDSK3DM…

TBMにはタイプの異なる複数のTBIDを入れることができます。現在provided_token_binding、referred_token_bindingという2つのタイプが定義されています(TBPROTO)。providedの方は通常の使い方で、referredの方は認証・認可フェデレーションのときに使います。

トークンの発行とToken Binding

トークン発行時のサーバーの動作を図3に示します。クライアントから何らかのトークン発行の通信が発生した時点で、サーバーはTBMを受信し、検証を終えています。したがって、クライアントのTBIDはわかっています。

サーバーでは発行するトークン(ブラウザクッキーやOAuth 2.0の各種トークン、OIDCのID Tokenなど)と、クライアントのTBIDをひもづけて(= bindして)管理します。ひもづけはトークン内にTBID(のハッシュ)を書き込んだり、バックエンドのデータベースで管理したりという方法があるでしょう。こうやってひもづけたTBIDの持ち主にしか、このトークンは使えない、ということになります。

図3: トークンの発行
図3: トークンの発行

トークンの使用とToken Bindingの検証

クライアントがトークンを使用するときには、必ずTBMがいっしょに来ているはずです。TBM内の署名を検証することで、通信相手が確かにTBIDに対応していることはずでにわかっています。ここで、トークンにひもづけられたTBIDと、いまトークンを提示したリクエストのTBIDを比べて、同一であれば(ハッシュの場合はハッシュが一致すれば)、確かにトークン発行時にひもづけた持ち主が持ってきた、ということがわかります(図4)。一致すればサービスを提供し、そうでなければ不正なリクエストとして拒絶します。

図4: トークンの使用とToken Bindingの検証
図4: トークンの使用とToken Bindingの検証

Token Bindingのユースケースでの動作

Token Bindingを使ったOAuth 2.0やOpenID Connectでの動作がどのようになるか見てみましょう。

OAuth 2.0とToken Bindingの例: Refresh Token

より実際的なアプリケーションとして、OAuth 2.0のrefresh tokenの場合を例に考えてみましょう(図5)。ここではOAuth 2.0のRPが同時にHTTPリクエストのクライアントでもあります。OAuth 2.0におけるToken Bindingの使用方法については、IETFのoauth WGで検討されています(draft-ietf-oauth-token-binding )。

refresh tokenは、OAuth 2.0の仕様上、client_idとclient_secrectを使ったクライアント認証がなければ使えないようになってはいます。しかしもしrefresh tokenが漏洩してしまうと、同一クライアントの別インスタンス(例えば別のデバイスにインストールした同じアプリ)から使えてしまう可能性があります。TBIDはデバイスごとに別になっているので、この問題を解決できます。

図5: OAuth 2.0 Refresh Tokenの場合
図5: OAuth 2.0 Refresh Tokenの場合

refresh tokenの発行は、Authorization codeからaccess tokenを発行する場合や、トークンリフレッシュによって新たなaccess tokenを発行する場合に行われます。このとき、クライアントから認証サーバー(AS)に対して、Token Bindingを使うことができます。

クライアントからはリクエストとともにTBMを送信します(①)。サーバーではTBM内の署名を検証し、TBIDを得ます。発行したrefresh tokenをこのTBIDにひもづけて(②)送り返します(③)。クライアントがrefresh tokenを使うとき、同様に(発行時とは異なるかもしれないTLS接続上で)TBMを送ります(④)。サーバーでは、refresh tokenにひもづいたTBIDと、いま送られてきたTBIDを比較して、確かに以前に渡した相手が持ってきたことを検証することができます。

OpenID ConnectとToken Bindingの例: ID Token

次に、OpenID ConnectでのID Tokenのユースケースを見てみましょう(図6)。ID Tokenの発行時に、正しいクライアントだけがRPに提示できるようにID Tokenを発行できれば、tokenインジェクション攻撃に対する対策となります(他人のID Tokenによって別人のアカウントのセッションにつながれ、自分のクレジットカードや住所を別人のアカウントに設定してしまうかもしれない、という攻撃)。

図6: OpenID Connect ID Tokenの場合
図6: OpenID Connect ID Tokenの場合

この場合は、少し図式が複雑になります。ブラウザ(クライアント)はIdPからID Tokenを受けとり、それをRPに送ります。RPではID Token内に書き込まれたクライアントのTBIDと、現在の通信路のIDを検証して、正しい使用者であることを確認します。

ここで、TBIDがサーバーごとに別のものであったことを思い出しましょう。クライアントはIdP行きとRP行きの2つのTBIDを持っています。IdPにトークン発行要求を出すときには、IdP用のTBIDを使って身元を確認します。一方で、ID TokenをRPに提示するときには、RP用のTBIDを使います。発行されるID Tokenは、「RP用のTBID」に対してひもづけられていないと、RPが検証できなくなってしまいます。

このために使うのがreferred token binding IDです。RPはクライアントからのログイン要求(図では省略されています)に対し、「あっちのIdPで認証して、ID Tokenをもらってきてね」と返事します(①)。このときにHTTPリダイレクトが使われますが、そのヘッダに

Include-Referred-Token-Binding-ID: true

というヘッダを追加します。これは、RPからクライアントに対して「このリダイレクトにそってあっちのサーバーに行くときに、オレ用のTBIDを送ってくれ」と依頼していることになります。クライアントはこの要求を受け入れ、IdPへの認証およびID Token発行リクエストを送信するとき、TBMの中に2つのTBID(と署名)を入れて送ります(②)。ひとつはRP行きのTBIDで、IdPが発行するID TokenをこのTBIDにひもづけてもらうために使います(図中の緑色のTBID)。もうひとつはIdP行きのTBIDで、これは自分の認証セッションの身元をIdPが検証するために使います(図中の黄色のTBID、referred-に対してprovided TBIDと呼びます)。

IdPではprovided TBIDを使ってリクエスト元を検証した後、発行したID Tokenの中にreferred TBID(のハッシュ)を入れて送り返します(③)。このとき、IdPはreferred token bindingの署名(IdP行きのEKMに対して署名されている)を見て、確かにこのクライアントがこのreferred TBIDの持ち主であることを検証することができます。

クライアントがID TokenをRPに渡すときには、通常どおり、RP行きのTBIDを(provided TBIDとして)TBMに含めて送ります(④)。RPではこのTBIDを検証し、ID Token内に記録されているTBIDと比較することで、「確かにIdPが発行するときにひもづけたTBIDの持ち主だな」ということがわかります。

ここで、クライアントはRP用のTBIDをIdPに教えることになります。クライアントは、RPとIdPの間で認証情報をやり取りすることは承諾しているので、これは問題がないでしょう。このようなフェデレーションのためのヘッダの使い方、referred token binding idの定義と使い方について、それぞれ HTTPSTB と TBPROTO で規定されています。

検討課題: OAuth 2.0のAccess Tokenの場合

さて、OAuth 2.0のaccess tokenの場合にはさらに問題が複雑になります。OAuth 2.0では図7のように登場人物が関わり合い、それぞれの通信路がTLS接続になります。Token BindingはTLS接続ごとに使うことができるので、都合4つのTBIDが関わることになります。すでに複雑ですね!

図7: OAuth 2.0 code flowの場合
図7: OAuth 2.0 code flowの場合

まず、OAuth認可の前半を見てみましょう。Authorization codeを発行して、Clientのredirect_uriに届けるまでがこれに当たります(図8)。

図8: codeのToken Binding
図8: codeのToken Binding

これは前のID Tokenのときと同じで、referred TBIDの仕組みを使って行えます。Authorization codeをブラウザ-Client間のTBID(灰色の鍵)にひもづけることで、クライアントはcodeが横取りされていないことを検証できます。図には書いていませんが、クライアントからの最初の認可要求の302に Include-Referred-Token-Binding-ID ヘッダをつけることでこれに対応できるでしょう。

次に後半分部分を見てみます(図9)。

図9: access_tokenのToken Binding

図9: access_tokenのToken Binding
図9: access_tokenのToken Binding

クライアントはauthorization codeを認可サーバーに送って、access tokenをもらいます。そのaccess tokenに対してToken Bindingを使うとすると、クライアントからリソースサーバーRSに行くときのTBIDにaccess tokenをひもづけてもらう必要があります。先程と同様にreferred TBIDを送ればよさそうです(④)。このときクライアントは、次にアクセスするのがRSであることを知って、そのTBIDを自主的につけて送ります(Include…ヘッダをもらってきて、ということではなく)。

このときに2つの問題があります。

  • リソースサーバーはひとつなのか問題
  • JavaScript ClientとSame-Origin Policyの問題

「リソースサーバーはひとつなのか問題」は、発行されたアクセストークンの提示先が、TBIDのスコープ的にひとつなのかという問題です。最初の方で、TBIDはeTLD+1ごとに別々のものを割り当てると言いました。例えばapi.example.comとusercontents.example.com両方からリソースを取ってくるようなアプリでは、TBIDをどうしたらよいのでしょうか。解決策としては、複数のreferred TBIDをASに送ってどれでもいいようにしてもらう、同一のTBIDをふたつのスコープ両方に共通して使う、などがあり得ます。後者の同一のTBIDを使うことについて、 HTTPSTB の最新のドラフトでは、何をやっているかわかっているときには、クライアントは同一のTBIDを使ってもよい、と記述されることになりました。

「JavaScript ClientとSame-Origin Policyの問題」は、Clientがwebサーバー上の実装でなく、ブラウザ内のJavaScriptアプリの場合に発生する問題です。ASへのtoken発行要求や、RSへのリソースアクセスは、ブラウザのFetch APIを使って行うことになると思います。しかし、ClientとRSのオリジンが別である場合(ほとんどはそうでしょう)、Clientが「ブラウザからRSにアクセスするときのTBIDをASに送るように、ClientドメインのJavaScriptが指示」することになり、Same-Originポリシーに合わなくなってしまいます。この問題は11月にソウルで行われたIETF97のtokbing WGの会議中に指摘されました。この問題はまだこれから議論が始まるところです。主な検討はoauth WGで行われることになるでしょう。

まとめ

Token Bindingは、仕様標準化の最終段階に近づきつつあります。その概要と何ができるのか、そしてフェデレーションユースケースにおける使い方などを紹介しました。Token Bindingを使うと、これまでbearer token前提に考えてきたトークンも、Proof-of-possessionトークンとして使うことができるようになります。

OAuth 2.0のアクセストークンは、多分いちばんToken Bindingを使いたいシナリオだと思いますが、登場人物が多く、まだ検討が必要だと思います。今後の議論に注視しましょう。