freeeの開発情報ポータルサイト

丑三つ時におくるWebAuthnの勘所

この記事は freee Developers Advent Calendar 2021 の5日目です。
こんばんは。Identity & Access Managementを担当している てらら(id:a_terarara) です。

先日 死霊館 を鑑賞しました。(ホラー注意)
この作品は実話から作られたということですが、購入した中古戸建てでポルダーガイストに出くわしたり悪魔にとり憑かれたり中々ハードなお話で最後までハラハラさせてくれました。
その中で、全ての時計が3時7分に止まるという現象があったのをきっかけに今回のAdvent Calendarは丑三つ時にお送りしようと思った次第です。

なお、丑三つ時って3時のことやろって思ってたら実は2時〜2時半のことらしいです。

WebAuthnとの出会い

2021年10月時点の私はWebAuthnに触れたことが無く
「パスワードの代わりに公開鍵を保存してそれを使った署名・検証するのがFIDOで、それを提供するためのWebAPIがWebAuthnや!」
程度の印象で考えていました。

まあざっくりそれで問題は無いはずです。

ちょうど社内でハッカソン的なイベントがあり、その中でWebAuthnをプロトタイプ実装してみました。(参考:2020年の開発合宿
実際に実装してみるとハマりポイントや考慮すべきポイントに気付くことが出来ましたのでそちらをまとめてみようと思います。

WebAuthnの概念についてはMDN Web Docsの記載の通りで、そちらの登録・認証処理を参考にさせていただいております。

WebAuthnによる登録手順と各アクションに関連する重要なデータの流れ
図1: WebAuthnによる登録手順と各アクションに関連する重要なデータの流れ
出典: MDN Web Docs - Web authentication の概念と使い方 - 登録
by Mozilla Contributors is licensed under CC-BY-SA 2.5.

WebAuthn による認証手順と各アクションに関連する重要なデータの流れ
図2: WebAuthn による認証手順と各アクションに関連する重要なデータの流れ
出典: MDN Web Docs - Web authentication の概念と使い方 - 認証
by Mozilla Contributors is licensed under CC-BY-SA 2.5.

ハマりポイント1:CBORって何

認証器の公開鍵登録時と認証器を使ったユーザ認証時に AttestationObject または authenticatorData を検証する必要があります。
愚直に実装しているとこのObjectの中身を知りたくなると思います。
ところがこの子中々読める形式になってくれません。

MDNでattestationObjectのページを見てみると、

After decoding the CBOR encoded ArrayBuffer, the resulting JavaScript object will contain the following properties:

とあります。 つまりJSONをCBORしてArrayBuffer形式にされているのでしょう。

もう頭が壊れるかと思いました。

CBORはRFC8949で定義されているデータモデルで、ざっくり「バイナリの入ったJSONを扱うときにコスパが良い」形式のようです。
今後CBOR Web Token(CWT)に出会うこともあるでしょうから、今の内に備えておきたいです。

なお、attestationObjectをパースする際にいくつか試してみました。

  • Node.jsで cbor - npm
  • webpack5で cbor - npm + そのままだとビルドできないので下記の設定等を追加する必要がある
  resolve: {
    fallback: {
      stream: 'stream-browserify'
    }
  }
~~~~~~~~~~
  plugins: [
    new webpack.ProvidePlugin({
      Buffer: ['buffer', 'Buffer'],
      process: 'process/browser'
    }),
  ]

正直、WebAuthn APIのリクエスト・レスポンスをJSONで扱ってくれるwebauthn-jsonが一番簡単でした。
しかしながら認証器からCBORで受け取ったものをClient-Sideでパースするのは処理効率的な問題があるので素直にServer-Sideで扱いたいところではあります。

ハマりポイント2:省略可能パラメータが多い

ハマりポイントというより考慮するポイントなのですが、WebAuthnに渡すパラメータは非常にシンプルです。

図1の①にあたるPublicKeyCredentialCreationOptionsの必須パラメータは

  • rp(relying party info)
  • user(user info)
  • challenge

の3つです。
図2の①にあたるPublicKeyCredentialRequestOptionsに至っては

  • challenge

のみです。
それ以外のパラメータが考慮不要かというとそういうわけでもないです。
例えば PublicKeyCredentialCreationOptions には authenticatorSelection.authenticator_attachment というパラメータがあります。
こちらで値を指定することでユーザの認証手段を指定できます。

  • 指定しない場合
    認証器による認証方法を選択させるポップアップ
    認証器による認証方法を選択させるポップアップ
  • "platform" を指定した場合
    認証器によるtouchIDを促すポップアップ
    認証器によるtouchIDを促すポップアップ

認証手段によっては紛失時やスマートフォン機種変更時などのリカバリー方針にも関わってきますので、設計の早い段階から抑えておく必要があります。

余談1:ところで、フィッシングは防げるのか

パスワードを用いたユーザ認証の弱点としてフィッシング耐性が挙げられます。 一方で FIDO Allianceの仕様概要によると、

全てのFIDOプロトコルは、公開鍵暗号方式に基づいており、堅牢なフィッシング耐性を有しています。

とあります。 実際どうなのか考えてみました。

よくあるフィッシング手法としてダミーのWebページを用意するものがあります。

攻撃者がダミーサイトを使ってユーザのパスワードを搾取する
攻撃者がダミーサイトを使ってユーザのパスワードを搾取する
ユーザのパスワードさえ知ってしまえば誰でもそのユーザになり変わることが出来るので、かなり厄介であります。

一方でWebAuthnを使ったサイトであれば、攻撃者がダミーサイトで搾取出来たものを使ったとしても正しいサイトが正しく検証すれば認証を弾くことが出来ます。

攻撃者がダミーサイトを使ってユーザの認証器を使ってsignatureを搾取する
攻撃者がダミーサイトを使ってユーザの認証器を使ってsignatureを搾取する
例えば正しいサイトから受けたchallengeを経由させて指定されたtimeout時間内にこの処理を行ったとしても、認証器および正しいサイト側ではRPの検証が行われます。
確かに堅牢なフィッシング耐性を有しているといえそうです。

余談2:WebAuthnにおけるサーバの役割はRP?

WebAuthnに出てくる登場人物としてサーバを指す場合は RP(Relying Party)に位置します。 サーバ側がパスワードを保持しないのであれば、ユーザのクレデンシャルを認証器に寄せることが出来るため、サーバ側はRPに位置付けられるのも分かるように思います。
ただ、十分にWebAuthnを活用できない場合、例えばResidentKeyを認証器側に保持しない場合はユーザのクレデンシャル(と紐づくKey)および属性情報がサーバ側で管理されることになるのでRPと言い切るのは辛いなぁと思います。
パスワードレスな世界を実現するためには前述のパラメータ周りをしっかり活用しなければなりませんね。

おわりに

時は師走だというのに空気をさらに凍えさせるようなホラー映画の紹介から始まりましたが、いかがでしたでしょうか。
WebAuthnに触れるだけであればブラウザからAPIを2つ呼び出すだけで使えるので、興味ある方はさくっと作ってみることをオススメします。
そして一緒に認証認可基盤の開発を行ってくれる有志が出来たら幸いです。

jobs.freee.co.jp

明日はデータエンジニアの島袋さんです!
freeeのデータエンジニアって実際どうなの?を赤裸々に語っていただけるそうです。お楽しみに!