对于初学 IC 的开发者来说,Internet Identity(II)比较难以理解,所以我想写几篇文章尽可能简洁的介绍清楚 II 的设计和实现。

作者:tinywateer,腾讯专家级工程师

封面:DFINITY

Internet Identity(II)整体上的一些设计

II 的设计目标主要是:

  1. 屏蔽掉私钥/公钥等技术概念,不让用户接触它们,且更安全
  2. 用户登录不同的 DApp 有不同的身份,解决隐私问题
  3. 用户登录 DApp 有过期时间,在一段时间内有效

另外,一个必须的基础是服务端永远不保存私钥

第一个目标,II 是如何设计的呢?主要是 Anchor、设备、助记词  这三个方面。

a. 关于 Anchor。

Anchor 就是一个数字,也叫做 User Number。

Anchor 可以理解为 用户 的 ID,它在 II 智能合约中是一个索引,可以根据 Anchor 查出来用户的设备信息。

Anchor 是自增的。

b. 关于设备。

II 抽象出来了「设备」的概念,即 Anchor → 设备 → 私钥/公钥,有了设备这一层,用户不需要去关注私钥/公钥,只要关注设备就足够了。

设备的特点如下:

  1. 一个 Anchor 可以增加多个设备
  2. 每个设备都有自己独立的 私钥/公钥,私钥保存在本地的设备,公钥保存在服务端
  3. 每个设备都可以增加新的设备、删除其他的设备

一般要对 Anchor 增加多个设备,在任何一个设备都可以登录 IC 上的 DApp。

一个设备丢了,不会对使用造成影响,只需要在一台设备上删除丢失的设备,然后增加新的设备即可。

设备一般是电脑或者手机上的浏览器,在使用时需要刷脸或者指纹(基于 WebAuthn 实现),外人很难盗用,更加安全。

c. 关于助记词。

在极端情况下,所有设备都丢了,怎么办?

这时候,还有助记词,用助记词增加一个新的设备,然后把其他所有设备都删除即可。

虽然助记词底层有一对私钥和公钥,但是用户可以把它看做是「恢复账户」的文本,不需要去理解私钥和公钥概念。

助记词的公钥也是保存在服务端,而私钥不会保存在本地,当恢复时,使用助记词生成私钥,然后和服务端的公钥做验证,验证通过即可恢复。

本质上,对于服务端来说,助记词也是个「设备」(类型为 seed_phrase,用途为 recovery)。

这里需要注意两点:

  1. 设备和助记词 的「地位」其实是一样的
    1. 在一台设备上,可以删除和增加设备,也可以删除和新建一个助记词
    2. 用助记词增加一台新设备后,可以做同样的事情
  2. 要保证你的 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 是不变的)。

这就保证了:

  1. 当一个 Anchor 登陆不同 DApp 时,身份是不一样的
  2. 在一个 Anchor 不同的设备上登陆 DApp,身份是一样的

但是,有个比较严重的问题,由于目前大部分 DApp URL 中包含 Canister ID,当 Canister ID 被删掉重建时,意味着 DApp 中的身份数据会发生变化!

第三个目标,II 设计了一套 Delegation 机制。

Delegation 机制是 II 中非常重要的设计,它本质是权限委托,它的基本原理是:

  1. 基于一对私钥和公钥(A)对另一对私钥和公钥(B)进行签名,签名之后, 后者可以代表前者的权限
  2. 签名时可以带上 Scope 和 Expiration,Scope 是作用范围,也就是 frontend_host,Expiration 是过期时间,过期之后签名失效

Delegation 可以套娃,不断签下去,形成 Delegation Chain。

Delegation  机制

在 II 中,A 是任何一个设备的私钥和公钥,B 是 DApp 前端临时生成的私钥和公钥,B 的过期时间在 DAapp 前端设置。

签名之后,就表示 DApp 前端临时的私钥和公钥可以代表设备的私钥和公钥和 DApp 交互了,当然 DApp 前端也会获得和 DApp 绑定的用户的身份(Principal)。

另外:

  1. 签名过程,首先要验证设备的私钥和公钥,然后对  临时的公钥  做签名,当然还会带上 Anchor、frontend_host、Expiration,签名结果会保存下来
  2. 如果验证呢?在 DApp 前端向 IC 发送数据时,用临时的私钥加密,并且带上签名,IC 会验证签名的正确性,如果正确,就用临时的公钥解密,获取数据

另外,II 的这套签名机制有个叫法:Canister Signature,目前官方已经开放这个功能,不仅 II 可以使用,开发者开发 DApp 也可以使用了。

对 II 未来的展望

II 是 IC 生态 DApp 的单点登录系统(SSO),如果 IC 最终做成功了,II 则是整个互联网的单点登录系统。

而且,在未来,II 可以直接签发 Https 证书,也许会颠覆 Https 证书市场。

聊聊实现逻辑和核心代码

本文主要包含三个部分:

  1. Anchor 和 设备管理
  2. 如何生成身份(Principal)
  3. Delegation 实现逻辑

II 实现的函数如下:

II 的函数列表

前面部分是 Anchor 和 设备管理,get_principal 是生成身份,prepare_delegation 和 get_delegation 是 Delegation 的实现。

第一部分,Anchor 和 设备管理。

设备的数据结构:

设备的数据结构

DeviceData 主要包括 pubkey(公钥)、alias(别名)、credential_id(凭据,可选,用于 WebAuthN 认证)、purpose(用途)、key_type(类型)。

