nips icon indicating copy to clipboard operation
nips copied to clipboard

NIP-26: Delegated Event Signing

Open markharding opened this issue 1 year ago • 57 comments

This NIP defines how events should be verified and signed to support generating events on behalf of someone else. It should be possible to sign Nostr events from other keypairs.

Another application of this proposal is to abstract away the use of the 'root' keypairs when interacting with clients. For example, a user could generate new keypairs for each client they wish to use and authorize those keypairs to generate events on behalf of their root pubkey, where the root keypair is stored in cold storage.

I understand that this is a significant change to the protocol, so any feedback here is welcome.

markharding avatar Aug 04 '22 08:08 markharding

Looks cool!

You marked it mandatory but I think it’s intentional that all NIPs other than NIP01 are marked optional. Like you can't be a Nostr relay without supporting NIP01 but that doesn't and shouldn't apply to anything else. Unless you want to argue that it should!

Small typo, shnnorr should be schnorr.

AtlantisPleb avatar Aug 04 '22 15:08 AtlantisPleb

I still think the reverse flow is better because it is backwards-compatible: still let each event be signed by the pubkey as always, but allow each key to specify a parent key. Meanwhile the parent key can specify its set of child keys, and then clients can link all the children to the parent -- and later the parent can disown the children if it wants.

Basically this idea: https://gitlab.com/minds/minds/-/issues/3305#note_1049942432

I would say we are fine to expect eventual consistency from clients in merging children with parents, but not require that -- i.e. children and parents can be seem as separate entities for a while, but eventually they get merged into a single. If a client doesn't want to implement the merging that should be accepted too (I think we shouldn't expect these child-parent relationships to be happening a lot in the sense that normal humans won't create 5 child keys).

Ultimately it is fine if people can just manually link the a child to a parent, even if their client doesn't do that for them automatically, and then they just associate the two in their minds.

fiatjaf avatar Aug 04 '22 18:08 fiatjaf

I think it's ok if the minds relay implements this without forcing it onto other relays. damus would currently accept these since it doesn't yet do signature checking for all incoming messages... it wouldn't be too hard to implement this signature check either.

The only downside is that you could not propagate minds notes to other relays until this gets implemented in something like nostr-rs-relay, but maybe that's ok for now since most minds notes reside on their relay anyways?

jb55 avatar Aug 04 '22 19:08 jb55

I still think the reverse flow is better because it is backwards-compatible: still let each event be signed by the pubkey as always, but allow each key to specify a parent key. Meanwhile the parent key can specify its set of child keys, and then clients can link all the children to the parent -- and later the parent can disown the children if it wants.

Basically this idea: https://gitlab.com/minds/minds/-/issues/3305#note_1049942432

I would say we are fine to expect eventual consistency from clients in merging children with parents, but not require that -- i.e. children and parents can be seem as separate entities for a while, but eventually they get merged into a single. If a client doesn't want to implement the merging that should be accepted too (I think we shouldn't expect these child-parent relationships to be happening a lot in the sense that normal humans won't create 5 child keys).

Ultimately it is fine if people can just manually link the a child to a parent, even if their client doesn't do that for them automatically, and then they just associate the two in their minds.

I have a few issues with the 'reverse' method (ie. published from another event such as Kind:0).

  1. Order of operations. Relays and clients will mark all these events as invalid just because they haven't seen an event
  2. It is only relevant for point in time and will not respect legitimately historically signed events. Just because I 'revoke' a keypair, doesn't mean that every event that was signed with it previously is invalid or forged. It should be point in time forward that is rejected only.
  3. It seems like a lot more effort for clients and relays to be merging pubkeys together. (ie. if I request all events for an another I want their delegated events too, not just what was signed with their root pubkey). The proposed method here, all they have to do is changed their signature verification logic.

markharding avatar Aug 04 '22 19:08 markharding

I have a few issues with the 'reverse' method (ie. published from another event such as Kind:0).

