DNS and frontend trust assumptions for OISY and security-critical ICP dapps

I’m trying to understand the trust model around OISY and other security-critical ICP frontends.

My current understanding is that OISY’s threshold signing removes centralized key custody, but the OISY web origin may still be part of the transaction authorization trust boundary.

For example, suppose someone with control over the oisy.com domain/DNS repointed it so that https://oisy.com served malicious JavaScript (with valid HTTPS).

As I understand the current architecture:

  • OISY authenticates users with Internet Identity and stores a session identity/delegation in the browser.
  • ICP/ICRC transfers can be made by the frontend using that authenticated identity.
  • For Chain Fusion assets, the signer derives/signs based on the authenticated caller principal; the threshold private key isn’t exposed, but an authenticated caller can request signatures.

Would this mean that malicious JS served from oisy.com could, for a user with a valid session - or a user who subsequently signs in - initiate transfers/signatures sending their assets to an attacker without an independent trusted confirmation step?

If not, what security mechanism am I missing?

More generally, I’d be interested in DFINITY’s recommended architecture for users who want asset ownership where no single off-chain party has unilateral ability to cause their assets to move.

I’d also be interested to understand how the DNS side of the major security-critical ICP applications is managed:

  1. Who currently has administrative control over the oisy.com domain/DNS?
  2. Is control of it protected by a multisig/multi-person process, or ultimately held by a single organization/account?
  3. What is the corresponding arrangement for the NNS frontend (nns.internetcomputer.org, and its native nns.icp.net frontend)?
  4. Are these DNS domains in any way governed by consensus, or is their security ultimately dependent on conventional registrar/DNS administrative controls?
  5. Does DFINITY recommend bypassing custom domains entirely, using native canister URLs, hardware/offline signing, or some other approach?

@bjoern or @rbirkner are you able to clarify things?

Related reading:

Hold on, mate.

Let’s make sure anybody can understand the problem statement.

What you’re saying is, even if I build a perfectly decentralized system. If II authentication is tied to a domain name, which is ultimately controlled by a single entity. That could be dangerous?

For example, if somebody for instance, owned the domain name jupiterfaucet.com, they could actually point it at anything they wanted and they could change it at any time?

And you’re saying in theory, for a wallet frontend, that could be a large trust assumption?

Did we get that all right, mate?

Exactly, which is why Jupiter Faucet doesn’t require (or even support) logging into the frontend - you can operate everything directly from the NNS dapp or whatever wallet you already use.

That still leaves the question of DNS centralisation/control of the NNS dapp domain, oisy, or whatever else the typical user is using on the IC.

It’s also very explicit about the domains available and who manages them - https://www.jupiter-faucet.com/#domains

If there is a malicious JS bundle it would just be much easier to play into the tx building itself, build any tx with the signed user not knowing and showing him a happy screen and the signer would just oblige, tx building happens in the browser in this architecture

Technically there would be no need for the user to see anything different in the UI (the JS can just run in the background and transfer all of the user’s assets anywhere without them knowing until they check their balance, or at least once they try to transfer or withdraw themselves).

My understanding is that the responsible thing for any frontend on the IC to do has always been for it to encourage/require the user to switch to the canonical canister ID domain before the user establishes their account (so it’s tied to the canonical domain rather than one that random dev controls). This is what TAGGR does.

But still, the ‘canonical domain’ is still controlled by someone, DFINITY or someone in DFINITY. I’ve been trying to get this discussion moving for a while now, and I’ll be working on something soon where frontend login is sort of needed - so I’d like to get DFINITY’s and the wider community’s stance on the current situation. I think most people are oblivious to this - which needs to change, else it needs fixing.

First I put on my robe and wizard hat…

I think people understand that. In a world where everything is truly decentralized every app will be governed by a DAO. Changes to the front end are verifiably locked behind public proposals, and the Naka for code change is thousands of individuals.

The problem is, that world is delulu land.

Here’s what happens in reality when a person in the real world visits a website and the URL redirects to a random string of characters…. They leave and they don’t come back, ever.

By the way what do you think the Naka for a proposal on the NNS is, Lorimer?

They think it must be eliminated.

They think it is the enemy.

They don’t know that it’s a valuable commodity.

