-
Notifications
You must be signed in to change notification settings - Fork 720
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
Conversation
|
I think I must add the thought process on why this proposal is better than others that may have been considered. Another idea -- to be added to the draft above -- is for a way to unambiguously mark the last key in the sequence as the final one, such that if it is revealed everybody can consider that entire chain of identities as dead forever. This is basically a "nuke" option for when the seed is compromised. There is no possible recover from that event, but at least the person who got their seed compromised can make it likely that the attacker won't steal their identity by publishing invalidation events for all the keys in the chain until the last one. An (illustrative and probably broken) implementation of this scheme in JavaScript is provided here: https://github.com/nbd-wtf/nostr-tools/blob/nip41/nip41.ts, see also the (very brief) tests: https://github.com/nbd-wtf/nostr-tools/blob/nip41/nip41.test.js |
|
Love this in principle. I don't understand the mechanics of how the keys are generated. I tried a little bit but the math and the bitcoin derivation path things are all unfamiliar to me. I'd be happy to just install a tool on an offline computer that does this for me. |
|
Since this happens in somewhat trusted hardware and still has a root secret there which if compromised blows the whole thing.... how is this better than simple key delegation/revocation of an online key from a secure key held in somewhat trusted hardware? |
|
There is in principle no way to do revocation, only delegation with an expiration time. Delegation effectively creates new identities, that other clients have to painfully try to connect with the root identity, but they might choose not to -- and we have to survive in a world in which some clients will do that but others won't --, so a delegated key may show up as a completely new key to others, and that is desirable so the protocol can keep its simplicity. In the same way, reputation schemes and other things that require people to act on your public key directly may suffer from these inconsistencies. Delegation doesn't play well with NIP-04 and anything else that might require ECDH. I think NIP-26 delegation only really shines when one wants to allow a third-party to post things on their behalf. But even if I am wrong, nothing prevents one from using this scheme + delegation. It hurts nothing to have one extra fallback if your super-safe delegator root key gets compromised by any means. |
|
|
||
| ## 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. |
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
| A = A' + hash(A'||B) | ||
| B = B' + hash(B'||C) | ||
| C = C' + hash(C'||D) | ||
| D = ... |
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.
Wouldn't notation here be the other way around
| A = A' + hash(A'||B) | |
| B = B' + hash(B'||C) | |
| C = C' + hash(C'||D) | |
| D = ... | |
| A' = A + hash(A||B) | |
| B' = B + hash(B||C) | |
| C' = C + hash(C||D) | |
| D = ... |
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? Is it wrong? I think it is less confusing this way.
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 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.
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.
Good point.
| { | ||
| "kind": 13, | ||
| "tags": [ | ||
| ["p", "A"], | ||
| ["hidden-key", "A'"] | ||
| ], | ||
| "content": "optional explanation", | ||
| "pubkey": "B" | ||
| "created_at": ... | ||
| } |
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.
| { | |
| "kind": 13, | |
| "tags": [ | |
| ["p", "A"], | |
| ["hidden-key", "A'"] | |
| ], | |
| "content": "optional explanation", | |
| "pubkey": "B" | |
| "created_at": ... | |
| } | |
| { | |
| "kind": 13, | |
| "tags": [ | |
| ["p", "A'"], | |
| ["hidden-key", "A"] | |
| ], | |
| "content": "optional explanation", | |
| "pubkey": "B'" | |
| "created_at": ... | |
| } |
| ### 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. | ||
|
|
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.
In particular, all existing events signed by the compromised key become indistinguishable from new events generated by an impostor with that key.
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 -- although smart stateful clients can arrange things against that.
Or, better, the key owner can republish them with dates in the past.
| ["hidden-key", "A'"] | ||
| ], | ||
| "content": "optional explanation", | ||
| "pubkey": "B" |
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 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.)
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.
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 #p tag.
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 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.
|
Some observations -- sorry if stating the obvious, but to verify my understanding
|
|
I was wondering if BIP-32 style key generation could not be used to achieve the same result? The idea:
|
|
Here's an alternative idea that could be used with existing keys: create a backup keypair, publish the backup public key in a special message, and allow key rotation only to the previously published backup key.
This could be applied to existing keys as well. |
I think this is not worth it, I think, because you don't want to reveal your private key if just some people know, or if you are not really sure it has leaked or not. |
How can anyone know what is the "correct" backup key? An attacker can just publish backdated backup keys. And doesn't matter what you put in there to guard against that, the key owner themselves can always create a series of prepared backup keys and rotate to multiple at the same time. |
I meant to follow the exact logic as in this PR;and that message contains the compromised secret key. But public key is sufficient. |
The event posted by the user as a response to the compromise contains the new pubkey, which has to have a previous 'backup announce' event.
Yes, backdating is an issue to be mitigated, e.g. by timestamps or including its hash in some subsequent message. The bigger issue in my view is if the 'backup announce' event is not found, having in mind the non-guaranteed event notification nature of Nostr. Because in that case there is no way to verify that the new key has been indeed preannounced.
I don't understand, what is the concern here? |
|
|
||
| ## 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. |
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 explanation mentions lowercase and uppercase notations, whereas later A and A' is used, which is confusing.
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.
Lowercase is private key, uppercase is public key. ' is key derived from the non-' -- or vice-versa.
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'll improve it.
| D = ... | ||
| ``` | ||
|
|
||
| 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 |
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.
In case of second, etc. compromise the message could not include only the previous to-invalidate key, but all of them, for convenience.
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.
Good idea.
It does not. That's the entire point. |
|
|
||
| ### 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. |
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.
typo: softwre -> software
| D = ... | ||
| ``` | ||
|
|
||
| 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 |
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 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).
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.
Update: I have corrected this below
I have tried to work this out in detail, but it does not 'add up'.
Generation uses to add a number (a hash) to a secret key.
Verification acts on the private keys (secret keys are not available), and while the same hash can be computed, the same addition does not hold!
This would only worked if:
public_key(secret_key + DIFF) == public_key(secret_key) + DIFF
which is certainly not the case.
Note that Schnorr signatures have additive property, but not the keys!
I have the impression that verification cannot work as intended here.
|
But this is exactly the case, actually with one difference:
|
|
I retract/correct the above. Indeed it is: with the clarification that the addition on the right side is plain numerical (scalar) addition, while on the right side (public key) it is point addition (see wikipedia). Unit test: https://github.com/catenocrypt/nip41-proto0/blob/main/src/keys.rs#L355 |
|
Prototype implementation available: |
|
This is amazing. |
|
Sorry, probably I am to saying something very naive: I am not a crypto-guy. But why you can not use an "Out-of-chain" key in the revocation message, given that the rest of information to validate the request can be put in the tags? For example, referring to the current draft, consider a chain of just two keys A and B. On the key compromisation the user generate a new key C (or another chain to take into account future issues) and posts: And then use C from that time on? With serilization-signed-with-b I just mean the same signature as if the event was published with the key B (obviously the signature of the event is based on C instead). In this way you can have an infinite series of keys. And such revocation can be used also for regular key updates ("regular" as in "Change my key every 4 month"). Probably this scheme can be simplified, I kept a close relation to the original nip because it seems to me that any attack to the "Out-of-chain" method can be direct also to the original one. But as I sayid I have very minimal experience (almost none) in the crypto field... |
|
While working on the prototype implementation, the idea came to me that this could be achieved by using only BIP32 derivations. I think that's better as the cryptography employed is standard. So I have created a new PR with this version: #450
|
|
I see. I've read this and read again, but I don't understand what is happening on BIP-32. Is it basically the same thing but using a bunch of other components? |
I think most people who first read this PR are put off to some degree by the details of key generation. It uses some BIP32 derivation (for the hidden keys), but also direct key hashing and tweaking. My new proposal achieves the same results using only BIP32-style derivation. I think this way it is a bit easier to understand and digest the scheme, and also implementation is a bit easier (as it is using logic widely used in bitcoin code, no custom stuff). |
I'm not sure I got your concerns correctly, but I try to answer it. I think you don't understand the 'hijacking' issue. Let's say there is a mechanism to rotate to a new, arbitrary key, and there is no scheme with any key pre-generation. One solution to this is restrict rotation to a 'new' key which can be verified that belongs to the user. If the 'previous' key A can be deterministically derived from the 'new' key B, that means B has been created before the first usage of A. The user can do this by pre-generating the keys, but the attacker cannot. He cannot come up with a key which would generate A. (I use 'new' in quotes because at generation it is actually generated last). So no, no arbitrary C key can be used as new key, because clients should verify the rotation messages, and the relationship would not hold. It holds only for one key. How this helped; if not please give the PR text some more reads :) |
|
First, thanks for the replay, I understand that I placed my question in the wrong place. I did not note that was a PR, I thought it was an issue. If you think it may be clearer, I can continue in a new issue.
She can make a new request asking to change to C, providing the "Revoke secret" B, making clear to all the clients that the previous request to change to X was unauthorized (well, actually the change to X should be ignored in the first place since the attacker can not provide B). My "Proposal" is a sort of mix between nip-41 and a "Change key" request signed with the previous key alone. EDIT. Please ignore the rest. The described attack is not possible because the private part of B is not compromised. However I think to have found the issue with such mechanism: after the user asked for a change to C, an attacker can get the B that she provided and make a new request to change to X, but with an antecedent timestamp. But... is this an issue also for nip-41 ? Yes, the user can still use the next key in the chain; but iterating this attack, the evil-guy can force user to consume al the keys in the chain. |
Well, I hope you are right about this and people start to get more interested on the idea. So far, though, I think it has done more harm than good, because before we had two people that understood NIP-41 (me and you), now it's just one (you). |
|
In order to understand this PR sufficiently enough to think clearly about it and offer good feedback, one first needs to understand BIP32 (which while about "derivation paths" does not use the word "path" in it anywhere). To understand that, one needs to understand extended keys (extended by randomness), child key derivation functions, hardened child keys vs not hardened child keys (which I don't get), and of course all the elliptic curve stuff it is based on, plus the notation such as the meaning of the apostrophe (which I still don't understand). Maybe bitcoin programmers find all this stuff obvious and well-known, but I'm struggling. I think I understand extending a key with randomness... I don't know why, or how that brings about the claimed properties, or even what the claimed properties are. I think I understand the slash notation (repeated application of a child key derivation function). I spent 40 minutes or so trying to understand BIP32 today and then had to get back to work. I aspire to be able to review this PR. |
|
Yes, I have the same trouble understanding BIP-32 myself. I also think the advantages of "reusing a thing that is widely used in Bitcoin" are not as straightforward, because BIP-32 was clearly not intended to serve this purpose, so my head just starts spinning. |
|
I'm not sure if the previous two comments refer to #450 or this PR (@mikedilger), but I have the feeling that #450 is meant and I feel addressed. While trying to understand the details of this PR #158, the steps of the derivation reminded my of something familiar. I realized that it is very similar to the derivation function of BIP32, and then it clicked, that it could be done entirely using BIP32 derivation.
I reproduce here the the key derivation -- the only operation called upon is BIP32 derivation (with derivation path), no 'low-level' hashing or key arithmetic operation is needed. Besides, BIP32 derivation is also used in this original version, so concerns about complexity of BIP32 applies to both version. |
|
I don't think this should be a NIP on itself, it is too restrictive and won't be used in practice by most people. I'll close this and reintroduce it, maybe, at a later stage, as a part of another NIP that will include multiple ways of doing key soft-invalidations. |
I think this is pointing to another newer branch with the same name |
|
Fixed above. |
|
@fiatjaf I think instead of generating 256 secret keys, it is better to generate just 2: the one the user will use regularly (a) and the other one for recovery (b). Better because the user has to store fewer things. Then the recovery one can publish the migration event pointing to a third one that is part of a new pair of secret keys (x and a new recovery key y). x and y would be generated after key-a compromise. This would need a new tag {
"kind": 13,
"tags": [
["p", "A"],
["hidden_key", "A'"]
["follow_me", "X"] // this one was generated after compromise
],
"content": "optional explanation",
"pubkey": "B"
"created_at": ...
} |
|
Indeed, it looks like that vastly simplifies all the things. |
By reading the NIP i thought user would have to store the whole 256 key set, that's why I suggested the I thought the key derivation would be somewhat simple but now I saw the proof of concept implementation uses HD wallet so it brings its complexities/lib dependencies but atleast just requires the user to store the current key and the seed, so also just two things. I think I skipped the "Key generation" section 🤔️ |
|
I think we could get rid of the HD wallet thing since it is an absolutely bad idea conceived by crazy bitcoiners and replace it with a simpler algorithm that just adds 1, 2, 3 to the root key and hashes that to generate the next key in the sequence, or something like that. But I think your proposal is probably still better? I'm not sure. |
|
Yeah either way my addition would make it possible to migrate to a new key as many times as one wants. Looking forward to see that simpler algorithm be brought to life as I can't help further cause i'm a noob on this area. |
This is a protocol for a backwards-compatible best-effort unambiguous key invalidation flow that aims to reduce the damage from compromised keys from catastrophic to just very bad.
Readable: https://github.com/nostr-protocol/nips/blob/3b823c8f22bac877cc4ee461e1eef2c925ffc64e/41.md