dfinity / wg-identity-authentication

Repository of the Identity and Wallet Standards Working Group
https://wiki.internetcomputer.org/wiki/Identity_%26_Authentication
Apache License 2.0
26 stars 8 forks source link

Split ICRC-34 into global and session variants and add ICRC-28. #151

Closed sea-snake closed 3 months ago

sea-snake commented 4 months ago

As discussed in #115, the standard has been split into two standards and ICRC-28 standard has been added.

sea-snake commented 4 months ago

The benefits of the additional restriction to have the session identity to be equal to one of the signer identities is not yet clear to me.

It simplifies the implementation of a relying party like e.g. a marketplace that works with assets on a give principal. All canisters of the relying party work with the same principal as the one that holds the assets.

Alternatively you would have to make a mapping of anonymous session principal to the signer principal holding the assets within the relying party. Which technically would also work, so yeah it's definitely a point up for discussion.

I can imagine that one would want to proof to dapp X that he's user Y on dapp Z and as such want to have global delegations, but you could also achieve that with signing data with ICRC-32.

As a signer the main challenge is creating new identities (with new principal) for each relying party. Right now I can't derive a new identity from an Internet Identity + origin of relying party. I could maybe do something similar to ic-siwe but that would make me dependent on a canister holding a state for all wallet users which I would like to avoid.

frederikrothenberger commented 4 months ago

The benefits of the additional restriction to have the session identity to be equal to one of the signer identities is not yet clear to me.

It simplifies the implementation of a relying party like e.g. a marketplace that works with assets on a give principal. All canisters of the relying party work with the same principal as the one that holds the assets.

Imho, not accounting for an identity abstraction layer in the reyling party is a maintainability footgun and brings tons of additional restrictions:

So overall I don't think any serious relying party will get very far with the model of having the session identity be equal to exactly one signer identity. As such, we should probably design the standards more tailored to the expected way relying parties handle sessions vs signer identities.

dostro commented 4 months ago

II introduced a new concept to crypto dapps of using anonymous delegation identities, yet most known methods of working with crypto requires a global identifier. This thread highlights the increased complexity of finding a middle ground to support both.

I propose the following requirements:

IMHO tooling should abstract the developer complexity of working with a global vs anonymous delegation, not standards. If we split the standards, I fear we're going to split the ecosystem into signers and dapps that only care about global delegations (i.e. marketplaces and defi) and those that don't (i.e. NNS).

sea-snake commented 4 months ago

If we split the standards, I fear we're going to split the ecosystem

Can't agree more on that topic.

Based on the list of requirements you've mentioned it would basically mean that a signer can make the user choose between:

This would mean that the whole session is either anonymous or not but not both at the same time (which I defined in the current spec). In practice this means that we expect all ICRC-25 methods to behave from the perspective of the current session.

The relying party wouldn't know if the session is either global or anonymous in this case which is preferable over a relying party deciding between global or anonymous.

In the case of get_delegation, making the targets required would make the method support both use cases while also informing the user with which canisters will be interacted (instead of just all).

sea-snake commented 4 months ago

I'll update this MR to move it back into a single get_delegation method. Explain the above context and also mention ICRC-28 for scenario's where the delegation is global.

frederikrothenberger commented 4 months ago

@sea-snake: Maybe let's first settle this debate in the next working group session. In particular, I don't think the model described here leads to a usable solution for the "anonymous" case.

frederikrothenberger commented 4 months ago

I'm trying to get more clarity on the pros and cons of the different types of delegation. And I came up with this comparison table:

Property Derived session identity Delegation with restricted targets
Signer principal consistent with delegation principal :x: :white_check_mark:
Allows using shared infrastructure
Example: Make authenticated calls to hot-key controlled neuron on governance canister from a dapp
:white_check_mark: :x:
Scales to 1 canister per user architecture
Projects that use it: OpenChat, Hot or Not
:white_check_mark: :x:
Simplicity: only uses explicitly managed identities
rather than deriving one per context
:x: :white_check_mark:
Privacy: session identities not reused across contexts :white_check_mark: :x:

Please let me know if I missed some aspects in that comparison.


