sasl external: request tls client cert, plumb sha256 certfp through to user, relay to services; advertise sasl=PLAIN,EXTERNAL on tls
This commit is contained in:
parent
8d31feb4e7
commit
02528df5e9
7 changed files with 75 additions and 9 deletions
|
|
@ -67,13 +67,18 @@ impl Command for Cap {
|
|||
match params[0].to_ascii_uppercase().as_str() {
|
||||
"LS" => {
|
||||
let cap302 = params.get(1).map(|v| v == "302").unwrap_or(false);
|
||||
let secure = s.users.get(&uid).map(|u| u.secure).unwrap_or(false);
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.cap = true; // hold registration until CAP END
|
||||
u.cap_302 |= cap302;
|
||||
}
|
||||
s.send(
|
||||
uid,
|
||||
format!(":{} CAP {who} LS :{}", s.name, Caps::ls_line(cap302)),
|
||||
format!(
|
||||
":{} CAP {who} LS :{}",
|
||||
s.name,
|
||||
Caps::ls_line(cap302, secure)
|
||||
),
|
||||
);
|
||||
}
|
||||
"REQ" => {
|
||||
|
|
@ -177,6 +182,28 @@ impl Command for Authenticate {
|
|||
}
|
||||
s.send(uid, "AUTHENTICATE +".to_string());
|
||||
CmdResult::Ok
|
||||
} else if arg.eq_ignore_ascii_case("EXTERNAL") {
|
||||
// CertFP: only works on TLS with a client cert; the fingerprint
|
||||
// goes to services, which map it to an account.
|
||||
let certfp = s.users.get(&uid).and_then(|u| u.certfp.clone());
|
||||
match certfp {
|
||||
Some(fp) if have_services => {
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.sasl_mech = Some("EXTERNAL".to_string());
|
||||
}
|
||||
s.sasl_relay(uid, &format!("S EXTERNAL {fp}"));
|
||||
s.send(uid, "AUTHENTICATE +".to_string());
|
||||
CmdResult::Ok
|
||||
}
|
||||
_ => {
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_SASLFAIL,
|
||||
":SASL EXTERNAL requires a client certificate",
|
||||
);
|
||||
CmdResult::Fail
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.numeric(uid, RPL_SASLMECHS, "PLAIN :are available SASL mechanisms");
|
||||
s.numeric(uid, ERR_SASLFAIL, ":Unsupported SASL mechanism");
|
||||
|
|
|
|||
|
|
@ -24,8 +24,9 @@ pub enum Event {
|
|||
out: OutSink,
|
||||
sock: Option<TcpStream>,
|
||||
secure: bool,
|
||||
link: bool, // a server-to-server connection, not a client
|
||||
outbound: bool, // (link) we dialed them
|
||||
certfp: Option<String>, // TLS client-cert fingerprint (clients only)
|
||||
link: bool, // a server-to-server connection, not a client
|
||||
outbound: bool, // (link) we dialed them
|
||||
},
|
||||
Line {
|
||||
uid: Uid,
|
||||
|
|
@ -70,13 +71,14 @@ impl Ircd {
|
|||
out,
|
||||
sock,
|
||||
secure,
|
||||
certfp,
|
||||
link,
|
||||
outbound,
|
||||
} => {
|
||||
if link {
|
||||
self.server.add_link(uid, addr, out, sock, outbound);
|
||||
} else {
|
||||
self.server.add_conn(uid, addr, out, sock, secure);
|
||||
self.server.add_conn(uid, addr, out, sock, secure, certfp);
|
||||
}
|
||||
}
|
||||
Event::Line { uid, line } => {
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ impl Server {
|
|||
out: OutSink,
|
||||
sock: Option<TcpStream>,
|
||||
secure: bool,
|
||||
certfp: Option<String>,
|
||||
) {
|
||||
let uuid = self.next_uuid();
|
||||
self.uuid_local.insert(uuid.clone(), uid);
|
||||
|
|
@ -198,6 +199,7 @@ impl Server {
|
|||
cloak: String::new(),
|
||||
vhost: None,
|
||||
secure,
|
||||
certfp,
|
||||
account: None,
|
||||
signon: now(),
|
||||
addr,
|
||||
|
|
@ -666,6 +668,7 @@ mod tests {
|
|||
cloak: String::new(),
|
||||
vhost: None,
|
||||
secure: false,
|
||||
certfp: None,
|
||||
account: None,
|
||||
signon: 0,
|
||||
addr: "127.0.0.1:1".parse().unwrap(),
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ pub fn run_reactor(mut listener: MioListener, core: Sender<Event>, counter: Arc<
|
|||
out,
|
||||
sock: None,
|
||||
secure: false,
|
||||
certfp: None,
|
||||
link: false,
|
||||
outbound: false,
|
||||
})
|
||||
|
|
@ -380,6 +381,7 @@ pub fn accept_loop(
|
|||
out: OutSink::Thread(out_tx),
|
||||
sock: Some(shutdown),
|
||||
secure: false,
|
||||
certfp: None,
|
||||
link,
|
||||
outbound: false,
|
||||
})
|
||||
|
|
@ -427,6 +429,7 @@ pub fn connect_link(addr: &str, core: Sender<Event>, counter: Arc<AtomicU64>) {
|
|||
out: OutSink::Thread(out_tx),
|
||||
sock: Some(shutdown),
|
||||
secure: false,
|
||||
certfp: None,
|
||||
link: true,
|
||||
outbound: true,
|
||||
})
|
||||
|
|
@ -501,6 +504,7 @@ fn tls_conn(
|
|||
return; // handshake failed
|
||||
}
|
||||
};
|
||||
let certfp = conn.peer_cert_fp();
|
||||
let (out_tx, out_rx) = mpsc::channel::<String>();
|
||||
if core
|
||||
.send(Event::Connect {
|
||||
|
|
@ -509,6 +513,7 @@ fn tls_conn(
|
|||
out: OutSink::Thread(out_tx),
|
||||
sock: Some(shutdown),
|
||||
secure: true,
|
||||
certfp,
|
||||
link,
|
||||
outbound: false,
|
||||
})
|
||||
|
|
|
|||
15
src/tls.rs
15
src/tls.rs
|
|
@ -12,7 +12,8 @@ use std::io::{self, Read, Write};
|
|||
use std::net::{Shutdown, TcpStream};
|
||||
use std::time::Duration;
|
||||
|
||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslStream};
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslStream, SslVerifyMode};
|
||||
|
||||
/// A live TLS connection: read/write plaintext, tune the read timeout (the
|
||||
/// socket engine polls with one to interleave reads and queued writes), and shut
|
||||
|
|
@ -23,6 +24,9 @@ pub trait TlsConn: Send {
|
|||
fn flush(&mut self) -> io::Result<()>;
|
||||
fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()>;
|
||||
fn shutdown(&self);
|
||||
/// SHA-256 fingerprint (lowercase hex) of the peer's certificate, if it sent
|
||||
/// one. Drives SASL EXTERNAL / CertFP.
|
||||
fn peer_cert_fp(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
/// A TLS backend: performs the server-side handshake on an accepted socket.
|
||||
|
|
@ -47,6 +51,10 @@ impl OpensslBackend {
|
|||
b.set_private_key_file(key, SslFiletype::PEM).map_err(err)?;
|
||||
b.set_certificate_chain_file(cert).map_err(err)?;
|
||||
b.check_private_key().map_err(err)?;
|
||||
// Request (but don't require) a client cert so SASL EXTERNAL / CertFP can
|
||||
// read its fingerprint. We never validate the chain — services match the
|
||||
// fingerprint to an account — so the callback always accepts.
|
||||
b.set_verify_callback(SslVerifyMode::PEER, |_valid, _ctx| true);
|
||||
Ok(OpensslBackend {
|
||||
acceptor: b.build(),
|
||||
})
|
||||
|
|
@ -78,4 +86,9 @@ impl TlsConn for OpensslConn {
|
|||
fn shutdown(&self) {
|
||||
let _ = self.0.get_ref().shutdown(Shutdown::Both);
|
||||
}
|
||||
fn peer_cert_fp(&self) -> Option<String> {
|
||||
let cert = self.0.ssl().peer_certificate()?;
|
||||
let digest = cert.digest(MessageDigest::sha256()).ok()?;
|
||||
Some(digest.iter().map(|b| format!("{b:02x}")).collect())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
src/users.rs
19
src/users.rs
|
|
@ -127,12 +127,17 @@ impl Caps {
|
|||
}
|
||||
|
||||
/// The `CAP LS` token list; `sasl` carries its mechanisms for 302 clients.
|
||||
pub fn ls_line(cap302: bool) -> String {
|
||||
/// EXTERNAL is only offered on TLS connections (it needs a client cert).
|
||||
pub fn ls_line(cap302: bool, secure: bool) -> String {
|
||||
SUPPORTED_CAPS
|
||||
.iter()
|
||||
.map(|c| {
|
||||
if *c == "sasl" && cap302 {
|
||||
"sasl=PLAIN".to_string()
|
||||
if secure {
|
||||
"sasl=PLAIN,EXTERNAL".to_string()
|
||||
} else {
|
||||
"sasl=PLAIN".to_string()
|
||||
}
|
||||
} else {
|
||||
(*c).to_string()
|
||||
}
|
||||
|
|
@ -209,6 +214,7 @@ pub struct User {
|
|||
pub cloak: String, // masked host shown under +x ("" until computed)
|
||||
pub vhost: Option<String>, // displayed-host override (CHGHOST/SETHOST vhost)
|
||||
pub secure: bool, // connected over TLS (drives WHOIS 671 / sslinfo)
|
||||
pub certfp: Option<String>, // TLS client-cert fingerprint (SASL EXTERNAL / CertFP)
|
||||
pub account: Option<String>, // logged-in account name (set by services)
|
||||
pub signon: u64, // unix secs at registration (WHOIS 317)
|
||||
pub addr: SocketAddr,
|
||||
|
|
@ -465,8 +471,13 @@ mod tests {
|
|||
assert!(!c.set("bogus-cap", true)); // unknown cap rejected
|
||||
assert!(c.has("server-time") && c.has("multi-prefix") && !c.has("sasl"));
|
||||
assert_eq!(c.enabled(), "server-time multi-prefix"); // SUPPORTED order
|
||||
assert!(Caps::ls_line(true).contains("sasl=PLAIN")); // 302 shows mechs
|
||||
assert!(Caps::ls_line(false).contains("sasl") && !Caps::ls_line(false).contains("sasl="));
|
||||
assert!(Caps::ls_line(true, false).contains("sasl=PLAIN")); // 302 shows mechs
|
||||
assert!(!Caps::ls_line(true, false).contains("EXTERNAL")); // plaintext: no EXTERNAL
|
||||
assert!(Caps::ls_line(true, true).contains("sasl=PLAIN,EXTERNAL")); // TLS offers it
|
||||
assert!(
|
||||
Caps::ls_line(false, false).contains("sasl")
|
||||
&& !Caps::ls_line(false, false).contains("sasl=")
|
||||
);
|
||||
c.set("server-time", false);
|
||||
assert!(!c.has("server-time"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue