コインチェックのパスキー機能導入のウラ側

この記事は、コインチェック株式会社（以下、コインチェック）アドベントカレンダー6日目（シリーズ1）の記事です。

https://qiita.com/advent-calendar/2024/coincheck 

---

こんにちは、コインチェックのアプリケーション基盤グループ（通称: 基盤G）の大曽根です。
基盤GではRuby, RailsのアップデートやCIの管理など開発全体に関わる部分の保守開発に加えて、認証・認可など各機能から共通で呼ばれる機能の開発を行なっています。

Coincheckは2024年11月13日よりパスキーを使った認証機能の提供を始めました（参考: [お知らせ](https://corporate.coincheck.com/news/vesXK9br) ）。今回はCoincheckでのパスキー導入の裏側のうち、課題として出てきた部分を中心にお話しできればと思います。

# パスキーとは
パスキーは、パスワード認証やSMS認証、TOTP認証が抱える問題を解消するために作られた認証方式です。パスキーの認証情報は端末やサービスごとに作成されるため、別サービスで使うこともフィッシングサイトで誤って使ってしまうこともありません。端末の設定によりますが、指紋認証や顔認証、PINコードを用いて認証することができます。

# プロジェクトを推進する上で出てきた課題
## パスキーという言葉のゆらぎ
”パスキー” という言葉はこの数年でだいぶ一般化してきたところですが、実はこの言葉の定義には揺らぎがあります。

当初（2023年末）に調べた段階では、

1. Webauthnという認証方式のことをパスキーと呼ぶケース
1. 生体認証のことをパスキーと呼ぶケース
1. Discoverable Credential（ユーザー識別子を含むFIDOクレデンシャル）をパスキーと呼ぶケース
1. デバイス間で同期されたFIDOクレデンシャルをパスキーと呼ぶケース
1. 3.と4.の両方を満たしたものをパスキーと呼ぶケース

など、サイトによってかなり揺らぎがありました。大きく分けると1と2が認証方式のことを、3以降がクレデンシャルのことを示していることになります。
また、Discoverable Credentialは旧名 Resident Key と呼ばれており、登録時の設定項目にもまだ名前がある状態で、言葉の意味が複雑な状態が続いています。

当初は5.の定義が多い印象でしたが、2023年に多数のサービスで導入が進んだ後には、1. の定義が多くなっている印象を受けました。
現在においては、1. を広義のパスキー、5. を狭義のパスキー と定義しているサイトもあります。

Coincheckにおけるパスキーの調査・開発が本格化したのは2024年の4月だったため、1.の定義をパスキーと定義してプロジェクトをスタートさせることにしました。
さらに、”パスキー” 以外にもわかりづらい単語がいくつかあったのでプロジェクトスタートに合わせて社内向けの用語集を作り、パスキーの定義やWebauthnとの意味合いの整理を行い、認識相違によるコミュニケーションのトラブルを回避することにしました。

この記事においても、パスキーはWebauthnのこと、と定義して説明しています。

## ビジネスサイドにどうやってわかりやすく説明するか

パスキーの研究、仮実装が進んだ段階で、他のバックエンドエンジニア、アプリ開発のエンジニアにはうまく伝えられる状態になりましたが、デザインやCS対応を行なっているメンバーなど、ビジネスサイドのメンバーにどうやって伝えるかが課題として生まれました。

パスキーの [登録、認証のフロー](https://developer.mozilla.org/ja/docs/Web/API/Web_Authentication_API) は少し複雑で、他サービスで使ったことはある場合でも、そのフローや内部的な仕組みはSMS認証やTOTP認証のように説明せずとも伝わるものではありませんでした。
そこでプロジェクトにビジネスサイドのメンバーが入った段階でパスキーの説明資料をつくり、定例会議の場で仕組みの概要を説明し、その後出てきた質問は適宜slackなどで回答していくことにしました。

（参考: 説明資料の目次）

![passkey-index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219455/93a60450-edfa-430f-b6f1-e04deae33c8e.png)

また、先行して弊社の管理画面側にパスキー認証機能を導入して実際に使っていただくことで、PCに限定されてしまいますがどういう画面が出てきてどう操作するのか、を理解するきっかけを作りました。

資料を共有・説明してPJに参加するメンバーの知識水準を上げていったことで、後々の動作検証で発生した問題を解消するときに関係者全員で議論ができるようになっただけでなく、FAQの精緻化にも貢献できるようになりました。

# 開発上の課題

## サーバサイド側の設定値をどう決めるか
パスキーの登録時、認証時には、サーバサイドからチャレンジに加えてサーバ側の設定情報を返す必要がありますが、これをどう設定するかは定例会議で何度も議論を重ねることになりました。項目ごとにすでにさまざまなサイトで説明がされていますが、OSやブラウザのアップデートに伴い挙動が変わっている点があるため改めて記載します。
従って、ここで記載している情報は、2024年12月現時点での挙動を元にした設定値となります。

登録時のレスポンス例

```json
{
  "challenge": "<チャレンジ文字列>",
  "timeout": "<タイムアウトのミリ秒数>",
  "extensions": {},
  "rp": {
    "name": "Coincheck",
    "id": "coincheck.com"
  },
  "user": {
    "name": "<ユーザ識別子>",
    "id": "<ユーザごとに固有な文字列>",
    "displayName": "<キーの名前>"
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -37
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "attestation": "direct",
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "residentKey": "preferred",
    "userVerification": "required"
  },
  "excludeCredentials": [
    {
      "type": "public-key",
      "id": "<キーの識別子>"
    }
  ]
}
```

| 項目 | 説明 | 採用した設定値 |
|:-----------|:------------|:------------|
| user.name <br> user.id <br> user.displayName | ・nameはユーザ名<br>・id は ユーザごとに変更されない一意な文字列が求められる <br>・displayNameはユーザの表示名 | ・name: Coincheckにログインしているユーザのメールアドレスが記録されるようにした <br> ・id: ランダム生成した文字列 <br>・displayName: “パスキー” の文字がセットされるようにした |
| pubKeyCredParams | どのアルゴリズムの利用を許可するかの設定。<br> IANAの [COSE Algorithms](https://www.iana.org/assignments/cose/cose.xhtml) のページでどのアルゴリズムかを参照できる。 | ES256、PS256、RS256の3種類を採用した。<br>現時点ではRS256はWindowsHello利用時に使われるため必須で、ES256はWindowsHello以外の認証方法で使うため必須とした。<br>他は必要に応じて社内で調査・検討を行い追加した。|
| attestation | 認証器のクレデンシャル情報をどのようにサーバサイドで受け取るかの設定 | “direct” に設定し、サーバ側で証明書の検証を行うことにした。<br>また、WindowsHelloにおいては、”direct” にしないとキーの種類IDであるaaguidが取得できない。 |
| authenticatorSelection.authenticatorAttachment | FaceIDやTouchIDなどのプラットフォーム認証器、Yubikeyなどのローミング認証器のどちらを使うか、あるいは両方利用可能にするか | “platform” に設定し、プラットフォーム認証器のみを許可することにした。 |
| authenticatorSelection.residentKey | ユーザーネームレス認証を利用可能にするために設定するもの | AndroidのChromeでは “preffered” ないし "required" に設定しないとデバイス間同期がされなかった |
| authenticatorSelection.userVerification | 端末側の認証操作（指紋/顔/PIN）を必須にするかどうか | 認証操作は必ず必要なので “required” を指定した |
| excludeCredentials | すでに登録されているキーの情報の配列 | 同一端末ですでに登録されているときに二重登録を防止できる |


認証時のレスポンス例

```json
{
  "challenge": "<チャレンジ文字列>",
  "timeout": "<タイムアウトのミリ秒数>",
  "extensions": {},
  "allowCredentials": [
    {
      "type": "public-key",
      "id": "<キーの識別子>"
    }
  ],
  "rpId": "coincheck.com",
  "userVerification": "required"
}
```

| 項目 | 説明 | 採用した設定値 |
|:-----------|:------------|:------------|
| allowCredentials | そのユーザに登録されているキーの情報の配列 | 当初空配列にしていたが、AndoridのChromeにおいてこれを返さないと認証時にキーが表示されず突破できない事象が発生したため返すように変更した| 
| userVerification | 端末側の認証操作（指紋/顔/PIN）を必須にするかどうか | 認証操作は必ず必要なので “required” を指定した| 

## AndroidのFacetIDに悩まされた件
サーバサイド、Webサイト側の実装が一通り進んだ後に、アプリエンジニアのパスキー開発が本格化しました。iOS・Android両方予定通り進んでいたのですが、Androidに限ってはFacetID（ファセットID）と呼ばれる特殊な値を用いる必要があり、工数増加の要因となりました。以下では、FacetIDに関わるAndroid側・サーバ側それぞれの対応をお話しします。

### Android側の対応
Credential Manager APIのパスキー作成・認証の実行結果に含まれるclientDataJSONのoriginが、Webドメイン（ `https://coincheck.com` ）ではなくFacetIDという値になっており、この値をサーバー側で検証できるようにする必要がありました。

FacetIDは、証明書のフィンガープリント(SHA-256)から生成されているためビルド時の証明書に紐づいています。
https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-appid-and-facets-v2.0-id-20180227.html#the-appid-and-facetid-assertions

#### FacetIDのフォーマット
`android:apk-key-hash:<base64_encoded_sha256_hash-of-apk-signing-cert>`

#### Androidでパスキーを動作させるには
1. 公開サーバーにアプリのSHA-256 証明書のフィンガープリントを追加したassetlinks.jsonを配置します。
※ 公開サーバーである必要があります。IP制限などをかけたサーバーではアプリ起動時に設定情報の取得ができずパスキーが動作しません。
2. 1.で追加した証明書のフィンガープリントに対応した証明書でビルドします。

### サーバ側の対応
サーバ側としては、Androidの場合に登録・認証時に返すレスポンスのうち rpId の部分を FacetID に変えて返却する必要が発生したため、Androidかどうかの判定条件を付け加える対応を行いました。

# 動作確認の課題
## 端末/OS/ブラウザによる挙動差異
開発中はMacでGoogleパスワードマネージャやiCloudキーチェーンがOFFの状態で開発をしていましたが、これが有効な場合や、他OS（Windows、iOS、Android）での挙動が異なり、一部はサーバサイドの設定値の変更を余儀なくされる結果となりました。また、OSのバージョンによっても挙動が変わるため、動作確認用の端末を数台用意する必要がある点が課題となりました。
社内の端末の利用ルールとパスキーの動作テストで求められる環境で相違がある場合、端末を用意するのに調整作業が発生するため、パスキー導入を検討される場合はテスト段階の端末・アカウントをどう用意するか、どの程度用意に時間がかかるかを見積もっておく必要があると感じました。

この問題はQAを実施する際にも表れ、QA担当者が持っている端末・アカウントの環境によってQA手順が変わることになり、テスト完了までの日数が増加する要因にもなりました。

# 導入の成果・まとめ
## 導入の成果
リリースして約3週間が経った現在でも、パスキー登録ユーザ数は増加の一途を辿っています。現段階ではパスキー認証の利用は任意としているため、あまり登録されないと想定していましたが、多くの方にパスキー認証をご利用いただけている状態となっております、ありがとうございます。
一方、登録したものの別なOSでうまくログインできなくなってしまった、などの要因でサポートに連絡をいただくケースもあり、パスキーを快適に利用していただくにはまだまだユーザのペインを取り除く必要があります。

## まとめ
今回はCoincheckにおけるパスキー導入の裏側で起きていた課題を中心にお話ししました。
パスキーは現在においては設定項目が複雑でわかりにくいこと、開発しているアプリケーションだけでなく、ブラウザやOSなどアプリケーション側でコントロールできない部分で挙動が変わる点があるため、開発時に考慮すべき点が通常のPJより幅広いと感じました。

パスキーはSMSやTOTP認証に比べて圧倒的な堅牢性を保つため、2023年からビッグテックを中心に導入が進んでいます。パスキーが利用できるサービスはデバイスやOSプラットフォームの進化も相まって今後も増えていき、より便利に使えるようになっていくと考えられます。
しかし、パスキーという言葉自体の定義に揺らぎがあるように、全てのサービスで同じように使えるようになるわけではありません。また、ユーザの使用する端末やOS、ブラウザの組み合わせにより登録できないケースや他デバイスで利用できないケースがあるのも事実です。

コインチェックではセキュリティ面からパスキーのさらなる普及を推進していく予定ですが、登録・利用できないケースの発生を限りなく0に近づけること（開発面の対応）だけでなく、FAQなどで様々なOS・ブラウザでの登録・認証フローを丁寧に説明すること（運用面の対応）が求められると考えています。

コインチェックでは皆様により安全に、そして快適にサービスを利用していただくための仕組みづくりを継続して行なっていきます。