Looking at that table, it seems that by going with the global, target restricted delegation we lose complexity (yay) but also features (nay). And in some scenarios, a signer is not even capable of deriving additional identities (i.e. the slide wallet that @sea-snake is developing).

I think there is room to support both, as it is important to allow a low barrier to entry for devs (an users), but also provide solutions for more complex architectures.


I'm very curious whether we do agree (as a working group) on the following point (independent of the delegation type):

The user should not be aware of the (session) delegation identity: In web 2.0 nobody cares about the details of the session mechanism, as long as sign-in works and your data is there. The same should apply to the delegation we give to the application. The whole thing should be hidden behind a permission (on the permission request) that states "Allow login to application" (or similar).

dostro commented 4 months ago

Allows using shared infrastructure Example: Make authenticated calls to hot-key controlled neuron on governance canister from a dapp

This would work with a global delegation as well, just with a wallet prompt requiring a signature, which I would consider a feature in its own right.

Scales to 1 canister per user architecture Projects that use it: OpenChat, Hot or Not

Global delegations work for our new Vaults architecture is 1 canister per user - we just need to take a couple extra steps pre/post canister deployment to refresh delegations with the new target. 

Privacy: session identities not reused across contexts

True, but I’m not sure how strong this really is given that any wallet could create any number of new addresses, yet people for the most part don’t care to go through the trouble of doing it.

The user should not be aware of the (session) delegation identity... The whole thing should be hidden behind a permission (on the permission request) that states "Allow login to application" (or similar).

The reason I think users should know whether they connect with a global vs anonymous identifier is because a user might expect their assets to be readable by each app, but if they’re not explicitly told they’re connecting to another app as an anonymous identifier, they’d be surprised to see an empty balance. Wallets will have to support both for a unified ICP ecosystem, and users will need to know what to expect.

frederikrothenberger commented 4 months ago

Allows using shared infrastructure Example: Make authenticated calls to hot-key controlled neuron on governance canister from a dapp

This would work with a global delegation as well, just with a wallet prompt requiring a signature, which I would consider a feature in its own right.

Depends on how many interactions you need. The neuron hot-key feature was specifically added because going through transaction approval was deemed too cumbersome (e.g. for voting).

Scales to 1 canister per user architecture Projects that use it: OpenChat, Hot or Not

Global delegations work for our new Vaults architecture is 1 canister per user - we just need to take a couple extra steps pre/post canister deployment to refresh delegations with the new target.

I see. Yes, making it a two step process sidesteps the issue to some extent.

The user should not be aware of the (session) delegation identity... The whole thing should be hidden behind a permission (on the permission request) that states "Allow login to application" (or similar).

The reason I think users should know whether they connect with a global vs anonymous identifier is because a user might expect their assets to be readable by each app, but if they’re not explicitly told they’re connecting to another app as an anonymous identifier, they’d be surprised to see an empty balance. Wallets will have to support both for a unified ICP ecosystem, and users will need to know what to expect.

