WebAuthn預覽 – 基於公鑰的免密認證登入

SuperChe發表於2019-02-25

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
}
複製程式碼

Reference

相關文章