Passkeys 是目前資訊安全領域最重要的發展,因為它們是唯一能有效對抗日益嚴重的網路釣魚攻擊的原則性解決方案。這與記憶體安全是對抗記憶體損壞攻擊的唯一原則性解決方案如出一轍。
不幸的是,在伺服器端實作它們看起來可能比使用密碼雜湊更為複雜。部分原因無法避免,因為 passkeys 必須與瀏覽器互動才能獲得其防釣魚特性。然而,另一部分則可以透過定義可互通的 passkey 記錄編碼來更有效地抽象化。
WebAuthn 規範 定義了 credential record 作為一個抽象概念,包含多個元件,例如 type、id、publicKey、backupState、transports 以及其他旗標。Google 建議使用以 Credential ID 為主鍵的資料庫表格,並包含 public_key、backed_up 與 transports 等欄位。Adam Langley 出色的 Tour of WebAuthn 同樣建議使用 cred_id 作為主鍵,以及獨立的 public_key_spki 與 backed_up 欄位。
所有指南都建議使用函式庫來處理 WebAuthn 驗證,但這仍然讓應用程式可能採用無法互通的資料庫結構。將整個驗證流程與資料庫互動委託給函式庫或框架,也可能不切實際。
可互通且規格完善的 passkey 記錄,讓應用程式能像處理密碼雜湊一樣以不透明字串來操作,是一種中間抽象層。
c2sp.org/passkey-record 是一項規格提案,它借用 Password Hashing Competition (PHC) Strings 的語法,並重複使用現有的 authenticator data 編碼來處理大部分工作。一筆記錄看起來像這樣:
$webauthn$v=1$transports=hybrid+internal$<base64 authenticator data>
承載內容是 authenticator data,這是 CTAP2 CBOR 編碼,涵蓋大部分 credential record 欄位,已經由 WebAuthn 規範,並包含在 AuthenticatorAttestationResponse 的 JSON 編碼中(即使未使用 attestation,這也是 navigator.credentials.create() 的回傳型別)。Transports 是唯一缺少的欄位,它們以 PHC 參數的形式儲存。
應用程式只需要負責追蹤哪些 passkey 記錄與使用者帳戶相關聯,這對網頁開發者來說是熟悉的工作,因為它與實作密碼驗證並無太大差異(只是每個帳戶可以有多個 passkeys)。這些不透明字串可以傳遞給函式庫來驗證登入斷言(或產生帶有適當 excludedCredentials 的註冊請求)。
透過規格完善且可互通的儲存格式,希望能讓使用者在切換 passkey 函式庫(甚至後端語言)的同時,保留憑證資料庫。
其他您可能想儲存的欄位
除了 passkey 記錄之外,應用程式可能仍希望儲存中繼資料欄位,例如使用者自選的暱稱、建立時間與上次使用時間戳記,以提供美觀的 passkey 管理介面。這些都不需要 WebAuthn 函式庫做任何特殊處理。
唯一的例外是 backed up 狀態旗標。Passkeys 會向伺服器回報它們是否已備份(例如備份至 iCloud Keychain 或 Google 帳戶),而伺服器可以利用這個訊號來建議從帳戶移除密碼。這個旗標可能在每次登入時改變,而 passkey 記錄本身則是不可變的,因此必須另外儲存,並在每次登入時更新。我認為這個邏輯對一般網站來說過於複雜,因為它們仍會繼續支援電子郵件密碼重設。
潛在的 crypto/passkey API
在這些 passkey 記錄之上,我草擬了 一個潛在的 crypto/passkey 無狀態 Go 套件 API。
註冊流程如下:
- 呼叫
RelyingParty.NewRegistration,並傳入已登入(或已識別)的使用者詳細資料,以及任何現有的 passkey 記錄 - 將回傳的 JSON 傳給
parseCreationOptionsFromJSON(),然後傳給navigator.credentials.create() - 將回傳的 JSON 編碼 PublicKeyCredential 傳給
RelyingParty.Register - 將回傳的 passkey 記錄儲存到資料庫
登入流程如下:
- 在產生登入頁面時呼叫
RelyingParty.NewLogin - 將回傳的請求儲存在具有短暫 TTL 的鍵值快取中,鍵值為
RequestID(request),並將回傳的 JSON 傳給parseRequestOptionsFromJSON(),再傳給navigator.credentials.get() - 將回傳的 JSON 編碼 PublicKeyCredential 傳給
Inspect,並使用回傳的 requestID 從鍵值快取取回請求,以及使用回傳的 userID 從資料庫取回 passkey 記錄 - 將 JSON PublicKeyCredential、請求以及 passkey 記錄傳給
RelyingParty.Login
應用程式負責:
- 為每位使用者關聯一個不透明、永久且保護隱私的使用者 ID;
- 儲存與使用者相關聯的 passkey 記錄;以及
- 快取由
RelyingParty.NewLogin產生的請求挑戰。
函式庫提供 JSON 值,可直接傳給 parseCreationOptionsFromJSON() 與 parseRequestOptionsFromJSON(),並接受由 對 PublicKeyCredential 呼叫 JSON.stringify() 所回傳的 JSON 值。
此 API 已針對可發現憑證流程(亦即 passkeys,由 authenticator 儲存並提供使用者 ID 給伺服器)進行最佳化,但 RelyingParty.NewLoginForUser 方法也可用於第二因素流程或重新驗證提示。此 API 同時適用於 modal 與 conditional UI(自動填入)流程。
此外還提供一些輔助函式,用於從 passkey 記錄中擷取資訊(AAGUID、BackedUp),以及從 JSON 編碼的 PublicKeyCredential 中擷取資訊(ResponseBackedUp)。
目前尚未有實作;我希望先取得對 passkey 記錄格式 以及 Go API 的回饋,之後再考慮為 Go 1.28 提出正式提案。
關於重複的 Credential ID
使用這種儲存模型無法確保不同帳戶不會共用具有相同 Credential ID 的 passkeys,而規格建議應該避免這種情況。
進行此檢查的原因是避免攻擊者透過自己的帳戶故意注入衝突的 Credential ID,導致根據 ID 查詢憑證時對應到錯誤的公開金鑰或使用者 ID。
如果根本沒有建立 Credential ID 索引,這種攻擊就不可能發生!索引的存在反而引入了需要索引來緩解的攻擊。
登入嘗試會攜帶使用者 ID,如果您利用它來查詢使用者的 passkey 記錄以驗證登入,就不需在意其他使用者是否擁有相同 Credential ID 的 passkey,就像不需在意兩個使用者共用同一組密碼一樣。
不要讓攻擊者決定您的 PRIMARY KEY,這樣就不會發生 PRIMARY KEY 衝突攻擊。
如需更多 Go API 預覽,請在 Bluesky 追蹤 @filippo.abyssdomain.expert,或在 Mastodon 追蹤 @[email protected]。
附圖
更多來自今年 CENTOPASSI(一項 GPS 追蹤的摩托車競賽,需謹慎規劃,穿越 100 個座標點與 1700 公里次要道路,歷時三天半)的照片。以下是從荒涼且仍覆雪的 Campo Imperatore 下山後,Castel del Monte (AQ) 的景象。
我的工作得以持續,感謝 Geomys 這個由專業 Go 維護者組成的組織,其資金來自 Ava Labs、Teleport、Datadog、Tailscale 以及 Sentry。透過 retainer 合約,他們確保我們開源維護工作的永續性與可靠性,並能直接取得我與其他 Geomys 維護者的專業知識。(詳情請參閱 Geomys 公告。)
以下是其中一些贊助者的話:
Teleport — 過去五年,攻擊與入侵已從傳統惡意軟體與安全漏洞,轉向利用社交工程、憑證竊取或網路釣魚來識別並入侵有效的使用者帳戶與憑證。Teleport Identity 旨在透過存取監控消除弱點存取模式、透過存取請求最小化攻擊面,並透過強制存取審查清除未使用的權限。
Ava Labs — 我們在 Ava Labs,AvalancheGo(與 Avalanche Network 互動最廣泛使用的用戶端)的維護者,相信開源密碼學協定的永續維護與開發對區塊鏈技術的廣泛採用至關重要。我們很榮幸能透過持續贊助 Filippo 及其團隊來支持這項必要且具影響力的工作。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.