从 II 登陆到与后端交互,发生了什么?

图 - 1
图 - 1

先考虑如图 - 1所示的几个基本的数据结构,其中Delegation Identity、Ed25519KeyIdentity都是继承于SignIdentity。

接下来我将简单介绍一下从点击登陆按钮开始到通过agent.call 调用后端这一过程发生了什么:

II 登陆:

通常,在用户点击login 按钮之后,会调用 AuthClient.login()

图 - 2
图 - 2

其函数签名如 图 -2

其中identity Provider可以指定 II 服务的提供者,默认为 https://identity.ic0.app, 如果你是用的本地的II,这里需要填上本地II对应的URL,详细可见:

https://kluy7-aiaaa-aaaad-qamvq-cai.ic.fleek.co/2021/07/Gh202107aa106da32145b6840e146a99bff34771/

maxTimeToLive可以指定这一次登陆的存活时间,在未过期的这段时间里访问应用可以不用再登陆。

图 - 3
图 - 3

上图为login函数体,其首先通过Ed25519KeyIdentity.generate()生成一个Ed25519KeyIdentity实例,其包含一对公私钥,然后将其存入local Storage,然后根据options里面的Identity Provider打开对应的窗口,其url应如下所示:

post image
图 - 4
图 - 4

图 - 4 为provider的入口文件,首先是一系列的检查,判断你的Provider是否正规且安全,随后会调用 login flow里面的login函数:

图 - 5
图 - 5

其首先会判断一下你是否为新用户(userNumber是否已知),已知则调用loginKnown Anchor,这里讨论已知的情况:

图 - 6
图 - 6

这里就是我们熟知的Authenticate页面:

图 - 7
图 - 7

当点击了Authenticate按钮之后会调用 IIconnection里面的login方法,参数为usernumber:

图 - 8
图 - 8

首先调用II Connection内部的lookupAuthenticators方法获取用户所有已认证的设备,如果设备数为0则返回,否则将user Number和devices传入fromWebauthnDevices函数:

图 - 9
图 - 9

接下来就是你触摸Yubikey或者指纹的时候了,完成这一过程之后会生成一个经过你认证的MultiWebAuthnIdentity,其也是继承于 SignIdentity,然后将其传入requestFEDelegation函数,

图 - 10
图 - 10

在这里会另外生成一个Ed25519KeyIdentity 和一个过期时间,将传入的identity加上Ed25519KeyIdentity的publicKey以及过期时间和一个targets(作用域,这里的canisterId为II的canisterId) 作为参数传入DelegationChain的create函数,这个函数会返回一个DelegationChain:

图 - 11
图 - 11

create方法可以调用多次,在options里面传入之前的Chain就可以将当前的Delegation添加到那个Chain的后面,所以这个chain是可以增长的,但这里只调用了一次;函数内部首先会根据传入的Identity、需要被委托的PublicKey(被委托后他就具有用户的权利,可以签名消息)、过期时间、options 生成一个单一的SignedDelegation:

图 - 12
图 - 12

一个delegation如 图 - 1所示有三个属性,这里根据传入的参数构建一个普通的delegation,过期时间为10分钟,然后用传入的 SignIdentity(为当时的MultiWebAuthnIdentity)对这个delegation的requestId加上一个前缀进行一次签名,然后将delegation和signature合成一个SignedDelegation返回,这里的delegation好比机构的一封没有盖章的文件,虽然里面有指定谁能进行重要的操作,但并不具有效应,当机构进行盖章之后(签名),这个文件才能生效(SignedDelegation)。

回到图 - 11,create方法将生成的SignedDelegation结合MultiWebAuthnIdentity的公钥(如果options里面传入了之前的Chain,则为这个chain的公钥)组成一个DelegationChain返回。

再回到图-10,requestFEDelegation方法将create方法返回的Chain加上刚才生成的Ed25519KeyIdentity合起来生成一个DelegationIdentity返回。

再再回到图-9,随后会根据requestFEDelegation返回的DelegationIdentity调用IIConnection的createActor方法创建一个和 II 后端交互的Actor,由于此DelegationIdentity是经过你验证的 identity委托的,所以它能代表是你向后端发出的消息,create Actor如下:

图 - 13
图 - 13

通过MultiWebAuthnIdentity、DelegationIdentity和Actor构建一个 IIConnection,联合着user Number返回。

再再再回到图-4,会取到刚才返回的userNumber和Connection,然后会判断当前URL的hash,如果为 “#authorize” 则会调用auth.ts 里面的setup方法:

图 - 14
图 - 14

这里会向源窗口发送一条 "authorize-ready" 信息告诉它我准备好了,在图 - 3中可以看到源窗口在之前就添加了一个事件监听,所以其可以收到这条消息,收到消息之后会调用_getEventHandler来处理这条消息:

图 - 15
图 - 15

还记得图-3第一步就生成的Ed25519KeyIdentity吗?这里将其publicKey取出并转成Der格式,加上你设置的MaxTimeToLive,构建一个request结构体,并将其回复给Provider。

回到图-14,provider收到源窗口回复的信息之后,会将userNumber、connection、回复的message、源窗口URL传入handleAuthRequest函数:

图 - 16
图 - 16

首先会调用connection的getPrincipal方法,得到用户的Principal,这里就不贴图了,其函数内部首先会检查这个connection里面的delegation Identity是否已经过期了(10分钟),如果过期,会让你重新认证一次,没过期就用connection里面的Actor调用后端的get Principal方法拿到用户的principal;然后将principal和URL传入confirmRedirect函数:

图 - 17
图 - 17

可见,这里就是我们所熟知的proceed页面,当点击了proceed之后会返回 true,然后执行如下语句:

图 - 18
图 - 18

首先拿到源窗口回复的message里面的sessionPublicKey(就是最最最开始生成的Ed25519KeyIdentity的公钥),将其联合其他几个参数传入prepareDelegation方法,这里也不贴图了,其内部首先也会检查这个connection是否过期,没过期就调用后端的prepare_delegation方法,会返回一个userKey和timestamp:

图 - 19
图 - 19

首先是经典的一系列检查,然后根据user Number和前端URL生成一个seed,对这个seed以及一系列message进行签名,更新一下rootHash,最后将seed转化一下变成userKey联合着过期时间返回。

回到图-18,随后会调用retryGetDelegation方法:

图 - 20
图 - 20

这里会去循环调用后端的get_delegation方法,如果后端返回了则跳出循环,否则抛出异常,循环次数最大为5次。

图 - 21
图 - 21

这个函数会返回一个signedDelegation,其中delegation由最开始的Ed25519KeyIdentity的公钥、过期时间(timestamp,其实就是那个maxTimeToLive)、targets(Null)组成,联合着刚才的signature返回。

将get_delegation方法返回的signedDelegation转化一下变成一个新的signedDelegation(里面内容没变),将其放入一个空数组,联合上userKey返回。

回到图 - 14,利用handleAuthRequest函数返回的 [signedDelegation]、userKey生成一个response回复给源窗口。

图 - 22
图 - 22

源窗口收到回复后调用_handleSuccess方法(如上图),首先会根据message里面的 [signedDelegation]生成一个delegations(其实就是copy一份),然后结合着message里面的userPublicKey(后端返回的那个userKey)生成一个delegationChain,将其写入localStorage,最后一步了,利用最最最开始生成的那个Ed25519KeyIdentity加上这个Chain生成一个delegationIdentity。这就是我们和后端交互的时候创建agent的时候传入的那个Identity。

ok,到这里我们可以梳理一下最终的这个DelegationIdentity的组成:

图 - 23
图 - 23

请注意这两个存入local Storage的东西,这个delegationChain里面包含着 II 的过期时间,当你刷新页面的时候,会重新调用一次 AuthClient.create()方法,其会尝试从local Storage里面直接取出Ed25519KeyIdentity和DelegationChain,并且检查是否过期,通过这两个东西直接构建出DelegationIdentity,这样就不需要重新登陆了。

这里想多说一下这个signature,回到图-19,可以看到签名是对seed,session_key(Ed25519KeyIdentity),expiration(过期时间)签名,其内部其实是对这些东西组成的一个结构体的hash签名,这是canister signature,后来验证是子网来验证。用 II 的Actor调用后端的函数本身就是用户认证过的,所以将Ed25519KeyIdentity的公钥设置为委托,并且对其进行签名,都是用户同意的,所以生成的这个delegationChain是可以代表用户的,即这个最终的DelegationIdentity是认证过的identity,用它来调用后端的函数也是可以代表用户的。

Agent.call():

图 - 24
图 - 24

这里会先看看identity是否存在,然后拿到sender,对参数进行一些转化赋值给submit,然后构建一个request结构体。

图 - 25
图 - 25

然后将这个结构体传给delegationIdentity里面的transformRequest方法:

图 - 26
图 - 26

可以看到,在和后端交互的时候,会用identity里面的Ed25519KeyIdentity对这个requestId进行一次签名,然后伴着Identity里面的delegationChain中的delegations和pubKey一起传给后端,这里的sender_sig可以利用delegation里面的publicKey来验证,然后delegation里面的签名由子网验证,这样就可以做到信息的可靠性。sender_pubKey可用来生成principal,call函数的最后一段就是将请求发给后端,然后等待http response。

结语:

Internet computer 的身份系统奇妙而伟大,值得所有ic开发者去学习,这篇文章只是从表层去阐述了一下整个流程,其设计思想还需要读者自己去摸索,我也建议在阅读这篇文章前后都去熟悉一下agentjs和II的源码结构,了解各个数据结构的构造。其中有部分内容为我所推断,并没得到有效的证实,如有不同意见可随时与我交流。另外,由于DelegationChain的create方法可以被多次调用,理论上是可以在不同Dapp中使用同一个DelegationIdentity,只需要将delegation添加进delegationChain的delegations数组里,但我并没有实现过,如果有此经验的小伙伴欢迎与我交流,感谢🙏