//! TLS backends — echoIRCd's answer to InspIRCd's `IOHook` seam and its //! `ssl_openssl` / `ssl_gnutls` modules. A [`TlsBackend`] wraps an accepted //! socket in a TLS session; the socket engine then drives the resulting //! [`TlsConn`] for any listener that has a backend attached. //! //! This backend is openssl. The `openssl` crate keeps all its `unsafe` internal, //! so the daemon itself stays `#![forbid(unsafe_code)]`. A pure-Rust `rustls` //! backend (or a gnutls one) only has to implement these same two traits and it //! slots straight in — exactly the pluggable-provider shape InspIRCd uses. use std::io::{self, Read, Write}; use std::net::{Shutdown, TcpStream}; use std::time::Duration; 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 /// it down. The concrete backend type stays hidden behind this. pub trait TlsConn: Send { fn read(&mut self, buf: &mut [u8]) -> io::Result; fn write_all(&mut self, buf: &[u8]) -> io::Result<()>; fn flush(&mut self) -> io::Result<()>; fn set_read_timeout(&self, dur: Option) -> 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; } /// A TLS backend: performs the server-side handshake on an accepted socket. pub trait TlsBackend: Send + Sync { fn accept(&self, sock: TcpStream) -> io::Result>; } fn err(e: E) -> io::Error { io::Error::other(e.to_string()) } // --- openssl backend -------------------------------------------------------- pub struct OpensslBackend { acceptor: SslAcceptor, } impl OpensslBackend { /// Build an acceptor from a PEM certificate chain + private key. pub fn new(cert: &str, key: &str) -> io::Result { let mut b = SslAcceptor::mozilla_intermediate(SslMethod::tls()).map_err(err)?; 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(), }) } } impl TlsBackend for OpensslBackend { fn accept(&self, sock: TcpStream) -> io::Result> { let stream = self.acceptor.accept(sock).map_err(err)?; Ok(Box::new(OpensslConn(stream))) } } struct OpensslConn(SslStream); impl TlsConn for OpensslConn { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.0.read(buf) } fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { self.0.write_all(buf) } fn flush(&mut self) -> io::Result<()> { self.0.flush() } fn set_read_timeout(&self, dur: Option) -> io::Result<()> { self.0.get_ref().set_read_timeout(dur) } fn shutdown(&self) { let _ = self.0.get_ref().shutdown(Shutdown::Both); } fn peer_cert_fp(&self) -> Option { let cert = self.0.ssl().peer_certificate()?; let digest = cert.digest(MessageDigest::sha256()).ok()?; Some(digest.iter().map(|b| format!("{b:02x}")).collect()) } }