FIDO/WebAuthNにおけるAndroid SafetyNet AttestationとKey Attestationの違い
TL;DR
背景
FIDO/WebAuthN in Androidの文脈で、SafetyNet Attestation API とKey Attestationの違いってなんだっけ?というところから始まっています。
SafetyNet Attestation API
SafetyNet Attestaion APIのチェック項目
アプリがインストールされた端末のHardware/Software情報を分析し、端末の改ざんがないかチェックするAPIです。
APIを叩くことでAndroid Compatibility Test Suite(CTS)というOEMベンダーやHWプラットフォーム間で互換性を担保するためのテストスイートの結果から判定されます。「WebAuthN 8.5 Android SafetyNet Attestation Statement Format」のVerification Procedureに ctsProfileMatch
属性のチェックを要求されていますが*1、これがテストスイートの判定結果です。値がtrueであれば、root化などの改ざんされた痕跡がないことが保証されます。
しかしながら、CTSにパスしたからといってAuthentictorにTEEを使っていることは保証されません。つまり、入札要件にHardware-backed Authenticatorを要求される場合、通らない事態が起こりえます。例えば、米国では政府機関の職員はハードウェアベースのAuthenticatorを使うことが強く推奨されております*3が、これに批准するのはSP800-63-BのAAL3もしくはFIDOのAuthenticator Level 3 *4 になります。
Secure Element
つまりHALをインターフェースにしたTEEのようなTamper-resistantなハードウェアをチェックする機構がSafetyNet Attestation APIにも実装されたものの*5、Android 9(Pie)からでしか使えません。
チェックの経路
CTSは公開されていますが、アプリ開発者はCTSをUSBでつないだ端末もしくはエミュレーターでしか走らせることができません。つまり、ユーザーが認証情報を登録したいアプリおよびインストールされたローカル端末内でAttestationが完結するわけではありません。ドキュメントにある次の図が示すとおり、①アプリがGoogle Play Servicesの中にある SafetyNet Attestation API
を叩いて、②Google Play ServicesがGoogleのBackend APIに対して更にリクエストを投げる形になります。
こ子でのポイントはネットワーク通信がクライアントとGoogleのBackend APIおよびFIDOサーバーの間で計2回のネットワーク通信をすることになります。FIDOサーバーへの連携はまだしも、Google Backend APIへの互換性チェックは「10 things you might be doing wrong when using the SafetyNet Attestation API」にある通り多くの帯域や電力を消費すること*7 になるため、ユーザーにとって嬉しいことではありません。
Key Attestation
さて、Android 7(Nougat)ではKey Attestationが追加されました。「WebAuthN 8.5 Android SafetyNet Attestation Statement Format」*8にも書かれていますが、こちらとSafetyNet Attestation APIがある場合はこちらを使うほうがベター(SHOULD)です。これは次の点からだと自分は思います。
- Authenticatorの真正性を確認できる
- 登録に必要な情報以外を集める必要がない
- 通信量を減らせる
Authenticatorの真正性を確認できる
Google Play Storeがプリインストールされた端末の初期バージョンがAndroid 7の場合、attest_key()
を呼び出しTEEにあるAttestation Keyをつかうことで、 keyPairGenerator.generateKeyPair()
で生成される認証(Assertion)用鍵に対して署名ができます。Attestation Keyは工場出荷前にTEEに埋め込まれたものなので登録リクエストの証明書チェーンを検証することで、FIDO認定のAuthenticatorが使われたか判断することができます。
登録に必要な情報以外を集める必要がない
FIDOはプライバシーも考慮した仕様です。そのため、個人を特定できないように、大量生産したAuthenticatorごとに共通したAttestation Keyを埋め込んでいます*9。せっかく、そのような設計にしているのに、SafetyNetではその過程で窃取可能な情報の範囲を端末全体に広げてしまっています。その先がGoogleとはいえ、FIDOのプライバシーポリシーとしては望ましくないでしょう。
通信量を減らせる
GoogleのBackend APIに通信するとき、必ずしも端末の完全性に必要な情報のみを送るわけではありません。互換性チェックのため、様々な情報を多くの帯域や電力を生贄に消費することになります。FIDOはIoT分野の認証も狙ってるようなので、地味に大切じゃないかなぁ。これをKey Attestationにすれば、通信量はCRL関係の通信のみに限定されます。
といったことから、Androidアプリ開発者は次のようなフローで2つのAttestationを使い分ける形になると思います。試しに書いてみたので、実機での検証はまだです。
// Generate a Key Pair KeyPairGenerator keyPairGenerator = null; keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); AlgorithmParameterSpec params = new KeyGenParameterSpec.Builder("Key1", KeyProperties.PURPOSE_SIGN) .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) .setDigests(KeyProperties.DIGEST_SHA256) .setUserAuthenticationRequired(true) . setUserAuthenticationValidityDurationSeconds(5 * 60) .setAttestationChallenge("challenge".getBytes()) .build(); keyPairGenerator.initialize(params); KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); SecretKey privKey = (SecretKey) keyStore.getKey("Key1", null); SecretKeyFactory factory = SecretKeyFactory.getInstance(key.getAlgorithm(), "AndroidKeyStore"); KeyInfo keyInfo = (KeyInfo) factory.getKeySpec(privKey, KeyInfo.class); if (keyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware()) { // build Android Key Attestation Statement Format } else { // Send a request to SafetyNet Attestation API // Build Android SafetyNet Attestation Statement Format }
以上。
おまけ
AttestとCertifyの違いについて一時期ものすごく気になっていたのですが、最終的に次の様な使い分けになると自分の中で結論を出しています。
- Attest: 何かしらが本物であることを断言すること。対象が美術品などであれば鑑定人が必要で、対象が証言などであれば証人(及び証人の署名)が必要。
- Certify: 何かしらが基準に沿ったナニカであること。
なのでITにおける認証情報の登録の文脈で、例えば製造元の秘密鍵によって証明書チェーンが構築されるFIDOでは登録プロセスの一部でAttestされていると言えるのではないでしょうか。そもそもがここらへんは欧米発祥の概念であるので、しゅっと一言の日本語にするのは難しいと思いました。
...っていうことを国際法律事務所にいる弁護士の友人に聞いたところ「ん〜、同じじゃない?」と言われたので、拘ることを辞めました。
余談
最近、アウトプット疲れを起こしていたのですが、その一因として気負いすぎていたことがあると思いました。砕いてい言うと、発表・講演とブログ上でのアウトプットに対して同じ情報量と質を自分に対して求めていて、それが重荷になっていたようです。よって、改めてアウトプットの目的の1つである「自分の思考の整理をすること」に立ち止まり、つまりブログでは自分が理解できるレベルで満足する方向に決めました。 あと、発表や講演をする目的である「他の人に知見を共有する」には、それなりに聴講者のことを考える労力に加え、情報のまとめ・削減・制御が必要なので、その一部を構成する内容を順次公開していったほうが最終的な編集工数が減ると見込みました。
余談の余談
自分は文章化や可視化がめちゃくちゃ苦手(=それなりにするのに時間がかかる)ので、まずはバシバシ数を出して慣れようと思いました。
*1:https://www.w3.org/TR/webauthn/#android-safetynet-attestation
*2:https://developer.android.com/training/safetynet/attestation
*3:https://www.slideshare.net/FIDOAlliance/nist-80063-guidance-fido-authentication
*4:https://fidoalliance.org/certification/authenticator-level-3/
*5:https://source.android.com/compatibility/tests
*6:https://developer.android.com/training/safetynet/attestation
*7:https://android-developers.googleblog.com/2017/11/10-things-you-might-be-doing-wrong-when.html
*8:https://www.w3.org/TR/webauthn/#android-safetynet-attestation
*9:https://fidoalliance.org/wp-content/uploads/2014/12/FIDO_Alliance_Whitepaper_Privacy_Principles.pdf