ヤフー株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。LINEヤフー Tech Blog

テクノロジー

Yahoo! JAPANでの生体認証の取り組み(FIDO2サーバーの仕組みについて)

目次

  1. はじめに
  2. Yahoo! JAPANが対応したWebAuthnとは
  3. WebAuthnとFIDO認証について
  4. WebAuthnの処理について
    1. 登録と認証の流れ
    2. 削除機能
  5. ブラウザーでの実装
    1. navigator.credentials.create()について
    2. navigator.credentials.get()について
  6. サーバーでの実装
    1. 登録(公開鍵の登録)
      1. /attestation/optionsの処理
        1. リクエストパラメーター
        2. navigator.credentials.create()の引数に必要な値
      2. /attestation/resultの処理
        1. CBORについて
        2. attestationObjectについて
        3. 公開鍵を作成する
        4. clientDataJSONについて
        5. challengeの検証
        6. 各種パラメーターの検証
        7. attestationの検証
    2. 認証(署名の検証)
      1. /assertion/optionsの処理
        1. リクエストパラメーター
        2. navigator.credentials.get()の引数に必要な値
      2. /assertion/resultの処理
        1. authenticatorDataについて
        2. clientDataJSONについて
        3. challengeの検証
        4. 各種パラメーターの検証
        5. signatureの検証
        6. その他注意事項
      3. RPサーバーでの処理
  7. まとめ
  8. 参考リンク

はじめに

こんにちは、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ではスマートフォンデバイスなどに付属している生体認証機能を利用してログインできるサービスを提供しています。

WebAuthnの構成図
図1:生体認証を利用したログイン

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アライアンスが出しているスライドも参考にしてください。

WebAuthnの処理について

WebAuthn API(実際にブラウザーにアクセスする関数)から得られた値を使ってどのようにサーバーで登録や認証を行うのかを解説します。
以下の図がクライアント、サーバーの構成図です。

WebAuthnの構成図
図2:WebAuthnの構成図

構成図に登場する用語に関して以下にまとめました。

名前 解説
認証器 公開鍵暗号方式を用いるうえで秘密鍵と公開鍵の作成を行い、秘密鍵をセキュア領域に保存しておく機能を持つもの
認証時には本人性の検証結果に対して秘密鍵を用いて署名を行う
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の詳細についてはサーバーでの実装のところで詳しく紹介いたします。


以下のシーケンス図はユーザーがログインを行うまでの処理を表しています。

認証のシーケンス図
図4:認証のシーケンス図

登録のときと同様に、認証のときも生体情報を含むクレデンシャルデータは認証器の中で本人性の検証結果の署名に使われるだけなのでサーバーに送られることはありません。

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()メソッドに渡さないといけない値は/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()メソッドに渡さないといけない値は/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/optionsnavigator.credentials.create()で必要な引数を作成してブラウザーに返すエントリーポイントです。
主にはランダムなchallengeの値を作成したり、RP情報を含んだoptionsを作成します。

/attestation/resultnavigator.credentials.create()で生成された値をパースして検証し、公開鍵をデータベースに登録エントリーポイントです。

次は各エントリーポイントの処理の詳細を解説します。

/attestation/optionsの処理

以下は/attestation/optionsで行っている処理の流れです。

  1. リクエストパラメーターを受け取る
  2. ランダムなchallengeの値を作成する
  3. challengeをキーにしてIDをデータベースに格納する
  4. navigator.credentials.create()の引数に必要な値を生成する
  5. ブラウザーに値を返す

リクエストパラメーター

/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)

/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で行っている処理の流れです。

  1. navigator.credentials.create()で生成された値を受け取る
  2. clientDataJSONをbase64urlデコードする(RPアプリケーションでエンコードしているので)
  3. clientDataJSONをJSONデコードする
  4. challengeの検証
  5. attestationObjectをbase64urlデコードする(RPアプリケーションでエンコードしているので)
  6. attestationObjectをCBORデコードする
  7. attestationの検証を行う
  8. attestationObjectの中に含まれているauthenticatorDataをパースする
  9. 各種パラメーターの検証
  10. authenticatorDataに含まれている公開鍵の要素から検証に使える公開鍵を作成する
  11. IDと紐づけて公開鍵をデータベースに格納する
  12. 認証回数をデータベースに格納する

CBORについて

