mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
fix(concord): only the community's staff can readmit a banned member
A readmission (ff7716f6a6) moved a banned member's held base to any
Direct Invite with a newer root epoch, and never checked the sender. A
bundle's community_root is not bound to its id, so anyone could mint a
"newer epoch" under a root of their own. One tap on Accept would then
replace the held community with theirs, and a huge epoch would block
every genuine readmission after it.
A readmission now needs what a catch-up needs: a seal-verified sender
who is staff (and not banned) in the held fold, for the same owner. The
test covers the forged case in both acceptPlan and visible().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e7b8d59a7e
commit
d8dba40e5b
+13
-5
@@ -349,7 +349,7 @@ class ConcordDirectInviteInbox(
|
||||
): DirectInviteAcceptPlan {
|
||||
if (opened.isExpired(nowMs)) return DirectInviteAcceptPlan.Expired
|
||||
if (held == null) return DirectInviteAcceptPlan.Join
|
||||
if (isReadmission(held, heldState, opened.invite, me)) return DirectInviteAcceptPlan.Readmit
|
||||
if (isReadmission(held, heldState, opened, me)) return DirectInviteAcceptPlan.Readmit
|
||||
if (ConcordInviteVend.catchUpChannelIds(held, opened.invite).isEmpty()) return DirectInviteAcceptPlan.NothingNew
|
||||
if (heldState == null) return DirectInviteAcceptPlan.RosterNotLoaded
|
||||
// Death wins every race (CORD-02 §9): a dissolved community takes no new keys.
|
||||
@@ -369,18 +369,26 @@ class ConcordDirectInviteInbox(
|
||||
* could never be accepted. A banned member has no live membership a bundle could hijack
|
||||
* (the reason a bundle may never move a held base, CORD-06 §2), and the join re-checks the
|
||||
* ban against the NEW epoch's roster, failing closed.
|
||||
*
|
||||
* A bundle's `community_root` is not bound to its id, so anyone could mint a "newer epoch"
|
||||
* under a root of their own and, on one tap, replace the held base (and, with a huge epoch,
|
||||
* block every genuine readmission after it). So, as for a catch-up, only a sender who is
|
||||
* staff in the held fold (seal-verified) may readmit us, into the same owner's community.
|
||||
*/
|
||||
fun isReadmission(
|
||||
held: ConcordCommunityListEntry,
|
||||
heldState: ConcordCommunityState?,
|
||||
invite: CommunityInvite,
|
||||
opened: OpenedDirectInvite,
|
||||
me: HexKey,
|
||||
): Boolean =
|
||||
heldState != null &&
|
||||
!heldState.dissolved &&
|
||||
heldState.authority.isBanned(me) &&
|
||||
invite.communityId.equals(held.id, ignoreCase = true) &&
|
||||
invite.rootEpoch > held.rootEpoch
|
||||
opened.invite.communityId.equals(held.id, ignoreCase = true) &&
|
||||
opened.invite.owner.equals(held.owner, ignoreCase = true) &&
|
||||
opened.invite.rootEpoch > held.rootEpoch &&
|
||||
heldState.authority.isStaff(opened.sender) &&
|
||||
!heldState.authority.isBanned(opened.sender)
|
||||
|
||||
/**
|
||||
* What a UI shows out of [pending], given the communities this account already holds
|
||||
@@ -424,7 +432,7 @@ class ConcordDirectInviteInbox(
|
||||
// a catch-up from a non-staff sender, for channels the fold doesn't know as Private,
|
||||
// or into a dissolved community is refused by [acceptPlan], so it is not offered.
|
||||
val heldState = held?.let { heldStateOf(it.id) }
|
||||
val readmit = held != null && me != null && isReadmission(held, heldState, opened.invite, me)
|
||||
val readmit = held != null && me != null && isReadmission(held, heldState, opened, me)
|
||||
val newChannels =
|
||||
when {
|
||||
held == null || readmit -> emptyList()
|
||||
|
||||
+7
@@ -503,5 +503,12 @@ class ConcordDirectInviteInboxTest {
|
||||
|
||||
// Dissolved: death wins.
|
||||
assertNotEquals(DirectInviteAcceptPlan.Readmit, ConcordDirectInviteInbox.acceptPlan(reInvite, held, banned.withDissolved(true), me.pubKey))
|
||||
|
||||
// A bundle's root is not bound to the community id, so anyone can mint a "newer epoch"
|
||||
// under their own root. Only staff of the held community may readmit us: a stranger's
|
||||
// forgery must not move the held base, nor even be offered.
|
||||
val forged = assertNotNull(ConcordActions.openDirectInvite(ConcordActions.buildDirectInvite(stranger, me.pubKey, inviteFor(c, root = "66".repeat(32), epoch = c.rootEpoch + 50)), me))
|
||||
assertNotEquals(DirectInviteAcceptPlan.Readmit, ConcordDirectInviteInbox.acceptPlan(forged, held, banned, me.pubKey))
|
||||
assertTrue(ConcordDirectInviteInbox.visible(listOf(forged), listOf(held), heldStateOf = { banned }, me = me.pubKey).isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user