nips icon indicating copy to clipboard operation
nips copied to clipboard

add nip-21: non public dms.

Open fiatjaf opened this issue 1 year ago • 19 comments

Click here to read it properly formatted: https://github.com/nostr-protocol/nips/blob/nip-21-non-public-dms/21.md

fiatjaf avatar Jul 20 '22 23:07 fiatjaf

This nip requires to know which relay the recipient can be reached at. Damus clients for example only talk to damus relay I think. So while challenge and response could be forwarded to the correct relay, it does defeat the point of the whole nip.

Edit: So while you may own your data, if your relay starts hating on you, you're out of luck communicating with your homies.

Also "#p" query should be an array.

Giszmo avatar Jul 20 '22 23:07 Giszmo

I like how simple this is. ACK.

Damus clients for example only talk to damus relay I think.

Not true, damus uses relays in your profile and you can add any relay in the settings.

jb55 avatar Jul 21 '22 01:07 jb55

ACK. Though, couldn't this be generalized to "client authentication" than just limiting kind 4 requests?

Semisol avatar Jul 21 '22 01:07 Semisol

On Wed, Jul 20, 2022 at 06:51:37PM -0700, Semisol wrote:

ACK. Though, couldn't this be generalized to "client authentication" than just limiting kind 4 requests?

how would this work? I'm ok with just kind4 for now and it expand it once we have other sensitive types encrypted note types and use cases.

jb55 avatar Jul 21 '22 22:07 jb55

how would this work?

The server could request auth at any time using CHALLENGE, and a 20001 event would still be sent just without the subscription ID

Semisol avatar Jul 23 '22 01:07 Semisol

Would this be better if instead of the challenge-response thing we just included a a timestamp and a signature of that timestamp plus the relay URL as the subscription ID, as suggested in the Telegram chat?

Like ["REQ", "my-dms/<currenttimestamp>/<pubkey>/<signature>", {...}] in which <signature> is the bip340 signature of the string "identifying myself as <pubkey> for relay <relay url> at <currenttimestamp>"?

fiatjaf avatar Jul 23 '22 16:07 fiatjaf

Honestly, I was thinking this would be a good idea, but now that I wrote the comment above I think it is awful. It involves complicated parsing, timestamp checking and breaks one of the purposes of the subscription id, which is to identify from which subscription each event is being received.

fiatjaf avatar Jul 23 '22 16:07 fiatjaf

May a "REQ" query contain extra parameters? {"kinds": [4],"authors": ["aeae"], "nip-21-ts":"<currenttimestamp>", "nip-21-sig":signatureOf("identifying myself as <pubkey> for relay <relay url> at <currenttimestamp>")}.

It would leave the channel name alone.

That said, I am still against producing an incentive to not share all events across any set of relays. We will have abuse of trust, bugs that leak all DMs to other relays, etc.

I know currently some relays provide more results, ignoring unknown constraints while others ignore a query with unknown constraints.

Giszmo avatar Jul 23 '22 20:07 Giszmo

It seems the question is whether you want atomicity of a request or you want to make it have multiple rounds. A couple of questions come to mind for interactive challenge implementation:

  1. In case there are rounds (e.g. the intermediate CHALLENGE msg), would this mean the order of req, challenge, event must be strict? I'm not too familiar with communication in nostr, but is it possible to have multiple requests to a relay happen concurrently in which case you could get req1, req2, req3, challenge1, event2, event3, event1 or similar? What if the user sends 2 requests that require a challenge concurrently?
  2. Does this require implementations to hold state about requests in progress? Would they also need to remember their content to avoid some weird tampering between the first request and the one after the challenge? Could this open a spam attack on memory if these are not garbage collected?

I can see both having their pros/cons and honestly don't know what's better. For the interactive mode, it might be worth considering adding a type/kind to support different challenges e.g. a REQ that requires a PoW to fight spam more effectively in case that's ever needed. Similarly, the noninteractive variant could have a challenge json substructure whose kind is "timestamp-sig" or smth.

phyro avatar Jul 25 '22 20:07 phyro

@phyro currently, authentication is not really a thing yet with relays. They generously deliver upon any anonymous request. Some require registration though in order to send events.

@fiatjaf this nip would introduce a proof-of-keys to the relay - whatever client the relay is talking to, if it can sign with that pubkey, those messages are probably for them, so they should not be filtered out. Why the commitment to a channel name? I can't imagine a scenario where parties that don't trust each other would share the same websocket.

