Expand description
Rekey blob primitive (GROUP_PROTOCOL.md).
A rotation (a Private removal, a re-founding, or a scheduled rekey) mints a fresh-random key for the next epoch and delivers it to every member who STAYS, one per-recipient blob each. This module is that atom — the located, wrapped single blob — built and opened in isolation. The 3303 event that carries a set of these blobs, and the apply/recipient-set logic, sit on top (later sub-pieces).
Each blob is:
- located by
recipient_pseudonym(pairwise_secret, scope, epoch): an opaque tag only the sender↔recipient pair can compute, so a recipient jumps straight to their own blob (no trial-decryption) and a removed member can’t even find a slot for a pair they’re not in; and - wrapped under the same pairwise secret via NIP-44 v2 (
cipher), so only that recipient decrypts it.
The pairwise_secret is the NIP-44 v2 ConversationKey between the two identities — the ECDH-derived
secret names. It is symmetric: the sender derives it from (their sk, recipient pk), the
recipient recomputes the identical secret from (their sk, sender pk). (How the recipient learns
the sender’s pubkey is the 3303-envelope layer’s job, not this atom’s — open_rekey_blob takes it
as a parameter.)
The wrapped plaintext binds (scope, epoch) so a blob can’t be spliced into a different
coordinate: even though only the authorized rotator can mint blobs, the binding makes a
cross-scope/epoch reuse fail closed on open, the same discipline as the message envelope.
Structs§
- Parsed
Rekey - A parsed, inner-signature-verified Rekey — what
open_rekey_eventyields. Authority (does the rotator’s roster rank permit this rotation?) and blob-opening are SEPARATE later steps: therotatorpubkey here is what the recipient pairs against inopen_rekey_blob, and what the roster check is run against. - Rekey
Blob - One located, wrapped rekey blob — the unit a 3303 Rekey event carries N of.
locatoris the recipient-pseudonym hex (where the recipient finds it);wrappedis the base64 NIP-44 ciphertext ofscope_id ‖ epoch ‖ new_key.
Functions§
- build_
channel_ rekey_ event - Build a signed 3303 channel-Rekey distributing
blobsintroducingnew_epochforchannel_id. - build_
rekey_ blob - Build one rekey blob: the fresh
new_keyfor(scope, epoch), located + wrapped torecipient_pk.sender_skis the rotator’s identity secret (the ECDH half the recipient pairs against). - build_
server_ root_ rekey_ event - Build a 3303 SERVER-ROOT rekey (a base rotation): the new server root reaches recipients ONLY
via per-recipient ECDH blobs (
RekeyScope::ServerRoot), while the event itself is enveloped under the PRIOR root and addressed bybase_rekey_pseudonym(prior_root, community_id, new_epoch). - epoch_
key_ commitment - A commitment to the prior epoch’s key (fork detection): a Rekey references it so two managers
who both rotate epoch N→N+1 produce a detectable fork (resolved by authority-first→time→id),
and so a recipient who holds the prior key can confirm the rotator did too (a legitimacy check).
Domain-separated SHA-256 over
prev_epoch_be ‖ prev_key; the epoch binds the commitment to the specific link in the chain. - open_
rekey_ blob - Open a blob addressed to me: recompute the pairwise secret from
(my_sk, sender_pk), confirm the blob’slocatoris the one THIS pair+scope+epoch produces (rejects a blob handed to us under the wrong coordinate), decrypt, and verify the wrapped plaintext binds the SAME(scope, epoch)before returning the new key. Any mismatch (wrong sender, wrong coordinate, tamper, splice) isErr. - open_
rekey_ event - Open + verify a 3303 Rekey outer with the server-root key (which every member always holds):
version-check, decrypt, parse the inner, verify the rotator’s inner signature, and read the rekey
fields. Does NOT check authority (the rotator’s roster rank) or open any blob; the caller does
both, pairing against the returned
rotator. A wrong key (or non-member) fails the MAC →Err. - rekey_
pairwise_ secret - The pairwise sender↔recipient secret: the NIP-44 v2 ConversationKey, HKDF-extracted from
the ECDH shared point. Symmetric —
pairwise_secret(a_sk, b_pk) == pairwise_secret(b_sk, a_pk)— so the recipient recomputes exactly what the sender used, for BOTH the locator and the wrap key.