2023/10/17
執筆者: 張 君子, 大池 陸斗, 小岩井 航介 サービス開発1部
先日公開されたAndroid14から、Credential Provider APIによるパスキー管理ができるようになりましたので、実際にパスキー管理を行うアプリを試作してみました。 弊社内で、アプリ・サービスの開発を通して技術知識を身に着ける取り組みとして、「つくる会」という取り組みがあり、この取り組みの中で、今回の試作を行いました。 「つくる会」の詳細については、別のブログで紹介予定です。
注意:ここでの記載内容は、執筆者の個人の意見であり、会社の見解を示すものではありません。また、こちらで紹介しているソースコードは、調査・試作目的で作られたものであり、実際に利用される場合には十分ご注意ください。 また、当記事執筆時点のCredential Provider APIのバージョンはbeta02となっており、その後のアップデートでAPIの利用方法が変更になっている可能性があります。
本アプリのソースコードはGitHubにて公開しております。https://github.com/ko-koiwai/MyCredentialManager
当ブログエントリは、中国語、英語でも公開しています。
秘密情報(パスワード)を外部に送信せず認証できる方式で、パスワードより安全な認証として、最近各種Webサービスで普及が進んでいます。 パスキーの詳細は、他のサイトで説明がされていますので、当記事では説明を省略させていただきます。
Androidでは、2022年12月からパスキーが利用可能となりましたが、Webサイトごとに作成されたパスキーは、Googleアカウントに紐づいて管理される前提となっています。 一方、いままでのパスワードの管理は、ユーザは自由に行うことができました。ノートに書いておくこともできますし、市販のパスワードマネージャアプリで管理することもできます。なお、auも、「データお預かり」アプリで、パスワード管理機能を提供しています。(https://www.au.com/mobile/service/data-oazukari/passwords/)
パスキーが、よりパスワードに近いユーザ体験を実現するために、Android 14からは、市販のパスワードマネージャアプリでもパスキーの管理が可能となります。それを実現するのが、AndroidのCredential Provider API という機能です。
パスキーの仕組みについての理解を深めるため、このAPIを利用して、実際にパスキー管理アプリを試作してみることにしました。
実際に着手したのは2023年6月末です。 この時点では、まだAPIがalpha版だったことに加え、ドキュメントが整備されておらず、APIリファレンス(https://developer.android.com/jetpack/androidx/releases/credentials) や、AOSP上の実際のAPIのソースコード(https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/credentials/credentials/src/main/java/androidx/credentials) から使い方を想像して実装していきました。 PendingIntentも理解していなかった我々には、非常にハードルが高く、android.credentialsで実装して動かないと悩んでいたら、実は androidx.credentialsを利用すべきだったことに気づいて、大幅に実装を直すようなこともありました。
7/27に、Googleから、Credential Provider 導入のためのガイドが公開されました。 (https://developer.android.com/training/sign-in/credential-provider) このガイドを見ながら、少しずつ動くようになりました。 それでも、日本語ガイドの情報が古かったりして、実際にパスキーの登録、認証ができるようになるには時間を要しました。
Credential Provider APIは、パスキーの登録フェーズ、パスキーによる認証フェーズ、ともに、2回のやり取りが発生します。 まずブラウザからのパスキー登録、認証リクエストはCredentialProviderService()を継承したサービスによって受け取ります。それらのリクエストはそれぞれオーバーライドしたonBeginCreateCredentialRequest, onBeginGetCredentialRequestで受け取れます。受け取ったリクエストはPendingIntentによりアプリのActivityに転送されます。 次に転送されたリクエストはそれぞれパスキーの登録、認証処理を経て、レスポンスがPendingIntentでサービスに返されます。 最後にレスポンスを受け取ったonBeginCreateCredentialRequest, onBeginGetCredenttialRequestはそれぞれ結果に応じてcallbackメソッドを呼び出します。
パスキーは、簡単に言ってしまえば、Webサイトごとに作成された公開鍵・秘密鍵ペアです。パスキーをWebサイトに登録する際には公開鍵を送信し、パスキーを実際に利用する際にはWebサイトから受信したチャレンジに対して秘密鍵で署名した結果を送信します。 秘密鍵は、Webサイトとユーザに紐づけてパスキー管理アプリが保存しておく必要がありますが、その方法はアプリに任されています。秘密鍵は大事な情報なので、簡単にアクセスされないように保存しておく必要があります。 今回のアプリでは、パスキーを理解することと、Credential Provider APIの利用方法を理解することが主眼ですので、パスキーの保管については、一旦SharedPreferenceにjson化して保管しています。
Credential Provider APIを通じてパスキーの登録、利用には、W3C WebAuthn仕様で定められたフォーマットに則ってデータを生成する必要があります。 このデータフォーマットが、少し複雑で、実装には苦労しました。
基本的には、Googleによる解説記事(英語版)の通りに実装すればよいのですが、途中で独自実装が必要な部分があります。 主には、クレデンシャルの保存の部分、RPIDの検証の部分と、WebAuthn仕様で定められたフォーマットに沿ってデータを保存する部分です。
Googleの実装ガイドでは、作成された秘密鍵、RPとCredential IDの組み合わせは、アプリ独自で保存することとあります。 今回は、クレデンシャルの安全な保存については、スコープ外としましたが、それでも、将来的に、保存処理をかんたんに実装できるように、データモデル(data class)の定義と、保存処理の抽象化を行ってあります。
パスキーは、ログイン先のドメイン(RP IDと呼ばれます)と紐づけて管理されます。登録後、ログイン時にも、RP IDが一致することを前提に、秘密鍵による署名が行われます。ここがパスキーのフィッシング対策の大前提になっているため、仮に悪意のあるWebサイトやアプリ等がRP IDを偽って登録しようとした場合には、それを防ぐ必要があります。
一方、ブラウザは任意のRP IDに対する要求を投げることができるので、パスキー管理アプリとして、どのブラウザを信用するかを決める必要があります。 ここの検証は、CallingAppInfo.getOrigin()を使って行うことができます。引数にはJSONを入れる必要がありますが、Google のパスワードマネージャが利用しているブラウザリストが下記に公開されており、これをキャッシュして使うことも可能です。 https://gstatic.com/gpm-passkeys-privileged-apps/apps.json なお、当リストは、あくまでGoogle Password Managerが利用するリストをGoogle社の好意で公開されているものであり、Google社ないしAndroid 公式見解としての信頼できるブラウザアプリの一覧ではない点に留意ください。
ブラウザに返却するjsonは、androidx.credentials.webauthn.AuthenticatorAttestationResponse と、androidx.credentials.webauthn.FidoPublicKeyCredentialと、 androidx.credentials.CreatePublicKeyCredentialResponseを組み合わせて作成することができるのですが、現状、これだけでは完結しません。
まず、AuthenticatorAttestationResponseにはcredentialPublicKeyを設定する必要があります。これは、COSE(RFC9052)という、CBOR(RFC7049)形式で表現された公開鍵が必要です。仕様をすべて読むととても難しいのですが、とりあえずES256形式の公開鍵をCOSEに変換するには下記コードで行うことができました。実際の認証器の挙動を見ながら、見よう見まねで実装したものです。
private fun getPublicKeyFromKeyPair(keyPair: KeyPair?): ByteArray { // credentialPublicKey CBOR if (keyPair==null) return ByteArray(0) if (keyPair.public !is ECPublicKey) return ByteArray(0) val ecPubKey = keyPair.public as ECPublicKey val ecPoint: ECPoint = ecPubKey.w // for now, only covers ES256 if (ecPoint.affineX.bitLength() > 256 || ecPoint.affineY.bitLength() > 256) return ByteArray(0) val byteX = bigIntToByteArray32(ecPoint.affineX) val byteY = bigIntToByteArray32(ecPoint.affineY) // refer to RFC9052 Section 7 for details return "A5010203262001215820".chunked(2).map { it.toInt(16).toByte() }.toByteArray() + byteX+ "225820".chunked(2).map { it.toInt(16).toByte() }.toByteArray() + byteY } private fun bigIntToByteArray32(bigInteger: BigInteger):ByteArray{ var ba = bigInteger.toByteArray() if(ba.size < 32) { // append zeros in front ba = ByteArray(32) + ba } // get the last 32 bytes as bigint conversion sometimes put extra zeros at front return ba.copyOfRange(ba.size - 32, ba.size)
}
なお、CBORの検証は、下記Webサイトが便利です。 https://cbor.me
さらに、当記事執筆時点ではW3C WebAuthn仕様で未取り込みのEasy Accessor Fieldを付与しないと、Chromeから認識されませんでした。そのため、APIで作成されたJSONに対して、publicKeyAlgorithm, publicKey, authenticatorDataの各項目を追加してからブラウザに返却しています。
今回試作したパスキー管理アプリは、webauthn.io, webauthn.me といったテスト用サイトで利用することが可能でした。一点、webauthn.meにおいては、デフォルトでresidentKey利用のセットがされておらず、そのままではAndroid標準のFIDO2のダイアログが表示されてしまいますので、Debugger上でauthenticatorSelection.residentKey = preferred, もしくは requireResidentKeyを設定する必要があります。
今回作成したパスキー管理アプリは、ブラウザからの利用は問題なく動くようになりましたが、アプリからの利用は、未完となっています。 Androidアプリでのパスキー利用は、アプリの署名値をキーにした利用と、Webドメインをキーにした利用の2パターンあります。Webドメインをキーにするには、そのアプリがWebドメインの持ち主が開発したアプリであることを確認することが必須です。(さもなくばフィッシングの温床となります) こちらは、DeepLinkでも用いられる Digital Asset Linksの仕組みを利用して、アプリの署名値を検証する必要がありますが、今回は時間切れでその実装まで行えませんでした。
すでにMac, iOS, Android, Windows それぞれでOS標準のパスキーが利用可能な今、Credential Provider APIによって第三者アプリがパスキー管理できるようになるメリットは何でしょうか。 第一に、異なるプラットフォーム間でパスキーが共有できることにあると思います。AndroidとiOSのスマホ2台持ちや、Windowsのパソコンとスマホでパスキーを共有することが、第三者パスキー管理アプリによってできるようになりました。 さらには、家族間、もしくは会社内で、パスワードの共有がどうしても必要な場合があった場合にも、今後はパスワードと同じ感覚でパスキーを共有することができるようになります。 一方、パスキーを使ったログインを提供するサービス目線では、課題も見えてきます。今までは、パスキーはOSが管理しているという、一定の安心感をもってサービスを提供することができていましたが、今後は、パスキーがだれによって管理されているかわからないわけです。パスワード管理アプリでも、情報漏洩のニュースを度々耳にすることがあると思いますが、同様にパスキーも今後絶対漏洩しない保証はないわけです。 万が一パスキーが漏洩した時、サービスを利用されているお客様の情報や資産をどう守るか、そういったことも今後は考えていく必要があります。 とはいえ、少なくともパスワードに比較すれば、パスキーは、使いまわしされない、フィッシングされないと、とても利点が大きいものですので、Credential Provider APIの導入で、さらにパスキーが普及し、インターネット全体としてのセキュリティが向上することが期待されます。
Credential Providerの実装にあたっては、FIDOアライアンスの中で、Google社の Adam Langley氏、Jerry Shi氏、AgileBits社(1Password)のRene Leveille氏、Dashlane社のRew Islam氏に助言を頂きました。