Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions 03.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This NIP defines an event with `kind:1040` that can contain an [OpenTimestamps](
{
"kind": 1040
"tags": [
["p", <target-event-author>, <relay-url>],
["e", <target-event-id>, <relay-url>],
["k", "<target-event-kind>"]
],
Expand Down
199 changes: 199 additions & 0 deletions D8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
NIP-D8
======

Nostr Key Rotation
------------------

`draft` `optional`

By default nostr public keys solely represent a user's identity, which makes it impossible to recover from key loss or compromise.

This NIP defines a simple mechanism for key rotation by means a migration mechanism involving the use of a single-purpose key which can be stored more securely than a user's main key.

Some terms:

- An `identity` key is a user's initial nostr public key and serves as the user's persistent identifier.
- An `authorized` key is a nostr public key that can sign on behalf of a user's `identity` key. A user's `identity` key is itself an `authorized` key until it is invalidated.
- A `migration` key is a nostr public key authorized in advance as the only way to generate or invalidate `authorized` keys.

A tree of these keys is created using three new event kinds, defined below. The current state of this tree MUST be validated as described in the [validation](#Validation) section below.

## Creating a Migration Key

Any `authorized` key can be used to sign a `kind 260` event with the following tags:

- A `p` tag containing the new `migration` pubkey and a relay hint
- A `proof` tag containing a schnorr signature of the `authorized` pubkey using the new `migration` key

Only the first `migration` key published by a given `authorized` key is valid. If multiple conflicting `kind 260` events exist, the first one published MUST be used. Migration keys cannot be invalidated except by invalidating the corresponding `authorized` key.

Migration keys MUST NOT be used for anything other than signing a single `kind 261` event.

## Migrating to a new Authorized Key

A `kind 261` migration event MUST have the following tags:

- A `p` tag containing a new `authorized` pubkey and a relay hint
- A `proof` tag containing a schnorr signature of the `migration` pubkey using the new `authorized` key

It MAY also include:

- An `as_of` tag containing a seconds-resolution timestamp for retroactive key invalidation

Only a `migration` key can sign a `kind 261` migration event. Only the first `kind 261` event published by a given `migration` key is valid. If multiple conflicting events exist, the first one published MUST be used.

Migrating away from an `authorized` key also invalidates the `migration` key used. All events created by an `authorized` key or its corresponding `migration` key after migration MUST be ignored.

This event MAY include a message in the `content` field. Clients SHOULD fetch this event and notify its users that a migration has taken place.

## Sequencing

Because event timestamps can be forged, a sequencer is required in order to establish the order in which events were actually published. All events defined in this document MUST be attested to using `kind 1040` events as defined in [NIP 03](./03.md). These events MUST be published to the tagged pubkey's inbox relays.

## Validation

Key state transitions MUST be validated according to the following process:

1. Select a `target` pubkey to validate.
2. Fetch `kind 260` events signed by the `target` key in order to identify the `target`'s `migration` key.
3. Fetch `kind 261` events signed by the `target`'s `migration` key. If any exist, events created by the `target` after its `as_of` (or `created_at`) field are invalid and should be ignored.
4. Fetch all `kind 260` events `p`-tagging the `target`. If no valid ones exist, the `target` key is an `identity` key and is `authorized` on its own behalf.
5. Repeat steps 1-4 for the author of the `kind 260` event which `p`-tags the `target` key until an `identity` key is reached, or a link fails to be established.

Because fake attestations can be created by third parties, attestations without a corresponding valid event may be discarded. However, genuinely missing events **may result in an invalid or incorrect key state**. For this reason, users should be careful to store events and attestations where they are easily discoverable. This protocol is *not* partition tolerant.

When checking the validity of an event published by an `authorized` key, it's important to keep in mind that events' `created_at` field may be forged, and so can't be trusted without reservation (although key validity windows do reduce the amount of damage an attacker can do). When validating events based on timestamp, it's recommended to obtain an sequencer attestation for those events as well.

## Threat Model

This NIP relies on the assumption that `migration` keys can be stored more securely than `authorized` keys.

Current nostr users have already exposed their keys in many places. This NIP allows those users to upgrade their account's security today without immediately migrating to the new key.

A general-purpose `authorized` key has to be stored on an internet-connected device in order to support frequent usage, while a single-purpose `migration` key can be stored offline and still remain useful.

Even if an attacker gains control of a user's `identity` key, they can't do any permanent damage if the user has already generated and secured a `migration` key.

However, if the user has **not** generated a `migration` key for their account, or their `migration` key has been compromised, an attacker can *lock the owner out of their account*, escalating key compromise to permanent account loss. They is the key trade-off of this NIP and exists for all users, regardless of whether they adopt it. For this reason, it is recommended that users generate a `migration` key early and store it securely. Additionally, if a `migration` key is generated and then lost, the user loses the ability to rotate their key.

## Usage Recommendations

### On Behalf Of Tag

When using a `authorized` key that is not the user's `identity` key, an "on behalf of" tag (`=`) containing the user's `identity` key SHOULD be included on events published by the user.

This provides a simple way to filter events for all of a user's `authorized` keys without knowing them in advance (though they still need to be validated).

### Denial of Service Risk

This NIP imposes a soft limit of 16 `authorized` keys per user. Implementations SHOULD stop validating keys after this point, and users SHOULD avoid creating more than 16 different keys. This limit is intended to leave ample room for multiple rotatations without allowing users to burden the network with key validations.

### Progressive Enhancement

In order to degrade gracefully when keys remain unlinked by implementations that don't support this NIP, important metadata events MAY be re-published under a new `authorized` key (especially kinds `0`, `10002`, `10050`, and any other events containing important routing information).

Authors MAY also re-publish other historical events under the new key (for example recent or pinned notes), however this should be done sparingly to conserve resources.

### Creating References

When third parties tag a user, they SHOULD reference the user's `authorized` key, not the user's `identity` key. This allows implementations that do not support this NIP to understand notes in their immediate context. Clients that do support this NIP are responsible for mapping the `authorized` key to its `identity` key if necessary for their use case.

### Implementation support

Clients SHOULD implement key validation. Relays MAY implement key validation and MAY drop invalid events from their database. Relays MUST NOT delete `kind 260`, `kind 261`, or `kind 262` events in response to a `kind 5` deletion request.

## Example

In this example, Alice creates three different keys:

- Key `A` is her `identity` key, since it's the first key she creates and where she begins her attestation chain. It is also by default an `authorized` key.
- Key `A'` is her `migration` key, which is used in turn to create a new `authorized` key `B`.
- Key `A` is later compromised and subsequently invalidated by `A'`

Throughout, Bob and Carol interact with Alice's account. Bob uses a client that lacks support for key rotation, while Carol uses a client that has it.

First, Alice decides she would like to opt-in to the ability to rotate her key without abandoning her original key, so she publishes a `kind 260` using her `identity` key `A`:

```json
{
"id": "<A.1>",
"pubkey": "<A>",
"kind": 260,
"tags": [
["p", "<pubkey A'>", "<url>"],
["proof", "<A signed by A'>"]
]
}
```

She also requests a `kind 1040` OTS attestation which `e`-tags event `A.1` to be published to her inbox relays.

At this point, nothing special needs to happen. Alice continues signing events with her `identity` key, and Bob and Carol carry on as usual. However, Alice's `identity` key eventually gets leaked and an attacker starts posting offensive memes.

The attacker also attempts to publish a new `kind 260` event, `A.2` in an attempt to hijack Alice's key state and point people to key `X`. However, both Alice and Carol properly reject it based on OTS attestations, while Bob ignores it entirely.

At this point, Alice needs to switch to a new key, so she publishes a `kind 261` event signed by her `migration` key `A'` designating a new key `B` to switch to:

```json
{
"id": "<A'.1>",
"pubkey": "<A'>",
"kind": 261,
"content": "My key got ganked! Unfollow this key and follow <B> instead.",
"tags": [
["p", "<pubkey B>", "<url>"],
["proof", "<A' signed by B>"],
["as_of", "<X.1.created_at>"]
]
}
```

She also requests a `kind 1040` OTS attestation which `e`-tags event `A'.1` to be published to her inbox relays.

Alice also decides to re-publish her `kind 10002`, `kind 10050`, and `kind 0` events under the new `authorized` key `B`. She then sends event `A.3`, a `kind 1` signed by key `A`, telling her followers that she has switched to key `B`, and event `B.1`, a `kind 1` signed by key `B` welcoming people to her new key. She adds a `=` tag to `B.1` pointing to her `identity` key, `A`.

Bob is following Alice, and normally reads only from key `A`. He sees Alice's `kind 1` post (as well as several unfunny memes from the attacker), unfollows `A` and starts following `B`. Alice's profile loads fine, but their DM history and all her past notes are invisible to Bob.

Carol is also following Alice, but her client automatically picks up events `A.1`, `A.2` and `A'.1`, ignores `A.2` based on the corresponding OTS attestations, and marks key `B` as `authorized` to post on behalf of `A`. Her client then sends a `REQ` like this:

```typescript
[{"kinds": [1], "authors": ["<A>"], until: "<A'.1.as_of>"},
{"kinds": [1], "#=": ["<A>"]}]
```

This returns both of Alice's `kind 1` events: `A.3` (via the `authors` filter), and `B.1` (because of the `#=` filter), while successfully ignoring events created by the attacker after key invalidation took place.

Finally, Alice publishes event `B.2`, which is a `kind 260` event designating key `B'` as her new `migration` key.

At the end of the day, this is the state of Alice's keys:

- `A` remains her `identity` key, but is no longer `authorized` as of `A'.1.as_of`.
- `A'` (her first `migration` key) is invalid as of `A'.1.as_of`.
- `B` is `authorized` to publish events on behalf of her `identity` key `A`.
- `B'` is a valid `migration` key, able to invalidate `B` and create new `authorized` keys.
- `X` is not and never was a valid `migration` key, because `A'` was created first. Any attempts by the attacker to create new `authorized` keys will be ignored.

## Appendix: Ownership Proofs

Kinds `260` and `261` require that users prove they own a given pubkey before designating it either as a `migration` key or an `authorized` key.

This proof is deliberately designed **not to use common event signing APIs** in order to ensure that the user has direct access to their private key. This helps prevent a malicious signer or client from signing key rotation events via bunker URL or browser extension.

Below is some example typescript code for creating and validating a key creation proof:

```typescript
import { schnorr } from '@noble/curves/secp256k1'
import { bytesToHex } from '@noble/hashes/utils'

// Generate our keys
const parentSecret = schnorr.utils.randomPrivateKey()
const parentPubkey = bytesToHex(schnorr.getPublicKey(parentSecret))
const childSecret = schnorr.utils.randomPrivateKey()
const childPubkey = bytesToHex(schnorr.getPublicKey(childSecret))

// Prove that the user owns the child key
const proof = bytesToHex(schnorr.sign(parentPubkey, childSecret))

// Verify the proof
const valid = schnorr.verify(proof, parentPubkey, childPubkey)
```
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
- [NIP-B7: Blossom](B7.md)
- [NIP-C0: Code Snippets](C0.md)
- [NIP-C7: Chats](C7.md)
- [NIP-D8: Key Rotation](D8.md)
- [NIP-EE: E2EE Messaging using MLS Protocol](EE.md)

## Event Kinds
Expand Down Expand Up @@ -144,6 +145,8 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
| `44` | Channel Mute User | [28](28.md) |
| `62` | Request to Vanish | [62](62.md) |
| `64` | Chess (PGN) | [64](64.md) |
| `260` | Create Migration Key | [D8](D8.md) |
| `261` | Migrate Key | [D8](D8.md) |
| `443` | KeyPackage | [EE](EE.md) |
| `444` | Welcome Message | [EE](EE.md) |
| `445` | Group Event | [EE](EE.md) |
Expand Down