oper: TLS client-cert fingerprint login — oper block gains an optional fp=<sha256>; password=* means cert-only. Named OperBlock struct replaces the (name,pass,level) tuple. (Password login was never broken — verified live.)
This commit is contained in:
parent
853be58d18
commit
c4456cf002
6 changed files with 66 additions and 15 deletions
|
|
@ -5,7 +5,9 @@
|
||||||
//! network = echoNet
|
//! network = echoNet
|
||||||
//! bind = 127.0.0.1:6767
|
//! bind = 127.0.0.1:6767
|
||||||
//! motd = Welcome to echoIRCd
|
//! motd = Welcome to echoIRCd
|
||||||
//! oper = god secret
|
//! oper = god secret # name + password (+ optional level)
|
||||||
|
//! oper = god * fp=<sha256-cert-fp> # cert-only login (no password)
|
||||||
|
//! oper = god secret fp=<sha256-cert-fp> # password AND matching cert
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::map::HashMap;
|
use crate::map::HashMap;
|
||||||
|
|
@ -29,6 +31,17 @@ pub struct LinkBlock {
|
||||||
pub autoconnect: bool,
|
pub autoconnect: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An oper login: `oper = <name> <password|*> [level] [fp=<sha256-fingerprint>]`.
|
||||||
|
/// `password = *` means no password is checked (cert-only login); a `fp=` token
|
||||||
|
/// requires the user's TLS client-certificate SHA-256 fingerprint to match.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct OperBlock {
|
||||||
|
pub name: String,
|
||||||
|
pub password: String,
|
||||||
|
pub level: u32,
|
||||||
|
pub fingerprint: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Config for the `antimixedutf8` module (blocks mixed-script look-alike spam).
|
/// Config for the `antimixedutf8` module (blocks mixed-script look-alike spam).
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AntiMixedCfg {
|
pub struct AntiMixedCfg {
|
||||||
|
|
@ -70,7 +83,7 @@ pub struct Config {
|
||||||
pub tls_cert: Option<String>, // PEM certificate chain
|
pub tls_cert: Option<String>, // PEM certificate chain
|
||||||
pub tls_key: Option<String>, // PEM private key
|
pub tls_key: Option<String>, // PEM private key
|
||||||
pub motd: Vec<String>,
|
pub motd: Vec<String>,
|
||||||
pub opers: Vec<(String, String, u32)>, // (name, password, operlevel)
|
pub opers: Vec<OperBlock>, // oper logins (see OperBlock)
|
||||||
pub cloak_key: Option<String>, // secret key for host cloaking (+x); None = off
|
pub cloak_key: Option<String>, // secret key for host cloaking (+x); None = off
|
||||||
pub sid: String, // this server's 3-char server id (S2S)
|
pub sid: String, // this server's 3-char server id (S2S)
|
||||||
pub serverdesc: String, // this server's description
|
pub serverdesc: String, // this server's description
|
||||||
|
|
@ -197,8 +210,24 @@ impl Config {
|
||||||
"oper" => {
|
"oper" => {
|
||||||
let mut it = v.split_whitespace();
|
let mut it = v.split_whitespace();
|
||||||
if let (Some(n), Some(p)) = (it.next(), it.next()) {
|
if let (Some(n), Some(p)) = (it.next(), it.next()) {
|
||||||
let level = it.next().and_then(|l| l.parse().ok()).unwrap_or(0);
|
let mut b = OperBlock {
|
||||||
c.opers.push((n.to_string(), p.to_string(), level));
|
name: n.to_string(),
|
||||||
|
password: p.to_string(),
|
||||||
|
level: 0,
|
||||||
|
fingerprint: None,
|
||||||
|
};
|
||||||
|
// trailing tokens (any order): a number is the operlevel, a
|
||||||
|
// `fp=`/`certfp=` token is the required TLS cert fingerprint.
|
||||||
|
for tok in it {
|
||||||
|
if let Some(fp) =
|
||||||
|
tok.strip_prefix("fp=").or_else(|| tok.strip_prefix("certfp="))
|
||||||
|
{
|
||||||
|
b.fingerprint = Some(fp.to_ascii_lowercase());
|
||||||
|
} else if let Ok(l) = tok.parse::<u32>() {
|
||||||
|
b.level = l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.opers.push(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// +G censor word: `badword = <find> [replace]` (no replace ⇒ block)
|
// +G censor word: `badword = <find> [replace]` (no replace ⇒ block)
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ impl Command for Stats {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
'o' => {
|
'o' => {
|
||||||
let opers: Vec<String> = s.opers.iter().map(|(n, _, _)| n.clone()).collect();
|
let opers: Vec<String> = s.opers.iter().map(|o| o.name.clone()).collect();
|
||||||
for n in opers {
|
for n in opers {
|
||||||
s.numeric(uid, RPL_STATSOLINE, &format!("O * * {n} :oper"));
|
s.numeric(uid, RPL_STATSOLINE, &format!("O * * {n} :oper"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,15 +123,37 @@ impl Command for Oper {
|
||||||
}
|
}
|
||||||
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||||
let (name, pass) = (params[0].clone(), params[1].clone());
|
let (name, pass) = (params[0].clone(), params[1].clone());
|
||||||
let Some((hash, level)) = s
|
let Some(block) = s.opers.iter().find(|o| o.name == name).cloned() else {
|
||||||
.opers
|
|
||||||
.iter()
|
|
||||||
.find(|(n, _, _)| *n == name)
|
|
||||||
.map(|(_, p, lvl)| (p.clone(), *lvl))
|
|
||||||
else {
|
|
||||||
s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect");
|
s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect");
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
};
|
};
|
||||||
|
let (hash, level) = (block.password.clone(), block.level);
|
||||||
|
// fingerprint login: the block demands a specific TLS client-cert SHA-256
|
||||||
|
// fingerprint, so the user must be on a matching certificate.
|
||||||
|
if let Some(want_fp) = &block.fingerprint {
|
||||||
|
let user_fp = s.users.get(&uid).and_then(|u| u.certfp.clone());
|
||||||
|
if !user_fp
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|f| f.eq_ignore_ascii_case(want_fp))
|
||||||
|
{
|
||||||
|
s.snotice_c(
|
||||||
|
'o',
|
||||||
|
&format!("Failed OPER for {name}: certificate fingerprint mismatch"),
|
||||||
|
);
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_PASSWDMISMATCH,
|
||||||
|
":Password incorrect (a matching TLS client certificate is required)",
|
||||||
|
);
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// `password = *` means cert-only: the fingerprint above is the whole check.
|
||||||
|
if hash == "*" {
|
||||||
|
s.oper_up(uid);
|
||||||
|
crate::modules::operlevels::set(s, uid, level);
|
||||||
|
return CmdResult::Ok;
|
||||||
|
}
|
||||||
// a KDF password (bcrypt / pbkdf2) is slow — verify it off the core thread
|
// a KDF password (bcrypt / pbkdf2) is slow — verify it off the core thread
|
||||||
// (result arrives as OperAuth), so it can't freeze the server or be a DoS.
|
// (result arrives as OperAuth), so it can't freeze the server or be a DoS.
|
||||||
if crate::modules::password_hash::is_slow(&hash) {
|
if crate::modules::password_hash::is_slow(&hash) {
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@ pub fn handle(s: &mut Server, method: &str, _params: &str) -> Result<String, Rpc
|
||||||
let opers: Vec<String> = s
|
let opers: Vec<String> = s
|
||||||
.opers
|
.opers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, _pass, _lvl)| {
|
.map(|o| {
|
||||||
obj(&[
|
obj(&[
|
||||||
("name", qstr(name)),
|
("name", qstr(&o.name)),
|
||||||
("type", qstr("")),
|
("type", qstr("")),
|
||||||
("online", "0".into()),
|
("online", "0".into()),
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcE
|
||||||
Some(name) if !name.is_empty() => {
|
Some(name) if !name.is_empty() => {
|
||||||
// apply the named oper block's level; reject an unknown name rather
|
// apply the named oper block's level; reject an unknown name rather
|
||||||
// than silently opering with defaults
|
// than silently opering with defaults
|
||||||
match s.opers.iter().find(|o| o.0 == name).map(|o| o.2) {
|
match s.opers.iter().find(|o| o.name == name).map(|o| o.level) {
|
||||||
Some(level) => {
|
Some(level) => {
|
||||||
s.oper_up(uid);
|
s.oper_up(uid);
|
||||||
crate::modules::operlevels::set(s, uid, level);
|
crate::modules::operlevels::set(s, uid, level);
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ pub struct Server {
|
||||||
pub nick_index: HashMap<String, Uid>, // lower nick -> uid
|
pub nick_index: HashMap<String, Uid>, // lower nick -> uid
|
||||||
pub channels: HashMap<String, Channel>, // lower name -> channel
|
pub channels: HashMap<String, Channel>, // lower name -> channel
|
||||||
pub events: VecDeque<Hook>,
|
pub events: VecDeque<Hook>,
|
||||||
pub opers: Vec<(String, String, u32)>, // (name, password, operlevel) from config
|
pub opers: Vec<crate::config::OperBlock>, // oper logins from config
|
||||||
pub cloak_key: Option<String>, // host-cloaking key (see modules::cloak)
|
pub cloak_key: Option<String>, // host-cloaking key (see modules::cloak)
|
||||||
pub line_ctags: String, // client-only tags of the line being handled
|
pub line_ctags: String, // client-only tags of the line being handled
|
||||||
// --- server-to-server (see crate::link) ---
|
// --- server-to-server (see crate::link) ---
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue