Skip to main content

Module rekey

Module rekey 

Source
Expand description

Concord v2 rekeys (CORD-06) — the per-recipient key-delivery atom + the 3303 event that carries a rotation.

A rotation mints a fresh-random key for the next epoch and delivers it only to the members who STAY, one per-recipient blob each. Two structural shifts from v1, both security-load-bearing:

D1 — the locator is PUBLIC and authenticates nothing. v1 located a blob by HKDF(pairwise_ECDH_secret, …) — a value only the sender↔recipient pair could compute, so in v1 a matching locator proved the blob was minted for that pair. v2 locates by HKDF(rotator_xonly || recipient_xonly, …) — public inputs (full NIP-46 bunker parity: a bunker computes it without a raw key). A public locator proves NOTHING; it is a lookup index only. Authenticity now rests entirely on (a) the crate’s rotator seal + roster authority — verified by the caller before any blob is trusted — and (b) the blob’s bound plaintext (scope+epoch checked after decrypt). So [open_blob] does NOT gate on the locator (that was v1’s open_rekey_blob assumption — do not port it): the decrypt itself (only the addressed recipient’s key opens a blob wrapped to them) plus the bound-plaintext check are the gate.

D5 — the blob wrap carries base64. NIP-44/NIP-46 encrypt surfaces are string-typed, and the 72 raw bytes aren’t valid UTF-8, so the wrapped plaintext is base64(scope_id ‖ epoch_be ‖ new_key) — a string a bunker can nip44_encrypt/nip44_decrypt to the recipient’s identity key with no raw secret. The wrapped field is then the standard NIP-44 payload string, so a local-keys wrap and a bunker wrap produce identical wire output.

The 3303 event itself is a v2 stream event (kind-1059 wrap, ENCRYPTED seal signed by the rotator’s real identity) at the rekey address — reusing super::stream. Its seal is what tells the recipient WHO rotated, which is both the ECDH counterparty and the authority actor.

What lives here: the blob atom, the 3303 build/parse, chunk-set assembly, and the continuity/fork comparators — all PURE. The stateful orchestration (recipient-set computation, the base+channels lockstep read-cut, DB epoch archival, and the D2 BAN-vs-MANAGE_CHANNELS authority gate, which is an apply-path concern keyed on prior-vs-current-root addressing) sits in the service layer.

Structs§

RekeyBlob
One located, wrapped rekey blob — the unit a 3303 event carries N of.
RekeyChunk
A parsed, seal-verified 3303 chunk. The rotator is the seal’s real signer (the ECDH counterparty AND the authority actor the caller gates on).
Rotation
A collected rotation: all chunks sharing one correlation key, and whether the set is complete (all n chunks present).

Enums§

Continuity
The verdict of the prevcommit continuity check (CORD-06 §2).
RekeyError
Errors from the rekey layer.
RekeyScope
What a rekey rotates (CORD-06 §1). The 32-byte scope id is stamped into every blob’s plaintext so a blob can’t be spliced onto another coordinate.

Constants§

MAX_REKEY_BLOBS_PER_EVENT
Max recipients (blobs) Vector puts in ONE 3303 event when SENDING. Lower than the spec’s stated 120 because a v2 rekey rides the CORD-01 double-wrap (blob array → encrypted seal → wrap, two NIP-44 base64 expansions): a 120-blob event measures ~77 KB, over strfry’s 64 KB maxEventSize, while 80 blobs measure ~55 KB (a full one is size-guarded by test). The spec’s 120 assumes a lighter envelope — a CORD-06 erratum (see the divergence ledger). A larger recipient set splits across chunk events.
MAX_REKEY_BLOBS_RECEIVED
Max blobs Vector will ACCEPT in one received 3303 chunk (a DoS bound checked after decrypt). Kept at the spec’s stated 120 — higher than the send cap — so a chunk minted by another client at the spec limit (and delivered by a relay with a larger maxEventSize) still parses. An array over this is rejected.

Functions§

am_i_removed
Have I been removed by this rotation? Only answerable on a COMPLETE rotation (all n chunks held): I’m removed iff none of the union’s blobs carries my locator. On an incomplete rotation the answer is None — keep recovering.
base_rekey_group
The rekey group key for a BASE rotation — always under the PRIOR root (the one handle every retained member still holds through the rotation).
blob_locator
The public per-recipient locator (D1). Both parties compute it from public keys alone; it addresses the blob and nothing more.
bound_plaintext_b64
The base64 string a blob’s NIP-44 layer actually encrypts (D5). Exposed so the service-layer bunker path can signer.nip44_encrypt(recipient, this).
build_blob_local
Build one blob with LOCAL keys (the bunker path drives the same wire via the _b64 helpers + a NIP-46 nip44_encrypt). The wrap is the pairwise conversation key ConversationKey::derive(rotator_sk, recipient_pk), so only the recipient’s identity key opens it.
build_rekey_chunks_local
Split a full recipient blob set into 3303 chunk events (≤120 blobs each), all sharing the rotation’s (scope, new_epoch, prev_commit) so a receiver correlates them. Local-keys convenience.
build_rekey_rumor
Build the unsigned 3303 rumor (rotator is the pubkey; the seal will carry the signature). Enforces the monotonic-epoch and chunk-range invariants at mint.
channel_rekey_group
The rekey group key for a CHANNEL rekey addressed under addressing_root. The caller chooses the root: a STANDALONE channel rekey rides the CURRENT root (MANAGE_CHANNELS); a channel rekey forced by a removal rides the PRIOR root alongside the base rekey (D2 — inherits the removal’s BAN authority, and the prior-root address is exactly what distinguishes the two classes on the wire so a base-fork loser can still open it).
check_continuity
Check a rotation’s prev_commit against the (epoch, key) I currently hold for its scope. A match proves the rotation extends the very key I hold; a higher prev_epoch means I’m behind; anything else is a fork/garbage.
collect_rotations
Group a batch of parsed chunks into rotations by correlation key. Chunks whose n disagrees with their siblings, or that repeat an index, are the caller’s concern to police; here the first-seen n per correlation wins and duplicate indices are ignored (idempotent re-delivery).
find_my_blob
Find my blob in a chunk’s array by my public locator (the lookup step, D1). None means this chunk doesn’t carry my key — never a removal on its own (only “removed” once ALL chunks are held and none has it).
lowest_key_winner
Deterministic same-epoch fork winner (CORD-06 §3): among candidate rotations at one continuity point, the one whose decrypted new_key is lexicographically lowest wins. Every retained member decrypts its own blob from each fork and computes the identical winner. Returns the index into candidates of the winner, or None if the caller decrypted no candidate.
open_blob_local
Open a blob addressed to me with LOCAL keys. Per D1 this does NOT check the locator: the decrypt (only my identity key opens a blob wrapped to me by the rotator) plus the bound scope/epoch ARE the authenticity boundary. A blob relocated to a foreign locator still won’t decrypt for a non-recipient, and a spliced one fails the bound check.
parse_bound_plaintext
Parse + verify a decrypted bound plaintext (the base64 already stripped), checking scope+epoch strict-equal the coordinate it was opened under before yielding new_key. Exposed for the bunker open path.
parse_rekey_chunk
Parse a 3303 chunk from a seal-verified stream open. Rejects a non-3303 rumor, a plaintext seal (the rekey plane is encrypted-only), malformed or duplicate machinery tags, a bad chunk range, and an over-cap blob array.
seal_rekey_chunk
Seal + wrap a 3303 rumor into its stream event at the rekey address. The seal is ENCRYPTED (20013 — the rekey plane MUST NOT be plaintext-sealed) and signed by the rotator; the wrap by the rekey group key.

Type Aliases§

RotationKey
The key that groups chunks of ONE rotation: (rotator, scope_id, new_epoch, prev_commit). Two rotators racing the same epoch, or one rotator over two channels, never alias.