目录
1、WebAuthn简介
2、FIDO2:客户端到验证器协议(CTAP)
3、浏览器和平台
4、Web身份验证API 基本流程
5、使用 WebAuthn API
5.1 注册WebAuthn凭据
5.2使用WebAuthn凭据进行身份验证
6、 WebAuthn演示(注册和认证)
6.1 注册演示:
6.2 认证演示:
6.3 浏览器中存储
WebAuthn 是FIDO联盟FIDO2规范集的核心组件,是一种基于Web的API,允许网站更新其登录页面,以便在支持的浏览器和平台上添加基于FIDO的身份验证。FIDO2使用户能够利用常见设备轻松地在移动的和桌面环境中对在线服务进行身份验证。
Web服务和应用程序可以-并且应该-打开此功能,通过生物识别技术,移动的设备和/或FIDO安全密钥为用户提供更轻松的登录体验-并且比单独的密码具有更高的安全性。
FIDO联盟决定与万维网国际标准组织万维网联盟(W3C)合作,为整个Web平台标准化FIDO身份验证。这种标准化将通过支持该标准的Web浏览器和Web应用服务器的整个社区来发展FIDO生态系统。
它允许服务器与现在内置于设备中的强身份验证器集成,如Windows Hello或Apple的Touch ID。为网站创建私钥-公钥对(称为 凭据 )而不是密码。私钥安全地存储在用户的设备上;公钥和随机生成的凭据 ID 将发送到服务器进行存储。然后,服务器可以使用该公钥来证明用户的身份。
公钥不是秘密的,因为如果没有相应的私钥,它实际上是无用的。服务器未收到任何机密这一事实对用户和组织的安全性具有深远的影响。数据库对黑客不再那么有吸引力,因为公钥对他们没有用。
FIDO联盟成员公司于2015年将FIDO规范提交给W3C进行正式标准化。然后,他们在W3C内部工作,最终确定了API,这被称为Web身份验证或WebAuthn。WebAuthn于2019年3月被正式认可为W3C Web标准。如今WebAuthn是FIDO联盟FIDO2规范的一部分,FIDO联盟运行认证程序以确保合规性。
什么是公钥密码?
公钥密码学发明于20世纪70年代,是解决共享秘密问题的一种方法。它是现代互联网安全的支柱;例如,每次我们连接到HTTPS网站时,都会发生公钥交易。
公钥加密使用密钥对的概念;一个私钥由用户安全存储,一个公钥可以与服务器共享。这些“密钥”是彼此具有数学关系的长随机数。
FIDO2的另一个组件,客户端到认证者协议(CTAP)
,是WebAuthn的补充。它使外部身份验证器(如安全密钥或移动的电话)能够与支持WebAuthn的浏览器一起使用,并且还可以用作桌面应用程序和Web服务的身份验证器。
WebAuthn目前在Google Chrome
、Mozilla Firefox、Microsoft Edge和Apple Safari
Web浏览器以及Windows 10和Android
平台中得到支持。
浏览器兼容性如下:
本节规范性地指定了用于创建和使用公钥凭据的API。基本的 这个想法是凭证属于用户并由WebAuthn Authenticator管理,WebAuthn依赖方通过客户端平台与WebAuthn Authenticator交互。 依赖方脚本可以(在用户同意的情况下)请求 浏览器以创建新凭证供信赖方将来使用。参见下图:
注册流程:
1、依赖方服务端开始准备用户信息,以及依赖方信息,这些属于公钥凭证创建相关内容选项。
2、把对应信息在依赖房应用程序通过WebAuthnAPI,生成依赖方id,用户信息,依赖方信息,客户数据哈希,把这些信息发送身份验证器进行验证。
3、身份验证器开始验证用户信息,会生成新的密钥对,和对应认证。
4、身份验证器,把新的公钥和认证id等相关认证信息返回。
5、经浏览器和依赖方js应用返回客户JOSN数据,认证对象信息,这一步是验证器认证响应。
6、服务端进行验证。
脚本还可以请求用户的权限,以便使用现有凭据执行身份验证操作。参见下图1、依赖方服务端发起询问,需要公钥认证请求需要的参数。
1、依赖方服务端发起询问,需要公钥认证请求需要的参数。
2、经依赖方JS应用,浏览器得到依赖方id,客户对应的哈希数据发送给身份验证器。
3、身份验证器开始验证用户信息,开始创建认证信息。
4、验证数据签名返回给浏览器。
5、经依赖方返回客户端的json数据,验证数据,签名等信息。
6、依赖方服务端
在基于密码的用户注册流程中,服务器通常会向用户呈现一个表单,询问用户名和密码。密码将被发送到服务器进行存储。
在WebAuthn中,服务器必须提供将用户绑定到凭证(私钥-公钥对)的数据;该数据包括用户和组织(也称为“依赖方”)的标识符。然后,网站将使用Web身份验证API来提示用户创建新的密钥对。需要注意的是,我们需要从服务器随机生成的字符串作为挑战,以防止重放攻击。
navigator.credentials.create()
服务器将通过调用客户机上的navigator.credentials.create()开始创建新的凭据。
navigator.credentials.create({ publicKeyCredentialCreationOptions })
.then(function (newCredentialInfo) {
// 发送新的凭证信息到服务器进行验证和注册.
}).catch(function (err) {
// 没有可接受的身份验证者或用户拒绝同意。再做对应处理。
});
publicKeyCredentialCreationOptions 对象包含许多必需和可选字段,服务器指定这些字段为用户创建新凭证。
const publicKeyCredentialCreationOptions = {
// 在服务器上生成的加密随机字节的缓冲区,并且需要防止“重放攻击”。
challenge: bufferToBase64URLString(utf8StringToBuffer('sd')),
// 服务器希望从认证器接收证明数据(direct)
attestation: 'direct',
// 这是一个对象数组,描述服务器可以接受哪些公钥类型
// -7表示服务器接受使用SHA-256签名算法的椭圆曲线公钥
pubKeyCredParams: [
{
alg: -7,
type: 'public-key',
},
],
// 依赖方的id,是浏览器当前域名的子集
rp: {
id: 'duosecurity.dev',
name: 'duosecurity',
},
// 用户信息
user: {
id: '5678',
displayName: 'username',
name: 'username',
},
timeout: 1000,
// 希望限制在单个身份验证器上为同一帐户创建多个凭据的依赖方使用。
excludeCredentials: [
{
id: 'C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg',
type: 'public-key',
transports: ['internal'],
},
],
};
返回的create()
对象是一个包含公钥和用于验证注册事件的其他属性的对象。
PublicKeyCredential {
// 新生成的凭证的ID;它将用于在认证用户时标识凭证。ID是一个base64编码的字符串
id: 'ADSUllKQmbqdGtpu4sjseh4cg2TxSvrbcHDTBsv4NSSX9...',
// 属性返回包含内部槽中的标识符
rawId: ArrayBuffer(59),
response: AuthenticatorAttestationResponse {
// 这表示从浏览器传递到身份验证器的数据,以便将新凭据与服务器和浏览器相关联。
// 验证器将其作为UTF-8字节数组提供。
clientDataJSON: ArrayBuffer(121),
// 此对象包含凭证公钥、可选的证明证书和其他用于验证注册事件的元数据。
// 它是以CBOR编码的二进制数据。
attestationObject: ArrayBuffer(306),
},
type: 'public-key'
}
解析clientDataJSON数据:
// decode the clientDataJSON into a utf-8 string
const utf8Decoder = new TextDecoder('utf-8');
const decodedClientData = utf8Decoder.decode(
credential.response.clientDataJSON)
// parse the string as an object
const clientDataObj = JSON.parse(decodedClientData);
console.log(clientDataObj)
{
// 这是传递到create()调用中的challenge是否相同
challenge: "p5aV2uHXr0AOqUk7HQitvi-Ny1....",
// 服务器必须验证这个“origin”字符串与应用程序的源代码匹配。
origin: "https://webauthn.guide",
// 服务器验证该字符串是不是"webauthn.create",否则可能执行不正确的操作。
type: "webauthn.create",
// 客户端和callerOrigin之间的令牌绑定状态
tokenBinding: { // 如果缺失,表示客户端不支持令牌的绑定
// status存在,则该成员必须存在,
// 并且必须是与依赖方通信时使用的令牌绑定ID的base64url编码
id:'ASAFDQWE12312aasDASD......';
// supported 支持令牌绑定,但与依赖方通信时未协商
// present 与依赖方通信时使用了令牌绑定,id必须存在
status: 'present' | 'supported'';
}
}
解析attestationObject:
// note: a CBOR decoder library is needed here.
const decodedAttestationObj = CBOR.decode(
credential.response.attestationObject);
console.log(decodedAttestationObject);
{
// 这里的authenticator数据是一个字节数组,包含有关注册事件的元数据,
// 以及我们将用于未来身份验证的公钥。
authData: Uint8Array(196),
fmt: "fido-u2f", // 表示证明格式
attStmt: {
sig: Uint8Array(70), // 签名
x5c: Array(1), // X.509 格式编码的证明证书。
alg: -7 // -7 使用SHA-256对应的哈希算法的摘要
},
}
解析验证器数据:
const {authData} = decodedAttestationObject;
// get the length of the credential ID
const dataView = new DataView(
new ArrayBuffer(2));
const idLenBytes = authData.slice(53, 55);
idLenBytes.forEach(
(value, index) => dataView.setUint8(
index, value));
const credentialIdLength = dataView.getUint16();
// get the credential ID
const credentialId = authData.slice(
55, 55 + credentialIdLength);
// get the public key object
const publicKeyBytes = authData.slice(
55 + credentialIdLength);
// the publicKeyBytes are encoded again as CBOR
const publicKeyObject = CBOR.decode(
publicKeyBytes.buffer);
console.log(publicKeyObject)
{
1: 2,
3: -7,
-1: 1,
-2: Uint8Array(32) ...
-3: Uint8Array(32) ...
}
authData
是规范中描述的字节数组。解析它将涉及从数组中切片字节并将它们转换为可用的对象。
最后检索到的publicKeyObject
是一个以COSE标准编码的对象,这是一种描述凭证公钥和使用它所需的元数据的简洁方式。
1:
1
字段描述密钥类型。值2
表示关键点类型为椭圆曲线格式。
3:
3
字段描述用于生成身份验证签名的算法。-7
值指示此身份验证器将使用ES256。
-1:
-1
字段描述此键的“曲线类型”。值1
指示该键使用“P-256”曲线。
-2:
-2
字段描述此公钥的x坐标。
-3:
-3
字段描述此公钥的y坐标。
注册完成后,现在可以对用户进行身份验证。在认证期间,创建断言,这是用户拥有私钥的证明。此断言包含使用私钥创建的签名。服务器使用在注册期间检索到的公钥来验证此签名。
navigator.credentials.get()
在身份验证过程中,用户证明他们拥有他们注册的私钥。 它们通过提供一个assertion
来实现,是在客户端上调用navigator.credentials.get()
生成的。这将检索在注册期间生成的包含签名的凭证。
const credential = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions
});
publicKeyCredentialCreationOptions
对象包含许多必需和可选字段,服务器指定这些字段为用户创建新凭证。
const publicKeyCredentialRequestOptions = {
challenge: Uint8Array.from(
randomStringFromServer, c => c.charCodeAt(0)),
// 这个数组告诉浏览器服务器希望用户使用哪些凭据进行身份验证。
allowCredentials: [{
id: Uint8Array.from(
credentialId, c => c.charCodeAt(0)),
type: 'public-key',
transports: ['usb', 'ble', 'nfc'], // 传输方式 USB、蓝牙,NFC
}],
timeout: 60000,
}
const assertion = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions
});
从assertion
调用返回的get()
对象再次是PublicKeyCredential
对象。它与我们在注册时收到的对象略有不同;特别是它包括signature
成员,并且不包括公钥。
console.log(assertion);
PublicKeyCredential {
id: 'ADSUllKQmbqdGtpu4sjseh4cg2TxSvrbcHDTBsv4NSSX9...',
rawId: ArrayBuffer(59),
response: AuthenticatorAssertionResponse {
// 认证器数据类似于注册期间接收的authData,但值得注意的是,这里不包括公钥。
authenticatorData: ArrayBuffer(191),
clientDataJSON: ArrayBuffer(118), // 用作签名的一些数据
signature: ArrayBuffer(70), // 私钥签名
userHandle: ArrayBuffer(10),
},
type: 'public-key'
}
解析和验证身份验证数据:
获得认证后,将其发送到服务器进行验证。在验证数据完全有效之后,使用在注册期间存储在数据库中的公钥来验证签名。
const storedCredential = await getCredentialFromDatabase(
userHandle, credentialId);
const signedData = (
authenticatorDataBytes +
hashedClientDataJSON);
const signatureIsValid = storedCredential.publicKey.verify(
signature, signedData);
if (signatureIsValid) {
return "签名校验成功! ";
} else {
return "签名校验失败. "
}
有关WebAuthn的演示,请访问https//webauthn.io/https://webauthn.io/
用Chrome 浏览器进行测试,第一步:输入用户名(zj),单击“Register” 按钮。用电脑测试,需打开蓝牙。
然后输入电脑密码,则提示验证成功!
1、输入用户名,点击“”按钮,进行认证,登录成功,如下所示:
在Chrome浏览器中,点击“设置”->“自动填充”->“密码管理”可以找到对应的通用密钥,如下图所示: