use std::path::PathBuf;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use openssl::nid::Nid;
use openssl::ssl::{Ssl, SslAcceptor, SslMethod, SslOptions, SslVerifyMode, SslVersion};
use openssl::x509::{X509, X509NameRef, X509VerifyResult};
use tokio::net::TcpStream;
use tokio_openssl::SslStream;
pub const DEFAULT_CIPHERS: &str = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:\
ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384";
#[derive(Debug, Clone)]
pub struct TlsSettings {
pub identities: Vec<(PathBuf, PathBuf)>,
pub client_ca: Option<PathBuf>,
pub ciphers: String,
}
#[derive(Debug, Clone)]
pub struct TlsInfo {
pub version: String,
pub cipher: String,
pub client_cert: Option<ClientCert>,
}
#[derive(Debug, Clone)]
pub struct ClientCert {
pub common_name: Option<String>,
pub organization: Option<String>,
pub verified: bool,
pub error: Option<String>,
pub serial_hex: String,
}
pub struct TlsAcceptor {
settings: TlsSettings,
current: RwLock<Arc<SslAcceptor>>,
}
fn build(s: &TlsSettings) -> Result<SslAcceptor, String> {
if s.identities.is_empty() {
return Err("at least one TLS certificate/key pair is required".into());
}
let mut b =
SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server()).map_err(|e| e.to_string())?;
b.set_min_proto_version(Some(SslVersion::TLS1_2))
.map_err(|e| e.to_string())?;
b.set_cipher_list(&s.ciphers)
.map_err(|e| format!("invalid cipher list: {e}"))?;
b.set_options(
SslOptions::NO_COMPRESSION
| SslOptions::NO_RENEGOTIATION
| SslOptions::CIPHER_SERVER_PREFERENCE,
);
for (cert, key) in &s.identities {
b.set_certificate_chain_file(cert)
.map_err(|e| format!("{}: {e}", cert.display()))?;
b.set_private_key_file(key, openssl::ssl::SslFiletype::PEM)
.map_err(|e| format!("{}: {e}", key.display()))?;
b.check_private_key()
.map_err(|e| format!("{} does not match {}: {e}", key.display(), cert.display()))?;
}
if let Some(ca) = &s.client_ca {
b.set_ca_file(ca)
.map_err(|e| format!("{}: {e}", ca.display()))?;
let pem = std::fs::read(ca).map_err(|e| format!("{}: {e}", ca.display()))?;
let mut names = openssl::stack::Stack::new().map_err(|e| e.to_string())?;
for cert in X509::stack_from_pem(&pem).map_err(|e| e.to_string())? {
names
.push(cert.subject_name().to_owned().map_err(|e| e.to_string())?)
.map_err(|e| e.to_string())?;
}
b.set_client_ca_list(names);
b.set_verify_callback(SslVerifyMode::PEER, |_, _| true);
}
b.set_session_id_context(b"ocpp-csms")
.map_err(|e| e.to_string())?;
Ok(b.build())
}
fn name_entry(name: &X509NameRef, nid: Nid) -> Option<String> {
name.entries_by_nid(nid)
.next()
.and_then(|e| e.data().to_string().ok())
}
impl TlsAcceptor {
pub fn new(settings: TlsSettings) -> Result<TlsAcceptor, String> {
let acceptor = build(&settings)?;
Ok(TlsAcceptor {
settings,
current: RwLock::new(Arc::new(acceptor)),
})
}
pub fn reload(&self) -> Result<(), String> {
let acceptor = build(&self.settings)?;
*self.current.write().unwrap_or_else(|e| e.into_inner()) = Arc::new(acceptor);
Ok(())
}
pub fn verifies_clients(&self) -> bool {
self.settings.client_ca.is_some()
}
pub async fn accept(&self, tcp: TcpStream) -> Result<(SslStream<TcpStream>, TlsInfo), String> {
let acceptor = self
.current
.read()
.unwrap_or_else(|e| e.into_inner())
.clone();
let ssl = Ssl::new(acceptor.context()).map_err(|e| e.to_string())?;
let mut stream = SslStream::new(ssl, tcp).map_err(|e| e.to_string())?;
Pin::new(&mut stream)
.accept()
.await
.map_err(|e| e.to_string())?;
let ssl = stream.ssl();
let client_cert = ssl.peer_certificate().map(|cert| {
let result = ssl.verify_result();
let verified = self.verifies_clients() && result == X509VerifyResult::OK;
ClientCert {
common_name: name_entry(cert.subject_name(), Nid::COMMONNAME),
organization: name_entry(cert.subject_name(), Nid::ORGANIZATIONNAME),
verified,
error: (!verified).then(|| {
if self.verifies_clients() {
result.error_string().to_owned()
} else {
"client certificates are not configured".into()
}
}),
serial_hex: cert
.serial_number()
.to_bn()
.and_then(|b| b.to_hex_str())
.map(|s| s.to_string())
.unwrap_or_default(),
}
});
let info = TlsInfo {
version: ssl.version_str().to_owned(),
cipher: ssl
.current_cipher()
.map(|c| c.name().to_owned())
.unwrap_or_default(),
client_cert,
};
Ok((stream, info))
}
}