CORD-06: Rekeys and Refoundings
Non-ratcheted, asynchronous key rotation for post-removal secrecy.
Rekeys rotate a Channel’s encryption key to cryptographically sever a removed user (or many), authorized by a valid administrator. A single-channel rekey typically follows removing someone’s access to a specific channel — e.g. removing a role (Tester) with access to a private channel (#testers).
Refoundings are whole-Community Rekeys, rotating the base community key in addition to all relevant channels. This removes someone absolutely from a Private Community, severing even the uppermost planes.
1. Rekey Blobs - The 3303 event
Section titled “1. Rekey Blobs - The 3303 event”Key distribution rides “Rekey Blobs”: a Community-encrypted kind 3303 package delivering the fresh key to up to 120 participants per event, spanning several events for a large community.
A rekey has a scope, either a Channel (a specific Private Channel’s ID) or the community_root (a base rotation):
Scope = Channel(channel_id) | community_rootid32(Scope) = channel_id[32] | 0x00…00 (all-zeroes, never collides with a Channel ID)Private Channels are independently keyed (cryptographically unrelated to the community_root, CORD-03), so a compromised Private Channel exposes only itself and can be rekeyed alone. A Public Channel derives its key from the community_root, so it has no independent rekey — it rotates only when the base does (a Refounding).
Because a rekey targets one specific key, it must name exactly which one it replaces: Scope binds to the blob’s ciphertext (not just an outer tag), so a blob minted for one channel can never be replayed against another.
The Rekey Event:
{ "kind": 1059, // gift wrap "pubkey": "<group_pk>", // the rekey address (community_root-derived) "tags": [["p", "<random ephemeral pubkey>"]], // ephemeral, like any Stream event (CORD-01) "content": "nip44_encrypt(conv_key, { // seal "kind": 20013, "pubkey": "<rotator real pubkey>", "sig": "<rotator real signature>", // authorship + authority actor "content": "nip44_encrypt(conv_key, { // rumor "kind": 3303, "pubkey": "<rotator real pubkey>", "content": "[ {blob}, {blob}, … ]", // per-recipient blobs (§2) "tags": [ ["scope", "<id32(Scope) hex>"], // all-zero hex = community_root ["newepoch", "<decimal>"], ["prevepoch", "<decimal>"], ["prevcommit", "<hex>"], ["chunk", "<i>", "<n>"] // this event is chunk i of n for the rotation ] })" })"}Each blob in the content array is one located, wrapped key:
{ "locator": "<hex>", // where its recipient finds it (§2) "wrapped": "<base64>" } // NIP-44 ciphertext under the Rotator↔recipient pairwise keyThe wrapped plaintext is fixed-width per form, the width declaring the form. A Channel rotation’s blob is 72 bytes: scope_id[32] ‖ epoch_be[8] ‖ new_key[32]. A base rotation also carries the next epoch’s Control Plane keys (CORD-02 §2): a member’s blob is 104 bytes — scope_id[32] ‖ epoch_be[8] ‖ new_root[32] ‖ new_control_pk[32] — and a staff recipient’s (CORD-04 §3) is 136, appending new_control_root[32]. One historical form joins these: a 72-byte base blob is a legacy, pre-split rotation (§3), honored when reading old epochs and never minted anew. Any other width is malformed and the blob is dropped. The scope and epoch live inside the ciphertext, and a recipient verifies both against the event’s tags before accepting the key, making a blob unspliceable; a staff recipient additionally requires that new_control_root derive to exactly new_control_pk (CORD-02 §5), refusing a mismatched pair rather than adopting a plane split from its readers. The wrap key is the NIP-44 conversation key between the Rotator and the recipient — one ECDH either side can compute, so a NIP-46 bunker account opens its blob with a single nip44_decrypt, no raw-key access needed.
2. Receiving & Processing Rekeys
Section titled “2. Receiving & Processing Rekeys”A receiver detects whether they’ve been removed from the channel or community; the client then either visibly removes them or switches to the new keys (a new epoch).
Subscription. Rekeys arrive in real-time by precomputing the next rekey address:
{ "kinds": [1059], "authors": [ // per PRIVATE Channel you hold; the NEXT channel-epoch's rekey address: group_key("concord/rekey-pseudonym", community_root, channel_id, channel_epoch + 1).pk, // and the next base-rotation address: group_key("concord/base-rekey-pseudonym", prior_community_root, community_id, root_epoch + 1).pk ]}Each key blob has a locator, derived from the Rotator’s and recipient’s public keys plus the rotation scope and epoch:
locator = hkdf(rotator_xonly || recipient_xonly, "concord/recipient-pseudonym", scope_id, epoch)To check if you are part of a rekey, compute your locator and search for it within all rekey events, and you must validate the rekey came from a role-authorized administrator before accepting it. A rotation to many recipients spans several events, each tagged ["chunk", i, n] and correlated by the Rotator (the seal’s npub) at one newepoch and prevcommit, so two Rotators concurrently rekeying the same epoch never merge into one set:
- If any chunk contains your locator, decrypt the new keys and shift to the new epoch.
- Only once you hold all
nchunks and none contains your locator have you been removed. A missing chunk is never a removal — the client refetches until the set is complete before concluding anything.
The locator derives from public inputs on purpose, and it leaks nothing an outsider can reach: the locator list lives inside the encrypted event, the Rotator’s npub is sealed, and channel ids are never public, so only a key-holding member can compute or search a locator. A member can confirm a fellow member’s presence in a rotation — information members effectively hold anyway (the member list, Banlist, and Guestbook are all member-visible) — and the trade buys something real: a locator computes from public keys alone, so a NIP-46 bunker account finds its blob without touching a raw private key.
Before adopting a new key, a receiver must also verify continuity: recompute the epoch-key commitment (CORD-02, A.5) over the key it currently holds and require it to equal the event’s prevcommit, with all chunks of one rotation carrying identical continuity fields. A match proves the rotation extends the very key you hold; a mismatch with a higher prevepoch means you missed a rotation (fetch the gap first); any other mismatch is a fork or garbage, reject it. prevcommit is a convergence check, not a secrecy mechanism — post-removal secrecy rests entirely on a removed member receiving no blob; prevcommit just keeps honest members advancing along one shared chain.
3. Refounding
Section titled “3. Refounding”A Refounding is a deep whole-Community rekey, performed when banning a member from a Private Community or when converting a Public Community to Private:
- The Refounder prepares a compaction of the current Control Plane into a new, slim representation. If the Refounder cannot reliably fold all Control events, the Refounding must be aborted.
- The
community_rootis rolled, and a freshcontrol_rootis minted alongside it (CORD-02 §2). The pair travels in the same base blobs (§1) — every member receives the newcontrol_pk, staff also the secret — so converging on a base rotation converges both. - The compacted Control Plane is republished at the new epoch’s Control address — signed by the new
control_root-derived signer, readable under the newcommunity_root(CORD-02 §5) — only after confirmed publication of the root roll. - All Private Channels relevant to the removed user(s) are rekeyed, each sealed and addressed under the prior
community_root, never the freshly minted one (Public Channels rotate with the base for free, CORD-03). This matters under a race: if two Refoundings collide, the base converges on one winner and the losers drop their new roots, so a channel rekey sealed under a new root would be unreadable to every base-fork loser; sealing under the shared prior root keeps it openable on either branch. - The new epoch’s Guestbook is seeded with a membership snapshot (CORD-02 §5), a best-effort final step — a Refounding succeeds with or without it.
After a Refounding, the Control Plane shrinks back to its smallest size by trimming prior history into a snapshot of the prior epoch’s state: the last Control Plane state is simply rewrapped, and because Control Plane seals are plaintext (CORD-02 §5), the re-encryption under the new epoch preserves the original authors’ signatures, verifiable by a fresh joiner.
Compaction keeps Refoundings fast and asynchronous: members hop across dozens of epochs with thousands of Control events without reprocessing the entire Control Plane each time.
Authority. A single-channel Rekey requires MANAGE_CHANNELS, a Refounding requires BAN, and in both the Rotator must strictly outrank every removed target (CORD-04). A receiver verifies the seal’s real npub against its folded Roster before honoring anything, and a rotation cites the Grant it acts under like any authority action (CORD-04’s vac), so a just-demoted admin’s rotation is never honored by a lagging client. Holding a key is never authority: a removed member still holding the prior root — or a demoted staffer still holding the control_root (CORD-02 §2) — can construct a perfectly shaped rotation or edition, and every honest member opens the seal, folds the Roster, and drops it.
Failure and races. A Refounding is resumable, not atomic: every step is idempotent (re-publishing a compaction re-wraps the same signed heads, re-sending blobs re-delivers the same keys), so a crashed Refounder simply resumes. Mid-gap, the community degrades gracefully: existing members keep their old Control fold and already hold the new root; only a fresh joiner waits on the re-anchor. The state being rotated is acquired in full before the first publish, so a mid-flight failure never leaves half a rotation as the only copy.
Two rotations racing to the same epoch converge deterministically: among authorized candidates at the same continuity point, the lexicographically lowest new base key wins — the control_root pair rides the winner’s blobs (§1), never compared — every client computes the same winner, and a losing Refounder re-issues anything only its branch knew (its fresh channel keys) on the winning chain, its own minted pair dropped with its root. Both forks’ keys are retained, so messages sent into the losing fork stay readable, and the same-epoch heal is down-only — a held epoch re-converges solely to a strictly lower sibling, so a flaky fetch that returns only the higher sibling can never re-fork a settled epoch.
Upgrading a legacy Community. An epoch minted before the control_root split (CORD-02 §2) keyed the Control Plane by the member-held legacy derivation, which every client retains for reading such epochs (CORD-02 §5). The upgrade is not a new mechanism, it is the next Refounding: a compliant Rotator performing any base rotation MUST mint the split — a fresh control_root beside the new root, delivered in the §1 blob widths — so a private Community upgrades as a side effect of its next ban, with nobody deciding to. The epoch is the version boundary and the blob width the signal, no version field needed (CORD-02, Appendix B): a 104- or 136-byte base blob announces an upgraded epoch, while a 72-byte one is the legacy form — honored when reading old rotations, its acceptor folding that epoch’s Control at the legacy address, and never minted by a compliant Rotator. A Public Community may go years without a ban, so a client SHOULD offer staff a deliberate upgrade Refounding; until one lands, the plane stays exactly as writable as it always was — the pre-split status quo, never worse.
Stale software degrades honestly, in two grades. A legacy client that reads its base blob leniently — the legacy layout is a strict prefix of the new — still extracts the new community_root: membership and every Chat plane survive, and only its Control fold freezes at the boundary epoch, a safe prompt to update, since a frozen fold can be starved of new state but never fed forgeries. One that enforces the old 72-byte width drops the blob and parks at the prior epoch entirely, the same experience as a missed rotation. Either way a staffer on stale software cannot write at the new address at all, so staff update first — the Refounder’s client, by definition, already has. What a Rotator MUST NOT do is mirror editions to the new epoch’s legacy-derived address to appease stale readers: the mirror re-opens exactly the member-writable surface the split closes.