WebAuthnをエミュレートするWebアプリの開発
こんにちは、SSTでWeb脆弱性診断用のツール(スキャンツール)開発をしている坂本(Twitter, GitHub)です。
最近 WebAuthn *1 を触る機会がありました。
WebAuthnを使うと、Webアプリのログインで指紋認証や顔認証など多要素認証を組み込むことが可能となります。
一方で脆弱性診断の観点から見ると、burpやzapなどのスキャンツールで WebAuthnを使ったログイン処理をどうやって再現するかが悩ましく感じました。
もちろん、2022年2月時点でほとんどの(筆者の知る限りではすべての)Webサイトで WebAuthn を始めとする多要素認証はオプション扱いです。
多要素認証のフローそのものの診断を要望されない限りは、従来のID/パスワード認証によるログイン処理でスキャンや手動診断が可能です。
とはいえ転ばぬ先の杖と言いますし、診断ツールを開発している筆者としても WebAuthnによるログイン処理をどうやって自動化し、スキャン処理に組み込むか強く興味が引かれました。
そこで筆者はWebAuthnをエミュレートするWebアプリを開発し、スキャンツールと組み合わせることで WebAuthn によるログイン処理をスキャン時に自動化できないか検証しました。
結果として一部のデモアプリで自動化に成功し、スキャンツールでログインの先までスキャンできました。
本記事では burp や zap などスキャンツールを使う人向けに、WebAuthnとは何か、スキャンツールにおける問題は何か、どうやってWebAuthnをエミュレートするWebアプリを開発したのかを紹介していきます。
なお安易な流用による事故を防ぐため、開発したWebアプリのソースコードは非公開とさせていただきます。
重要な注意
- 本記事で紹介する内容は、セキュリティ対策技術を向上させる目的のために調査・検証したものとなります。犯罪や不正行為を助長するものではありません。
- 本記事で紹介する内容について試したり検証する際は、自身の管理下にあるWebサイトで行ってください。
- 本記事で紹介する内容をもとにしたいかなる行為も、またそれにより発生したいかなるトラブルや損害についても、筆者および筆者が所属する、あるいは筆者が過去所属した会社は一切の責任を負いかねます。
WebAuthnとは
WebAuthn *2 はWebアプリで秘密鍵・公開鍵ペアによる認証をサポートするため、FIDO *3 が策定した標準仕様です。
「秘密鍵・公開鍵ペアによる認証」のところで、スマートフォン/PCサポートする指紋認証・顔認証・ハードウェアトークンが利用可能です。
Webアプリに多要素認証を組み込むための標準仕様である、とも言えます。
多要素認証では「認証の3要素」と呼ばれる以下の認証方式から、複数を組み合わせてユーザーを認証します。
- パスワード, PIN, 画像などの知識認証
- ハードウェアトークンなどの所持認証
- 例: Google Titan ( 参考記事 )
- 指紋や顔などの生体認証
Webアプリの世界では、Webブラウザで動く制約から長くIDとパスワードによる知識認証が主流でした。
Webブラウザの性質上、ハードウェアトークンや生体認証等の多種多様なハードウェアデバイスを制御するのが困難だったからです。
しかしスマートフォンやPCデバイスでは指紋認証や顔認証が普及し、ネイティブアプリがそれらの機能を使うための仕様、FIDO UAF *4 が公開されました。
Webブラウザの世界でも特定のセキュリティキー(ハードウェアトークン)に限定して2要素認証をサポートする FIDO U2F *5 が公開されます。
UAFとU2Fのいいとこ取りをしたのがWebAuthnになります。
ブラウザ上のJavaScript APIから生体認証やハードウェアトークンの認証機能にアクセスできるようにして、さらにWebサーバ側の仕組みもセットで仕様化しています。
これによりWebアプリにWebAuthnを組み込むだけで、WebAuthnに対応している生体認証やハードウェアトークン全てが利用可能になります。
Webアプリの世界でも「IDとパスワードによる知識認証」+「WebAuthnによる所持認証または生体認証」による多要素認証が容易に実現できるようになったのです。
- 認証器(Authenticator)
- 秘密鍵と公開鍵を生成・管理するデバイス。スマホやPCでは鍵ペアの管理時に生体認証/ハードウェアトークン等と連携する。
- CTAP *6
- 取り外し可能な認証器(セキュリティキーなど)とOSが通信するためのプロトコル。
- WebAuthn API
- ブラウザ上から認証器に公開鍵や署名を要求するためのJavaScript API。
- Relying Party(RP)
- Webアプリ(サーバサイド + クライアントHTML/JS)全体としてのIDプロバイダー実装部分。
- FIDO2サーバー *7
- RPの中で公開鍵や署名を検証するサーバー、あるいはコンポーネント
- FIDO2 MDS(Meta Data Service) *8
- 一定の審査基準を満たした認証器の一覧を提供する。危殆化した認証器を使っていないかなどのチェックに使う。
認証器の説明で出てきた「秘密鍵と公開鍵」が、WebAuthnを支える重要な仕組みになります。
秘密鍵と公開鍵は、数学的に以下のような特徴を持ったペアになっています。
- 秘密鍵で暗号化したデータ(= 署名) *9 は、公開鍵でしか復号(= 検証)できない
- → 公開鍵で正しく復号できたか検証すれば、相手(= 秘密鍵) を認証できたことになる。
- 秘密鍵から公開鍵を生成できるが、逆はできない。
- → 公開鍵を不特定多数に公開しても問題ない。
この特徴を活用して、WebAuthnでは以下のような2ステップで認証を実現します。
【ステップ1: 公開鍵の登録】
- クライアント側の認証器で秘密鍵と公開鍵のペアを作成し、公開鍵をサーバに登録しておく。
【ステップ2: 認証処理】
- RPサーバ側でランダムなバイト列である challenge を生成し、クライアントに送る。
- クライアント側で認証器の秘密鍵を使ってchallengeに署名(暗号化)してサーバに返送する。
- RPサーバ側で事前に登録された公開鍵を使って元のchallengeを復号できれば、そのクライアントが事前に公開鍵を登録したクライアントであると認証できる。
各ステップについて、ブラウザ/RPサーバ/認証器がどのように連携するのか具体的に見ていきます。
【ステップ1: 公開鍵の登録】
- (ブラウザ側のクライアントJS) ログイン済みの画面上でRPサーバに対してchallengeを要求 (JavaScriptのXHRやfetchを利用)
- (RPサーバ) challengeを生成してログイン中のユーザに紐付け、ブラウザに返す。
- (ブラウザ側のクライアントJS) 受け取ったchallengeとRPやユーザ情報を組み合わせ、WebAuthn API である
navigator.credentials.create()
に渡す。 - (ブラウザとOS) 認証器(Authenticator)を起動する。このときに指紋認証や顔認証でユーザ検証を行う。検証をパスしたら秘密鍵・公開鍵ペアを生成する。
- 秘密鍵そのものではないが、1:1対応する credentialIDも生成する。*10
- (ブラウザとOS) 公開鍵とcredentialID、認証器の情報などを
navigator.credentials.create()
の戻り値として返す。※秘密鍵そのものはブラウザに返さない。 - (ブラウザ側のクライアントJS) 戻り値と元のchallengeなどを RPサーバに送る。(JavaScriptのXHRやfetchを利用)
- (RPサーバ) challengeと認証器情報を検証する。問題なければ公開鍵とcredentialIDをログインIDに紐付けて保存する。
【ステップ2: 認証処理】
- (ブラウザ側のクライアントJS) ログイン画面上などからログインIDをパラメータとしてRPサーバにchallengeを要求 (JavaScriptのXHRやfetchを利用)
- (RPサーバ) challengeを生成し、ログインIDから取得したcredentialIDと一緒にブラウザに返す。
- (ブラウザ側のクライアントJS) 受け取ったchallengeとcredentialIDなどを、WebAuthn API である
navigator.credentials.get()
に渡す。 - (ブラウザとOS) 認証器(Authenticator)を起動する。このときに指紋認証や顔認証でユーザ検証を行う。検証をパスしたら credentialIDから秘密鍵を再生成して、challengeに対する署名を生成する(= 秘密鍵による暗号化)。
- (ブラウザとOS) 署名と認証器の情報を
navigator.credentials.get()
の戻り値として返す。 - (ブラウザ側のクライアントJS) 戻り値と元のchallengeなどを RPサーバに送る。(JavaScriptのXHRやfetchを利用)
- (RPサーバ) 受け取った署名を公開鍵で復号する。元のchallengeと一致し、他の検証もパスすればログイン成功。
※上記で紹介した図には attestationObject や clientDataJSONなど、本記事では解説を省略した内容も含んでいます。
これらの詳細については参考資料一覧で紹介している資料等をご確認ください。
以上がWebAuthnの流れになります。
JavaScript APIを介して、ブラウザ/RPサーバ/認証器が連携するのが大きな特徴となっています。
ポイント:
- 秘密鍵/公開鍵ペアはローカルの認証器で生成。
- 公開鍵とcredentialIDをサーバに保存する。
- 秘密鍵そのものはサーバには保存しない。(credentialID単体では秘密鍵を再生成できない)
- 生体認証の情報はサーバに保存しない。
- 生体認証は「ローカルで鍵ペアを操作するときのユーザ確認」として使うイメージ。
生体認証やハードウェアトークンを使って多要素認証を実現できる WebAuthn ですが、脆弱性診断の観点ではどうでしょうか?
診断ツールを開発している筆者として、ツールを使った自動検査(スキャン)への影響について考えてみます。
スキャンツールとの相性
burpやzapなどのスキャンツールはHTTPリクエスト/レスポンスに基づいて脆弱性の有無を検査(スキャン)します。
一般的なスキャンの流れは以下のようになります。
- ブラウザのHTTPプロキシとしてburpやzapを設定し、スキャン対象のWebアプリケーションを操作してHTTPリクエストとレスポンスを記録する。(クロール)
- 記録されたHTTPリクエストに脆弱性検査用の疑似攻撃パターンを埋め込み、スキャン対象のWebアプリケーションに送信する。レスポンスをクロール時の内容と比較するなどして脆弱性の有無を検査する。(スキャン)
Webアプリの作りによっては、特定の疑似攻撃パターンでログインセッションが無効化される、つまりログアウトしてしまうことがあります。
このため大抵のスキャンツールには「ログイン処理を再現し、ログインセッションを維持する機能」が組み込まれています。
具体的には下図のようにログイン時のリクエストを再送し、新たに発行されたセッションID(cookieなど)でHTTPリクエストを更新します。
これにより、ログイン後でないと正常にアクセスできないページの脆弱性を見つけることが可能になります。
WebAuthnを使うとどうなるでしょうか?
クロールのときはブラウザ側のWebAuthn APIである navigator.credentials.create()/get()
が動作してWebアプリ(RPサーバー)からのchallengeを正常に処理します。
しかし、いざスキャンするとなるとスキャンツールにはWebAuthn APIの機能が無い*11ため、そもそも鍵ペアを処理できないし、スキャン時に発行された challengeに対する署名もできません。
結果としてWebAuthnを使ったログイン処理に失敗し、ログインできていない状態でスキャンすることになります。
ログイン後のページに脆弱性があったとしても、レスポンスはログイン失敗のメッセージとなり、その先にある脆弱性を見逃す可能性があります。
このように WebAuthn とスキャンツールとの相性は決して良いものとは言えません。
どうすればこの問題を解決できるでしょうか?
WebAuthnをエミュレートするWebアプリの開発
WebAuthn とスキャンツールの相性問題を解決するため、次の2種類のアプローチを考えてみました。
- WebAuthn API のエミュレータを自分で実装し、署名や鍵ペアの生成を処理する。
- スキャンツールから実際のブラウザ上の WebAuthn API を呼び出す。
1番目のアプローチについては暗号処理を自前で実装する必要があるため、難易度が高いです。
またRPサーバ側でFIDO MDSを使って認証器の真正性を検証していれば、独自実装の認証器が拒否されてしまう可能性もあるため、このアプローチは見送りました。
2番目のアプローチについて調べてみると、PC用Chromeブラウザの Developer Tools で “WebAuthn emulated authenticator” というエミュレータ機能を見つけました。
このエミュレータ機能を使うと生体認証やハードウェアトークンなしに WebAuthn API を動かすことができます。
ユーザ操作を介さないため、スキャンツールのような自動化処理に組み込めそうです。
実際にサンプルアプリで使ってみたところ、FIDO MDS の検証もpassしてくれました。
これをスキャンツールから直接あるいは間接的に呼び出せないでしょうか?
Selenium WebDriver + DevTools Protocol で実現
いろいろ調べた結果辿り着いたのが以下の図になります。
様々なスキャンツールと汎用的に組み合わせられるよう、独立したエミュレート用Webアプリを作ることにしました。
Webアプリとして動かせれば、スキャンツールのログイン処理などと組み合わせることが可能になります。
【起動時の処理】
- エミュレート用Webアプリが Selenium WebDriver 経由で Chrome を起動します。
- Chrome起動後に、DevTools Protocols で WebAuthn emulated authenticator を有効化しています。
【スキャンする時】
- (スキャンツール) RPサーバから発行されたchallengeをエミュレート用WebアプリにHTTPリクエストで送ります。
- (エミュレート用Webアプリ) WebAuthn API を呼び出すJavaScriptを生成し、受け取った challenge を埋め込み、Selenium WebDriver 経由で Chrome に実行させます。
- (Selenium WebDriverで起動されたChrome) WebAuthn emulated authenticator が有効化されているので、WebAuthn API が自動で実行され、戻り値を返します。
- (エミュレート用Webアプリ) WebAuthn API の戻り値をスキャンツールが取り出しやすい形式に加工して、HTTPレスポンスとして返します。
- (スキャンツール) HTTPレスポンスから必要な値を抽出し、RPサーバに送ります。
Selenium WebDriverを活用し、Webブラウザによる処理をWebアプリとしてカプセル化した形になりました。
このようにWebAuthn処理の自動化の目処が立ちましたが、スキャンツールと組み合わせると短期間に何回も繰り返しWebAuthn処理を行うことになります。
自動化を想定していないユースケースを自動化したことになりますが、それによる影響は無いでしょうか?
ここで、FIDO MDS が再登場します。
FIDO MDS(Meta Data Service)についての懸念点
WebAuthn では navigator.credentials.create()
による公開鍵の登録フローで、RPサーバ側で認証器の情報を検証することができます。
FIDO MDS(Meta Data Service) ではFIDOに登録された認証器の一覧をDLすることが可能で、それとブラウザから送られた認証器の情報を見比べて以下の点を検証します。
- 登録された認証器か?
- 脆弱性が報告された(= 危殆化)認証器か?
登録フローが呼び出される度に、RPサーバがFIDO MDSから認証器の一覧を毎回DLしていた場合どうなるでしょうか。
スキャンツールで大量に登録フローが実行されると、FIDO MDS に対しても大量アクセスが発生する可能性があります。
検査対象がブロックされるだけならまだしも、FIDO MDSを利用している他のアプリにまで影響がでると大変なことになります。
今回検証に使ったWebAuthnのデモアプリは2種類あり、片方はソースコードが公開されていたので FIDO MDS の検証をコメントアウトして動かしました。
もう片方も作った人に確認し、外部にアクセスが飛ばないことを確認しています。
これにより、この懸念点をクリアしてスキャンすることができました。
スキャンした結果
WebAuthnを利用したデモアプリを2種類用意し、開発したエミュレート用Webアプリを使って WebAuthn の登録フローと認証フローをスキャンしてみました。
スキャンツールとしては、自社開発している診断ツールを使っています。
WEB+DB PRESS Vol.114 特集2 生体認証でさよならパスワード 作って学ぶWebAuthn
- https://gihyo.jp/magazine/wdpress/archive/2020/vol114
- こちらで紹介されている WebAuthn4J を使ったサンプルアプリ
- 登録フロー, 認証フローそれぞれでスキャン成功。
- 他の人が作ったWebアプリ。
- あえて中身を見ずに、JS部分もminifyされたまま挑戦。
- 登録フローはスキャンできたが、認証フローがスキャンできなかった。(RPサーバがエラーを返すなど不安定が動きがあり、特に認証フローについては大半のログイン処理が失敗していた)
- 他の人が作ったこともあり原因までは調べることができませんでした。
どちらのケースでも次のような苦労がありました。
- エミュレート用WebアプリのレスポンスをそれぞれのRPサーバにあわせてカスタマイズする必要があった。
- WebAuthnAPI を呼び出すJavaScriptを解析し、同じパラメータを渡すように Selenium WebDriver に実行してもらうJavaScriptをカスタマイズする必要があった。
WebAuthnではRPサーバとやりとりする形式や、WebAuthn API を呼び出すJavaSciriptまでは定義していません。
このため、WebアプリによりRPサーバに渡す形式やJavaScriptの構成が異なってきます。
エミュレート用Webアプリをそれに適合させ、スキャンツールでパラメータを抽出できるようにするためには、アプリごとに若干のカスタマイズが必要でした。
また対象のデモアプリが最終的にどのようなパラメータで WebAuthnAPIを呼び出しているかは、実際にアプリを動かして Developer Tools などからステップ実行するなどして解析する必要がありました。
2つ目のアプリについてはminifyされた状態でステップ実行することになり、デバッグスキルのトレーニングになったと思います。
もう一つ苦労した点として、認証フローのクロールとスキャンがややこしい点があります。
というのも、emulated authenticator を使ってクロールやスキャンをすると2つの認証器が存在することになり、ユーザがどちらに紐付いているか混乱するからです。
- クロールで使うChromeの emulated authenticator
- エミュレート用Webアプリが起動した Chrome の emulated authenticator
詳細は省きますが、クロールとスキャンで整合性を保つために相当な苦労がありました。
ちょっと気を抜いて操作してしまうと2つのemulated authenticatorの情報が混ざってしまい、WebAuthn のフローを正しく再現できなくなります。
感想とまとめ
本記事では WebAuthn の概要を紹介し、スキャンツールとの相性について考察しました。
また WebAuthn をエミュレートするWebアプリを開発し、スキャンツールと組み合わせることでWebAuthnの登録/認証フローを自動化し、一部のWebアプリで認証後の機能をスキャンできることを確認しました。
一方でエミュレート用Webアプリには以下のような課題も見つかりました。
- RPサーバがエラーを返して正常にスキャンできないケースがあった。
- RPサーバごとにカスタマイズが必要。
- クロールとスキャンで整合性を保つための注意が必要。
上記のような課題はありつつも、Selenium WebDriver でWebブラウザ上の処理を自動化して、スキャンツールと組み合わせられるようWebアプリとしてカプセル化するという技術的に面白い挑戦ができたことは収穫だったと感じます。
2022年2月現在、筆者の知る限りのWebサービスでは WebAuthn は未サポート or オプション扱いです。
当面の間、WebAuthn「しか」サポートしていない認証が出てくることはまずありえないでしょう。
とはいえ世の中の流れ的にパスワードレスや多要素認証が普及していくことは確実で、その中でWebAuthnも広がっていくものと思います。
「WebAuthnに対応したので、実装箇所で脆弱性がないか検査したい/してほしい」、そうしたときに本記事が何かしらの参考になれば幸いです。
参考資料集
WebAuthnの仕様, 解説記事
WebAuthn 101 Demystifying WebAuthn, Christiaan Brand, Blackhat 2019
- https://i.blackhat.com/USA-19/Thursday/us-19-Brand-WebAuthn-101-Demystifying-WebAuthn.pdf
- FIDOとWebAuthnの歴史や仕組みが非常にわかりやすくまとまっていました。
Web Authentication: An API for accessing Public Key Credentials Level 2
- ブラウザ対応状況(caniuse)
yubico WebAuthn document
Web Authentication API - Web API | MDN
- https://developer.mozilla.org/ja/docs/Web/API/Web_Authentication_API
- WebAuthn のブラウザJS API のリファレンス
「安全・安心・便利」FIDO(ファイド)を使ったパスワードレスログインとは - Corporate Blog - ヤフー株式会社
- https://about.yahoo.co.jp/info/blog/20190220/fido.html
- 全体の流れがわかりやすくまとまってる解説記事です。
Yahoo! JAPANでの生体認証の取り組み(FIDO2サーバーの仕組みについて) - Yahoo! JAPAN Tech Blog
Guide to Web Authentication
WEB+DB PRESS Vol.114 特集2 生体認証でさよならパスワード 作って学ぶWebAuthn
- https://gihyo.jp/magazine/wdpress/archive/2020/vol114
- 実際に WebAuhn を使ったJavaサンプルコードを解説しています。こちらのサンプルコードを本記事では検証に使っています。
Google Chrome の WebAuthn Emulated Authenticator
Emulate Authenticators and Debug WebAuthn in Chrome DevTools
How we built the Chrome DevTools WebAuthn tab | Google Developers
Chrome DevTools Protocol - WebAuthn domain
Chromeデベロッパーツールを使ってWebAuthnを簡単に試してみよう! - Qiita
*1:Web Authentication API, 「ウェブオースン」と読むようです
*2: https://www.w3.org/TR/webauthn-2/ : 2022-02時点
*4: Universal Authentiction Framework, https://fidoalliance.org/specifications/
*5: Universal 2nd Factor, https://fidoalliance.org/specifications/
*6: Client to Authenticator Protocols , https://fidoalliance.org/specifications/
*7: “2” を省略する表記もあり。以降の説明では省略表記を使っています。
*8: “2” を省略する表記もあり。以降の説明では省略表記を使っています。
*9:本記事ではイコールとして扱っていますが、厳密には異なるという記事もあります : https://qiita.com/angel_p_57/items/d7ffb9ec13b4dde3357d
*10:厳密には認証器と組み合わせて秘密鍵を再生成するためのID
*11:2022年2月時点のburpとzapについて筆者が確認した範囲