The Identity & Wallet Standards working group is looking for feedback on IC wallet standards that we have been working on over the past months. These standards are intended to form a solid basis for a healthy ecosystem of interoperable wallets and dapps on the IC.
In particular, they address the following problems that currently exist on the IC:
It is difficult to do cross-dapp asset management (i.e. token payments, NFT transfers, etc.) due to dapp-specific principals.
Transaction approval flows are not supported by wallets that issue delegations on session keys.
Both of these issues are addressed by the following two draft standards:
The standards are intentionally kept concise and simple but lend themselves to be extended in the future.
Together with the IC Interface Specification, they form a complete set of interactions between a dapp, a wallet, the user and the IC as shown in the diagram:
The standards are still in draft but (hopefully) close to being finalized. We are currently seeing the first PoC implementations being built to make sure that they hold up in practice.
Now is the perfect time to join the discussion: the drafts are concrete enough to be implemented but still open enough to be changed if necessary.
If you are interested in wallets and / or are a developer that wants to integrate with wallets, please read the drafts, provide feedback and ask questions.
Please either reply in this thread, open issues and leave comments in the working group GitHub repository or join the working groups sessions for a live discussion. The next session is on September 5 (see calendar).
I’ve yet to dive in, but on first blush here are some thoughts/questions:
Do canister wallets maintain first-class citizenship? (I see a bunch of json rpc stuff and cbor here that is particularly a pain to work with inside motoko canisters).
With that being said, canister wallets really, really, really, need derived canister ids to be implemented at the protocol level.
Please namespace any function calls and types to avoid collisions in the future. (ie. icrc21_consent_message…the type error_info looks particularly ripe for collision).
For 25, can you better define a “relying party”? Is this like a dapp? Is it basically always a web page(or UI of a native app)? A real-world use case would be great here.
I can imagine that a dapp frontend or canister communicating with a wallet canister can be considered another message transport protocol using the same spec.
Right now the spec is limited to message transport protocols that allow the wallet to verify the origin of the dapp. This basically limits it to web only at the moment.
Regarding 1, maybe it makes more sense to define the spec in Candid IDL and define an additional spec for Candid → JSON RPC. This would also clarify and standardize any future data transports over JSON RPC in other specs unrelated to wallet standards.
Then individual transport layers can have their own spec that go into detail whether Candid, JSON RPC or something else is used.
Regarding4, I mentioned in the message thread of the spec the idea of sending a der encoded public key in the permission request. This basically means that subsequent canister call requests to the wallet can have a signed signature included in the request. The wallet then no longer has to verify the origin of the dapp, it already did during the permission request earlier, so it only needs to verify the signature.
This would mean in practice that for example a native mobile dapp only needs to run a proxy webpage for the permission request, all subsequent canister call requests can be made directly without the need of a webpage in between.
This will likely also be something that could work for canister wallets, a user of a dapp connects to the canister wallet webpage. And then afterwards canister call requests can be made directly to the canister wallet itself without a webpage in between.
Also there’s the case where a dapp on domain A might connect to a wallet but wants to acts as if it’s domain B from the same dapp ecosystem. Signatures would allow to share the connection across multiple domains by simply sharing the keys across the domains.
Probably sharing the keys should happen securely, so delegation come back into the story which means one would make a canister call request with either no signature (origin is checked), a signed signature (signature is checked) or a signature + delegation chain (both are checked).
For canister to web wallet communication, the signature that will be sent in the permission request could be made by the canister by certifying the challenge, similar to how II creates the first signature in the delegation chain within the canister (Just finished implementing a validation check for those in JS ).
For canister to canister wallet communication, the caller can basically be the origin of the permission request.
So there’s basically a lot of possible extensions for the spec to support many use cases that might not be covered by the initial spec.
I appreciate the effort you’ve put into the diagram. However, the concept of a “Target canister” or its purpose isn’t clear to me.
For better clarity, perhaps a flowchart could be incorporated to visualize the user journey. This would give us insights into how users would engage with the wallet, canisters, and dapps.
Lastly, could you please mention the key advantages of introducing these standards, especially in relation to existing wallets like Internet Identity or Plug?
Hey there!
Thank you for your amazing work. I really like the idea of consent messages specified by canisters - great job. But I have a lot of comments about ICRC-25. They are written imperatively, but this is just because it makes the style of writing more consistent and readable.
Hope you find this useful.
General
Specify the signature algorithm that is used for public key deriviation and canister call signature procedure:
Current spec has no notion of that information.
Different networks (networks[n].chainId) might use different algorithms.
If the reader is expected to fetch this information from somewhere else, please provide a link to that place in the spec.
If this is only for the ICP ledger, then rename to icpLedger.
If this is a ledger of any coin, how to differentiate between them? Is there a spec for ledger APIs (I mean, some ledgers might not have accoundIds at all)?
Makeresponse.identities[n].ledger.subaccountsa mandatory field, instead of optional:
Otherwise it is valid to pass an empty object inside - identities[n].ledger = {}, the interpretation of which is undefined.
Moveresponse.signatures into response.identities[n].signatures
Seems like the renderer glitched and it shows that signatures field is the direct child of the response object.
If that’s not the case, then I believe we need a separate signature for each identity.
Define the challenge signature procedure more clearly:
What hashing algorithm is used to prepare the message before signing, if there is one?
What is the possible use-case for returning multiple identities inside the response?
The app most certainly uses only one user identity at a time.
The app can always re-authorize a user by sending another permissions request to the wallet.
Relying party knows too much
Since a relying party is the untrusted one, we want to tell it as little info as possible.
A relying party can track users by using browser-stored data, even if they switch their identity to another one, since the relying party can remember public keys used by this user.
Canister calls are made by the wallet itself anyway, a relying party doesn’t need any identity information, since it most probably won’t do anything useful with it.
If the relying party for some technical reason needs to know the public key of the user, then the user should be informed about this, which means, that there should be some kind of read_identity permission scope. When the wallet receives such a scope, it will prompt its owner clearly that “this app can track you, take the following precautions if you need privacy”.
The user has to be able to proactively switch their identity using only their wallet (if they need anonymity) - how a relying party would know if such an identity switch happened and it is time to refetch the public key from the wallet?
IMO, the relying party should only be responsible for preparation of the canister calls. The wallet itself should be capable of showing a user the identity they are logged in currently at any website. All the management and use of identities has to be done in the wallet itself, completely controlled by the user and hidden from everyone else.
Canister call
Clarify trust model
The spec states that “the wallet is trusted”, but we treat it as untrusted. The spec states that “the relying party” is untrusted, but we treat it as trusted here. Seems like they both might be untrusted.
So, the best way seems to be that the signature verification has to happen on both sides: in the wallet by default, and if the relying party wants to protect their users against a malicious wallets, it should also verify the state tree signature once again on its side.
The key algorithm is part of the der key encoding data itself. This a standardized way to transmit keys and their info as binary value, it’s currently already used as encoding format in for example delegation chain keys. As for signature verification this indeed happens on both sides in both the dapp (relying party) and the wallet.
You can find a link above for the demo implementation. I’ll make sure to share the code snippets used for both the relying party and the wallet.
I agree with above comment that some flowcharts could help explain the flows in more detail and the verification steps within.
To not confuse with inline base64-encoded image data.
Instead of making this part of the name it could also be defined in the spec. But agree that it should be an URL so it’s resource can be found in a standardized way (base64 images would still be valid with data: scheme).
That would make the assumption that the dapp is a wallet, it could in theory be any sort of dapp that uses this spec to communicate with other client dapps.
If that’s not the case, then I believe we need a separate signature for each identity.
Indeed, that’s the only way to validate each identity its public key.
If the relying party for some technical reason needs to know the public key of the user, then the user should be informed about this, which means, that there should be some kind of read_identity permission scope. When the wallet receives such a scope, it will prompt its owner clearly that “this app can track you, take the following precautions if you need privacy”.
This could indeed be an additional scope, some dapps like marketplaces will need the public key to know which NFTs a user owns and is thus able to list. But I can indeed imagine that some dapps only need to for example create a canister for the user and make some calls to configure it.
What is the possible use-case for returning multiple identities inside the response?
The app most certainly uses only one user identity at a time.
The app can always re-authorize a user by sending another permissions request to the wallet.
First thing that comes to mind is a tax reporting dapp that makes tax reports for all your identities and their subaccounts (that you choose to share from the wallet). Also not all wallets use subaccounts, some wallets like plug use multiple derives identities instead of subaccounts. The spec currently would support both types of wallets.
So, the best way seems to be that the signature verification has to happen on both sides: in the wallet by default, and if the relying party wants to protect their users against a malicious wallets, it should also verify the state tree signature once again on its side.
Currently for both requests defined in the spec, the wallet would identify the dapp based on it’s origin. This is also how Internet Identity currently scopes a dapp. This would limit the spec to only web use cases, which is why I proposed the possibility of sending a public key in the permission request to the wallet. So subsequent canister call requests no longer need to rely on the origin. This can also be an extension of the spec.
During the permissions request a random challenge is sent to the wallet so the dapp can verify the returned signature to confirm the wallet really controls the identities it says it controls. This is primarily important in web2 dapps that tend to use the public key and signature as authentication method for a user login. Without the signature, this wouldn’t be possible.
As for responses from canister call requests, this is why it contains the content map and certificate, so the dapp can verify if the content map matches the request it sent to the wallet, if the certificate signature is valid and lastly if the signature is actually a response for the request (content map → request id → check if it’s within certificate).
Define the challenge signature procedure more clearly:
What hashing algorithm is used to prepare the message before signing, if there is one?
In the demo implementation, the challenge isn’t hashed before it’s sent. The wallet signs "\x0Aic-wallet-challenge" + challenge with the identity private key (or the private key of an identity that has been delegated by an identity e.g. Internet Identity). Identity signature(s) check implementation: https://github.com/slide-computer/identity/blob/master/src/signature/identity.ts
Restricting signing to only a specific algorithm in the spec would make it impossible to verify that the wallet controls an identity unless that identity happens to be made with that specific algorithm. Delegating to an identity of the required algorithm won’t solve this either, because you’ll still need to validate the delegation chain in that case which will have a signature that isn’t the required algorithm.
On a positive note, most dapps won’t use the public key + signature as a way of authentication session token like web2 dapps would. So they won’t need to validate the signature to begin with, they would instead make (delegated) canister calls to fetch private user data instead, which I would recommend for IC dapps (You could in theory implement http endpoints and session tokens on the IC too just like a web2 dapp but that’s less secure than normal canister calls)
The user has to be able to proactively switch their identity using only their wallet (if they need anonymity) - how a relying party would know if such an identity switch happened and it is time to refetch the public key from the wallet?
Unless the wallet is a browser plugin that can sent events, such a notification of identity change won’t be possible. Having a standard for wallet events like eth wallets have would definitely be interesting for such cases.
Wallets that are web based won’t be able to sent such events, though maybe with a hidden iframe and post messages something could be done here.
So if a dapp makes a canister call request to the wallet, the wallet should indeed know for which identity this call is being made so an optional field for this purpose could definitely help here. Then the wallet can switch to the correct identity or ask the user to switch (or some other wallet specific UX).
Do canister wallets maintain first-class citizenship? (I see a bunch of json rpc stuff and cbor here that is particularly a pain to work with inside motoko canisters).
Yes, I think so. Nothing of the spec prevents a canister from being the entity that owns the assets. Note, that the feature is built around signatures, i.e. a canister based wallet would need to hold asset using its canister signature public key(s).
With that being said, canister wallets really, really, really, need derived canister ids to be implemented at the protocol level.
Yes, I agree with this point. However it is orthogonal to these standards.
Please namespace any function calls and types to avoid collisions in the future. (ie. icrc21_consent_message…the type error_info looks particularly ripe for collision).
Ah, yes. Total no-brainer, will be incorporated in the future. Thanks!
For 25, can you better define a “relying party”? Is this like a dapp? Is it basically always a web page(or UI of a native app)? A real-world use case would be great here.
The relying party is any entity (external to the wallet) that requests a canister call to be made using an identity controlled by the wallet. The standard does not impose anything more. It is an interaction model that works very well for dapps, but it could also be a regular Web 2.0 application.
Thanks a lot for all the work you put in, especially the Demo!
Right now the spec is limited to message transport protocols that allow the wallet to verify the origin of the dapp. This basically limits it to web only at the moment.
Small correction: The spec only requires an authentic channel, meaning you can trust that the message actually comes from the relying party (and was not tampered with by intermediaries). Whether or not you know about the domain is only relevant if you want to hand out delegations scope to that particular origin (which would be an extension to the standard with additional transport requirements).
Regarding 1, maybe it makes more sense to define the spec in Candid IDL and define an additional spec for Candid → JSON RPC. This would also clarify and standardize any future data transports over JSON RPC in other specs unrelated to wallet standards.
I would be very cautious to bring in candid into the mix. So far the whole of the IC interface specification does not use candid (i.e. the arguments sent to the canister is always just treated as a blob). We should try to be consistent with that model and use protocols / encodings that are established for the front-end communication use-case.
JSON RPC was specifically requested by NFID and I think it makes sense. It also aligns well with what other crypto projects are doing (i.e. WalletConnect also uses JSON RPC).
Essentially, the purpose of these standards is to allow generic canister calls on behalf of a wallet identity.
Say, you want to call a canister, but you want to have the user do it using the Identity that controls his ICP. In that case the target canister is the canister that this canister call should go to.
Lastly, could you please mention the key advantages of introducing these standards, especially in relation to existing wallets like Internet Identity or Plug?
See my first post:
In particular, they address the following problems that currently exist on the IC:
It is difficult to do cross-dapp asset management (i.e. token payments, NFT transfers, etc.) due to dapp-specific principals.
Transaction approval flows are not supported by wallets that issue delegations on session keys.
@senior.joinu: Thanks a lot for your feedback. ICRC-25 is newer and less polished, feedback like yours help it get faster over the finish line, so much appreciated!
Specify the signature algorithm that is used for public key deriviation and canister call signature procedure:
Yes, the spec should point to this section of the Internet Computer interface specification.
Different networks (networks[n].chainId) might use different algorithms.
These networks must still be compliant with the IC interface specification, otherwise they are not IC networks.
Makeresponse.identities[n].ledger.subaccountsa mandatory field, instead of optional:
Otherwise it is valid to pass an empty object inside - identities[n].ledger = {}, the interpretation of which is undefined.
It is valid. Not all wallet interactions need to deal with ledger based assets. We only included the ledger metadata to facilitate a probably common interaction (which is showing the balance available in the relying party front-end). But the standard also works for identities that do not hold tokens (but e.g. might control canisters, and the canister call being made is an install_code call to the management canister).
Moveresponse.signatures into response.identities[n].signatures
Seems like the renderer glitched and it shows that signatures field is the direct child of the response object.
If that’s not the case, then I believe we need a separate signature for each identity.
Correct, this is an error that needs fixing.
Define the challenge signature procedure more clearly:
What hashing algorithm is used to prepare the message before signing, if there is one?
This is also specified in this section of the Internet Computer interface specification. However, there is a small mistake in the choice of domain separator (I left a comment there).
What is the possible use-case for returning multiple identities inside the response?
The app most certainly uses only one user identity at a time.
The app can always re-authorize a user by sending another permissions request to the wallet.
@sea-snake already made an example. Another one, that I had in mind is an asset management / staking front-end. Then this would make it easy to connect all the different keys that hold neurons / icp at once.
Relying party knows too much
Since a relying party is the untrusted one, we want to tell it as little info as possible.
A relying party can track users by using browser-stored data, even if they switch their identity to another one, since the relying party can remember public keys used by this user.
Canister calls are made by the wallet itself anyway, a relying party doesn’t need any identity information, since it most probably won’t do anything useful with it.
If the relying party for some technical reason needs to know the public key of the user, then the user should be informed about this, which means, that there should be some kind of read_identity permission scope. When the wallet receives such a scope, it will prompt its owner clearly that “this app can track you, take the following precautions if you need privacy”.
The user has to be able to proactively switch their identity using only their wallet (if they need anonymity) - how a relying party would know if such an identity switch happened and it is time to refetch the public key from the wallet?
IMO, the relying party should only be responsible for preparation of the canister calls. The wallet itself should be capable of showing a user the identity they are logged in currently at any website. All the management and use of identities has to be done in the wallet itself, completely controlled by the user and hidden from everyone else.
So, this one is a little bit more complicated to unpack. Let me try to be concise:
The wallet is trusted by the user. Without that property, none of the interactions make sense, asking for consent is pointless, since all the information presented could be falsified and all assets could be stolen by the wallet at any moment.
Relying party however does not need to trust the wallet (and it shouldn’t have to). I.e. if a user built a wallet to specifically send wrong / false information to the relying party, the relying party must be able to detect that. Without that property, we will not get an ecosystem, where the type of wallet is a user choice (because the relying parties would mandate specific, trusted wallets in order to protect themselves). The relying party is given only the necessary information (which admittedly is quite a lot) to verify that all interactions with the IC were completed as requested.
Yes, the standards are built on a different privacy model than the existing one with different principals per dapp. And yes, a dapp can remember all identities used with it. But that’s fine as long as it is an explicit user choice (and it should be) to share a specific key. Which is exactly what the permission request is for.
Maybe there is a point to be made about the need for an additional message to end a wallet connection (i.e. disconnect). I’m not sure, maybe that is a good topic to pick up in the next working group session.
Clarify trust model
The spec states that “the wallet is trusted”, but we treat it as untrusted. The spec states that “the relying party” is untrusted, but we treat it as trusted here. Seems like they both might be untrusted.
So, the best way seems to be that the signature verification has to happen on both sides: in the wallet by default, and if the relying party wants to protect their users against a malicious wallets, it should also verify the state tree signature once again on its side.
Yes, some clarification should be added. To quickly summarize:
The user trusts the wallet
The wallet has as a root of trust the IC root key and all interactions are verified against that.
The wallet does not trust the relying party.
The relying party has as a root of trust the IC root key and all interactions are verified against that.
These lists will be used in the next working group session to guide the discussion. Please leave a comment if you feel any point is not (well) represented in the list.