From 20604b1d08b7005d2abe3f68e11eef929aba2694 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Sat, 6 Dec 2025 10:46:46 -0700 Subject: [PATCH] fixups to signal trust set update set --- signal-gateway/src/gateway/mod.rs | 116 ++------------ .../src/gateway/signal_trust_set.rs | 142 +++++++++++++++++- 2 files changed, 157 insertions(+), 101 deletions(-) diff --git a/signal-gateway/src/gateway/mod.rs b/signal-gateway/src/gateway/mod.rs index 746292a..2693228 100644 --- a/signal-gateway/src/gateway/mod.rs +++ b/signal-gateway/src/gateway/mod.rs @@ -10,7 +10,7 @@ use crate::{ }, prometheus::{Prometheus, PrometheusConfig}, signal_jsonrpc::{ - Envelope, Identity, MessageTarget, RpcClient, RpcClientError, SignalMessage, connect_tcp, + Envelope, MessageTarget, RpcClient, RpcClientError, SignalMessage, connect_tcp, }, }; use chrono::Utc; @@ -297,110 +297,26 @@ impl Gateway { unreachable!("one_of_fields should ensure exactly one transport is configured") } - /// Update trust for admins with safety numbers configured. - /// - /// For each admin with safety numbers: - /// 1. Check current identities in signal-cli via listIdentities - /// 2. If any trusted identity is NOT in our config, remove the contact entirely and re-add only configured ones - /// 3. Otherwise, just trust any new safety numbers from config that aren't already trusted - async fn update_trust(&self, signal_cli: &impl RpcClient) { - for (uuid, safety_numbers) in &self.config.admin_signal_uuids { - if safety_numbers.is_empty() { - continue; - } - - // Get current identities from signal-cli - let current_identities: Vec = match signal_cli - .list_identities(Some(self.config.signal_account.clone()), Some(uuid.clone())) - .await - { - Ok(value) => serde_json::from_value(value).unwrap_or_default(), - Err(err) => { - debug!("Could not list identities for {uuid} (may not exist yet): {err}"); - Vec::new() - } - }; - - // Check if any trusted identity in signal-cli is NOT in our config - let has_revoked_identity = current_identities.iter().any(|id| { - id.trust_level.is_trusted() && !safety_numbers.contains(&id.safety_number) - }); - - if has_revoked_identity { - // Log which identities are being revoked - for id in ¤t_identities { - if id.trust_level.is_trusted() && !safety_numbers.contains(&id.safety_number) { - warn!( - "Revoking trust for admin {uuid}: safety number {} is trusted but not in config", - id.safety_number - ); - } - } - - // Remove contact to clear all existing trust - info!("Resetting trust for admin {uuid}"); - if let Err(err) = signal_cli - .remove_contact( - Some(self.config.signal_account.clone()), - uuid.clone(), - true, // forget - delete identity keys and sessions - false, // hide - ) - .await - { - error!("Failed to remove contact {uuid}: {err}"); - continue; - } - - // Re-add all configured safety numbers - for safety_number in safety_numbers { - info!("Trusting safety number for admin {uuid}"); - if let Err(err) = signal_cli - .trust( - Some(self.config.signal_account.clone()), - uuid.clone(), - false, - Some(safety_number.clone()), - ) - .await - { - error!("Failed to trust admin {uuid}: {err}"); - } - } - } else { - // Just add any new safety numbers that aren't already trusted - let already_trusted: Vec<_> = current_identities - .iter() - .filter(|id| id.trust_level.is_trusted()) - .map(|id| &id.safety_number) - .collect(); - - for safety_number in safety_numbers { - if !already_trusted.contains(&safety_number) { - info!("Trusting new safety number for admin {uuid}"); - if let Err(err) = signal_cli - .trust( - Some(self.config.signal_account.clone()), - uuid.clone(), - false, - Some(safety_number.clone()), - ) - .await - { - error!("Failed to trust admin {uuid}: {err}"); - } - } - } - } - } - } - async fn do_run( &self, signal_cli: &impl RpcClient, alert_rx: &mut UnboundedReceiver, ) -> Result<(), RpcClientError> { - self.update_trust(signal_cli).await; + // Retry trust update until it succeeds + loop { + match self + .config + .admin_signal_uuids + .update_trust(signal_cli, &self.config.signal_account) + .await + { + Ok(()) => break, + Err(err) => { + error!("Trust update failed: {err}"); + tokio::time::sleep(Duration::from_secs(10)).await; + } + } + } let mut signal_rx = signal_cli .subscribe_receive(Some(self.config.signal_account.clone())) diff --git a/signal-gateway/src/gateway/signal_trust_set.rs b/signal-gateway/src/gateway/signal_trust_set.rs index 7844ac6..cd80511 100644 --- a/signal-gateway/src/gateway/signal_trust_set.rs +++ b/signal-gateway/src/gateway/signal_trust_set.rs @@ -4,11 +4,12 @@ //! - Map: `{"uuid1": ["safety1", "safety2"], "uuid2": []}` - UUIDs with safety numbers //! - Sequence: `["uuid1", "uuid2"]` - UUIDs with no safety numbers (simpler) -use crate::signal_jsonrpc::Envelope; +use crate::signal_jsonrpc::{Envelope, Identity, RpcClient}; use serde::de::{MapAccess, SeqAccess, Visitor}; use serde::{Deserialize, Deserializer}; use std::collections::HashMap; use std::fmt; +use tracing::{debug, info, warn}; /// A set of Signal UUIDs with optional safety numbers for trust verification. /// @@ -58,6 +59,145 @@ impl SignalTrustSet { pub fn get(&self, uuid: &str) -> Option<&Vec> { self.map.get(uuid) } + + /// Update trust for all UUIDs with safety numbers configured. + /// + /// For each UUID with safety numbers: + /// 1. Check current identities in signal-cli via listIdentities + /// 2. If any trusted identity is NOT in our config, remove the contact entirely and re-add only configured ones + /// 3. Otherwise, just trust any new safety numbers from config that aren't already trusted + /// + /// Returns an error if any trust operation fails. + pub async fn update_trust( + &self, + signal_cli: &impl RpcClient, + signal_account: &str, + ) -> Result<(), String> { + info!("Updating trust for {} configured UUIDs", self.map.len()); + + for (uuid, safety_numbers) in &self.map { + if safety_numbers.is_empty() { + continue; + } + + // Get current identities from signal-cli + let current_identities: Vec = match signal_cli + .list_identities(Some(signal_account.to_owned()), Some(uuid.clone())) + .await + { + Ok(value) => serde_json::from_value(value).unwrap_or_default(), + Err(err) => { + debug!("Could not list identities for {uuid} (may not exist yet): {err}"); + Vec::new() + } + }; + + // Check if any trusted identity in signal-cli is NOT in our config + let has_revoked_identity = current_identities.iter().any(|id| { + id.trust_level.is_trusted() && !safety_numbers.contains(&id.safety_number) + }); + + if has_revoked_identity { + // Log which identities are being revoked + for id in ¤t_identities { + if id.trust_level.is_trusted() && !safety_numbers.contains(&id.safety_number) { + warn!( + "Revoking trust for {uuid}: safety number {} is trusted in signal-cli but not in config", + id.safety_number + ); + } + } + + // Remove contact to clear all existing trust + warn!("Resetting all trust for {uuid} due to revoked identity"); + signal_cli + .remove_contact( + Some(signal_account.to_owned()), + uuid.clone(), + true, // forget - delete identity keys and sessions + false, // hide + ) + .await + .map_err(|err| format!("Failed to remove contact {uuid}: {err}"))?; + + // Re-add all configured safety numbers + for safety_number in safety_numbers { + info!("Trusting safety number for {uuid}"); + signal_cli + .trust( + Some(signal_account.to_owned()), + uuid.clone(), + false, + Some(safety_number.clone()), + ) + .await + .map_err(|err| format!("Failed to trust {uuid}: {err}"))?; + } + + // Verify the reset worked correctly + let new_identities: Vec = signal_cli + .list_identities(Some(signal_account.to_owned()), Some(uuid.clone())) + .await + .map_err(|err| format!("Failed to verify trust reset for {uuid}: {err}")) + .and_then(|value| { + serde_json::from_value(value) + .map_err(|err| format!("Failed to parse identities for {uuid}: {err}")) + })?; + + let trusted_now: Vec<_> = new_identities + .iter() + .filter(|id| id.trust_level.is_trusted()) + .map(|id| &id.safety_number) + .collect(); + + // Check all configured safety numbers are now trusted + for safety_number in safety_numbers { + if !trusted_now.contains(&safety_number) { + return Err(format!( + "Verification failed for {uuid}: safety number {} should be trusted but isn't", + safety_number + )); + } + } + + // Check no unexpected safety numbers are trusted + for sn in &trusted_now { + if !safety_numbers.contains(sn) { + return Err(format!( + "Verification failed for {uuid}: safety number {} is trusted but not in config", + sn + )); + } + } + + info!("Trust reset verified for {uuid}: {} safety numbers trusted", trusted_now.len()); + } else { + // Just add any new safety numbers that aren't already trusted + let already_trusted: Vec<_> = current_identities + .iter() + .filter(|id| id.trust_level.is_trusted()) + .map(|id| &id.safety_number) + .collect(); + + for safety_number in safety_numbers { + if !already_trusted.contains(&safety_number) { + info!("Trusting new safety number for {uuid}"); + signal_cli + .trust( + Some(signal_account.to_owned()), + uuid.clone(), + false, + Some(safety_number.clone()), + ) + .await + .map_err(|err| format!("Failed to trust {uuid}: {err}"))?; + } + } + } + } + + Ok(()) + } } impl<'de> Deserialize<'de> for SignalTrustSet {