It’s given freely in every interaction.

But once you lose it, it’s gone forever.

Trust nobody they say.

They don’t know what they’re saying.

sometimes its hard to spell words becase they are long thats why i like oisy i can type it every time and my icp is always there every time i log in so that feels good wow thats amazing thanks you oisy

Because so many choose to follow DFINITY, it’s typically 1, but only when people willingly copy the DFINITY vote. You could argue that given that it’s their choice, the NC is actually much higher and based on owned stake rather than followee stake (that’s the standard way of evaluating the NC).

To me that’s a social challenge rather than a technical one. I’m actively working on addressing that challenge - and it’s completely orthogonal to the point of this thread, which is about a technical challenge.

Really it comes down to whether or not the infrastructure is even capable of delivering on its promises and design intentions.

They say you shouldn’t trust anybody.

But you can usually trust anybody.

It’s everybody who is really the problem.

I wouldn’t trust them if I were you.

If you want to be safe trust nobody.

You can always trust nobody,

Because nobody listens.

And nobody cares.

I don’t think this is true at all.

It’s really hard to know with Dionysus. Sometimes I think he’s telling the truth, but every time I trust him he ends up letting me down.

Do you know what I mean?

i know what you mean danisus is like a box of choclats becase you just dont know what your gonna get i dont think anybody knows

Bumping

You should build something like Icahn and save the internet.

i dont know what bumping is but it sounds funny my momma always said no bumping in the house but now im older and me and jenny are like peas and carrots thats all i have to say about that

Related - OhShii Labs on X: “:warning: If someone buys the https://t.co/k5dBEECLc8 domain, do NOT log in with Internet Identity on https://t.co/k5dBEECLc8 or https://t.co/T724LUq7aW. The domain expired on September 3rd and currently no longer serves the app. Both apps are still live on-chain at their native links:” / X

Nevermind the fact that the original domain owner could have unilaterally stolen user funds at any point… :man_shrugging: Clearly users haven’t had the limitations of the current technology adequately communicated to them. @marc0olo are you able to get DFINITY’s stance on the questions above?

@Lorimer, in my opinion you’re not missing some hidden security mechanism here. There isn’t one.

DFINITY’s own security guidance actually documents this exact scenario. I think the more useful thing is to make the OISY case concrete rather than keep talking about it as a hypothetical, because the repository shows something stronger than what has been assumed in this thread.

1. The documented position

The Security best practices → Decentralization / Canister control page is pretty explicit about this: applications are often accessed through a custom domain registered with a DNS registrar by one of the developers. That developer can point the domain to a completely different web application, potentially not hosted on ICP at all, and users will still trust whatever is served from that domain.

The same documentation points out the important Internet Identity part: the session key for that origin is stored in the browser, so malicious JavaScript served from the same origin can control that session and anything the session is allowed to do.

So when you ask “what am I missing?”, my answer is basically: nothing. This is a known centralisation point. The problem is that most users are never really told that the trust model works this way.

2. Native canister URL vs custom domain

There is a very simple solution if the goal is to avoid this particular class of problem: use the native canister URL and don’t put a custom domain in the derivation path.

I don’t think we should pretend this is some unsolved problem.

There is still a tradeoff, obviously. A native canister URL is ugly, harder to remember and worse for UX. But from a security perspective, it removes the per-application DNS dependency.

With a custom domain, the binding depends on DNS records controlled by whoever owns the domain: a CNAME to <domain>.icp1.io, a TXT record _canister-id.<domain> containing the canister ID, a CNAME for _acme-challenge.<domain> to .icp2.io, plus the .well-known/ic-domains file in the canister.

The canister file proves that the canister agrees to serve that domain. The TXT record proves that the DNS owner agrees. Those are two different things.

Change the DNS records and you can send users somewhere else. You can even remove the ICP records completely, point the domain at another server and obtain a valid certificate for the same domain. At that point ICP isn’t involved anymore. There is no consensus, no proposal and no on-chain record saying “this domain changed hands”.

With the native canister URL, the hostname itself contains the canister ID. If you want to serve a different application there, you actually need control over that canister. That’s a meaningful security improvement.

There is still a residual trust assumption, because response verification happens at the HTTP gateway rather than directly in the browser. The gateway checks the BLS signature and witness, and the browser receives the already verified response. So users still rely on DNS and TLS for the shared icp0.io / icp.net namespace, as well as the gateway implementation. But that’s a very different situation. With a custom domain, every application has its own off-chain DNS trust point. With a native canister URL, the DNS namespace is shared and much more constrained, while the canister ID is actually part of the thing being certified.

And yes, raw. bypasses certification entirely, with allow_raw_access defaulting to true. That’s another thing worth keeping in mind.

3. The OISY case

Looking at dfinity/oisy-wallet on main, src/frontend/src/lib/constants/app.constants.ts contains:

const IS_ICP_DOMAIN_URL = DOMAIN_URL_HOSTNAME.endsWith('.icp0.io');

export const AUTH_DERIVATION_ORIGIN =
  IS_SIGNER_DOMAIN || BETA || (PROD && IS_ICP_DOMAIN_URL)
    ? STAGING ? 'https://tewsx-...-cai.icp0.io' : 'https://oisy.com'
    : ...;

Then src/frontend/src/lib/utils/auth.utils.ts passes that derivationOrigin whenever the current origin appears in VITE_AUTH_ALTERNATIVE_ORIGINS, and scripts/build.ii-alternative-origins.mjs writes .well-known/ii-alternative-origins from the same variable.

SIGNER_DOMAINS.md also says it directly: the different domains derive identity from the same canonical origin as the main wallet, and production uses https://oisy.com.

So oisy.com isn’t just a nice front door for OISY; It is the principal derivation root.

The native canister URL is configured as an alternative origin of the custom domain, not the other way around. A user who deliberately opens cha4i-riaaa-aaaan-qeccq-cai.icp0.io can still end up with a principal derived from https://oisy.com.

That’s why imo the usual advice of “just establish your account from the native canister URL, like TAGGR does” doesn’t really apply to OISY. There isn’t a separate canister-derived account to move to.

If someone controls the domain, they don’t necessarily need to break Internet Identity itself. They can serve malicious JavaScript from the trusted origin.

There is another important detail here. When no derivationOrigin is supplied, II derives from event.origin directly, and II doesn’t require that origin to be hosted on ICP. So malicious JavaScript served from https://oisy.com, with a perfectly valid certificate, can request the same derivation origin as the real OISY.

That means the resulting principals are the same. Same backend accounts, same chain-fusion addresses.

To be fair, i think this is clearly a deliberate UX decision. It allows the signer subdomains and the PWA to behave as one wallet instead of creating multiple identities. And the canister side isn’t just sitting there uncontrolled. Production upgrades go through an Orbit station approval process with reproducible Docker builds and reviewer validation scripts, as described in HACKING.md and SIGNER_DOMAINS.md.

That’s actually the interesting asymmetry:

Changing the application code requires an on-chain governance action. Changing where oisy.com points requires access to a registrar or DNS account.

so those are very different security models.

a smaller thing also caught my attention while looking through it.

IS_ICP_DOMAIN_URL checks .icp0.io specifically. That’s probably worth another look now that icp.net is the default gateway domain for new frontend canisters.

4. The “obvious” fix

There is a tempting configuration change here: make the canister origin the derivation origin and make the custom domain the alternative origin.

But there is one important caveat; simply switching an existing application from a custom domain to its native canister domain as the derivation origin is not a trivial fix. The derivation origin is part of the Internet Identity principal derivation. Changing it means that existing users can end up with different principals, even if they authenticate with the same Internet Identity anchor. For an application that already has users and assets associated with those principals, that can turn into a significant migration problem.

So I wouldn’t suggest that existing dapps should simply change their derivation origin overnight.

For new applications, however, I think this is something that could reasonably be documented as a security best practice.

If a dapp chooses to use Internet Identity alternative origins, the native canister domain should preferably be used as the canonical derivation origin, with custom domains configured as alternative origins.

That way, the application’s identity is rooted in the canister rather than in a DNS-controlled domain from the beginning. The custom domain can still provide the UX and branding people want, but losing or compromising that domain doesn’t become equivalent to losing the application’s identity root.

