6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

ずいぶん前に WebAuthnぽいことができるWinデスクトップアプリ用ライブラリWebAuthnModokiDesktopβ を作ったんですけど、それを全体的に見直して作り替えました。
この記事では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の仕様で行っています。
    • HID(USB)
    • NFC
    • BLE
  • CTAP以外に、FIDOサーバー的な機能も実装しています。
    • チャレンジ生成
    • 登録結果(Attestation)の署名検証
    • 認証結果(Assertion)の署名検証

ソース

とりあえず使ってみる

  • 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のドキュメント等の公式情報は見つけられていないのですが、そういう制限があるのでご注意ください。この時点で一般のアプリではつかえねーもの化しちゃっているのではと個人的には思います。

プロジェクト作成

プロジェクトを新規作成する方法をみてプロジェクトを作成します。

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

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

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コマンドが通った場合は DeviceStatusg.FIDO2.CTAP.DeviceStatus.Ok がセットされ、CTAPResponse.Status0 がセットされます。

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キーにユーザー情報を登録します。

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の結果IsSuccesstrueであれば検証OKです、おめでとうございます。CredentialIDPublicKeyPemを保存しておきましょう。

GetAssertionAsync - 認証

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の結果IsSuccesstrueであれば検証OKです。認証OKということでログインさせてやりましょう。

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

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

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を光らせることができます。ただ光らせるだけです。

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

Authentication

pic

その他

ほかには以下のような機能があります。

  • NFCタイプのFIDOキーが使えます。
  • BLEタイプのFIDOキーが使えます。
  • 指紋センサが付いたFIDOキーでPINではなく、指紋認証をすることができます。
  • キーにデータを記録することができます。(Resident Key)
  • PINの登録、変更をすることができます。

以下のことはできません。

  • 指紋の登録。ベンダー提供の専用ツールで行ってください。
  • キーのリセット。ベンダー提供の専用ツールで行ってください。

おつかれさまでした

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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?