OpenSKで認証時にボタンが反応しない件を調べてみた


[nRF52840-DKにOpenSKを入れてUSB FIDOキーにしてみた](https://qiita.com/gebo/items/2b5ad44899e1b9a9c609)の続きです。

---
2020/08/20 追記
本投稿8/16で同日に修正を[OpenSK](https://github.com/google/OpenSK)にプルリクエストしたところ即座にマージされたのでこの現象は発生しなくなりました。めっちゃ早い対応！

---

OpenSKを使って認証するときにセキュリティキーのボタンを押しても反応しない以下の謎がありました。

**「セキュリティキーにタッチしてください」のポップアップ画面でキー(nRF52840-DK)のボタンを押しても反応せずキャンセルするしかない**

![auth.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/8f36fc32-0823-12b9-b6da-29e02fac91fb.png)

調査してみると以下のような状況です。

- Windowsでのみ発生する。WindowsであればChromeでもEdgeでも発生する。
- WebAuthnのデモサイトで現象を確認できる。どこか特定のサイトだけで発生するわけではなくどこでも現象を確認できる。
- 登録は成功するんだけど、認証するときにボタンを押しても反応せず、キャンセルするしかない。
- OpenSKのissueにもBugで上がっているようだ、英語力がアレなんですがたぶん↓
  - [Error while authenticating on Windows 10 when using userVerification #28](https://github.com/google/OpenSK/issues/28)
  - [Can't recognize "User intent" using "Push button" of nRF52840-MDK USB Dongle #114](https://github.com/google/OpenSK/issues/114)

- [Yubikey](https://www.yubico.com/yubikey-2/?lang=ja)、[BioPass](https://ftsafe.co.jp/products/fido2/)などの製品では発生しない。

  

バグならば仕方ない、そのうち修正されるだろうと思っていたのですが、夏休み暇なので自分で調べてみたという話です。



## 環境

- ビルド環境
  - Mac OS X Catalina 10.15.5
  - vscode 1.48.0
- 実行環境
  - Windows 10 Pro 64bit 1909
- セキュリティキー
  - nRF52840-DK
  - [OepnSK](https://github.com/google/OpenSK)  



## どこが怪しいのか

Macだと正常に動作して、Winだとおかしい、ということはOpenSKの問題ではないようにも思います。Webサーバ(RP)かブラウザ(Platform)の問題？。「セキュリティキーにタッチしてください」のポップアップはWebAuthnを実装したブラウザの機能で表示しているのでブラウザが怪しい。OpenSKから受け取ったの認証結果(Assertion)の検証でブラウザが内部的にコケてしまっているのかも知れません。

OpenSKとブラウザでどんなやりとりがされているのか調べてみます。



## どこが怪しいのか２

ユーザープログラムがコールするJavaScriptのWebAuthnの認証APIは`navigator.credentials.get()`です。ブラウザはこのAPIを受けて内部でCTAPコマンドをセキュリティキー(OpenSK)に放り投げます。認証で使うCTAPコマンドは`authenticatorClientPIN`,`authenticatorGetAssertion`,`authenticatorGetNextAssertion`です。今回の問題はPINを検証した後のUserPresenceフェーズで発生しているので`authenticatorGetAssertion`で何か起こっているようです。



## CTAPコマンドの通信内容をトレースする

`authenticatorGetAssertion`を見てみましょう。以下のツールを使って通信内容を見ることができます。

[CTAPcs](https://github.com/gebogebogebo/CTAPcs) - [HIDTest01.exe](https://github.com/gebogebogebo/CTAPcs/tree/master/examples/bin)



- HIDTest01.exeを立ち上げる
- 接続確認
  - セキュリティキーをUSBに挿して `GetInfoボタン` をクリック→ログにそれっぽいのが出たら接続OK
- 登録
  - RPIDは test.com のまま、PINを入力、ResidentKeyは チェックOFF で `MakeCredentialボタン` をクリック→セキュリティキーが光ったらボタンを押す→成功すると `CredentialID` が取れる
- 認証
  - そのまま `GetAssertionボタン` をクリック→セキュリティキーが光ったらボタンを押す
  - ここでログにでてくる **CTAP ResponseDataJson が authenticatorGetAssertion の応答**なので、これを調査します。
  - OpenSK、Yubikey、BioPassのログを比べてみます。



```:OpenSK
- CTAP ResponseDataJson = 
{
"1":
	{	"id":"LiSHLJSRyKpyEUlCqCwmo99VZCoYjfGnsWNn2RPitrcYP80L_HFb1_DY4doGnAfkp738pWbX_yCPXnGZ6r0Vt6b0MAmVy6vV-RUtMWAEQZkpsBHRpEWmX4kIgJZi3LT02CacCHktzRcLuxvU_P8tuA",
		"type":"public-key"
	},
"2":
	"matxXYSjvF4OkqpQ5npYE2N_0XRL0wGrCPhxkd24FuAFAAAAGQ",
"3":
	"MEUCIQCD0jAejoDvRtKIZ7ZRBikQ6Yma51Ib7KK5Mt-SctHrKgIgS5ujml4W28-66Xq50UVsZl97rKhTz5jgAfViJqs8Bqc",
"4":
	{
		"id":""
	}
}
```

```:Yubikey
- CTAP ResponseDataJson = 
{
"1":
	{
		"id":"KL-rWIRGn26kZxtQXasrN4TI7vsnkvb3xAu_3huZ-nxF-wk-aOWtsXeP2f3rZiU-EBYjHcPJoQDE0KyOFLc9BA",
		"type":"public-key"
	},
"2":
	"matxXYSjvF4OkqpQ5npYE2N_0XRL0wGrCPhxkd24FuAFAAAA3w",
"3":
	"MEUCIQDsA3Fr3LPapPUWBn4m56QTfvNvIxMmLKqKsjBe4YRDWAIgPUdmgwcn0P7h-PxR2fPDYXcBsLYFRvQdxzpIrYllkoc"
}
```


```:BioPass
- CTAP ResponseDataJson = 
{
"1":
	{
		"id":"HpVOwMsjnnLFXVPNTszaMxzbO16pm7FQn8E3Gk4bnI-NoNrwp5-UVvq4dcp1AYdPCwh_Al3YkBAqZvDF8zV81rnvIuROH9b_pkUL1K6epYdINUwfIiCOJcZwYcqyoE9S",
		"type":"public-key"
	},
"2":
	"matxXYSjvF4OkqpQ5npYE2N_0XRL0wGrCPhxkd24FuAFAAAAHQ",
"3":
	"MEUCIBY7hxOq8lc-RoUUKL5MuKf7QyhHyCL7DXhU78QawymzAiEA-FbLH9iBW12WaocTr3NxXJMMxbCnmW2LgJLWFoZQzWA"
}
```



**わかりにくいようでわかりやすいのですが、OpenSKだけ"4"があります。**

```:OpenSK
"4":
	{
		"id":""
	}
```

**これは何なんでしょうか？**



## "4"って何？

[CTAPの仕様書](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetAssertion)によると、**user (0x04) - PublicKeyCredentialUserEntity** というものです。



> **PublicKeyCredentialUserEntity**<br>PublicKeyCredentialUserEntity structure containing the user account information. User identifiable information (name, DisplayName, icon) MUST not be returned if user verification is not done by the authenticator.<br>ユーザーアカウント情報を含むPublicKeyCredentialUserEntity構造体。ユーザ認証が認証者によって行われていない場合、ユーザを特定できる情報 (name, DisplayName, icon) は返されてはなりません[MUST]。



[6.1. Commands](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#commands)

```:EXAMPLE2
var user = {
      id: Uint8Array.from(window.atob("MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII="), c=>c.charCodeAt(0)),
      icon: "https://pics.example.com/00/p/aBjjjpqPb.png",
      name: "johnpsmith@example.com",
      displayName: "John P. Smith"
    };
```

id , icon ,name ,displayNameのメンバを持つ構造体で、要するにユーザー情報ですね(雰囲気理解)。確かに登録時にユーザー名は入力してますが、ログを見る限り空っぽです。YubikeyやBioPassでは存在すらしていない。何でなんでしょうか？



## PublicKeyCredentialUserEntityとは

FIDO2には**ResidentKey**という機能がありまして、セキュリティキーの中にユーザー情報を格納することができます。登録時にユーザー情報をキーの中に保管しておいて、認証時にキーの中にあるユーザー情報を取り出しそのユーザーでログインするなんてことを想定した機能です。ResidentKey機能によってユーザーは自分自身が誰かをわざわざ入力しなくてもキー(所持)とPIN(記憶)または指紋(生体)による２要素認証を実現できます。このキーの中に格納されるユーザー情報が **PublicKeyCredentialUserEntity構造体** です。便利なんですけどキーのメモリを使うので格納できる量には限界があるという問題もあります。ResidentKeyを使っているRPはあんまりいないんじゃないかと思います。



さて、今の使い方では**ResidentKey機能は使っておりません。**なので、PublicKeyCredentialUserEntityは無い、あるいはPublicKeyCredentialUserEntityの中身が空、というセキュリティキー側の動きは仕様的には問題なさそうです。しかし**ブラウザは空のPublicKeyCredentialUserEntityを受け付けないようで、エラーにするわけでもなくシカト(無視)する**ようです。つまり、OpenSKはちゃんとボタンプッシュを検知して応答しているんだけども、ブラウザにシカトされている状況、というわけです。YubikeyもBioPassもPublicKeyCredentialUserEntityが無い応答をしているのでブラウザ側はOpenSKのような応答に対応していないのか、CTAP仕様がそうなのかもしれません。

とりあえず、OpenSKもPublicKeyCredentialUserEntityを無しにするようにすればブラウザは反応してくれるかもしれませんのでやってみます。



## OepnSKを修正する

ソースをみてみます。Rustはよくわからないしデバック実行もできないのでとにかくコードを舐めるようにみます...



半日ほどかかってようやく該当の場所を見つけました。

```rust:mod.rs
    let user = if flags & UV_FLAG != 0 {
        Some(PublicKeyCredentialUserEntity {
            user_id: credential.user_handle.clone(),
            user_name: None,
            user_display_name: credential.other_ui.clone(),
            user_icon: None,
        })
    } else {
        Non
    };
```

このifの中に入っているのにもかかわらず、credential.user_handleが空なので空のPublicKeyCredentialUserEntityが生成されてしまうんでしょう。登録時にResidentKeyをfalseにしているのでcredential.user_handleが空なのは正常だと思います。つまり、この判定を修正する必要があるのかもしれません。



修正しました(**1行!**)。<br>これで空のPublicKeyCredentialUserEntityを生成することはなくなるはずです。

```rust:mod.rs
        let user = if (flags & UV_FLAG != 0) && (credential.user_handle.len() > 0) {
            Some(PublicKeyCredentialUserEntity {
                user_id: credential.user_handle.clone(),
                user_name: None,
                user_display_name: credential.other_ui.clone(),
                user_icon: None,
            })
        } else {
            None
        };
```



## テスト

認証するときにボタンを押したらちゃんと認証できるようになりました。<br>しかし修正方法がこれでいいのか、OpenSKはあえて対応していないのかもしれません...



| 環境                                                | [YubiOn](https://yubionfido2demo.yubion.com/register_jp.html) | [fido.identityserver.com](https://fido.identityserver.com/) | [webauthn.io](https://webauthn.io/) |
| --------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------- | ----------------------------------- |
| Windows 10 Pro 1909<br/>Chrome 84.0.4147.125        | OK                                                           | OK                                                          | OK                                  |
| Windows 10 Pro 1909<br/>Edge 84.0.522.59            | OK                                                           | OK                                                          | OK                                  |
| Mac OS X Catalina 10.15.5 <br/>Chrome 84.0.4147.125 | OK                                                           | OK                                                          | OK                                  |



## おつかれさまでした

Rustの言語仕様は非常に興味深く面白い！