Skip to main content

ocpp_service/
tls.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)
3
4//! TLS for the OCPP endpoint (OCPP 2.0.1/2.1 Part 2 §A00, OCPP 1.6 security whitepaper).
5//!
6//! - TLS 1.2 or higher only (A00.FR.313), no TLS compression (A00.FR.321), no renegotiation.
7//! - Cipher suites: the four required by A00.FR.318 (ECDHE-ECDSA and plain RSA key
8//!   exchange with AES-GCM) plus ECDHE-RSA. Serving both suite families requires
9//!   an ECDSA and an RSA certificate; up to two certificate/key pairs are loaded.
10//! - Client certificates (Security Profile 3) are requested but not required at the
11//!   TLS level, so that profile 2 and profile 3 stations can share one port. The
12//!   verification outcome is passed to the WebSocket handshake, which enforces the
13//!   station's configured profile.
14//! - Certificates are reloaded with [`TlsAcceptor::reload`] (wired to SIGHUP).
15
16use std::path::PathBuf;
17use std::pin::Pin;
18use std::sync::{Arc, RwLock};
19
20use openssl::nid::Nid;
21use openssl::ssl::{Ssl, SslAcceptor, SslMethod, SslOptions, SslVerifyMode, SslVersion};
22use openssl::x509::{X509, X509NameRef, X509VerifyResult};
23use tokio::net::TcpStream;
24use tokio_openssl::SslStream;
25
26/// Default TLS 1.2 cipher list: A00.FR.318 suites plus ECDHE-RSA (forward secrecy with RSA keys).
27pub const DEFAULT_CIPHERS: &str = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:\
28ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384";
29
30#[derive(Debug, Clone)]
31pub struct TlsSettings {
32    /// One or two (certificate chain PEM, private key PEM) pairs, e.g. ECDSA and RSA.
33    pub identities: Vec<(PathBuf, PathBuf)>,
34    /// CA bundle (PEM) used to verify charging station certificates (Security Profile 3).
35    pub client_ca: Option<PathBuf>,
36    /// OpenSSL cipher list for TLS 1.2.
37    pub ciphers: String,
38}
39
40/// Outcome of the TLS handshake, attached to the HTTP request.
41#[derive(Debug, Clone)]
42pub struct TlsInfo {
43    pub version: String,
44    pub cipher: String,
45    pub client_cert: Option<ClientCert>,
46}
47
48#[derive(Debug, Clone)]
49pub struct ClientCert {
50    pub common_name: Option<String>,
51    pub organization: Option<String>,
52    /// Chain verified against the configured CA bundle (includes the validity period).
53    pub verified: bool,
54    /// OpenSSL verification error when not verified.
55    pub error: Option<String>,
56    pub serial_hex: String,
57}
58
59pub struct TlsAcceptor {
60    settings: TlsSettings,
61    current: RwLock<Arc<SslAcceptor>>,
62}
63
64fn build(s: &TlsSettings) -> Result<SslAcceptor, String> {
65    if s.identities.is_empty() {
66        return Err("at least one TLS certificate/key pair is required".into());
67    }
68    let mut b =
69        SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server()).map_err(|e| e.to_string())?;
70    b.set_min_proto_version(Some(SslVersion::TLS1_2))
71        .map_err(|e| e.to_string())?;
72    b.set_cipher_list(&s.ciphers)
73        .map_err(|e| format!("invalid cipher list: {e}"))?;
74    b.set_options(
75        SslOptions::NO_COMPRESSION
76            | SslOptions::NO_RENEGOTIATION
77            | SslOptions::CIPHER_SERVER_PREFERENCE,
78    );
79    // OpenSSL keeps one certificate slot per key type, so an ECDSA and an RSA pair can coexist.
80    for (cert, key) in &s.identities {
81        b.set_certificate_chain_file(cert)
82            .map_err(|e| format!("{}: {e}", cert.display()))?;
83        b.set_private_key_file(key, openssl::ssl::SslFiletype::PEM)
84            .map_err(|e| format!("{}: {e}", key.display()))?;
85        b.check_private_key()
86            .map_err(|e| format!("{} does not match {}: {e}", key.display(), cert.display()))?;
87    }
88    if let Some(ca) = &s.client_ca {
89        b.set_ca_file(ca)
90            .map_err(|e| format!("{}: {e}", ca.display()))?;
91        let pem = std::fs::read(ca).map_err(|e| format!("{}: {e}", ca.display()))?;
92        let mut names = openssl::stack::Stack::new().map_err(|e| e.to_string())?;
93        for cert in X509::stack_from_pem(&pem).map_err(|e| e.to_string())? {
94            names
95                .push(cert.subject_name().to_owned().map_err(|e| e.to_string())?)
96                .map_err(|e| e.to_string())?;
97        }
98        b.set_client_ca_list(names);
99        // Request a certificate; the profile check happens after the handshake.
100        b.set_verify_callback(SslVerifyMode::PEER, |_, _| true);
101    }
102    b.set_session_id_context(b"ocpp-csms")
103        .map_err(|e| e.to_string())?;
104    Ok(b.build())
105}
106
107fn name_entry(name: &X509NameRef, nid: Nid) -> Option<String> {
108    name.entries_by_nid(nid)
109        .next()
110        .and_then(|e| e.data().to_string().ok())
111}
112
113impl TlsAcceptor {
114    pub fn new(settings: TlsSettings) -> Result<TlsAcceptor, String> {
115        let acceptor = build(&settings)?;
116        Ok(TlsAcceptor {
117            settings,
118            current: RwLock::new(Arc::new(acceptor)),
119        })
120    }
121
122    /// Re-reads certificates and keys from disk; keeps the old ones on error.
123    pub fn reload(&self) -> Result<(), String> {
124        let acceptor = build(&self.settings)?;
125        *self.current.write().unwrap_or_else(|e| e.into_inner()) = Arc::new(acceptor);
126        Ok(())
127    }
128
129    pub fn verifies_clients(&self) -> bool {
130        self.settings.client_ca.is_some()
131    }
132
133    pub async fn accept(&self, tcp: TcpStream) -> Result<(SslStream<TcpStream>, TlsInfo), String> {
134        let acceptor = self
135            .current
136            .read()
137            .unwrap_or_else(|e| e.into_inner())
138            .clone();
139        let ssl = Ssl::new(acceptor.context()).map_err(|e| e.to_string())?;
140        let mut stream = SslStream::new(ssl, tcp).map_err(|e| e.to_string())?;
141        Pin::new(&mut stream)
142            .accept()
143            .await
144            .map_err(|e| e.to_string())?;
145        let ssl = stream.ssl();
146        let client_cert = ssl.peer_certificate().map(|cert| {
147            let result = ssl.verify_result();
148            let verified = self.verifies_clients() && result == X509VerifyResult::OK;
149            ClientCert {
150                common_name: name_entry(cert.subject_name(), Nid::COMMONNAME),
151                organization: name_entry(cert.subject_name(), Nid::ORGANIZATIONNAME),
152                verified,
153                error: (!verified).then(|| {
154                    if self.verifies_clients() {
155                        result.error_string().to_owned()
156                    } else {
157                        "client certificates are not configured".into()
158                    }
159                }),
160                serial_hex: cert
161                    .serial_number()
162                    .to_bn()
163                    .and_then(|b| b.to_hex_str())
164                    .map(|s| s.to_string())
165                    .unwrap_or_default(),
166            }
167        });
168        let info = TlsInfo {
169            version: ssl.version_str().to_owned(),
170            cipher: ssl
171                .current_cipher()
172                .map(|c| c.name().to_owned())
173                .unwrap_or_default(),
174            client_cert,
175        };
176        Ok((stream, info))
177    }
178}