feat: ACL + capacity limit on call rooms, unified fingerprint format
- 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:
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user