What I don't understand is why the interactivity is necessary in the first place. Let's say I have a multi-account client and want to efficiently query all DMs of all accounts. Why not - upon opening the websocket - immediately send a kind-20001 event, requesting authorization to query private data over this websocket later? That event could even aggregate keys:

{
  "id": ...,
  "pubkey": <sum of pubkeys>, // Edit: As mentioned by @phyro below, key aggregation can be tricky.
  "created_at": <timestamp>,
  "kind": 20001,
  "tags": [],
  "content": JSON.stringify({
    relay: "chat.relay.nostr.com",
    timeout: <timestamp> + 60 * 60,
    pubkeys: pubkeys, // ~~could be optional. See below~~
    kinds: [4] // could be optional
  }),
  "sig": <sig using sum of seckeys>
}

This would hold a commitment to

  • multiple signers' pubkeys
  • the relay url
  • created_at
  • timeout

The relay could easily check for all pubkeys and store them with the websocket.

Theoretically the list of pubkeys would not be needed but that would defer the signature checking to the later requests for kind-4 (or other private) events, trading a bit of bandwidth/memory for cpu, with virtually no privacy implications.

Giszmo avatar Jul 25 '22 22:07 Giszmo

@Giszmo this specific case is not safe. Consider you have a pubkey A. I could forge the signature with

pubkeys: [A, -A+x*G]

If you sign the sum of pubkeys, you sign x*G which doesn't prove you know the pubkey of either of the keys. When doing multisigs, you need to be careful about keys cancelling each other. There's also a question whether you want to authorize actions or yourself. Fiatjaf's model was action based auth while yours is more similar to getting an auth token or similar (e.g. a session) from what I understand. Token/session based model seems to move it a bit further towards the stateful spectrum.

phyro avatar Jul 25 '22 22:07 phyro

@Giszmo nice, yes, I think just connecting and sending an event 20001 is good and simpler. The relay can then check the timestamp of the event and compare with its current time. Also agree that committing to the subscription is not necessary. I think this solves your concerns above, right @phyro?

All the other possibilities you suggest I think we can leave for a future NIP if they prove necessary, and keep this simple for now.

I'll update the NIP accordingly.

fiatjaf avatar Jul 25 '22 23:07 fiatjaf

@fiatjaf I think this solves my concurrency questions if it's done strictly before you start doing other requests. Some other questions apply thought i.e. do you want a session/token model or authenticated requests. I do think doing a challenge at start is simpler than the challenge per request, but less simple than noninteractive per request (because it introduces auth sessions which requires state on the relay side as well as the client probably?). In the end, I think it depends a lot on the protocol design decisions and to be honest, I'm not too familiar with nostr. What I'd generally try to answer here is whether nostr is a stateless or stateful protocol design as I think some solutions here are stateful (both challenge based solutions) and others stateless (noninteractive one). I don't know whether the protocol is already stateful on other parts though.

Edit: I'm not saying noninteractive is better, but rather that it depends what properties the system wants to have e.g. while "noninteractive per request auth" model is stateless, it comes with the tradeoff of sig verification per request and larger REQ messages.

phyro avatar Jul 25 '22 23:07 phyro

@phyro relays already hold "sessions" per client as the clients remain connected for long times. Each relay implementation already has to have some object where it stores the client's filters and channel names. It's easy to add a list of authorized pubkeys for future queries on that.

Also thanks for correction on the aggregate signing not being possible that easily but if I spell out the pubkeys, others could check if they sum up correctly, right?

@fiatjaf may I suggest to assign a nip16 style kind range for private events? We might do plain text group chats and other stuff there, too. The nip would only slightly change. The rule could be that relays allow querying by only approved pubkeys in "#p" and "authors" queries but the events returned may contain additional p-tags.

I know you want to improve nip4 but if confidentiality is desired by one end, it's crucial that both ends support this nip which is more likely to happen if it uses a new kind.

Giszmo avatar Jul 26 '22 14:07 Giszmo

@Giszmo I see, then it doesn't really introduce state :+1: Regarding key aggregation, note that I spelled out the pubkeys as well pubkeys: [A, B], the second pubkey just happens to be set as B = -A+x*G. The issue is that pubkeys should not be able to cancel out each other and here the pubkey B cancels out A and thus the sum doesn't prove knowledge of private key of A which is used in both keys so it doesn't prove knowledge of private key of either. I think the solution is to use something like Musig2 or similar where they challenge the pubkeys and scale them to prevent any canceling e.g.

