パスワードレスな世界を実現するFIDO認証とは?
こんにちは!小林です。
本記事はアドベントカレンダーほぼ厚木の民5日目の記事になります。 今回は FIDO というパスワードを使用しないオンライン認証技術について書こうと思います。
パスワードってアカウントごとに毎回作って覚えるのめちゃくちゃ大変ですよね〜(12文字以上の英数字で大文字か記号を入れろとか...)。また、漏洩のリスクも高いのでセキュリティ的にも問題があります。
そんな従来のパスワード認証の課題を解決する、安全かつユーザビリティの高い認証技術として登場したFIDO認証の仕組みを紹介します。
https://gyazo.com/f0025b4597b94f4a8ce5c5fb64ca9a09
FIDO認証の仕組み
「パスワードを使わない認証」というと指紋などの生体認証をイメージしてしまうと思いますが、FIDOは生体認証そのものではなく、認証器を使用した公開鍵暗号方式による認証技術のことを言います。
https://image.slidesharecdn.com/20190614interop2019final-190710161238/95/fido2-11-638.jpg?cb=1562775337
公開鍵暗号方式は、電子署名や共通鍵の配送などに使用される暗号方式のことで、秘密鍵と公開鍵という非対称のペアの鍵を使用する暗号方式のことです。
秘密鍵で暗号化したものは公開鍵でしか復号化できず、公開鍵で暗号化したものは秘密鍵でしか復号化できません。
FIDOではこの仕組みを使い、サーバーから送られてきたチャレンジというランダムな文字列に対して、認証器*1 内の秘密鍵で署名(暗号化)して返し、サーバー側は返ってきた署名を事前に登録しておいた公開鍵を使って検証(復号化)することで、ログインしようとしているユーザーを認証します。
この署名の前に、指紋認証で認証器を使っているのがアカウント所有者か確認することが多いですが、認証器が生体認証に対応しているかどうかはFIDO仕様の範囲外なので、単純なPIN入力しか対応していない認証器も存在します。
ここで重要なのは検証するデータがサーバーが生成するランダムな値に対する署名に過ぎず、ユーザーの機密情報は一切サーバー側に流れていない点です。
そのため、ユーザーが認証器に対してユーザー検証する際に生体認証などを使用しても、生体情報は認証器内に留まっており、安全にWebサービスにログインすることができます。
*1 USBキーなどの外部デバイス、もしくはスマホやPCに内蔵された物理的なセキュリティキー
FIDO2 と CTAP・WebAuthn
FIDO のWeb認証としてプラットフォーム化されたものを特に FIDO2 と呼びます。
FIDO2 は以下の図のように、外部認証器とクライアント(ブラウザ)間の通信用のアプリケーションプロトコル CTAP と登録と認証のJavaScript API を定義する WebAuthn で構成されています。
なお、CTAP に関してはブラウザと外部認証器間のプロトコルになるので、内部認証器とのやり取りの場合は Windows や Andoroid といったプラットフォームが独自のAPIを用意しているようです。
https://www.inovex.de/blog/wp-content/uploads/2019/12/Screenshot-2019-12-01-at-11.06.31.png
また、CTAP を実装するのはブラウザが担当する部分になります。
そのため、Webサービス側の開発者はクライアントサイドから WebAuthn API を実行するだけで、ブラウザから認証器のレスポンスを取得することができ、その結果をもとに公開鍵の登録や署名の検証を行います。
CTAP のコマンドにまで説明するとかなり細かい話になってしまうので、以降、WebAuthn の説明に留めます。
WebAuthn 概要
WebAuthn API で用意されている API は、 navigator.credentials.create() と navigator.credentials.get() のたった2つだけです。
navigator.credentials.create(options) は登録用の API で、実行すると認証器に対して新たな秘密鍵・公開鍵のペアの作成をリクエストし、成功すると認証器から AttestationObject というデータが取得できます。
AttestationObject には公開鍵とクレデンシャルID(鍵ペアに紐づいたID)、公開鍵の証明書などが入っています。
navegator.credentials.get(options) はログイン時に実行するAPIで、登録時に受け取ったクレデンシャルIDを引数のoptions に渡すと、Assertion と呼ばれるチャレンジに対する秘密鍵の署名などのデータが認証器から取得できます。
それでは、実際に登録や認証を行う場合のサーバーサイドも含めたフローはどのような流れになっているのかと、二つのAPIの引数として与えているoptionsに関して説明します。
登録フロー
サーバーサイドも含めた全体的な登録フローは以下の図のようになります。
https://media.prod.mdn.mozit.cloud/attachments/2018/09/25/16189/2d5b3c88e13b013bd2c29b421d1b7353/WebAuthn_Registration_r4.png
0. 登録画面からサーバーにユーザー情報やオプションパラメーターを送信する。なお、ここは WebAuthn の範囲外なのでサービスごとに送るデータやプロトコルは異なる。
1. サーバーはチャレンジを生成し、0. で受け取った情報などを基にoptions = PublicKeyCredentialsCreationOptions を生成してクライアントに返す。
2. クライアントがサーバーからoptionsを受け取り、navigator.credentials.create(options)を実行すると、ブラウザがCTAPコマンド用のデータに変換して認証器に送信する。
3. 認証器へのユーザー検証(生体認証やPIN入力)が成功すると、秘密鍵と公開鍵のペア、AttestationObject が作成される。
4. クライアントサイド(ブラウザ)に AttestationObject が返される。
5. クライアントは navigator.credentials.create(options) の戻り値として AuthenticatorAttestationResponse を受け取り、適宜エンコードを行ってサーバーサイドに渡す。
6. サーバーサイドでは AttestationObject をもとに、要求したオプションが実際に認証器で実行されたかの確認やドメインの検証などを行い、問題なければ公開鍵とクレデンシャルIDをユーザーアカウントに紐付けて DB に登録する。
登録オプション
上のフローの1. で生成するPublicKeyCredentialsCreationOptionsについて解説します。
このオプションを設定することで、認証器に対して、ユーザー検証を省略したり同一認証器による二重アカウントの作成を防ぐといった要求を行うことができます。
table: PublicKeyCredentialCreationOptions
パラメーター名 説明
* rp ドメイン名などのサーバー情報
* user ログインIDとなるユーザー情報
* challenge チャレンジ文字列
* PubKeyCredParams 鍵生成に使用する暗号アルゴリズムの指定値
timeout navigator.credentials.get API のレスポンスが返ってくるまでのタイムアウトの指定値
excludeCredentials 登録済みのクレデンシャルIDを渡すことで、同一認証器による二重アカウントの作成を防ぐオプション
authenticatorAttachment 内部認証器か外部認証器かを制限するオプション
requireResidentKey 認証器にユーザー情報の保存を要求するオプション
userVerification ユーザー検証を要求するか指定するオプション
attestation AttestationObject に認証器情報や公開鍵の証明書を含めるか要求するオプション
extensions 拡張機能のリクエスト領域
* ~ は必須パラメータ
オプションの中でも requireResidentKey と userVerification は特に重要なオプションなので、もう少し詳しく説明します。
通常、登録時に生成された秘密鍵などのクレデンシャルは認証器内に保存されないのですが、requireResidentKey を True に設定すると、RPID(サーバーのドメイン)に紐づく形で、ユーザー情報とともにクレデンシャルが保存されます。
この機能を使えば、ログイン時に自分がどのユーザーか示さなくても、認証器からユーザー情報を取り出すことができるので、パスワードだけでなくログインIDの入力も省略することができます!ただし、認証器の記憶領域によって保存できるクレデンシャル数には上限があるので注意が必要です。
userVerification は生体認証や PIN 入力などのユーザー検証を必要とするか設定できるオプションで、discouraged (不要)、preferred (可能なら)、required (必要) の3つの値を取り得ります。
ユーザー検証を行うことで、認証器の所有認証と合わせて二要素認証が満たすことができ、パスワードレスな認証が実現できます。
認証フロー
続いて、認証フローについて説明しますが、ご覧の通り、登録時とほとんど同じような流れになります。
https://media.prod.mdn.mozit.cloud/attachments/2018/02/10/15802/4da3ac431c758c55db51e7ef0d33836e/MDN%20Webauthn%20Authentication%20(r1).png
0. 登録画面からサーバーにユーザー情報やオプションパラメーターを送信する。なお、ここは WebAuthn の範囲外なのでサービスごとに送るデータやプロトコルは異なる。
1. サーバーはチャレンジを生成し、0. で受け取った情報を基にoptions = PublicKeyCredentialsCreationOptionsパラメーターを生成してクライアントに返す。
2. クライアントがサーバーからoptionsを受け取り、navigator.credentials.get(options)を実行すると、ブラウザがCTAPコマンド用のデータに変換して認証器に送信する。
3. 認証器へのユーザー検証(生体認証やPIN入力)が成功すると、クレデンシャルIDに紐づく秘密鍵でチャレンジに署名され、Assertion が作成される。
4. クライアントサイド(ブラウザ)に Assertion が返される。
5. クライアントは navigator.credentials.get(options) の戻り値として AuthenticatorAssertionResponse を受け取り、適宜エンコードを行ってサーバーサイドに渡す。
6. サーバーサイドは Assertion をもとに、要求したオプションが実際に認証器で実行されたかの確認やドメインの検証などを行い、最後に登録済みの公開鍵を使って署名の検証が成功したら、ログインを許可する。
認証オプション
認証用のオプション PublicKeyCredentialsRequestOptions は登録時に比べるとパラメーターの数も少なく、新しく登場するのは、allowCredentials くらいです。
table: PublicKeyCredentialRequestOptions
パラメーター名 説明
* challenge チャレンジ文字列
timeout navigator.credentials.get API のレスポンスが返ってくるまでのタイムアウトを指定する値
rpId サーバーのドメイン
allowCredentials 認証するユーザーのクレデンシャルID。空の場合は ResidentKey 認証が行われる。
userVerification ユーザー検証を要求するか指定する値
extensions 拡張機能のリクエスト領域
* ~ は必須パラメータ
allowCredentials はどのユーザーの認証をするか指定するオプションで、 サーバーはログインしようとしているユーザーのクレデンシャルIDを設定します。
認証器はこのクレデンシャルIDから秘密鍵と公開鍵を再生成することができるので、登録時と同じ認証器を持っていれば認証することができます。
この場合は、ユーザーが最初にサーバーにログインIDなどのユーザー情報を伝えなければならないのですが、登録時に ResidentKey を設定しておくと認証器がユーザー情報を覚えてくれているので allowCredentials は不要となり、ログインIDの入力も省略できます。
ただ、もし一つのWebサービスに対して複数 ResidentKey 登録している場合は、全てのアサーションが取得されてしまうので、以下の画面のようにブラウザ側でユーザー選択を行う必要があります。
https://gyazo.com/4a8bccf2f524a9381bf822006b1fca32
WebAuthn demo サイト
色々説明してきましたが、以下のように様々な WebAuthn のデモサイトが提供されているので、ぜひ興味のある方は実際に動かして試してみてください (Android スマホや TouchID の付いた MacBook などで試せます)。
Auth0 のデモは通信の流れが視覚的に表されていてとてもわかりやすいです。若干使い辛いですが DEBUGGER タブからオプションパラメータを変更したり、データの中身を見ることもできます。 ただ、ResidentKey 認証は Microsoft のデモしか対応していないようなので、いろんなパラメータを試したい場合はこちらもおすすめです。
caBLE
FIDO2 はこれまで説明してきた通り、安全かつ便利な認証技術ですが、課題も残されています。
それは、生体認証に使われる生体情報や秘密鍵が、認証器にのみ保存されることで外部から守られていることにより、内部認証器を使用している場合は、デバイスを変えるたびに、新しく公開鍵ペアを作らなければなりません。
例えば、スマホで FIDO 認証しているWebサービスに PC からログインする場合などは、別途認証器を登録する必要があります。
もちろん外部認証器を使用することで問題は解決するのですが、わざわざ FIDO 認証をするために認証器を購入するユーザーは少ないと思います。
そこで、スマホを外部認証器に使用する方法として、Google が caBLE (cloud assisted BLE) という FIDO2 の拡張を提案しています。これが実現すれば、わざわざ専用の認証器を用意しなくても、誰しもが持っているスマホを認証器として使えるようになり、デバイスを跨いでパスワードレス認証が可能になると期待されています。
caBLE の現状としては、今のところ chrome 上で Google アカウントにログインする際にのみ使用することができます。簡単に設定できるので、興味がある方はやってみてください。
ただ、Bluetooth 経由で通常のペアリングを行わずにスマホと接続するのですが、サーバーサイドに追加実装が必要だったり、接続手順が少しややこしく、他のブラウザやWebサービスで使用できるように標準化されるにはまだまだ時間がかかりそうな感じがします。。。
まとめ
以上、パスワードを使わない認証技術 FIDO について紹介しました。FIDO認証はすでに chrome や Edge 、Firefox などの主要ブラウザは対応済みですし、国内のサービスも Yahoo や docomo など様々なところで使われ始めています。 FIDO や WebAuthnに関してはググるといろんなサイトでまとめてあるので、もっと詳しく知りたい方はぜひ色々と調べてみて下さい。最後までお読みいただき有り難うございました。
参考文献
FIDO 仕様書