フレクトのクラウドblog re:newal

http://blog.flect.co.jp/cloud/からさらに引っ越しています

Passkey のフィッシング耐性

こんにちは。エンジニアの山下です。

パスワードレス技術として Passkey が注目を集めていますが、この Passkey はフィッシング耐性を持つと言われています。今回は Passkey のフィッシング耐性を支える仕組みについて調査した結果を書きたいと思います。

Passkey と FIDO

そもそも Passkey は FIDO Alliance という団体によって定められた概念です。FIDO Alliance の 公式ページ では Passkey を以下のように定義しています:

Any passwordless FIDO credential is a passkey.

FIDO Alliance は FIDO (FIDO2) というパスワードレス認証の技術標準を定めており、この FIDO で使用されるパスワード以外のクレデンシャルを全て Passkey と呼ぶ、と定義は述べています。

実のところ、フィッシング耐性を持つように設計されているのは FIDO であり、単なるクレデンシャルに過ぎない Passkey にはいかなる力もありません。従って、以降は FIDO ではフィッシング耐性をどのように実現しているのかが話の主題となります。

FIDO については 公式ページ にある以下の画像を見るとイメージが掴みやすいと思います:

FIDO では Authenticator と呼ばれる認証デバイス(PC・スマホ・USB ドングルなど)を利用するのですが、上の画像は Authenticator としてスマホを利用した場合のログインの一連の動作を表しています。上記における USER APPROVAL および KEY SELECTED が Authenticator の利用箇所です。ブラウザを使って目的のサイトにアクセスした端末と Authenticator が連動して認証処理を実現している様子が表現されています。

Passkey は WebAuthn とセットで語られることも多いですが、この WebAuthn は FIDO の仕様の一部です。FIDO は全体の挙動を定めるプロトコルの CTAP と、その CTAP においてブラウザ・Authenticator の間の通信に使用するインターフェースとなる JavaScript API を定める WebAuthn で構成されています。

FIDO のフィッシング耐性

FIDO のフィッシング耐性は以下の3つの要素で成り立っているようです:

  • 機密情報のネットワークからの隔離
  • Origin Binding
  • Channel Binding

大まかに述べると、最初の「機密情報のネットワークからの隔離」は機密情報(パスワードや生体情報などのユーザが直接関与するシークレット)を守るためのアーキテクチャの原則で、それ以外は中間者攻撃 *1 に対抗するためのセキュリティ上のテクニックという構成になっています。

以下、それぞれについて順に説明していきます。

機密情報のネットワークからの隔離

古典的な認証システムでは、パスワードなどの機密情報を暗号化されたネットワークチャネルを通じてサーバに送信しますが、FIDO ではそもそも機密情報をネットワーク上に露出させる必要がないような設計になっています。

先述の通り、FIDO では Authenticator と呼ばれる認証用のデバイスを用いて認証を行いますが、機密情報の扱いは Authenticator の中で閉じるようになっています。具体的には以下の流れで認証を行います:

  1. サーバがブラウザにチャレンジ(認証要求)を送信する
  2. ブラウザが Authenticator にチャレンジを送信する
  3. Authenticator がユーザに機密情報の入力を促す
  4. ユーザが Authenticator に機密情報を入力する
  5. Authenticator がブラウザにチャレンジの結果を送信する
  6. ブラウザがサーバにチャレンジの結果を送信する

一方で、詐取対象となり得る以下のものはネットワークに晒されます:

  • Authenticator が生成するクレデンシャル
  • Cookie などに含まれる認証情報(セッションなど)

前者ですが、新規サービスの登録要求の際、Authenticator が認証に成功すると内部でクレデンシャルを生成します。このクレデンシャルは Authenticator がブラウザに送信する応答内容の作成等に使用されます。

しかしながら、このクレデンシャルは公開鍵暗号のキーペアで、その秘密鍵はやはりネットワーク上に露出することはありません。従って、利用している公開鍵暗号アルゴリズムが暗号学的に突破されない限り、安全性は機密情報と同程度になると考えらえます。

後者の認証情報については、残念ながら上記のフローの範疇では一切保護されないため、別途セキュリティ手法を導入するなどして保護する必要があります。後述の Channel Binding はその一例となります。

Origin Binding

Origin Binding とはクレデンシャルとチャレンジ要求元サーバの Origin を紐付けて、ある Origin からの要求で作成されたクレデンシャルは同一の Origin からの要求でなければ受け付けないようにする仕組みのことです。具体的には、ブラウザから Authenticator へリクエストを発行する際に Origin を同封し、Authenticator はその Origin を元に必要なクレデンシャルを判断するという流れになります。

フィッシングの代表的手法に、ユーザを本物そっくりの偽サイトへ誘導してクレデンシャルを詐取する中間者攻撃がありますが、この手の詐取は Origin Binding で防ぐことができます。外観がそっくりでも Origin は異なるため、プロトコルによって機械的に詐取の防止が可能です。

