如何開發支持 FIDO U2F 登錄的網站
前言
U2F (Universal 2nd Factor) 是 Yubico, Yahoo 和 Google 聯合開發的基於物理設備的雙因素認證協議,目前已經完成標準化,從屬於 FIDO (Fast Identity Online) 聯盟名下。
特點
相較於其他雙因素驗證方案,U2F 有以下特點:
- 優勢
- 相較於 OTP (Google Authenticator, Authy, 簡訊驗證碼 等)
- 操作簡單,註冊和登錄均不需要輸入文字/掃描二維碼,只需要按一下設備上的按鈕
- 安全性高,私鑰明文不會離開設備
- 相較於其他基於物理設備的方案 (各種 U 盾)
- 無需驅動/瀏覽器插件
- 相較於 OTP (Google Authenticator, Authy, 簡訊驗證碼 等)
- 劣勢
- 需要購買硬體,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 開發的細節。
代碼實現
註冊
首先導入 https://demo.yubico.com/js/u2f-api.js ,詳細的文檔可以在 這裡 查閱
典型的 U2F 註冊流程,應該發生在用戶已經完成 用戶名/密碼 註冊之後,將 U2F 設備綁定到一個用戶名下。
首先,後端生成隨機字元串,作為挑戰,記錄下來並發送到前端。
然後,前端使用 u2f 對象,完成簽名
// AppId, 網站的 HTTPS 基地址nvar appId = 「https://demo.yubico.com」;nn// 構建參數nvar params = { n // 後端發送的隨機字元串,作為挑戰n 「challenge」: 「XXXXXXXXXXXXXXXX」,n // U2F 協議版本號,固定值n 「version」: 「U2F_V2」n};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
https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.pdf
我的博客鏈接: 如何開發支持 U2F 的網站
推薦閱讀:
※快訊:烏克蘭、俄羅斯、印度等多國遭受Petya勒索病毒襲擊(附樣本)
※迪士尼或以延期《加勒比海盜5》上映的代價 為好萊塢網路安全現身說法
※Struts2爆遠程代碼執行漏洞(S2-045) 附POC