WebAuthn预览 - 基于公钥的免密认证登陆

Summary

WebAuthn(也叫Web Authentication API)是Credential Management API的一个扩展,它经过公钥保证了免密认证的安全性。咱们经过一个Demo来看它作了什么。git

准备工做

  • Firefox Nightly
  • 下载源文件 https://github.com/fido-alliance/webauthn-demo/tree/completed-demo
  • Node.js + NPM
  • 推荐用最新的Windows:Windows Hello集成了认证模块

核心概念

WebAuthn用公钥证书代替了密码,完成用户的注册和身份认证(登陆)。它更像是现有身份认证的加强或补充。为了保证通讯数据安全,通常基于HTTPS(TLS)通讯。在这个过程当中,有4个模块。github

  • 服务器:它能够被认为一个依赖方(Relying Party),它会存储用户的公钥并负责用户的注册、认证。在Demo的代码中,用Express实现。
  • JS脚本:串联用户注册、认证。在Demo中,位于static目录。
  • 浏览器:须要包含WebAuthn的Credential Management API
  • 认证模块(Authenticator):它可以建立、存储、检索身份凭证。它通常是个硬件设备(智能卡、USB),也可能已经集成到了你的操做系统(好比Windows Hello)

注册

注册过程分为7个阶段web

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,并存储公钥和用户信息。ui

身份认证(登陆)

一样分为7步,多数内容与注册类似。url

0. 发起登陆请求

浏览器发起登陆请求,包含用户基本信息。spa

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
}
复制代码

Reference

相关文章
相关标签/搜索