Introduction
This document describes and specifies Internet Identity from various angles and at various levels of abstraction, namely: 本文件从不同角度和不同抽象层次描述和规定了互联网身份,即:
High level goals, requirements and use cases
Overview of the security and identity machinery, including the interplay of identities, keys, and delegations
Interface as used by client applications frontends, i.e., our client authentication protocol
The interface of the Internet Identity Service backend, i.e., describing its contract at the Candid layer, as used by its frontend
Important implementation notes about the Internet Identity Service backend
Internal implementation notes about the Internet Identity Service frontend
The Internet Identity Service consists of
its backend, a canister on the IC. More precisely, a canister on a dedicated subnet with a well-known canister id, and
its frontend, a web application served by the backend canister.
Similarly, the client applications consist of a frontend (served by a canister) and (typically) one or more backend canisters. Only the frontend interacts with the Internet Identity Service directly (via the client authentication protocol described below).
同样,客户端应用程序由一个前端(由容器提供服务)和(通常)一个或多个后端容器组成。 只有前端直接与互联网身份服务交互(通过下面描述的客户端身份验证协议)。Goals, requirements and use cases
The Internet Identity service allows users to
maintain identities on the Internet Computer 维护互联网计算机上的身份
log in with these identities using one out of a set of security devices 使用一组安全设备中的一个以这些身份登录
manage their set of security devices 管理他们的一组安全设备
Some functional requirements are
users have separate identities (or "pseudonyms") per client application (more precisely, per client application frontend "hostname", though see Alternative Frontend Origins for caveat about .raw domains) 每个客户端应用程序的用户都有单独的身份(或“假名”)(更准确地说,每个客户端应用程序前端“主机名”,但请参阅替代前端起源以了解有关 .raw 域的警告)
these identities are stable, i.e., do not depend on a user's security devices 这些身份是稳定的,即不依赖于用户的安全设备
the client frontends interact with any canister on the Internet Computer under the user's identity with that frontend 客户端前端以用户身份与该前端的互联网计算机上的任何容器进行交互
users do not need ever to remember secret information (but possibly per-user non-secret information) 用户不需要记住秘密信息(但可能是每个用户的非秘密信息)
a security device does not need to be manually touched upon every interaction with a client application; a login is valid for a certain amount of time per identity 每次与客户端应用程序交互时,无需手动触摸安全设备; 每个身份的登录在一定时间内有效
Some security requirements are
The separate identities of a single user cannot be related merely based on their public key or principal ids, to impede user tracking。单个用户的单独身份不能仅仅基于其公钥或主体 ID 进行关联,以阻碍用户跟踪。
The security of the identities does not depend on the privacy of data stored on canisters, or transmitted to and from canisters. In particular, the delegations handed out by the backend canister must not be sensitive information.
身份的安全性并不依赖于存储在容器上或与容器之间传输的数据的隐私性。 特别是,后端容器下发的委托不得是敏感信息。
(many more, of course; apply common sense)
Some noteworthy security assumptions are:
- The delivery of frontend applications is secure. In particular, a user accessing the Internet Identity Service Frontend through a TLS-secured HTTP connection cannot be tricked into running another web application. 前端应用程序的交付是安全的。 特别是,通过 TLS 保护的 HTTP 连接访问 Internet 身份服务前端的用户不会被诱骗运行另一个 Web 应用程序。
Just for background: At launch this meant we relied on the trustworthiness of the boundary nodes as well as the replica the boundary nodes happens to fetch the assets from. After launch, certification of our HTTP Gateway protocol and trustworthy client-side code (browser extensions, proxies, etc.) have improved this situation. 仅作为背景:在启动时,这意味着我们依赖边界节点以及边界节点恰好从中获取资产的副本的可信度。 推出后,我们的 HTTP 网关协议和值得信赖的客户端代码(浏览器扩展、代理等)的认证改善了这种情况。
The security devices only allow the use of their keys from the same web application that created the key (in our case, the Internet Identity Service Frontend). 安全设备仅允许使用来自创建密钥的同一 Web 应用程序(在我们的示例中为 Internet 身份服务前端)的密钥。
The user's browser is trustworthy, postMessage communication between different origins is authentic. 用户的浏览器是值得信赖的,不同来源之间的postMessage通信是可信的。
For user privacy, we also assume the Internet Identity Service backend can keep a secret (but since data is replicated, we do not rely on this assumption for other security properties). 对于用户隐私,我们还假设互联网身份服务后端可以保守秘密(但由于数据是复制的,因此我们不依赖此假设来实现其他安全属性)。
Identity design and data model
The Internet Computer serves this frontend under hostnames https://identity.ic0.app (official) and https://identity.internetcomputer.org (experimental).
互联网计算机以主机名 https://identity.ic0.app/(官方)和 https://identity.internetcomputer.org/(实验)为该前端提供服务。
The canister maintains a salt (in the following the salt), a 32 byte long blob that is obtained via the Internet Computer's source of secure randomness.
该容器维护一个盐(以下简称盐),这是一个通过互联网计算机的安全随机源获得的 32 字节长的 blob。
Due to replication of data in canisters, the salt should not be considered secret against a determined attacker. However, the canister will not reveal the salt directly and to the extent it is unknown to an attacker it helps maintain privacy of user identities.
由于数据在canisters中复制,盐不应被视为针对坚定的攻击者的秘密。 然而,该canisters不会直接泄露盐,并且在攻击者不知道的情况下,它有助于维护用户身份的隐私。
A user account is identified by a unique Identity Anchor, a smallish natural number chosen by the canister. 用户帐户由唯一的身份锚来标识,身份锚Anchor是容器选择的一个较小的自然数。
A client application frontend is identified by its hostname (e.g., abcde-efg.ic0.app, nice-name.ic0.app, non-ic-application.com). Frontend application can be served by canisters or by websites that are not hosted on the Internet Computer. 客户端应用程序前端由其主机名标识(例如,https://abcde-efg.ic0.app/、https://nice-name.ic0.app/、https://non-ic-application.com/)。 前端应用程序可以由容器或未托管在互联网计算机上的网站提供服务。
A user has a separate user identity for each client application frontend (i.e., per hostname). This identity is a self-authenticating id of the DER encoded canister signature public key which has the form 用户的每个客户端应用程序前端(即每个主机名)都有一个单独的用户身份。 此身份是 DER 编码的容器签名公钥的自验证 ID,其形式如下
user_id = SHA-224(DER encoded public key) · 0x02` (29 bytes)
and the BIT STRING field of the DER encoded public key has the form
bit_string = |ii_canister_id| · ii_canister_id · seed
where the seed is derived as follows
seed = H(|salt| · salt · |user_number| · user_number · |frontend_host| · frontend_host)
where H is SHA-256, · is concatenation, |…| is a single byte representing the length of … in bytes, user_number is the ASCII-encoding of the Identity Anchor as a decimal number, and frontend_host is the ASCII-encoding of the client application frontend's hostname (at most 255 bytes). 其中 H 是 SHA-256,· 是串联,|…| 是表示…长度的单个字节(以字节为单位),user_number 是身份锚点的 ASCII 编码(十进制数),frontend_host 是客户端应用程序前端主机名的 ASCII 编码(最多 255 个字节)。
A frontend_host of the form <canister id>.icp0.io will be rewritten to <canister id>.ic0.app before being used in the seed. This ensures transparent pseudonym transfer between apps hosted on ic0.app and icp0.io domains. <canister id>https://.icp0.io/ 形式的 frontend_host 在用于种子之前将被重写为 <canister id>.ic0.app。 这可确保 ic0.app 和 https://icp0.io/ 域上托管的应用程序之间进行透明的假名传输。
The Internet Identity Service Backend stores the following data in user accounts, indexed by the respective Identity Anchor: 互联网身份服务后端将以下数据存储在用户帐户中,并由相应的身份锚索引:
a set of device information, consisting of
the device's public key (DER-encoded)
a device alias, chosen by the user to recognize the device
an optional credential id, which is necessary for WebAuthn authentication
When a client application frontend wants to authenticate as a user, it uses a session key (e.g., Ed25519 or ECDSA), and by way of the authentication flow (details below) obtains a delegation chain that allows the session key to sign for the user's main identity. 当客户端应用程序前端想要以用户身份进行身份验证时,它会使用会话密钥(例如 Ed25519 或 ECDSA),并通过身份验证流程(详细信息如下)获取委托链,该委托链允许会话密钥对用户的密钥进行签名 主要身份。
The delegation chain consists of one delegation, called the client delegation. It delegates from the user identity (for the given client application frontend) to the session key. This delegation is created by the Internet Identity Service Canister, and signed using a canister signature. This delegation is unscoped (valid for all canisters) and has a maximum lifetime of 30 days, with a default of 30 minutes. 委托链由一个委托组成,称为客户端委托。 它将用户身份(对于给定的客户端应用程序前端)委托给会话密钥。 该委托由互联网身份服务容器创建,并使用容器签名进行签名。 该委派没有范围(对所有容器都有效),最长生命周期为 30 天,默认值为 30 分钟。
The Internet Identity Service Frontend also manages an identity frontend delegation, delegating from the security device's public key to a session key managed by this frontend, so that it can interact with the backend without having to invoke the security device for each signature. 互联网身份服务前端还管理身份前端委托,将安全设备的公钥委托给该前端管理的会话密钥,以便它可以与后端交互,而无需为每个签名调用安全设备。
Client authentication protocol
This section describes the Internet Identity Service from the point of view of a client application frontend. 本节从客户端应用程序前端的角度描述互联网身份服务。
The client application frontend creates a session key pair (e.g., Ed25519).
It installs a message event handler on its own window.
It loads the url https://identity.ic0.app/#authorize in a separate tab. Let identityWindow be the Window object returned from this.
In the identityWindow, the user logs in, and the identityWindow invokes
window.opener.postMessage(msg, "*")
where msg is
interface InternetIdentityReady {
kind: "authorize-ready"
}The client application, after receiving the InternetIdentityReady, invokes
identityWindow.postMessage(msg, "https://identity.ic0.app")
where msg is a value of type
interface InternetIdentityAuthRequest {
kind: "authorize-client";
sessionPublicKey: Uint8Array;
maxTimeToLive?: bigint;
derivationOrigin?: string;
}where
the sessionPublicKey contains the public key of the session key pair.
the maxTimeToLive, if present, indicates the desired time span (in nanoseconds) until the requested delegation should expire. The Identity Provider frontend is free to set an earlier expiry time, but should not create a one larger.
the derivationOrigin, if present, indicates an origin that should be used for principal derivation instead of the client origin. Values must match the following regular expression: ^https:\/\/[\w-]+(\.raw)?\.(ic0\.app|icp0\.io)$. Internet Identity will only accept values that are also listed in the HTTP resource https://<canister_id>.ic0.app/.well-known/ii-alternative-origins of the corresponding canister (see Alternative Frontend Origins).
Now the client application window expects a message back, with data event.
If event.origin is not either "https://identity.ic0.app" or "https://identity.internetcomputer.org" (depending on which endpoint you are using), ignore this message.
The event.data value is a JS object with the following type:
interface InternetIdentityAuthResponse {
kind: "authorize-client-success";
delegations: [{
delegation: {
pubkey: Uint8Array;
expiration: bigint;
targets?: Principal[];
};
signature: Uint8Array;
}];
userPublicKey: Uint8Array;
}where the userPublicKey is the user's Identity on the given frontend and delegations corresponds to the CBOR-encoded delegation chain as used for authentication on the IC.
It could also receive a failure message of the following type
interface InternetIdentityAuthResponse {
kind: "authorize-client-failure";
text: string;
}The client application frontend needs to be able to detect when any of the delegations in the chain has expired, and re-authorize the user in that case.
The @dfinity/auth-client NPM package provides helpful functionality here.
The client application frontend should support delegation chains of length more than one, and delegations with targets, even if the present version of this spec does not use them, to be compatible with possible future versions. 客户端应用程序前端应支持长度超过 1 的委托链以及具有目标的委托(即使本规范的当前版本不使用它们),以便与可能的未来版本兼容。
The Internet Identity frontend will use event.origin as the "Frontend URL" to base the user identity on. This includes protocol, full hostname and port. This means 互联网身份前端将使用 event.origin 作为用户身份的“前端 URL”。 这包括协议、完整主机名和端口。 这意味着
Changing protocol, hostname (including subdomains) or port will invalidate all user identities.
- However, multiple different frontend URLs can be mapped back to the canonical frontend URL, see Alternative Frontend Origins.
- Frontend URLs on icp0.io are mapped to ic0.app automatically, see Identity design and data model.
The frontend application must never allow any untrusted JavaScript code to be executed, on any page on that hostname. Be careful when implementing a JavaScript playground on the Internet Computer.
Alternative Frontend Origins
To allow flexibility regarding the canister frontend URL, the client may choose to provide the canonical canister frontend URL (https://<canister_id>.ic0.app or https://<canister_id>.raw.ic0.app) as the derivationOrigin (see Client authentication protocol). This means that Internet Identity will issue the same principals to the frontend (which uses a different origin) as it would if it were using one of the canonical URLs.
This feature is also available for https://<canister id>.icp0.io (resp. https://<canister id>.raw.icp0.io).
This feature is intended to allow more flexibility with respect to the origins of a single service. Do not use this feature to allow third party services to use the same principals. Only add origins you fully control to /.well-known/ii-alternative-origins and never set origins you do not control as derivationOrigin!
https://<canister_id>.ic0.app and https://<canister_id>.raw.ic0.app do not issue the same principals by default . However, this feature can also be used to map https://<canister_id>.raw.ic0.app to https://<canister_id>.ic0.app principals or vice versa.
In general, Internet Identity only allows alternative origins of the form <canister id>.ic0.app or <canister id>.icp0.io. There is one exception: nns.ic0.app, which is treated as <nns-dapp canister id>.icp0.io.
In order for Internet Identity to accept the derivationOrigin the corresponding canister must list the frontend origin in the JSON object served on the URL https://<canister_id>.ic0.app/.well-known/ii-alternative-origins (i.e. the canister must implement the http_request query call as specified here).
JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "II Alternative Origins Principal Derivation Origins",
"description": "An object containing the alternative frontend origins of the given canister, which are allowed to use a canonical canister URL (https://<canister_id>.ic0.app or https://<canister_id>.raw.ic0.app) for principal derivation.",
"type": "object",
"properties": {
"alternativeOrigins": {
"description": "List of allowed alternative frontend origins",
"type": "array",
"items": {
"type": "string"
},
"minItems": 0,
"maxItems": 10,
"uniqueItems": true
}
},
"required": [ "alternativeOrigins" ]
}
Example
{
"alternativeOrigins": [
"https://alternative-1.com",
"https://www.nice-frontend-name.org"
]
}
The path /.well-known/ii-alternative-origins will always be requested using the non-raw https://<canister_id>.ic0.app domain (even if the derivationOrigin uses a .raw) and must be delivered as a certified asset. Requests to /.well-known/ii-alternative-origins must be answered with a 200 HTTP status code. More specifically Internet Identity will not follow redirects and fail with an error instead. These measures are required in order to prevent malicious boundary nodes or replicas from tampering with ii-alternative-origins.
To prevent misuse of this feature, the number of alternative origins must not be greater than 10.
In order to allow Internet Identity to read the path /.well-known/ii-alternative-origins, the CORS response header Access-Control-Allow-Origin must be set and allow the Internet Identity origin https://identity.ic0.app.
The Internet Identity Service Backend interface
This section describes the interface that the backend canister provides.
This interface is currently only used by its own frontend. This tight coupling means that this interface may change, even in incompatible ways. We therefore do not have to apply Candid best practices for backward-compatibility (such as using records for arguments and results). 该接口目前仅由其自己的前端使用。 这种紧密耦合意味着该接口可能会发生变化,即使是以不兼容的方式。 因此,我们不必应用 Candid 最佳实践来实现向后兼容性(例如使用参数和结果记录)。
The summary is given by the Candid interface:
type UserNumber = nat64; type PublicKey = blob; type CredentialId = blob; type DeviceKey = PublicKey; type UserKey = PublicKey; type SessionKey = PublicKey; type FrontendHostname = text; type Timestamp = nat64; type HeaderField = record { text; text; }; type HttpRequest = record { method: text; url: text; headers: vec HeaderField; body: blob; }; type HttpResponse = record { status_code: nat16; headers: vec HeaderField; body: blob; streaming_strategy: opt StreamingStrategy; }; type StreamingCallbackHttpResponse = record { body: blob; token: opt Token; }; type Token = record {}; type StreamingStrategy = variant { Callback: record { callback: func (Token) -> (StreamingCallbackHttpResponse) query; token: Token; }; }; type Purpose = variant { recovery; authentication; }; type KeyType = variant { unknown; platform; cross_platform; seed_phrase; }; type Challenge = record { png_base64: text; challenge_key: ChallengeKey; }; type DeviceData = record { pubkey : DeviceKey; alias : text; credential_id : opt CredentialId; purpose: Purpose; key_type: KeyType; }; type RegisterResponse = variant { // A new user was successfully registered. registered: record { user_number: UserNumber; }; // No more registrations are possible in this instance of the II service canister. canister_full; // The challenge was not successful. bad_challenge; }; type AddTentativeDeviceResponse = variant { // The device was tentatively added. added_tentatively: record { verification_code: text; device_registration_timeout: Timestamp;}; // Device registration mode is off, either due to timeout or because it was never enabled. device_registration_mode_off; // There is another device already added tentatively another_device_tentatively_added; }; type VerifyTentativeDeviceResponse = variant { // The device was successfully verified. verified; // Wrong verification code entered. Retry with correct code. wrong_code: record { retries_left: nat8}; // Device registration mode is off, either due to timeout or because it was never enabled. device_registration_mode_off; // There is no tentative device to be verified. no_device_to_verify; }; type Delegation = record { pubkey: PublicKey; expiration: Timestamp; targets: opt vec principal; }; type SignedDelegation = record { delegation: Delegation; signature: blob; }; type GetDelegationResponse = variant { // The signed delegation was successfully retrieved. signed_delegation: SignedDelegation; // The signature is not ready. Maybe retry by calling `prepare_delegation` no_such_delegation }; type InternetIdentityStats = record { users_registered: nat64; assigned_user_number_range: record { nat64; nat64; }; }; type InternetIdentityInit = record { assigned_user_number_range : record { nat64; nat64; }; }; type ChallengeKey = text; type ChallengeResult = record { key : ChallengeKey; chars : text; }; type DeviceRegistrationInfo = record { tentative_device : opt DeviceData; expiration: Timestamp; }; type IdentityAnchorInfo = record { devices : vec DeviceData; device_registration: opt DeviceRegistrationInfo; }; service : (opt InternetIdentityInit) -> { init_salt: () -> (); create_challenge : () -> (Challenge); register : (DeviceData, ChallengeResult) -> (RegisterResponse); add : (UserNumber, DeviceData) -> (); remove : (UserNumber, DeviceKey) -> (); // Returns all devices of the user (authentication and recovery) but no information about device registrations. // Note: Will be changed in the future to be more consistent with get_anchor_info. lookup : (UserNumber) -> (vec DeviceData) query; get_anchor_info : (UserNumber) -> (IdentityAnchorInfo); get_principal : (UserNumber, FrontendHostname) -> (principal) query; stats : () -> (InternetIdentityStats) query; enter_device_registration_mode : (UserNumber) -> (Timestamp); exit_device_registration_mode : (UserNumber) -> (); add_tentative_device : (UserNumber, DeviceData) -> (AddTentativeDeviceResponse); verify_tentative_device : (UserNumber, verification_code: text) -> (VerifyTentativeDeviceResponse); prepare_delegation : (UserNumber, FrontendHostname, SessionKey, maxTimeToLive : opt nat64) -> (UserKey, Timestamp); get_delegation: (UserNumber, FrontendHostname, SessionKey, Timestamp) -> (GetDelegationResponse) query; http_request: (request: HttpRequest) -> (HttpResponse) query; }
The init_salt method is mostly internal, see Salt.
The register and create_challenge methods
The register method is used to create a new user. The Internet Identity Service backend creates a fresh Identity Anchor, creates the account record, and adds the given device as the first device. 注册方法用于创建新用户。 互联网身份服务后端创建一个新的身份锚、创建帐户记录并将给定设备添加为第一台设备。
Authorization: This request must be sent to the canister with caller that is the self-authenticating id derived from the given DeviceKey. 授权:此请求必须发送到调用者所在的容器,该调用者是从给定 DeviceKey 派生的自身份验证 ID。
In order to protect the Internet Computer from too many "free" update calls, and to protect the Internet Identity Service from too many user registrations, this call is protected using a CAPTCHA challenge. The register call can only succeed if the ChallengeResult contains a key for a challenge that was created with create_challenge (see below) in the last 5 minutes and if the chars match the characters that the Internet Identity Service has stored internally for that key. 为了保护互联网计算机免遭过多“免费”更新调用的影响,并保护互联网身份服务免遭过多用户注册的影响,此调用使用验证码质询进行保护。 仅当 ChallengeResult 包含在过去 5 分钟内使用 create_challenge(见下文)创建的挑战的密钥,并且字符与互联网身份服务内部为该密钥存储的字符匹配时,注册调用才能成功。
The add method
The add method appends a new device to the given user's record.
The Internet Identity Service backend rejects the call if the user already has a device on record with the given public key.
This may also fail (with a reject) if the user is registering too many devices.
Authorization: This request must be sent to the canister with caller that is the self-authenticating id derived from any of the public keys of devices associated with the user before this call.
The remove method
The remove method removes a device, identified by its public key, from the list of devices a user has.
It is allowed to remove the key that is used to sign this request. This can be useful for a panic button functionality.
It is allowed to remove the last key, to completely disable a user. The canister may forget that user completely then, assuming the Identity Anchor generation algorithm prevents new users from getting the same Identity Anchor.
It is the responsibility of the frontend UI to protect the user from doing these things accidentally.
Authorization: This request must be sent to the canister with caller that is the self-authenticating id derived from any of the public keys of devices associated with the user before this call.
If the device has the protected flag, then the request must be sent to the canister with caller that is the self-authenticating id derived from the public key of that particular device.
The enter_device_registration_mode method
Enables device registration mode for the given identity anchor. When device registration mode is active, new devices can be added using add_tentative_device and verify_tentative_device. Device registration mode stays active for at most 15 minutes or until the flow is either completed or aborted.
Authorization: This request must be sent to the canister with caller that is the self-authenticating id derived from any of the public keys of devices associated with the user before this call.
The exit_device_registration_mode method
Exits device registration mode immediately. Any non verified tentative devices are discarded.
Authorization: This request must be sent to the canister with caller that is the self-authenticating id derived from any of the public keys of devices associated with the user before this call.
The add_tentative_device method
Tentatively adds a new device to the supplied identity anchor and returns a verification code. This code has to be used with the verify_tentative_device method to verify this device. If the flow is aborted or not completed within 15 minutes, the tentative device is discarded.
Tentatively added devices cannot be used to login into the management view or authorize authentications for other dApps.
Authorization: Anyone can call this
The verify_tentative_device method
For an anchor in device registration mode: if called with a valid verification code, adds the tentative device as a regular device to the anchor and exits registration mode. The registration flow is aborted if this method is called five times with invalid verification codes.
Returns an error if called for a device not in registration mode.
Authorization: This request must be sent to the canister with caller that is the self-authenticating id derived from any of the public keys of devices associated with the user before this call.
The lookup query method
Fetches all device data associated with a user.
Authorization: Anyone can call this
The get_anchor_info method
Fetches all data associated with an anchor including registration mode and tentatively registered devices.
Authorization: This request must be sent to the canister with caller that is the self-authenticating id derived from any of the public keys of devices associated with the user before this call.
The get_principal query method
Fetches the principal for a given user and front end.
Authorization: This request must be sent to the canister with caller that is the self-authenticating id derived from any of the public keys of devices associated with the user before this call.
The prepare_delegation method
The prepare_delegation method causes the Internet Identity Service backend to prepare a delegation from the user identity associated with the given Identity Anchor and Client Application Frontend Hostname to the given session key.
This method returns the user's identity that's associated with the given Client Application Frontend Hostname. By returning this here, and not in the less secure get_delegation query, we prevent attacks that trick the user into using a wrong identity.
The expiration timestamp is determined by the backend, but no more than maxTimeToLive (if present) nanoseconds in the future.
The method returns the expiration timestamp of the delegation. This is returned purely so that the client can feed it back to the backend in get_delegation.
The actual delegation can be fetched using get_delegation immediately afterwards.
Authorization: This request must be sent to the canister with caller that is the self-authenticating id derived from any of the public keys of devices associated with the user before this call.
The get_delegation query method
For a certain amount of time after a call to prepare_delegation, a query call to get_delegation with the same arguments, plus the timestamp returned from prepare_delegation, actually fetches the delegation.
Together with the UserKey returned by prepare_delegation, the result of this method is used by the Frontend to pass to the client application as per the client authentication protocol.
Authorization: This request must be sent to the canister with caller that is the self-authenticating id derived from any of the public keys of devices associated with the user before this call.
The Internet Identity Service backend internals
This section, which is to be expanded, describes interesting design choices about the internals of the Internet Identity Service Canister. In particular
Salt
The salt used to blind the hashes that form the seed of the Canister Signature "public keys" is obtained via a call to aaaaa-aa.raw_rand(). The resulting 32 byte sequence is used as-is.
Since this cannot be done during canister_init (no calls from canister init), the randomness is fetched by someone triggering the init_salt() method explicitly, or just any other update call. More concretely:
Anyone can invoke init_salt()
init_salt() traps if salt != EMPTY_SALT
Else, init_salt() calls aaaaa-aa.raw_rand(). When that comes back successfully, and still salt == EMPTY_SALT, it sets the salt. Else, it traps (so that even if it is run multiple times concurrently, only the first to write the salt has an effect).
all other update methods, at the beginning, if salt == EMPTY_SALT, they await self.init_salt(), ignoring the result (even if it is an error). Then they check if we still have salt == EMPTY_SALT and trap if that is the case.
Why we do not use canister_inspect_message
The system allows canisters to inspect ingress messages before they are actually ingressed, and decide if they want to pay for them (see the interface spec). Because the Internet Identity canisters run on a system subnet, cycles are not actually charged, but we still want to avoid wasting resources.
It seems that this implies that we should use canister_inspect_message to reject messages that would, for example, not pass authentication.
But upon closer inspection (heh), this is not actually useful.
One justification for this mechanism would be if we expect a high number of accidentally invalid calls. But we have no reason to expect them at the moment.
Another is to protect against a malicious actor. But that is only useful if the malicious actor doesn't have an equally effective attack vector anyways, and in our case they do: If they want to flood the NNS with calls, they can use calls that do authenticate (e.g. keeping removing and adding devices, or preparing delegations); these calls would pass message inspection.
On the flip side, implementing canister_inspect_message adds code, and thus a risk for bugs. In particular it increases the risk that some engineer might wrongly assume that the authentication check in canister_inspect_message is sufficient and will not do it again in the actual method, which could lead to a serious bug.
Therefore the Internet Identity Canister intentionally does not implement canister_inspect_message.
Internal data model and data structures used
The primary data structure used by the backend is a map from Identity Anchor to the list of user devices. Device lists are stored directly in canister stable memory. The total amount of storage is limited to 2KiB bytes per user. With the stable memory size of 8GiB we can store around 4 * 10^6 user records in a single canister.
Stable memory layout
All the integers (u64, u32, u16) are encoded in Little-Endian.
Storage ::= {
Header
UserRecords
}
Header ::= {
magic : u8[3] = "IIC"
version : u8 = 1
number_of_user_records : u32
user_number_range_lo : u64
user_number_range_hi : u64
entry_size: u16
salt: u8[32]
padding : u8[454]
}
UserRecords ::= UserRecord*
UserRecord ::= {
size : u16
candid_bytes: u8[510]
}
User record for Identity Anchor N is stored at offset sizeof(Header) + (N - user_number_range_lo) * sizeof(UserRecord). Each record consists of a 16 bit size ∈ [0..510] followed by size bytes of Candid-serialized list of devices.
type UserDeviceList = vec(record {
pubkey : DeviceKey;
alias : text;
credential_id : opt CredentialId;
});
Initialization
The Internet Identity canister is designed for sharded deployments. There can be many simultaneously installed instances of the canister code, each serving requests of a subset of users. As users are identified by their Identity Anchor, we split the range of Identity Anchors into continuous non-overlapping half-closed intervals and assign each region to one canister instance. The assigned range is passed to the canister as an init argument, encoded in Candid:
type InternetIdentityInit = record {
// Half-closed interval of Identity Anchors assigned to this canister, [ left_bound, right_bound )
assigned_user_number_range: record { nat64; nat64; };
};
Approach to upgrades
We don't need any logic recovery logic in pre/post-upgrade hooks because we place all user data to stable memory in a way that can be accessed directly. The signature map is simply dropped on upgrade, so users will have to re-request their delegations.
The Internet Identity Service frontend
The Internet Identity Service frontend is the user-visible part of the Internet Identity Service, and where it all comes together. It communicates with
the user
its backend using the Candid interface described above
the security devices, using the Web Authentication API
its past and future self, via the browser storage
client application frontends, via the OAUTH protocol
Storage used
The frontend only stores a single piece of local storage, namely the current Identity Anchor, if known under the key user_number.
Flows
The following flows are not prescriptive of the UI, e.g. "the frontend asks the user for X" may also mean that on the previous shown page, there is already a field for X.
The possible login subflows are shared among entry points / and /authorized, and are thus described separately. At the end of a successful login subflow:
The frontend knows the user_number (also stored in local storage).
the frontend has a temporary session key
the frontend has a device_identity for the present security device
the frontend has a frontend_delegation from the security device to the session key
All update calls to the Internet Identity Service Backend are made under the device_identity and are signed with the session key.
The steps marked with 👆 are the steps where the user presses the security device.
Subflow: Login as returning user
The frontend notices that user_number is present in local storage.
The frontend offers the choices
Welcome <Identity Anchor>. Do you want to log in?
Log in as a different user
User wants to log in
The frontend uses lookup to fetch the list of devices
The frontend creates a session key.
👆 The frontend creates a delegation from the security device key to the session key, and signs it with the security key, using any of the devices listed in the user account. It notes which device was actually used.
Let device_identity of type WebAuthenicationIdentity be the identity created from that, and let frontend_delegation be the signed delegation.
The frontend configures the agent to use the session key for all further update calls.
Login complete
Subflow: Login via initial registration
The frontend notices that no user_number is present in local storage.
The frontend offers the choices
Create new account
Log into existing account with existing device
Log into existing account with new device
The user chooses to create a new account
👆 The frontend asks the security device to create a new public key. Let device_identity of type WebAuthenicationIdentity be the identity created from that.
The frontend creates a session key.
👆 The frontend creates a delegation from the security device key to the session key, and signs it with the security key. Let frontend_delegation be that signed delegation.
The frontend configures the agent to use the session key for all further update calls.
The frontend asks the user for a device alias.
The frontend calls register(), and obtains the user_number.
It stores the user_number in local storage.
The frontend insistently tells the user to write down this number.
The frontend asks the user to create a recovery option (see Flow: Setup Recovery)
Login complete
Subflow: Login via existing device
The frontend notices that no user_number is present in local storage. (Or user said "log in as different user" in returning flow.)
The frontend offers the choices
Create new account
Log into existing account with existing device
Log into existing account with new device
The user selects "Log into existing account with existing device"
The frontend asks the user for their Identity Anchor, and stores that in user_number.
Continue as in "Subflow: Login as returning user"
Subflow: Login via new device
The frontend notices that no user_number is present in local storage.
The frontend offers the choices
Create new account
Log into existing account with existing device
Log into existing account with new device
The user selects "Log into existing account with new device"
The frontend asks the user for their Identity Anchor.
The frontend asks the user for a device alias.
👆 Frontend asks security device for a new public key and credential id.
The frontend calls add_tentative_device()
The frontend polls lookup() until the credential id is present or the timeout is reached.
The frontend stores the user_number in local storage
Login complete
(See "Flow: adding remote device" for what happens on the other device.)
Flow: Direct access to the frontend
This flow is the boring default
User browses to https://identity.ic0.app/
👆 The appropriate login subflow happens
User sees their management screen. In particular
Their Identity Anchor
A button to add additional devices
The list of their devices, with device aliases, a symbol marking recovery devices, and a button to remove
A "logout" button
One could imagine additional information, such as the last time a device was used, or even a list of recent client applications that the user logged into.
Flow: adding remote device
The user accesses https://identity.ic0.app/
👆 The appropriate login subflow happens
On the management page the user selects \"Add New Device\"
On the flow selection screen the user selects \"Remote Device\"
- Call enter_device_registration_mode() to start device registration flow
The UI polls get_anchor_info() until a tentative device is present or the timeout is reached
- Now the "Flow: Login via new device" happens on the remote device
The user enters the verification code
- Call verify_tentative_device() to complete the flow
Flow: Setup recovery
The user is offered two options for recovery and the option to skip
A security key
A BIP-39 seed phrase
Depending on their choice a) If they choose the seed phrase it is generated and displayed to them with a copy button and an explanation of having to keep this phrase secure b) 👆 If they choose the security key they generate new credentials for the touched device
The device is added to the users account with the purpose #recovery set
Flow: Use recovery
On the login page the user selects "Recover my account"
The user is prompted for their Identity Anchor
If no DeviceData with the purpose #recovery is found for the Identity Anchor an error is displayed
The user is asked to provide the seed phrase or 👆 their recovery security key
The management page is shown
Flow: authenticating client applications
The user accesses /#authorize
👆 The appropriate login subflow happens
The frontend listens to a message event (as per postMessage API)
The event.data should be a message as per our Client authentication protocol.
If the message contains a value for derivationOrigin:
- If the derivationOrigin value does not match the format specified in the section Client authentication protocol, the process flow is aborted with a failure message.
- The frontend calls https://<canister_id>.ic0.app/.well-known/ii-alternative-origins to retrieve the allowed alter native origins.
- If there is no such certified asset the flow is aborted with a failure message.
- The frontend validates the retrieved data as per schema specified in section JSON Schema
- If the event.origin is contained in the array value of the property alternativeOrigins the derivationOrigin will be used as the Application Frontend’s hostname.
If the message does not contain a value for derivationOrigin the event.origin is used as the Application Frontend’s hostname
The user is asked if they want to log into the client application, showing the client application frontend’s hostname.
The frontend calls prepare_delegation() with the client application frontend hostname, client application provided session key and desired time to live.
The frontend queries get_delegation() to get the delegation data
It posts that data to the client application, using event.source.postMessage and the types specified in Client authentication protocol.
After receiving the data the client application is expected to close the Internet Identity window / tab.
Flow: Deleting devices
The user is logged in, on the management view, and selects a device to delete.
If this is the device the user is currently logged in (the current device_identity), the user is warned.
If this is the last device of the user, the user is warned even more sternly.
The device is removed via remove().
If this was the device that the user has logged in with, log out (as per "Flow: logging out")
Else, refresh the device view.
Flow: Logging out
The user is logged in, on the management view, and clicks the logout button.
The user_number is removed from local storage
The page is reloaded (to send the user back to the beginning of "Flow: Direct access").
互联网计算机环境下的身份技术规范