[FIDO2] Android NativeでのFIDO2実装をひも解いてみる

Implement FIDO2 with Android Native Library

さて、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
app_ui intent_internal intent_internal_verified

んで一通りregister, signが完了すると以下のような結果を出力します。

登録時(regist) 検証時(verify)
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でよくある捌き方です。

  1. 上の例で定義したようなOptionをパラメータとしてIntentに渡してあげる。
  2. Intent先で認証器でのVerifyを行う
  3. 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 Intent例 Verify Pending

Verify結果の取得

上でのVerify結果をonActivityResultで受け取ります。

上の例だと正常にいけばresultCodeRESULT_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ならブラウザで得られるoriginrpIdと照合すればいいわけですが、 Nativeアプリだと同じようにはいきません。

ということでNativeアプリではアプリとoriginとの関係性を確かめる手立てとして、 rpIdで指定したドメイン配下にassetlinks.jsonを設置し、 そこにアプリの署名に使った鍵のフィンガープリント記載しておき、認証器での検証時にそれを用いるようです。これでアプリとrpIdの関係性が確認できますね。

今回私の作ったDemoではdefaultでrpIdniconico-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
exec signingReport fingerprint

UserVerificationのオプションがない

Webauthn API利用時にoptionとして指定できていたuserVerificationのsetがなさそうなんですよねぇ。 (参考:PublicKeyCredentialCreationOptions.Builder )

そもそもスマホアプリ使ってる時点でUPは満たされてそうな, またスマホの貸し借りとかも普通しないからUVもある意味満たされてる?だから不要って判断かしら? (適当)

relation

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に何を指定しようが登録時の検証方法が求められます。

つまりはtransportsAutenticatorAttachment: cross-platformの時に効いてくるみたい。

また、AutenticatorAttachment: cross-platformと指定してtransportsを特定のoptionのみ指定しても指定外のoptionも普通に利用できます。

違いはIntent先の画面が若干違うくらい。

usbのみ指定した場合 nfc or ble指定の場合
intent_usb intent_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認証使うサービスならよさそう。

まだこれから仕様もちょこちょこ変わるかもなのでフォローしないとですがいったんはこんなところで。

参考


See also