Join Nostr
2026-09-15 13:02:36 UTC
in reply to

inkan on Nostr: Congrats on shipping this. One thought on "NIP-4e, which separates encryption keys ...

Congrats on shipping this. One thought on

"NIP-4e, which separates encryption keys from identity keys"

The hard part is how senders know which encryption key is currently yours, especially how they can know when an old encryption key stopped being yours. Just announcing this through a 10044 doesn't do the trick, since senders can never know if they've received the latest announcement and thus can't be sure that they aren't sending their messages to a compromised encryption key.

Inkan solves this by recording key delegation and revocation declarations on-chain. The mechanism doesn't care what the key is used for, so it could be used under a 4E announcement.

I actually started experimenting with double ratchet, because Inkan makes it easy for an identity to assign (and revoke / rotate) device-bound keys.

NIP Proposal

draft optional

This NIP specifies online identities whose private key can be created in cold storage and stay there permanently. The key never has to touch an internet-connected device, and does not have to be kept close at hand. The identity can nonetheless be used easily for everyday online authentication, such as posting, replying, messaging, or signing in to services.

1. How it works

The identity-securing key never signs Nostr events itself. It delegates signing authority to delegatee keys, which handle the everyday signing on its behalf. If a delegatee key is lost or stolen, it is replaced, and the identity is untouched.

2. Delegations between keys

Delegations are time-indexed two-place relations. Between any two key pairs, at any moment, the first either delegates signing authority to the second or it does not. When it does, the second key is authorized to sign on the first's behalf. When it does not, the second has no such authority.

Delegations are either direct or indirect. A direct delegation arises when one key has expressly declared that it delegates signing authority to another. An indirect delegation arises because the relation is transitively closed: if X delegates to Y and Y delegates to Z, then X indirectly delegates to Z. This is what lets an identity be built as a chain (master → intermediate → signer), so that events signed by the bottom key are attributed all the way up to the master, while the master, having signed the top link once, retires to cold storage.

3. How a direct delegation is created

A direct delegation from key pair X to key pair Y is created when both key pairs sign a Declaration of Delegation of Signing Authority, by which X declares that it delegates signing authority to Y and Y declares that it accepts. The signed declaration is then recorded on-chain (the reference implementation uses Ethereum). The delegation begins at the time of the block in which the declaration was recorded, and remains in effect until it is revoked.

One special case: this holds only if neither X nor Y has previously permanently invalidated itself (§5). If either has, no delegation is created, even though the declaration sits on-chain.

As part of the declaration, X and Y also specify whether X can revoke the delegation unilaterally or whether Y's consent is required. The default is unilateral revocation by X, and it is the right default for most users: if Y's key is ever lost, X can still revoke it alone. Were Y's consent required, a lost Y could never be revoked, because Y would no longer be available to sign.

4. How a direct delegation is terminated

A direct delegation from X to Y is terminated when X signs a Declaration of Revocation of Signing Authority and records it on-chain, with Y's signature too if the original delegation required it. The delegation terminates at the time of the block in which the revocation was recorded. Everything Y signed while the delegation was in effect stays attributed to X; nothing it signs afterward does. A stolen signer therefore damages only the window between key compromise and revocation, and that window is the user's to close.

When a delegation and a revocation between the same X and Y land in the same block, the revocation is by convention treated as occurring first and the delegation thereafter. The convention is a tiebreaker for cases that block timestamps cannot order.

5. Permanent key invalidation

The third declaration is the Declaration of Permanent Invalidation of a Key Pair, signed by a key pair against itself, declaring that from then on it can no longer participate in any direct delegation, as delegator or delegatee. Once recorded on-chain, it takes effect at the time of its block. From that point, all existing direct delegations involving the key are terminated, and no new one involving it can come into existence, even if a declaration naming it is later recorded.

Its main use is invalidating a compromised master key. Such a key has no delegator above it, so ordinary revocation is unavailable; permanent invalidation lets its owner withdraw it from the system entirely.

6. Delegation timelines

A client can read the three kinds of declaration from the chain and, for any pubkey and any past moment up to the present, compute the direct delegations that pubkey was involved in at that moment. The indirect ones follow by transitive closure. So for any pubkey a client can compile a complete timeline of every direct and indirect delegation it has ever been involved in, and trace each back to the declaration that established it. Because the rules are fixed and the data is public, every client computes the same timeline, and no server has to be trusted to report it.

7. Bitcoin timestamping of events

Attribution needs an objective, auditable rule for when the signing of each event is deemed to have occurred. A self-declared created_at proves nothing on its own.

Events are anchored to Bitcoin using OpenTimestamps (OTS). An OTS proof for an event establishes that the event and its signature existed by the time of a specific Bitcoin block. An event's created_at is then treated as its objective signing time when a proof shows the event and its signature existed shortly after that time. The length of the "shortly after" window is configurable; the reference implementation defaults to four hours. Events without such a proof are treated as if they carried no valid signature at all: they are not displayed, and are attributed to no one.

A proof over the event id alone would show only that the event's contents existed by the attested block, not that anyone had yet signed them. Binding the signature into the proof as well shows that the signature itself existed by then, which is what an objective signing time requires. These proofs are published as kind 31045 events (§12).

8. Attribution of events

We now know, for any past time T and any two keys X and Y, whether X delegated (directly or indirectly) to Y at T; and for any event Y signed with both a valid signature and a valid OTS anchor, we have an objective signing time. Together these give a well-defined attribution rule:

An event E is attributed to a key X if and only if some key Y validly signed E at a time T and, as of T, X delegated signing authority to Y.

So a text note signed by a delegatee Y during a valid delegation period from X appears on X's timeline and is displayed there under X's profile, as if posted by X, even though X's own key never signed it.

9. Two profiles, one identity

You log in with your signer key and sign events with it, exactly as in any other Nostr client. Those events appear on the signer's own profile, and in addition on the profile of each of its delegators, up to your top-level identity.

You set the signer's profile by publishing a kind-0 event as usual. Separately, a signer can also set a profile for each of its current delegators, so that a top-level identity can present a face to the world without signing any event itself. This delegator-profile mechanism is kind 31065 (§12).

Your identity's profile is what you present to the world. Others should follow and interact with the identity, not with the signer beneath it. The signer is ephemeral and expected to be replaced; the identity is permanent, and is the thing followers should attach to.

10. Disavowal

When making a delegation declaration, the delegator may declare an avowal start time and an avowal end time. When making a revocation declaration, the revoker may declare a disavowal start time and a disavowal end time. A start time of zero is interpreted as the time at which the declaration was recorded on-chain; an end time of zero means unbounded.

For keys X and Y, take the latest delegation or revocation declaration between them recorded before any permanent invalidation of either (§5). If it is a delegation declaration, X avows Y's actions at exactly the times T inside its avowal window; if a revocation declaration, at exactly the times T outside its disavowal window; provided, in both cases, that X directly delegated to Y at T. Y's actions at T are the events Y validly signed with objective signing time T (§7).

Avowal composes along delegation paths: X currently avows S's actions at T if the relevant declaration between X and S avows them directly, or if, for some key Y, X avows Y's actions at T, X avows Y's actions at the present moment, and Y currently avows S's actions at T.

Clients can filter events whose signing the identity currently disavows out of the identity's timeline, or display them with a disavowal badge.

11. Re-ratification

A disavowed window covers everything the disavowed key signed within it, including events whose signing was caused by the identity's legitimate owner. The identity can in that situation make a kind 31066 re-ratification declaration as described in §12.3 below, specifying the events it re-ratifies in the e tags of that declaration. If the declaration is valid and effective, a client treats these events as re-ratified, until a disavowal recorded after the declaration's anchoring block prevails over it (§12.3) or a later declaration governs in its place.

12. Event kinds

This NIP defines three addressable event kinds (kinds 30000 to 39999 in NIP-01, where the latest event for a given (pubkey, kind, d-tag) replaces any earlier one).

Kind Name d tag Signed by
31045 Bitcoin timestamp id of the referenced event any party
31065 Attributed profile the delegator's pubkey a delegatee
31066 Re-ratification the re-ratifying pubkey an avowed key

12.1 Kind 31045: Bitcoin timestamp

A 31045 event carries an OpenTimestamps proof for another event (§7). It is addressable by the referenced event's id, so a consumer finds the proof for an event E by querying kind 31045 with #d = [E.id], whoever published it. Any valid proof for E is as good as any other, because the proof rests on Bitcoin and not on its publisher.

Tag Required Meaning
d yes Id of the referenced event. The addressable key.
s yes Signature of the referenced event. With d, binds the proof to a signed event, not just an id.
p no Author pubkey of the referenced event.
b no Bitcoin block height of the attestation, or not_yet_available while pending.
t no Bitcoin block time (unix seconds), or not_yet_available while pending.
c no created_at of the referenced event, when known.
alt no NIP-31 summary, e.g. Complete Bitcoin Timestamp.

content is the base64-encoded OpenTimestamps (.ots) proof. Its leaf must commit to sha256(referenced_id || referenced_signature), the concatenation of the referenced event's 32-byte id and 64-byte signature. A consumer should verify the proof against its own view of the Bitcoin chain rather than rely on a relay having accepted it.

Only d and s, together with a verifiable content proof, are required to verify a timestamp. Every single-letter tag (d, p, s, b, t, c) is relay-indexed, so it can also serve as a query key, for example finding proofs by referenced author (#p) or by anchoring block (#b). b and t additionally duplicate what the proof itself attests; alt is not indexed and serves only as a NIP-31 display label.

{
  "kind": 31045,
  "tags": [
    ["d", "<referenced-event-id>"],
    ["p", "<referenced-event-author>"],
    ["s", "<referenced-event-signature>"],
    ["b", "<bitcoin-block-height>"],
    ["t", "<bitcoin-block-time>"],
    ["alt", "Complete Bitcoin Timestamp"],
    ["c", "<referenced-event-created-at>"]
  ],
  "content": "<base64 .ots proof committing to sha256(id||sig)>"
}

12.2 Kind 31065: Attributed profile

A 31065 event is a profile for one key, published by another: how an identity that never signs anything nonetheless presents a face to the world (§9). Its d tag is the delegator's pubkey, the identity the profile describes, while the event is signed by a delegatee authorized to sign for it. The defining property is that event.pubkey, the signer, is not the subject d. A consumer must confirm, against the delegation timeline of §6, that the signer was a current delegatee of the subject before honoring the profile; one signed by a non-delegatee is ignored.

Tag Required Meaning
d yes The subject: the delegator's pubkey.
alt yes The exact string Attributed profile.

content is a JSON object carrying the subject's presentation. Its kind-0-style fields (name, display_name, about, picture, banner, website, nip05, lud16) mean what they mean in NIP-01 and NIP-05. It may also carry the subject's follow list, mute list, and a map giving relay lists for the members of the identity's delegation network (the keys tied to it by delegation), so a client can present and route for the identity without a further fetch.

{
  "kind": 31065,
  "pubkey": "<delegatee-that-signs>",
  "tags": [
    ["d", "<delegator-pubkey-the-profile-is-for>"],
    ["alt", "Attributed profile"]
  ],
  "content": "{\"name\":\"…\",\"display_name\":\"…\",\"about\":\"…\",\"website\":\"…\",\"nip05\":\"…\",\"picture\":\"…\",\"relay_lists_by_pubkey\":{\"<pubkey>\":{\"readWriteRelays\":[[\"r\",\"<relay-url>\",\"<scope>\"]],\"favoriteRelays\":[\"<relay-url>\"],\"relaySets\":[{\"id\":\"…\",\"name\":\"…\",\"relayUrls\":[\"<relay-url>\"]}]}},\"publicly_muted_pubkey_hex_ids\":[\"<muted-pubkey>\"],\"current_snapshot_of_dir_indir_followed_pubkey\":[[\"p\",\"<followed-pubkey>\"]]}"
}

Because 31065 is addressable, the current profile for an identity is simply the latest one whose d is that identity, signed by a key that is currently its delegatee. When the signer is rotated, the new signer republishes, and the identity's face carries across the change unbroken.

12.3 Kind 31066: Re-ratification declaration

A 31066 event can be used by key A to re-ratify an event signed by key B with objective signing time T (§7), where A currently disavows actions taken by B at T (§10). The d tag contains the re-ratifying pubkey and each e tag is the id of a re-ratified event. The re-ratification is valid only if the 31066 event is signed by a key whose actions the re-ratifying key currently avows (which includes the re-ratifying key itself), and only if no e tag contains the declaration's own event id. The 31066 event must be submitted for Bitcoin timestamping and is effective starting from the time of the block in which its proof is anchored. If there are multiple valid and effective 31066 events with the same re-ratifying pubkey in their d tag, the one with the latest created_at governs. A re-ratification overrides only disavowals recorded on-chain before the time of its anchoring block; a disavowal recorded after that time prevails over the re-ratification.

Tag Required Meaning
d yes The re-ratifying pubkey.
e yes, repeated Id of a re-ratified event, one tag per id.
alt yes Re-ratification declaration
{
  "kind": 31066,
  "pubkey": "<signing-key>",
  "tags": [
    ["d", "<re-ratifying-pubkey>"],
    ["e", "<re-ratified-event-id>"],
    ["e", "<re-ratified-event-id>"],
    ["alt", "Re-ratification declaration"]
  ],
  "content": ""
}

13. Reference implementation

Inkan (https://www.inkan.cc) implements this specification as a working Nostr client and a cooperating relay at wss://relay.inkan.cc. Delegation, revocation, permanent invalidation, avowal, and disavowal are recorded by an Ethereum contract deployed at 0x385C0A272b1f44687cb360557b25827bD7CdB7eb, which is the normative reference for the exact on-chain declaration encoding. A separate, network-disconnected "Management Utility" generates keys and signs declarations on an air-gapped machine, so a master key can be created and used to bootstrap an identity without ever touching a networked host.