Yes, I agree with being able to see the balances. But that does not in anyway depend on the delegation type (unless I'm missing something). Because showing the balances of the assets just uses icrc31_get_principals or icrc27_get_icrc1_accounts and then you look up the balances anonymously in the respective ledger. If the balance is not public, you won't be able to access it using the delegation anyway:

For non-public balances / assets you would need icrc49_call_canister to query it with the actual, unrestricted signer identity.

Bringing up this use-case in the context of ICRC-34 causes some confusion on my side, because as I see it, ICRC-34 has no part in that at all. This is why I think, whatever is returned by ICRC-34 is a technical detail related to sessions that should be hidden from the user.

@dostro: Could you clarify, how you would use either delegation in this context?

sea-snake commented 4 months ago

Invalidate sessions

While reading over above discussions, I'm wonder how the following common security features could be covered:

Both these points seems to conflict with one another, keeping a list of sessions would basically centralize this data to a single canister unless you want to keep this list across all canisters and keep them up to date. And you'd definitely don't want to have all your canisters call a central sessions canister on each and every method.

This basically goes back to the old request of invalidating delegations, instead of having multiple session principals, you have a single identity across all devices. And the delegation itself is per device, ideally these can be invalidated at some point by a user.

As far as I understand this would be a challenge with the protocol since it would need to keep a state of early manually invalidated delegations.

Alternatively, I thought about using short lived delegations that renew without user interaction before they expire where only the initial delegation request requires user interaction. But that would not work with mobile + signer interaction, also this would not allow you to block someone with access to the device or be convenient to the end user (not visiting relying party for a day logs you out).

Unless I'm missing something this would be something that could only be resolved on the protocol level (invalidate delegations), thus for now I suppose we can probably keep manual session invalidation out of scope if we can assume we can some day invalidate delegations on the protocol level.

Shared identity across devices

As mentioned above we do want to share a single session identity (from delegation) across devices. Else we brings back above issues with multi canister apps and multiple principals for a single user.

Global vs scoped by e.g. origin

Scales to 1 canister per user architecture Projects that use it: OpenChat, Hot or Not

This seems like a big issue with target restricted delegations, requesting new delegation targets for every single new chat I want to start (= different user canister) doesn't seem like a great user experience. Neither is it something we would want to happen for some wallets but not others.

But to have a delegation identity that does not have a target restriction, that would mean that we must scope it per relying party (e.g. origin) to prevent one relying party making calls directly to another.

Signer principal consistent with delegation principal

Since a wallet could have multiple principals, this could actually make things more complicated for the relying party. Since the relying party has to either create an account per principal or keep a list of principals instead of a single principal for a given account. Which brings back above issues with multi canister apps and multiple principals for a single user.

Mnemonic phrase identities

When I import a mnemonic phrase in wallet X and Y, I expect the same identity due to the standards that define how an identity is created for a given mnemonic phrase. In relation to that, I would expect the session delegation identity to also be consistent as an end user.

Both will need to be standardized first to actually be consistent across wallets. I suppose for now some inconsistency here until that has happened is expected.

TL;DR

I'm starting to lean towards only having relying party scoped delegation identities that are not equal to the global signer identities. Main reasons would be per user canister dapps and 1 identity vs multiple global signer identities per account.

This would basically split the concepts of multiple global signer identities holding assets and session identities for an account login into two separate things.

Calls as global signer identity could still be made with user interaction. The question remains, is there any use case for having delegations for global signer identities to avoid this user interaction that can't be done with a session identity? And does the risk of having bad UX from wallets with per user dapps make it worth having it?

Personally I'm leaning between not taking the risk and between supporting both but with very clear documentation that strongly recommends to use the scoped delegation identities for user accounts. If we were to support both, we'd still need to split them since there is a clear distinction between global with target scoping and session with relying party (e.g. origin) scoping and how that affects the user experience within a relying party.

frederikrothenberger commented 4 months ago

Thanks @sea-snake, you raise a few very important points!

I think having the session identity be different also opens up a host of use-cases that are only possible because of that difference:

I think the two examples above illustrate nicely, why it is beneficial to be able to distinguish sessions on behalf of signer from the actual signer itself. The pattern to go through transaction approval to associate a session with a signer principal with respect to some permissions seems both generally applicable and useful in practice.

Personally I'm leaning between not taking the risk and between supporting both but with very clear documentation that strongly recommends to use the scoped delegation identities for user accounts. If we were to support both, we'd still need to split them since there is a clear distinction between global with target scoping and session with relying party (e.g. origin) scoping and how that affects the user experience within a relying party.

If at all possible I would really advise against supporting features that come with warnings. As such, I'm definitely leaning more towards only supporting derived session principals. But coming from working on Internet Identity, I also do recognize that this might be a biased view. 😉


Regarding the open points:

Per device session that could be invalidated on demand "log out this device / all devices".

I think there is a lot of value in having delegations be verifiable in place (i.e. without checking remotely for revocation). As such, having a non-interactive refresh mechanism paired with short delegation life-times seems to be a practical solution to the problem. But I agree, that is definitely out of scope for ICRC-34. 😉

Multi canister applications need to recognize all these session principals as the same user.

Principals should be derived per application and not per canister. It should be up to the developer to define, what exactly constitutes a single application. Currently, we identify an application by it's websites origin and allowing the flexibility of using 10 additional different origins by declaring them as alternative origins. So far, this has been sufficient for many multi-canister applications.

sea-snake commented 4 months ago

Currently, we identify an application by it's websites origin and allowing the flexibility of using 10 additional different origins by declaring them as alternative origins. So far, this has been sufficient for many multi-canister applications.

Probably makes sense to standardize around this so if more signers were to scope by origin, a relying party doesn't have to implement alternative origins for each signer in a different way.

Besides origin, is there any other identifier of a relying party that could be verifiably used?

Right now I can't think of any other verifiable identifier of a relying party. A mobile app can't verifiably use it's app scheme (e.g. slide://) but that can be worked around by implementing the authentication with an in between web page that has a verifiable origin that calls back to the app scheme.

Also is the origin verifiable with other transport methods like beacon or wallet connect? As far as I'm aware this will probably end up being a user problem of verifying the origin themselves.

frederikrothenberger commented 4 months ago

Currently, we identify an application by it's websites origin and allowing the flexibility of using 10 additional different origins by declaring them as alternative origins. So far, this has been sufficient for many multi-canister applications.

Probably makes sense to standardize around this so if more signers were to scope by origin, a relying party doesn't have to implement alternative origins for each signer in a different way.

If we standardize, then we should probably build something on top of CNS, as the origin is bound to DNS which we ideally would want to move off of. This new system could then, in addition to having compatibility to origin based scoping, specific handling for mobile apps and other contexts.

There is a different working group specifically around that topic and we should get their help to work on this.

sea-snake commented 4 months ago

If we standardize, then we should probably build something on top of CNS, as the origin is bound to DNS which we ideally would want to move off of. This new system could then, in addition to having compatibility to origin based scoping, specific handling for mobile apps and other contexts.

Yeah I think for now we can keep the responsibility of identifying the relying party (preferably in a verifiable way) up to the transport layer and signer. As long as the relying parties are scoped, it doesn't matter how from the perspective of this spec.

So the main requirements of this spec would be:

sea-snake commented 4 months ago

Before I forget I wanted to write down my thoughts regarding Derived identity (DI) and Global identity (GI) from the perspective of a dapp developer.

Difference

With a DI, as a dapp developer I'm not responsible for keeping track of user sessions, I'll always get a delegation for the same identity, which is unique to my dapp.

With a GI, as a dapp developer, I'm also not responsible for keeping track of user sessions, I'll always get a delegation for the same identity, which is not unique to my dapp. Additionally I will need to ask the signer for a new delegation for each new canister I'd like to interact with.

Technical challenge for dapp

From a technical perspective of delegations, a GI has scoped targets and signer interaction is required to add new canisters to this scope. While a DI does not have scoped targets, so that no interaction is needed with the signer and thus can call any canister it wants.

Main issue I see with letting the user decide between DI and GI when connecting to the dapp, is the developer complexity of the dapp. Now suddenly my dapp might need to request a new delegation for each new canister it wants to call in case the user chose for a GI. While with a DI, it never has to ask for a new delegation because of a new canister.

I'd like to avoid introducing such challenges to dapp developers. As a dapp developer I want a single consistent delegation when a user connects to my app. This delegation should always result in the same user experience, primarily avoiding the need to interact with the signer at all times. This is particularly important with multi canister architecture dapps and mobile dapps that can't interact with non custodial wallets in the background.

DI or GI?

Then the question becomes, what should be the default for most dapps (DI or GI)?

Personally I lean towards DI, it requires the least user interaction with the signer. Basically the dapp never has to interact again with the signer until it expires. This makes it work with all possible dapp architectures and mobile dapps.

This means that dapps would be anonymous by default (DI).

A dapp could ask the signer for the global identities, and then the user can then decide to share these (or not). When the user does share these, the user would no longer be anonymous.

A global or anonymous connection connection screen as mentioned earlier would still be possible with this design, when a user picks anonymous, it would always reject any attempt to get the global identities and thus stays anonymous. A user can then at a later date decide to switch to non anonymous within the signer for a given dapp.

These global identities could be required by the dapp e.g. it does not allow certain functionality beyond browsing around without it. This would be expected behavior for e.g. marketplaces due to KYC and the fact that tokens should be held by the signer not the dapp.

In comparison to other chains

On Ethereum we only have global identities (GI), these are the same across all dapps. You do not have identities limited to the scope of a given dapp (DI). This makes data between different services composable, you can aggregate all information for a specific identity across multiple services.

But in practice this isn't completely true, in the example of OpenSea you actually have an account with web2 sessions that for some user actions in web3 requires interaction with the signer. This is done so that you don't need to interact with the signer for every action you take on OpenSea, instead it can call it's backend with the web2 session for basic account interactions. These web2 sessions are managed by OpenSea, and can be created with a web3 global identity proof.

By introducing DI, the complexity of managing these web2 sessions would be removed, the dapp no longer needs to implement it's own session management for all devices the user might interact with it's backend. Instead all of this would be simplified to a single session across all devices managed by the signer instead of dapp.

Composability

The risk of introducing DI by default would be affecting composability, user data across multiple dapps is no longer composable with a single user identity (GI). Every dapp has a different identity (DI) so you can't see which data belongs to which user across multiple dapps.

Though I would argue that this has always been a risk even in other chains, for example OpenSea uses web2 sessions for accounts, it only uses global identities for on chain interaction with tokens. I don't see how this could be avoided in dapps on the IC. Even if only GI is an option (and DI isn't), this would not prevent a dapp from creating it's own session system like OpenSea did.

