はじめに
ずいぶん前に 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)の署名検証
ソース
-
GitHubに公開しています。
- 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のドキュメント等の公式情報は見つけられていないのですが、そういう制限があるのでご注意ください。この時点で一般のアプリではつかえねーもの化しちゃっているのではと個人的には思います。
プロジェクト作成
プロジェクトを新規作成する方法をみてプロジェクトを作成します。
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コマンドが通った場合は
DeviceStatus
にg.FIDO2.CTAP.DeviceStatus.Ok
がセットされ、CTAPResponse.Status
に0
がセットされます。
ResponseGetInfoについて
さて、FIDOキーの情報ですが、以下のような情報が取れます。
- Versions
- FIDOキーが対応しているバージョンです。
U2F_V2,FIDO_2_0
などどセットされてきます。 - 本DllはU2Fには対応していないので、
FIDO_2_0
がないと正常に動作しません。
- FIDOキーが対応しているバージョンです。
- Aaguid
- Authenticator Attestation GUID。FIDOキーの製品を識別するためのIDです。個体識別情報ではありませんので注意。
- Option_rk
- Resident Key機能を持っているかどうか。通常
present_and_set_to_true
です。
- Resident Key機能を持っているかどうか。通常
- Option_up
- User Presence機能を持っているかどうか。通常
present_and_set_to_true
です。
- User Presence機能を持っているかどうか。通常
- Option_clientPin
- PINの機能を持っているかどうか。PINがセットされているかどうかもわかります。
- PINの機能を持っていないってことはないので、
absent
はありません。 - PINがまだ設定されていない場合、
present_and_set_to_false
となります。 - PINが設定されていれば、
present_and_set_to_true
となります。
- PINの機能を持っていないってことはないので、
- PINの機能を持っているかどうか。PINがセットされているかどうかもわかります。
- Option_uv
- User Verification機能(生体認証機能)を持っているかどうか。Feitian BioPassは指紋センサが付いているので
present_and_set_to_true
となりますが、Yubikeyはabsent
となります。
- User Verification機能(生体認証機能)を持っているかどうか。Feitian BioPassは指紋センサが付いているので
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の結果
IsSuccess
がtrue
であれば検証OKです、おめでとうございます。CredentialID
とPublicKeyPem
を保存しておきましょう。
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の結果
IsSuccess
がtrue
であれば検証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
Authentication
その他
ほかには以下のような機能があります。
- NFCタイプのFIDOキーが使えます。
- BLEタイプのFIDOキーが使えます。
- 指紋センサが付いたFIDOキーでPINではなく、指紋認証をすることができます。
- キーにデータを記録することができます。(Resident Key)
- PINの登録、変更をすることができます。
以下のことはできません。
- 指紋の登録。ベンダー提供の専用ツールで行ってください。
- キーのリセット。ベンダー提供の専用ツールで行ってください。
おつかれさまでした
需要なさそうなものを謎のモチベーションで作ってしまいました。