From 178cf79b63c650b820263beb059f949742a87748 Mon Sep 17 00:00:00 2001 From: An Long Date: Sun, 23 Aug 2026 02:53:34 +0900 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Switch=20TLS=20backend=20t?= =?UTF-8?q?o=20rustls=20and=20make=20the=20tls=20feature=20opt=20in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 8 ++- README.md | 11 +++- src/connection.rs | 41 ++++--------- src/error.rs | 20 +++---- src/lib.rs | 4 +- src/stream/mod.rs | 4 +- src/tls.rs | 150 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 188 insertions(+), 50 deletions(-) create mode 100644 src/tls.rs diff --git a/Cargo.toml b/Cargo.toml index 0de1296..d85e906 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ keywords = ["memcache", "memcached", "driver", "cache", "database"] edition = "2024" [features] -default = ["tls"] -tls = ["openssl"] +default = [] +tls = ["dep:rustls", "dep:rustls-pki-types", "dep:rustls-native-certs"] tokio = ["dep:tokio"] serde_json = ["dep:serde", "dep:serde_json"] @@ -20,7 +20,9 @@ byteorder = "1" url = "^2.1" rand = "0.10" enum_dispatch = "0.3" -openssl = { version = "^0.10", optional = true } +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"], optional = true } +rustls-pki-types = { version = "1", features = ["std"], optional = true } +rustls-native-certs = { version = "0.8", optional = true } r2d2 = "^0.8" tokio = { version = "1", features = ["io-util", "net", "rt", "sync", "time"], optional = true } serde = { version = "1", optional = true } diff --git a/README.md b/README.md index e06e9af..6ec01a9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,15 @@ The crate is called `memcache` and you can depend on it via cargo: memcache = "*" ``` +TLS support is behind the `tls` feature: + +```ini +[dependencies] +memcache = { version = "*", features = ["tls"] } +``` + +Connect with a `memcache+tls://` URL. Query parameters: `verify_mode` (`peer` by default, or `none`), `ca_path`, and `cert_path` with `key_path`, all PEM files. + ## Features ### `memcache::exp` (experimental) @@ -53,7 +62,7 @@ The classic client. See [Basic usage](#basic-usage). - [x] TCP connection - [x] UDP connection - [x] UNIX Domain socket connection - - [x] TLS connection + - [x] TLS connection (`tls` feature) - [x] Typed interface - [x] Memcached cluster support with custom key hash algorithm - [x] Authority diff --git a/src/connection.rs b/src/connection.rs index a8bb486..d8af6e0 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -12,7 +12,7 @@ use crate::protocol::{AsciiProtocol, BinaryProtocol, Protocol, ProtocolTrait}; use crate::stream::Stream; use crate::stream::UdpStream; #[cfg(feature = "tls")] -use openssl::ssl::{SslConnector, SslFiletype, SslMethod, SslVerifyMode}; +use crate::tls::{self, TlsConfig, VerifyMode}; use r2d2::ManageConnection; /// A connection to the memcached server @@ -83,10 +83,7 @@ enum Transport { #[cfg(feature = "tls")] struct TlsOptions { tcp_options: TcpOptions, - ca_path: Option, - key_path: Option, - cert_path: Option, - verify_mode: SslVerifyMode, + tls_config: TlsConfig, } struct TcpOptions { @@ -117,14 +114,14 @@ fn get_param(url: &Url, key: &str) -> Option { impl TlsOptions { fn from_url(url: &Url) -> Result { let verify_mode = match get_param(url, "verify_mode").as_ref().map(String::as_str) { - Some("none") => SslVerifyMode::NONE, - Some("peer") => SslVerifyMode::PEER, + Some("none") => VerifyMode::None, + Some("peer") => VerifyMode::Peer, Some(_) => { return Err(MemcacheError::BadURL( "unknown verify_mode, expected 'none' or 'peer'".into(), )); } - None => SslVerifyMode::PEER, + None => VerifyMode::Peer, }; let ca_path = get_param(url, "ca_path"); @@ -143,10 +140,12 @@ impl TlsOptions { Ok(TlsOptions { tcp_options: TcpOptions::from_url(url), - ca_path: ca_path, - key_path: key_path, - cert_path: cert_path, - verify_mode: verify_mode, + tls_config: TlsConfig { + ca_path, + key_path, + cert_path, + verify_mode, + }, }) } } @@ -240,24 +239,8 @@ impl Connection { .host_str() .ok_or(MemcacheError::BadURL("host required for TLS connection".into()))?; - let mut builder = SslConnector::builder(SslMethod::tls())?; - builder.set_verify(options.verify_mode); - - if options.ca_path.is_some() { - builder.set_ca_file(&options.ca_path.unwrap())?; - } - - if options.key_path.is_some() { - builder.set_private_key_file(options.key_path.unwrap(), SslFiletype::PEM)?; - } - - if options.cert_path.is_some() { - builder.set_certificate_chain_file(options.cert_path.unwrap())?; - } - - let tls_conn = builder.build(); let tcp_stream = tcp_stream(url, &options.tcp_options)?; - let tls_stream = tls_conn.connect(host, tcp_stream)?; + let tls_stream = tls::connect(host, tcp_stream, &options.tls_config)?; Stream::Tls(tls_stream) } }; diff --git a/src/error.rs b/src/error.rs index ac458dd..ab087d9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -231,8 +231,9 @@ pub enum MemcacheError { ServerError(ServerError), /// Command specific Errors CommandError(CommandError), + /// TLS related errors #[cfg(feature = "tls")] - OpensslError(openssl::ssl::HandshakeError), + TlsError(rustls::Error), /// Parse errors ParseError(ParseError), /// ConnectionPool errors @@ -245,7 +246,7 @@ impl fmt::Display for MemcacheError { MemcacheError::BadURL(ref s) => s.fmt(f), MemcacheError::IOError(ref err) => err.fmt(f), #[cfg(feature = "tls")] - MemcacheError::OpensslError(ref err) => err.fmt(f), + MemcacheError::TlsError(ref err) => err.fmt(f), MemcacheError::ParseError(ref err) => err.fmt(f), MemcacheError::ClientError(ref err) => err.fmt(f), MemcacheError::ServerError(ref err) => err.fmt(f), @@ -261,7 +262,7 @@ impl error::Error for MemcacheError { MemcacheError::BadURL(_) => None, MemcacheError::IOError(ref err) => err.source(), #[cfg(feature = "tls")] - MemcacheError::OpensslError(ref err) => err.source(), + MemcacheError::TlsError(ref err) => err.source(), MemcacheError::ParseError(ref p) => p.source(), MemcacheError::ClientError(_) => None, MemcacheError::ServerError(_) => None, @@ -278,16 +279,9 @@ impl From for MemcacheError { } #[cfg(feature = "tls")] -impl From for MemcacheError { - fn from(err: openssl::error::ErrorStack) -> MemcacheError { - MemcacheError::OpensslError(openssl::ssl::HandshakeError::::from(err)) - } -} - -#[cfg(feature = "tls")] -impl From> for MemcacheError { - fn from(err: openssl::ssl::HandshakeError) -> MemcacheError { - MemcacheError::OpensslError(err) +impl From for MemcacheError { + fn from(err: rustls::Error) -> MemcacheError { + MemcacheError::TlsError(err) } } diff --git a/src/lib.rs b/src/lib.rs index 5c57d55..9c3e022 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,8 +67,6 @@ assert_eq!(answer, 42); extern crate byteorder; extern crate enum_dispatch; -#[cfg(feature = "tls")] -extern crate openssl; extern crate r2d2; extern crate rand; extern crate url; @@ -79,6 +77,8 @@ mod error; pub mod exp; mod protocol; mod stream; +#[cfg(feature = "tls")] +mod tls; mod value; pub use crate::client::{Client, ClientBuilder, Connectable}; diff --git a/src/stream/mod.rs b/src/stream/mod.rs index 5b38472..33b1f75 100644 --- a/src/stream/mod.rs +++ b/src/stream/mod.rs @@ -10,7 +10,7 @@ pub(crate) use self::udp_stream::UdpStream; use crate::error::MemcacheError; #[cfg(feature = "tls")] -use openssl::ssl::SslStream; +use rustls::{ClientConnection, StreamOwned}; pub enum Stream { Tcp(TcpStream), @@ -18,7 +18,7 @@ pub enum Stream { #[cfg(unix)] Unix(UnixStream), #[cfg(feature = "tls")] - Tls(SslStream), + Tls(StreamOwned), } impl Stream { diff --git a/src/tls.rs b/src/tls.rs new file mode 100644 index 0000000..d938714 --- /dev/null +++ b/src/tls.rs @@ -0,0 +1,150 @@ +use std::fs::File; +use std::io::{self, BufReader}; +use std::net::TcpStream; +use std::sync::Arc; + +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::crypto::{CryptoProvider, ring}; +use rustls::{ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, SignatureScheme, StreamOwned}; +use rustls_pki_types::pem::PemObject; +use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}; + +use crate::error::MemcacheError; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum VerifyMode { + None, + Peer, +} + +pub(crate) struct TlsConfig { + pub(crate) ca_path: Option, + pub(crate) key_path: Option, + pub(crate) cert_path: Option, + pub(crate) verify_mode: VerifyMode, +} + +/// A verifier that accepts any server certificate, used for `verify_mode=none`. +#[derive(Debug)] +struct NoVerifier { + provider: Arc, +} + +impl ServerCertVerifier for NoVerifier { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms) + } + + fn supported_verify_schemes(&self) -> Vec { + self.provider.signature_verification_algorithms.supported_schemes() + } +} + +fn pem_error(path: &str, err: rustls_pki_types::pem::Error) -> MemcacheError { + MemcacheError::IOError(io::Error::new( + io::ErrorKind::InvalidData, + format!("failed to parse PEM file {}: {}", path, err), + )) +} + +fn load_certs(path: &str) -> Result>, MemcacheError> { + let mut reader = BufReader::new(File::open(path)?); + CertificateDer::pem_reader_iter(&mut reader) + .collect::, _>>() + .map_err(|err| pem_error(path, err)) +} + +fn load_key(path: &str) -> Result, MemcacheError> { + let mut reader = BufReader::new(File::open(path)?); + PrivateKeyDer::from_pem_reader(&mut reader).map_err(|err| pem_error(path, err)) +} + +fn build_client_config(config: &TlsConfig) -> Result { + let provider = Arc::new(ring::default_provider()); + let builder = ClientConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions() + .map_err(MemcacheError::TlsError)?; + + let builder = match config.verify_mode { + VerifyMode::None => builder + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoVerifier { provider })), + VerifyMode::Peer => { + let mut roots = RootCertStore::empty(); + match &config.ca_path { + Some(ca_path) => { + for cert in load_certs(ca_path)? { + roots.add(cert)?; + } + } + None => { + let native = rustls_native_certs::load_native_certs(); + if native.certs.is_empty() + && let Some(err) = native.errors.into_iter().next() + { + return Err(MemcacheError::IOError(io::Error::other(format!( + "failed to load native root certificates: {}", + err + )))); + } + for cert in native.certs { + roots.add(cert)?; + } + } + } + builder.with_root_certificates(roots) + } + }; + + match (&config.cert_path, &config.key_path) { + (Some(cert_path), Some(key_path)) => { + let certs = load_certs(cert_path)?; + let key = load_key(key_path)?; + Ok(builder.with_client_auth_cert(certs, key)?) + } + _ => Ok(builder.with_no_client_auth()), + } +} + +pub(crate) fn connect( + host: &str, + tcp_stream: TcpStream, + config: &TlsConfig, +) -> Result, MemcacheError> { + let client_config = Arc::new(build_client_config(config)?); + let server_name = ServerName::try_from(host.to_string()) + .map_err(|_| MemcacheError::BadURL(format!("invalid TLS server name: {}", host)))?; + let conn = ClientConnection::new(client_config, server_name)?; + let mut stream = StreamOwned::new(conn, tcp_stream); + // Drive the handshake eagerly so handshake failures surface at connect time + // rather than on the first read or write. + while stream.conn.is_handshaking() { + stream.conn.complete_io(&mut stream.sock)?; + } + Ok(stream) +}