余談ですが、Origin Binding はそれなりに強いネットワーク構成上の制約になるため、緩和のための要望が GitHub で議論されているようです。

FIDO における Origin Binding

FIDO では Origin は WebAuthn の定める RP ID に格納して Authenticator に通知されることになっています。

MDN のサンプルコードがわかりやすいです。create() では publicKeyrp.id として Origin が渡されます:

const publicKey = {
  challenge: new Uint8Array([117, 61, 252, 231, 191, 241, ...]),
  rp: { id: "acme.com", name: "ACME Corporation" },
  user: {
    id: new Uint8Array([79, 252, 83, 72, 214, 7, 89, 26]),
    name: "jamiedoe",
    displayName: "Jamie Doe"
  },
  pubKeyCredParams: [ {type: "public-key", alg: -7} ]
}

navigator.credentials.create({ publicKey })

get() では publicKeyrpId で Origin が渡されます:

const publicKey = {
  challenge: new Uint8Array([139, 66, 181, 87, 7, 203, ...]),
  rpId: "acme.com",
  allowCredentials: [{
    type: "public-key",
    id: new Uint8Array([64, 66, 25, 78, 168, 226, 174, ...])
  }],
  userVerification: "required",
}

navigator.credentials.get({ publicKey })

Origin の改竄可能性

1つ前のセクションを読めばすぐにわかりますが、Origin の指定は JavaScript で行います。これは WebAuthn が指定可能な Origin に幅を持たせているために生じた仕様だと思われます。*2

JavaScript はサーバからダウンロードしたプログラムに過ぎないので、攻撃者が悪意ある JavaScript を仕込んだ場合に Origin の改竄ができないかどうかが気になるところです。

結論から述べると、この rpId を改竄して Authenticator に送信しようとすると、リクエストはエラーとして処理されます。rpId が Origin を正確に表しているかどうかをブラウザが検証しているためです。この振る舞いは WebAuthn の仕様にも明記されています(太字による強調は本記事によるもの):

By default, the RP ID for a WebAuthn operation is set to the caller’s origin's effective domain. This default MAY be overridden by the caller, as long as the caller-specified RP ID value is a registrable domain suffix of or is equal to the caller’s origin's effective domain.

つまり Origin を偽装するには、ブラウザ自体を侵害して悪意あるリクエストを発行可能にするか、ブラウザと Authenticator の間の通信に割って入るかのいずれかが必要になります。どちらも実施するにはそれなりの手間と技術が必要になりそうです。

Channel Binding

Channel Binding とは、認証処理を TLS コネクションに紐づけることで、攻撃者による認証情報のハイジャックを防ぐ仕組みです。

Channel Bindign では、認証で使用する TLS コネクションを一意に特定可能な ID を生成し、その ID が保証する正しい経路でのみ認証情報のやりとりを許可します。具体的には、以下の流れで通信が正しい経路で行われていることを検証します:

  1. 通信の送信側で送信に使用する TLS コネクションの ID を生成する
  2. 通信の送信側から ID と改竄防止のための署名を受信側に送信する
  3. 通信の受信側で受信に使用した TLS コネクションの ID を生成する
  4. 通信の受信側で受信した ID と生成した ID が一致することを検証する

この検証によって中間者攻撃の検出が可能です。中間者攻撃を受けている場合、通信は以下のような構成になります:

Authenticator ⇄ ブラウザ ⇄ 攻撃者 ⇄ サーバ

攻撃者がブラウザとサーバの間に入ったことで、ブラウザ(送信側)とサーバ(受信側)が使用する TLS コネクションの ID が一致しなくなります。

また、認証情報が使用可能な通信経路が限定されるため、攻撃者が Cookie の詐取などで得た認証情報を別経路で使用することができなくなるという利点もあり、セキュリティにおいてはかなり恩恵の大きい機能です。このあたりの具体的な話については FIDO に詳しく述べられているので、興味がある方はそちらを参照していただければと思います。

FIDO における Channel Binding

FIDO では Token Binding を利用して Channel Binding を実現するように仕様が規定されています。Token Binding とは TLS コネクションを一意に特定可能な ID を生成する技術の標準仕様です。ただし、様々なユースケースを考慮して Channel Binding の利用は任意とされています。

Token Binding に関するデータの実態は WebAuthn で CollectedClientData として定義されているオブジェクトの tokenBinding パラメータの中に格納されます。この CollectedClientData はブラウザから取得した情報を元に Authenticator が作成し、それがブラウザ経由でサーバへ渡る形になります。Authenticator が作成するのは、同時に署名を作成してデータの改竄を防ぐためです。

CollectedClientData は clientDataJSON というパラメータとして送信されます。MDN のサンプルコードを見ると、Authenticator からブラウザへの応答の中に clientDataJSON が含まれていることがわかります:

