-
Notifications
You must be signed in to change notification settings - Fork 578
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
Conversation
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. |
There was a problem hiding this 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` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
26.md
Outdated
|
||
##### Signed Pairing Payload | ||
|
||
The Signed Pairing Payload should be a `base64` encoded JSON object as follows: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 specsigned 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 between1659665936
and1660665936
:
["s", "aabb", "created_at>1659665936 & created_at<1660665936", "...signature"]
- Allow signing kind 1 events by
ccdd
created after1659665936
:
["s", "ccdd", "kind=1 & created_at>1659665936", "...signature"]
- Allow signing kind 1 events by
eeff
forever:
["s", "ccdd", "kind=1", "...signature"]
There was a problem hiding this comment.
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"]
There was a problem hiding this comment.
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>] }]
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
I still think the reverse flow is better because it is backwards-compatible: still let each event be signed by the 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 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? |
I have a few issues with the 'reverse' method (ie. published from another event such as Kind:0).
|
I agree with you. Now how about this instead? It is basically the same thing you have, but it keeps the
In which 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). |
@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'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 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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
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). |
There was a problem hiding this 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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: Happy to hear from other relay and client maintainers on this though! |
A top level field is definitely an option I had |
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. |
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:
|
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. |
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. |
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. |
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. If an unwanted event is posted you could use the deletion event with a reason. |
This nip would create events that are not compatible with any relay.
That's ...
Why not - and I think this has been suggested above already -
has none of the above issues:
and while in theory clients might have to query many keys per user it will on I would design such a delegation system to allow retiring keys, so clients |
@Giszmo has made a strong argument. I am changing my allegiance to his proposal. |
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. |
Isn't this NIP suggesting that this is done? |
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. |
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. |
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. B can now produce an event like the following:
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 |
I think this is it! |
on the client side, I would just treat the pubkey as a calculated field, but otherwise no changes would be required! woot. |
I'm changing my allegiance to @fiatjaf's proposal. |
There was a problem hiding this 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.
Quick question before I publish my changes. The signature here doesn't reference the delegator at all, so I'm proposing:
Does that make sense? |
Yes, good catch, it would have been embarrassing to allow anyone to publish anything with anyone else's delegation. |
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 🤷 |
Looks good to me!, now we just need to see this working before we merge. |
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few requests:
- 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>
]
- 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.
- Define how event deletions are handled with delegations.
I would presume the best implementation would be to check ifpubkey == 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.
Are you proposing to not have the
If you could propose some wording for that section, it would be great.
That sounds like something NIP-9 should take into account? |
Yup, there's the runes-spec which we can link to and then there's the individual handling of each property: |
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? |
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. |
This is correct. |
Not compatible, but similar, right? Just use the same technique. Yeah, I agree. |
How should delegated events handle nip-09 event deletions? |
Good point. |
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:
I was thinking about building a simple HTML page where one can build rules tailored for delegated events. Might come in handy |
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:
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. |
How is this different from what is being proposed here @vinliao? Seems to be roughly the same. |
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. |
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. |
I have a basic POC working: https://wagmi-nostr.vercel.app/ 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:
|
only delegated ones was my proposal. |
Good questions I don't have answers to. I'm also wondering whether relays will store the delegate event long-term (say, decades). |
Merging this since it seems to satisfy everybody and no one has anything to add. We can always edit the NIP later if necessary. |
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.