For existing applications already using a custom domain as their derivation origin, the situation is more complicated because changing it can change every user’s principal. That is exactly why this is much easier to get right at the architecture stage than to fix later.

By the way, unfortunately that doesn’t actually solve the problem.

If oisy.com remains an accepted alternative origin, an attacker controlling oisy.com can still serve a page that points back to the canister origin. II can fetch /.well-known/ii-alternative-origins from the canister, see that oisy.com is an allowed origin, and accept it.

So the real boundary is not the order in which the origins are configured.

If a DNS-controlled domain remains part of the derivation set, control of that DNS remains equivalent to control of the identity derived from that origin.

Removing the custom domain entirely is what restores the stronger property.

That is why I don’t think this is something you can solve with one more configuration flag inside OISY. You either use only the native canister origin, or you need something outside the normal application configuration, such as a client that pins the canister ID, an extension/native application, a self-hosted gateway, or some additional canister-side security policy that makes possession of a frontend session insufficient by itself.

For example, spending limits, withdrawal allowlists, timelocks or a second factor bound to a key that the frontend never controls.

Those approaches make the security property independent of the hostname.

One practical thing for developers: if a Web2 domain is about to expire or you’re no longer going to control it, remember to remove it from the Internet Identity alternative origins first. Otherwise, whoever acquires the domain later may still be able to use it as an accepted origin.

5. DNS hijacking is another problem

There is also a related issue that I think deserves to be separated from simple domain expiry or someone intentionally changing the DNS.

Even if the legitimate owner renews the domain and never decides to repoint it, DNS itself can become the attack surface.

If an attacker compromises the registrar account, DNS provider account, a delegated DNS provider, or some other part of the DNS management chain, they can potentially hijack the domain without ever compromising the ICP canister.

From the user’s perspective, nothing necessarily looks broken.

The URL is still oisy.com.

The TLS certificate can still be valid.

The browser shows the same domain.

The attacker simply controls what that domain resolves to.

This is one of the reasons I had actually thought about building a small “DNS Guardian” in the past. The idea was basically to continuously monitor the DNS records of important ICP domains and compare them against an expected on-chain/canister configuration, with alerts when something changes. I never had the time to take it anywhere though.

And honestly, I’m not even sure there is a product there that people would pay for. It would probably require continuous inter-canister calls and monitoring infrastructure, and “DNS Guardian for paranoid ICP users” isn’t exactly the catchiest product pitch.

But for this particular threat model, being a little paranoid is not completely irrational :sweat_smile:

The interesting thing is that DNS changes are one of the few parts of the whole chain where you can have a perfectly valid ICP application on one side and still silently redirect users somewhere completely different before they ever reach it.

6. Regarding the questions

Questions 1 to 4 are ultimately things DFINITY would need to answer, I would support asking them.

I’d probably add one more:

Would the custom-domain service consider supporting an on-chain-attested canister/domain binding instead of relying on a DNS TXT record that the domain owner can change at any time?

That wouldn’t solve every DNS problem, but it would at least move the binding itself into something that ICP can attest to.

And regarding the example you linked directly, yes, an expired domain is basically the same failure mode with a different trigger.

Pointing users to the native canister URL after the domain is lost is a useful mitigation, but it isn’t a design that prevents the problem in the first place. The domain didn’t need to be maliciously compromised. It simply expired. That alone created a period in which somebody else could potentially register it and inherit the origin that users had been using for Internet Identity and that’s the part I think users should understand.

7. Why the native domain is genuinely safer on ICP

I initially described the native canister URL as a reduction in risk rather than an elimination of it. That’s technically correct, but I think it misses the most important point about why ICP is different here.

The canister ID participates in the cryptographic verification chain. A domain name doesn’t.

Every certified response contains an IC-Certificate. According to the HTTP gateway protocol specification, that certificate is signed by the NNS root key or by a subnet delegation ultimately signed by that root key.

The interface specification makes the delegation model even more explicit: delegations are scoped using canister_ranges, and the relevant canister ID has to fall inside the range.

So when you see:

cha4i-riaaa-aaaan-qeccq-cai.icp0.io

the canister ID isn’t just an arbitrary string in the URL. It is a checkable part of the certification chain.

Given the NNS root key, you can verify that the response was certified for that particular canister.

