如何開發支持 FIDO U2F 登錄的網站

前言

U2F (Universal 2nd Factor) 是 Yubico, Yahoo 和 Google 聯合開發的基於物理設備的雙因素認證協議,目前已經完成標準化,從屬於 FIDO (Fast Identity Online) 聯盟名下。

特點

相較於其他雙因素驗證方案,U2F 有以下特點:

  • 優勢
    • 相較於 OTP (Google Authenticator, Authy, 簡訊驗證碼 等)
      • 操作簡單,註冊和登錄均不需要輸入文字/掃描二維碼,只需要按一下設備上的按鈕
      • 安全性高,私鑰明文不會離開設備
    • 相較於其他基於物理設備的方案 (各種 U 盾)
      • 無需驅動/瀏覽器插件
  • 劣勢
    • 需要購買硬體,Yubikey U2F 售價 ¥150 左右,U2FZero 物料費用 $5 左右
    • 不兼容移動設備,只支持桌面瀏覽器 (Chrome > 49,Opera > 42)

適用場景

U2F 是嚴格基於物理設備的雙因素認證方案,相對於 OTP,設備的交接和管理非常便利,適合大型企業內部系統鑒權(ERP,CRM 等)

此外,U2F 可以作為普通雙因素驗證方案的補充,為網站用戶提供更好的體驗(Google,Github, Dropbox,Docker Hub, Salesforce 等網站均已支持)

工作流程

U2F 安全性的核心在於不對稱加密演算法,私鑰保存在設備上,簽名運算也在設備上執行,沒有任何手段可以獲取私鑰的明文。因此除非物理上獲取到了 U2F 設備,否則是無法是無法破解 U2F 認證流程的。

U2F 的工作流程和常見的不對稱加密演算法認證體系類似,都是圍繞著 「挑戰-響應」 展開的。

在硬體層面上,U2F 使用應用廣泛的 HID 協議(鍵盤,滑鼠等),支持 USB、藍牙 和 NFC,確保在多種操作系統上,無需驅動,即插即用。

在瀏覽器層面上,Chrome 將 HID 協議封裝成底層 JavaScript API。

FIDO 官方提供了 u2f-api.js 將瀏覽器底層 API 封裝成高層 API,通過一兩個調用,即可完成註冊和認證操作。

第三方封裝的高層 API會有不同的實現,了解 Yubico u2f-api.js 有助於了解 U2F 開發的細節。

代碼實現

註冊

首先導入 demo.yubico.com/js/u2f- ,詳細的文檔可以在 這裡 查閱

典型的 U2F 註冊流程,應該發生在用戶已經完成 用戶名/密碼 註冊之後,將 U2F 設備綁定到一個用戶名下。

首先,後端生成隨機字元串,作為挑戰,記錄下來並發送到前端。

然後,前端使用 u2f 對象,完成簽名

// AppId, 網站的 HTTPS 基地址nvar appId = https://demo.yubico.com」;nn// 構建參數nvar params = { n // 後端發送的隨機字元串,作為挑戰n challenge: XXXXXXXXXXXXXXXX,n // U2F 協議版本號,固定值n version: U2F_V2n};nn// 調用 u2f.registernu2f.register(params.appId, n [params],n function(data) {n});n

返回值 data 是一個字典

返回值 data 是一個字典。發生錯誤的情況下,data 只有 errorCode 欄位,定義如下(來自文檔)

interface ErrorCode { n const short OK = 0;n const short OTHER_ERROR = 1;n const short BAD_REQUEST = 2;n const short CONFIGURATION_UNSUPPORTED = 3;n const short DEVICE_INELIGIBLE = 4;n const short TIMEOUT = 5;n};n

正常情況下,data 包含如下內容

