diff --git a/Cargo.lock b/Cargo.lock index bfadf8f..cfab182 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/server/Cargo.toml b/server/Cargo.toml index 6e5df5c..ae75033 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -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"] diff --git a/server/src/healthcheck.rs b/server/src/healthcheck.rs index b7daa1f..fb891c5 100644 --- a/server/src/healthcheck.rs +++ b/server/src/healthcheck.rs @@ -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 { - 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, + end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, _ocsp_response: &[u8], - _now: std::time::SystemTime, - ) -> std::result::Result { + _now: UnixTime, + ) -> Result { 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 { + 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 { + verify_tls13_signature( + message, + cert, + dss, + &rustls::crypto::ring::default_provider().signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + 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::() { + 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")?, diff --git a/server/src/ldap_server.rs b/server/src/ldap_server.rs index dd94abf..5b4a134 100644 --- a/server/src/ldap_server.rs +++ b/server/src/ldap_server.rs @@ -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 { - 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::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::>(); - let private_key = read_private_key(&ldaps_options.key_file)?; - Ok((certs, private_key)) -} - fn get_tls_acceptor(ldaps_options: &LdapsOptions) -> Result { - 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()) } diff --git a/server/src/main.rs b/server/src/main.rs index 8af06df..2ad5e21 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -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}, diff --git a/server/src/mod.rs b/server/src/mod.rs index 0e0dd70..9fce5c4 100644 --- a/server/src/mod.rs +++ b/server/src/mod.rs @@ -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; diff --git a/server/src/tls.rs b/server/src/tls.rs new file mode 100644 index 0000000..0d6897c --- /dev/null +++ b/server/src/tls.rs @@ -0,0 +1,20 @@ +use anyhow::{Context, Result, anyhow}; +use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}; + +pub fn load_certificates(filename: &str) -> Result>> { + let certs = CertificateDer::pem_file_iter(filename) + .with_context(|| format!("Unable to open or read certificate file: {}", filename))? + .collect::, _>>() + .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::from_pem_file(filename) + .with_context(|| format!("Unable to load private key from {}", filename)) +}