Expand description
Key-derivation convention (GROUP_PROTOCOL.md) — FROZEN.
Every HKDF use in the Community protocol funnels through here. Changing any byte of the construction shifts every pseudonym and sub-key, orphaning all prior events — a forced migration to avoid. The layout is locked by the golden vectors in the test module; treat those as the spec.
Construction: HKDF-SHA256(IKM, salt=∅, info, L=32), where
info = utf8(label) || 0x00 || id32 || epoch_be —
label: ASCII purpose string, no terminator0x00: single separator byteid32: raw 32-byte id (channel id, or scope id), never hexepoch_be: the epoch as u64 big-endian (8 bytes); omitted where noted
Enums§
- Rekey
Scope - Scope of a per-recipient rekey blob. Disambiguates two blobs a single sender delivers to the same recipient in one epoch (a server-root rotation and a channel rekey), which would otherwise collide on the same tag.
Functions§
- banlist_
locator - Opaque coordinate for the banlist entity, HKDF-derived from the community id — a STABLE
logical id that survives a server-root rotation, so a re-anchored banlist binds the same coordinate
at every epoch (re-anchoring). Member-computable (members hold the community id from their
invite), outsider-opaque (the id is never on the wire — the relay sees only the rotating
control_pseudonym), and the content stays server-root-encrypted, so privacy is unchanged. - base_
rekey_ pseudonym - The relay-filterable address of a SERVER-ROOT (base) rekey for
(community, new_epoch). Keyed by the PRIOR server-root key — the base layer has no stable key above it, so the prior root is the handle every current member holds: a returning member derives this from the root they currently hold, finds the base rekey, learns the rotator from its inner sig, and recovers the next root (a short forward-walk; base rotations are rare). Binds the community id + epoch, and is label-separated from the channel-rekey / channel-message / control pseudonyms. - channel_
pseudonym - Channel pseudonym: the value carried in the relay-filterable
ztag. Every member derives the same one from the shared channel secret, so it both addresses and (by rotation) unlinks the channel’s traffic. - dissolved_
envelope_ key - Rotation-stable envelope key for the dissolution tombstone — community-id-derived so the tombstone is
openable by any member or joiner at ANY epoch. The control plane is server-root-encrypted (per-epoch),
which a post-rotation joiner can’t open for the publish-epoch; the tombstone carries no secret (content
is
{}), so a community-id key is the right scope — member-computable, outsider-opaque, epoch-free. - dissolved_
locator - Opaque coordinate for the owner-dissolution tombstone (vsk=10), HKDF-derived from the community
id — STABLE across a server-root rotation, exactly like
banlist_locator. This rotation-stability is load-bearing for dissolution: a fresh joiner after a re-founding derives only the NEW epoch root, but can still compute this community-scoped coordinate and discover the tombstone, so a dissolved community can never look “alive” to anyone who can derive the community id. Member-computable, outsider-opaque. - dissolved_
pseudonym - Rotation-stable relay
#zfor the dissolution tombstone — community-id-derived (NOT the per-epochcontrol_pseudonym), so ANY client that can derive the community id finds the tombstone at the SAME coordinate regardless of which epoch root it holds. This is what closes the post-rotation discoverability split: a fresh joiner who only ever derives a later epoch’s root still probes this fixed coordinate and learns the community is dead. Outsider-opaque (community id is never on the wire). - grant_
locator - Opaque coordinate for a member’s Grant entity (vsk=3), HKDF-derived from the community id
bound to the member’s x-only pubkey. Community-scoped (not server-root-scoped) so the coordinate is
STABLE across a base rotation — the keystone that lets a re-anchored grant fold under the new root
(re-anchoring): a new joiner holding only the new root still derives the same
entity_id. Member-computable, outsider-opaque (community id never on the wire), content still server-root- encrypted — privacy unchanged. Roles need no locator (theird-tag is the role’s random id). - invite_
links_ locator - Opaque coordinate for a CREATOR’s own invite-links entity (vsk=8) — the per-creator list of
active public-invite-link locators THEY published. Bound to the creator’s x-only pubkey exactly like a
per-member grant (
grant_locator), so a creator can only publish links at their own coordinate, and members fold every creator’s list into the aggregate active-set (is_public= aggregate non-empty). Community-id-derived (stable across rotation, member-computable, outsider-opaque). There is no shared registry — each creator owns only their own list (per-creator ownership). - public_
invite_ key - Public-invite sub-keys, all derived from the URL fetch-token. The token
is the IKM and there is no channel/epoch context, so the frozen
infouses the all-zero id and no epoch — the token alone provides uniqueness. The three labels domain-separate the decryption key, the relay locator (addressabled-tag), and the bundle’s signing key (so the owner can re-post under one coordinate to rotate, and joiners reject an impostor squatting the locator). - public_
invite_ locator - public_
invite_ signer - recipient_
pseudonym - Per-recipient rekey-blob tag.
IKMis the pairwise sender↔recipient ECDH secret (not the channel key), so only that pair can locate the blob and a removed member cannot derive tags for pairs they are not in. - rekey_
pseudonym - The relay-filterable address of a channel REKEY event for
(channel, epoch). Derived from the server-root key (NOT the channel key) + the channel id + the epoch the rekey introduces. Because the IKM is the server root — which every member always holds and which is stable across a channel rotation — any member can compute this for ANY epoch directly, WITHOUT holding that epoch’s (or the prior epoch’s) channel key. That is what makes epochs independently recoverable: a member fetches the rekey for whichever epoch(s) they choose (latest only, or all, in parallel), rather than chaining forward one key at a time. Distinct from the channel message pseudonym (channel key IKM) and the control pseudonym (community-id binding) by IKM/id, and domain-separated by label.