さて、Androidのリファレンスを見ていたらFIDO2のライブラリが載っていて結構前から 試してみようと思いつつも後回しになっていたやつをやっと試してみたのです。
んでブラウザでのWebauthn APIとは若干違いがあったのでまとめてみますよ。
はじめに
Android7(Nougat)以上がFIDOアライアンスから認定されましたが、 これはAndroidでのブラウザ経由でのアクセスだけでなく、FIDO2プロトコルをサポートするネイティブアプリケーションも含まれます。
This gives users the ability to leverage their device’s built-in fingerprint sensor and/or FIDO security keys for secure passwordless access to websites and native applications that support the FIDO2 protocols.
で実際にAndroid端末でブラウザからはWebauthn APIが叩けるのを確認していたのですが、 今回NativeライブラリでのアプリからのFIDO2がどんなものか試してみました。
今回自分が作成したDemoアプリは以下になります。
https://github.com/mkontani/FIDO2AndroidDemoApp
app UI | waiting Verify | result Verify |
---|---|---|
んで一通りregister
, sign
が完了すると以下のような結果を出力します。
登録時(regist) | 検証時(verify) |
---|---|
Android NativeでのFIDO2実装
Optionsの設定
基本的にはWebauthn APIと同じような実装で違和感もありません。
以下にWebauthn APIとAndroid FIDO2 APIでのPublicKeyCredentialCreationOptions
リクエスト例をそれぞれ示します。
Webauthn APIの例
1navigator.credentials
2 .create({
3 publicKey:
4 {
5 rp: {
6 id: location.hostname,
7 name: location.hostname
8 },
9 user: {
10 id: window.crypto.getRandomValues(new Uint8Array(32)),
11 name: document.getElementById("username").value,
12 displayName: document.getElementById("displayname").value
13 },
14 challenge: window.crypto.getRandomValues(new Uint8Array(32)),
15 pubKeyCredParams: [{
16 type: "public-key",
17 alg: -7 // cose_alg_ECDSA_w_SHA256,
18 },
19 {
20 type: "public-key",
21 alg: -257 // cose_alg_RSA_w_SHA256,
22 }],
23 timeout: 60000,
24 excludeCredentials: [],
25 authenticatorSelection: {
26 authenticatorAttachment: checkedByName("authenticatorAttachment"),
27 requireResidentKey: false,
28 userVerification: checkedByName("userVerification1")
29 },
30 attestation: attestationConveyancePreference,
31 extensions: null
32 };
33 })
Android FIDO2 APIの例
1PublicKeyCredentialCreationOptions.Builder optionsBuilder = new PublicKeyCredentialCreationOptions.Builder()
2 .setRp(new PublicKeyCredentialRpEntity(
3 RPID,
4 "FIDO2demo",
5 null))
6 .setUser(new PublicKeyCredentialUserEntity(
7 userName.toString().getBytes(),
8 userName.toString(),
9 null,
10 displayName.getText().toString()))
11 .setChallenge(challenge())
12 .setParameters(new ArrayList<PublicKeyCredentialParameters>(Arrays.asList(
13 new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY.toString(), EC2Algorithm.ES256.getAlgoValue()),
14 new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY.toString(), RSAAlgorithm.RS256.getAlgoValue())
15 ))
16 .setTimeoutSeconds(Double.parseDouble(60))
17 .setAuthenticatorSelection(
18 new AuthenticatorSelectionCriteria.Builder().setAttachment(attachment).build())
19 .setAttestationConveyancePreference(conveyancePref)
20 .setAuthenticationExtensions(extensions)
21 .build();
上の例の通り、基本的にはWebauthn APIでのnavigator.credenticals.create
のoptionとして渡していたオブジェクトと同等のものを
FIDO2 APIでも同じ要領でBuilderメソッドで構成しているだけです。
SignでのPublicKeyCredentialRequestOptions
も同じ感じです。
詳細は公式ドキュメントで確認を。
唯一詰まったのは後で書きますがRPの検証のところくらいでしょうか。
これも先人の知恵のおかげで解決できました。 ありがたや。
処理フロー
これもAndroidでよくある捌き方です。
- 上の例で定義したようなOptionをパラメータとして
Intent
に渡してあげる。 - Intent先で認証器でのVerifyを行う
- Verify結果を
onActivityResult
で受け取る
それぞれ具体的に見ていきます.
OptionをパラメータとしてIntent
発動
以下はRegister時の例となります.
1Fido2ApiClient fido2ApiClient = Fido.getFido2ApiClient(getApplicationContext());
2Task<Fido2PendingIntent> result = fido2ApiClient.getRegisterIntent(options);
3result.addOnSuccessListener(new OnSuccessListener<Fido2PendingIntent>() {
4 @Override
5 public void onSuccess(Fido2PendingIntent fido2PendingIntent) {
6 if (fido2PendingIntent.hasPendingIntent()) {
7 try {
8 fido2PendingIntent.launchPendingIntent(activity, REGISTER_REQUEST_CODE);
9 } catch (IntentSender.SendIntentException e) {
10 Log.d(LOG_TAG, "onSuccess: Exception");
11 e.printStackTrace();
12 }
13 }
14 }
15});
16
17result.addOnFailureListener(new OnFailureListener() {
18 @Override
19 public void onFailure(@NonNull Exception e) {
20 e.printStackTrace();
21 }
22});
Intent先でのVerify
上でのIntent処理の結果、以下のようなVerify画面に飛びます。
ダイアログに従って検証操作を行います。
Verify Intent例 | Verify Pending |
---|---|
Verify結果の取得
上でのVerify結果をonActivityResult
で受け取ります。
上の例だと正常にいけばresultCode
にRESULT_OK
,
requestCode
にPendingIntentの際に指定したREGISTER_REQUEST_CODE
が返ります。
1@Override
2protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
3 super.onActivityResult(requestCode, resultCode, data);
4 switch (resultCode) {
5 case RESULT_OK:
6 if (data.hasExtra(Fido.FIDO2_KEY_ERROR_EXTRA)) {
7 handleErrorResponse(data.getByteArrayExtra(Fido.FIDO2_KEY_ERROR_EXTRA), requestCode);
8 } else if (data.hasExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA)) {
9 try {
10 handleResponse(data.getByteArrayExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA), requestCode);
11 } catch (CborException e) {
12 e.printStackTrace();
13 } catch (Exception e) {
14 e.printStackTrace();
15 }
16 }
17 break;
18 case RESULT_CANCELED:
19 Log.d(LOG_TAG, "onActivityResult: RESULT_CANCELED");
20 Toast.makeText(this, "Operation canceled...", Toast.LENGTH_LONG).show();
21 break;
22 default:
23 }
24}
んで後はdata
に入っている検証結果のbyte列を良しなに捌くといった感じです。
次項で実装してへぇ~、となったところを一応書き出しておきます。
Notice of FIDO2 API
RPID - ORIGINの関連付け方法
ここが唯一詰まったところ。 最初にざっくり実装し終えて動作確認したところ、以下のようなエラーが返ってきました..
code: SECURITY_ERR
message: The incoming request cannot be validated
まあググるとすぐ解にありつけてアプリのoptionで指定したrpId
の検証処理が抜けているというもの。
(参考: medium.com/@jedri/android-fido2-api-demo-422edd87f445)
FIDO2やU2Fの特徴的な制約として、認証経路の正当性をとるためにrpId
とサービス提供者(RP, origin)
の関係性チェック(通常rpIdとoriginが一致するかを見てる?)
が認証器でのプロトコルレベルで定義されているようです。
Webauthn API
ならブラウザで得られるorigin
をrpId
と照合すればいいわけですが、
Nativeアプリだと同じようにはいきません。
ということでNativeアプリではアプリとorigin
との関係性を確かめる手立てとして、
rpId
で指定したドメイン配下にassetlinks.json
を設置し、
そこにアプリの署名に使った鍵のフィンガープリント記載しておき、認証器での検証時にそれを用いるようです。これでアプリとrpId
の関係性が確認できますね。
今回私の作ったDemoではdefaultでrpId
にniconico-pun.gitlab.io
を指定しているので以下に設置しています。
https://niconico-pun.gitlab.io/.well-known/assetlinks.json
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.nicopun.fido2_simple_demo",
"sha256_cert_fingerprints": [
"AB:3E:DF:36:52:44:AD:7F:C6:8B:E1:97:16:08:BA:D2:8E:EE:3F:F0:72:88:61:6E:3F:F3:A8:B9:4F:8C:F0:77"
]
}
}
]
package_name
にはManifestfile
で定義されているpackage
名を、sha256_cert_fingerprints
にはアプリの署名に使った鍵のsha256
をそれぞれ指定します。
たいていの人はAndroidStudioで開発してるだろうから一応簡単に書いておきます。
アプリへの署名
[AndroidStudio] -> [Build] -> [Generate Signed Bundle or APK]
fingerprintの導出
Gradleタブから[signingReport]実行
出力されるハッシュ値を利用。
exec signingReport | fingerprint |
---|---|
UserVerificationのオプションがない
Webauthn API利用時にoptionとして指定できていたuserVerification
のsetがなさそうなんですよねぇ。
(参考:PublicKeyCredentialCreationOptions.Builder )
そもそもスマホアプリ使ってる時点でUP
は満たされてそうな, またスマホの貸し借りとかも普通しないからUV
もある意味満たされてる?だから不要って判断かしら?
(適当)
requireResidentKeyのオプションがない
これもAndroidのリファレンス検索しても出てこない。 上の話と同じ感じか?アプリ側で良しなにやれってことか?(適当)
transportsのオプションの違い
assertion
時にtransports
(転送手段)として指定できる項目が若干違うようです。
サポートしているtransports名の比較
transports name | Webauthn API | Android FIDO2 API |
---|---|---|
usb | o | o |
nfc | o | o |
ble | o | x |
ble_classic | x | o |
ble_low_energy | x | o |
internal | o | x |
(参考1: www.w3.org/TR/webauthn/#transport )
(参考2: google/android/gms/fido/common/Transport)
Androidはble
が二種類に分岐しててinternal
オプションはない感じですね。
ちなみに挙動としては、Register
時にAutenticatorAttachment: platform
を指定すれば指紋認証
なりPIN認証
なり求められ,
そこで生成されたpubkeyId
を使ってSign
を行えばtransports
に何を指定しようが登録時の検証方法が求められます。
つまりはtransports
はAutenticatorAttachment: cross-platform
の時に効いてくるみたい。
また、AutenticatorAttachment: cross-platform
と指定してtransports
を特定のoptionのみ指定しても指定外のoptionも普通に利用できます。
違いはIntent先の画面が若干違うくらい。
usbのみ指定した場合 | nfc or ble指定の場合 |
---|---|
定義でもあくまでhint
と書かれてるからそれほど強制力のあるものではないようですね。
This enumeration defines hints as to how clients might communicate with a particular authenticator in order to obtain an assertion for a specific credential. www.w3.org/TR/webauthn/#transport
おわりに
Android Native LibraryでのFIDO2実装を試してみました。 当然AndroidでのWebauthn API対応ブラウザでもFIDO認証はできるわけですが、
- アプリで処理を完結できる
- サポートブラウザ依存から解放される
とメリットはありそうです。 特にスマホだと基本ブラウザよりアプリ使う人多いだろうからFIDO認証使うサービスならよさそう。
まだこれから仕様もちょこちょこ変わるかもなのでフォローしないとですがいったんはこんなところで。