Order of operations. Relays and clients will mark all these events as invalid just because they haven't seen an event It is only relevant for point in time and will not respect legitimately historically signed events. Just because I 'revoke' a keypair, doesn't mean that every event that was signed with it previously is invalid or forged. It should be point in time forward that is rejected only. It seems like a lot more effort for clients and relays to be merging pubkeys together. (ie. if I request all events for an another I want their delegated events too, not just what was signed with their root pubkey). The proposed method here, all they have to do is changed their signature verification logic.

I agree with you. Now how about this instead? It is basically the same thing you have, but it keeps the pubkey field as the event signer, so it is backwards-compatible:

{
  tags: [["root", rootKey, sig(rootKey, subKey)]]
  pubkey: subKey
  sig: sig(subKey, serializedEvent)
}

In which rootKey is the main key the user will hold, for example, outside of Minds; and subKey is the key Minds will hold (using Minds as an example, but could be applied to other use cases).

This creates a chain of attestation that is kept contained in each event. And then we can have expiry times there and whatever else (we can just add them to the same tag array and include them in the signature somehow).

fiatjaf avatar Aug 04 '22 19:08 fiatjaf

@markharding what do you have in mind for allowing Minds.com users to migrate out? Is this proposal part of that idea? Maybe I'm picturing something different in my mind and that's why I'm not getting your point.

fiatjaf avatar Aug 04 '22 19:08 fiatjaf

I'm not sure how this would work for migrating minds users, but thinking about it some more... I would want this at some point in the future where I move my private key to a hardware signing device/cold store. Let's say I have bought a bunch of nostr NFTs. I would feel less comfortable copying my private key into nostr apps everywhere. perhaps in the future a nostr account could have lots of certificates and collectibles, and could be quite valuable. Not to mention the reputation itself could be valuable.

With this proposal, I could sign a temporary "auth token" (s tag) and give that to a nostr app. If the app was evil, it could be annoying for a short amount of time if the subkey leaked, but with a future revocation mechanism I could use my root key to revoke the subkey, and my account would be safe. This signed subkey payload is basically like a bearer auth token (JWT/macaroon).

What's even more interesting is in the future the payload could have further restrictions which limits what kinds of events can be considered valid. For instance, the payload could assert you can only create kind 1 events with this subkey. This would require more event validation logic, but it would be a super powerful mechanism, akin to macaroons (but with less decentralized delegation).

I also like this approach because clients wouldn't have to change any query code, and it would not have to fetch keychains and map subkeys to a root identity, so it's much simpler. The only downside is that we add a new way to consider what is a valid event (vs before which just considered the signature and pubkey), which perhaps would become less of an issue once this mode of checking signatures is adopted by more clients and relays.

I'm ok with having an s tag to reduce the amount of code changes required by adding new top-level fields.

For these reasons, I'm going to give this a Concept ACK.

jb55 avatar Aug 04 '22 23:08 jb55

I hate to admit this, but @jb55 has made good points.

However I still fail to understand how exactly this helps the Minds integration.

I am also not fully convinced that the extra signature and key fit better in a tag than in a new top-level field on the JSON object.

Also, if we are going to do the unthinkable and change the default signature verification method of the base event and bloat the protocol, maybe we should consider other possibilities first (not that I have any in my mind, as I never thought this day would come).

fiatjaf avatar Aug 05 '22 01:08 fiatjaf

I have a few issues with the 'reverse' method (ie. published from another event such as Kind:0). Order of operations. Relays and clients will mark all these events as invalid just because they haven't seen an event It is only relevant for point in time and will not respect legitimately historically signed events. Just because I 'revoke' a keypair, doesn't mean that every event that was signed with it previously is invalid or forged. It should be point in time forward that is rejected only. It seems like a lot more effort for clients and relays to be merging pubkeys together. (ie. if I request all events for an another I want their delegated events too, not just what was signed with their root pubkey). The proposed method here, all they have to do is changed their signature verification logic.

I agree with you. Now how about this instead? It is basically the same thing you have, but it keeps the pubkey field as the event signer, so it is backwards-compatible:

{
  tags: [["root", rootKey, sig(rootKey, subKey)]]
  pubkey: subKey
  sig: sig(subKey, serializedEvent)
}

In which rootKey is the main key the user will hold, for example, outside of Minds; and subKey is the key Minds will hold (using Minds as an example, but could be applied to other use cases).

This creates a chain of attestation that is kept contained in each event. And then we can have expiry times there and whatever else (we can just add them to the same tag array and include them in the signature somehow).

I think having the pubkey still being the signer is going to cause headaches for other clients and relays. It seems like most relay schemas answer REQ: { "authors": [ PUBKEY ]} by looking up a pubkey column, with your proposed change they will now need to also query on another column called 'subkeys' or something?

Happy to hear from other relay and client maintainers on this though!

markharding avatar Aug 11 '22 08:08 markharding

I hate to admit this, but @jb55 has made good points.

However I still fail to understand how exactly this helps the Minds integration.

I am also not fully convinced that the extra signature and key fit better in a tag than in a new top-level field on the JSON object.

Also, if we are going to do the unthinkable and change the default signature verification method of the base event and bloat the protocol, maybe we should consider other possibilities first (not that I have any in my mind, as I never thought this day would come).

A top level field is definitely an option I had

markharding avatar Aug 11 '22 08:08 markharding

@markharding what do you have in mind for allowing Minds.com users to migrate out? Is this proposal part of that idea? Maybe I'm picturing something different in my mind and that's why I'm not getting your point.

I don't see it as 'migrating out', but rather pairing keys together. We want Nostr users to be able to post to our platform, but also to be able to use our platform too. The problem though is that currently they have two identities, their Minds custodial one, and their sovereign nostr identity - this proposal allows them to use their Nostr soverign identity.

markharding avatar Aug 11 '22 08:08 markharding

Why can't we instead work on a proposal for event signing requests? If a user has a client open, it could work like this:

  • App requests event signature via an encrypted event sent to the signer
  • Signer checks if the requested event satisfies whatever restrictions it put such as who can request, what kinds, etc.
  • Signer sends event or failure to the app

Semisol avatar Aug 12 '22 11:08 Semisol

I don't see it as 'migrating out', but rather pairing keys together. We want Nostr users to be able to post to our platform, but also to be able to use our platform too. The problem though is that currently they have two identities, their Minds custodial one, and their sovereign nostr identity - this proposal allows them to use their Nostr soverign identity.

Yes, this is great, I came to like very much this proposal. I think it will come very handy and allow Nostr to onboard other centralized providers and create interoperability all over the internet.

The only problem is: how do Minds users get their sovereign identity with which they will sign the delegated key? They can do that only if they already know Nostr before posting their first message to Minds, which is not the case for 99.999% of users.

fiatjaf avatar Aug 12 '22 11:08 fiatjaf

We could enable users to generate a sovereign Nostr keypair meant for cold storage. We would never see the private key. Then they could pair that to the delegated key.

ottman avatar Aug 12 '22 14:08 ottman

In which rootKey is the main key the user will hold, for example, outside of Minds; and subKey is the key Minds will hold (using Minds as an example, but could be applied to other use cases). This creates a chain of attestation that is kept contained in each event. And then we can have expiry times there and whatever else (we can just add them to the same tag array and include them in the signature somehow).

I think having the pubkey still being the signer is going to cause headaches for other clients and relays. It seems like most relay schemas answer REQ: { "authors": [ PUBKEY ]} by looking up a pubkey column, with your proposed change they will now need to also query on another column called 'subkeys' or something?

Happy to hear from other relay and client maintainers on this though!

yes this is why I prefer your approach. almost nothing would have to change in clients and it would only be a little amount of work to implement this form of signature checking.

jb55 avatar Aug 12 '22 17:08 jb55

Why can't we instead work on a proposal for event signing requests? If a user has a client open, it could work like this:

  • App requests event signature via an encrypted event sent to the signer
  • Signer checks if the requested event satisfies whatever restrictions it put such as who can request, what kinds, etc.
  • Signer sends event or failure to the app

The UX here is pretty bad. If I delegating posting to minds, now when I post on minds I have to open my other client to sign each event?

jb55 avatar Aug 12 '22 17:08 jb55

The UX here is pretty bad. If I delegating posting to minds, now when I post on minds I have to open my other client to sign each event?

You don't have to. Ideally you would set a filter on what you want to allow, such as Minds posting only kind 1 posts as you. Your client would automatically sign events matching that filter.

If an unwanted event is posted you could use the deletion event with a reason.

Semisol avatar Aug 12 '22 19:08 Semisol

This nip would create events that are not compatible with any relay. Author would not match with who signs. Basically:

author: Alice
sig: Alice2
footnote:
  msg: Alice2 may sign on Alice' behalf
  sig: Alice

That's ...

  • confusing: author doesn't sign?
  • bigger: delegation proof in every event? On trivial events like "likes", this doubles the size of the event.
  • repeated payload: delegation proof is just copied over and over?
  • no revocation possible yet: A rather important omission?

Why not - and I think this has been suggested above already - have this split into a replaceable event for key delegation? As above it was not written out with its benefits:

author: Alice
kind: 10008-delegation
tags: [["delegation", <pubkey Alice2>, <expiration date>, <comment>]]
sig: Alice

has none of the above issues:

  • Unaware relays/clients can validate all events
  • Alice2's events are plain old events with no extra payload per event
  • Delegation-aware clients interested in Alice can find Alice2 and display them as Alice
  • Revocation is a simple event replacement
  • Proof of prior delegation can be retained in form of the signed delegation event

and while in theory clients might have to query many keys per user it will on average not be "many" but less than 2.

I would design such a delegation system to allow retiring keys, so clients could stop querying newer messages for "Alice 1".

Giszmo avatar Aug 14 '22 03:08 Giszmo

@Giszmo has made a strong argument. I am changing my allegiance to his proposal.

fiatjaf avatar Aug 14 '22 13:08 fiatjaf

  • Unaware relays/clients can validate all events

This isn't true though. The events are invalid by themselves without the delegation proof and could not be published to relays. Unless you're suggesting the relay/client look for this delegation record each time it sees an invalid signature.

jb55 avatar Aug 15 '22 17:08 jb55

Unless you're suggesting the relay/client look for this delegation record each time it sees an invalid signature.

Isn't this NIP suggesting that this is done?

fiatjaf avatar Aug 15 '22 19:08 fiatjaf

My concern is that signature verification is no longer a bearer proof. You now have to do N queries for every invalid signature you see. This seems not ideal... I would prefer a larger event size over that.

The paranoia around event sizes is odd to me. If your a public relay storage requirements is going to be large regardless. A few extra bytes on delegates messages isn't that big of a deal at that point.

jb55 avatar Aug 15 '22 19:08 jb55

Nevermind, my question was based on a bad read of what you said.

I also now see that @Giszmo's proposal is flawed because it requires that relays and clients search for delegation events every time they see a wrong signature. I was thinking the proposal was similar to the original suggestion from @jb55. I remove my allegiance.

fiatjaf avatar Aug 15 '22 19:08 fiatjaf

This is the best proposal I have in my mind now, it mixes in the best points of all proposals:

Support A wants to delegate signing powers to B for 3 months and only for events of kind 1. A produces something like token = signature(sha256("nostr:delegation:kind=1&created_at>1660592348&created_at<1668368361")) and hands it to B.

B can now produce an event like the following:

