feat: ACL + capacity limit on call rooms, unified fingerprint format
Some checks failed
Mirror to GitHub / mirror (push) Failing after 37s
Build Release Binaries / build-amd64 (push) Failing after 3m38s

- Call rooms (call-*) restricted to the two authorized participants only
- Room capacity enforced at 2 for call rooms
- Unauthorized clients get immediate connection close
- Unified fingerprint format: SHA-256(Ed25519 pub)[:16] as xxxx:xxxx:...
  Used consistently in signal registration, handshake, and ACL checks

Tested: Alice+Bob authorized, attacker rejected with "not authorized"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-09 05:43:03 +04:00
parent 3351cb6473
commit 39ac181d63
2 changed files with 55 additions and 15 deletions

View File

@@ -78,11 +78,17 @@ pub async fn accept_handshake(
}; };
transport.send_signal(&answer).await?; transport.send_signal(&answer).await?;
// Derive caller fingerprint from their identity public key (first 8 bytes as hex) // Derive caller fingerprint: SHA-256(Ed25519 pub)[:16], formatted as xxxx:xxxx:...
let caller_fp = caller_identity_pub[..8] // Must match the format used in signal registration and presence.
.iter() let caller_fp = {
.map(|b| format!("{b:02x}")) use sha2::{Sha256, Digest};
.collect::<String>(); let hash = Sha256::digest(&caller_identity_pub);
let fp = wzp_crypto::Fingerprint([
hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7],
hash[8], hash[9], hash[10], hash[11], hash[12], hash[13], hash[14], hash[15],
]);
fp.to_string()
};
Ok((session, chosen_profile, caller_fp, caller_alias)) Ok((session, chosen_profile, caller_fp, caller_alias))
} }

View File

@@ -676,18 +676,16 @@ async fn main() -> anyhow::Result<()> {
transport.recv_signal(), transport.recv_signal(),
).await { ).await {
Ok(Ok(Some(SignalMessage::RegisterPresence { identity_pub, signature: _, alias }))) => { Ok(Ok(Some(SignalMessage::RegisterPresence { identity_pub, signature: _, alias }))) => {
// Compute fingerprint: SHA-256(Ed25519 pub key)[:16] as hex pairs with colons // Compute fingerprint: SHA-256(Ed25519 pub key)[:16], same as Fingerprint type
let hash = { let fp = {
use sha2::{Sha256, Digest}; use sha2::{Sha256, Digest};
Sha256::digest(&identity_pub) let hash = Sha256::digest(&identity_pub);
let fingerprint = wzp_crypto::Fingerprint([
hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7],
hash[8], hash[9], hash[10], hash[11], hash[12], hash[13], hash[14], hash[15],
]);
fingerprint.to_string()
}; };
let fp = hash[..16].iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.chunks(2)
.map(|c| c.join(""))
.collect::<Vec<_>>()
.join(":");
let fp = auth_fp.unwrap_or(fp); let fp = auth_fp.unwrap_or(fp);
(fp, alias) (fp, alias)
} }
@@ -952,6 +950,28 @@ async fn main() -> anyhow::Result<()> {
// Use the caller's identity fingerprint from the handshake // Use the caller's identity fingerprint from the handshake
let participant_fp = authenticated_fp.clone().unwrap_or(caller_fp); let participant_fp = authenticated_fp.clone().unwrap_or(caller_fp);
// ACL: call rooms (call-*) are restricted to the two authorized participants.
// Only the relay's call orchestrator creates these rooms — random clients can't join.
if room_name.starts_with("call-") {
let call_id = &room_name[5..]; // strip "call-" prefix
let authorized = {
let reg = call_registry.lock().await;
match reg.get(call_id) {
Some(call) => {
call.caller_fingerprint == participant_fp
|| call.callee_fingerprint == participant_fp
}
None => false, // unknown call — reject
}
};
if !authorized {
warn!(%addr, room = %room_name, fp = %participant_fp, "rejected: not authorized for this call room");
transport.close().await.ok();
return;
}
info!(%addr, room = %room_name, fp = %participant_fp, "authorized for call room");
}
// Register in presence registry // Register in presence registry
{ {
let mut reg = presence.lock().await; let mut reg = presence.lock().await;
@@ -1004,6 +1024,20 @@ async fn main() -> anyhow::Result<()> {
metrics.active_sessions.inc(); metrics.active_sessions.inc();
// Call rooms: enforce 2-participant limit
if room_name.starts_with("call-") {
let mgr = room_mgr.lock().await;
if mgr.room_size(&room_name) >= 2 {
drop(mgr);
warn!(%addr, room = %room_name, "call room full (max 2 participants)");
metrics.active_sessions.dec();
let mut smgr = session_mgr.lock().await;
smgr.remove_session(session_id);
transport.close().await.ok();
return;
}
}
let participant_id = { let participant_id = {
let mut mgr = room_mgr.lock().await; let mut mgr = room_mgr.lock().await;
match mgr.join( match mgr.join(