e1 = H(A | B | 0)
e2 = H(A | B | 1)

sumkey = e1*A + e2*B

and then sign with the sumkey. I don't really know Musig2. This should really be checked by a cryptographer (which I'm not).

phyro avatar Jul 26 '22 18:07 phyro

@phyro signature aggregation is off the table for this one I think. Sorry I started this topic here. Most users won't need it anyway. This can be introduced in a new nip

Giszmo avatar Jul 27 '22 22:07 Giszmo

pushed a rebased version of this here:

git fetch [email protected]:jb55/nips nip-21-non-public-dms
git range-diff origin/nip-21-non-public-dms...jb55/nip-21-non-public-dms
 -:  ------- >  1:  1b94488 nip16: small fix
 -:  ------- >  2:  dcbd504 NIP-25: Reactions
 -:  ------- >  3:  89bb08b nip25: include dislikes/downvotes
 -:  ------- >  4:  6903ff5 nip25: make generic like an explicit +
 1:  0cb764f =  5:  4620546 add nip-21: non public dms.
 2:  6141691 =  6:  95cec2b fix #p filter syntax.
 3:  beb2dab =  7:  86c9de4 add further possibilities.
 4:  c9afe18 =  8:  3ba2446 fix honor typo.
 5:  e52e231 =  9:  3962150 fix missing ellipsis in pubkey in filter example.
 6:  ddfcfe9 = 10:  c505dcc add clarifications with further details.
 7:  5cc000e = 11:  51839d7 markdown formatting.
 8:  ac8786c ! 12:  3a7b4d8 add event kind 20001 to table.
    @@ README.md: NIPs stand for **Nostr Implementation Possibilities**. They exist to
     -| 3    | Contacts                  | 2    |
     -| 4    | Encrypted Direct Messages | 4    |
     -| 5    | Event Deletion            | 9    |
    +-| 7    | Reaction                  | 25   |
     +| kind  | description               | NIP  |
     +|-------|---------------------------|------|
     +| 0     | Metadata                  | 1, 5 |
    @@ README.md: NIPs stand for **Nostr Implementation Possibilities**. They exist to
     +| 3     | Contacts                  | 2    |
     +| 4     | Encrypted Direct Messages | 4    |
     +| 5     | Event Deletion            | 9    |
    ++| 7     | Reaction                  | 25   |
     +| 20001 | Client Identification     | 21   |
      
      Please update this list when proposing NIPs introducing new event kinds.
 9:  23109d1 = 13:  db5b671 update to reduce the interactivity required.

jb55 avatar Aug 03 '22 20:08 jb55

Should the nip define how relays should behave if the identification is missing? I think the unaware client would be confused to see no kind-4 events at all on a relay but unless filters explicitly mention kind 4 there is no way of knowing if the client even expected to get kind 4.

On the other hand, if the relay would just pretend to not have events the client didn't authorize to get, line 3 in the example:

3. Client sends `["REQ", "my-dms", {"kinds": [4], "#p": ["aeae..."]}, {"kinds": [4],"authors": ["aeae"]}]`

can be changed into:

3. Client sends `["REQ", "my-dms", {"kinds": [4]}]`

which would be most consistent as clients will probably omit the second filter anyway, if they query for all they authored in another filter.

Giszmo avatar Aug 04 '22 04:08 Giszmo

Interesting idea from someone on nostr. In the presence of a revoke note (if we had those). relays could stop serving DMs from that key altogether.

jb55 avatar Aug 22 '22 18:08 jb55

I think this nip could be add some new connection options. My proposal will be:

  • client sends an kind N event.
  • relay sends a challenge, could be sending a number that must be returned singed.
  • client send de challenge answer
  • relay sends a message with ok/no ok and some other fields that could be: - Duration of the subscription - Restrictions (events sent should have PoW, etc ) - other fields

arejula27 avatar Dec 27 '22 12:12 arejula27

I readed before something about a token, as i see it, it is not needed because the relay would autheticate the socket connection and would only send restricted messages through it.

arejula27 avatar Dec 27 '22 13:12 arejula27

This is superseded by https://github.com/nostr-protocol/nips/pull/141.

fiatjaf avatar Jan 24 '23 13:01 fiatjaf