So the only option I see to maintain composability, would be:

Basically your GI is like your phone number or email on your account. It identifies you as a person across services, it holds assets across services and more. But not all dapps will require a GI, some dapps might not have assets, KYC or public account identifier.

TL;DR

DI moves web2 sessions away from the responsibility of the dapp and puts them in control of the signer. This also greatly simplifies and reduces risk for dapp developers since you don't need to build and maintain your own session system.

At the same time, this introduces the risk that dapps will use DI for purposes that GI should be used for instead. Overall I don't think this is avoidable, developers could always build their own session system as mentioned above.

I think having DI by default with additional GI if the dapp requires it and/or user intends to share it, will simplify and make dapps more secure since they no longer need to create and maintain their own session systems. And I don't think that above risk are avoidable by not having DI by default.

Global delegations are limited by their targets scope, they're only useful for calls to a dapp it's own canisters since no secure dapp would ever allow calls from other dapps without user interaction. Since the canisters called with a global delegation thus belong to the dapp that also has access to the session delegation, these canisters could just allow calls from the session delegation instead.

But it would make it more tricky for the called canisters to link the session identity to the global identity if needed, solutions like signed proofs or a single central canister with a mapping could be more complex than simply making the call with a global delegation. Which would be my main argument for having global delegations besides session delegations.

