Internet Identity: how can a user recover the principal for a specific web app?

Example: say there is web app wallet hosted on the IC at .icp0.io and a user uses it with II. Then the wallet funds will be held by the unique principal that the II generates for the given origin (domain). Say the user still has access to the II but the web wallet has disappeared. For example, the canister ran out of cycles or was overwritten. Then what exactly are the options that the user has to recover the funds?

I understand that the web wallet could be set up on another canister and there are ways to configure alternative origins so that two domains give the same principal. But I suppose that requires control of both domain, doesn’t it? Are there any options for recovery if the owner of original canister id is not cooperating?

6 Likes

I can think of hacky ways, like tweaking the local host configuration and booting a local SSL server so that localhost becomes the non-cooperating domain name, and running a custom dapp that would perform the required actions.

Another hack would be to use the browser debugger on the non-cooperating domain, assuming the related dapp is still live, to run some custom code by fetching libraries from CDNs.

In both cases, these are advanced workarounds that would require code, and I’m not sure if they would work.

Yes, it requires control and also a configuration file that needs to be deployed at the root. Moreover, it is worth noting that at the moment, you cannot use an alternative origin from a domain that is not ic0.app|icp0.io. So in your scenario, where the wallet would, for example, be hosted on hello.com, you cannot derive an origin from it to world.com.

Agree that works fine. For example, on Juno, each developer gets one canister to rule all their projects, and their address to send ICP is not their principal derived by the domain name but rather this particular canister ID. This canister can be managed by the developer only, and they can also add additional controllers to it.

3 Likes

Can I have two origins, A and B, where both are canister domains on icp0.io, and B is an alternative origin for A? And they can be asset canisters serving different frontends but users get the same principal when logging in at them? And if A is the canonical origin and B is the alternative origin and I want to add C into the group then I need control of A, not B?

There was a mismatch in my answer. It is not icp0.app|internetcomputer.org but ic0.app|icp0.io.

Precisely ^https:\/\/[\w-]+(\.raw)?\.(ic0\.app|icp0\.io)$ (documentation)

Yes.

Yes. Users get principal derived from A in this scenario, therefore yes you need to control A.

Note btw. that you can add up to max. 10 derivation origin I think.

Would it be possible to write a canister A in a way that the user can specify the id of B on a per-login basis?

Not sure what you mean.

I’m curious: Is it possible to calculate the principal for a specific identity based on its associated domain?

A principal is basically a public key so, I would assume you would have to know the private key as well, not just the domain but, guts feeling. @frederikrothenberger can probably answer this better than me.

1 Like

Nevermind. The answer is likely “no”. I suppose the II looks in certified assets provided by canister A to see if B is configured as an alternative origin. My question was if the mechanism was dynamic and flexible “enough” so the we could craft a canister A which keeps a list of alternative origins per user. So that the user with principal P can call A and define his own alternative origin B. And then if II can check if the dynamically configured B for user P matches the origin where the user attempts to log in at. If that was possible then the user could recover from the disappearance of B by permissionlessly configuring a new B.