{
  "pubkey": "B",
  "delegation": {"pubkey": "A", "condition": "kind=1&created_at>1660592348&created_at<1668368361", "token": token},
  "kind": 1,
  "content": "hello, I am A posting as B",
  "created_at": 1660594000,
  "tags": [],
  "sig": "<B's signature>"
}

Now nonsupporting clients will see this and treat it as an event from this mysterious B entity and that is fine. While supporting clients will see the delegation, throw away the reference to B, and link this event to A instead.

Meanwhile supporting relays can return this event whenever a client does a ["REQ", "", {"authors": ["A"]}], no need for clients to specify "B" there. But both supporting and nonsupporting relays can reply with this event when queried for events from "B".

fiatjaf avatar Aug 15 '22 19:08 fiatjaf

This is the best proposal I have in my mind now, it mixes in the best points of all proposals:

I think this is it!

jb55 avatar Aug 15 '22 19:08 jb55

on the client side, I would just treat the pubkey as a calculated field, but otherwise no changes would be required! woot.

jb55 avatar Aug 15 '22 20:08 jb55

This is the best proposal I have in my mind now, it mixes in the best points of all proposals:

Support A wants to delegate signing powers to B for 3 months and only for events of kind 1. A produces something like token = signature(sha256("nostr:delegation:kind=1&created_at>1660592348&created_at<1668368361")) and hands it to B.

B can now produce an event like the following:

{
  "pubkey": "B",
  "delegation": {"pubkey": "A", "condition": "kind=1&created_at>1660592348&created_at<1668368361", "token": token},
  "kind": 1,
  "content": "hello, I am A posting as B",
  "created_at": 1660594000,
  "tags": [],
  "sig": "<B's signature>"
}

Now nonsupporting clients will see this and treat it as an event from this mysterious B entity and that is fine. While supporting clients will see the delegation, throw away the reference to B, and link this event to A instead.

Meanwhile supporting relays can return this event whenever a client does a ["REQ", "", {"authors": ["A"]}], no need for clients to specify "B" there. But both supporting and nonsupporting relays can reply with this event when queried for events from "B".

this delegation could just be a tag. No need for this special treatment. Supporting relays could still support it in tags

cameri avatar Aug 15 '22 21:08 cameri

I was thinking it would be better to have it outside of the main event so it could be ignored very easily and also that it didn't really belong to the event, I don't know exactly, but it is better to have it inside the tags indeed because, as pointed out here, we don't want nonsupporting relays to strip off that field.

fiatjaf avatar Aug 15 '22 23:08 fiatjaf

I am very confused. My proposal contains no events that are any special by today's standard.

Alice publishes and signs a delegation event for Alice2.

Alice2 does her thing as if she were independent of Alice.

Clients aware of delegations know where to find the delegations.

They can query if Alice2 is a delegate: {kinds:[10008], "#delegation": [alice2]}

They can query if Alice has delegates: {kinds:[10008], authors: [alice]}

They can also hold on to these signed delegations (as can Alice2) in case Alice changes her mind and replaces it.

In OP's proposal, delegation to many keys is possible.

In contrast to my proposal, OP's proposal avoids ambiguity of who's actually delegating to the subkey. If Alice and Bob both delegate to Carol, in OP's proposal Carol would have to pick one of Alice' or Bob's pubkey. In my proposal, trolls could delegate to all delegate keys, making the first query above pointless.

Giszmo avatar Aug 16 '22 02:08 Giszmo

If we're switching to tags:

token = A_signature(sha256("nostr:delegation:kind=1&created_at>1660592348&created_at<1668368361"))
{
  "pubkey": "B",
  "kind": 1,
  "content": "hello, I am A posting as B",
  "created_at": 1660594000,
  "tags": [["delegation", "A", "token", "kind=1&created_at>1660592348&created_at<1668368361"]],
  "sig": "<B's signature>"
}

Relay returns these for query to {"authors": ["A"]}, {"#p": ["A"]}, etc

jb55 avatar Aug 17 '22 17:08 jb55