fixups to signal trust set update set

This commit is contained in:
Chris Beck
2025-12-06 10:46:46 -07:00
parent b0d1c1bd92
commit 20604b1d08
2 changed files with 157 additions and 101 deletions
+16 -100
View File
@@ -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<Identity> = 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 &current_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<SignalAlertMessage>,
) -> 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()))
+141 -1
View File
@@ -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<String>> {
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<Identity> = 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 &current_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<Identity> = 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 {