Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -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 }
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
41 changes: 12 additions & 29 deletions src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,10 +83,7 @@ enum Transport {
#[cfg(feature = "tls")]
struct TlsOptions {
tcp_options: TcpOptions,
ca_path: Option<String>,
key_path: Option<String>,
cert_path: Option<String>,
verify_mode: SslVerifyMode,
tls_config: TlsConfig,
}

struct TcpOptions {
Expand Down Expand Up @@ -117,14 +114,14 @@ fn get_param(url: &Url, key: &str) -> Option<String> {
impl TlsOptions {
fn from_url(url: &Url) -> Result<Self, MemcacheError> {
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");
Expand All @@ -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,
},
})
}
}
Expand Down Expand Up @@ -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)
}
};
Expand Down
20 changes: 7 additions & 13 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,9 @@ pub enum MemcacheError {
ServerError(ServerError),
/// Command specific Errors
CommandError(CommandError),
/// TLS related errors
#[cfg(feature = "tls")]
OpensslError(openssl::ssl::HandshakeError<std::net::TcpStream>),
TlsError(rustls::Error),
/// Parse errors
ParseError(ParseError),
/// ConnectionPool errors
Expand All @@ -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),
Expand All @@ -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,
Expand All @@ -278,16 +279,9 @@ impl From<io::Error> for MemcacheError {
}

#[cfg(feature = "tls")]
impl From<openssl::error::ErrorStack> for MemcacheError {
fn from(err: openssl::error::ErrorStack) -> MemcacheError {
MemcacheError::OpensslError(openssl::ssl::HandshakeError::<std::net::TcpStream>::from(err))
}
}

#[cfg(feature = "tls")]
impl From<openssl::ssl::HandshakeError<std::net::TcpStream>> for MemcacheError {
fn from(err: openssl::ssl::HandshakeError<std::net::TcpStream>) -> MemcacheError {
MemcacheError::OpensslError(err)
impl From<rustls::Error> for MemcacheError {
fn from(err: rustls::Error) -> MemcacheError {
MemcacheError::TlsError(err)
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand Down
4 changes: 2 additions & 2 deletions src/stream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ 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),
Udp(UdpStream),
#[cfg(unix)]
Unix(UnixStream),
#[cfg(feature = "tls")]
Tls(SslStream<TcpStream>),
Tls(StreamOwned<ClientConnection, TcpStream>),
}

impl Stream {
Expand Down
150 changes: 150 additions & 0 deletions src/tls.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub(crate) key_path: Option<String>,
pub(crate) cert_path: Option<String>,
pub(crate) verify_mode: VerifyMode,
}

/// A verifier that accepts any server certificate, used for `verify_mode=none`.
#[derive(Debug)]
struct NoVerifier {
provider: Arc<CryptoProvider>,
}

impl ServerCertVerifier for NoVerifier {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}

fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
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<HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
}

fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
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<Vec<CertificateDer<'static>>, MemcacheError> {
let mut reader = BufReader::new(File::open(path)?);
CertificateDer::pem_reader_iter(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|err| pem_error(path, err))
}

fn load_key(path: &str) -> Result<PrivateKeyDer<'static>, 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<ClientConfig, MemcacheError> {
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<StreamOwned<ClientConnection, TcpStream>, 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)
}
Loading