{n // 與伺服器發起的 Challenge 內容相同n challenge: "xxxxxxxxxxxxxxxxxxxxxxxxx",n // 見下文n clientData: "xxxxxxxxxxxxx",n // 見下文n registrationData: "xxxxxxxxxxxxxx",n version: "U2F_V2"n}n

clientData 為 Base64 編碼後的 JSON 字元串

{n // 固定值, typ 沒拼錯n typ: "navigator.id.finishEnrollment", n challenge: "xxxxxxxxxxxxxxxxxxxxxx",n origin: "https://demo.yubico.com"n}n

registrationData 為 Base64 編碼後的二進位數據,內容按順序如下

  • Head
    • 1 位元組,固定值為 0x05
  • PubKey
    • 65 位元組,應用證書公鑰,無壓縮 P-256 NIST 橢圓曲線坐標數據
  • PrivKeyHandle_Len
    • 1 位元組,無符號整數,KeyHandle 的長度
  • PrivKeyHandle
    • 長度由 KeyHandle_Len 決定,私鑰句柄,見下文
  • Main_PubKey
    • 長度不定,設備主證書公鑰,X.509 DER 二進位編碼的證書,同一批設備可能共用一個主證書
  • Sig
    • 長度不定,簽名,使用 SHA256-ECDSA (P-256 NIST) 演算法

Sig 使用 Main_PubKey 簽名,原始內容如下(拼接二進位數據)

  • 1 位元組固定值,0x00
  • SHA256(AppId)
  • SHA256(ClientData)
  • PrivKeyHandle
  • PubKey

最終,後端在驗證完簽名後,將 PrivKeyHandle, PubKey 和 Main_PubKey 記錄下來,並與用戶關聯,用於日後的驗證。

驗證

後端生成隨機數,作為挑戰,並記錄下來,然後和 PrivKeyHandle 一起,發送到前端。

前端調用 u2f.sign 方法,執行驗證操作。

// AppId, HTTPS 基地址nvar appId = "https://demo.yubico.com"; n// Challenge, 後端生成的隨機數nvar challenge = "xxxxxxxxxxxxxxxxx"; n// 參數nvar params = { n // U2F 協議版本號,固定值n "version": "U2F_V2",n // PrivKeyHandle, 先前記錄的私鑰句柄n "keyHandle": "XXXXXXX"n};n// 調用 u2f.signnu2f.sign(appId, challenge, [params], function(data) { n});n

和註冊類似,在失敗的時候,data 包含一個 errorCode

成功的時候,data 包含如下內容

{n // PrivKeyHandle, Base64 編碼n "keyHandle":"xxxxxxxxxxxx",n // 見下文n "clientData":"xxxxxxxxxxxx",n // 見下文n "signatureData":"xxxxxxxxxx"n}n

其中,clientData 欄位為 Base64 編碼後的 JSON

{n // 固定值,typ 沒拼錯n typ: "navigator.id.getAssertion", n // 先前伺服器發起的 Challengen challenge: "xxxxxxxxxxxxxxxxxxxxxx", n origin: "https://demo.yubico.com"n}n

signatureData 欄位為 Base64 編碼後的二進位數據,內容按順序如下:

  • Flag
    • 1 位元組,第 0 比特位 表示認證是否成功
  • Counter
    • 4 位元組, Big-Endian 無符號整數 (UInt32),簽名計數器
  • Sig
    • 長度不定,SHA256-ECDSA (P-256 NIST) 簽名

Sig 使用 PubKey 簽名,原始數據如下(拼接二進位數據):

  • SHA256(AppId)
  • Flag
  • Counter
  • SHA256(ClientData)

最終,伺服器在驗證完簽名後,認可用戶的身份,執行下一步操作。

在整個流程中,為了防止設備在不同的網站間追蹤,U2F 設備會為不同的網站生成不同的密鑰對。為了保證單台 U2F 設備支持無限多的網站登錄,密鑰對中的私鑰保存在 U2F 設備上是不可能的,因為晶元容量有限,且非常珍貴,因此才有了 PrivKeyHandle 這一參數。PrivKeyHandle 是用來讓 U2F 設備「回想」起私鑰的,而具體的內部實現方式,由各廠商自己決定。

在最簡單的實現方式中,U2F 設備上保存一個主密碼,該密碼不可從外部讀取。註冊時生成的私鑰經主密碼對稱加密後,作為 PrivKeyHandle 發送給伺服器。在驗證的時候,伺服器把 PrivKeyHandle 發送回 U2F 設備,U2F 設備用主密碼解密私鑰,並完成簽名。

而 Yubikey 和 U2FZero (以及其他廠商) 使用了一個更加複雜的方案,私鑰由 隨機數、AppId、設備主密碼經過複雜的演算法派生出來,PrivKeyHandle 中只包含一個 MAC (Message Authentication Code) 和隨機數,保證私鑰不會離開設備。

參考資料:

Yubico』s Take on U2F Key Wrapping | Yubico

Yubico/java-u2flib-server: Java server-side library for U2F

fidoalliance.org/specs/

我的博客鏈接: 如何開發支持 U2F 的網站

推薦閱讀:

快訊:烏克蘭、俄羅斯、印度等多國遭受Petya勒索病毒襲擊(附樣本)
迪士尼或以延期《加勒比海盜5》上映的代價 為好萊塢網路安全現身說法
Struts2爆遠程代碼執行漏洞(S2-045) 附POC

TAG:网络安全 | RSA加密 | 互联网 |