FIDOデバイスエミュレータを作成してみた。。。が

以前、[WebAuthnを使ったFIDOサーバを立ててみた](https://qiita.com/poruruba/items/243d39c8b77b98a99bab) で、FIDO認証に対応したサーバを作成しました。

今度は、FIDOデバイスを模擬するエミュレータを作ってみます。
普通に作ると、以下のような構成となります。

　FIDO認証対応サーバ
　　↓ (HTTP)
　アプリのHTMLページ
　WebAuthn(Javascript)
　　↓ (BLE、HID、NFC)
　FIDOデバイスエミュレータ

それをちょっと一工夫を入れて、最終的には、以下のような構成を目指します。

　FIDO認証対応サーバ
　　↓ (HTTP)
　アプリのHTMLページ
　WebAuthn(Javascript)
　　↓ (BLE)
　Androidスマホアプリ →(HTTP POST)→ FIDOデバイスエミュレータサーバ

どうでしょうか。スマホさえあれば、FIDOデバイスとしていつでもどこでも使えるようになりませんか？！
今回作成する部分は、FIDOデバイスエミュレータサーバの部分です。

で、で、なのですが、なんと、最後のFIDOデバイスとWebAuthnをつなぐトランスポートレイヤ(BLE、HID、NFCのいずれか)が作れるノウハウが私にはありませんっ！残念ながら私の能力ではここまでです。

サーバのソースコードを以下にアップしておきました。
　https://github.com/poruruba/fido2_server
以前に作成したFIDOサーバのソースコードにマージしています。

(2020/3/22 修正)
WebAuthnクライアントから受け入れるパケットをAPDU形式にしました。

(2020/4/5 修正)
x509証明書をv3にしました。それに伴い、証明書生成のためのnpmモジュールを変更しました。

# FIDOデバイスエミュレータのサーバのWebAPI

用意するWebAPIは、「FIDO U2F Raw Message Formats」で定義されている3つのコマンドです。このコマンドは、HIDやBLEやNFCでやり取りされる際の共通のコマンドです。

・U2F_REGISTER
・U2F_AUTHENTICATE
・U2F_VERSION

(参考情報)
https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html

それぞれのWebAPIへの入力には、WebAuthnクライアントであるブラウザから受け取ったFIDO U2F Raw Messageをそのまま渡します。以下のAPDUフォーマットになっているかと思います。

　CLS INS P1 P2 Lc1 Lc2 Lc3 Data Le1 Le2

ちょっと補足します。

・u2f_register(challenge, applicatoin)
　FIDO認証対応サーバに本FIDOデバイスを登録するためのトークンを生成します。FIDO認証対応サーバは、このトークンを覚えておきます。
　challenge: challenge parameter[32バイトBuffer]、ClientDataのSHA-256です。
　application: application parameter[32バイトBuffer]、アプリケーションアイデンティティのSHA-256です。単に32バイトの配列として扱います。

・u2f_authenticate(control, challenge, application, key_handle)
　FIDO認証対応サーバに登録しておいたトークンを使って、本FIDOデバイスを認証します。
　control: 1バイト、クライアントがチェックのためだけに認証するのか、ユーザプレゼンスを含めて認証するのかどうかを指示するものです。
　challenge: challenge parameter[32バイトBuffer]、ClientDataのSHA-256です。
　application: application parameter[32バイトBuffer]、アプリケーションアイデンティティのSHA-256です。単に32バイトの配列として扱います。
　key_handle: 本FIDOデバイスの内部で保持する公開鍵ペアを識別するハンドル。u2f_registerで生成されたトークンの中に含まれています。

・u2f_version()
　本FIDOデバイスのFIDOバージョンを返します。固定で"U2F_V2"を返します。

WebAuthnと接続するFIDOデバイスに仕立て上げるときには、WebAuthnから受けたコマンドをこのFIDOデバイスエミュレータサーバにWebAPIで問い合わせ、その応答をWebAuthnに返せばよいわけです。

# FIDOデバイスエミュレータサーバを立ち上げる

それでは、サーバを立ち上げましょう。
一番簡単なのは、Gitに上げたソースをCloneすることです。

```
> git clone https://github.com/poruruba/fido_server
> cd fido_server
> npm install
> node app.js
```

あとは、以下のURLにPOST呼び出しするだけです。

　http://localhost:10080/device/u2f_register
　http://localhost:10080/device/u2f_authenticate
　http://localhost:10080/device/u2f_version

実装には、以下のnpmを使わせていただきました。

・ecdsa-secp256r1
　https://github.com/forevertz/ecdsa-secp256r1

・jsrsasign
　https://kjur.github.io/jsrsasign/

## u2f_register

以下の処理を行います。

・楕円暗号公開鍵ペアの作成
　認証を行うサイトごとに楕円暗号公開鍵ペアを生成します。

・内部管理用のアプリケーションIDの決定
　FIDOデバイス内で複数の楕円暗号公開鍵ペアを生成し保持する前提で、それらを識別する番号を決めます。今回は単純に、1からのインクリメント値です。

・KeyHandleの作成
　認証を行うサイトが、レスポンスで返すトークンを識別するためのハンドルを生成します。今回は、公開鍵ペアの秘密鍵をFIDOデバイスエミュレータサーバで保持するのがめんどうなので、内部管理用のアプリケーションIDと公開鍵ペアの秘密鍵を返しちゃっています。当然ながら、秘密鍵がバレバレになってしまうので、実際にはこんなことはしないで下さい。

・X.509証明書の作成
　おれおれX.509証明書を作成します。（おれおれではだめな気がする。。。)

