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