1. U2F JS API

  FIDO U2F定义了JavaScript API供开发者开发支持U2F设备的在线服务网站。U2F JS API分为两类:底层基于消息端口的API和上层应用API。在FIDO的规格文档中介绍底层API用于与U2F设备进行消息通讯(使用MessagePort Object),发送和接收消息。本文重点关注屏蔽了通讯细节的上层API接口。

2. 接口定义

2.1. u2f接口

  使用WebIDL定义的u2f接口定义如下:

interface u2f {
    void register (DOMString appId, sequence registerRequests, sequence registeredKeys, function(RegisterResponse or Error) callback, optional unsigned long? opt_timeoutSeconds);
    void sign (DOMString appId, DOMString challenge, sequence registeredKeys, function(SignResponse or Error) callback, optional unsigned long? opt_timeoutSeconds);
};

2.2. register方法

2.2.1. 请求参数

  register方法中各参数描述如下:

参数名称 类型 可否为空 是否可选 描述
appId DOMString 请求中的应用ID
registerRequests sequence 注册请求序列
registeredKeys sequence 已经注册到U2F设备的信息
callback function(RegisterResponse or Error) 注册请求回调函数
opt_timeoutSeconds unsigned long 客户端等待请求处理的超时时间

2.2.2. 返回值

  register方法成功返回的数据(callback的参数)使用RegisterResponse结构。

dictionary RegisterResponse {
    DOMString version;
    DOMString registrationData;
    DOMString clientData;
};

  其中各属性含义如下:

  • version:U2F协议版本,如“U2F_V2”
  • registrationData:使用websafe-base64编码后的注册数据,数据格式参看《FIDO U2F设备应用与开发(一)-原理与协议》3.2节。
  • clientData:使用websafe-base64编码后的clientData,数据格式参看《FIDO U2F设备应用与开发(一)-原理与协议》3.5节。

    2.3. sign方法

    2.3.1. 请求参数

      sign方法中各参数描述如下:

参数名称 类型 可否为空 是否可选 描述
appId DOMString 请求中的应用ID
challenge DOMString 使用WEBSAFE-BASE64编码的挑战值
registeredKeys sequence 待签名用户的注册信息
callback function(SignResponse or Error) 签名请求回调函数
opt_timeoutSeconds unsigned long 客户端等待请求处理的超时时间

2.3.2. 返回值

  sign方法成功返回的数据(callback的参数)使用SignResponse结构。

dictionary SignResponse {
    DOMString keyHandle;
    DOMString signatureData;
    DOMString clientData;
};

  其中各属性含义如下:

  • keyHandle:请求中提供的key handle
  • signatureData:使用websafe-base64编码后的签名数据,数据格式参看《FIDO U2F设备应用与开发(一)-原理与协议》3.4节。
  • clientData:使用websafe-base64编码后的clientData,数据格式参看《FIDO U2F设备应用与开发(一)-原理与协议》3.5节。

    2.4. 错误码

      register和sign方法失败时返回的错误码定义如下:

    interface ErrorCode {
    const short OK = 0;
    const short OTHER_ERROR = 1;
    const short BAD_REQUEST = 2;
    const short CONFIGURATION_UNSUPPORTED = 3;
    const short DEVICE_INELIGIBLE = 4;
    const short TIMEOUT = 5;
    };

    2.5. 接口中的数据结构

    2.5.1. RegisterRequest

      使用WebIDL定义的RegisterRequest结构如下:

    dictionary RegisterRequest {
    DOMString version;
    DOMString challenge;
    };

    属性含义如下:

  • version:U2F协议版本,如“U2F_V2”
  • challenge:使用websafe-base64编码的挑战值

2.5.2. RegisteredKey

  使用WebIDL定义的RegisteredKey结构如下:

dictionary RegisteredKey {
    DOMString   version;
    DOMString   keyHandle;
    Transports? transports;
    DOMString?  appId;
};

各属性含义如下:

  • version:U2F协议版本,如“U2F_V2”
  • keyHandle:用于签名用户的key handle
  • transports:传输方式,可选参数
  • appId:在线服务网站应用Id

3. 编程接口实例探究

