nostr-protocol / nips

Nostr Implementation Possibilities
2.39k stars 582 forks source link

NIP-05 should allow redirections #1544

Open darioAnongba opened 4 weeks ago

darioAnongba commented 4 weeks ago

Hi @fiatjaf,

The NIP-05 specification contains the following security constraint:

The /.well-known/nostr.json endpoint MUST NOT return any HTTP redirects. Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint.

I would like to discuss why this constraint may not be appropriate in the context of NIP-05 and propose allowing redirections. Additionally, I will explain how preventing redirections diminishes the ease of implementation.

The problem

NIP-05 prohibits HTTP redirections by specifying that clients must not follow 301/302 redirects from the /.well-known/nostr.json endpoint, citing security concerns.

The security rationale

The NIP does not explain the specific security risks associated with allowing redirects. Generally, preventing clients from following redirects is intended to mitigate risks such as users being redirected to unexpected or unsafe locations if a domain is compromised.

However, in the context of NIP-05, clients trust the domain owner to provide the mapping from the Nostr identifier to the public key. The goal of NIP-05 is to facilitate this lookup, not to verify the authenticity of the information provided. Therefore, if a domain is compromised, disallowing redirects does not enhance security, as the trust has already been broken. A malicious domain could provide incorrect mappings regardless, and since there is no central authority to verify the information, the client’s trust in the domain is fundamental.

Reverse proxy VS HTTP redirection

In the context of NIP-05, HTTP redirects are functionally equivalent to reverse proxy configurations. The primary difference is that with reverse proxies, the user is unaware of the redirection and remains on the same domain, whereas with HTTP redirects, the client is informed to fetch the resource from a different location. Disallowing HTTP redirects while permitting reverse proxies seems unjustified, as both achieve the same result.

A reverse proxy configuration might look like this, effectively allowing redirects for Nostr addresses:

server {
    server_name mydomain.com;

    location / {
        proxy_pass http://api.mydomain.com;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

However, many modern deployment platforms like Vercel or DigitalOcean App Platform do not allow configuring reverse proxies directly. Developers using these platforms find it easier to implement an HTTP redirect, such as in Next.js:

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/.well-known/nostr.json',
        destination: 'https://api.mydomain.com/.well-known/nostr.json',
        permanent: true,
      },
    ];
  },
};

Preventing HTTP redirects forces developers to implement workarounds to intercept and forward the request to the backend, which is essentially the purpose of HTTP redirects.

Comparing with Lightning Address

Notably, most wallets that implement Lightning Addresses—such as Wallet of Satoshi, Alby, Breez, Blink, Phoenix, and others—allow HTTP redirects, despite the protocol having arguably higher security risks due to its transactional nature.

In practice, Lightning Address servers are often deployed on subdomains different from the root domain. For example, BTCPayServer instances are frequently hosted at pay.mydomain.com, and companies like Numeraire or Alby expose their APIs at api.numeraire.tech or api.getalby.com. There's even a tutorial in the BTCPayServer documentation recommending developers implement a redirect for LN Addresses here.

Proposed Change

NIP-05 should permit HTTP redirects, especially redirects to subdomains. Allowing HTTP redirects would simplify implementation for developers using platforms that do not provide access to reverse proxy configurations. Permitting redirects in this context would not increase the attack surface for NIP-05, as clients already trust the domain to provide accurate mappings.

vitorpamplona commented 4 weeks ago

For reference, NIP-05 redirects because of this discussion https://github.com/nostr-protocol/nips/issues/127 and this PR https://github.com/nostr-protocol/nips/pull/128

darioAnongba commented 4 weeks ago

Thanks for the reference, I see that the rationale leading to the decision is not very solid. It is somehow trying to prevent DDoS by forbidding redirects, but:

  1. You can redirect with reverse proxy to an external server:

    server {
    listen 80;
    server_name mydomain.com;
    
    location = /.well-known/nostr.json {
        proxy_pass https://mydifferentdomain.com;
        proxy_set_header Host mydifferentdomain.com;
    }
    }
  2. To prevent a high amount of requests to a server, you usually implement a caching mechanism, respected by most CDNs. The nostr.json is not supposed to change often so just cache for some time. The requests won't even hit your server:

    'Cache-Control', 's-maxage={seconds}, stale-while-revalidate={seconds}, stale-if-error={seconds}'
  3. Allowing redirects to same subdomain should at least be allowed since I am not targeting domain that I do not control. But again, reverse proxy redirects break this argument.

  4. It is not a security issue as there is no security involved in NIP-05. There is no risk of hacking, nor funds are risk and the data is public by definition.

  5. It is a subjective matter. Some servers might actually like being pointed to and provide the NIP-05 service to others.

fiatjaf commented 4 weeks ago

Notably, most wallets that implement Lightning Addresses—such as Wallet of Satoshi, Alby, Breez, Blink, Phoenix, and others—allow HTTP redirects, despite the protocol having arguably higher security risks due to its transactional nature.

Lightning Address initially didn't support redirects because redirects are dangerous and insane, but people started implementing redirect support and the protocol had to bend. Arguably UX was greatly favored there because Lightning Address URLs are inherently dynamic -- i.e. you had to have a server there on your /.well-known path generating invoices, and that made things difficult for most people who were self-hosting their Lightning Address on their own personal domain, so it made sense for them to redirect to another place. NIP-05 has no such problem since it's super static and you just need a file or a very simple handler to return your pubkey.

fiatjaf commented 4 weeks ago

What is your goal here? Why do you need redirects? Why can't you just return your desired response from a Next.js API handler? If you don't have the user information in there you can fetch it from the other place that has the information at runtime and return it in any way you want, you can probably even just do something in a single line, like return await fetch(...), effectively proxying the request but without requiring nginx or anything.

It is somehow trying to prevent DDoS by forbidding redirects, but: You can redirect with reverse proxy to an external server

Proxying a request is different than returning redirect responses. In the redirect case the target server will be hit by multiple HTTP requests from many different clients. In the proxy case all requests will be coming from your IP.

darioAnongba commented 4 weeks ago

Hi @fiatjaf, happy to see you joining the conversation

Lightning Address initially didn't support redirects because redirects are dangerous and insane

This is subjective and not an argument for protocol specification as it doesn't explain why it would be dangerous or insane in the context of NIP-05.

Lightning Address URLs are inherently dynamic -- i.e. you had to have a server there on your /.well-known path generating invoices

The redirect of the .well-known for LN addresses is mainly for aesthetic reasons as the response of the well-known endpoint includes a callback field that is the one pointing (maybe) to an outside domain for invoice generation. Thus, LN addresses are as static as NIP-05 and there is actually no redirection involved for invoice generation as that is performed on the second call. The data returned by the .well-known on LN addresses can be cached and reused.

NIP-05 has no such problem since it's super static and you just need a file or a very simple handler to return your pubkey.

I would argue that the vast majority of Nostr users use a service for NIP-05, among my contacts, I see countless nostrplebs, primal.net, getalby.com, etc. Only few own a domain name that they use to return a static nostr.json file for their own identifier.

What is your goal here? Why do you need redirects? Why can't you just return your desired response from a Next.js API handler?

My goal is to redirect the calls from mydomain.com/.well-known/nostr.json to api.mydomain.com/.... "Redirection" is the correct term. What you are suggesting is intercepting the request, fetching the data and returning it with or without alteration. This is what I implemented as a workaround but it's not a redirection and it's now part of the source code of the NextJS app. Whether it is possible to implement this workaround with a handler in my specific case for NextJS is not part of this conversation.

Proxying a request is different than returning redirect responses. In the redirect case the target server will be hit by multiple HTTP requests from many different clients. In the proxy case all requests will be coming from your IP.

The specification defines how the server behaves when receiving a request and how the client does when sending one. The specification is agnostic to the number of requests or the number of different clients the server allows. You could add a recommendation like "Servers SHOULD return a Cache-Control header ...`.

My point is that, similarly to firewalls, it's good to start by closing the doors and slowly open them if there is a clear use case or advantage. Like to LN Addresses, nothing inherent with redirects increases the attack surface and allowing them increases UX and ease of development.

fiatjaf commented 4 weeks ago

It looks like you don't really have a justification for this change since you are already doing what the NIP specifies and proxying requests in your server (with no harm to yourself or anyone) and you have cited no other use cases that would need the client-side redirects.

