echoIRCd/src/tls.rs

91 lines
3.4 KiB
Rust

//! TLS backends: 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. An alternative backend (e.g. rustls) only has to
//! implement these same two traits and it slots straight in.
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<usize>;
fn write_all(&mut self, buf: &[u8]) -> io::Result<()>;
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.
pub trait TlsBackend: Send + Sync {
fn accept(&self, sock: TcpStream) -> io::Result<Box<dyn TlsConn>>;
}
fn err<E: std::fmt::Display>(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<OpensslBackend> {
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<Box<dyn TlsConn>> {
let stream = self.acceptor.accept(sock).map_err(err)?;
Ok(Box::new(OpensslConn(stream)))
}
}
struct OpensslConn(SslStream<TcpStream>);
impl TlsConn for OpensslConn {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
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<Duration>) -> 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<String> {
let cert = self.0.ssl().peer_certificate()?;
let digest = cert.digest(MessageDigest::sha256()).ok()?;
Some(digest.iter().map(|b| format!("{b:02x}")).collect())
}
}