#はじめに
これはCTAPのお勉強をしたメモです。
WebAuthn(ウェブオースン)ではなく、CTAP(シータップ)であります。
今回はCTAP2の BLE(Bluetooth Low Energy) です。
###教科書
###教材
###復習
###環境
- Windows10 Pro 1903 (64bit)
- Visual Studio 2019
- C#
- .Net Framework 4.6
- WPFアプリケーション
- ここで使っているサンプルプログラムはGitHubにアップしています
####WPF(.net)アプリでBLEを使うことについて
WindowsでBLEを取り扱う場合は通常UWPアプリで作成しないといけないんですが、私はUWPアプリが嫌いなので無理やりWPF(.net)でBLEを使えるようにしています。お作法的にはよくないので注意です。
無理やりWPF(.net)でBLEを使えるようにする詳細はコチラ
#目次
1.AllinPass FIDO2について
2.WindowsでのBLE通信
3.CTAP BLE
#1.AllinPass FIDO2について
CTAPで規定されている全てのインタフェース(HID,NFC,BLE)が実装されており、UserVerificationでPINだけでなく指紋認証もできるデバイスなんで、これ1個あればCTAPを完全に理解することができます!
デバイス的には完璧なんですが、$130USDということで、これ買う人いるんかなってかんじですが、欲しい人はコチラ
FIDO2デバイス AllinPass FIDO2
##セットアップ
###初期設定
Bio Pass FIDO2 Managerというメーカー提供のツールでPINと指紋の登録をします。
PINの設定はCTAPコマンドでもできますが、指紋登録はCTAPの仕様にないのでこのツールからしかできません。
ツールはMicrosoft StoreからGETします。
###ペアリング
AllinPassとWindowsPCでペアリングをする必要があります。
一般的なワイヤレスイヤホンを追加するのと同じやり方です。
参考:Bluetoothデバイスの追加方法(Bluetoothオーディオ編)
ちなみに ペアリング(LTK)の必要性はCTAPの仕様に書いてあります。
8.3.2. Pairing
個人的にはこの事前ペアリング無しで通信できるほうが使いやすいのになぁと思います。
#2.WindowsでのBLE通信
BLEの基本的な部分をざっくりと。
GATT(Generic attribute profile-ガット) ってやつです。
GATTについて本気で掘っていくときりがないので、雰囲気的に そしてWindowsに限定して以下のサイトを参考に上げます。
#3.CTAP BLE
BLEでFIDO2デバイスとお話する方法です
CTAPの仕様では 8.3. Bluetooth Smart / Bluetooth Low Energy Technology のところです。
##FIDOデバイスのUUID
BLE通信するうえで一番重要なServiceとCharacteristicの定義です。
####Service
FIDO Service のUUIDは0000fffd-0000-1000-8000-00805f9b34fb
です。
8.3.5.1. FIDO Serviceに
An authenticator SHALL implement the FIDO Service described below. The UUID for the FIDO GATT service is 0xFFFD;
とだけ書いてあり、ちっとわかりにくいですね。
####Characteristic
8.3.5.1. FIDO Service
UUIDがF1D0から始まるのがイイね。
Characteristic Name | UUID | Property | memo |
---|---|---|---|
FIDO Control Point | F1D0FFF1-DEAA-ECEE-B42F-C9BA7ED623BB | Write | デバイスへ送信バッファ |
FIDO Status | F1D0FFF2-DEAA-ECEE-B42F-C9BA7ED623BB | Notify | デバイスからの応答バッファ |
FIDO Control Point Length | F1D0FFF3-DEAA-ECEE-B42F-C9BA7ED623BB | Read | 送信バッファの最大サイズ(2byte) |
FIDO Service Revision Bitfield | F1D0FFF4-DEAA-ECEE-B42F-C9BA7ED623BB | Read/Write | 通信プロトコルの種類(1byte) |
FIDO Service Revision | 00002A28-0000-1000-8000-00805F9B34FB | Read | リビジョン |
#####FIDO Control Point,FIDO Control Point Length
-
FIDO Control Point
がFIDOデバイスへの送信ポートです。 - データの送信はこのUUIDを指定して行います。
- 送信バッファの最大サイズ(バイト数)が
FIDO Control Point Length
に2byteで格納されています。 - CTAP仕様では20~512の範囲となっています。
- お勉強に使ったAllinPassは0x00,0x9B=155byteでした。
- 送信バッファの最大サイスを超えるデータを送信したい場合はパケットを分割して送信します(後述)。
#####FIDO Status
FIDOデバイスからの応答を受信するポートです。
#####FIDO Service Revision Bitfield
1byteのビットフィールドで通信プロトコルの種類を指定します。読み取り&書き込みができます。
AllinPassは初期設定のまま0x20(0010-0000=FIDO2)で特に変更しません。
Bit | Version |
---|---|
7 | U2F 1.1 |
6 | U2F 1.2 |
5 | FIDO2 |
4-0 | Reserved |
#####FIDO Service Revision
- これはGATT標準でSoftware Revision Stringの属性です。
- 意味はそのまま、ソフトウエアのリビジョン的なものを文字列で持っています。
- AllinPassは"2.0"ってなってました。
##接続
デバイスと接続する手順です。ここはCTAPは関係なく、通常のBLEの話なのでさくっと紹介します。
サンプルプログラムはコチラ。
###アドバタイズパケットのスキャン開始
BluetoothLEAdvertisementWatcherクラスをnewしてなんやかんやするだけです。
受信イベントWatcher_Receivedの説明はこの次。
private void ButtonScan_Click(object sender, RoutedEventArgs e)
{
this.AdvWatcher = new BluetoothLEAdvertisementWatcher();
// インターバルがゼロのままだと、CPU負荷が高くなりますので、適切な間隔(SDK サンプルでは 1秒)に指定しないと、アプリの動作に支障をきたすことになります。
this.AdvWatcher.SignalStrengthFilter.SamplingInterval = TimeSpan.FromMilliseconds(1000);
// rssi >= -60のときスキャンする
//this.advWatcher.SignalStrengthFilter.InRangeThresholdInDBm = -60;
// パッシブスキャン/アクティブスキャン
// スキャン応答のアドバタイズを併せて受信する場合=BluetoothLEScanningMode.Active
// ActiveにするとBluetoothLEAdvertisementType.ScanResponseが取れるようになる。(スキャンレスポンスとは追加情報のこと)
// ※電力消費量が大きくなり、またバックグラウンド モードでは使用できなくなるらしい
//this.advWatcher.ScanningMode = BluetoothLEScanningMode.Active;
this.AdvWatcher.ScanningMode = BluetoothLEScanningMode.Passive;
// アドバタイズパケットの受信イベント
this.AdvWatcher.Received += this.Watcher_Received;
// スキャン開始
this.AdvWatcher.Start();
addLog("Scan開始しました.BLE FIDOキーをONにしてください");
addLog("");
}
###アドバタイズパケットの受信イベント~FIDOサービスのチェック
意外と身の回りにはBLEデバイスがいるらしく受信イベントには色々入ってきますよ。
AllinPassのサイドのボタンを押すとAllinPassがアドバタイジングを始めるのでこれをUUIDで識別します。
private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
{
await this.Dispatcher.InvokeAsync(() => {
this.CheckArgs(args);
});
}
public async void CheckArgs(BluetoothLEAdvertisementReceivedEventArgs args)
{
// FIDOサービスを検索
var fidoServiceUuid = new Guid("0000fffd-0000-1000-8000-00805f9b34fb");
if (args.Advertisement.ServiceUuids.Contains(fidoServiceUuid) == false) {
// ちがうやつ
return;
}
// 発見-スキャン停止
addLog("Scan FIDO Device");
this.AdvWatcher.Stop();
// 接続
・・・
}
###BLEデバイスに接続~送受信設定
もう この辺は おまじないという理解で いいんじゃないかと。
// connect
BleDevice = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
// FIDOのサービスをGET
var services = await BleDevice.GetGattServicesForUuidAsync(new Guid("0000fffd-0000-1000-8000-00805f9b34fb"));
if (services.Services.Count <= 0) {
// サービス無し
return;
}
Service_Fido = services.Services.First();
// Characteristicアクセス
// - コマンド送信ハンドラ設定
// - 応答受信ハンドラ設定
{
// FIDO Control Point Length(Read-2byte)
// 送信バッファの最大サイズを調べます。
await DebugMethods.OutputLog(Service_Fido, new Guid("F1D0FFF3-DEAA-ECEE-B42F-C9BA7ED623BB"));
// FIDO Status(Notiry) 受信データ
{
var characteristics = await Service_Fido.GetCharacteristicsForUuidAsync(new Guid("F1D0FFF2-DEAA-ECEE-B42F-C9BA7ED623BB"));
if (characteristics.Characteristics.Count > 0) {
this.Characteristic_Receive = characteristics.Characteristics.First();
if (this.Characteristic_Receive == null) {
Console.WriteLine("Characteristicに接続できない...");
} else {
if (this.Characteristic_Receive.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Notify)) {
// イベントハンドラ追加
this.Characteristic_Receive.ValueChanged += characteristicChanged_OnReceiveFromDevice;
// これで有効になる
await this.Characteristic_Receive.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
}
}
}
}
// FIDO Control Point(Write) 送信データ
{
var characteristics = await Service_Fido.GetCharacteristicsForUuidAsync(new Guid("F1D0FFF1-DEAA-ECEE-B42F-C9BA7ED623BB"));
if (characteristics.Characteristics.Count > 0) {
this.Characteristic_Send = characteristics.Characteristics.First();
if (this.Characteristic_Send == null) {
Console.WriteLine("Characteristicに接続できない...");
}
}
}
addLog("BLE FIDOキーと接続しました!");
}
##通信パケットフォーマット
やっと通信のところです。CTAPコマンドをパケットに入れて送受信します。
パケットのフォーマットはCTAP仕様では以下です。
8.3.4. Framing
####Request from Client to Authenticator
送信パケットのフォーマット。特に難しいことはないです。
でっかいデータは分割送信するんですが、それについては後述です。
#####Command identifier
- PING=0x81
- MSG=0x83
- CANCEL=0xbe
CTAPコマンドを送信する場合は0x83を設定します。
#####High part of data length,Low part of data length
送信データDATA部のバイト数を指定します。
例えばDATA部が1byteの時は0x00-0x01とします。
#####Data
送信データ、つまりCTAPコマンドを設定します。
※CTAPコマンドの詳細はここでは説明しません。復習のリンクから過去のポストを見てください。
####Response from Authenticator to Client
応答パケットのフォーマット
#####Response status
- KEEPALIVE=0x82
- MSG=0x83
- ERROR=0xbf
正常応答の時は0x83になり、DATA部に応答が詰まっています。
エラーは0xbfでDATA部にエラーコードが設定されます。エラーコードはs The ERROR constants and values are: のあたりを参照してください。
KEEPALIVEなんですけど、これはデバイス側での操作待ちのときに送信されてきます。例えば指紋認証が完了するのを待っている最中です。この辺の動きはサンプルプログラムを動かして見るとよくわかると思います。
#####High part of data length,Low part of data length,Data
受信データDATA部のバイト数、応答データです。
送信パケットと同じです。
##通信処理
CTAPのauthenticatorGetInfoを送信して応答を受信する例です。
CTAPのauthenticatorGetInfoについてはコチラを参照。
- 5.4. authenticatorGetInfo (0x04)
- CTAP2 お勉強メモ#2 - 接続(GetInfo)
###送信
private async Task<bool> sendCommand(byte[] command)
{
// 受信バッファを用意
ReceveData = new List<byte>();
// 送信 Characteristic_Sendは接続時にとっておいたもの
var result = await Characteristic_Send.WriteValueAsync(command.AsBuffer(), GattWriteOption.WriteWithResponse);
if (result != GattCommunicationStatus.Success) {
// error
return (false);
}
return (true);
}
private async void ButtonGetInfo_Click(object sender, RoutedEventArgs e)
{
var cmd = new byte[4];
// Command identifier
cmd[0] = 0x83; // MSG
// High part of data length
cmd[1] = 0x00;
// Low part of data length
cmd[2] = 0x01; // 1byte
// Data (s is equal to the length)
cmd[3] = 0x04; // authenticatorGetInfo(0x04)
var result = await sendCommand(cmd);
addLog("送信しました");
}
###受信
接続のときに設定した受信イベントハンドラで受信データを受けます。
※複数パケットに分割された応答の処理もありますが、説明は後述します。
protected void characteristicChanged_OnReceiveFromDevice(GattCharacteristic sender, GattValueChangedEventArgs eventArgs)
{
byte[] data = new byte[eventArgs.CharacteristicValue.Length];
Windows.Storage.Streams.DataReader.FromBuffer(eventArgs.CharacteristicValue).ReadBytes(data);
// parse
{
// [0] STAT
if (data[0] == 0x81) {
addLog($"PING");
} else if (data[0] == 0x82) {
addLog($"KEEPALIVE");
} else if (data[0] == 0x83) {
addLog($"MSG");
// [1] HLEN
// [2] LLEN
// [3-] DATA
var buff = data.Skip(3).Take(data.Length).ToArray();
// 最初の1byteは応答ステータスで2byteからCBORデータ
var cbor = buff.Skip(1).Take(buff.Length).ToArray();
// 受信バッファに追加
ReceveData.AddRange(cbor.ToList());
} else if (data[0] == 0xbe) {
// CANCEL
addLog($"CANCEL");
} else if (data[0] == 0xbf) {
// ERROR
addLog($"ERROR");
} else {
// データの続き
addLog($"CBOR Data...");
var buff = data;
// 最初の1byteは応答ステータスで2byteからCBORデータ
var cbor = buff.Skip(1).Take(buff.Length).ToArray();
// 受信バッファに追加
ReceveData.AddRange(cbor.ToList());
}
}
addLog("受信しました");
return;
}
##分割送信、分割受信
以下のケースに対応する仕様があります。
- 分割送信:送信するCommandが長い時の対応
- 分割受信:受信するResponseが長いときの対応
###分割送信
送信バッファの最大サイズ(バイト数)はFIDO Control Point Length
に格納されています、という話をしましたが、これを超えるパケットを送信したい場合はパケットを分割して送信します。
AllinPassは0x00,0x9B=155byteでしたので、これを超える場合の例です。
ポイントはPacket sequence 0x00..0x7f
の行で、要するにパケットシーケンス番号を指定して分割送信しましょう、ということです。
詳細は8.3.10. Framing fragmentation
private async void ButtonGetAssertion_Click(object sender, RoutedEventArgs e)
{
// 送信データを作成
var payloadb = ・・・;
var cmd = new List<byte>();
// Command identifier
cmd.Add(0x83); // MSG
// High part of data length
cmd.Add(0x00);
// Low part of data length
cmd.Add((byte)(payloadb.Length + 1));
// パケット2つに分割送信してみる
// fidoControlPointLength=0x9B(155byte)
// なので、1パケット155になるように分割する
// ※155より小さい値で分割してもエラーになる
// ※このサンプルでは固定値にしていますが、fidoControlPointLengthが155とは限らないので注意
var send1 = payloadb.Skip(0).Take(151).ToArray();
var send2 = payloadb.Skip(151).Take(100).ToArray();
// Frame 0
cmd.Add(0x02); // authenticatorGetAssertion (0x02)
cmd.AddRange(send1);
var result1 = await sendCommand(cmd.ToArray());
// Frame 1
cmd.Clear();
cmd.Add(0x00); // Packet sequence 0x00..0x7f (high bit always cleared)
cmd.AddRange(send2);
var result2 = await sendCommand(cmd.ToArray());
}
※ハマりやすいポイントとしては、ちゃんとFIDO Control Point Length
で設定されているバイト数で分割しないとエラーになるという事です。FIDO Control Point Length
と異なる値、例えば100byteで分割送信とかしてもエラーになります。
###分割受信
受信データが大きい場合、いくつかのパケットに分かれて受信します。
これも分割送信と同じルールで送られてきます。
authenticatorMakeCredential のレスポンスはサイズが大きいのでいくつかのパケットに分割されるはずです。
#おつかれさまでした
これでCTAP2はコンプリート!