以前の投稿 FIDOデバイスエミュレータを作成してみた。。。が で、FIDOデバイスエミュレータを作成しました。
ですが、ブラウザ上のWebAuthnと接続するトランスポートレイヤ層の実装ができずに、未完成となっていました。
あれから月日がたち、ようやく進捗できるように知識がたまってきたので、改めてトランスポートレイヤ層の実装をして完成を目指したいと思います。
結論
おしいところで未完成です。
(2020/4/5 修正)
FIDOデバイスエミュレータ内で生成するx509証明書のバージョンがv3である必要があるそうです。試作したのはv1だったので、そこで引っかかってしまいました。
いろいろWebで探しているのですが、簡単にv3を作成する方法がわからなかったため、
あと一歩で完成に至っていません。
x509証明書をv3にしてもだめでした。
#やったこと
ブラウザ上のWebAuthnと、サーバのFIDOデバイスエミュレータの間をArduinoのESP32で中継するようにしました(以降、仮想FIDOデバイスと呼びます)。WebAuthnとはBLEで接続し、サーバとの間はWiFi経由でHTTP Post呼び出しで接続するようにしました。(今回頑張ったところはここです)
#仮想FIDOデバイスの要件
仮想FIDOデバイスがブラウザ上のWebAuthnから接続を受けるには、以下の要件を満たす必要があります。
・CTAPで定義されたBLEまたはHIDまたはNFCデバイスであること。(今回はBLE)
・BLEペアリングおよびボンディングできること
・プライマリサービス0xFFFDが存在すること
詳細 : FIDO Alliance Download Specification
https://fidoalliance.org/specifications/download/
・FIDO U2F Raw Message Formats
・FIDO U2F Bluetooth® protocol
・FIDO U2F Authenticator Transports Extension
あとは、Bluetooth protocolに従って、プライマリサービス0xFFFDのキャラクタラスティックでコマンドをWriteValueで受信、レスポンスをNotificationで返すように実装します。
仮想FIDOデバイスの実装の解説
すべてのソースコードは以下に上げておきましたので、必要に応じて参照してください。
https://github.com/poruruba/fido_server/tree/master/pseudo_fido_device
・ESP32を使います。今回は、ESP32 DevKitCを使いました。
・Arduinoベースで作成しました。Visual Studio Codeで、PlatformIOを使うと便利です。
まずは、setup()から見ていきましょう。
void setup() {
Serial.begin(9600);
Serial.println("Starting setup");
WiFi.begin(wifi_ssid, wifi_password);
Serial.println("Connecting to Wifi AP...");
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println(WiFi.localIP());
Serial.println("Starting BLE work!");
xTaskCreate(taskServer, "server", 20000, NULL, 5, NULL);
}
FIDOエミュレータサーバと通信するためにWiFiを使います。
WiFiアクセスポイントにつながって初めて、BLEが開始されます。
実際のBLE処理は、void taskServer(void*) で定義されています。
今度は、void taskServer(void*) の処理を見ていきましょう。
まずは、以下のことをしています。
・BLEデバイスの初期設定
BLEDevice::init(DEVICE_NAME);
ここで設定した文字列が、Generic Accessサービスのデバイス名になります。
・セキュリティレベルの設定
BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_MITM);
・認証時のコールバック関数の設定
BLEDevice::setSecurityCallbacks(new MySecurity());
ペアリングの際のPasskeyの処理をコールバック関数で実装します。
以下の各関数を実装します。
class MySecurity : public BLESecurityCallbacks {
bool onConfirmPIN(uint32_t pin){
// PIN認証の場合(今回は使わない)
}
uint32_t onPassKeyRequest(){
// セントラル側がPasskeyを決めた場合に、ペリフェラルに入力するPasskey
// 今回は使わない
}
void onPassKeyNotify(uint32_t pass_key){
// ペリフェラル側でPasskeyを決めた場合に、セントラル側に伝えたいPasskey
}
bool onSecurityRequest(){
/* ペアリング要否 */
}
void onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl){
if(cmpl.success){
// ペアリング完了
}else{
// ペアリング失敗
}
}
};
・BLEサーバの生成
特にこれといった処理はしていません。コールバック関数もありますが、セントラルとBLE接続されたときや切断されると呼び出されますが、特に何もしていません。
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyCallbacks());
以降、ペアリング関係の処理をしています。一つずつ説明します。
これはお決まり。
BLESecurity *pSecurity = new BLESecurity();
pSecurity->setKeySize(16);
以下は、ボンディングできるようにする設定です。
/* ESP_LE_AUTH_NO_BOND, ESP_LE_AUTH_BOND, ESP_LE_AUTH_REQ_MITM */
// pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_MITM);
pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);
下記の関数は、Arduinoにパッケージ化されているESP-IFのバージョンには未実装です。
// pSecurity->setStaticPIN(BLE_PASSKEY);
代わりに、以下を実装しています。上記と同じ内容です。通常は、BLEデバイス側でPasskeyを生成する際に、乱数から生成しますが、面倒なので、固定値にしています。乱数にする場合には以下の2行をコメントアウトしてください。
/* for fixed passkey */
uint32_t passkey = BLE_PASSKEY;
esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &passkey, sizeof(uint32_t));
仮想FIDOデバイスには、入力装置がなく、出力装置のみあるという前提にしています。
これにより、Passkeyの生成は、ペリフェラル側となり、セントラル側でPasskey入力が求められるようになります。
/* ESP_IO_CAP_IN, ESP_IO_CAP_OUT, ESP_IO_CAP_KBDISP */
pSecurity->setCapability(ESP_IO_CAP_OUT);
// pSecurity->setCapability(ESP_IO_CAP_IN);
ボンディングには以下の通りの鍵交換の設定が必要です。
pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
以降は、プライマリサービス(0xFFFD)とDevice Informationサービスの設定です。
見ていただければわかるかと思います。
3点ほど補足します。
①
fidoControlPointで以下のようにしています。
pCharacteristic_fidoControlPoint->setAccessPermissions(ESP_GATT_PERM_WRITE /* ESP_GATT_PERM_WRITE_ENCRYPTED */ );
本来であれば、ESP_GATT_PERM_WRITE_ENCRYPTEDを指定して、ペアリング完了しなければWriteできないようにするべきです。ですが、なぜかArduinoのESP32ではうまく動きませんでした。
②
以下の部分が、大事な大事なFIDOエミュレータサーバとWebAuthnとのつなぎ役の部分のコールバックになります。後述します。
pCharacteristic_fidoControlPoint->setCallbacks(new MyCharacteristicCallbacks());
③
fidoControlPointLength は、後ほど説明するfidoControlPointで送受信するパケットの最大長です。今回は20バイトとしています。
あとはアドバタイジング開始です。
アドバタイジングデータに、大事なプライマリサービス0xFFFDを含めるようにしています。
loop()では何をしているかというと、何もしていません。
さきほどの、xTaskCreate(taskServer, "server", 20000, NULL, 5, NULL); によって、必要な処理が別タスクで処理されているためです。
fidoControlPointのキャラクタリスティック処理
fidoControlPointに、WebAuthnからのコマンドを受信します。
WriteValueを受け付けると、コールバッククラスのMyCharacteristicCallbacksのonWriteが呼ばれてきます。
uint8_t* value = pCharacteristic->getData();
std::string str = pCharacteristic->getValue();
受信データはgetData()でバイト配列のポインタを取得できます。
しかしながら、受信データサイズがわからないため、getValue()でstring型でも受け取りそのサイズを受信サイズとみなしています。(ちょっとトリッキーですがこれしか方法がないようです。)
以降は、FIDOのBluetooth protocolにのっとったやり取りになります。
受信データサイズ全体は大きくて数百バイトなのですが、一度に送受信できるサイズは、前述の20バイトとしていました。
したがって、20バイトに分割受信または分割送信する必要があります。
必ず先頭が0x83で、続いて2バイトでビッグエンディアンがあり、送信したいデータ(20バイトに収まる分)が続きます。
そのあとに、20バイトきっかりずつ分割受信されてきます。ただし、2つ目以降の受信パケットの先頭が0x00、次が0x01、その次が0x02でインクリメントされます。20バイトぴったりで受信している時点ではまだ次があると考え、20バイトより小さかったらそれが最後のパケットとみなします。
そしていよいよ、FIDOエミュレータサーバへの転送です。
HTTP Postで転送します。Post時のJson生成には、ArduinoJsonを使いました。
HTTPClient http;
switch(recv_buffer[1]){
case 0x01: http.begin(endpoint_u2f_register); break;
case 0x02: http.begin(endpoint_u2f_authenticate); break;
case 0x03: http.begin(endpoint_u2f_version); break;
default:
Serial.println("Unknown INS");
return;
}
WebAuthnから受信したデータは、APDU形式になっています。
具体的には、以下の通りです。
CLS INS P1 P2 Lc1 Lc2 Lc3 データ Le1 Le2
INSの値が、コマンドコードに相当します。その値によって、FIDOエミュレータサーバの接続先エンドポイントを切り分けています。
以下の部分が、JSON生成部分です。create_string() で、バイト配列を16進数文字列に変換しています。serializeJson のところで、JSON文字列化しています。
json_request["input"] = create_string(&recv_buffer[0], recv_len).c_str();
serializeJson(json_request, json_buffer, sizeof(json_buffer));
以下で、HTTP Postの実行です。
int status_code = http.POST((uint8_t*)json_buffer, strlen(json_buffer));
以下のところで、受信したJSONデータをパースし、16進数文字列をparse_hexでバイト配列に変換しています。
DynamicJsonDocument json_response(2048);
deserializeJson(json_response, *resp);
const char* result = json_response["result"];
unsigned short len = parse_hex(result, recv_buffer);
そのあとは、受信時の20バイトサイズのパケット分割受信と同様に、20バイトサイズ分割のNotificationを行っています。
以下が、Notificationの実行部分です。
pCharacteristic_fidoStatus->setValue(value_fidoStatus, packet_size + 1);
pCharacteristic_fidoStatus->notify(true);
さっそく実施してみたが。。。
(2020/4/5 修正)
x509v3証明書にしたので、lib-fido2の修正はいらなくなりました。
FIDOサーバ側で以下のエラーが発生しました。
npmのfido2-libを使っています。
一難去ってまた一難なのですが、ログイン処理(U2F_Authenticate)でエラーとなってしまいました。
FIDOエミュレータサーバおよび仮想FIDOデバイスでは正しく処理しているつもりが、ブラウザ上のWebAuthnからエラーが返ってきているようです。
ブラウザからこんな感じの画面です。
x509がv3ではないからなのではないかと疑っています。なぜならば、U2F_Register時に間違った署名を付けてもそのままFIDOサーバまでスルーしているようで、U2F_Authenticateで初めてちゃんとチェックするようにしていると思われます。
考えられることとして、x509証明書がオレオレ証明書で、FIDO Allianceが認めたものでも何でもないことですかね。
#その他
・WebAuthnのWebページは、BLEを扱うためHTTPSでホスティングされている必要があります。
・仮想FIDOデバイスとはあらかじめボンディングが完了している必要があります。OSの機能で設定してしておきます。
#結局
あとは、x509v3を簡単に作れる方法を調べればよいはず。。。。
あと少し!?
x509証明書がFIDO Allianceに認められていないものとなると、これ以上は無理かなあ。
以上