webauthn-ruby を覗いてみた
WebAuthnは、Ruby on Railsや他のRubyアプリケーションでWeb Authentication (WebAuthn) APIを使用するためのGemです。WebAuthnは、パスワードレス認証を可能にし、ユーザーが生体認証(指紋や顔認証)やセキュリティキーを使用して安全に認証を行うことができます。
私自身、抽象的な理解で止まっていたので、仕組みを理解するために実装を少し覗いてみました。
FIDOやパスキーに関してはページの最後に参考資料をまとめました。
理解に自信がない方はこのページを読む前に先に確認してみてください。
厚生労働省の資料がとても分かりやすかったです。
webauthn-rubyを覗き見
READMEの見出しに合わせて見ていきます。
Credential Registration
クライアントとサーバーで鍵交換をします。
クライアントでは認証器で鍵を生成し、秘密鍵を保持します。
生成した公開鍵をサーバーに送信して保持します。
Initiation phase
こちらのフェーズを確認していきます。
if !user.webauthn_id
user.update!(webauthn_id: WebAuthn.generate_user_id)
end
まずユーザーに紐づくwebauthn_idがなければ、設定するようにしています。
options = WebAuthn::Credential.options_for_create(
user: { id: user.webauthn_id, name: user.name },
exclude: user.credentials.map { |c| c.webauthn_id }
)
新しいWebAuthnクレデンシャルを作成するためのオプションを生成しています。
オプションを紹介します。
user
ユーザーの情報を設定します。
これは必須のオプションです。
exclude
重複した認証器でのクレデンシャル生成が行われないように、登録済みの認証器を除外するためのオプションです。
例では少し紛らわしいですが、user.webauthn_idとuser.credentialsのwebauthn_idは異なるものです。
- user.webauthn_idはユーザーごとに一意の値
- user.credentialsのwebauthn_idは認証機ごとに割り振られる値
となります。
CreationOptionsクラスのインスタンスを生成していることがわかります。
session[:creation_challenge] = options.challenge
チャレンジを生成してセッション変数に設定しています。
Optionsクラスのchallengeメソッドでチャレンジを生成していることがわかります。
Verification phase
こちらのフェーズを確認していきます。
webauthn_credential = WebAuthn::Credential.from_create(params[:publicKeyCredential])
クライアントから送信された公開鍵認証情報を取得しています。
PublicKeyCredentialWithAttestationクラスにはfrom_clientは実装されていないので、継承元のPublicKeyCredentialクラスを確認してみます。
webauthn_credentialがPublicKeyCredentialWithAttestationクラスのインスタンスということがわかったので、verifyメソッドの定義を確認してみます。
webauthn_credential.verify(session[:creation_challenge])
継承元のPublicKeyCredentialクラスにもverifyメソッドがあるようですが、いったん割愛して、
response.verifyのverifyメソッドの定義を確認してみます。
responseの正体はAuthenticatorAttestationResponseクラスのインスタンスのようです。
AuthenticatorAttestationResponseクラスのverifyメソッドを見てみます。
このあたりが署名データの検証処理なのかなと想像しています。
続いて継承元のAuthenticatorResponseクラスのverifyメソッドを見てみます。
チャレンジやオリジンなどの検証処理がされています。
ピックアップしてverify_item(:challenge, expected_challenge)
を確認してみます。
verify_itemメソッドではsendメソッドを使って、動的にメソッドを呼び出しています。
valid_challenge?メソッドを見てみるとチャレンジの検証をしているのがわかります。
検証した結果に問題がなければ、公開鍵の登録を行います。
user.credentials.create!(
webauthn_id: webauthn_credential.id,
public_key: webauthn_credential.public_key,
sign_count: webauthn_credential.sign_count
)
Credential Authentication
次は交換した鍵を使ってログイン認証を行います。
Initiation phase
こちらのフェーズを確認していきます。
options = WebAuthn::Credential.options_for_get(allow: user.credentials.map { |c| c.webauthn_id })
optionsに代入されるのは、RequestOptionsクラスのインスタンスのようです。
認証プロセスの開始時に必要なオプションを設定しています。
例ではオプションでallowを設定しています。
ユーザーが使用できるクレデンシャルを特定の認証器に制限しています。
session[:authentication_challenge] = options.challenge
チャレンジを生成してセッション変数に設定します。
これはCredential Registration - Initiation phaseと同じ仕組みでチャレンジを生成しています。
Verification phase
こちらのフェーズを確認していきます。
webauthn_credential = WebAuthn::Credential.from_get(params[:publicKeyCredential])
PublicKeyCredentialWithAssertionクラスにはfrom_clientは実装されていないので、継承元のPublicKeyCredentialクラスを確認してみます。
PublicKeyCredentialWithAssertionクラスのインスタンスを生成していることがわかります。
stored_credential = user.credentials.find_by(webauthn_id: webauthn_credential.id)
認証器に紐づくcredentialを取得します。
webauthn_credential.verify(
session[:authentication_challenge],
public_key: stored_credential.public_key,
sign_count: stored_credential.sign_count
)
webauthn_credentialはPublicKeyCredentialWithAssertionクラスのインスタンスです。
verifyメソッドを確認してみます。
Credential Registration - Verification phaseと同様、responseはresponse_classメソッドから作成されたインスタンスなので、AuthenticatorAssertionResponseクラスのインスタンスとなります。
AuthenticatorAssertionResponseクラスのverifyメソッドを見てみます。
継承元のAuthenticatorResponseクラスでチャレンジなどの検証が行われます。
Credential Registration - Verification phaseで確認した内容と同様です。
その下にあるverify_item(:signature〜
を確認するため、valid_signature?を見てみます。
引数のWebAuthn::PublicKey.deserialize(public_key)
は、PublicKeyクラスのインスタンスを返しています。
PublicKeyクラスのverifyメソッドを見てみます。
COSE(CBOR Object Signing and Encryption)アルゴリズムを使用して署名検証を行っていることがわかります。
stored_credential.update!(sign_count: webauthn_credential.sign_count)
最後にサインインの回数を更新します。
まとめ
次世代認証とも言われていますが、仕組みを紐解いてみると意外と昔からある技術をうまく使っている印象でした。
なんだかセキュリティスペシャリスト(現:情報処理安全確保支援士)を勉強してたときのことが蘇ってきました。
このあたりは最近の試験にも取り上げられそうな気もします(すでに取り上げられてたりして)。
全然関係ないですが、公開鍵暗号方式が素因数分解に時間がかかることを利用したものであることをレポートにまとめてこい!と言われて頭を抱えた日を思い出しました。
話が逸れましたが・・・それなりにソースコードを読むだけでもかなり理解度が深まりました。
実際に実装する機会があれば、またこれを読み返してみたいと思います。
参考資料
FIDO
次世代認証技術「FIDO」
これまでの認証とFIDOの違い
認証認可の調査研究
WebAuthn demo
Discussion