・署名の生成
　生成した楕円暗号公開鍵ペアを使って署名を生成します。

## u2f_authenticate

以下の処理を行います。

・内部管理用のアプリケーションIDの抽出
　KeyHandleに埋め込んでおいた内部管理用のアプリケーションIDを抽出します。

・楕円暗号公開鍵ペアの復元
　KeyHandleに埋め込んでおいた楕円暗号公開鍵ペアの秘密鍵を抽出します。

・署名回数カウンタの決定
　署名回数カウンタをインクリメントします。内部管理用アプリケーションIDごとにカウンタを覚えておくべきですが、今回は手を抜いています。

・署名生成
　署名を生成します。

ソースコードを示します。

```js:api/controllers/fido_emulator/index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const Redirect = require(HELPER_BASE + 'redirect');

const rs = require('jsrsasign');
const ECDSA = require('ecdsa-secp256r1')
const crypto = require('crypto');

const curveLength = Math.ceil(256 / 8);

const FIDO_ISSUER = process.env.FIDO_ISSUER || 'FT FIDO 0200';
const FIDO_SUBJECT = process.env.FIDO_SUBJECT || 'FT FIDO P2000000000000';
const FIDO_EXPIRE = Number(process.env.FIDO_EXPIRE) || 365;

var total_counter = Number(process.env.COUNTER_START) || 0;
var total_application_id = Number(process.env.APPLICATION_ID_START) || 1;

// X509証明書の楕円暗号公開鍵ペアの作成
var kp_cert = rs.KEYUTIL.generateKeypair('EC', 'secp256r1');

exports.handler = async (event, context, callback) => {
  if( event.path == "/device/u2f_register"){
    var body = JSON.parse(event.body);
    console.log(body);
    
    var input = Buffer.from(body.input, 'hex');
    var result = await u2f_register(input.subarray(7, 7 + 32), input.subarray(7 + 32, 7 + 32 + 32));
    
    return new Response({
      result: Buffer.concat([ result, Buffer.from([0x90, 0x00])]).toString('hex')
    });
  }else
  if( event.path == "/device/u2f_authenticate"){
    var body = JSON.parse(event.body);
    console.log(body);

    var input = Buffer.from(body.input, 'hex');
    var result = await u2f_authenticate(input[2], input.subarray(7, 7 + 32), input.subarray(7 + 32, 7 + 32 + 32), input.subarray(7 + 32 + 32 + 1, 7 + 32 + 32 + 1 + input[7 + 32 + 32]));

    return new Response({
      result: Buffer.concat([ result, Buffer.from([0x90, 0x00])]).toString('hex')
    });
  }else
  if( event.path == "/device/u2f_version"){
    var result = await u2f_version();
    return new Response({
      result: Buffer.concat([ result, Buffer.from([0x90, 0x00])]).toString('hex')
    });
  }
};

async function u2f_register(challenge, application){
  // 楕円暗号公開鍵ペアの作成
  var kp = rs.KEYUTIL.generateKeypair('EC', 'secp256r1');

  var pubkey = Buffer.from(kp.pubKeyObj.pubKeyHex, 'hex');
  var privkey = Buffer.from(kp.prvKeyObj.prvKeyHex, 'hex');
  var privateKey = new ECDSA({
    d: privkey,
    x: pubkey.slice(1, 1 + curveLength),
    y: pubkey.slice(1 + curveLength)
  });
  var userPublicKey = pubkey;

  // 内部管理用のアプリケーションIDの決定
  console.log('application_id='+ total_application_id);

  // KeyHandleの作成
  var keyHandle = Buffer.concat([Buffer.from([(total_application_id >> 24) & 0xff, (total_application_id >> 16) & 0xff, (total_application_id >> 8) & 0xff, total_application_id & 0xff]),  				privkey] );
  total_application_id++;
  var keyLength = Buffer.from([keyHandle.length]);

  // X.509証明書の作成
  var tbsc = new rs.KJUR.asn1.x509.TBSCertificate();

  tbsc.setSerialNumberByParam({'int': 1234});
  tbsc.setSignatureAlgByParam({'name': 'SHA256withECDSA'});
  tbsc.setIssuerByParam({'str': "/CN=FT FIDO 0200"});  
  tbsc.setNotBeforeByParam({'str': "190511235959Z"});
  tbsc.setNotAfterByParam({'str': "340511235959Z"});
  tbsc.setSubjectByParam({'str': "/CN=FT FIDO P2000000000000"});  
  tbsc.setSubjectPublicKey(kp.pubKeyObj);

/*
  //サブジェクトキー識別子
  var extSKI = new rs.KJUR.asn1.x509.Extension();
  extSKI.oid = '2.5.29.14';
  const ski = rs.KJUR.crypto.Util.hashHex(kp_cert.pubKeyObj.pubKeyHex, 'sha1');
  const derSKI = new rs.KJUR.asn1.DEROctetString({ hex: ski });
  extSKI.getExtnValueHex = () => {return derSKI.getEncodedHex() };
  tbsc.appendExtension(extSKI);
*/

  // FIDO U2F certificate transports extension
  var extSKI2 = new rs.KJUR.asn1.x509.Extension();
  extSKI2.oid = '1.3.6.1.4.1.45724.2.1.1';
  extSKI2.getExtnValueHex = () => { return "03020640" };
  tbsc.appendExtension(extSKI2);
    
  var cert = new rs.KJUR.asn1.x509.Certificate({'tbscertobj': tbsc, 'prvkeyobj': kp_cert.prvKeyObj });
  cert.sign();
  var attestationCert = Buffer.from(cert.hTLV, 'hex');
  
  // 署名の生成
  var input = Buffer.concat([
    Buffer.from([0x00]),
    application,
    challenge,
    keyHandle,
    userPublicKey
  ]);
  const sign = crypto.createSign('RSA-SHA256');
  sign.update(input);
  var signature = sign.sign(privateKey.toPEM());
  
  console.log('userPublicKey(' + userPublicKey.length + ')=' + userPublicKey.toString('hex'));
  console.log('keyHandle(' + keyHandle.length + ')=' + keyHandle.toString('hex'));
  console.log('attestationCert(' + attestationCert.length + ')=' + attestationCert.toString('hex'));
  console.log('signature(' + signature.length + ')=' + signature.toString('hex'));

  // レスポンスの生成(concat)
  return Buffer.concat([
    Buffer.from([0x05]),
    userPublicKey,
    keyLength,
    keyHandle,
    attestationCert,
    signature
  ]);
}

async function u2f_authenticate(control, challenge, application, keyHandle){
  console.log('control=', control);

  var userPresence = Buffer.from([0x01]);

  // 内部管理用のアプリケーションIDの抽出
  var application_id = (keyHandle.readUInt8(0) << 24) | (keyHandle.readUInt8(1) << 16) | (keyHandle.readUInt8(2) << 8) | keyHandle.readUInt8(3);
  console.log('application_id=' + application_id);

  // 楕円暗号公開鍵ペアの復元
  var ecdh = crypto.createECDH('prime256v1');
  ecdh.setPrivateKey(keyHandle.slice(4));

  var pubkey = ecdh.getPublicKey();
  var privkey = ecdh.getPrivateKey();
  var privateKey = new ECDSA({
    d: privkey,
    x: pubkey.slice(1, 1 + curveLength),
    y: pubkey.slice(1 + curveLength)
  })

  // 署名回数カウンタの決定
  total_counter++;
  console.log('total_counter=' + total_counter);
  var counter = Buffer.from([ (total_counter >> 24) & 0xff, (total_counter >> 16) & 0xff, (total_counter >> 8) & 0xff, total_counter & 0xff ])

  // 署名生成
  var input = Buffer.concat([
    application, 
    userPresence,
    counter,
    challenge
  ]);
  const sign = crypto.createSign('RSA-SHA256');
  sign.update(input);
  var signature = sign.sign(privateKey.toPEM());

  console.log('sigunature(' + signature.length + ')=' + signature.toString('hex'));

  // verify sample code
/*  
  const verify = crypto.createVerify('RSA-SHA256')
  verify.write(input)
  verify.end();

  var result =  verify.verify(
    privateKey.asPublic().toPEM(),
    signature
  );
  console.log('verify result=' + result);
*/

  // レスポンスの生成(concat)
  return Buffer.concat([
    userPresence,
    counter,
    signature
  ]);
}

async function u2f_version(){
  var version = Buffer.from('U2F_V2');
  return Promise.resolve(version);
}
```

以下は、それぞれの環境に合わせて変更してください。

・FIDO_ISSUER：X.509証明書に含める発行者名
・FIDO_SUBJECT：X.509証明書に含めるサブジェクト名
・FIDO_EXPIRE：X.509証明書の有効期間
・COUNTER_START：署名回数カウンタの初期値
・APPLICATION_ID_START：内部管理用アプリケーションIDの初期値

# 最後に

ここまでできていて、最後の最後のHIDまたはBluetoothペリフェラルがうまく作れていません。悲しい。。。
認定済みFIDOデバイスであることの証明が必要な気がするが、Chromeで実際についないでいないので不明。。。

FIDOデバイスとWebAuthnをつなぐトランスポートレイヤの仕様は以下にあります。

　BLE：[FIDO Bluetooth® Specification v1.0](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-bt-protocol-v1.2-ps-20170411.html)
　HID：[FIDO U2F HID Protocol Specification](https://fidoalliance.org/specs/fido-u2f-v1.0-ps-20141009/fido-u2f-hid-protocol-ps-20141009.html)
　NFC：[FIDO NFC Protocol Specification v1.0](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-nfc-protocol-v1.2-ps-20170411.html)

以上