设备分为普通设备和用于恢复的设备:

  1. 普通设备,purpose 为 authentication,key_type 为 unknown
  2. 用于恢复的设备,purpose 为 recovery,key_type 又分为两种情况:
    1. 如果使用助记词,key_type 为 seed_phrase
    2. 如果使用安全秘钥,key_type 为 cross_platform

在第一次使用 II,创建 Anchor 时,会提示输入第一个设备名称,点击 Create 后,会调用后端的 register 函数同时创建 Anchor 以及增加第一个设备。

创建 Anchor 和第一个设备
register 函数

register 几个重要的点:

  1. check_entry_limits 是检查一些限制,包括别名、公钥和凭据的长度限制
  2. store.allocate_user_number() 会生成一个 Anchor,它是自增 ID,加 1 即可
  3. 然后把 Anchor 和设备信息保存下来

注册 Anchor 之后,可以继续增加设备,这时候调用的是 add 函数。

add 函数

add 函数需要注意的一点是,trap_if_not_authenticated 会验证请求的合法性,验证方法是检查前端的身份(caller,通过设备公钥生成)是不是在已知的设备身份列表里。

trap_if_not_authenticated 不仅在设备的函数中做验证,也会为身份和 Delegation 的函数做验证。

具体代码是:

trap_if_not_authenticated

当然,设备支持还支持删除,这里不再赘述。

第二部分,如何生成身份(Principal)。

在上篇文章中有说过,身份主要是根据 Anchor(user_number)和 frontend_host(frontend)生成。

get_principal 函数

check_frontend_length 是检查 frontend 长度,不能超过 255 字节。

计算身份用到了 calculate_seed 和 der_encode_canister_sig_key 两个函数,如下:

calculate_seed 和 der_encode_canister_sig_key 函数

这里没有特别要说的,略过。

第三部分,关于 Delegation 实现逻辑。

在 DApp 前端,选择 II 登陆的时候,前端会生成一对私钥和公钥,这个私钥和公钥是临时的,有过期时间。

然后会跳转到 identity.ic0.app ,并显示 DApp 的 URL,点击 Proceed,会进行 Delegation 的处理,完成后,跳转回 DApp URL。

II 跳转确认

在点击 Proceed 的时候前端会调用 prepare_delegation 和 get_delegation 两个函数。

Delegation 数据结构
prepare_delegation 函数
get_delegation 函数

Delegation 的逻辑是在 prepare_delegation 中,get_delegation 只是前端重新获取了一次。

前端传给 prepare_delegation 的参数主要是:

  1. user_number:也就是 Anchor
  2. frontend:DApp URL
  3. session_key: 前端生成的临时公钥
  4. 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:

  1. 身份,代表着用户登录到 DApp 的「分身」
  2. Delegation 代表着经过设备授权的可以与 DApp 交互的权限

下面两张图是前端存储里保存的身份和 Delegation 例子。

前端的身份
前端的 Delegation

总结

附一张 II 的架构图:

II 架构图

另外下面这张图描述了登陆 DApp 的流程:

II 登陆 DApp 流程图

最后,II 的代码地址,见这里

讲讲前端部分

说到前端,主要是 Agent-JS,在 IC 上开发 DApp 必须要用到这个库。

Agent-JS 简单来说,是这样:

  1. Ed25519:生成公私钥对,实现 SignIdentity
  2. Auth Client:请求 II 生成 DelegationIdentity,它提供 transformRequest 方法进行签名
  3. Http Agent :封装 DelegationIdentity、提供 call、query、readState、status 通用函数
  4. Actor:封装 IDL、Http Agent、Canister ID 等生成直接请求后端函数的 Actor

具体的代码还是挺多的,这里不一一列举。

只讲讲 Auth Client 和 DelegationIdentity 的部分。

Auth Client 的核心代码在  这里,也可以看下面的图。

可以看到,使用 Ed25519 生成一对临时的公私钥对,默认有效时间 8 小时,并绑定了 _getEventHandler。

Auth Client 的 login 代码

在 _getEventHandler 中,如果认证成功,会执行 _handleSuccess 函数,_handleSuccess 从返回的数据中提取 Delegation,从而生成 Identity。

_handleSuccess 函数

注意,服务端返回的 message.userPublicKey.buffer 是用户的 Identity 对应的公钥,不是临时公钥。

DelegationChain.fromDelegations 这部分代码在  这里,这也是核心代码。

也可以看图:

DelegationChain 函数

然后调用 DelegationIdentity.fromDelegation 传入 公私钥对(key)、DelegationChain 生成 DelegationIdentity。

具体见:

DelegationIdentity 函数

注意,DelegationIdentity 中的 getPublicKey 返回的是用户身份的公钥,不是临时公钥。

DelegationIdentity 还提供了 sign 函数,通过临时的私钥对请求进行签名,发送给后端。

具体的签名动作是在 transformRequest 函数中,每次发送请求都会执行 transformRequest,后端会验证 Delegation 是否正确。

transformRequest 中的 sender_pubkey 就是用户身份的公钥,在 DApp 中可以拿到并使用。

免责声明:作为区块链信息平台,本站所发布文章仅代表作者及嘉宾个人观点,与 Web3Caff 立场无关。本文内容仅用于信息分享,均不构成任何投资建议及要约,并请您遵守所在国家或地区的相关法律法规。