Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NIP-26: Delegated Event Signing #28

Merged
merged 2 commits into from
Sep 23, 2022
Merged

Conversation

markharding
Copy link
Contributor

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.

@AtlantisPleb
Copy link
Contributor

AtlantisPleb commented Aug 4, 2022

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.

Copy link
Contributor

@jb55 jb55 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok! A few comments.

26.md Outdated
Delegated Event Signing
-----

`draft` `mandatory` `author:markharding` `author:minds`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only nip01 is mandatory, some relays may not want to implement this and that's ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is that all of the events coming out of Minds will be invalid to most clients and rejected by relays too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had some comments on this here:

#28 (comment)

26.md Outdated

##### Signed Pairing Payload

The Signed Pairing Payload should be a `base64` encoded JSON object as follows:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why base64 encode it? could it not just be a json string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it doesn't need to be base64 encoded, but stringified json is pretty igly imho

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well humans won't be consuming this, base64 encoded is 141 bytes and simple json string is 104. seems like a no brainer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it is ugly, I think they should all go in the same tag array:

[
  "subkey",
  <signed pairing signature (64-bytes schnorr signature of the sha256 hash of the signed pairing payload>,
  <32-bytes hex-encoded public key of who is authorized to sign>,
  <unix timestamp of issued time>,
  <optional, if present unix timestamp of invalidation time>
]

77 bytes.

But I think we can remove the issued time and keep just the invalidation time.

Or just keep the 4th string for the runes-like thing @jb55 has in mind which is indeed pretty cool.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like fiatjaf's suggestion. In which case I suggest the signature to be done on the JSON of the following array [<pubkey of delegate>,<issue time>[, <expiry>]] (no whitespace)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markharding Yeah, null is not allowed by NIP-01 so empty strings should be used instead.
@jb55's suggestion of using runes is a better approach and we won't need a key for each new condition. I also want to be able to search for events signed by a third party but if the pubkey is wrapped in an object of course it will be easier for developers to reason about it but at the expense of relays and clients to index/search for them.
If there's no intention on allowing clients to search for these events, then we should consider using subkey instead of s since tags with 1 letter are meant to be indexable and query-able.

I propose the following format: ["s", <delegate's pubkey in hex>, <scope>, <signed payload>]
Where:

  • scope follows runes-like spec
  • signed payload is the signature of the JSON stringified tag ["s", <delegate's pubkey in hex>, <scope>] without spaces

Issue time is NOT needed since you can include created_at>1659665936 in scope, but if you really need it then it's okay, it should be added somewhere after the delegate's pubkey.

Here's some examples:

  • Allow signing events of any kind by aabb created between 1659665936 and 1660665936:
    ["s", "aabb", "created_at>1659665936 & created_at<1660665936", "...signature"]
  • Allow signing kind 1 events by ccdd created after 1659665936:
    ["s", "ccdd", "kind=1 & created_at>1659665936", "...signature"]
  • Allow signing kind 1 events by eeff forever:
    ["s", "ccdd", "kind=1", "...signature"]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To revoke this permission, the owner of the delegated pubkey could send an event with tag having a capital S:
["S", <delegate's pubkey>, <scope>, "<original signed payload>"]".
So if I want to revoke the permission of ccdd to send kind 1 events after 1659665936, I'll include the following tag:
["S", "ccdd", "kind=1 & created_at>1659665936", "original signature"]

Copy link
Member

@cameri cameri Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my suggestion it is trivial for existing clients/relays to search for all the delegations and revocations:
["REQ", "sub", { "#s": [<delegate pubkey>] }, { "#S": [<delegate pubkey>] }]

Copy link
Member

@fiatjaf fiatjaf Aug 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cameri do you think it is worth implementing this same rune language from here? https://github.com/rustyrussell/runes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it's perfect for this use-case.

That library may seem like a lot of code but it's bloated with comments and human-readable validation tests... but we could implement a much shorter compliant version that just returns true or false.

@fiatjaf
Copy link
Member

fiatjaf commented Aug 4, 2022

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.

@jb55
Copy link
Contributor

jb55 commented Aug 4, 2022

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?

@markharding
Copy link
Contributor Author

markharding commented Aug 4, 2022

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.

@fiatjaf
Copy link
Member

fiatjaf commented Aug 4, 2022

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
Copy link
Member

fiatjaf commented Aug 4, 2022

@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.

@jb55
Copy link
Contributor

jb55 commented Aug 4, 2022

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.

26.md Outdated

##### Signed Pairing Signature

The Signed Pairing Signature is a schnorr signature of the sha256 hash of the Signed Pairing Payload. For example `2ed3e4b8470ce37b7e1946441a323d1d71c8a846fe49787ec406e14a44632cc96e48cabccc4a526eedd51aca33bf2a5cf7fb85462d23ad6d4de29c8b91abc41b` is a signature of the payload mentioned above.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the signature should probably be done on the sha256 of the base64 encoded signed pairing payload, so that the receiver of the event doesn't have to base64 decode it before verifying it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just scrap the base64 encoding because it just makes the event larger for no reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes thats fine with me

@fiatjaf
Copy link
Member

fiatjaf commented Aug 5, 2022

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).

Copy link
Collaborator

@Semisol Semisol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I do like the idea of a key and subkeys system, there are certain issues in this NIP as highlighted by others.
In my opinion, if the protocol will be changed in a backwards-incompatible way, it should be thought out over a longer period and should be tested more throughly.

Approach NACK

26.md Outdated

`draft` `mandatory` `author:markharding` `author:minds`

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A request-to-sign protocol or some kind of way apps can post on a user's behalf without knowing the public key may be better for this. This also allows users to implement custom validation on what and what cannot be posted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand more on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood @Semisol correctly, nostr integration in custodial solutions should use a completely different approach.

The Problem

Minds has a huge user database and would like to both let users' activity be visible in the broader nostr ecosystem and let users post on minds using self-custodial keys.

This Nip's Approach

Minds' users won't all create nostr keys and keep them save on day one, so Minds will create "custodial" keys for them and whenever they post on Minds, Minds posts on nostr using that key. They will have followers on nostr and Minds will show them those, too and replies from nostr etc.

Those users that now want to post from nostr with their own keys need a way to link their priorly established Minds keys with their new keys or else their custodial key's followers would be lost and they would start from zero. If the user enters his nostr pubkey in Minds, Minds will provide them with a proof they have to include in all their events so they can use the Minds pubkey with their nostr privkey's signature.

Semisol's Suggestion

Minds users that have their private key can use that to request Minds to sign and publish events one by one both on Minds and nostr. Minds could have an api where the user could submit his signed nostr event and Minds would swap the pubkey and sig and publish it.

Such an approach could be less disruptive for network, relays and client devs. The api could be: Send the event as ephemeral kind 20234, put the actual kind in a tag ["kind", "1"]. The service that cared about your pubkey would pick up the event, set the right kind, swap the pubkey, sign and publish. It would - for better or worse - give more control to Minds.

26.md Outdated

#### Modifying event verification

When the `s` tag is provided, events **must** be signed and verified by the respective private key of the `signerkey`. Clients/relays **should** confirm that no revocation have been created with a greater `created_at` value.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raises a few questions:

  • How will expires_at be handled? Can't the events' timestamps be backdated?
  • In the future, where a NIP to revoke is implemented if at all, how do you handle revocation?
    For compromised keys, you can't validate if the events were actually posted before the revocation event, and you have only two options which is to ignore the backdating issue or mark all events by the child key as invalid.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expires_at is really at the mercy of the clients/relays clocks being correct. It's not ideal, but is standard with JWT's etc. In terms of Can't the events' timestamps be backdated?; yes they probably can, but it doesn't change the fact that the attestation existed at somepoint with that timestamp. Revocation is not part of this proposal (I don't think it should be), however if there was a revocation with a greater created_at it would invalidate the keypairing.

@markharding
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

@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.

@Semisol
Copy link
Collaborator

Semisol commented Aug 12, 2022

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

@fiatjaf
Copy link
Member

fiatjaf commented Aug 12, 2022

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.

@ottman
Copy link

ottman commented Aug 12, 2022

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.

@jb55
Copy link
Contributor

jb55 commented Aug 12, 2022

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
Copy link
Contributor

jb55 commented Aug 12, 2022

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?

@Semisol
Copy link
Collaborator

Semisol commented Aug 12, 2022

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.

@Giszmo
Copy link
Member

Giszmo commented Aug 14, 2022

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".

@fiatjaf
Copy link
Member

fiatjaf commented Aug 14, 2022

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

@jb55
Copy link
Contributor

jb55 commented Aug 15, 2022

  • 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.

@fiatjaf
Copy link
Member

fiatjaf commented Aug 15, 2022

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?

@jb55
Copy link
Contributor

jb55 commented Aug 15, 2022

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.

@fiatjaf
Copy link
Member

fiatjaf commented Aug 15, 2022

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
Copy link
Member

fiatjaf commented Aug 15, 2022

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".

@jb55
Copy link
Contributor

jb55 commented Aug 15, 2022

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
Copy link
Contributor

jb55 commented Aug 15, 2022

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

@Semisol
Copy link
Collaborator

Semisol commented Aug 20, 2022

I'm changing my allegiance to @fiatjaf's proposal.
one change though: NIP-09 should allow the parent key to "delete" events by delegated keys.

Copy link
Collaborator

@Semisol Semisol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will re-review when the revised proposal is pushed.

@markharding
Copy link
Contributor Author

Quick question before I publish my changes. The signature here doesn't reference the delegator at all, so I'm proposing:

nostr:delegation:<pubkey of publisher (delegatee)>:<conditions query string>

Does that make sense?

@fiatjaf
Copy link
Member

fiatjaf commented Aug 22, 2022

Yes, good catch, it would have been embarrassing to allow anyone to publish anything with anyone else's delegation.

@markharding
Copy link
Contributor Author

Hi all

Thanks for the feedback so far! I've just pushed up some changes. Please feel free to open MR's against this one with wording changes or anything that makes things more clear. I'm not sure the way I have explained the 'query string' style conditions is the best 🤷

@fiatjaf
Copy link
Member

fiatjaf commented Aug 24, 2022

Looks good to me!, now we just need to see this working before we merge.

@cameri
Copy link
Member

cameri commented Aug 25, 2022

NIP should probably explain the conditions part and/or point to the runes spec otherwise some will be left scratching their heads on how to implement that part.

Copy link
Collaborator

@Semisol Semisol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few requests:

  1. Please use the SHA256 hash of the following serialized JSON to match up with NIP-01:
[
    "delegation",
    <pubkey of the delegator>,
    <pubkey of the delegatee>,
    <conditions query string>
]
  1. Please define how a conditions query string is formatted.

I'd recommend a runes-like syntax with a condition being field || operator || value and the string being & joined conditions, where the operators are:

  • = for equality (accepts list)
  • ! for inequality (accepts list)
  • < for lower than (accepts number)
  • > for greater than (accepts number)

A list is defined as a comma seperated list of numbers. In the case of (in)equality, it checks if the value is/isn't in the list.

  1. Define how event deletions are handled with delegations.
    I would presume the best implementation would be to check if pubkey == deleter_pubkey || delegating_pubkey == deleter_pubkey, which would allow say an integration to delete events only they signed, and the delegator also being able to delete them.

@markharding
Copy link
Contributor Author

A few requests:

  1. Please use the SHA256 hash of the following serialized JSON to match up with NIP-01:
[
    "delegation",
    <pubkey of the delegator>,
    <pubkey of the delegatee>,
    <conditions query string>
]

Are you proposing to not have the nostr:delegation:<pubkey of publisher (delegatee)>:<conditions query string> string as proposed by @fiatjaf and @jb55? I don't mind either way.

  1. Please define how a conditions query string is formatted.

I'd recommend a runes-like syntax with a condition being field || operator || value and the string being & joined conditions, where the operators are:

  • = for equality (accepts list)
  • ! for inequality (accepts list)
  • < for lower than (accepts number)
  • > for greater than (accepts number)

A list is defined as a comma seperated list of numbers. In the case of (in)equality, it checks if the value is/isn't in the list.

If you could propose some wording for that section, it would be great.

  1. Define how event deletions are handled with delegations.
    I would presume the best implementation would be to check if pubkey == deleter_pubkey || delegating_pubkey == deleter_pubkey, which would allow say an integration to delete events only they signed, and the delegator also being able to delete them.

That sounds like something NIP-9 should take into account?

@cameri
Copy link
Member

cameri commented Aug 25, 2022

  1. Please define how a conditions query string is formatted.

Yup, there's the runes-spec which we can link to and then there's the individual handling of each property: kind, created_at are numeric, id, pubkey, content are string, sig is pointless to handle but could be treated as string, and then any other field is for matching tags and is treated as a list so the value is a comma separated list. To add comma separated list I think we must escape the commas as per the spec I believe since they are not allowed by default (I could be wrong).

@cameri
Copy link
Member

cameri commented Aug 25, 2022

nostr:delegation:<pubkey of publisher (delegatee)>:<conditions query string>

Since the pubkey of the delegator is not included, doesn't that mean that it is therefore malleable and thus you could publish events as anyone?
Regardless of whether we hash this string or an array like @Semisol proposes, we must include all fields so they are not malleable. Delegations are strictly between two pubkeys so I think they must be part of the hash.

@fiatjaf
Copy link
Member

fiatjaf commented Aug 25, 2022

There is no risk of malleability there since the delegator is signing this string. If there was a risk then adding the public key wouldn't help either way.

Arguably it was a mistake to make the default NIP-01 event serialization include the pubkey of the author.

@Semisol
Copy link
Collaborator

Semisol commented Aug 25, 2022

There is no risk of malleability there since the delegator is signing this string. If there was a risk then adding the public key wouldn't help either way.

This is correct.
I still recommend we hash an array to be compatible with NIP-01.

@fiatjaf
Copy link
Member

fiatjaf commented Aug 25, 2022

Not compatible, but similar, right? Just use the same technique. Yeah, I agree.

@cameri
Copy link
Member

cameri commented Aug 27, 2022

How should delegated events handle nip-09 event deletions?
If the delegator or delegatee wants to delete an event, how would that work?
Assuming the rule allows the delegatee to use kind 5 to delete events and the delegator can always delete delegated events

@fiatjaf
Copy link
Member

fiatjaf commented Aug 27, 2022

Good point.

@jb55
Copy link
Contributor

jb55 commented Aug 27, 2022

Assuming the rule allows the delegatee to use kind 5 to delete events and the delegator can always delete delegated events

yes, delegator should always be able to delete delegated events.

what if kind=5 is allowed, should this enable the delegatee to delete non-delegated events or only delegated ones?

@cameri
Copy link
Member

cameri commented Aug 27, 2022

Assuming the rule allows the delegatee to use kind 5 to delete events and the delegator can always delete delegated events

yes, delegator should always be able to delete delegated events.

what if kind=5 is allowed, should this enable the delegatee to delete non-delegated events or only delegated ones?

I don't think so, but this NIP should outline what to do in each case so there's no ambiguity.

I propose:

  • delegator can always delete delegated and non-delegated events
  • delegatee can only delete delegated events sent by same delegatee
  • a kind/5 rule would prevent delegatee from deleting any events, period

I was thinking about building a simple HTML page where one can build rules tailored for delegated events. Might come in handy

@vinliao
Copy link

vinliao commented Sep 1, 2022

I've been reading about other decentralized social protocol, and Farcaster (https://farcaster.xyz) seems to have solved this key-delegation problem (in a quite elegant way, I think).

Steps:

  1. Users have main key (let's call it authority key).
  2. User signs event with authority key that authorizes a signing key.
  3. Signing key is stored in the browser or phone.
  4. Signing key is unexportable, signing key can't receive money (different curve).
  5. New device or browser means authorizing a new signing key from the authority key.
  6. Events (tweets) are signed with signing key only.
  7. If signing key is compromised, user can revoke the signing key by signing a revocation event.
  8. Relays reject events from pubkeys that have been revoked.

If this is implemented in Nostr, it will enable delegated signing while still adhering to NIP-01. Read more: https://github.com/farcasterxyz/protocol#45-signer-authorizations.

The big difference between this and the proposals and comments above (including OP's) is the non-exportable part. With this, users that use browser wallets like MetaMask don't need to think about keys at all. Big UX win.

This idea seems redundant for people who locally hold their private key ("why bother with creating a signing key when I can sign with my authority key?") Theoretically, wallets from other chains can create a Schnorr signing key, enabling those wallets to "talk Nostr." Plus, not everyone is technically sophisticated enough to handle their raw private key.

I can create a PoC of this is people are interested. It doesn't seem to be hard.

P.S. I think Farcaster's README is worth reading. Very well-written. Some ideas over there can be imported into Nostr.

@fiatjaf
Copy link
Member

fiatjaf commented Sep 1, 2022

How is this different from what is being proposed here @vinliao? Seems to be roughly the same.

@vinliao
Copy link

vinliao commented Sep 2, 2022

Seems to me that what's being proposed here is Schnorr main key and Schnorr delegate key. The Farcaster one (I think) will enable browser-based wallets (MetaMask, or even wallets from other chains) create a Schnorr delegate key. You gotta take my word for it because I haven't made any working implementation (yet) - I might be wrong.

@fiatjaf
Copy link
Member

fiatjaf commented Sep 2, 2022

Got it, so the difference is that on Farcaster you can delegate to keys of different curves etc? Indeed, that can be useful but only if you want to tie your Nostr identity to other software -- given that these other software won't be producing Nostr events.

I.e. you want to tie your Nostr identity to your Ethereum key you can use a standardized format and that can help, but if you want to sign Nostr events using the Ethereum signature algorithm that won't work because all the clients and relays are not prepared to verify these signatures.

@vinliao
Copy link

vinliao commented Sep 3, 2022

I have a basic POC working: https://wagmi-nostr.vercel.app/

Screenshot from 2022-09-03 17-09-48

User can talk Nostr (NIP-01, no reply capability yet) without having to touch keys - they just have to sign event with whatever Eth wallet provider they like. Basic idea: Eth keypair is the authority, it creates Schnorr alias (with normal NIP-01 event), user uses Schnorr alias to create Nostr events, private key of Schnorr alias is stored in the browser (with localStorage), clearing cookies means new Schnorr alias, Nostr events created by Schnorr alias contains event ID of the delegate event.

Again, I think this is big UX win. "web3 chat" seems to be a saturated domain, though, so I'm not sure what sort of product can be built with this...

Edit: I haven't published any of the event to relays because it would be spammy as hell. Repo link: https://github.com/vinliao/wagmi-nostr.

@ivanmladek
Copy link

I have a basic POC working: https://wagmi-nostr.vercel.app/

Screenshot from 2022-09-03 17-09-48

User can talk Nostr (NIP-01, no reply capability yet) without having to touch keys - they just have to sign event with whatever Eth wallet provider they like. Basic idea: Eth keypair is the authority, it creates Schnorr alias (with normal NIP-01 event), user uses Schnorr alias to create Nostr events, private key of Schnorr alias is stored in the browser (with localStorage), clearing cookies means new Schnorr alias, Nostr events created by Schnorr alias contains event ID of the delegate event.

Again, I think this is big UX win. "web3 chat" seems to be a saturated domain, though, so I'm not sure what sort of product can be built with this...

Edit: I haven't published any of the event to relays because it would be spammy as hell. Repo link: https://github.com/vinliao/wagmi-nostr.

OK I got the delegated Schnorr key publishing events and after cache reset, can confirm, with the same Metamask signature, a different Schnorr delegate key is used.

Couple of questions:

  1. Who is responsible for the security of the main key? If I loose my Nostr private key as an individual user, it sucks, but not the end of the world. What happens if the main delegator, who at some point could delegate hundreds of thousands or even millions of Schnorr aliases, looses the main key? What happens to the delegates and their ability to post to Nostr or other apps, after they reset their cache?

@Semisol
Copy link
Collaborator

Semisol commented Sep 11, 2022

what if kind=5 is allowed, should this enable the delegatee to delete non-delegated events or only delegated ones?

only delegated ones was my proposal.

@vinliao
Copy link

vinliao commented Sep 13, 2022

OK I got the delegated Schnorr key publishing events and after cache reset, can confirm, with the same Metamask signature, a different Schnorr delegate key is used.

Couple of questions:

Who is responsible for the security of the main key? If I loose my Nostr private key as an individual user, it sucks, but not the end of the world. What happens if the main delegator, who at some point could delegate hundreds of thousands or even millions of Schnorr aliases, looses the main key? What happens to the delegates and their ability to post to Nostr or other apps, after they reset their cache?

Good questions I don't have answers to. I'm also wondering whether relays will store the delegate event long-term (say, decades).

@fiatjaf fiatjaf merged commit b62aa41 into nostr-protocol:master Sep 23, 2022
@fiatjaf
Copy link
Member

fiatjaf commented Sep 23, 2022

Merging this since it seems to satisfy everybody and no one has anything to add. We can always edit the NIP later if necessary.

@ottman
Copy link

ottman commented Sep 28, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants