目次
- はじめに
- Yahoo! JAPANが対応したWebAuthnとは
- WebAuthnとFIDO認証について
- WebAuthnの処理について
- ブラウザーでの実装
- サーバーでの実装
- まとめ
- 参考リンク
はじめに
こんにちは、Yahoo! JAPANの上野博司と申します。
Yahoo! JAPAN IDを使ったID管理や認証機能の開発を担当しています。
いきなりですが、皆さんはYahoo! JAPANのサービスにログインしていますか。
Yahoo! JAPANでは現在100を超えるサービスをユーザーに提供しています。 これらのサービス間でデータ連携を行うことでより良いユーザー体験を作るために、ログインは重要な要素です。
一般的に使われているログイン方法としてIDとパスワードを入力する方法があります。
しかし、パスワードをしっかりと管理することや、スマートフォンなどで入力することはユーザー体験上あまり良いものではなく、不便なことが多いです。
そこで、Yahoo! JAPANでは現在パスワードを使わない(パスワードレス)ログインの仕組みを提供しています。
そして先日パスワードレスログインの一つである「生体認証を利用したログイン」(プレスリリース)をリリースしました。
今回は開発を担当しました「生体認証を利用したログイン」の裏側の仕組みであるWebAuthn(Web Authentication)を使いYahoo! JAPANがどのようにユーザーをログインさせているのかに焦点を当てて、WebAuthnのサーバーの実装に関して解説します。
もし、WebAuthnのしくみに興味があり、サーバー側の実装を知りたい場合の参考にしていただけたら幸いです。
Yahoo! JAPANが対応したWebAuthnとは
Yahoo! JAPANではスマートフォンデバイスなどに付属している生体認証機能を利用してログインできるサービスを提供しています。
Yahoo! JAPANでは生体認証機能を利用する際にFIDO2のWebAuthnという技術仕様を採用しています。
WebAuthnはFIDO(Fast IDentity Online 読み方はファイドです)認証を基礎とする、ブラウザー経由で生体認証を行うためのしくみです。
WebAuthn自体は2015年に認証に関するグローバルなコンソーシアムであるFIDOアライアンスからの技術提案をもとにして、 2016年からW3C(World Wide Web Consortium)において議論が交わされ、アップデートを繰り返し策定された仕様です( https://www.w3.org/TR/webauthn/ )。
WebAuthnに沿う実装(言い換えるとFIDO認証に沿う実装)を行うことで、生体情報などのクレデンシャルデータをサーバーに送ることなくユーザーはログインを行うことができます。
WebAuthnとFIDO認証について
FIDOアライアンスが進めているFIDOのプラットフォーム拡大のためのプロジェクトにFIDO2プロジェクトというものがあります。
WebAuthnはFIDO2プロジェクト内でのブラウザーを使いFIDO認証を実現させるための仕様です。
また、ブラウザーでWebAuthnを実現するために用意されているAPIのことをWebAuthn APIと言います。
したがって、WebAuthnの仕組みを理解するためにはFIDO認証に関する知識があることが望ましいです。
今回はWebAuthnに焦点を当てて解説しますのでFIDO認証に関しては以下のYahoo! JAPANの近藤や五味が執筆したTechblogを参考にしていただけたら幸いです。
また、特に参考にしていただきたいところに関して章名を書きました。
- 次世代認証プロトコルFIDOの動向の「UAFの仕様構成」
- FIDO認証の進化とさらなる応用展開 (第3回FIDOアライアンス東京セミナー講演)の「認証モデル」、「Web認証」
また、FIDOアライアンスが出しているスライドも参考にしてください。
WebAuthnの処理について
WebAuthn API(実際にブラウザーにアクセスする関数)から得られた値を使ってどのようにサーバーで登録や認証を行うのかを解説します。
以下の図がクライアント、サーバーの構成図です。
構成図に登場する用語に関して以下にまとめました。
名前 | 解説 |
---|---|
認証器 |
公開鍵暗号方式を用いるうえで秘密鍵と公開鍵の作成を行い、秘密鍵をセキュア領域に保存しておく機能を持つもの 認証時には本人性の検証結果に対して秘密鍵を用いて署名を行う Authenticatorとも言われます |
ブラウザー |
WebAuthn APIの機能を積んでいるブラウザー 現在ではFirefox、ChromeやEdgeなどの主要なブラウザーで対応されている |
RP |
Relying Partyの略 WebAuthn APIを使用するサービス事業者 今回の説明ではRPはIDプロバイダーを指します |
RPアプリケーション |
ブラウザー上で動くRPが提供しているアプリケーション 鍵登録を行うユーザーの設定画面やログインを行う画面をイメージしてもらいたいです |
RPサーバー |
RPが提供している認証サーバー |
FIDO2サーバー |
WebAuthnでの認証を実現するためのサーバー 大きな役割は以下の3つ 1. challengeを含むWebAuthn APIで使用する値を作成する 2. 認証器から送られてきた公開鍵を検証して格納する 3. 認証器が署名した本人性の確認結果の検証を行う 詳細に関しては後述します |
データベース |
「challengeとIDの紐づけ」や「公開鍵」や「認証回数」などを格納するデータベース challengeの値などは一時的な格納、公開鍵は恒久的、認証回数は毎回更新するなど、特徴が違うので用途のよってデータベースを使い分けたほうが良いです(今回は説明を簡略化するのために同じデータベース扱いにします) |
登録と認証の流れ
次にWebAuthnでの鍵登録処理と認証処理の流れをシーケンス図を使って見ていきます。
(今回はサーバーの実装をピックアップしているので認証器の処理はかなり簡略化して書いています)
以下のシーケンス図はユーザーが認証時の署名検証に必要な公開鍵を登録するまでの処理を表しています。
図3:登録のシーケンス図
ここで重要なのが、認証器が生体認証を扱うものだとしても、生体情報を含むクレデンシャルデータは認証器のセキュア領域に保存され、サーバーへは送られません。
FIDO2サーバーのエントリーポイントである/attestaion/options
と/attestaion/result
の詳細についてはサーバーでの実装のところで詳しく紹介いたします。
以下のシーケンス図はユーザーがログインを行うまでの処理を表しています。
登録のときと同様に、認証のときも生体情報を含むクレデンシャルデータは認証器の中で本人性の検証結果の署名に使われるだけなのでサーバーに送られることはありません。
FIDO2サーバーのエントリーポイントである/assertion/options
と/assertion/result
の詳細についてもサーバーでの実装のところで詳しく紹介いたします。
以下はシーケンス図上でやりとりされていたパラメーターの説明です。(各パラメーターの中身に関しては後述)
名前 | 解説 |
---|---|
attestation |
認証器の正当性を証明するためのデータ(認証器の構成証明) ユーザーの認証用公開鍵もattestationの中に含まれています 主に登録時に使用されます |
assertion |
認証器で行った本人性の検証した結果のデータ(本人性の認証証明) 主に認証時に使用されます |
options | WebAuthn APIを使用するときに必要なパラメーター郡 |
削除機能
実際の運用に際してはスマートフォンや外部認証器の買い替え、紛失、破損に伴いユーザーが登録した公開鍵を削除したいケースが存在します。
したがって、データベースに登録した公開鍵を削除する機能が必要です。削除の実装はサービス事業者側の実装に任されており、今回はWebAuthn APIに関連するサーバー機能に焦点を当てているので、削除機能に関しては割愛いたします。
詳細:https://www.w3.org/TR/webauthn/#sample-decommissioning
ブラウザーでの実装
前述のシーケンス図(図3と図4)でのWebAuthnAPIについて解説いたします。
名前 | 解説 |
---|---|
navigator.credentials.create() | 秘密鍵、公開鍵の鍵ペアを作成して、attestationを作成するメソッド |
navigator.credentials.get() | 本人性の確認をした結果を秘密鍵を使って署名したものを返すメソッド |
以下では登録と認証で使用しているnavigator.credentials.create()
とnavigator.credentials.get()
でどのような値が返ってくるのかを確認します。
navigator.credentials.create()について
navigator.credentials.create()
メソッドに渡さないといけない値は/attestation/options
から受け取る値を使用します。(実際には型の変換などが必要なのでJavaScriptでの処理が多少必要です)
渡す引数に関しては後述の/attestation/optionsの処理で解説します。
まずはnavigator.credentials.create()
から返ってくる値を確認します。
{
"id": "EbNlO4Ai2F-2ZDe2Kd6FG_VEtFkymOI8ML6ljEnZXYGoCII1FY4dm8UHRQtBNlJ37WBfy9VEjFunDhlCPW8Vbg",
"rawId": "EbNlO4Ai2F-2ZDe2Kd6FG_VEtFkymOI8ML6ljEnZXYGoCII1FY4dm8UHRQtBNlJ37WBfy9VEjFunDhlCPW8Vbg",
"type": "public-key",
"response": {
"attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2l 〜中略〜 ftYF_L1USMW6cOGUI9bxVupQECAyYgASFYIGNS96nGQ5mPVrSeWQOMTuPpA-fjiQyfuZVf-_7ol884IlggTQGANRYL_ajap1v8cO_vokedD3FPk2taaUE82WxEUfY",
"clientDataJSON": "eyJjaGFsbGVuZ2UiOiJObVptTURJM1lXW 〜中略〜 HM6Ly9sb2dpbi55YWhvby5jby5qcCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ"
}
}
それぞれの値の説明は以下です。
名前:型 | 解説 |
---|---|
id : String | 生成された公開鍵のIDを示すrawIdをbase64urlエンコードした文字列 |
rawId : ArrayBuffer | 生成された公開鍵のIDを示す値 |
type : String | 'public-key'固定の文字列 |
response : Object | 認証器で生成された公開鍵情報を含むオブジェクト |
response.clientDataJSON : ArrayBuffer | 認証器にクライアントから渡したデータをJSONシリアライズされたデータ |
response.attestationObject : ArrayBuffer | 公開鍵データや認証器の情報やattestationを検証するための値などが入ったデータ |
実際にHTTP通信でサーバーに値を送ることを考えるとRPアプリケーションでArrayBufferになっている値をbase64urlエンコードをしておくと良いです。
上記の値を使いどのように公開鍵の登録処理を行うかに関しては登録(公開鍵の登録)で詳しく解説します。
navigator.credentials.get()について
navigator.credentials.get()
メソッドに渡さないといけない値は/assertion/options
から受け取る値を使用します。(実際には型の変換などが必要なのでJavaScriptでの処理が多少必要です)
渡す引数に関しては後述の/assertion/optionsの処理で解説します。
navigator.credentials.get()
から返ってくる値を確認します。
{
"id": "8nd2hK0_D3D-7Wyn22Hc5M3royPMJyMetjQeKG13H90BW3I8P3M0zQwc2vjeP0q8-nsD3sOJz05lf5vohOFvlQ",
"rawId": "8nd2hK0_D3D-7Wyn22Hc5M3royPMJyMetjQeKG13H90BW3I8P3M0zQwc2vjeP0q8-nsD3sOJz05lf5vohOFvlQ",
"type": "public-key",
"response": {
"authenticatorData": "sVezMUrLXoraigep3QdZ 〜中略〜 DYfY-6gtgHbEBAAAAJg",
"signature": "MEUCIQC-QZkGD6Q_4u__Jqm6b7 〜中略〜 QrTLJrgIgHHqetV1w-o6JwtBEoNrc4U2yuDWWLNf4yt0qgzUpTVc",
"clientDataJSON": "eyJjaGFsbGVuZ2UiOiJOekZp 〜中略〜 sb2dpbi55YWhvby5jby5qcCIsInR5cGUiOiJ3ZWJhdXRobi5nZXQifQ"
}
}
それぞれの値の説明は以下です。
名前:型 | 解説 |
---|---|
id: String | 認証時に使われた公開鍵のIDを示すrawIdをbase64urlエンコードした文字列 |
rawId : ArrayBuffer | 生成された公開鍵のIDを示す値 |
type: String | 'public-key'固定の文字列 |
response: Object | 認証器で署名された情報や、認証器の情報などを含んだオブジェクト |
response.authenticatorData: ArrayBuffer | 認証時の情報が入ったデータ |
response.clientDataJSON: ArrayBuffer | 認証器にクライアントから渡したデータをJSONシリアライズされたデータ |
response.signature: ArrayBuffer | 秘密鍵を使って本人性の検証を行った結果を署名したデータ |
実際にHTTP通信でサーバーに値を送ることを考えるとRPアプリケーションでArrayBufferになっている値をbase64urlエンコードをしておくと良いです。
上記の値を使いどのように公開鍵の登録処理を行うかに関しては認証(署名の検証)で詳しく解説します。
サーバーでの実装
先程のシーケンス図(図3と図4)でサーバーで何をしていたのかを簡単に示しました。
以下ではサーバーでの処理に関してエントリーポイントごとに何をしているのかを紹介いたします。
登録(公開鍵の登録)
以下は公開鍵の登録時に使うエントリーポイントです。
# challengeを含んだ鍵作成時に必要な値を渡すためのエントリーポイント
/attestation/options
# 実際に認証器で作成した公開鍵を検証して格納するためエントリーポイント
/attestation/result
/attestation/options
はnavigator.credentials.create()
で必要な引数を作成してブラウザーに返すエントリーポイントです。
主にはランダムなchallengeの値を作成したり、RP情報を含んだoptionsを作成します。
/attestation/result
はnavigator.credentials.create()
で生成された値をパースして検証し、公開鍵をデータベースに登録エントリーポイントです。
次は各エントリーポイントの処理の詳細を解説します。
/attestation/optionsの処理
以下は/attestation/options
で行っている処理の流れです。
- リクエストパラメーターを受け取る
- ランダムなchallengeの値を作成する
- challengeをキーにしてIDをデータベースに格納する
- navigator.credentials.create()の引数に必要な値を生成する
- ブラウザーに値を返す
リクエストパラメーター
/attestation/options
でのリクエストパラメーターに関して以下にサンプルを載せます。
{
"username": "test_id@example.com",
"displayName": "テストID",
"authenticatorSelection": {
"requireResidentKey": false,
"authenticatorAttachment": "cross-platform",
"userVerification": "preferred"
},
"attestation": "direct",
"timeout" : 2000
}
名前:型 | 解説 |
---|---|
username : String | 現在登録処理を行っているユーザーを示す値 |
displayName: String | ユーザーが画面表示ように使っている値 |
authenticatorSelection : Object | RPから認証器への要求事項を示すオブジェクト |
authenticatorSelection.authenticatorAttachment : String |
認証器の接続状態を指定するための文字列 ビルドインの認証器を表す"platform"と外部接続の認証器を表す"cross-platform"があります |
authenticatorSelection.requireResidentKey : Boolean | 認証器にユーザーの情報を保存するかを指定する |
authenticatorSelection.userVerification : String | ユーザー認証をどこまで求めるかを指定する文字列 |
attestation : String | 認証器からの公開鍵作成時の情報がRPとしてどのくらい必要かを指定する文字列 |
timeout : Int | 認証のタイムアウト時間を指定する整数値(ms) |
navigator.credentials.create()の引数に必要な値
/attestation/options
ではブラウザーのnavigator.credentials.create()
で使用する引数を返す必要があります。
実際に返す値がどのようなものなのか、サンプルを以下に載せます。
{
"challenge" : "NmZmMDI3YWZlZDQ3M2RmMThiYTQ1YTMyNmYxNDljMWY",
"rp" :{
"id" : "yahoo.co.jp",
"name" : "Yahoo Japan Corporation"
}
"user" : {
"id" : "test_id",
"name" : "test_id@example.com",
"displayName" : "テストID"
},
"pubKeyCredParams" : {
{
"type" : "public-key",
"alg" : -7
}
},
"authenticatorSelection" : {
"requireResidentKey" : false,
"authenticatorAttachment" : "cross-platform",
"userVerification" : "preferred"
},
"attestation" : "direct",
"timeout" : 20000
)
名前:型 | 解説 |
---|---|
challenge: ArrayBuffer |
FIDO2サーバーから渡ってくるランダム値 詳細は[clientDataJSONについて](#clientdatajsonについて)を参照 |
rp: Object | RPの情報を含むオブジェクト |
rp.name: String | RPの名前(サービス事業者名など)を示す文字列 |
rp.id: String |
RPの識別子(ドメインなど)を示す文字列
サイトを区別するためのスコープの役目も果たしています |
user: Object | 公開鍵を作るユーザー名を含むオブジェクト |
user.id: ArrayBuffer | ユーザー識別子を示す値 |
user.name: String | ユーザー名(メールアドレスなど)を示す文字列 |
user.displayName: String | ユーザーに表示するための名前を示す文字列 |
pubKeyCredParams: Object | 公開鍵作成時の情報を含むオブジェクト |
pubKeyCredParams.alg: String | 鍵作成時のアルゴリズム(COSE Algorithms準拠(注:http://self-issued.info/docs/draft-jones-webauthn-cose-algorithms-01.html ))を示す文字列 |
pubKeyCredParams.type: String | 'public-key'固定の文字列 |
authenticatorSelection: Object | RPから認証器への要求事項を示すオブジェクト |
authenticatorSelection.authenticatorAttachment: String | 認証器の接続状態を指定するための文字列 |
authenticatorSelection.requireResidentKey: Boolean |
認証器にユーザーの情報を保存するかを指定する デフォルトはfalse |
authenticatorSelection.userVerification: String | 認証器でユーザーの本人確認を行うかを指定 |
attestation: String | 認証器からの公開鍵作成時の情報をRPがどのくらい必要かを指定する文字列 |
timeout: Int | 認証のタイムアウト時間を指定する整数値 |
/attestation/resultの処理
以下は/attestation/result
で行っている処理の流れです。
navigator.credentials.create()
で生成された値を受け取る- clientDataJSONをbase64urlデコードする(RPアプリケーションでエンコードしているので)
- clientDataJSONをJSONデコードする
- challengeの検証
- attestationObjectをbase64urlデコードする(RPアプリケーションでエンコードしているので)
- attestationObjectをCBORデコードする
- attestationの検証を行う
- attestationObjectの中に含まれているauthenticatorDataをパースする
- 各種パラメーターの検証
- authenticatorDataに含まれている公開鍵の要素から検証に使える公開鍵を作成する
- IDと紐づけて公開鍵をデータベースに格納する
- 認証回数をデータベースに格納する
CBORについて
attestationObjectは(CBOR_Concise Binary Object Representation)(注:http://cbor.io/ )でエンコードされています。FIDO2では認証器などの物理デバイスで扱うメッセージフォーマットのサイズを小さくするため、CBORでのエンコードが採用さ れています。
attestationObjectについて
attestationObjectは認証器の正当性を証明するためのデータが格納されているオブジェクトで図5のような構成です。
以下がattestationObjectの中身です。
名前 | 解説 |
---|---|
authenticatorData | authenticatorDataは認証器の信頼性や認証器自体の情報に関しての情報が格納されているデータ 中身に関してはバイナリで表されており、bytes単位で項目ごとに区切って値を取得する必要があります |
fmt |
attestation自体の正当性を証明するための方法(Attestation Statement Format)がどのような種類なのかを示した値 詳細に関してはこの後の[attestationの検証](#attestationの検証)を参照 |
attStmt | Attestation Statementのことを指しており、fmtの値に合わせてattestationの検証に必要な値が格納されている |
さらにauthenticatorDataの中身に関してもう少し詳しく見てみましょう。
名前 | 長さ(bytes) | 解説 |
---|---|---|
rpIdHash | 32 bytes | 認証時に指定したrpIdをSHA256でハッシュ化した値 |
flags | 1 bytes |
User Verification(UV)やUser Presence(UP)などが格納されている値 それぞれbit(0 or 1)で表現されており、bit単位で区切って値を確認する必要がある UVは認証器を用いてユーザーに対して本人性の確認のために生体認証やPIN認証などを行うことを指します UPは認証器に触れるようなユーザーの存在を確認する動作を指します(ユーザーの本人性の検証には使えない) |
signCount | 4 bytes | 認証器での認証回数 |
attestedCredentialData | 可変 |
認証器自体の情報や公開鍵情報が格納されている 公開鍵データがどのくらいのデータサイズなのかもattestedCredentialDataに格納されており、他のデータと同じようにbyte単位で区切ってデータを取得する |
これらの値はパラメーターの検証時に必要なのでパースしておく必要があります。
公開鍵を作成する
attestedCredentialDataの中に含まれている公開鍵の要素を使い公開鍵を作成する必要があります。
以下では公開鍵を構成するattestedCredentialDataの中身に関して解説します。
(attestedCredentialDataもバイナリで表現されており、bit単位で区切ってパースする必要があります)
名前 | 長さ(bytes) | 解説 |
---|---|---|
aaguid | 16 bytes | 認証器ごとの識別子 |
credentialIdLength | 2 bytes | credentialIdのサイズを示した値 |
credentialId | credentialIdLengthで示した長さ | 公開鍵ごと割り振られているユニークなID |
credentialPublicKey | 可変 |
RP側で指定した鍵のアルゴリズムに従って作成された公開鍵の要素 |
credentialPublicKey中にあるkty
には暗号アルゴリズムを示すEC
やRSA
などの値が格納されています。
これらの暗号アルゴリズムに従い公開鍵を作成します。
作成方法に関してはJWKの仕様を参照してください。
clientDataJSONについて
以下はclientDataJSONの中に格納されている値です。
名前 | 解説 |
---|---|
challenge |
サーバーで発行したランダムな文字列 リプレイ攻撃対策に使用されます 一時的に保存する値なので、指定した時間を過ぎたら破棄するような実装が必要 |
type |
正当な署名を別の署名に置き換えてしまうような攻撃の対策のために設定されている値 公開鍵作成時(navigator.credentials.create())は"webauthn.create"、認証時(navigator.credentials.get())は"webauthn.get"という値が設定されている |
origin | WebAuthnAPIの呼び出し元の情報(ex. https://login.yahoo.co.jp など) |
これらの値はchallengeの検証や各種パラメーターの検証での検証で使います。
challengeの検証
/attestaion/options
で発行したchallengeの値を検証する必要があります。
/attestaion/options
で発行したchallengeの値はデータベースに格納されているので、その値と突合させることによりclientDataJSONに含まれたchallengeの値の検証ができます。
このとき、challengeに紐付いたIDをデータベースから取得します。
各種パラメーターの検証
上記の各種パラメーターの検証
に関して載せます。
- originの検証
- rpIdHashを使いrpIdが一致するか検証
- typeが'webauthn.create'になっていることを確認
- flagsの検証
- UPの確認
- UVの確認
attestationの検証
認証器で作成されたattestation(正確にはattestationObject)の検証を行います。
これはサーバーに渡ってきた値が本当に信頼できる認証器で作成されたかを確かめる必要があるからです。
attestaionの検証にはAttestation Statement Format
に従って検証を行う必要があります。
Attestation Statement Format
はattestationのfmt
部分に書かれている値で種類を判別することができ、大きく6つのフォーマットがあります。
名前 | 解説 |
---|---|
Packed Attestation Statement Format |
WebAuthn仕様に最適化されたフォーマット コンパクトながらも拡張性があります https://www.w3.org/TR/webauthn/#packed-attestation |
TPM Attestation Statement Format |
主にWindows系の認証器で使われているフォーマット https://www.w3.org/TR/webauthn/#tpm-attestation |
Android Key Attestation Statement Format |
主にAndroidデバイスで使われているフォーマット https://www.w3.org/TR/webauthn/#android-key-attestation |
Android SafetyNet Attestation Statement Format |
主にAndroidデバイスで使われているフォーマット 2018年現在では市場に出ているAndroidデバイスの多くはこのフォーマットを使っている https://www.w3.org/TR/webauthn/#android-safetynet-attestation |
FIDO U2F Attestation Statement Format |
FIDO U2Fとの互換性を担保するためのフォーマット https://www.w3.org/TR/webauthn/#fido-u2f-attestation |
None Attestation Statement Format |
RPがattestaion情報を受け取ることを希望しないときのフォーマット https://www.w3.org/TR/webauthn/#none-attestation |
また、認証器ごとの情報が記載されているMetadataを使い、サーバー側が受け入れ可能な認証器でattestationが発行されたかを検証する必要もあります。
FIDOアライアンスはMetadataを取得できるMetadata Service(MDS)を提供しており、Metadataの更新があった際にはMDSに問い合わせをして、対象の認証器のMetadataが存在する場合は最新のMetadataを取得することが可能です。
Metadataの更新は登録のやるたびに行う必要はなく、1日1回更新があれば更新する程度で良いです。
MDSにMetadataが存在しない場合は、認証器ベンダーからMetadataを入手する必要があります。
これらのAttestation Statement Formatごとの検証方法やMDSの使い方などに関しては、Yahoo! JAPANとして、今回とは別にまとめていく予定です。
また、明日のアドベントカレンダーでは、同じくFIDO2サーバーの開発に携わった浜田が「FIDO2 attestation formatの紹介」という記事も公開します。
認証(署名の検証)
認証時に使うエントリーポイントは以下です。
# challengeを含んだ鍵作成時に必要な値を渡すためのエントリーポイント
/assertion/options
# 送られてきたsignatureを検証するエントリーポイント
/assertion/result
/assertion/options
はnavigator.credentials.get()
で必要な引数を作成してブラウザーに返すエントリーポイントです。
主にはランダムなchallengeの値を作成したり、署名作成時に必要なoptionsを作成します。
/assertion/result
はnavigator.credentials.get()
で生成された値をパースして検証し、渡ってきた署名を検証するエントリーポイントです。
次は各エントリーポイントの処理の詳細を解説します。
/assertion/optionsの処理
以下は/assertion/options
で行っている処理の流れです。
- リクエストパラメーターを受け取る
- ランダムなchallengeの値を作成
- challengeをキーにしてIDをデータベースに格納
- navigator.credentials.get()の引数に必要な値を生成
- ブラウザーに値を返す
リクエストパラメーター
/assertion/options
でのリクエストパラメーターに関して以下にサンプルを載せます。
{
"username": "test_id@example.com",
"userVerification": "preferred"
}
パラメーターの説明は以下です。
名前:型 | 解説 |
---|---|
username: String | 現在登録処理を行っているユーザーを示す値 |
userVerification: String | 認証器でユーザーの本人確認を行うかを指定 |
navigator.credentials.get()の引数に必要な値
/assertion/options
ではブラウザーのnavigator.credentials.get()
で使用する引数を返す必要があります。
実際に返す値がどのようなものなのか、サンプルを以下に載せます。
{
"status": "ok",
"errorMessage": "",
"challenge": "NzFiYTY2NTBmNTUwZjcwMDlmN2RmNDZhYjgzNTM0NmI",
"timeout": 20000,
"rpId": "yahoo.co.jp",
"allowCredentials": [
{
"id": "8nd2hK0_D3D-7Wyn22Hc5M3royPMJyMetjQeKG13H90BW3I8P3M0zQwc2vjeP0q8-nsD3sOJz05lf5vohOFvlQ",
"type": "public-key",
"transports" : {
"usb",
"nfc",
"ble",
"internal" }
}
],
"userVerification": "required"
}
名前:型 | 解説 |
---|---|
status: String | 今回の処理のステータスメッセージ |
errorMessage: String | エラーメッセージ |
challenge: String | ランダムなchallengeの値 |
timeout: Int | タイムアウト(ms) |
rpId: String | RPを表す文字列 |
allowCredentials: Array | ユーザーが認証器で作成した鍵ペアに紐付いている情報 |
allowCredentials.id : String | ユーザーが認証器で作成した鍵ペアに紐付いている値 |
allowCredentials.type: String | webAuthnでの認証を示すための値(public-key固定) |
allowCredentials.transports: Array |
認証器からassertionを受け取るときの通信手段のヒント USB、NFC、BLE、ビルドインなどが定義されている |
userVerification: String | 認証器でユーザーの本人確認を行うかを指定 |
/assertion/resultの処理
以下は/assertion/result
で行っている処理の流れです。
navigator.credentials.get()
で生成された値を受け取る- clientDataJSONをbase64urlデコードする(RPアプリケーションでエンコードしているので)
- clientDataJSONをJSONデコードする
- challengeの検証
- authenticatorDataをbase64urlデコードする(RPアプリケーションでエンコードしているので)
- authenticatorDataをCBORデコードする
- 各種パラメーターの検証
- authenticatorData + clientDataJSONハッシュの合成データを作成
- 上記で作成した合成データとデータベースに格納していた公開鍵を使ってsignatureの検証を行う(※)
- 今回渡ってきた認証器での認証回数のほうが前回の認証回数よりも大きいことを確認する
- 認証回数の更新を行う
(※)データベースに保存されている認証回数が0回であり、渡ってきた認証回数が0回の場合は確認しなくても大丈夫
authenticatorDataについて
認証時のauthenticatorDataについては登録時のauthenticatorDataと基本的には同じです。
ただし、公開鍵などの情報は格納されていないので、格納されているデータと、基本的にはrpIdHash
とflags
とsignCount
だけです。
clientDataJSONについて
認証時のclientDataJSONについては登録時のclientDataJSONと同じなので割愛させてもらいます。
challengeの検証
challengeを発行した先は/assertion/options
ですが、attestation/result
で行ったchallengeの検証と同じ処理を行います。詳細はchallengeの検証を参照
各種パラメーターの検証
- originの検証
- rpIdHashを使いrpIdが一致するか検証
- typeが'webauthn.get'になっていることを確認
- flagsの検証
- UPの確認
- UVの確認
signatureの検証
authenticatorData + clientDataJSONハッシュの合成データを作成
と上記で作成した合成データとデータベースに格納していた公開鍵を使ってsignatureの検証を行う
と書いた部分に関してはもう少し詳しく説明します。
認証器で秘密鍵を使って署名したものがsignatureです。そしてsignatureのもとになったデータがauthenticatorData
とclientDataJSONのハッシュ値
を結合させた値です(図6)。
合成値の作り方は以下です。
- authenticatorDataをbase64urlデコードし、バイナリデータを取得する(A)
- clientDataJSONをbase64urlデコードし、JSON文字列を取得する
- 上記のJSON文字列をSHA256でハッシュ化する(B)
- (A)と(B)を連結させる
上記で作成した合成データとデータベースに格納されている公開鍵を使用してsignatureの検証を行います。
その他注意事項
※注)clientDataJSONはデコードする前の値を使って デコードしたものを同じロジックでエンコードしたとしてもJSON形式である関係上 もとからあった改行やスペースに関してJSONデコード時に消えてしまい、もう一度エンコードしても同じハッシュデータにならないので注意が必要です。
※注)また、ハッシュアルゴリズムに関して、鍵の作成を除いてSHA256に統一されています。これはアルゴリズムの処理速度の差によってサイドチャネル攻撃が成功してしまう可能性があることに起因しています。
RPサーバーでの処理
最後に/assertion/result
の処理結果に応じて、RPサーバーがユーザー認証に必要なtokenやCookieを発行し、ログイン処理が終了が完了します。(今回はIDプロバイダーがRPなのでそのような処理になる)
まとめ
今回はYahoo! JAPANでリリースした「生体認証を利用したログイン」の裏側のしくみであるWebAuthnについて解説しました。
今回の解説でWebAuthnの処理の流れと必要なパラメーターに関して理解していただき、FIDO2サーバーの実装のイメージを持っていただければ幸いです。
また、今回紹介したパラメーターの他にも、いろいろなオプショナルパラメーターがありますので今回の記事を読んでいただき、興味を持ってもらいましたら、W3Cの仕様を見てみるのも良いです。
今後もYahoo! JAPANとして、WebAuthnなどの認証の最新仕様を追いつつ、ユーザーにより良いログイン体験を提供していきたいです。
参考リンク
- Web Authentication: An API for accessing Public Key Credentials Level 1
- Server Requirements and Transport Binding Profile
- FIDO2プロジェクト
- 次世代認証プロトコルFIDOの動向
- FIDO認証の進化とさらなる応用展開 (第3回FIDOアライアンス東京セミナー講演)
- FIDO認証の概要説明
- JWKの仕様
- WEB+DB PRESS Vol.107 Web Authentication API ......ブラウザから生体認証を行うしくみ
関連記事
- 「安全・安心・便利」FIDO(ファイド)を使った パスワードレスログインとは
(2019年2月20日 Yahoo! JAPANコーポレートブログ) - ヤフーが世界で初めて、「FIDO2」を活用したパスワード不要のウェブサービスを実現!(2019年4月15日 linotice)
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました