-
Notifications
You must be signed in to change notification settings - Fork 723
NIP-41: Key Invalidation. first draft. #158
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,114 @@ | ||||||||||||||||||||||||||||||||||||||||||
| NIP-41 | ||||||||||||||||||||||||||||||||||||||||||
| ====== | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Stateless Unambiguous Key Invalidation | ||||||||||||||||||||||||||||||||||||||||||
| -------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| `draft` `optional` `author:fiatjaf` `author:RubenSomsen` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| The idea of this NIP is that compromised keys can be invalidated in a way that prevents identity theft. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Cryptographers must forgive me for trying to write this in a way that humans like myself can understand. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## Notation | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Lowercase letters denote private keys, uppercase letters denote public keys. `||` means concatenate. `sk_` means "secret key", `pk_` means "public key". `G` is the generator point of the secp256k1 curve. | ||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This explanation mentions lowercase and uppercase notations, whereas later
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lowercase is private key, uppercase is public key.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll improve it. |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## Motivation and explanation | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Currently if a private key `A` is compromised, for example, because they chose to input it in a malicious or infected client, they can generate a new key and send events from `A` saying: "hey, this key was compromised, my new key is `B`". | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ### Problems with the approach above | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| This is not a **safe** way to rotate keys, because the attacker who has `a` could publish an recent with a lower `created_at` value pointing people to a new key `X`, for example, instead of `B`. And for any reader only seeing these two events it becomes at least very hard to decide which one, `X` or `B`, is the actual new key of `A`'s owner. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Even if the attacker doesn't do anything, it is still not a **scalable** way (for the lack of a better word) to do this, since it will require manual action from all readers to migrate to stop following the previous key and start following the new. The amount of manual action required grows as the number of followers of `A` grows. And they may not even see the note from `A` because they have a giant feed with hundreds of notes, or they may forget to perform the manual unfollow-refollow process and so on. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ### A better solution | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| If we can create a cryptographically verifiable way to _point users to a new key_ that could both _negate the possibility of identity theft_ and _allow clients to perform the unfollow/follow_ process automatically. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| The way this NIP does it is by generating a sequence of keys such that each one of these _commits_ (i.e., that hides a value in a way that it can't be changed but can be revealed later) to the following. The scheme is as follows: | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
| A = A' + hash(A'||B) | ||||||||||||||||||||||||||||||||||||||||||
| B = B' + hash(B'||C) | ||||||||||||||||||||||||||||||||||||||||||
| C = C' + hash(C'||D) | ||||||||||||||||||||||||||||||||||||||||||
| D = ... | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't notation here be the other way around
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? Is it wrong? I think it is less confusing this way.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is confusing both ways. I don't understand it. In the original, A' is never defined, so how do I compute A? The single-tick isn't defined above this statement either.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. |
||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| The idea here is that a client will pregenerate 256 keys in sequence (arbitrary number) and use the last one, which in this case will be `A`. If `A` gets compromised they can publish a special event | ||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In case of second, etc. compromise the message could not include only the previous to-invalidate key, but all of them, for convenience.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it could help understanding if it is stated here that in fact N key pairs are generated, for each index a pair of keypairs (that is, 2x2 keys). |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| "kind": 13, | ||||||||||||||||||||||||||||||||||||||||||
| "tags": [ | ||||||||||||||||||||||||||||||||||||||||||
| ["p", "A"], | ||||||||||||||||||||||||||||||||||||||||||
| ["hidden-key", "A'"] | ||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||
| "content": "optional explanation", | ||||||||||||||||||||||||||||||||||||||||||
| "pubkey": "B" | ||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If possible, A should publish the event, not B. That makes it more likely clients who follow A will actually query for this event. Otherwise how would they see the event from B unless they queried all Kind 13 events? Also, relays need to ensure that Kind 5 does not delete Kind 13 events. (There should probably be an "undeletable" range of events.)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it enough to have B's key as a tag? Clients will have to explicitly query for this event kind anyway, so they can query with the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fact that clients have to explicitly query for this is sad to me, I would have prefer that they didn't have to add any extra code, but of course that is impossible. Anyway, I like this NIP very much still because there could be dedicated standalone apps that only manage people's contact lists, and people can visit or open these only once every two months (for example) and they will automatically fix update people's invalidated keys, then all other clients will just see the updated contact list and be done with it. |
||||||||||||||||||||||||||||||||||||||||||
| "created_at": ... | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+43
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| And from this event alone any client will be able to verify that `hash(A'||B) + A' = A`, or, in other words, that `hash(.tags["hidden-key"] || .pubkey) + .tags["hidden-key"] = .tags["p"]`, and that the next key in the sequence can't be anything other than `B`, which has a private key `b` that can be assumed to have not been compromised. So it is easy to just stop following `A` and start following `B`. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ### Justifying the NIP title | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| The invalidation is **unambiguous**, i.e., once an invalidation event is fired from the new key, the previous is invalidated. Even if new events come from the old key, they must be considered as coming from an unknown untrusted person, as the key is now in the hands of an attacker. Only the owner of the root key is able to figure out the next key. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| The invalidation is **stateless**, i.e., the clients who see an invalidation event don't have to know anything else, by reading that and that only they can conclude that the previous key was irrevocably compromised and revoked, and can react properly, by, for example, unfollowing the previous key and following the new one. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## Implementation | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| If, at the time of first generating their Nostr public keys, one decides to use this NIP, they will have a chain of 256 keys that can be used in sequence, which means a user can lose up to 255 keys, which should be much more than enough for any user that is not actively trying to lose their keys. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ### Key generation | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| This is intended to be run on safe and trusted hardware. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| A (BIP-32)[https://bips.xyz/32) seed is generated by any means (probably using BIP-39 words is the best idea), then the sequence of 256 hidden keys is generated with the following paths: | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
| sk0' = m/44'/1237'/41'/0' | ||||||||||||||||||||||||||||||||||||||||||
| sk1' = m/44'/1237'/41'/1' | ||||||||||||||||||||||||||||||||||||||||||
| ... | ||||||||||||||||||||||||||||||||||||||||||
| sk2' = m/44'/1237'/41'/255' | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| - The "root" key, i.e. the last key in the sequence, is equal to the "hidden" key for the first position: `sk0` = `sk0'`. | ||||||||||||||||||||||||||||||||||||||||||
| - Its immediate child, i.e. the second-to-last key in the sequence is calculated as follows: | ||||||||||||||||||||||||||||||||||||||||||
| - calculate the public key for `sk0`: `pk0 = sk0 * G`; | ||||||||||||||||||||||||||||||||||||||||||
| - take the bytes for the X coordinates of that: `pk0x = getX(pk0)`; | ||||||||||||||||||||||||||||||||||||||||||
| - calculate the public key for `sk1'`: `pk1' = sk1' * G`; | ||||||||||||||||||||||||||||||||||||||||||
| - take the bytes for the X coordinates of that: `pk1x' = getX(pk1')`; | ||||||||||||||||||||||||||||||||||||||||||
| - concatenate the bytes `pk0x` and `pk1x'` and calculate the sha256 hash of that: `hash = sha256(pk0x || pk1x')`; | ||||||||||||||||||||||||||||||||||||||||||
| - add that, as a number, to the hidden private key `sk1'` (which is a number): `sk1 = sk1' + hash`; | ||||||||||||||||||||||||||||||||||||||||||
| - the result is the second-to-last secret key `sk1`. | ||||||||||||||||||||||||||||||||||||||||||
| - Now proceed to calculate `sk2` using the same process above, but moving everything one number up. | ||||||||||||||||||||||||||||||||||||||||||
| - And so on and so forth until you get to `sk255`. | ||||||||||||||||||||||||||||||||||||||||||
| - `sk255` is the key to be used first. If that gets compromised one must move to `sk254` and so on. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Presumably this key generation process happens in somewhat trusted hardware by a trusted dedicated program, and from it users may copy the initial key and paste it in the normal (still trusted) client they use for day-to-day operations. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ### Reader implementation | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Clients can query relays for the invalidation of key `A` whenever they want by using the filter `{"#p": ["A"], "kinds": [13]}`. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| The verification process for the validity of an invalidation (`kind: 13`) event was given in the explanation above. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## Additional comments | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ### This is not a general "rotation" scheme for keys | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| This scheme is intended to make it less catastrophic when a key is compromised because it was input into a computer, phone or Nostr client softwre that turns out later to have been compromised. It isn't intended to allow people to rotate their keys every month as a routine practice, nor is it supposed to let users be reckless and give their private keys to any malware or trusted third parties. A compromised key is still a bad event, just not one of awful and unrepairable consequences if this NIP is followed and relied upon. | ||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo: |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In particular, all existing events signed by the compromised key become indistinguishable from new events generated by an impostor with that key.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes -- although smart stateful clients can arrange things against that. |
||||||||||||||||||||||||||||||||||||||||||
| ### Fallback mechanism possibilities | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| What happens if Bob is following Carol and Carol publishes an invalidation event for her key, but Bob's client doesn't see it for any reason or doesn't support the automatic refollow mechanism for the new key? Well, in this case we are at least not worse than the current state of things, but Bob has other possibilities: | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| - there can be centralized third-party services keeping track of these invalidations and sending notifications through any means and Bob may be subscribed to one of these; | ||||||||||||||||||||||||||||||||||||||||||
| - or there could be a central directory Bob can use to check from time to time if any of their followed keys has been invalidated and perform the fixes manually in his client; | ||||||||||||||||||||||||||||||||||||||||||
| - anyone with a supporting client can be 100% sure the key was really invalidated, so they can alert others about that fact without any fear of spreading wrong information; | ||||||||||||||||||||||||||||||||||||||||||
| - and last but not least Bob may want to verify things manually if he sees suspicious activity from Carol's key. At least he doesn't have to ask her (and then get a response from the attacker stating that "no, it's just me, Carol!"). | ||||||||||||||||||||||||||||||||||||||||||
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.
Please define the hash function here