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

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