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

相關文章
相關標籤/搜索