さて、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認証使うサービスならよさそう。
まだこれから仕様もちょこちょこ変わるかもなのでフォローしないとですがいったんはこんなところで。