navigator.credentials.create({ publicKey }).then((publicKeyCredential) => {
  const response = publicKeyCredential.response;

  // Access attestationObject ArrayBuffer
  const attestationObj = response.attestationObject;

  // Access client JSON
  const clientJSON = response.clientDataJSON;

  // Return authenticator data ArrayBuffer
  const authenticatorData = response.getAuthenticatorData();

  // Return public key ArrayBuffer
  const pk = response.getPublicKey();

  // Return public key algorithm identifier
  const pkAlgo = response.getPublicKeyAlgorithm();

  // Return permissible transports array
  const transports = response.getTransports();
});

つまり、全体の流れとしては以下になります:

  1. ブラウザが Authenticator に Token Binding の情報を送信する
  2. Authenticator が clientDataJSON とその署名を作成する
  3. Authenticator がブラウザに clientDataJSON とその署名を送信する
  4. ブラウザがサーバに clientDataJSON とその署名を送信する
  5. サーバが独自に Token Binding の情報を作成し clientDataJSON の中に含まれる tokenBinding が正当であるかを検証する

少なくともこのレベルの視点においては、なかなか堅牢そうな造りに見えます。

Token Binding の実際

突然ですが、悲しいお知らせをしなくてはなりません。WikipediaToken Binding に書かれている以下の一文をお読みください。

Only Microsoft Edge has support for token binding.

恐ろしいことにほとんどのブラウザが Token Binding をサポートしていないのです。状況は芳しくなく、ChromiumGroups には Token Binding に関する実装コードを全削除した旨が記録として残されていたりします。

MDN でも tokenBinding パラメータの横に Deprecated を示すゴミ箱アイコンが表示されており、Token Binding は廃れていく方向にあるように見えます。

念のため Google ChromeSafariMicrosoft Edge で Passkey を使用した際に clientDataJSON がそれぞれどうなるのかを試してみました。なお、サーバは Auth0 を使用しています。

結果は以下の通りです:

{
    "type": "webauthn.get",
    "challenge": "WBlOWP_-Mkf6Z-fbLbnK0gUgYRD-lBPltbGTFk_GNlY",
    "origin": "https://test-dyamashita.jp.auth0.com"
    "crossOrigin": false
}
{
    "type": "webauthn.get",
    "challenge": "b0SV24IqAAUOaMXuXPvQegQ_fmgOfTKsl79CCiZvIMM",
    "origin": "https://test-dyamashita.jp.auth0.com"
}
{
    "type": "webauthn.get",
    "challenge": "wCjiCpMCXCa9oQWT1pFoKMXKXyFG5yIAqtvHFGit6DQ",
    "origin": "https://test-dyamashita.jp.auth0.com",
    "crossOrigin": false
}

Token Binding をサポートしているはずの Microsoft Edge ですら tokenBinding パラメータがないという結果になりました。やはり現状では Passkey の Channel Binding は機能していない状態になってしまうようです。

FIDO は ブログ で Channel Binding の利用によって克服される問題について語っていますが、これらについては別途対策を取る必要があるということになりそうです。

まとめ

今回調べてわかった結果をまとめます:

  • 機密情報のネットワークからの隔離
    • 機密情報は Authenticator の中で扱いがとじているので詐取は困難
    • Authenticator が生成するクレデンシャルも公開鍵暗号により詐取は困難
    • ただし認証情報自体の詐取に対する効力はなさそう
  • Origin Binding
    • 外観の類似などの人間の勘違いを基礎にした詐取は防止できる
  • Channel Binding
    • Passkey では機能していない

ちなみに、上記は WebAuthn を使ってフロントエンドを実装する際に特別注意を払わずとも自動的に有効になります。

WebAuthn が絡むもので現在機能しているのは Origin Binding のみですが、Origin Binding に使用するパラメータ rp.id ないし rpId は特に指定せずともデフォルトで現在の Origin が自動で適用されるようになっているためです。MDN に記載があるので、詳しくはそちらを参照ください。

また Channel Binding も Authenticator から受け取った clientDataJSON をそのままサーバに返すのみでよいので、必要な実装を行えば自然に有効になるようになっています。もっとも、先述の通り、現在は Token Binding のサポート状況のため機能していませんが ... 。

FIDO について色々調べている中で Channel Binding について熱く語られている記事を目にしていたため、実際には Channel Binding が機能していない点には非常に驚きました。Origin Binding でも中間者攻撃に対抗できるとはいえ、Channel Binding による恩恵はかなり大きいので、今後のアップデートで代替方法が検討されることに期待したいです。

なお、上記で困難と書いている内容についても、当然ながら、ソーシャルエンジニアリングなどによって突破される可能性はあります。安心し切ってセキュリティホールを見落とさないようにしましょう。(自戒

以上です。最後までお読みいただき、ありがとうございました。

*1:厳密には中間者攻撃以外の攻撃もカバーしています。特に Channel Binding は詐取した認証情報を別のコネクション上で使用することを阻止するため、より広範な攻撃を阻止できます。

*2:どのように幅を持たせているかについては こちら に明記されています。