oisy.com doesn’t appear in that certificate, the state tree or an NNS registry entry.

Its binding comes from DNS and WebPKI.

The .well-known/ic-domains file doesn’t change that. It says that the canister is willing to serve that domain. It doesn’t prove that the domain currently resolves to that canister.

On the normal web, the domain is the best identity we have, so everyone builds around it. On ICP, there is the canister ID. Using a custom domain effectively puts a Web2 name in front of a Web3 object that already has a cryptographically verifiable identity.

If the goal is maximum security, the simplest answer is therefore also the least attractive one from a UX perspective:

use the native canister domain.

If you want the custom domain, you’re choosing convenience and branding in exchange for an additional off-chain trust assumption.

8. Certification doesn’t protect you if DNS sends you somewhere else

This is another point that gets mixed up quite often.

People say “but ICP responses are certified”.

Yes, they are.

But certification doesn’t prove that DNS sent you to the correct canister.

The gateway can correctly verify a response from the canister it was asked to reach. If DNS has already been changed so that oisy.com points somewhere else, you’re no longer asking the gateway for the legitimate OISY canister.

The attacker can serve a completely legitimate ICP canister from their own infrastructure. The response can be correctly certified, the certificate can be valid, the canister can really exist on ICP.

None of that tells you that oisy.com was supposed to point there. So the certification mechanism protects the response. It doesn’t magically make DNS trustworthy. This is also why the native canister URL matters.

9. The trust set is different

This is probably the simplest way I can describe the difference.

With a custom domain, the things that can affect what users receive include the canister controllers, the registrar, the DNS provider, potentially whoever compromises those accounts, and the WebPKI/ACME side of obtaining certificates.

With the native canister URL, you remove the per-application registrar and DNS provider from the equation.

You still trust the gateway namespace and its DNS, so I wouldn’t call it trustless.

But the failure mode is different.

A compromise of one application’s custom domain can quietly affect that application.

A compromise of icp0.io or icp.net would potentially affect the whole ICP ecosystem at once. That’s obviously a huge target, but it is also a shared, highly visible target. Changes are much harder to hide.

That’s not “decentralised DNS”.

It’s a smaller and much more observable trust assumption.

And importantly, canister controllers are at least something you can inspect. Under SNS, NNS or an Orbit station, code changes can be associated with governance actions and wasm hashes.

A registrar login doesn’t have that property.

10. Why this matters even more with Internet Identity

This matters even more when the domain is used as the Internet Identity derivation origin.

A principal derived from the native canister origin is tied to that canister, while a principal derived from https://oisy.com is tied to that web origin. If someone gains control of the domain, they don’t need to break Internet Identity or steal a seed phrase. They only need to become the origin the browser believes is https://oisy.com.

Because OISY is a wallet, that’s not just an application routing issue. The origin is part of the identity users use to access their assets.

The practical attack still requires users to reach the compromised origin.

If they already have a valid session, malicious JavaScript may be able to use that existing session without another login. If they don’t, they may simply go through the normal Internet Identity flow, seeing the same domain, TLS certificate and potentially even application metadata they normally expect.

There is also a more subtle point in the II implementation: certification protects the response after the canister has been resolved, but resolving a custom domain to a canister can still involve the domain itself. So if DNS is compromised, the pointer to the canister can potentially be compromised before certification even enters the picture.

This makes the attack more of a harvesting window than an instant private-key-style compromise. The attacker needs users to arrive, but the user may receive very little indication that anything has changed.

That’s why using the native canister domain as the canonical derivation origin is the cleaner security model for new applications. Custom domains can still be used as alternative origins, but then DNS becomes part of the security boundary.

And that’s really the question here: ICP can certify that a response came from a particular canister. It can’t, by itself, tell the browser that DNS sent it to the right place in the first place.

I’d be happy to see any corrections or clarifications from DFINITY here, as I may have misunderstood or overlooked some details in the Internet Identity implementation. In any case, I wouldn’t consider what I’ve described an open vulnerability in OISY. It’s ultimately a design choice: OISY chose a custom domain as its canonical derivation origin, and that comes with a different trust model. Whether that trade-off is worth it is a matter of security vs. UX, and reasonable people can disagree on it.