attestationObjectは(CBOR_Concise Binary Object Representation)(注:http://cbor.io/ )でエンコードされています。FIDO2では認証器などの物理デバイスで扱うメッセージフォーマットのサイズを小さくするため、CBORでのエンコードが採用さ れています。

attestationObjectについて

attestationObjectは認証器の正当性を証明するためのデータが格納されているオブジェクトで図5のような構成です。

attestationObjectの構成
図5:attestationObjectの構成

以下が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には暗号アルゴリズムを示すECRSAなどの値が格納されています。
これらの暗号アルゴリズムに従い公開鍵を作成します。
作成方法に関しては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をデータベースから取得します。

各種パラメーターの検証

上記の各種パラメーターの検証に関して載せます。

  1. originの検証
  2. rpIdHashを使いrpIdが一致するか検証
  3. typeが'webauthn.create'になっていることを確認
  4. flagsの検証
    1. UPの確認
    2. 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/optionsnavigator.credentials.get()で必要な引数を作成してブラウザーに返すエントリーポイントです。 主にはランダムなchallengeの値を作成したり、署名作成時に必要なoptionsを作成します。

/assertion/resultnavigator.credentials.get()で生成された値をパースして検証し、渡ってきた署名を検証するエントリーポイントです。

次は各エントリーポイントの処理の詳細を解説します。

/assertion/optionsの処理

以下は/assertion/optionsで行っている処理の流れです。

  1. リクエストパラメーターを受け取る
  2. ランダムなchallengeの値を作成
  3. challengeをキーにしてIDをデータベースに格納
  4. navigator.credentials.get()の引数に必要な値を生成
  5. ブラウザーに値を返す

リクエストパラメーター

/assertion/optionsでのリクエストパラメーターに関して以下にサンプルを載せます。

{
    "username": "test_id@example.com",
    "userVerification": "preferred"
}

パラメーターの説明は以下です。

名前:型 解説
username: String 現在登録処理を行っているユーザーを示す値
userVerification: String 認証器でユーザーの本人確認を行うかを指定

/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で行っている処理の流れです。

  1. navigator.credentials.get()で生成された値を受け取る
  2. clientDataJSONをbase64urlデコードする(RPアプリケーションでエンコードしているので)
  3. clientDataJSONをJSONデコードする
  4. challengeの検証
  5. authenticatorDataをbase64urlデコードする(RPアプリケーションでエンコードしているので)
  6. authenticatorDataをCBORデコードする
  7. 各種パラメーターの検証
  8. authenticatorData + clientDataJSONハッシュの合成データを作成
  9. 上記で作成した合成データとデータベースに格納していた公開鍵を使ってsignatureの検証を行う(※)
  10. 今回渡ってきた認証器での認証回数のほうが前回の認証回数よりも大きいことを確認する
  11. 認証回数の更新を行う

(※)データベースに保存されている認証回数が0回であり、渡ってきた認証回数が0回の場合は確認しなくても大丈夫

authenticatorDataについて

認証時のauthenticatorDataについては登録時のauthenticatorDataと基本的には同じです。
ただし、公開鍵などの情報は格納されていないので、格納されているデータと、基本的にはrpIdHashflagssignCountだけです。

 

clientDataJSONについて

認証時のclientDataJSONについては登録時のclientDataJSONと同じなので割愛させてもらいます。

challengeの検証

challengeを発行した先は/assertion/optionsですが、attestation/resultで行ったchallengeの検証と同じ処理を行います。詳細はchallengeの検証を参照

各種パラメーターの検証

  1. originの検証
  2. rpIdHashを使いrpIdが一致するか検証
  3. typeが'webauthn.get'になっていることを確認
  4. flagsの検証
    1. UPの確認
    2. UVの確認

signatureの検証

authenticatorData + clientDataJSONハッシュの合成データを作成上記で作成した合成データとデータベースに格納していた公開鍵を使ってsignatureの検証を行うと書いた部分に関してはもう少し詳しく説明します。

認証器で秘密鍵を使って署名したものがsignatureです。そしてsignatureのもとになったデータがauthenticatorDataclientDataJSONのハッシュ値を結合させた値です(図6)。

signatureの作成方法
図6:signatureの生成方法

合成値の作り方は以下です。

  1. authenticatorDataをbase64urlデコードし、バイナリデータを取得する(A)
  2. clientDataJSONをbase64urlデコードし、JSON文字列を取得する
  3. 上記のJSON文字列をSHA256でハッシュ化する(B)
  4. (A)と(B)を連結させる

上記で作成した合成データとデータベースに格納されている公開鍵を使用してsignatureの検証を行います。

その他注意事項

※注)clientDataJSONはデコードする前の値を使って デコードしたものを同じロジックでエンコードしたとしてもJSON形式である関係上 もとからあった改行やスペースに関してJSONデコード時に消えてしまい、もう一度エンコードしても同じハッシュデータにならないので注意が必要です。

※注)また、ハッシュアルゴリズムに関して、鍵の作成を除いてSHA256に統一されています。これはアルゴリズムの処理速度の差によってサイドチャネル攻撃が成功してしまう可能性があることに起因しています。

RPサーバーでの処理

最後に/assertion/resultの処理結果に応じて、RPサーバーがユーザー認証に必要なtokenやCookieを発行し、ログイン処理が終了が完了します。(今回はIDプロバイダーがRPなのでそのような処理になる)

まとめ

今回はYahoo! JAPANでリリースした「生体認証を利用したログイン」の裏側のしくみであるWebAuthnについて解説しました。
今回の解説でWebAuthnの処理の流れと必要なパラメーターに関して理解していただき、FIDO2サーバーの実装のイメージを持っていただければ幸いです。
また、今回紹介したパラメーターの他にも、いろいろなオプショナルパラメーターがありますので今回の記事を読んでいただき、興味を持ってもらいましたら、W3Cの仕様を見てみるのも良いです。

今後もYahoo! JAPANとして、WebAuthnなどの認証の最新仕様を追いつつ、ユーザーにより良いログイン体験を提供していきたいです。

参考リンク

関連記事

こちらの記事のご感想を聞かせください。

  • 学びがある
  • わかりやすい
  • 新しい視点

ご感想ありがとうございました

このページの先頭へ