3.1. 注册过程

  让我们来到yubico的U2F设备测试网站(https://demo.yubico.com/u2f), 使用yubico的安全key,看看register方法和sign方法如何使用。
  从淘宝上购买的两个U2F Key,如图1所示,其中一个飞天品牌的带蓝牙功能。
FIDO U2F应用开发(二)-编程接口_第1张图片
  首先测试注册过程,如图2所示。
FIDO U2F应用开发(二)-编程接口_第2张图片
  通过跟踪网站客户端与服务端的交互消息,我们发现开始注册过程后,客户端向服务端的提交了两次请求,对应《FIDO U2F设备应用与开发(一)-原理与协议》第3节描述的3个阶段中的第1和第3阶段。
  客户端第一次向服务端提交请求后,参数包含用户名和口令,如图3所示。
FIDO U2F应用开发(二)-编程接口_第3张图片

  在客户端JS脚本对服务端返回的注册请求进行register函数调用后,将register的注册数据提交给服务端,提交的表单数据如图4所示。
FIDO U2F应用开发(二)-编程接口_第4张图片
  由图4提交的数据可以看到register返回成功,提交的表单数据为:

名称
mode bind
username zhangkai
password zhangkai
enroll-data {"challenge": "OlyOzHxaxx6LUXX5chXsxj4GfspKTskBANNOtQ_UwcA", "version": "U2F_V2", "appId": "https://demo.yubico.com"}
data(u2f设备接口调用返回数据) {"registrationData":"BQSns2lmNJhJPSFbiDioTABT5xd2OZQpmpZFREJpbiaQC8zssXg0jLaxz8_gMioQQILSE5lsbH5BqpJwWR4rJoI1QMxn5LhVlLKhs_W-F7x4ppkw9K57h7dsTCDsikFv9BnpfSvj8XYhEHV-KEoBg8sNXq_I6-PRQ5_Z6yDkAFzrlBQwggJKMIIBMqADAgECAgQSSnL-MA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjAsMSowKAYDVQQDDCFZdWJpY28gVTJGIEVFIFNlcmlhbCAyNDk0MTQ5NzIxNTgwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ9ixu9L8v2CG4QdHFgFGhIQVPBxtO0topehV5uQHV-4ivNiYi_O-_XzfIcsL9dehUNhEr-mBA8bGYH2fquKHwCozswOTAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMTATBgsrBgEEAYLlHAIBAQQEAwIFIDANBgkqhkiG9w0BAQsFAAOCAQEAoU8e6gB29rhHahCivnLmDQJxu0ZbLfv8fBvRLTUZiZFwMmMdeV0Jf6MKJqMlY06FchvC0BqGMD9rwHXlmXMZ4SIUiwSW7sjR9PlM9BEN5ibCiUQ9Hw9buyOcoT6B0dWqnfWvjjYSZHW_wjrwYoMVclJ2L_aIebzw71eNVdZ_lRtPMrY8iupbD5nGfX2BSn_1pvUt-D6JSjpdnIuC5_i8ja9MgBdf-Jcv2nkzPsRl2AbqzJSPG6siBFqVVYpIwgIm2sAD1B-8ngXqKKa7XhCkneBgoKT2omdqNNaMSr6MYYdDVbkCfoKMqeBksALWLo2M8HRJIXU9NePIfF1XeUU-dzBFAiAtXTkSxA8NFX8RU-qNtKdzBkuVSk-rIFjhkCJRALTIBwIhAKjY3XT8vJgjgyOyGhEyxGF8zQonpWvdOwFoTe77cOv-","version":"U2F_V2","challenge":"OlyOzHxaxx6LUXX5chXsxj4GfspKTskBANNOtQ_UwcA","attestation":"direct","clientData":"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCIsImNoYWxsZW5nZSI6Ik9seU96SHhheHg2TFVYWDVjaFhzeGo0R2ZzcEtUc2tCQU5OT3RRX1V3Y0EiLCJvcmlnaW4iOiJodHRwczovL2RlbW8ueXViaWNvLmNvbSIsImNpZF9wdWJrZXkiOiJ1bnVzZWQifQ"}

3.2. 鉴权过程

  注册成功后,可执行鉴权(login)过程,如图5所示。
FIDO U2F应用开发(二)-编程接口_第5张图片
  通过跟踪网站客户端与服务端的交互消息,第一次客户端请求时携带了用户名和密码,如图6所示。
FIDO U2F应用开发(二)-编程接口_第6张图片
  在客户端JS脚本对服务端返回的签名请求进行sign函数调用后, U2F设备产生签名后,客户端将签名数据提交到服务端,如图7所示。
FIDO U2F应用开发(二)-编程接口_第7张图片
  提交的表单数据为:

名称
mode verify
rup
username zhangkai
password zhangkai
sign-data {"challenge": "UGZj34u9u3KVWe3jFrcInm7ZcrPWaX_j9tohZ-34FT0", "version": "U2F_V2", "keyHandle": "3sHb84XcS8HfFaQJ_nhf4aRlWe_wYRKcg5wKelF51hOiP4iNJtGPbsfe5InJmGfoxUSjtqT46HBwG7jkFtc01Q", "appId": "https://demo.yubico.com"}
data(u2f设备接口返回数据) {"keyHandle":"3sHb84XcS8HfFaQJ_nhf4aRlWe_wYRKcg5wKelF51hOiP4iNJtGPbsfe5InJmGfoxUSjtqT46HBwG7jkFtc01Q","clientData":"eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoiVUdaajM0dTl1M0tWV2UzakZyY0lubTdaY3JQV2FYX2o5dG9oWi0zNEZUMCIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby55dWJpY28uY29tIiwiY2lkX3B1YmtleSI6InVudXNlZCJ9","signatureData":"AQAAAAEwRQIhAMCCFSBV7V8kr07XDY2bT3aPI9siDiOFdFBIm8FVTRq1AiBVaYi06GWIHw6uHE_3MFkjrbSY13k5ukPU9_xnNAo_xQ"}

  服务端 验证签名后,返回验证成功信息。

3.3. 异常处理

  实验过程中,如果在交互时不按U2F设备的按钮和不插入设备,register和sign函数都会返回错误码。错误码的定义可参看2.4节。

3.4. u2f-api.js

  u2f-api.js是yubico提供的U2F js api,封装了第2节接口规范中描述的接口。可从地址: https://demo.yubico.com/js/u2f-api.js 处获取。u2f-api.js中的主要定义如下:

var u2f = u2f || {};
u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds)
u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds)

  请注意在这个脚本中将register和sign操作的超时时间定义为30秒:

u2f.EXTENSION_TIMEOUT_SEC = 30;

  在执行3.1节的注册过程时,通过跟踪浏览器消息,可以看到第一次向服务器请求后返回的页面中包含如下JS代码:

setTimeout(function() {
  var request = {"challenge": "OlyOzHxaxx6LUXX5chXsxj4GfspKTskBANNOtQ_UwcA", "version": "U2F_V2", "appId": "https://demo.yubico.com"};
  console.log("Register: ", request);
  var appId = request.appId;
  var registerRequests = [{version: request.version, challenge: request.challenge, attestation: 'direct'}];
  $('#promptModal').modal('show');
  console.log(appId, registerRequests);
  u2f.register(appId, registerRequests, [], function(data) {
    console.log("Register callback", data);
    $('#promptModal').modal('hide');
    $('#bind-data').val(JSON.stringify(data));
    $('#bind-form').submit();
  });
}, 1000);

  这段代码中,使用U2F的上层函数register进行了注册,读者可以与2.2节的函数参数做一下比对,在这段代码中registeredKeys参数使用是空数组“[]”。
  仔细阅读u2f-api.js,会发现脚本使用了EXTENSION_ID为“kmendfapggjehodndflmmgagdbamhnfd”的chrome内置扩展完成与U2F设备的通讯。
  在执行3.2节的鉴权过程时,通过跟踪浏览器消息,可以看到第一次向服务器请求后返回的页面中包含如下JS代码:

setTimeout(function() {
  var request = {"challenge": "UGZj34u9u3KVWe3jFrcInm7ZcrPWaX_j9tohZ-34FT0", "version": "U2F_V2", "keyHandle": "3sHb84XcS8HfFaQJ_nhf4aRlWe_wYRKcg5wKelF51hOiP4iNJtGPbsfe5InJmGfoxUSjtqT46HBwG7jkFtc01Q", "appId": "https://demo.yubico.com"};
  console.log("sign: ", request);
  var appId = request.appId;
  var challenge = request.challenge;
  var registeredKeys = [{version: request.version, keyHandle: request.keyHandle}];
  $('#promptModal').modal('show');
  u2f.sign(appId, challenge, registeredKeys, function(data) {
    $('#promptModal').modal('hide');
    $('#verify-data').val(JSON.stringify(data));
    $('#verify-form').submit();
  });
}, 1000);

  这段代码中,使用U2F的上层函数sign进行了注册。

4. 浏览器兼容测试

  使用购买的U2F设备在PC上对chrome、firefox、IE浏览器进行了测试。其中chrome版本为69,firefox版本为firefox quantum 62, 这两种浏览器目前都支持U2F设备。
  在firefox中使用U2F需要打开一个开关(默认没有打开),如图8所示。
FIDO U2F应用开发(二)-编程接口_第8张图片
  这里有个有趣的问题,测试时使用的网站仍然是https://demo.yubico.com/u2f ,JS脚本仍然使用的是u2f-api.js,前面提到这个脚本是针对chrome内置扩展应用与USB口通信的,在firefox中怎么也能正常使用呢?
  原因就是firefox实现了自己的u2f对象,而且这个u2f对象的所有属性都是只读的,u2f-api.js没有改写这个对象,从控制台如下输出可以看到:

TypeError: setting getter-only property "u2f"

  IE浏览器使用的版本为11,不支持U2F设备。
  在安卓手机上安装了chrome app,使用U2F设备的BLE模式(蓝牙功能)测试了U2F的支持,没有成功,在chrome调用register函数时,U2F设备没有闪烁。

5. 参考文献

  1. https://fidoalliance.org/how-fido-works/
  2. https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/FIDO-U2F-COMPLETE-v1.2-ps-20