So regarding this PR, I would opt to keep it mostly "as is" on a technical level but really clarify the different concepts of DI and GI. Make it clear when each should and shouldn't be used as a dapp developer.

frederikrothenberger commented 4 months ago

Thanks a lot @sea-snake for the comprehensive summary! I agree with the conclusion to keep both options open.

But it would make it more tricky for the called canisters to link the session identity to the global identity if needed, solutions like signed proofs or a single central canister with a mapping could be more complex than simply making the call with a global delegation. Which would be my main argument for having global delegations besides session delegations.

I think a lot of that complexity can be abstracted away form devs using tooling and / or reusable components that can be deployed into applications.

I'll take a closer look again in the coming week to see if there are other things that should be clarified before merging this draft.

sea-snake commented 4 months ago

@frederikrothenberger Thanks for the feedback, I'll make updates for these points over the weekend.

sea-snake commented 4 months ago

@frederikrothenberger Made updates for all feedback points. Let me know what you think of these changes :D

sea-snake commented 3 months ago

@frederikrothenberger Fixed the details you mentioned.

sea-snake commented 3 months ago

@frederikrothenberger Above details should be fixed now, naming difference of publicKey and pubkey is there intentionally to stay aligned with existing delegation naming inconsistencies 😅

Interestingly enough, I already noticed this issue of missing public key in the signer-js library but somehow forgot to update the spec...