From 45f45a46b92280b1e6f12c119cfc840fde272945 Mon Sep 17 00:00:00 2001 From: Jean Date: Tue, 21 Jul 2026 14:13:49 +0000 Subject: [PATCH] Warn an oper whose TLS cert fingerprint isn't on their account --- lang/de.json | 3 ++- lang/es-ar.json | 3 ++- lang/es.json | 3 ++- lang/fr.json | 3 ++- lang/pt-br.json | 3 ++- lang/pt.json | 3 ++- src/engine/mod.rs | 16 +++++++++++++++- src/engine/state.rs | 5 +++++ src/engine/tests.rs | 18 ++++++++++++++++++ 9 files changed, 50 insertions(+), 7 deletions(-) diff --git a/lang/de.json b/lang/de.json index 7e1bae8..c17df05 100644 --- a/lang/de.json +++ b/lang/de.json @@ -1373,5 +1373,6 @@ "Syntax: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Syntax: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nÜberträgt den Gründer oder ändert eine Kanaleinstellung.", "Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "Dein Konto ist ausstehend. Bitte ein bestehendes Mitglied, mit \u0002/msg NickServ VOUCH {account}\u0002 für dich zu bürgen.", "You vouched for \u0002{account}\u0002; their account is now active.": "Du hast für \u0002{account}\u0002 gebürgt; das Konto ist jetzt aktiv.", - "Your opponent left. Game \u0002#{id}\u0002 is over.": "Dein Gegner hat die Verbindung getrennt. Spiel \u0002#{id}\u0002 ist vorbei." + "Your opponent left. Game \u0002#{id}\u0002 is over.": "Dein Gegner hat die Verbindung getrennt. Spiel \u0002#{id}\u0002 ist vorbei.", + "You opered up with a TLS certificate fingerprint that isn't on your account. Add it with \u0002/msg NickServ CERT ADD\u0002.": "Du hast dich mit einem TLS-Zertifikat-Fingerabdruck geopert, der nicht in deinem Konto hinterlegt ist. Füge ihn mit \u0002/msg NickServ CERT ADD\u0002 hinzu." } \ No newline at end of file diff --git a/lang/es-ar.json b/lang/es-ar.json index 35d1aea..02c5a0e 100644 --- a/lang/es-ar.json +++ b/lang/es-ar.json @@ -1373,5 +1373,6 @@ "Syntax: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxis: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfiere el fundador o cambia un ajuste del canal.", "Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "Tu cuenta está pendiente. Pedile a un miembro existente que te avale con \u0002/msg NickServ VOUCH {account}\u0002.", "You vouched for \u0002{account}\u0002; their account is now active.": "Avalaste a \u0002{account}\u0002; su cuenta ya está activa.", - "Your opponent left. Game \u0002#{id}\u0002 is over.": "Tu oponente se fue. La partida \u0002#{id}\u0002 terminó." + "Your opponent left. Game \u0002#{id}\u0002 is over.": "Tu oponente se fue. La partida \u0002#{id}\u0002 terminó.", + "You opered up with a TLS certificate fingerprint that isn't on your account. Add it with \u0002/msg NickServ CERT ADD\u0002.": "Te hiciste operador con una huella de certificado TLS que no está en tu cuenta. Agregala con \u0002/msg NickServ CERT ADD\u0002." } \ No newline at end of file diff --git a/lang/es.json b/lang/es.json index e204b57..fcc8ff2 100644 --- a/lang/es.json +++ b/lang/es.json @@ -1373,5 +1373,6 @@ "Syntax: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxis: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfiere el fundador o cambia un ajuste del canal.", "Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "Tu cuenta está pendiente. Pide a un miembro existente que te avale con \u0002/msg NickServ VOUCH {account}\u0002.", "You vouched for \u0002{account}\u0002; their account is now active.": "Avalaste a \u0002{account}\u0002; su cuenta ya está activa.", - "Your opponent left. Game \u0002#{id}\u0002 is over.": "Tu oponente se fue. La partida \u0002#{id}\u0002 terminó." + "Your opponent left. Game \u0002#{id}\u0002 is over.": "Tu oponente se fue. La partida \u0002#{id}\u0002 terminó.", + "You opered up with a TLS certificate fingerprint that isn't on your account. Add it with \u0002/msg NickServ CERT ADD\u0002.": "Te has hecho operador con una huella de certificado TLS que no está en tu cuenta. Añádela con \u0002/msg NickServ CERT ADD\u0002." } \ No newline at end of file diff --git a/lang/fr.json b/lang/fr.json index 9f5a2c2..d1ab0d0 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -1373,5 +1373,6 @@ "Syntax: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Syntaxe : \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfère le fondateur ou modifie un réglage du salon.", "Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "Votre compte est en attente. Demandez à un membre existant de vous parrainer avec \u0002/msg NickServ VOUCH {account}\u0002.", "You vouched for \u0002{account}\u0002; their account is now active.": "Vous avez parrainé \u0002{account}\u0002 ; son compte est maintenant actif.", - "Your opponent left. Game \u0002#{id}\u0002 is over.": "Ton adversaire est parti. La partie \u0002#{id}\u0002 est terminée." + "Your opponent left. Game \u0002#{id}\u0002 is over.": "Ton adversaire est parti. La partie \u0002#{id}\u0002 est terminée.", + "You opered up with a TLS certificate fingerprint that isn't on your account. Add it with \u0002/msg NickServ CERT ADD\u0002.": "Vous êtes passé opérateur avec une empreinte de certificat TLS absente de votre compte. Ajoutez-la avec \u0002/msg NickServ CERT ADD\u0002." } \ No newline at end of file diff --git a/lang/pt-br.json b/lang/pt-br.json index e1b4a3a..19e7e0c 100644 --- a/lang/pt-br.json +++ b/lang/pt-br.json @@ -1373,5 +1373,6 @@ "Syntax: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxe: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfere o fundador ou altera uma configuração do canal.", "Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "Sua conta está pendente. Peça a um membro existente para avalizar você com \u0002/msg NickServ VOUCH {account}\u0002.", "You vouched for \u0002{account}\u0002; their account is now active.": "Você avalizou \u0002{account}\u0002; a conta agora está ativa.", - "Your opponent left. Game \u0002#{id}\u0002 is over.": "Seu oponente saiu. O jogo \u0002#{id}\u0002 acabou." + "Your opponent left. Game \u0002#{id}\u0002 is over.": "Seu oponente saiu. O jogo \u0002#{id}\u0002 acabou.", + "You opered up with a TLS certificate fingerprint that isn't on your account. Add it with \u0002/msg NickServ CERT ADD\u0002.": "Você virou operador com uma impressão digital de certificado TLS que não está na sua conta. Adicione-a com \u0002/msg NickServ CERT ADD\u0002." } \ No newline at end of file diff --git a/lang/pt.json b/lang/pt.json index 55478d4..6446c13 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -1373,5 +1373,6 @@ "Syntax: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxe: \u0002SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfere o fundador ou altera uma definição do canal.", "Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "A sua conta está pendente. Peça a um membro existente que o avalize com \u0002/msg NickServ VOUCH {account}\u0002.", "You vouched for \u0002{account}\u0002; their account is now active.": "Avalizou \u0002{account}\u0002; a conta está agora ativa.", - "Your opponent left. Game \u0002#{id}\u0002 is over.": "O teu adversário saiu. O jogo \u0002#{id}\u0002 acabou." + "Your opponent left. Game \u0002#{id}\u0002 is over.": "O teu adversário saiu. O jogo \u0002#{id}\u0002 acabou.", + "You opered up with a TLS certificate fingerprint that isn't on your account. Add it with \u0002/msg NickServ CERT ADD\u0002.": "Tornou-se operador com uma impressão digital de certificado TLS que não está na sua conta. Adicione-a com \u0002/msg NickServ CERT ADD\u0002." } \ No newline at end of file diff --git a/src/engine/mod.rs b/src/engine/mod.rs index f2a4166..81d2c76 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1657,7 +1657,21 @@ impl Engine { } NetEvent::OperUp { uid, oper_type } => { let what = if oper_type.is_empty() { "opered up".to_string() } else { format!("opered up as \x02{oper_type}\x02") }; - self.notify_line('o', &uid, None, &what).into_iter().collect() + let mut out: Vec = self.notify_line('o', &uid, None, &what).into_iter().collect(); + // #556: once linked (not on the burst), warn an oper who authed with a + // TLS cert that isn't on their account — the ircd only checks the oper + // block, never the account's own cert list. + if self.synced { + if let (Some(ns), Some(account), Some(fp)) = (self.nick_service.clone(), self.network.account_of(&uid).map(str::to_string), self.network.fingerprint_of(&uid).map(|f| f.to_ascii_lowercase())) { + let certs = self.db.certfps(&account); + if !certs.is_empty() && !certs.contains(&fp) { + let lang = self.lang_for_uid(&uid); + let text = echo_api::render(&lang, "You opered up with a TLS certificate fingerprint that isn't on your account. Add it with \x02/msg NickServ CERT ADD\x02.", &[]); + out.push(NetAction::Notice { from: ns, to: uid.clone(), text }); + } + } + } + out } NetEvent::UserKilled { uid } => { // A killed service agent (NickServ, ChanServ, …) has a fixed uid; diff --git a/src/engine/state.rs b/src/engine/state.rs index 0cda915..cd000e9 100644 --- a/src/engine/state.rs +++ b/src/engine/state.rs @@ -162,6 +162,11 @@ impl Network { } } + // A user's TLS cert fingerprint, if they presented one on connect. + pub fn fingerprint_of(&self, uid: &str) -> Option<&str> { + self.users.get(uid).map(|u| u.fingerprint.as_str()).filter(|f| !f.is_empty()) + } + // Record a server's name for its SID, so the `server` extban can resolve which // server a user is on (their uid's SID prefix). pub fn set_server_name(&mut self, sid: &str, name: String) { diff --git a/src/engine/tests.rs b/src/engine/tests.rs index f7061af..ebb76b5 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -5318,6 +5318,24 @@ } // Logging out reverses a services vhost, restoring the connect-time host (the + // On oper-up, echo warns a user whose TLS cert isn't on their account's list — + // but only after the burst, and not when the cert is registered. + #[test] + fn oper_up_warns_on_a_certfp_not_on_the_account() { + let mut e = engine_with("certwarn", "alice", "sesame"); + e.db.certfp_add("alice", &"a".repeat(64)).unwrap(); + e.handle(NetEvent::EndBurst); // arm the warning (synced) + e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "alice".into(), host: "h".into(), ip: "0.0.0.0".into() }); + e.handle(NetEvent::Privmsg { msgid: None, from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() }); + e.handle(NetEvent::UserCert { uid: "000AAAAAB".into(), fp: "b".repeat(64) }); + let out = e.handle(NetEvent::OperUp { uid: "000AAAAAB".into(), oper_type: "NetAdmin".into() }); + assert!(out.iter().any(|a| matches!(a, NetAction::Notice { to, text, .. } if to == "000AAAAAB" && text.contains("isn't on your account"))), "warns on a mismatched cert: {out:?}"); + // With the registered cert, no warning. + e.handle(NetEvent::UserCert { uid: "000AAAAAB".into(), fp: "a".repeat(64) }); + let out = e.handle(NetEvent::OperUp { uid: "000AAAAAB".into(), oper_type: "NetAdmin".into() }); + assert!(!out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("isn't on your account"))), "no warning with a registered cert: {out:?}"); + } + // When an oper deopers, the ircd leaves the oper vhost in place; echo puts the // connect-time cloak back (or the services vhost, if one is assigned). #[test]