LoginSignup
3
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-09-02

以前、WebAuthnを使ったFIDOサーバを立ててみた で、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ごとにカウンタを覚えておくべきですが、今回は手を抜いています。

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

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

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
 HID:FIDO U2F HID Protocol Specification
 NFC:FIDO NFC Protocol Specification v1.0

以上

3
3
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
3
3