refactor(server): migrate to rustls 0.23 and centralize TLS logic (#1389)

This commit upgrades the TLS stack to Rustls 0.23

Key changes:
- Dependencies: Updated 'rustls' (v0.23), 'tokio-rustls' (v0.26), and 'actix-web' (v4.12.1).
- Build Fix: Configured 'rustls' to use the 'ring' provider (disabling default 'aws-lc-rs') to ensure ARMv7 compatibility.
- Refactor: Created 'server/src/tls.rs' to handle certificate loading (DRY).
- LDAP: Updated 'ldap_server.rs' to use the new TLS module and Rustls APIs.
- Healthcheck: Updated 'healthcheck.rs' to use Rustls 0.23 types.
This commit is contained in:
lyzstrik
2026-01-31 09:47:11 +01:00
committed by GitHub
parent d1904a2759
commit 6f94134fdc
7 changed files with 195 additions and 150 deletions
Generated
+65 -50
View File
@@ -76,6 +76,7 @@ dependencies = [
"actix-codec",
"actix-rt",
"actix-service",
"actix-tls",
"actix-utils",
"base64 0.22.1",
"bitflags 2.10.0",
@@ -181,11 +182,11 @@ dependencies = [
"futures-core",
"impl-more",
"pin-project-lite",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.23.4",
"tokio-rustls 0.26.4",
"tokio-util",
"tracing",
"webpki-roots 0.22.6",
]
[[package]]
@@ -211,6 +212,7 @@ dependencies = [
"actix-rt",
"actix-server",
"actix-service",
"actix-tls",
"actix-utils",
"actix-web-codegen",
"bytes",
@@ -530,6 +532,28 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]]
name = "backtrace"
version = "0.3.76"
@@ -788,6 +812,15 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "cmake"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
dependencies = [
"cc",
]
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -1284,6 +1317,12 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
version = "1.15.0"
@@ -1487,6 +1526,12 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "fslock"
version = "0.2.1"
@@ -2700,7 +2745,7 @@ dependencies = [
"once_cell",
"quoted_printable",
"rustls 0.21.12",
"rustls-pemfile",
"rustls-pemfile 1.0.4",
"serde",
"socket2 0.4.10",
"tokio",
@@ -2814,8 +2859,9 @@ dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"reqwest 0.11.27",
"rustls 0.20.9",
"rustls-pemfile",
"rustls 0.23.35",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"sea-orm",
"secstr",
"serde",
@@ -2826,7 +2872,7 @@ dependencies = [
"thiserror 2.0.17",
"time",
"tokio",
"tokio-rustls 0.23.4",
"tokio-rustls 0.26.4",
"tokio-stream",
"tokio-util",
"tracing",
@@ -2838,7 +2884,7 @@ dependencies = [
"url",
"urlencoding",
"uuid 1.19.0",
"webpki-roots 0.22.6",
"webpki-roots 0.26.11",
]
[[package]]
@@ -4066,7 +4112,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"rustls 0.21.12",
"rustls-pemfile",
"rustls-pemfile 1.0.4",
"serde",
"serde_json",
"serde_urlencoded",
@@ -4233,18 +4279,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
dependencies = [
"log",
"ring 0.16.20",
"sct",
"webpki",
]
[[package]]
name = "rustls"
version = "0.21.12"
@@ -4263,6 +4297,7 @@ version = "0.23.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring 0.17.14",
@@ -4293,6 +4328,15 @@ dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.13.2"
@@ -4329,6 +4373,7 @@ version = "0.103.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
dependencies = [
"aws-lc-rs",
"ring 0.17.14",
"rustls-pki-types",
"untrusted 0.9.0",
@@ -5297,17 +5342,6 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "tokio-rustls"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls 0.20.9",
"tokio",
"webpki",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
@@ -5880,25 +5914,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
dependencies = [
"ring 0.17.14",
"untrusted 0.9.0",
]
[[package]]
name = "webpki-roots"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
dependencies = [
"webpki",
]
[[package]]
name = "webpki-roots"
version = "0.23.1"
+17 -9
View File
@@ -18,7 +18,6 @@ actix-http = "3"
actix-rt = "2"
actix-server = "2"
actix-service = "2"
actix-web = "4.3"
actix-web-httpauth = "0.8"
anyhow = "*"
async-trait = "0.1"
@@ -35,12 +34,12 @@ jwt = "0.16"
ldap3_proto = "0.6.0"
log = "*"
rand_chacha = "0.3"
rustls-pemfile = "1"
rustls-pemfile = "2"
serde_json = "1"
sha2 = "0.10"
thiserror = "2"
time = "0.3"
tokio-rustls = "0.23"
tokio-rustls = "0.26"
tokio-stream = "*"
tokio-util = "0.7"
tracing = "*"
@@ -48,7 +47,20 @@ tracing-actix-web = "0.7"
tracing-attributes = "^0.1.21"
tracing-log = "*"
urlencoding = "2"
webpki-roots = "0.22.2"
webpki-roots = "0.26"
[dependencies.actix-web]
features = ["rustls-0_23"]
version = "4.12.1"
[dependencies.rustls]
default-features = false
features = ["ring", "logging", "std", "tls12"]
version = "0.23"
[dependencies.rustls-pki-types]
features = ["std"]
version = "1"
[dependencies.chrono]
features = ["serde"]
@@ -145,7 +157,7 @@ features = ["smallvec", "chrono", "tokio"]
version = "^0.1.6"
[dependencies.actix-tls]
features = ["default", "rustls"]
features = ["default", "rustls-0_23"]
version = "3"
[dependencies.sea-orm]
@@ -163,10 +175,6 @@ version = "0.11"
default-features = false
features = ["rustls-tls-webpki-roots"]
[dependencies.rustls]
version = "0.20"
features = ["dangerous_configuration"]
[dependencies.url]
version = "2"
features = ["serde"]
+79 -42
View File
@@ -1,4 +1,4 @@
use crate::{configuration::LdapsOptions, ldap_server::read_certificates};
use crate::{configuration::LdapsOptions, tls};
use anyhow::{Context, Result, anyhow, bail, ensure};
use futures_util::SinkExt;
use ldap3_proto::{
@@ -8,6 +8,11 @@ use ldap3_proto::{
LdapSearchScope,
},
};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::crypto::{verify_tls12_signature, verify_tls13_signature};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{DigitallySignedStruct, SignatureScheme};
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio_rustls::TlsConnector as RustlsTlsConnector;
use tokio_util::codec::{FramedRead, FramedWrite};
@@ -74,56 +79,81 @@ pub async fn check_ldap(host: &str, port: u16) -> Result<()> {
check_ldap_endpoint(TcpStream::connect((host, port)).await?).await
}
fn get_root_certificates() -> rustls::RootCertStore {
let mut root_store = rustls::RootCertStore::empty();
root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
root_store
}
fn get_tls_connector(ldaps_options: &LdapsOptions) -> Result<RustlsTlsConnector> {
let mut client_config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(get_root_certificates())
.with_no_client_auth();
let (certs, _private_key) = read_certificates(ldaps_options)?;
// Check that the server cert is the one in the config file.
let certs = tls::load_certificates(&ldaps_options.cert_file)?;
let target_cert = certs.first().expect("empty certificate chain").clone();
#[derive(Debug)]
struct CertificateVerifier {
certificate: rustls::Certificate,
certificate_path: String,
certificate: CertificateDer<'static>,
}
impl rustls::client::ServerCertVerifier for CertificateVerifier {
impl ServerCertVerifier for CertificateVerifier {
fn verify_server_cert(
&self,
end_entity: &rustls::Certificate,
_intermediates: &[rustls::Certificate],
_server_name: &rustls::ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: std::time::SystemTime,
) -> std::result::Result<rustls::client::ServerCertVerified, rustls::Error> {
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
if end_entity != &self.certificate {
return Err(rustls::Error::InvalidCertificateData(format!(
"Server certificate doesn't match the one in the config file {}",
&self.certificate_path
)));
return Err(rustls::Error::InvalidCertificate(
rustls::CertificateError::NotValidForName,
));
}
Ok(rustls::client::ServerCertVerified::assertion())
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
verify_tls12_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
verify_tls13_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}
let mut dangerous_config = rustls::client::DangerousClientConfig {
cfg: &mut client_config,
};
dangerous_config.set_certificate_verifier(std::sync::Arc::new(CertificateVerifier {
certificate: certs.first().expect("empty certificate chain").clone(),
certificate_path: ldaps_options.cert_file.clone(),
}));
Ok(std::sync::Arc::new(client_config).into())
let verifier = Arc::new(CertificateVerifier {
certificate: target_cert,
});
let client_config = rustls::ClientConfig::builder_with_provider(
rustls::crypto::ring::default_provider().into(),
)
.with_safe_default_protocol_versions()
.context("Failed to set default protocol versions")?
.dangerous()
.with_custom_certificate_verifier(verifier)
.with_no_client_auth();
Ok(Arc::new(client_config).into())
}
#[instrument(skip_all, level = "info", err, fields(host = %host, port = %ldaps_options.port))]
@@ -134,10 +164,17 @@ pub async fn check_ldaps(host: &str, ldaps_options: &LdapsOptions) -> Result<()>
};
let tls_connector =
get_tls_connector(ldaps_options).context("while preparing the tls connection")?;
let domain = match host.parse::<std::net::IpAddr>() {
Ok(ip) => ServerName::IpAddress(ip.into()),
Err(_) => ServerName::try_from(host.to_string())
.map_err(|_| anyhow!("Invalid DNS name: {}", host))?,
};
check_ldap_endpoint(
tls_connector
.connect(
rustls::ServerName::try_from(host).context("while parsing the server name")?,
domain,
TcpStream::connect((host, ldaps_options.port))
.await
.context("while connecting TCP")?,
+12 -49
View File
@@ -1,14 +1,14 @@
use crate::configuration::{Configuration, LdapsOptions};
use crate::tls;
use actix_rt::net::TcpStream;
use actix_server::ServerBuilder;
use actix_service::{ServiceFactoryExt, fn_service};
use anyhow::{Context, Result, anyhow};
use anyhow::{Context, Result};
use ldap3_proto::{LdapCodec, control::LdapControl, proto::LdapMsg, proto::LdapOp};
use lldap_access_control::AccessControlledBackendHandler;
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler};
use lldap_ldap::{LdapHandler, LdapInfo};
use lldap_opaque_handler::OpaqueHandler;
use rustls::PrivateKey;
use tokio_rustls::TlsAcceptor as RustlsTlsAcceptor;
use tokio_util::codec::{FramedRead, FramedWrite};
use tracing::{debug, error, info, instrument};
@@ -102,55 +102,18 @@ where
Ok(requests.into_inner().unsplit(resp.into_inner()))
}
fn read_private_key(key_file: &str) -> Result<PrivateKey> {
use rustls_pemfile::{ec_private_keys, pkcs8_private_keys, rsa_private_keys};
use std::{fs::File, io::BufReader};
pkcs8_private_keys(&mut BufReader::new(File::open(key_file)?))
.map_err(anyhow::Error::from)
.and_then(|keys| {
keys.into_iter()
.next()
.ok_or_else(|| anyhow!("No PKCS8 key"))
})
.or_else(|_| {
rsa_private_keys(&mut BufReader::new(File::open(key_file)?))
.map_err(anyhow::Error::from)
.and_then(|keys| {
keys.into_iter()
.next()
.ok_or_else(|| anyhow!("No PKCS1 key"))
})
})
.or_else(|_| {
ec_private_keys(&mut BufReader::new(File::open(key_file)?))
.map_err(anyhow::Error::from)
.and_then(|keys| keys.into_iter().next().ok_or_else(|| anyhow!("No EC key")))
})
.with_context(|| {
format!("Cannot read either PKCS1, PKCS8 or EC private key from {key_file}")
})
.map(rustls::PrivateKey)
}
pub fn read_certificates(
ldaps_options: &LdapsOptions,
) -> Result<(Vec<rustls::Certificate>, rustls::PrivateKey)> {
use std::{fs::File, io::BufReader};
let certs = rustls_pemfile::certs(&mut BufReader::new(File::open(&ldaps_options.cert_file)?))?
.into_iter()
.map(rustls::Certificate)
.collect::<Vec<_>>();
let private_key = read_private_key(&ldaps_options.key_file)?;
Ok((certs, private_key))
}
fn get_tls_acceptor(ldaps_options: &LdapsOptions) -> Result<RustlsTlsAcceptor> {
let (certs, private_key) = read_certificates(ldaps_options)?;
let certs = tls::load_certificates(&ldaps_options.cert_file)?;
let private_key = tls::load_private_key(&ldaps_options.key_file)?;
let server_config = std::sync::Arc::new(
rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certs, private_key)?,
rustls::ServerConfig::builder_with_provider(
rustls::crypto::ring::default_provider().into(),
)
.with_safe_default_protocol_versions()
.context("Failed to set default protocol versions")?
.with_no_client_auth()
.with_single_cert(certs, private_key)?,
);
Ok(server_config.into())
}
+1
View File
@@ -17,6 +17,7 @@ mod mail;
mod sql_tcp_backend_handler;
mod tcp_backend_handler;
mod tcp_server;
mod tls;
use crate::{
cli::{Command, RunOpts, TestEmailOpts},
+1
View File
@@ -12,3 +12,4 @@ pub mod mail;
pub mod sql_tcp_backend_handler;
pub mod tcp_backend_handler;
pub mod tcp_server;
pub mod tls;
+20
View File
@@ -0,0 +1,20 @@
use anyhow::{Context, Result, anyhow};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
pub fn load_certificates(filename: &str) -> Result<Vec<CertificateDer<'static>>> {
let certs = CertificateDer::pem_file_iter(filename)
.with_context(|| format!("Unable to open or read certificate file: {}", filename))?
.collect::<Result<Vec<_>, _>>()
.with_context(|| format!("Error parsing certificates in {}", filename))?;
if certs.is_empty() {
return Err(anyhow!("No certificates found in {}", filename));
}
Ok(certs)
}
pub fn load_private_key(filename: &str) -> Result<PrivateKeyDer<'static>> {
PrivateKeyDer::from_pem_file(filename)
.with_context(|| format!("Unable to load private key from {}", filename))
}