Gotcha. Indeed II looks for a ./.well-known/ii-alternative-origins (e.g. https://nns.ic0.app/.well-known/ii-alternative-origins) file in canister A that contains a defined list (max 10 entries) of known hosts that are allowed for derivation.

It would work if the user had his own personal A in which he configures B and if B disappears then configures a new B.

@timo: This seems like a misuse of the alternative origins feature.

Rather than having a lot assets stranded in no longer maintained applications (and their related principals), I think we should move to a different model of asset management entirely: I.e. have the user provide a principal (preferrably fully under the users control) and provide that to the dapp to handle valuable assets using transaction approval.

See also the work done by the Identity WG on the signer standards: wg-identity-authentication/topics/signer_standards_overview.md at main · dfinity/wg-identity-authentication · GitHub

1 Like

Happy New 2024 Yeras Cryptolovers :santa:

In my application the relying party would be a wallet frontend (hosted in a canister) and the target canister would be a token ledger (or multiple ones). The wallet is an every-day wallet, not a large-value store, so going through the signer for single-transaction approvals does not seem feasible. Neither from the user perspective, having the extra context switches, nor from the developer perspective, having to make the signer understand every single kind of canister interaction that the wallet wants to do. Hence, it would boil down to the signer signing a delegation to the wallet frontend so the wallet can act on the signer’s behalf. At this point the whole approach becomes similar to the alternative origin approach, the signer representing the canonical origin and the relying party the alternative origin. The difference would just be the the last step in the delegation chain is created in canister code instead of by II. Do I understand that right?

The wallet is an every-day wallet, not a large-value store, so going through the signer for single-transaction approvals does not seem feasible.

Could you elaborate further on why that is? The signer interaction would not make a transaction more cumbersome, rather the confirmation screen (that is common anyway) will be shown by the signer instead of the wallet front-end.
The idea is that you would use the signer interface even for small amounts, i.e. raise the base-line security for transactions in general.

I do concede that it might be a tad slower because the signer needs to load. But I hope we can address most of that with better caching.

having to make the signer understand every single kind of canister interaction that the wallet wants to do.

That’s the nice thing about ICRC-21 and ICRC-33. Together, they allow calling canisters that the signer does not have a specific integration with, while still presenting the user with a meaningful consent screen.

So supporting many ledgers (or any other canister for that matter) would not increase effort on the dev side. There is an initial effort to introduce another agent for transaction approval, but after that it’s just like using agent-js. Or at least that’s the goal. :wink:

Hence, it would boil down to the signer signing a delegation to the wallet frontend so the wallet can act on the signer’s behalf.

Without restrictions, this is extremely dangerous. To be secure, either we would need to derive different principals, just as II does (and you end up with the same issue), or it has to be scoped to specific canisters, as suggested here, which would not allow interactions with ledgers.

How exactly would that look like in a browser? Say for example I want to use the signer with II. I suppose the app would in one tab and the signer would open in another tab, then I log in with II and confirm. Maybe the signer can stay open for subsequent confirmations. For the user there remains a switch between tabs, which I want to avoid. Can the signer appear in a pop-up that is not a tab? Can it be in an iframe? Not sure how the user can verify that he is indeed interacting with the signer. Is it possible to make this all seamless?

Suppose we could make it as seemless as the typical Ok-Cancel confirmation pop up windows that are presented directly by an OS. The ones that appear on top of all other windows and which I can confirm just by a key-press on Return. I would argue it is still too much of an interruption to the user. Think of a file system explorer where you drag and drop files around and you have to confirm every single move. In my application there are many task that appear to be more of “administrative” nature than to be an actual transfer and they all have to be signed. Open a subaccount, create an allowance, adjust an allowance in balance or adjust its expiration date or delete it, transfer between own subaccounts, change the mapping from allowance to subaccount (possible in HPL), etc. Moreover, in the case of HPL even queries have to be signed. Refresh your balances, find allowances given to you by others, etc. require signatures. Non-interactive balance updates in the background would not be possible in the signer model for that ledger.

ICRC-21 requires support by the target canister, here the ledgers. I have an ICRC-1 wallet that works for all ICRC-1 tokens. Not all of them have ICRC-21 built in and some will never have. With the signer model I can now longer support them.

ICRC-21 requires an additional call to the target canister, turning one call into two. This adds another 3 seconds of latency to the whole transaction. In HPL for example I am going to great length to shave off even 1 second of latency. Adding 3 would not be acceptable. The two calls also add load to the target canister. If I want to design a system that can receive 10k ingress requests per second from end users, now I have to design it for 20k. It essentially doubles the cost of the whole system by doubling the required number of subnets. I understand that the additional update call will be replaced by a replicated query in the future by I guess that does not completely remove the effects on latency and load.

With a direct call the wallet frontend can submit the call and the user can already do the next thing while the wallet is polling for the response in the background. With the signer model the signer would be polling. I am not sure how I would do that to let the user continue in the actual app while the signer is polling and then inform the user if something went wrong.

Overall, the signer model seems to make sense if I want to try out a new wallet that I don’t trust on the assets that I usually manage with my trusted wallet. But for a reference wallet that is supposed to attract newcomers I don’t think it is the right model.

The trust assumption for my case is as follows: the user trusts a specific open-source wallet version. This means if a new version appears the user does not want to be forced to use it (trust it). Switching to a new version should be opt-in. So old versions need to be kept available. Stronger speaking, the user must be protected against the wallet or specific versions of it disappearing.

In conclusion I am left with the options to use a delegation, or an alternative origin, or to not use II.

I wanted to have a specific signer only for this one wallet application. It would be scoped to the application, not to the target canisters. It would be used with different versions of the wallet application, but not with any other apps.

The signer interaction would not make a transaction more cumbersome, rather the confirmation screen (that is common anyway) will be shown by the signer instead of the wallet front-end.

How exactly would that look like in a browser? Say for example I want to use the signer with II. I suppose the app would in one tab and the signer would open in another tab, then I log in with II and confirm. Maybe the signer can stay open for subsequent confirmations. For the user there remains a switch between tabs, which I want to avoid. Can the signer appear in a pop-up that is not a tab? Can it be in an iframe? Not sure how the user can verify that he is indeed interacting with the signer. Is it possible to make this all seamless?

Suppose you signed in using Internet Idenitity, then whenever you need to approve something II would appear in a pop-up showing the transaction approval screen directly (as II should remember the previous interaction, so no need to select identity again). Once approved the call will be executed an the pop-up will close again.

Think of a file system explorer where you drag and drop files around and you have to confirm every single move. In my application there are many task that appear to be more of “administrative” nature than to be an actual transfer and they all have to be signed. Open a subaccount, create an allowance, adjust an allowance in balance or adjust its expiration date or delete it, transfer between own subaccounts, change the mapping from allowance to subaccount (possible in HPL), etc. Moreover, in the case of HPL even queries have to be signed. Refresh your balances, find allowances given to you by others, etc. require signatures. Non-interactive balance updates in the background would not be possible in the signer model for that ledger.

Note that transaction approval is intended for security critical transactions and not for mundane things. So I would recommend a more measured approach here and use the delegation in conjunction with the signer integration. I.e. have two principals that have access to the same HPL ledger account:

  1. the delegation principal: it should be restricted to only allow administrative tasks. I.e. the following actions would not be allowed using the delegation identity:
    • Increase allowance of an external party to receive additional funds.
    • Transfer funds directly to an external party
    • Anything else that would allow an attacker to bypass transaction approval to gain tokens
  2. the signer identity: unscoped identity, but requires transaction approval

That way, users are still protected with transaction approval against someone trying to steal funds, but can do the administration in a fast and non-obtrusive way. After all, it is still a financial application so I think having extra security is warranted even if it adds 2 seconds for the pop-up to load.

Regarding ICRC-21 support: yes, it needs to be seen if it gains enough traction to be widely adopted.

The trust assumption for my case is as follows: the user trusts a specific open-source wallet version. This means if a new version appears the user does not want to be forced to use it (trust it). Switching to a new version should be opt-in. So old versions need to be kept available. Stronger speaking, the user must be protected against the wallet or specific versions of it disappearing.

In that case, the user must own the front-end assets. I.e. have one front-end canister per user. You could do the following: have an index / orchestrator canister, with a very basic front-end that redirects users to their respective front-end canister and provisions new ones for new users. Users also need to be able to access their front-end canister directly.

Whenever a new version of the front-end is published, the user front-end canister prompts the user to do the upgrade. Only the user (the controller) can authorize that and change the assets. In a sense, the upgrade model would then become similar to that of native applications where users have to install code explicitly on their machine.

I wanted to have a specific signer only for this one wallet application. It would be scoped to the application, not to the target canisters. It would be used with different versions of the wallet application, but not with any other apps.

This is exactly what II does, no? It gives you a principal scoped to a specific application (URL). So it is stable across versions but only available to that particular dapp.

I see, so the signer will be integrated into II. Makes sense, I thought at first that it would be a separate thing.

If the user has his own frontend canister then indeed the problem with the changing principal goes away.

What if the index/orchestrator redirects to one of multiple (for multiple versions) fixed frontend canisters that are not per-user, that each exist only once for all users. Say the index/orchestrator is immutable, blackholed. That’s where the principal comes from when the user logs in with II into the orchestrator. Than the orchestrator passes the session key to the frontend that the user has chosen (or signs a delegation to another session key that the chosen frontend has generated).

What if the index/orchestrator redirects to one of multiple (for multiple versions) fixed frontend canisters that are not per-user, that each exist only once for all users. Say the index/orchestrator is immutable, blackholed. That’s where the principal comes from when the user logs in with II into the orchestrator. Than the orchestrator passes the session key to the frontend that the user has chosen (or signs a delegation to another session key that the chosen frontend has generated).

Yes, that works too. However, you will be stuck with the issue that if the orchestrator goes away, the user won’t have access to the identity anymore.

Also, the orchestrator should probably not be blackholed as it integrates with external services, which will almost certainly change over time.

Why is there a restriction on only allowing canister ID’s as the canonical origin? Is there plans to get rid of this, seems it would be useful to choose your origin?