darioAnongba commented 3 weeks ago

I think I gave some justifications:

  1. Redirects are not dangerous or pose a security threat in the context of NIP-05
  2. Forbidding redirects does not prevent DDoS, implement a cache instead
  3. Devs deploying on their root domain using services like Vercel or DigitalOcean App platform do not have access to the reverse proxy so can't set the redirect there. They need to use HTTP redirects
  4. Implementing manually a proxy in the codebase of your app for a use case that is external to the app itself is a workaround and here is a specific example for NextJS:

What I want to do by simply setting a redirect in the config of NextJS, outside of the src:

redirects: async () => {
    return [
      {
        source: '/.well-known/nostr.json',
        destination: 'https://api.numeraire.tech/.well-known/nostr.json',
        permanent: true,
      },
    ];
  },

What I have to do (implement a manual interceptor that could very well not behave as expected in the api directory) and an additional rewrite policy in config:

import type { NextApiRequest, NextApiResponse } from 'next';

export default async (req: NextApiRequest, res: NextApiResponse) => {
    const { method, query } = req;

    const backendUrl = new URL('https://api.numeraire.tech/.well-known/nostr.json');

    for (const key in query) {
        const value = query[key];
        if (Array.isArray(value)) {
          value.forEach(val => {
            backendUrl.searchParams.append(key, val);
          });
        } else if (typeof value === 'string') {
          backendUrl.searchParams.append(key, value);
        }
      }

    try {
      const response = await fetch(backendUrl.toString(), {
        method,
        headers: {
          'Content-Type': 'application/json',
        },
      });

      const data = await response.json();

      res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate=30, stale-if-error=600');
      res.status(response.status).json(data);
      } catch (error) {
      console.error('Error fetching from backend:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }

and

rewrites: async () => {
    return [
      {
        source: '/.well-known/nostr.json',
        destination: '/api/nostr',
      },
    ];
  },

The handler is a manual implementation that differs in each framework.

Given that you personally don't like redirects, maybe @dennisreimann from BTCPayServer having written the tutorial about redirects for LN addresses will have some insights.

fiatjaf commented 3 weeks ago

Sorry, but points 1, 2 and 3 are not justifications for adding a new breaking change to the protocol, they are just arguments about why past decisions might have not been based on the most solid arguments. We can argue that all day, but even if we conclude you are right these things will not justify making a change now. To justify making a change we would need an actual positive justification, like a real world valuable use case that isn't possible today.

Point 4 is just telling me that it took you extra code to implement the handler in Next.js? That is already built already though, and changing it now would be extra work for you. Also, if it's just a static set of pubkeys that you have there wouldn't it make more sense to serve them directly from the Next.js app?

I would argue that the vast majority of Nostr users use a service for NIP-05, among my contacts, I see countless nostrplebs, primal.net, getalby.com, etc. Only few own a domain name that they use to return a static nostr.json file for their own identifier.

I don't get this point. Why do these users have care about your suggested change?

mikedilger commented 3 weeks ago

If you allow redirects now, most clients will not follow them so they will be useless. This ship has already sailed.

darioAnongba commented 3 weeks ago

It is not a breaking change to the protocol. In the same way as you should not forbid redirections (a specific aspect of the HTTP protocol) on the Nostr protocol, you should not enforce redirections neither. Clients should be free to follow redirects if they want to. Removing the section about "security constraints" (that has nothing to do with security) will not break consensus.

The use case is clear. I want to specify that a specific queried resource is permanently (301) or temporarily (302) available at a different location. Here a subdomain. That is what HTTP redirects are for and Nostr has no business in that context.

We are not serving a static file. We are building the Numeraire SwissKnife, a lightning wallet similar to Alby Hub allowing creating LN and Nostr addresses, we propose it as a service through a dashboard. You can see the docs at docs.numeraire.tech and reach the API at api.numeraire.tech. the nextJS website at the root domain (numeraire.tech) should not be aware of Nostr.

We are talking about protocol design here. Whether or not clients have already blocked redirections by following the NIP is irrelevant to the NIP itself, it will simply become an opinionated choice for clients.

In definitive, I'm just trying to point out a flaw in the NIP design, if you don't want to change it because you don't like redirects, do as you please.