iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0

前言:從契約到通行證

在昨天的註冊儀式中,我們的後端伺服器(RP)與使用者的驗證器簽訂了一份基於公鑰密碼學的信任契約。這份契約的核心是我們安全地儲存了使用者的公鑰。

今天,我們將進入第二場核心儀式——驗證 (Authentication/Assertion)。如果說註冊是「建立信任」,那麼驗證就是「行使信任」。這個過程快如閃電,卻又堅如磐石。使用者不再需要輸入任何密碼,只需透過一個簡單的生物辨識動作,就能向伺服器提交一份不可偽造的「通行證」,證明「我就是我」。

驗證流程的鳥瞰圖:更簡潔高效的信任之舞

與註冊相比,驗證的流程更加直接,因為信任的基礎已經建立。我們不再需要交換公鑰,只需要驗證一個簽章。

https://ithelp.ithome.com.tw/upload/images/20250829/20151778E7Hw5gzWAi.png


第一幕:後端的「挑戰」- 建構 PublicKeyCredentialRequestOptions

與註冊類似,驗證流程也由後端發起一個「挑戰」開始。但這次的「挑戰書」內容有所不同,它更像是一份「通關密語」的請求。

一個典型的 PublicKeyCredentialRequestOptions 後端生成範例 (Node.js):

import { randomBytes } from 'crypto';

async function generateAuthenticationOptions(user) {
  // 從資料庫中讀取這位使用者先前註冊的所有憑證
  const userCredentials = await database.getCredentialsByUserId(user.id);

  // `challenge`:同樣地,必須是一個每次登入都不同的隨機、不可預測的字串
  const challenge = randomBytes(32);

  return {
    challenge,
    // `allowCredentials`:指定我們只接受哪些憑證來進行登入
    // 這是為了告訴瀏覽器,這位使用者曾經用哪些驗證器註冊過
    // 瀏覽器會優先嘗試啟動這些指定的驗證器
    allowCredentials: userCredentials.map(cred => ({
      type: 'public-key',
      id: cred.credentialId, // 從資料庫讀出的憑證 ID
      transports: cred.transports, // ['internal', 'usb', 'nfc', 'ble']
    })),
    timeout: 60000,
    userVerification: 'preferred', // 'preferred' 表示我們希望進行使用者驗證(如指紋),但如果驗證器不支援,也可以接受僅使用者在場(按一下)
  };
}

// 同樣地,challenge 和 allowCredentials 中的 id 在傳送給前端前
// 需要轉換成 Base64URL 格式。

第二幕 & 第三幕:前端簽署與後端核對

前端拿到 options 後,呼叫 navigator.credentials.get()。驗證器會使用儲存在安全晶片中的私鑰,對後端發來的 challenge 和其他數據進行簽章,產生一份「斷言 (Assertion)」。

現在,輪到後端執行本次儀式最關鍵的任務:核對這份簽章過的通行證。這一步的核心是:「用我們儲存的公鑰,能否成功解開這份由私鑰加密的簽章?」

後端驗證 AuthenticatorAssertionResponse 的核心步驟:

  1. 查找憑證:從前端傳來的 credentialId,去資料庫中找到對應的憑證紀錄,取得儲存的公鑰 (Public Key)簽章計數器 (Sign Count)。如果找不到,驗證失敗。

  2. 驗證 clientDataJSON

    • 檢查 type:必須是 webauthn.get
    • 檢查 challenge:必須與 Session 中儲存的 challenge 完全一致。
    • 檢查 origin:必須與我們的網站來源完全一致。
  3. 驗證 authData

    • 驗證 rpIdHash:與註冊時一樣,確保是針對我們網站的請求。
    • 檢查 Flags:確保 UP (使用者在場) 為 1。如果我們的 userVerification 策略設為 required,則還需檢查 UV (使用者已驗證) 也為 1。
  4. 驗證簽章計數器 (signCount) - 防止憑證複製攻擊

    • 這是 FIDO 安全模型中一個至關重要的機制!
    • authData 中包含一個 32 位元的 signCount,每次簽章後,驗證器內部的計數器都會加一。
    • 驗證規則:從驗證器收到的 signCount 必須大於 我們資料庫中為該憑證儲存的 signCount
    • 為何重要?:想像一個攻擊者設法複製了你的驗證器(例如,透過某種物理攻擊)。如果沒有 signCount,他就可以用這個複製的驗證器在另一個地方登入。但有了 signCount,即使他複製了憑證,他發出的第一個簽章的 signCount 會和你合法登入後的 signCount 相同或更小。我們的伺服器會因為「計數器沒有增加」而拒絕這次登入,從而有效地阻止了這種攻擊。
  5. 驗證簽章 (signature)

    • 這是密碼學驗證的核心。
    • 我們需要將 authDataclientDataJSON 的 SHA-256 雜湊值拼接在一起,形成一個「待簽章的數據 payload」。
    • 然後,使用從資料庫中取出的公鑰,來驗證前端傳來的 signature 是否是對上述 payload 的有效簽章。
    • 如果驗證成功,就 100% 證明了這個請求是由儲存了對應私鑰的那個驗證器所發出的。
  6. 更新簽章計數器

    • 所有驗證都通過後,務必將資料庫中的 signCount 更新為本次從 authData 中收到的新值。這確保了下一次登入時,計數器驗證依然有效。

第四幕:驗證通過,完成登入

當後端伺服器成功驗證了簽章,並更新了簽章計數器後,就等於確認了使用者持有有效的私鑰,身分驗證宣告成功。

此刻,登入流程的最後一哩路就此打通:

  1. 後端回應:伺服器通常會生成一個代表使用者登入狀態的 Session 或 JWT (JSON Web Token),並將其回傳給前端。這代表著伺服器已經承認了這位使用者的身分。
  2. 前端反饋:前端接收到 Session 或 Token 後,會將其儲存起來,並立即將頁面重新導向至會員專屬的儀表板或首頁。

從使用者輕觸指紋,到看見登入後的頁面,整個過程可能不到一秒。相較於傳統輸入密碼、等待 OTP 簡訊的流程,FIDO/WebAuthn 不僅在安全性上是跨世代的躍進,在使用者體驗上更是無可比擬的提升。


結語:信任的快速通道

我們完成了!通過解構驗證儀式,我們看到 WebAuthn 如何在不傳輸任何秘密的情況下,實現快速且極度安全的登入。challenge 確保了每次登入的唯一性,origin 綁定防範了釣魚,而 signCount 機制則為我們的憑證安全提供了更深一層的保障。

至此,我們已經徹底掌握了 WebAuthn 協議中最核心的註冊與驗證兩大流程的理論與實踐細節。我們已經具備了所有必要的知識儲備。

從明天開始,我們將從抽象的協議理論轉向具體的工程實踐。我們將開始繪製我們專案的系統架構圖,設計資料庫結構,並引入威脅模型來審視我們的設計,為第二週的後端實作鋪平道路。


上一篇
【Day 04 —協議詳解 II】信任的誕生:解構 WebAuthn 註冊儀式
下一篇
Day 06: 【架構設計】從理論到實踐:無密碼系統藍圖與威脅模型
系列文
『零信任』的革命:從 FIDO 協議到自動化部署,30天打造次世代身分認證系統9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言