對於初學 IC 的開發者來說,Internet Identity(II)比較難以理解,所以我想寫幾篇文章盡可能簡潔的介紹清楚 II 的設計和實現。
作者:tinywateer,騰訊專家級工程師
封面:DFINITY
Internet Identity(II)整體上的一些設計
II 的設計目標主要是:
- 屏蔽掉私鑰/公鑰等技術概念,不讓用戶接觸它們,且更安全
- 用戶登錄不同的 DApp 有不同的身份,解決隱私問題
- 用戶登錄 DApp 有過期時間,在一段時間內有效
另外,一個必須的基礎是服務端永遠不保存私鑰。
第一個目標,II 是如何設計的呢?主要是 Anchor、設備、助記詞 這三個方面。
a. 關於 Anchor。
Anchor 就是一個數字,也叫做 User Number。
Anchor 可以理解為用戶的 ID,它在 II 智能合約中是一個索引,可以根據 Anchor 查出來用戶的設備信息。
Anchor 是自增的。
b. 關於設備。
II 抽像出來了「設備」的概念,即 Anchor → 設備→ 私鑰/公鑰,有了設備這一層,用戶不需要去關注私鑰/公鑰,只要關注設備就足夠了。
設備的特點如下:
- 一個 Anchor 可以增加多個設備
- 每個設備都有自己獨立的私鑰/公鑰,私鑰保存在本地的設備,公鑰保存在服務端
- 每個設備都可以增加新的設備、刪除其他的設備
一般要對 Anchor 增加多個設備,在任何一個設備都可以登錄 IC 上的 DApp。
一個設備丟了,不會對使用造成影響,只需要在一台設備上刪除丟失的設備,然後增加新的設備即可。
設備一般是電腦或者手機上的瀏覽器,在使用時需要刷臉或者指紋(基於 WebAuthn 實現),外人很難盜用,更加安全。
c. 關於助記詞。
在極端情況下,所有設備都丟了,怎麼辦?
這時候,還有助記詞,用助記詞增加一個新的設備,然後把其他所有設備都刪除即可。
雖然助記詞底層有一對私鑰和公鑰,但是用戶可以把它看做是「恢復賬戶」的文本,不需要去理解私鑰和公鑰概念。
助記詞的公鑰也是保存在服務端,而私鑰不會保存在本地,當恢復時,使用助記詞生成私鑰,然後和服務端的公鑰做驗證,驗證通過即可恢復。
本質上,對於服務端來說,助記詞也是個「設備」(類型為 seed_phrase,用途為 recovery)。
這裡需要注意兩點:
- 設備和助記詞的「地位」其實是一樣的
- 在一台設備上,可以刪除和增加設備,也可以刪除和新建一個助記詞
- 用助記詞增加一台新設備後,可以做同樣的事情
- 要保證你的 Anchor 的設備和助記詞至少有一個存在,否則你的 Anchor 就無法恢復了,建議不要輕易修改助記詞
第二個目標,II 是如何設計的呢?這裡主要是身份。
身份,即 Identity,是 IC 的一個特有抽象,其實在我看來,身份本質上是用戶(Anchor)登錄到不同 DApp 時的一個「分身」。
身份是基於公鑰計算得來。
公鑰的計算方式是:
|ii_canister_id| · ii_canister_id · seed
身份的計算方式是:
SHA-224(Public-Key) · 0x02
其中 seed 計算方式是:
seed = SHA-256(|salt| · salt · |user_number| · user_number · |frontend_host| · frontend_host)
注:|...| 表示... 的長度,user_number 就是 Anchor,frontend_host 是 DApp URL。
可以看到,身份的決定因素是 Anchor 和 frontend_host(II Canister ID 是不變的)。
這就保證了:
- 當一個 Anchor 登陸不同 DApp 時,身份是不一樣的
- 在一個 Anchor 不同的設備上登陸 DApp,身份是一樣的
但是,有個比較嚴重的問題,由於目前大部分 DApp URL 中包含 Canister ID,當 Canister ID 被刪掉重建時,意味著 DApp 中的身份數據會發生變化!
第三個目標,II 設計了一套 Delegation 機制。
Delegation 機制是 II 中非常重要的設計,它本質是權限委託,它的基本原理是:
- 基於一對私鑰和公鑰(A)對另一對私鑰和公鑰(B)進行簽名,簽名之後, 後者可以代表前者的權限
- 簽名時可以帶上 Scope 和 Expiration,Scope 是作用範圍,也就是 frontend_host,Expiration 是過期時間,過期之後簽名失效
Delegation 可以套娃,不斷簽下去,形成 Delegation Chain。
在 II 中,A 是任何一個設備的私鑰和公鑰,B 是 DApp 前端臨時生成的私鑰和公鑰,B 的過期時間在 DAapp 前端設置。
簽名之後,就表示 DApp 前端臨時的私鑰和公鑰可以代表設備的私鑰和公鑰和 DApp 交互了,當然 DApp 前端也會獲得和 DApp 綁定的用戶的身份(Principal)。
另外:
- 簽名過程,首先要驗證設備的私鑰和公鑰,然後對 臨時的公鑰 做簽名,當然還會帶上 Anchor、frontend_host、Expiration,簽名結果會保存下來
- 如果驗證呢?在 DApp 前端向 IC 發送數據時,用臨時的私鑰加密,並且帶上簽名,IC 會驗證簽名的正確性,如果正確,就用臨時的公鑰解密,獲取數據
另外,II 的這套簽名機制有個叫法:Canister Signature,目前官方已經開放這個功能,不僅 II 可以使用,開發者開發 DApp 也可以使用了。
對 II 未來的展望
II 是 IC 生態 DApp 的單點登錄系統(SSO),如果 IC 最終做成功了,II 則是整個互聯網的單點登錄系統。
而且,在未來,II 可以直接簽發 Https 證書,也許會顛覆 Https 證書市場。
聊聊實現邏輯和核心代碼
本文主要包含三個部分:
- Anchor 和設備管理
- 如何生成身份(Principal)
- Delegation 實現邏輯
II 實現的函數如下:
前面部分是 Anchor 和設備管理,get_principal 是生成身份,prepare_delegation 和 get_delegation 是 Delegation 的實現。
第一部分,Anchor 和設備管理。
設備的數據結構:
DeviceData 主要包括 pubkey(公鑰)、alias(別名)、credential_id(憑據,可選,用於 WebAuthN 認證)、purpose(用途)、key_type(類型)。
設備分為普通設備和用於恢復的設備:
- 普通設備,purpose 為 authentication,key_type 為 unknown
- 用於恢復的設備,purpose 為 recovery,key_type 又分為兩種情況:
- 如果使用助記詞,key_type 為 seed_phrase
- 如果使用安全秘鑰,key_type 為 cross_platform
在第一次使用 II,創建 Anchor 時,會提示輸入第一個設備名稱,點擊 Create 後,會調用後端的 register 函數同時創建 Anchor 以及增加第一個設備。
register 幾個重要的點:
- check_entry_limits 是檢查一些限制,包括別名、公鑰和憑據的長度限制
- store.allocate_user_number() 會生成一個 Anchor,它是自增 ID,加 1 即可
- 然後把 Anchor 和設備信息保存下來
註冊 Anchor 之後,可以繼續增加設備,這時候調用的是 add 函數。
add 函數需要注意的一點是,trap_if_not_authenticated 會驗證請求的合法性,驗證方法是檢查前端的身份(caller,通過設備公鑰生成)是不是在已知的設備身份列表裡。
trap_if_not_authenticated 不僅在設備的函數中做驗證,也會為身份和 Delegation 的函數做驗證。
具體代碼是:
當然,設備支持還支持刪除,這裡不再贅述。
第二部分,如何生成身份(Principal)。
在上篇文章中有說過,身份主要是根據 Anchor(user_number)和 frontend_host(frontend)生成。
check_frontend_length 是檢查 frontend 長度,不能超過 255 字節。
計算身份用到了 calculate_seed 和 der_encode_canister_sig_key 兩個函數,如下:
這裡沒有特別要說的,略過。
第三部分,關於 Delegation 實現邏輯。
在 DApp 前端,選擇 II 登陸的時候,前端會生成一對私鑰和公鑰,這個私鑰和公鑰是臨時的,有過期時間。
然後會跳轉到 identity.ic0.app ,並顯示 DApp 的 URL,點擊 Proceed,會進行 Delegation 的處理,完成後,跳轉回 DApp URL。
在點擊 Proceed 的時候前端會調用 prepare_delegation 和 get_delegation 兩個函數。
Delegation 的邏輯是在 prepare_delegation 中,get_delegation 只是前端重新獲取了一次。
前端傳給 prepare_delegation 的參數主要是:
- user_number:也就是 Anchor
- frontend:DApp URL
- session_key: 前端生成的臨時公鑰
- max_time_to_live: Delegation 過期時間,由前端指定,但是服務端有限制,最大 8 天(MAX_EXPIRATION_PERIOD_NS)
prepare_delegation 對這四者做一個簽名,並保存在服務端。
prepare_delegation 還會返回通過 user_number(Anchor)和 frontend 計算出來的公鑰,它代表著登陸到 DApp 的身份,前端會拿到這個身份,注意,這個身份和前端臨時生成的私鑰和公鑰沒有關係。
前端調用 get_delegation 之後,就可以獲得 Delegation 和它的簽名了。
所以,在 II 跳轉回 DApp URL 之後,前端就可以獲取到身份和 Delegation:
- 身份,代表著用戶登錄到 DApp 的「分身」
- Delegation 代表著經過設備授權的可以與 DApp 交互的權限
下面兩張圖是前端存儲裡保存的身份和 Delegation 例子。
總結
附一張 II 的架構圖:
另外下面這張圖描述了登陸 DApp 的流程:
最後,II 的代碼地址,見這裡。
講講前端部分
說到前端,主要是 Agent-JS,在 IC 上開發 DApp 必須要用到這個庫。
Agent-JS 簡單來說,是這樣:
- Ed25519:生成公私鑰對,實現 SignIdentity
- Auth Client:請求 II 生成 DelegationIdentity,它提供 transformRequest 方法進行簽名
- Http Agent :封裝 DelegationIdentity、提供 call、query、readState、status 通用函數
- Actor:封裝 IDL、Http Agent、Canister ID 等生成直接請求後端函數的 Actor
具體的代碼還是挺多的,這裡不一一列舉。
只講講 Auth Client 和 DelegationIdentity 的部分。
Auth Client 的核心代碼在 這裡,也可以看下面的圖。
可以看到,使用 Ed25519 生成一對臨時的公私鑰對,默認有效時間 8 小時,並綁定了_getEventHandler。
在_getEventHandler 中,如果認證成功,會執行_handleSuccess 函數,_handleSuccess 從返回的數據中提取 Delegation,從而生成 Identity。
注意,服務端返回的 message.userPublicKey.buffer 是用戶的 Identity 對應的公鑰,不是臨時公鑰。
DelegationChain.fromDelegations 這部分代碼在 這裡,這也是核心代碼。
也可以看圖:
然後調用 DelegationIdentity.fromDelegation 傳入公私鑰對(key)、DelegationChain 生成 DelegationIdentity。
具體見:
注意,DelegationIdentity 中的 getPublicKey 返回的是用戶身份的公鑰,不是臨時公鑰。
DelegationIdentity 還提供了 sign 函數,通過臨時的私鑰對請求進行簽名,發送給後端。
具體的簽名動作是在 transformRequest 函數中,每次發送請求都會執行 transformRequest,後端會驗證 Delegation 是否正確。
transformRequest 中的 sender_pubkey 就是用戶身份的公鑰,在 DApp 中可以拿到並使用。
免責聲明:作為區塊鏈信息平台,本站所發布文章僅代表作者及嘉賓個人觀點,與 Web3Caff 立場無關。本文內容僅用於信息分享,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。