feat: Sprint 2 — security hardening, MLS key rotation, E2E tests
- DS sender identity binding (Phase 4.3): explicit audit logging of sender_prefix in enqueue/batch_enqueue, documenting that sender identity is always derived from authenticated session - Username enumeration mitigation (Phase 4.5): 5ms timing floor on resolveUser responses + rate limiting to prevent bulk enumeration - Add /update-key REPL command for MLS leaf key rotation via propose_self_update + auto-commit + fan-out to group members - Add 4 new E2E tests: message delivery round-trip, key rotation update path, oversized payload rejection, multi-party group (12 total)
This commit is contained in:
@@ -59,6 +59,8 @@ enum SlashCommand {
|
||||
MeshServer { addr: String },
|
||||
/// Display safety number for out-of-band key verification with a contact.
|
||||
Verify { username: String },
|
||||
/// Rotate own MLS leaf key in the active group.
|
||||
UpdateKey,
|
||||
}
|
||||
|
||||
fn parse_input(line: &str) -> Input {
|
||||
@@ -144,6 +146,7 @@ fn parse_input(line: &str) -> Input {
|
||||
Input::Empty
|
||||
}
|
||||
},
|
||||
"/update-key" | "/rotate-key" => Input::Slash(SlashCommand::UpdateKey),
|
||||
_ => {
|
||||
display::print_error(&format!("unknown command: {cmd}. Try /help"));
|
||||
Input::Empty
|
||||
@@ -613,6 +616,7 @@ async fn handle_slash(
|
||||
Ok(())
|
||||
}
|
||||
SlashCommand::Verify { username } => cmd_verify(session, client, &username).await,
|
||||
SlashCommand::UpdateKey => cmd_update_key(session, client).await,
|
||||
};
|
||||
if let Err(e) = result {
|
||||
display::print_error(&format!("{e:#}"));
|
||||
@@ -634,6 +638,7 @@ fn print_help() {
|
||||
display::print_status(" /whoami - Show your identity");
|
||||
display::print_status(" /mesh peers - Discover nearby qpq nodes via mDNS");
|
||||
display::print_status(" /mesh server <host:port> - Show how to reconnect to a mesh node");
|
||||
display::print_status(" /update-key - Rotate your MLS leaf key in the active group");
|
||||
display::print_status(" /verify <username> - Show safety number for key verification");
|
||||
display::print_status(" /quit - Exit");
|
||||
}
|
||||
@@ -1076,6 +1081,51 @@ async fn cmd_leave(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_update_key(
|
||||
session: &mut SessionState,
|
||||
client: &node_service::Client,
|
||||
) -> anyhow::Result<()> {
|
||||
let conv_id = session
|
||||
.active_conversation
|
||||
.as_ref()
|
||||
.context("no active conversation; switch to a group first")?
|
||||
.clone();
|
||||
|
||||
let my_key = session.identity_bytes();
|
||||
|
||||
let member = session
|
||||
.get_member_mut(&conv_id)
|
||||
.context("no group member for active conversation")?;
|
||||
|
||||
anyhow::ensure!(
|
||||
member.group_ref().is_some(),
|
||||
"active conversation has no MLS group"
|
||||
);
|
||||
|
||||
// Propose a self-update (leaf key rotation).
|
||||
let proposal = member.propose_self_update()?;
|
||||
|
||||
// Immediately commit the pending proposal.
|
||||
let (commit, _welcome) = member.commit_pending_proposals()?;
|
||||
|
||||
// Fan out the commit to all other group members.
|
||||
let others: Vec<Vec<u8>> = member
|
||||
.member_identities()
|
||||
.into_iter()
|
||||
.filter(|id| id.as_slice() != my_key.as_slice())
|
||||
.collect();
|
||||
|
||||
// Send proposal followed by commit so recipients can process in order.
|
||||
for rk in &others {
|
||||
enqueue(client, rk, &proposal).await?;
|
||||
enqueue(client, rk, &commit).await?;
|
||||
}
|
||||
|
||||
session.save_member(&conv_id)?;
|
||||
display::print_status("key rotation complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_join(
|
||||
session: &mut SessionState,
|
||||
client: &node_service::Client,
|
||||
|
||||
Reference in New Issue
Block a user