Add the SASL PLAIN verification exchange
fedserv now handles the SASL exchange an ircd relays over ENCAP (modes H/S/C/D): on PLAIN it answers the empty challenge, decodes the client payload, and verifies the credentials against the account store, replying D S or D F. Wiring the account login on success and advertising the mechanism list to the ircd are the remaining integration steps.
This commit is contained in:
parent
18a28ed49b
commit
63028d99e5
5 changed files with 96 additions and 1 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -29,6 +29,12 @@ dependencies = [
|
||||||
"password-hash",
|
"password-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.8.3"
|
version = "1.8.3"
|
||||||
|
|
@ -107,6 +113,7 @@ version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
"base64",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
argon2 = { version = "0.5", features = ["std"] }
|
argon2 = { version = "0.5", features = ["std"] }
|
||||||
|
base64 = "0.22"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ pub mod db;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::proto::{NetAction, NetEvent};
|
use crate::proto::{NetAction, NetEvent};
|
||||||
use db::{Db, RegError};
|
use db::{Db, RegError};
|
||||||
use service::{Sender, Service, ServiceCtx};
|
use service::{Sender, Service, ServiceCtx};
|
||||||
|
|
@ -11,11 +13,12 @@ pub struct Engine {
|
||||||
services: Vec<Box<dyn Service>>,
|
services: Vec<Box<dyn Service>>,
|
||||||
network: Network,
|
network: Network,
|
||||||
db: Db,
|
db: Db,
|
||||||
|
sasl_sessions: HashMap<String, String>, // client uid -> in-progress mechanism
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
pub fn new(services: Vec<Box<dyn Service>>, db: Db) -> Self {
|
pub fn new(services: Vec<Box<dyn Service>>, db: Db) -> Self {
|
||||||
Self { services, network: Network::default(), db }
|
Self { services, network: Network::default(), db, sasl_sessions: HashMap::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
||||||
|
|
@ -49,6 +52,41 @@ impl Engine {
|
||||||
NetEvent::AccountRequest { reqid, kind, account, p2, p3, .. } => {
|
NetEvent::AccountRequest { reqid, kind, account, p2, p3, .. } => {
|
||||||
self.account_request(reqid, kind, account, p2, p3)
|
self.account_request(reqid, kind, account, p2, p3)
|
||||||
}
|
}
|
||||||
|
NetEvent::Sasl { client, mode, data, .. } => self.sasl(client, mode, data),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SASL agent side of the exchange the ircd relays to us (modes H/S/C/D).
|
||||||
|
// PLAIN only for now: verify the credentials against the account store. NOTE:
|
||||||
|
// this proves the password; wiring the account login (metadata) + advertising
|
||||||
|
// the mechanism list to the ircd are the remaining integration steps.
|
||||||
|
fn sasl(&mut self, client: String, mode: String, data: Vec<String>) -> Vec<NetAction> {
|
||||||
|
let agent = match self.services.first() {
|
||||||
|
Some(s) => s.uid().to_string(),
|
||||||
|
None => return Vec::new(),
|
||||||
|
};
|
||||||
|
let mk = |mode: &str, d: Vec<String>| {
|
||||||
|
vec![NetAction::Sasl { agent: agent.clone(), client: client.clone(), mode: mode.to_string(), data: d }]
|
||||||
|
};
|
||||||
|
match mode.as_str() {
|
||||||
|
"H" => Vec::new(), // host info
|
||||||
|
"S" => match data.first().map(String::as_str) {
|
||||||
|
Some("PLAIN") => {
|
||||||
|
self.sasl_sessions.insert(client.clone(), "PLAIN".to_string());
|
||||||
|
mk("C", vec!["+".to_string()]) // empty challenge -> client sends the payload
|
||||||
|
}
|
||||||
|
_ => mk("D", vec!["F".to_string()]), // unsupported mechanism
|
||||||
|
},
|
||||||
|
"C" => {
|
||||||
|
let is_plain = self.sasl_sessions.remove(&client).as_deref() == Some("PLAIN");
|
||||||
|
let ok = is_plain && verify_plain(&data, &self.db);
|
||||||
|
mk("D", vec![if ok { "S".to_string() } else { "F".to_string() }])
|
||||||
|
}
|
||||||
|
"D" => {
|
||||||
|
self.sasl_sessions.remove(&client);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,3 +130,18 @@ impl Engine {
|
||||||
ctx.actions
|
ctx.actions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode a SASL PLAIN payload (authzid \0 authcid \0 passwd) and verify it.
|
||||||
|
fn verify_plain(data: &[String], db: &Db) -> bool {
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
|
let Some(b64) = data.first() else { return false };
|
||||||
|
let Ok(raw) = STANDARD.decode(b64) else { return false };
|
||||||
|
let parts: Vec<&[u8]> = raw.split(|&b| b == 0).collect();
|
||||||
|
if parts.len() != 3 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match (std::str::from_utf8(parts[1]), std::str::from_utf8(parts[2])) {
|
||||||
|
(Ok(authcid), Ok(passwd)) => db.verify(authcid, passwd),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,27 @@ impl Protocol for InspIrcd {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ENCAP <target> <subcmd> … — we only care about relayed SASL:
|
||||||
|
// ENCAP <target> SASL <client> <agent> <mode> [data…]
|
||||||
|
"ENCAP" => {
|
||||||
|
let _target = tokens.next();
|
||||||
|
match tokens.next().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
Some("SASL") => {
|
||||||
|
let p: Vec<&str> = tokens.collect();
|
||||||
|
if p.len() >= 3 {
|
||||||
|
vec![NetEvent::Sasl {
|
||||||
|
client: p[0].to_string(),
|
||||||
|
agent: p[1].to_string(),
|
||||||
|
mode: p[2].to_string(),
|
||||||
|
data: p[3..].iter().map(|s| s.to_string()).collect(),
|
||||||
|
}]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => vec![NetEvent::Unknown { line: line.to_string() }],
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => vec![NetEvent::Unknown { line: line.to_string() }],
|
_ => vec![NetEvent::Unknown { line: line.to_string() }],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +150,15 @@ impl Protocol for InspIrcd {
|
||||||
reqid, kind, account, status, code, message
|
reqid, kind, account, status, code, message
|
||||||
))]
|
))]
|
||||||
}
|
}
|
||||||
|
// ENCAP * SASL <agent> <client> <mode> [data…]
|
||||||
|
NetAction::Sasl { agent, client, mode, data } => {
|
||||||
|
let mut line = format!("ENCAP * SASL {} {} {}", agent, client, mode);
|
||||||
|
for d in data {
|
||||||
|
line.push(' ');
|
||||||
|
line.push_str(d);
|
||||||
|
}
|
||||||
|
vec![self.from_us(line)]
|
||||||
|
}
|
||||||
NetAction::Raw(s) => vec![s.clone()],
|
NetAction::Raw(s) => vec![s.clone()],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ pub enum NetEvent {
|
||||||
Quit { uid: String },
|
Quit { uid: String },
|
||||||
// An ircd relaying an IRCv3 account-registration request to us as the authority.
|
// An ircd relaying an IRCv3 account-registration request to us as the authority.
|
||||||
AccountRequest { reqid: String, origin: String, kind: String, account: String, p2: String, p3: String },
|
AccountRequest { reqid: String, origin: String, kind: String, account: String, p2: String, p3: String },
|
||||||
|
// An ircd relaying a SASL exchange step to us (the SASL agent). mode = H/S/C/D.
|
||||||
|
Sasl { client: String, agent: String, mode: String, data: Vec<String> },
|
||||||
Unknown { line: String },
|
Unknown { line: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,6 +29,8 @@ pub enum NetAction {
|
||||||
Privmsg { from: String, to: String, text: String },
|
Privmsg { from: String, to: String, text: String },
|
||||||
Notice { from: String, to: String, text: String },
|
Notice { from: String, to: String, text: String },
|
||||||
AccountResponse { reqid: String, kind: String, account: String, status: String, code: String, message: String },
|
AccountResponse { reqid: String, kind: String, account: String, status: String, code: String, message: String },
|
||||||
|
// A SASL exchange step back to the ircd, sourced from our SASL agent. mode = C/D.
|
||||||
|
Sasl { agent: String, client: String, mode: String, data: Vec<String> },
|
||||||
Raw(String),
|
Raw(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue