10 KiB
NIP-41
Stateless Unambiguous Key Invalidation
draft
optional
author:fiatjaf
author:RubenSomsen
author:optout
The idea of this NIP is that compromised keys can be invalidated to prevent identity theft, in a secure way.
Cryptographers must forgive me for trying to write this in a way that humans like myself can understand.
Motivation and explanation
Currently if a private key A
is compromised, for example, due to usage in a malicious or compromised client, the owner 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 secure way to rotate keys, because the attacker who has a
could publish a competing event with a lower created_at
value pointing to a new key X
(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 formalized communication for key rotation and a scheme whereby new keys can cryptographically verifiably verified that could both negate the possibility of identity theft, exclude the possibility of hijacked key rotation, and allow clients to perform the unfollow/follow process automatically.
The way this NIP achieves this is by pre-generating a sequence of extended key pairs 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. Initially only the last key is published and it used for identity. In case of a compromise, the key can be rotated to the previous one, and it is possible to verify that it is a genuine pre-existing key, as the compromised can can be genreated from it.
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.
Earlier work
The first version of this NIP employed a custom derivation scheme, using key hashing and arithmetics, quite similar to BIP32 but different. Employing the established BIP32 standard allows for easier implementation.
Notation
The following abbreviations are used:
SK
secret key (32 bytes)PK
public keyESK
extended secret key, consisting of private key part (32 bytes) and 'chain code' part (32 bytes), as defined in BIP32.EPK
extended public key, consisting of public key part (33 bytes) and 'chain code' part (32 bytes), as defined in BIP32.CC
is the chain code
Implementation
Generation of keys uses key generation used by Bitcoin BIP-32 'Hierarchically Deterministic' wallets
Verification operates on public keys only, taking use of the BIP-32 property that a public key derived from the corresponding public key of an extended private key is the same as the one corresponding to the derived private key.
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). The extended private keys are then derived iteratively:
ESK_1 = f(ESK_0)
ESK_2 = f(ESK_1)
ESK_3 = f(ESK_2)
...
ESK_N = f(ESK_N-1)
where:
- the
f()
function takes the BIP3241
th child of the previous extended private key, ESK_0
is derived from the seed, using derivation pathm/44'/1237'/41'
(using values from NIP-06, and41
as present NIP number)- N is an arbitrary number, such as 256.
The user will take the key pair (SK_N, PK_N)
of the last extended key ESK_N
, and use them.
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.
Note that in the iterations non-hardened derivation is used, this is in order to allow verification to proceed on the extended public keys (in case of hardened derivation this is not possible),
Another way to define the extended private keys is that they are derived from the seed using the following derivation paths:
m/44'/1237'/41'
m/44'/1237'/41'/41
m/44'/1237'/41'/41/41
m/44'/1237'/41'/41/41/41
...
Note that the keys are children of each other, not siblings, as is the case e.g. for Bitcoin addresses in an HD wallet.
Rotation and Verification
If at some point the key gets compromised, the user can publish a special event to rotate to a new identity. The form of the event is:
{
"kind": 13,
"tags": [
["p", "PK_N"],
["hidden-key", "CC_N-1'"]
],
"content": "optional explanation",
"pubkey": "PK_N-1"
"created_at": ...
}
From this event alone any client will be able to verify that PK(f'(EPK(PK_N-1, CC_N-1))) = PK_N
. f'()
here is very similar to f()
, but it performs derivation on the public keys.
The verified relationship means that the newly published key must have been generated before the compromised key (as the latter can be generated from it), so it can be assumed to be belonging to the user and not made up by an attacker, so the clients can start using the new key.
Reader implementation
Clients can query relays for the invalidation of a 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.
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!").
Why not use a fix length derivation path?
Would it be possible to use a derivation path of fixed length, with consecutive indices to generate the keys?
It would be possible, but rotation requires publishing the chaincode, which would allow the reconstruction of other sibling keys. Only through using an ever-growing-length derivation path it is possible to use an iterative derivation employing a hash of previous keys.
Notes on Implementation
32 vs 33-byte public keys. In Nostr usually the 32-byte 'X-only' format public key is used. However, in the BIP32 scheme employed here the 33-byte 'compressed' format public key is used. The 32-byte format misses the parity. In practice this means that at verification both parity alternatives have to be checked.
Library vs. own implemetation. For derivation an 'off-the-shelve' BIP32-capable library implementation can be used directly, or an own implementation. The own implementation is recommended, for the following reasons:
- Due to a limitation that stems from serialization format that is not relevant here, implementations typically limit the maximum depth of a derivation path to 256, meaning that at most 253 keys could be generated in this scheme (though in practice that's probably sufficient).
- Constructing an extended public key from its parts is needed at verification, but this is not a typical operation in the BIP32 use cases, and it cannot be easily accessed, only through some workarounds.
By referencing BIP32, it is not the task of this spec to defined the details of the derivation. For convenience we shortly reproduce it here. The non-hardened ESK derivation pseudo-code is:
hash64 = HMAC_SHA512(CC || PK33 || N4)
SK = SK + hash64[0..31]
CC = hash64[32..63]
where HMAC_SHA512 is the corresponding hash function, PK33
is the 33 byte public key ||
is byte concatenation, N4
is the 4-byte (BE) serialization of the child number []
denotes byte sub-array, +
is scalar addition (using BE 32 byte numbers).
The hardened version is similar, but the hash argument is ('0' || SK32)
.
The EPK derivation is also similarm with +
here being point addition:
hash64 = HMAC_SHA512(CC || PK33 || N4)
PK = PK + hash64[0..31]
CC = hash64[32..63]