解析attestationObject:
1. // note: a CBOR decoder library is needed here. 2. const decodedAttestationObj = CBOR.decode( 3. credential.response.attestationObject); 4. 5. console.log(decodedAttestationObject); 6. { 7. // 这里的authenticator数据是一个字节数组,包含有关注册事件的元数据, 8. // 以及我们将用于未来身份验证的公钥。 9. authData: Uint8Array(196), 10. fmt: "fido-u2f", // 表示证明格式 11. attStmt: { 12. sig: Uint8Array(70), // 签名 13. x5c: Array(1), // X.509 格式编码的证明证书。 14. alg: -7 // -7 使用SHA-256对应的哈希算法的摘要 15. }, 16. }
解析验证器数据:
1. const {authData} = decodedAttestationObject; 2. 3. // get the length of the credential ID 4. const dataView = new DataView( 5. new ArrayBuffer(2)); 6. const idLenBytes = authData.slice(53, 55); 7. idLenBytes.forEach( 8. (value, index) => dataView.setUint8( 9. index, value)); 10. const credentialIdLength = dataView.getUint16(); 11. 12. // get the credential ID 13. const credentialId = authData.slice( 14. 55, 55 + credentialIdLength); 15. 16. // get the public key object 17. const publicKeyBytes = authData.slice( 18. 55 + credentialIdLength); 19. 20. // the publicKeyBytes are encoded again as CBOR 21. const publicKeyObject = CBOR.decode( 22. publicKeyBytes.buffer); 23. console.log(publicKeyObject) 24. 25. { 26. 1: 2, 27. 3: -7, 28. -1: 1, 29. -2: Uint8Array(32) ... 30. -3: Uint8Array(32) ... 31. }
authData是规范中描述的字节数组。解析它将涉及从数组中切片字节并将它们转换为可用的对象。
最后检索到的publicKeyObject是一个以COSE标准编码的对象,这是一种描述凭证公钥和使用它所需的元数据的简洁方式。
1: 1字段描述密钥类型。值2表示关键点类型为椭圆曲线格式。
3: 3字段描述用于生成身份验证签名的算法。-7值指示此身份验证器将使用ES256。
-1: -1字段描述此键的“曲线类型”。值1指示该键使用“P-256”曲线。
-2: -2字段描述此公钥的x坐标。
-3: -3字段描述此公钥的y坐标。
5.2使用WebAuthn凭据进行身份验证
注册完成后,现在可以对用户进行身份验证。在认证期间,创建断言,这是用户拥有私钥的证明。此断言包含使用私钥创建的签名。服务器使用在注册期间检索到的公钥来验证此签名。
navigator.credentials.get()
在身份验证过程中,用户证明他们拥有他们注册的私钥。 它们通过提供一个assertion来实现,是在客户端上调用navigator.credentials.get()生成的。这将检索在注册期间生成的包含签名的凭证。
1. const credential = await navigator.credentials.get({ 2. publicKey: publicKeyCredentialRequestOptions 3. });
publicKeyCredentialCreationOptions对象包含许多必需和可选字段,服务器指定这些字段为用户创建新凭证。
1. const publicKeyCredentialRequestOptions = { 2. challenge: Uint8Array.from( 3. randomStringFromServer, c => c.charCodeAt(0)), 4. // 这个数组告诉浏览器服务器希望用户使用哪些凭据进行身份验证。 5. allowCredentials: [{ 6. id: Uint8Array.from( 7. credentialId, c => c.charCodeAt(0)), 8. type: 'public-key', 9. transports: ['usb', 'ble', 'nfc'], // 传输方式 USB、蓝牙,NFC 10. }], 11. timeout: 60000, 12. } 13. 14. const assertion = await navigator.credentials.get({ 15. publicKey: publicKeyCredentialRequestOptions 16. });
从assertion调用返回的get()对象再次是PublicKeyCredential对象。它与我们在注册时收到的对象略有不同;特别是它包括signature成员,并且不包括公钥。
1. console.log(assertion); 2. 3. PublicKeyCredential { 4. id: 'ADSUllKQmbqdGtpu4sjseh4cg2TxSvrbcHDTBsv4NSSX9...', 5. rawId: ArrayBuffer(59), 6. response: AuthenticatorAssertionResponse { 7. // 认证器数据类似于注册期间接收的authData,但值得注意的是,这里不包括公钥。 8. authenticatorData: ArrayBuffer(191), 9. clientDataJSON: ArrayBuffer(118), // 用作签名的一些数据 10. signature: ArrayBuffer(70), // 私钥签名 11. userHandle: ArrayBuffer(10), 12. }, 13. type: 'public-key' 14. }
解析和验证身份验证数据:
获得认证后,将其发送到服务器进行验证。在验证数据完全有效之后,使用在注册期间存储在数据库中的公钥来验证签名。
1. const storedCredential = await getCredentialFromDatabase( 2. userHandle, credentialId); 3. 4. const signedData = ( 5. authenticatorDataBytes + 6. hashedClientDataJSON); 7. 8. const signatureIsValid = storedCredential.publicKey.verify( 9. signature, signedData); 10. 11. if (signatureIsValid) { 12. return "签名校验成功! 🎉"; 13. } else { 14. return "签名校验失败. 😭" 15. }
6、 WebAuthn演示(注册和认证)
有关WebAuthn的演示,请访问
6.1 注册演示:
用Chrome 浏览器进行测试,第一步:输入用户名(zj),单击“Register” 按钮。用电脑测试,需打开蓝牙。
然后输入电脑密码,则提示验证成功!
6.2 认证演示:
1、输入用户名,点击“”按钮,进行认证,登录成功,如下所示:
6.3 浏览器中存储
在Chrome浏览器中,点击“设置”->“自动填充”->“密码管理”可以找到对应的通用密钥,如下图所示: