Summary
WebAuthn(也叫Web Authentication API)是Credential Management API的一个扩展,它通过公钥保证了免密认证的安全性。我们通过一个Demo来看它做了什么。
准备工作
- Firefox Nightly
- 下载源文件 https://github.com/fido-alliance/webauthn-demo/tree/completed-demo
- Node.js + NPM
- 推荐用最新的Windows:Windows Hello集成了认证模块
核心概念
WebAuthn用公钥证书代替了密码,完成用户的注册和身份认证(登录)。它更像是现有身份认证的增强或补充。为了保证通信数据安全,一般基于HTTPS(TLS)通信。在这个过程中,有4个模块。
- 服务器:它可以被认为一个依赖方(Relying Party),它会存储用户的公钥并负责用户的注册、认证。在Demo的代码中,用Express实现。
- JS脚本:串联用户注册、认证。在Demo中,位于static目录。
- 浏览器:需要包含WebAuthn的Credential Management API
- 认证模块(Authenticator):它能够创建、存储、检索身份凭证。它一般是个硬件设备(智能卡、USB),也可能已经集成到了你的操作系统(比如Windows Hello)
注册
注册过程分为7个阶段
0. 发起注册请求
浏览器发起注册请求,包含用户基本信息。
1. 服务器返回Challenge,用户信息,依赖方信息(Relying Party Info)
- Challenge是一个很大的随机数,由服务器生成,这是保证通信安全的关键。
2. 浏览器调用认证模块生成证书
这是一个异步任务,JS脚本调用浏览器的navigator.credentials.create创建证书。
getMakeCredentialsChallenge({username, name})
.then((response) => {
let publicKey = preformatMakeCredReq(response);
return navigator.credentials.create({ publicKey })
})
.then((response) => {
console.log(response);
let makeCredResponse = publicKeyCredentialToJSON(response);
return sendWebAuthnResponse(makeCredResponse)
})
.then((response) => {
if(response.status === 'ok') {
loadMainContainer()
} else {
alert(`Server responed with error. The message is: ${response.message}`);
}
})
.catch((error) => alert(error))
复制代码
浏览器到认证模块之间的数据用JSON格式传递,并包含以下内容:
- Challenge
- 用户信息 + 依赖方信息:用来管理证书
- ClientData:浏览器会自动创建、填充参数。其中,origin是关键属性,它会被服务器用来验证请求的源头
3. 认证模块创建一对公钥/私钥和attestation数据
4. 认证模块把公钥/Credential rawID/attestation发送给浏览器
浏览器会以{ AttestationObject, ClientDataJSON }的格式返回给JS脚本。
5. 浏览器把Credential发送给服务器
6. 服务器完成注册
检查Challenge、Origin,并存储公钥和用户信息。
身份认证(登录)
同样分为7步,多数内容与注册相似。
0. 发起登录请求
浏览器发起登录请求,包含用户基本信息。
1. 服务器返回Challenge,用户信息,依赖方信息(Relying Party Info)
2. 浏览器调用认证模块检索证书
JS脚本调用浏览器的navigator.credentials.get检索证书。
getGetAssertionChallenge({username})
.then((response) => {
console.log(response)
let publicKey = preformatGetAssertReq(response);
return navigator.credentials.get({ publicKey })
})
.then((response) => {
console.log(response)
let getAssertionResponse = publicKeyCredentialToJSON(response);
return sendWebAuthnResponse(getAssertionResponse)
})
.then((response) => {
if(response.status === 'ok') {
loadMainContainer()
} else {
alert(`Server responed with error. The message is: ${response.message}`);
}
})
.catch((error) => alert(error))
复制代码
3. 认证模块创建一对公钥/私钥和attestation数据
4. 认证模块把公钥/Credential rawID/attestation发送给浏览器
5. 浏览器把Credential发送给服务器
6. 服务器完成注册
检查Challenge、Origin,并验证公钥和用户信息。
let verifyAuthenticatorAssertionResponse = (webAuthnResponse, authenticators) => {
let authr = findAuthr(webAuthnResponse.id, authenticators);
let authenticatorData = base64url.toBuffer(webAuthnResponse.response.authenticatorData);
let response = {'verified': false};
if(authr.fmt === 'fido-u2f') {
let authrDataStruct = parseGetAssertAuthData(authenticatorData);
if(!(authrDataStruct.flags & U2F_USER_PRESENTED))
throw new Error('User was NOT presented durring authentication!');
let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
let signatureBase = Buffer.concat([authrDataStruct.rpIdHash, authrDataStruct.flagsBuf, authrDataStruct.counterBuf, clientDataHash]);
let publicKey = ASN1toPEM(base64url.toBuffer(authr.publicKey));
let signature = base64url.toBuffer(webAuthnResponse.response.signature);
response.verified = verifySignature(signature, signatureBase, publicKey)
if(response.verified) {
if(response.counter <= authr.counter)
throw new Error('Authr counter did not increase!');
authr.counter = authrDataStruct.counter
}
}
return response
}
复制代码