Parent: #108
Problem
EventKind::RotateChannelKey is not listed in required_permission() in crates/state/src/materialize.rs:220-240 — it falls into the _ => None catch-all. apply_mutation at crates/state/src/materialize.rs:405-415 also performs no author check:
EventKind::RotateChannelKey { channel_id, encrypted_keys } => {
if !encrypted_keys.is_empty() {
let keys = state.channel_keys.entry(channel_id.clone()).or_default();
for (peer_id, key_bytes) in encrypted_keys {
keys.insert(*peer_id, key_bytes.clone());
}
}
}
An outsider who is not a member, not an admin, and has never interacted with the server can emit a RotateChannelKey event signed by their own identity and the materializer will accept it, overwriting per-peer encrypted key entries for the target channel.
Exploit (reproduced)
A standalone binary built against willow-state creates a genesis signed by Alice, a channel, and then a RotateChannelKey signed by a brand-new Mallory identity with prev = EventHash::default() and deps = vec![ch_hash]. Materialization produces:
mallory's injected key present in state.channel_keys['general']: true
BUG CONFIRMED: outsider rotated channel keys
Fix
- Add
EventKind::RotateChannelKey { .. } to required_permission() in materialize.rs mapping to Some(Permission::ManageChannels) (or a new Permission::RotateKeys variant).
- Additionally assert
state.members.contains_key(&event.author) inside the RotateChannelKey arm of apply_mutation.
- While in the area, audit
PinMessage/UnpinMessage (same _ => None fall-through) and decide whether they should also require SendMessages or ManageChannels.
Test
Add to crates/state/src/tests.rs:
#[test]
fn rotate_channel_key_by_outsider_is_rejected() {
// genesis + CreateChannel by alice, then RotateChannelKey by mallory
// (fresh identity, never added as member). Assert state.channel_keys
// for the channel is empty or does not contain mallory's injected entry.
}
Out of scope
- Permission check on
PinMessage/UnpinMessage — tracked separately if we decide to restrict them.
- Key rotation via
KickMember flow, which already enforces authority via the governance path.
Parent: #108
Problem
EventKind::RotateChannelKeyis not listed inrequired_permission()incrates/state/src/materialize.rs:220-240— it falls into the_ => Nonecatch-all.apply_mutationatcrates/state/src/materialize.rs:405-415also performs no author check:An outsider who is not a member, not an admin, and has never interacted with the server can emit a
RotateChannelKeyevent signed by their own identity and the materializer will accept it, overwriting per-peer encrypted key entries for the target channel.Exploit (reproduced)
A standalone binary built against
willow-statecreates a genesis signed by Alice, a channel, and then aRotateChannelKeysigned by a brand-new Mallory identity withprev = EventHash::default()anddeps = vec![ch_hash]. Materialization produces:Fix
EventKind::RotateChannelKey { .. }torequired_permission()inmaterialize.rsmapping toSome(Permission::ManageChannels)(or a newPermission::RotateKeysvariant).state.members.contains_key(&event.author)inside theRotateChannelKeyarm ofapply_mutation.PinMessage/UnpinMessage(same_ => Nonefall-through) and decide whether they should also requireSendMessagesorManageChannels.Test
Add to
crates/state/src/tests.rs:Out of scope
PinMessage/UnpinMessage— tracked separately if we decide to restrict them.KickMemberflow, which already enforces authority via the governance path.