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所示,其中一个飞天品牌的带蓝牙功能。
首先测试注册过程,如图2所示。
通过跟踪网站客户端与服务端的交互消息,我们发现开始注册过程后,客户端向服务端的提交了两次请求,对应《FIDO U2F设备应用与开发(一)-原理与协议》第3节描述的3个阶段中的第1和第3阶段。
客户端第一次向服务端提交请求后,参数包含用户名和口令,如图3所示。
在客户端JS脚本对服务端返回的注册请求进行register函数调用后,将register的注册数据提交给服务端,提交的表单数据如图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所示。
通过跟踪网站客户端与服务端的交互消息,第一次客户端请求时携带了用户名和密码,如图6所示。
在客户端JS脚本对服务端返回的签名请求进行sign函数调用后, 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所示。
这里有个有趣的问题,测试时使用的网站仍然是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. 参考文献
- https://fidoalliance.org/how-fido-works/
- https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/FIDO-U2F-COMPLETE-v1.2-ps-20