Warn an oper whose TLS cert fingerprint isn't on their account
All checks were successful
CI / check (push) Successful in 5m31s

This commit is contained in:
Jean Chevronnet 2026-07-21 14:13:49 +00:00
parent cb552c04b0
commit 45f45a46b9
No known key found for this signature in database
9 changed files with 50 additions and 7 deletions

View file

@ -1373,5 +1373,6 @@
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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 <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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."
}

View file

@ -1373,5 +1373,6 @@
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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 <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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."
}

View file

@ -1373,5 +1373,6 @@
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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 <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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."
}

View file

@ -1373,5 +1373,6 @@
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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 <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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."
}

View file

@ -1373,5 +1373,6 @@
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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 <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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."
}

View file

@ -1373,5 +1373,6 @@
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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 <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|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."
}

View file

@ -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<NetAction> = 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;

View file

@ -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) {

View file

@ -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]