FIDOキーをWindowsデスクトップアプリで使うためのライブラリ CTAPcs

# はじめに
ずいぶん前に [WebAuthnぽいことができるWinデスクトップアプリ用ライブラリWebAuthnModokiDesktopβ](https://qiita.com/gebo/items/f6d3024f7e164ac0a195) を作ったんですけど、それを全体的に見直して作り替えました。
この記事では**CTAPcs**の基本的な使い方を紹介したいと思います。

**gebogebogebo/CTAPcs**
https://github.com/gebogebogebo/CTAPcs

- **CTAP(シータップ)です。**
- 本ライブラリ・デモプログラムはWebAuthn、CTAPを勉強しながら作成した検証プログラムです。
- サンプルレベルの品質なので重要なクレデンシャル情報が入っているFIDOキーは使わないことをお勧めします。
- 本ライブラリ・デモプログラムを利用することによって生じるいかなる問題についても、その責任を負いません。



# 環境

- Windows10 1903,1909
- Visual Studio 2017,2019
- C# Desktop Applicaion
- .Net Framework 4.6.1



# どんなものか

- YubikeyなどのFIDOキーをデスクトップアプリから使うためのDllです。
    - g.FIDO2.dll
    - g.FIDO2.CTAP.dll
    - g.FIDO2.CTAP.HID.dll
    - g.FIDO2.CTAP.NFC.dll
    - g.FIDO2.CTAP.BLE.dll
    - g.FIDO2.Util.dll
- **FIDOキーはFIDO2対応しているものだけ使えます。U2Fは使えません。**
- JavaScriptのWebAuthnみたいなもので、似たようなことをデスクトップアプリからできます。
- FIDOキーとの通信は[CTAPの仕様](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html)で行っています。
    - HID(USB)
    - NFC
    - BLE
- CTAP以外に、FIDOサーバー的な機能も実装しています。
    - チャレンジ生成
    - 登録結果（Attestation）の署名検証
    - 認証結果（Assertion）の署名検証



# ソース

- [GitHub](https://github.com/gebogebogebo/CTAPcs)に公開しています。
  - **gebogebogebo/CTAPcs**
  - https://github.com/gebogebogebo/CTAPcs
- ライセンスはMITです。
- OSSをいくつか使っています。



# とりあえず使ってみる

- HID(USB)タイプのFIDOキーを使います。
- 以下のAPIを使います。
  - GetInfoAsync - FIDOキーの情報の取得
  - MakeCredentialAsync - 登録
  - GetAssertionAsync - 認証
  - ClientPINgetRetriesAsync - PINリトライ回数を調べる
  - WinkAsync - FIDOキーのLEDを光らせる
- PIN認証でResidentKey無しのシンプルな利用です。



### 注意事項
USBタイプのFIDOキーはHID（ヒューマンインタフェースデバイス）でWindowsに認識されるので、専用のドライバが不要ですべてのアプリから取り扱い可能、という点が良いところだったのですが、Windowsさんはいつの間にか（たぶん1903から）制限をかけるようになり、アクセスに**Administratorの権限が必要**になりました。これは手軽に使うことができなくなったという点で非常に残念ですが、Windows Helloなどセキュリティに関わる部分のポリシーに基づく対応なのかもしれません。この権限に関わる仕様変更について、Microsoftのドキュメント等の公式情報は見つけられていないのですが、そういう制限があるのでご注意ください。この時点で一般のアプリではつかえねーもの化しちゃっているのではと個人的には思います。




## プロジェクト作成
[プロジェクトを新規作成する方法](https://github.com/gebogebogebo/CTAPcs/blob/master/readme/CTAPcs_ReadMe_01.md#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E6%96%B0%E8%A6%8F%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)をみてプロジェクトを作成します。



## GetInfoAsync - FIDOキーの情報を取得する

FIDOキーをUSBに挿して以下のコードを実行します。

```csharp
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using g.FIDO2.CTAP.HID;
using g.FIDO2.Util;
using g.FIDO2;

namespace HIDTest02
{
    public partial class MainWindow : Window
    {
        private HIDAuthenticatorConnector con;

        public MainWindow()
        {
            InitializeComponent();
            con = new HIDAuthenticatorConnector();
        }

        private async void ButtonGetInfo_Click(object sender, RoutedEventArgs e)
        {
            var res = await con.GetInfoAsync();
            if (res.DeviceStatus == g.FIDO2.CTAP.DeviceStatus.Unauthorized) {
                MessageBox.Show("Excute Administrator ?");
            } else if (res.DeviceStatus == g.FIDO2.CTAP.DeviceStatus.NotConnected) {
                MessageBox.Show("FIDO Key Not Connected");
            } else if (res.DeviceStatus == g.FIDO2.CTAP.DeviceStatus.Ok) {
                MessageBox.Show($"GetInfoAsync\r\n- Status = {res.CTAPResponse.Status}\r\n- StatusMsg = {res.CTAPResponse.StatusMsg}"); ;
            } else {
                MessageBox.Show("Error");
            }
        }
    }
}
```



### 処理概要

- `HIDAuthenticatorConnector` クラスの `GetInfoAsync` メソッドでUSB FIDOキーの情報を取得します。
- 正常に終了すると `ResponseGetInfo` クラスが戻り値で返ってきます。
- `ResponseGetInfo` クラスは `DeviceStatus` メンバと `CTAPResponse` メンバで構成されていて、`DeviceStatus` にHIDレイヤのステータス、`CTAPResponse` にCTAPレイヤのステータスを持っています。
- FIDOキーを正常に認識してCTAPコマンドが通った場合は `DeviceStatus` に `g.FIDO2.CTAP.DeviceStatus.Ok` がセットされ、`CTAPResponse.Status` に `0` がセットされます。



#### ResponseGetInfoについて

さて、FIDOキーの情報ですが、以下のような情報が取れます。

- Versions
    - FIDOキーが対応しているバージョンです。`U2F_V2,FIDO_2_0`などどセットされてきます。
    - 本DllはU2Fには対応していないので、`FIDO_2_0` がないと正常に動作しません。
- Aaguid
    - Authenticator Attestation GUID。FIDOキーの製品を識別するためのIDです。個体識別情報ではありませんので注意。
- Option_rk
    - Resident Key機能を持っているかどうか。通常 `present_and_set_to_true` です。
- Option_up
    - User Presence機能を持っているかどうか。通常 `present_and_set_to_true` です。
- Option_clientPin
    - PINの機能を持っているかどうか。PINがセットされているかどうかもわかります。
        - PINの機能を持っていないってことはないので、`absent` はありません。
        - PINがまだ設定されていない場合、`present_and_set_to_false` となります。
        - PINが設定されていれば、`present_and_set_to_true` となります。
- Option_uv
    - User Verification機能（生体認証機能）を持っているかどうか。Feitian BioPassは指紋センサが付いているので `present_and_set_to_true` となりますが、Yubikeyは `absent` となります。

## MakeCredentialAsync - 登録
FIDOキーにユーザー情報を登録します。

```csharp
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using g.FIDO2.CTAP.HID;
using g.FIDO2.Util;
using g.FIDO2;

namespace HIDTest02
{
    public partial class MainWindow : Window
    {
        private HIDAuthenticatorConnector con;
        private byte[] creid;
        private string pubkey;

        public MainWindow()
        {
            InitializeComponent();
            con = new HIDAuthenticatorConnector();
            con.KeepAlive += OnKeepAlive;
        }

        private void OnKeepAlive(object sender, EventArgs e)
        {
            // MakeCredentialAsync()、GetAssertionAsync()で
            // PIN認証が通ってFIDOキーのタッチ待ちになるとこのイベントが発生します
        }

        private async void ButtonMakeCredential_Click(object sender, RoutedEventArgs e)
        {
            string rpid = "test.com";
            var challenge = AttestationVerifier.CreateChallenge();
            var param = new g.FIDO2.CTAP.CTAPCommandMakeCredentialParam(rpid, challenge);
            var res = await con.MakeCredentialAsync(param, "1234");
            if (res.DeviceStatus == g.FIDO2.CTAP.DeviceStatus.NotConnected) {
                // FIDOキーが接続されていない場合
                return;
            } else if (res.DeviceStatus == g.FIDO2.CTAP.DeviceStatus.Timeout) {
                // FIDOキーのタッチ待ちでTimeoutした場合
                return;
            } else if (res.DeviceStatus == g.FIDO2.CTAP.DeviceStatus.Ok) {
                string verifyResult = "";
                if (res.CTAPResponse.Status == 0) {
                    if (res.CTAPResponse.Attestation != null) {
                        // verify
                        var v = new AttestationVerifier();
                        var verify = v.Verify(rpid,challenge, res.CTAPResponse.Attestation);
                        verifyResult = $"- Verify = {verify.IsSuccess}\r\n- CredentialID = {Common.BytesToHexString(verify.CredentialID)}\r\n- PublicKey = {verify.PublicKeyPem}";
                        if (verify.IsSuccess) {
                            // store
                            creid = verify.CredentialID.ToArray();
                            pubkey = verify.PublicKeyPem;
                        }
                    }
                }
                MessageBox.Show($"MakeCredentialAsync\r\n- Status = {res.CTAPResponse.Status}\r\n- StatusMsg = {res.CTAPResponse.StatusMsg}\r\n{verifyResult}");
            }
        }

    }
}
```




### 処理概要

- `HIDAuthenticatorConnector` クラスの `MakeCredentialAsync` メソッドでUSB FIDOキーの登録を実行します。
- `MakeCredentialAsync` には以下の情報を渡します。
  - RpId(今回は**test.com**)を指定します。これは登録のキーとなります。
  - 応答検証のためのチャレンジを渡します。チャレンジは `AttestationVerifier.CreateChallenge()` で生成して渡してやればOKです。
  - 認証要素としてPIN(今回は**1234**)を指定します。
- FIDOキー側でタッチ待ち状態になると`OnKeepAlive()`イベントが発生しますので、アプリ側でガイドメッセージを出すようなことが可能です。ただしこのイベントはUIスレッドではないので注意してください。
- 正常に終了すると `ResponseMakeCredential` クラスが返ってきます。



#### ResponseMakeCredentialの検証

- `ResponseMakeCredential.CTAPResponse.Attestation` が登録結果情報です。
- `Attestation` には電子署名が付いていて、これを検証(Verify)することで **なりすまし**や**情報の改ざん**がない事を確認することができます。
- `AttestationVerifier` クラスの `Verify` メソッドで検証します。このときRpIdと最初に生成したチャレンジを渡してやる必要があります。
- Verifyの結果`IsSuccess`が`true`であれば検証OKです、おめでとうございます。`CredentialID`と`PublicKeyPem`を保存しておきましょう。



## GetAssertionAsync - 認証

```csharp
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using g.FIDO2.CTAP.HID;
using g.FIDO2.Util;
using g.FIDO2;

namespace HIDTest02
{
    public partial class MainWindow : Window
    {
        private HIDAuthenticatorConnector con;
        private byte[] creid;
        private string pubkey;

        public MainWindow()
        {
            InitializeComponent();
            con = new HIDAuthenticatorConnector();
            con.KeepAlive += OnKeepAlive;
        }

        private void OnKeepAlive(object sender, EventArgs e)
        {
            // MakeCredentialAsync()、GetAssertionAsync()で
            // PIN認証が通ってFIDOキーのタッチ待ちになるとこのイベントが発生します
        }

        private async void ButtonGetAssertion_Click(object sender, RoutedEventArgs e)
        {
              var rpid = "test.com";
            var challenge = AttestationVerifier.CreateChallenge();
            var param = new g.FIDO2.CTAP.CTAPCommandGetAssertionParam(rpid, challenge, creid);
            param.Option_up = true;

            var res = await con.GetAssertionAsync(param, "1234");
            if (res.DeviceStatus == g.FIDO2.CTAP.DeviceStatus.NotConnected) {
                // FIDOキーが接続されていない場合
                return;
            } else if (res.DeviceStatus == g.FIDO2.CTAP.DeviceStatus.Timeout) {
                // FIDOキーのタッチ待ちでTimeoutした場合
                return;
            } else if (res.DeviceStatus == g.FIDO2.CTAP.DeviceStatus.Ok) {
                string verifyResult = "";
                if ( res.CTAPResponse.Assertion != null) {
                    // verify
                    var v = new AssertionVerifier();
                    var verify = v.Verify(rpid,pubkey,challenge, res.CTAPResponse.Assertion);
                    verifyResult = $"- Verify = {verify.IsSuccess}";
                }
                MessageBox.Show($"GetAssertionAsync\r\n- Status = {res.CTAPResponse.Status}\r\n- StatusMsg = {res.CTAPResponse.StatusMsg}\r\n{verifyResult}");
            }          
        }

    }
}
```



### 処理概要
- `HIDAuthenticatorConnector` クラスの `GetAssertionAsync`メソッドでUSB FIDOキーの認証を実行します。
- `GetAssertionAsync`には以下の情報を渡します。
  - キーとして、RpId(今回は**test.com**)を指定します。登録の時に指定したものと同じものを指定する必要があります。
  - もう一つのキーとして、登録時にGetした`CredentialID`を指定します。
  - 応答検証のためのチャレンジを渡します。チャレンジは `AssertionVerifier.CreateChallenge()` で生成した値を渡してやればOK。
  - 認証要素としてPIN(今回は**1234**)を指定します。
- FIDOキー側でタッチ待ち状態になると`OnKeepAlive()`イベントが発生しますので、アプリ側でガイドメッセージを出すようなことが可能です。ただしこのイベントはUIスレッドではないので注意してください。
- 正常に終了すると `ResponseGetAssertion` クラスが返ってきます。

#### ResponseGetAssertionの検証

- `ResponseGetAssertion.CTAPResponse.Assertion` が認証結果情報です。
- `Assertion`には電子署名が付いていて、これを検証(Verify)することで **なりすまし**や**情報の改ざん**がない事を確認することができます。
- `AssertionVerifier` クラスの `Verify` メソッドで検証します。このときRpIdと登録時に生成した公開鍵、最初に生成した `challenge` を渡してやる必要があります。
- Verifyの結果`IsSuccess`が`true`であれば検証OKです。認証OKということでログインさせてやりましょう。



## ClientPINgetRetriesAsync - PINリトライ回数を調べる

FIDOキーでPINを何度も間違うとロックがかかります。ロックがかかるという事はすなわち**詰み**です。FIDOキーをリセットして出荷状態に戻さないと使えません。
以下のコードでPINリトライカウンタを調べることができます。リトライカウンタはPINを間違えると減り、PINが通ると初期値に戻ります。ゼロになるとロック状態になります。

```csharp
private async void ButtonClientPINgetRetries_Click(object sender, RoutedEventArgs e)
{
    var con2 = new HIDAuthenticatorConnector();
    var res = await con2.ClientPINgetRetriesAsync();
    if (res.DeviceStatus == g.FIDO2.CTAP.DeviceStatus.Ok) {
        MessageBox.Show($"ClientPINgetRetriesAsync\r\n- Status = {res.CTAPResponse.Status}\r\n- StatusMsg = {res.CTAPResponse.StatusMsg}\r\n- PIN Retry Count = {res.CTAPResponse.RetryCount}");
    }
}
```





## WinkAsync - FIDOキーのLEDを光らせる

どうでもいい機能として、FIDOキーのLEDを光らせることができます。**ただ光らせるだけです。**

```csharp
private async void ButtonWink_Click(object sender, RoutedEventArgs e)
{
    var con = new HIDAuthenticatorConnector();
    for (int intIc = 0; intIc < 5; intIc++) {
        var ret = await con.WinkAsync();
        await Task.Delay(1000);
    }
}
```



# 登録と認証のシーケンス

今回のサンプルプログラムは簡単な利用例ですが、実際はクライアント側とサーバー側で役割を分ける構成を想定しています。


## Registration

![pic](https://github.com/gebogebogebo/CTAPcs/blob/master/pu/USB_FIDO_Key_Registration/USB%20FIDO%20Key%20Registration%20(Non%20Resident%20Key).png?raw=true)





## Authentication

![pic](https://github.com/gebogebogebo/CTAPcs/blob/master/pu/USB_FIDO_Key_Autheication/USB%20FIDO%20Key%20Autheication%20(Non%20Resident%20Key).png?raw=true)




# その他

ほかには以下のような機能があります。
- NFCタイプのFIDOキーが使えます。
- BLEタイプのFIDOキーが使えます。
- 指紋センサが付いたFIDOキーでPINではなく、指紋認証をすることができます。
- キーにデータを記録することができます。（Resident Key）
- PINの登録、変更をすることができます。

  

以下のことはできません。
- 指紋の登録。ベンダー提供の専用ツールで行ってください。
- キーのリセット。ベンダー提供の専用ツールで行ってください。



# おつかれさまでした

需要なさそうなものを謎のモチベーションで作ってしまいました。