🔨

webauthn-ruby を覗いてみた

2024/07/16に公開

https://github.com/cedarcode/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は認証機ごとに割り振られる値

となります。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/credential.rb#L10-L13

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential/creation_options.rb#L20-L57

CreationOptionsクラスのインスタンスを生成していることがわかります。

session[:creation_challenge] = options.challenge

チャレンジを生成してセッション変数に設定しています。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential/options.rb#L19-L22

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential/options.rb#L56-L59

Optionsクラスのchallengeメソッドでチャレンジを生成していることがわかります。

Verification phase

こちらのフェーズを確認していきます。

webauthn_credential = WebAuthn::Credential.from_create(params[:publicKeyCredential])

クライアントから送信された公開鍵認証情報を取得しています。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/credential.rb#L18-L21

PublicKeyCredentialWithAttestationクラスにはfrom_clientは実装されていないので、継承元のPublicKeyCredentialクラスを確認してみます。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential.rb#L11-L21

webauthn_credentialがPublicKeyCredentialWithAttestationクラスのインスタンスということがわかったので、verifyメソッドの定義を確認してみます。

  webauthn_credential.verify(session[:creation_challenge])

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential_with_attestation.rb#L12-L18

継承元のPublicKeyCredentialクラスにもverifyメソッドがあるようですが、いったん割愛して、
response.verifyのverifyメソッドの定義を確認してみます。

responseの正体はAuthenticatorAttestationResponseクラスのインスタンスのようです。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential.rb#L18

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential_with_attestation.rb#L8-L11

AuthenticatorAttestationResponseクラスのverifyメソッドを見てみます。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/authenticator_attestation_response.rb#L41-L51

このあたりが署名データの検証処理なのかなと想像しています。

続いて継承元のAuthenticatorResponseクラスのverifyメソッドを見てみます。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/authenticator_response.rb#L27-L47

チャレンジやオリジンなどの検証処理がされています。

ピックアップしてverify_item(:challenge, expected_challenge)を確認してみます。

verify_itemメソッドではsendメソッドを使って、動的にメソッドを呼び出しています。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/authenticator_response.rb#L63-L71

valid_challenge?メソッドを見てみるとチャレンジの検証をしているのがわかります。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/authenticator_response.rb#L81-L83

検証した結果に問題がなければ、公開鍵の登録を行います。

  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クラスのインスタンスのようです。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/credential.rb#L14-L17

認証プロセスの開始時に必要なオプションを設定しています。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential/request_options.rb#L10-L17

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential/options.rb#L13-L18

例ではオプションでallowを設定しています。
ユーザーが使用できるクレデンシャルを特定の認証器に制限しています。

session[:authentication_challenge] = options.challenge

チャレンジを生成してセッション変数に設定します。
これはCredential Registration - Initiation phaseと同じ仕組みでチャレンジを生成しています。

Verification phase

こちらのフェーズを確認していきます。

webauthn_credential = WebAuthn::Credential.from_get(params[:publicKeyCredential])

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/credential.rb#L22-L25

PublicKeyCredentialWithAssertionクラスにはfrom_clientは実装されていないので、継承元のPublicKeyCredentialクラスを確認してみます。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential.rb#L10-L21

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メソッドを確認してみます。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential_with_assertion.rb#L12-L24

Credential Registration - Verification phaseと同様、responseはresponse_classメソッドから作成されたインスタンスなので、AuthenticatorAssertionResponseクラスのインスタンスとなります。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key_credential_with_assertion.rb#L8-L11

AuthenticatorAssertionResponseクラスのverifyメソッドを見てみます。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/authenticator_assertion_response.rb#L40-L47

継承元のAuthenticatorResponseクラスでチャレンジなどの検証が行われます。
Credential Registration - Verification phaseで確認した内容と同様です。

その下にあるverify_item(:signature〜を確認するため、valid_signature?を見てみます。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/authenticator_assertion_response.rb#L56-L59

引数のWebAuthn::PublicKey.deserialize(public_key)は、PublicKeyクラスのインスタンスを返しています。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key.rb#L13-L38

PublicKeyクラスのverifyメソッドを見てみます。

https://github.com/cedarcode/webauthn-ruby/blob/f030a78bd14999ce4905b74df92d8ca548832455/lib/webauthn/public_key.rb#L53-L58

COSE(CBOR Object Signing and Encryption)アルゴリズムを使用して署名検証を行っていることがわかります。

  stored_credential.update!(sign_count: webauthn_credential.sign_count)

最後にサインインの回数を更新します。

まとめ

次世代認証とも言われていますが、仕組みを紐解いてみると意外と昔からある技術をうまく使っている印象でした。
なんだかセキュリティスペシャリスト(現:情報処理安全確保支援士)を勉強してたときのことが蘇ってきました。
このあたりは最近の試験にも取り上げられそうな気もします(すでに取り上げられてたりして)。

全然関係ないですが、公開鍵暗号方式が素因数分解に時間がかかることを利用したものであることをレポートにまとめてこい!と言われて頭を抱えた日を思い出しました。

話が逸れましたが・・・それなりにソースコードを読むだけでもかなり理解度が深まりました。
実際に実装する機会があれば、またこれを読み返してみたいと思います。

参考資料

FIDO

次世代認証技術「FIDO」

https://www.mhlw.go.jp/content/10800000/000537443.pdf

これまでの認証とFIDOの違い

https://www.cao.go.jp/others/soumu/pitch2m/pdf/20190820_6303siryou.pdf

認証認可の調査研究

https://www.mhlw.go.jp/content/12600000/000689498.pdf

WebAuthn demo

https://webauthn.io/

SMARTCAMP Engineer Blog

Discussion