Repository: blackbeam/rust-mysql-simple (language: rust)

Issue:
Add support for rustls via a feature flag
Hi there,

Something I find missing in the current `mysql` crate is the forced dependency on OpenSSL, which I want to avoid as much as possible. Other crates like `reqwest` solve this by having a feature flag to use rustls instead. `reqwest` relies on `hyper-rustls`. 

Proposed solution:  
Add a feature flag to the `mysql` crate to use `rustls` instead of `openssl`, or even to disable TLS completely for those that don't use it.

Thanks!


Discussion / hints:
I agree that it would nice to have.

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index c9f32b2f..92844a23 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ debug = true
 
 [features]
 default = ["native-tls"]
+rustls-tls = ["rustls", "webpki", "webpki-roots", "rustls-pemfile"]
 nightly = []
 
 [dev-dependencies]
@@ -52,6 +53,23 @@ url = "2.1"
 version = "0.2.3"
 optional = true
 
+[dependencies.rustls]
+version = "0.20.0"
+features = ["dangerous_configuration"]
+optional = true
+
+[dependencies.rustls-pemfile]
+version = "0.2.1"
+optional = true
+
+[dependencies.webpki]
+version = "0.22.0"
+optional = true
+
+[dependencies.webpki-roots]
+version = "0.22.1"
+optional = true
+
 [target.'cfg(target_os = "windows")'.dependencies]
 named_pipe = "~0.4"
 
diff --git a/Makefile b/Makefile
index 40c55c22..2930ba84 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,8 @@
 mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
 mkfile_dir := $(dir $(mkfile_path))
 MYSQL_DATA_DIR = $(mkfile_dir)tests/rust-mysql-simple-test
-MYSQL_SSL_CA = $(mkfile_dir)tests/ca-cert.pem
-MYSQL_SSL_CERT = $(mkfile_dir)tests/server-cert.pem
+MYSQL_SSL_CA = $(mkfile_dir)tests/ca.crt
+MYSQL_SSL_CERT = $(mkfile_dir)tests/server.crt
 MYSQL_SSL_KEY = $(mkfile_dir)tests/server-key.pem
 MYSQL_PORT = 3307
 BASEDIR := $(shell mysqld --verbose --help 2>/dev/null | grep -e '^basedir' | awk '{ print $$2 }')
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 4daf6ec0..fe550251 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -44,6 +44,12 @@ jobs:
           SSL=true COMPRESS=false cargo test
           SSL=false COMPRESS=true cargo test
           SSL=true COMPRESS=true cargo test
+
+          SSL=true COMPRESS=false cargo test --no-default-features --features rustls-tls
+          SSL=true COMPRESS=true cargo test --no-default-features --features rustls-tls
+
+          SSL=false COMPRESS=true cargo test --no-default-features
+          SSL=false COMPRESS=false cargo test --no-default-features
         env:
           RUST_BACKTRACE: 1
           DATABASE_URL: mysql://root:root@127.0.0.1:3306/mysql
@@ -84,6 +90,12 @@ jobs:
           SSL=true COMPRESS=false cargo test
           SSL=false COMPRESS=true cargo test
           SSL=true COMPRESS=true cargo test
+
+          SSL=true COMPRESS=false cargo test --no-default-features --features rustls-tls
+          SSL=true COMPRESS=true cargo test --no-default-features --features rustls-tls
+
+          SSL=false COMPRESS=true cargo test --no-default-features
+          SSL=false COMPRESS=false cargo test --no-default-features
         env:
           RUST_BACKTRACE: 1
           DATABASE_URL: mysql://root@127.0.0.1/mysql
@@ -129,6 +141,12 @@ jobs:
           SSL=true COMPRESS=false cargo test
           SSL=false COMPRESS=true cargo test
           SSL=true COMPRESS=true cargo test
+
+          SSL=true COMPRESS=false cargo test --no-default-features --features rustls-tls
+          SSL=true COMPRESS=true cargo test --no-default-features --features rustls-tls
+
+          SSL=false COMPRESS=true cargo test --no-default-features
+          SSL=false COMPRESS=false cargo test --no-default-features
         env:
           RUST_BACKTRACE: 1
           DATABASE_URL: mysql://root:password@127.0.0.1/mysql
@@ -173,6 +191,12 @@ jobs:
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL COMPRESS=true cargo test"
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=$SSL cargo test"
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=$SSL COMPRESS=true cargo test"
+
+          docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=true COMPRESS=false cargo test --no-default-features --features rustls-tls"
+          docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=true COMPRESS=true cargo test --no-default-features --features rustls-tls"
+
+          docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=false COMPRESS=true cargo test --no-default-features"
+          docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=false COMPRESS=false cargo test --no-default-features"
         env:
           RUST_BACKTRACE: 1
           DATABASE_URL: mysql://root:password@127.0.0.1/mysql
@@ -235,6 +259,12 @@ jobs:
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL COMPRESS=true cargo test"
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=true cargo test"
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=true COMPRESS=true cargo test"
+
+          docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=true COMPRESS=false cargo test --no-default-features --features rustls-tls"
+          docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=true COMPRESS=true cargo test --no-default-features --features rustls-tls"
+
+          docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=false COMPRESS=true cargo test --no-default-features"
+          docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=false COMPRESS=false cargo test --no-default-features"
         env:
           RUST_BACKTRACE: 1
           DATABASE_URL: mysql://root:password@127.0.0.1/mysql
diff --git a/src/conn/mod.rs b/src/conn/mod.rs
index 9722c005..c366b056 100644
--- a/src/conn/mod.rs
+++ b/src/conn/mod.rs
@@ -23,14 +23,11 @@ use mysql_common::{
     value::ServerSide,
 };
 
-#[cfg(feature = "native-tls")]
 use mysql_common::{
     constants::{DEFAULT_MAX_ALLOWED_PACKET, UTF8_GENERAL_CI},
-    packets::SslRequest
+    packets::SslRequest,
 };
 
-
-
 use std::{
     borrow::{Borrow, Cow},
     cmp,
@@ -67,10 +64,7 @@ use crate::{
     Value::{self, Bytes, NULL},
 };
 
-
-#[cfg(feature = "native-tls")]
 use crate::DriverError::TlsNotSupported;
-#[cfg(feature = "native-tls")]
 use crate::SslOpts;
 
 use self::binlog_stream::BinlogStream;
@@ -375,7 +369,6 @@ impl Conn {
         }
     }
 
-    #[cfg(feature = "native-tls")]
     fn switch_to_ssl(&mut self, ssl_opts: SslOpts) -> Result<()> {
         let stream = self.0.stream.take().expect("incomplete conn");
         let (in_buf, out_buf, codec, stream) = stream.destruct();
@@ -530,7 +523,6 @@ impl Conn {
 
         self.handle_handshake(&handshake);
 
-        #[cfg(feature = "native-tls")]
         if self.is_insecure() {
             if let Some(ssl_opts) = self.0.opts.get_ssl_opts().cloned() {
                 if !handshake
@@ -628,7 +620,6 @@ impl Conn {
         attrs
     }
 
-    #[cfg(feature = "native-tls")]
     fn do_ssl_request(&mut self) -> Result<()> {
         let ssl_request = SslRequest::new(
             self.get_client_flags(),
@@ -1168,11 +1159,7 @@ mod test {
             time::Duration,
         };
 
-        use mysql_common::{
-            binlog::events::EventData, packets::binlog_request::BinlogRequest,
-            value::json::Serialized,
-        };
-        use serde_json::json;
+        use mysql_common::{binlog::events::EventData, packets::binlog_request::BinlogRequest};
 
         use crate::{
             from_row, from_value, params,
@@ -1898,7 +1885,7 @@ mod test {
             let port = 28000 + (rand::random::<u16>() % 2000);
             let opts = OptsBuilder::from_opts(get_opts())
                 .prefer_socket(false)
-                .ip_or_hostname(Some("127.0.0.1"))
+                .ip_or_hostname(Some("localhost"))
                 .bind_address(Some(([127, 0, 0, 1], port)));
             let conn = Conn::new(opts).unwrap();
             let debug_format: String = format!("{:?}", conn);
@@ -1916,7 +1903,7 @@ mod test {
             let port = 30000 + (rand::random::<u16>() % 2000);
             let opts = OptsBuilder::from_opts(get_opts())
                 .prefer_socket(false)
-                .ip_or_hostname(Some("127.0.0.1"))
+                .ip_or_hostname(Some("localhost"))
                 .bind_address(Some(([127, 0, 0, 1], port)))
                 .tcp_connect_timeout(Some(::std::time::Duration::from_millis(1000)));
             let mut conn = Conn::new(opts).unwrap();
diff --git a/src/conn/opts.rs b/src/conn/opts/mod.rs
similarity index 97%
rename from src/conn/opts.rs
rename to src/conn/opts/mod.rs
index dbd6341b..fb685e48 100644
--- a/src/conn/opts.rs
+++ b/src/conn/opts/mod.rs
@@ -18,33 +18,30 @@ use crate::{consts::CapabilityFlags, Compression, LocalInfileHandler, UrlError};
 /// Default value for client side per-connection statement cache.
 pub const DEFAULT_STMT_CACHE_SIZE: usize = 32;
 
+mod native_tls_opts;
+mod rustls_opts;
+
+#[cfg(feature = "native-tls")]
+pub use native_tls_opts::ClientIdentity;
+
+#[cfg(feature = "rustls-tls")]
+pub use rustls_opts::ClientIdentity;
+
 /// Ssl Options.
 #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
 pub struct SslOpts {
-    pkcs12_path: Option<Cow<'static, Path>>,
-    password: Option<Cow<'static, str>>,
+    #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
+    client_identity: Option<ClientIdentity>,
     root_cert_path: Option<Cow<'static, Path>>,
     skip_domain_validation: bool,
     accept_invalid_certs: bool,
 }
 
 impl SslOpts {
-    /// Sets path to the pkcs12 archive.
-    ///
-    /// If you have the client's private key and certificate in PEM format, you
-    /// can translate them to pkcs12 using `openssl`:
-    ///
-    /// ```text
-    /// openssl pkcs12 -password pass: -export -out path.p12 -inkey privatekey.pem -in cert.pem -no-CAfile
-    /// ```
-    pub fn with_pkcs12_path<T: Into<Cow<'static, Path>>>(mut self, pkcs12_path: Option<T>) -> Self {
-        self.pkcs12_path = pkcs12_path.map(Into::into);
-        self
-    }
-
-    /// Sets the password for a pkcs12 archive (defaults to `None`).
-    pub fn with_password<T: Into<Cow<'static, str>>>(mut self, password: Option<T>) -> Self {
-        self.password = password.map(Into::into);
+    /// Sets the client identity.
+    #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
+    pub fn with_client_identity(mut self, identity: Option<ClientIdentity>) -> Self {
+        self.client_identity = identity;
         self
     }
 
@@ -74,12 +71,9 @@ impl SslOpts {
         self
     }
 
-    pub fn pkcs12_path(&self) -> Option<&Path> {
-        self.pkcs12_path.as_ref().map(|x| x.as_ref())
-    }
-
-    pub fn password(&self) -> Option<&str> {
-        self.password.as_ref().map(AsRef::as_ref)
+    #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
+    pub fn client_identity(&self) -> Option<&ClientIdentity> {
+        self.client_identity.as_ref()
     }
 
     pub fn root_cert_path(&self) -> Option<&Path> {
diff --git a/src/conn/opts/native_tls_opts.rs b/src/conn/opts/native_tls_opts.rs
new file mode 100644
index 00000000..0d31f2a1
--- /dev/null
+++ b/src/conn/opts/native_tls_opts.rs
@@ -0,0 +1,51 @@
+#![cfg(feature = "native-tls")]
+
+use native_tls::Identity;
+
+use std::{borrow::Cow, path::Path};
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ClientIdentity {
+    pkcs12_path: Cow<'static, Path>,
+    password: Option<Cow<'static, str>>,
+}
+
+impl ClientIdentity {
+    /// Creates new identity with the given path to the pkcs12 archive.
+    pub fn new<T>(pkcs12_path: T) -> Self
+    where
+        T: Into<Cow<'static, Path>>,
+    {
+        Self {
+            pkcs12_path: pkcs12_path.into(),
+            password: None,
+        }
+    }
+
+    /// Sets the archive password.
+    pub fn with_password<T>(mut self, pass: T) -> Self
+    where
+        T: Into<Cow<'static, str>>,
+    {
+        self.password = Some(pass.into());
+        self
+    }
+
+    /// Returns the pkcs12 archive path.
+    pub fn pkcs12_path(&self) -> &Path {
+        self.pkcs12_path.as_ref()
+    }
+
+    /// Returns the archive password.
+    pub fn password(&self) -> Option<&str> {
+        self.password.as_ref().map(AsRef::as_ref)
+    }
+
+    pub(crate) fn load(&self) -> crate::Result<Identity> {
+        let der = std::fs::read(self.pkcs12_path.as_ref())?;
+        Ok(Identity::from_pkcs12(
+            &*der,
+            self.password.as_deref().unwrap_or(""),
+        )?)
+    }
+}
diff --git a/src/conn/opts/rustls_opts.rs b/src/conn/opts/rustls_opts.rs
new file mode 100644
index 00000000..2e572508
--- /dev/null
+++ b/src/conn/opts/rustls_opts.rs
@@ -0,0 +1,79 @@
+#![cfg(feature = "rustls-tls")]
+
+use rustls::{Certificate, PrivateKey};
+use rustls_pemfile::{certs, rsa_private_keys};
+
+use std::{borrow::Cow, path::Path};
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ClientIdentity {
+    cert_chain_path: Cow<'static, Path>,
+    priv_key_path: Cow<'static, Path>,
+}
+
+impl ClientIdentity {
+    /// Creates new identity.
+    ///
+    /// `cert_chain_path` - path to a certificate chain (in PEM or DER)
+    /// `priv_key_path` - path to a private key (in DER or PEM) (it'll take the first one)
+    pub fn new<T, U>(cert_chain_path: T, priv_key_path: U) -> Self
+    where
+        T: Into<Cow<'static, Path>>,
+        U: Into<Cow<'static, Path>>,
+    {
+        Self {
+            cert_chain_path: cert_chain_path.into(),
+            priv_key_path: priv_key_path.into(),
+        }
+    }
+
+    /// Sets the certificate chain path (in DER or PEM).
+    pub fn with_cert_chain_path<T>(mut self, cert_chain_path: T) -> Self
+    where
+        T: Into<Cow<'static, Path>>,
+    {
+        self.cert_chain_path = cert_chain_path.into();
+        self
+    }
+
+    /// Sets the private key path (in DER or PEM) (it'll take the first one).
+    pub fn with_priv_key_path<T>(mut self, priv_key_path: T) -> Self
+    where
+        T: Into<Cow<'static, Path>>,
+    {
+        self.priv_key_path = priv_key_path.into();
+        self
+    }
+
+    /// Returns the certificate chain path.
+    pub fn cert_chain_path(&self) -> &Path {
+        self.cert_chain_path.as_ref()
+    }
+
+    /// Returns the private key path.
+    pub fn priv_key_path(&self) -> &Path {
+        self.priv_key_path.as_ref()
+    }
+
+    pub(crate) fn load(&self) -> crate::Result<(Vec<Certificate>, PrivateKey)> {
+        let cert_data = std::fs::read(self.cert_chain_path.as_ref())?;
+        let key_data = std::fs::read(self.priv_key_path.as_ref())?;
+
+        let mut cert_chain = Vec::new();
+        for cert in certs(&mut &*cert_data)? {
+            cert_chain.push(Certificate(cert));
+        }
+        if cert_chain.is_empty() && !cert_data.is_empty() {
+            cert_chain.push(Certificate(cert_data));
+        }
+
+        let mut priv_key = None;
+        for key in rsa_private_keys(&mut &*key_data)?.into_iter().take(1) {
+            priv_key = Some(PrivateKey(key));
+        }
+
+        let priv_key = priv_key.unwrap_or_else(|| PrivateKey(key_data));
+
+        Ok((cert_chain, priv_key))
+    }
+}
diff --git a/src/error.rs b/src/error/mod.rs
similarity index 95%
rename from src/error.rs
rename to src/error/mod.rs
index e8d0f3b3..c9812992 100644
--- a/src/error.rs
+++ b/src/error/mod.rs
@@ -17,6 +17,8 @@ use std::{error, fmt, io, result, sync};
 
 use crate::{Row, Value};
 
+pub mod tls;
+
 impl<'a> From<packets::ServerError<'a>> for MySqlError {
     fn from(x: packets::ServerError<'a>) -> MySqlError {
         MySqlError {
@@ -58,10 +60,8 @@ pub enum Error {
     MySqlError(MySqlError),
     DriverError(DriverError),
     UrlError(UrlError),
-    #[cfg(feature = "native-tls")]
-    TlsError(native_tls::Error),
-    #[cfg(feature = "native-tls")]
-    TlsHandshakeError(native_tls::HandshakeError<std::net::TcpStream>),
+    #[cfg(any(feature = "native-tls", feature = "rustls"))]
+    TlsError(tls::TlsError),
     FromValueError(Value),
     FromRowError(Row),
 }
@@ -70,11 +70,9 @@ impl Error {
     #[doc(hidden)]
     pub fn is_connectivity_error(&self) -> bool {
         match self {
-            #[cfg(feature = "native-tls")]
-            Error::TlsError(_) | Error::TlsHandshakeError(_) => true,
-            Error::IoError(_)
-            | Error::DriverError(_)
-            | Error::CodecError(_) => true,
+            #[cfg(any(feature = "native-tls", feature = "rustls"))]
+            Error::TlsError(_) => true,
+            Error::IoError(_) | Error::DriverError(_) | Error::CodecError(_) => true,
             Error::MySqlError(_)
             | Error::UrlError(_)
             | Error::FromValueError(_)
@@ -84,32 +82,14 @@ impl Error {
 }
 
 impl error::Error for Error {
-    fn description(&self) -> &str {
-        match *self {
-            Error::IoError(_) => "I/O Error",
-            Error::CodecError(_) => "protocol codec error",
-            Error::MySqlError(_) => "MySql server error",
-            Error::DriverError(_) => "driver error",
-            Error::UrlError(_) => "url error",
-            #[cfg(feature = "native-tls")]
-            Error::TlsError(_) => "tls error",
-            #[cfg(feature = "native-tls")]
-            Error::TlsHandshakeError(_) => "tls handshake error",
-            Error::FromRowError(_) => "from row conversion error",
-            Error::FromValueError(_) => "from value conversion error",
-        }
-    }
-
     fn cause(&self) -> Option<&dyn error::Error> {
         match *self {
             Error::IoError(ref err) => Some(err),
             Error::DriverError(ref err) => Some(err),
             Error::MySqlError(ref err) => Some(err),
             Error::UrlError(ref err) => Some(err),
-            #[cfg(feature = "native-tls")]
+            #[cfg(any(feature = "native-tls", feature = "rustls"))]
             Error::TlsError(ref err) => Some(err),
-            #[cfg(feature = "native-tls")]
-            Error::TlsHandshakeError(ref err) => Some(err),
             _ => None,
         }
     }
@@ -176,20 +156,6 @@ impl From<::nix::Error> for Error {
     }
 }
 
-#[cfg(feature = "native-tls")]
-impl From<native_tls::Error> for Error {
-    fn from(err: native_tls::Error) -> Error {
-        Error::TlsError(err)
-    }
-}
-
-#[cfg(feature = "native-tls")]
-impl From<native_tls::HandshakeError<std::net::TcpStream>> for Error {
-    fn from(err: native_tls::HandshakeError<std::net::TcpStream>) -> Error {
-        Error::TlsHandshakeError(err)
-    }
-}
-
 impl From<UrlError> for Error {
     fn from(err: UrlError) -> Error {
         Error::UrlError(err)
@@ -210,10 +176,8 @@ impl fmt::Display for Error {
             Error::MySqlError(ref err) => write!(f, "MySqlError {{ {} }}", err),
             Error::DriverError(ref err) => write!(f, "DriverError {{ {} }}", err),
             Error::UrlError(ref err) => write!(f, "UrlError {{ {} }}", err),
-            #[cfg(feature = "native-tls")]
+            #[cfg(any(feature = "native-tls", feature = "rustls"))]
             Error::TlsError(ref err) => write!(f, "TlsError {{ {} }}", err),
-            #[cfg(feature = "native-tls")]
-            Error::TlsHandshakeError(ref err) => write!(f, "TlsHandshakeError {{ {} }}", err),
             Error::FromRowError(_) => "from row conversion error".fmt(f),
             Error::FromValueError(_) => "from value conversion error".fmt(f),
         }
@@ -239,7 +203,6 @@ pub enum DriverError {
     MismatchedStmtParams(u16, usize),
     InvalidPoolConstraints,
     SetupError,
-    #[cfg(feature = "native-tls")]
     TlsNotSupported,
     CouldNotParseVersion,
     ReadOnlyTransNotSupported,
@@ -282,7 +245,6 @@ impl fmt::Display for DriverError {
             ),
             DriverError::InvalidPoolConstraints => write!(f, "Invalid pool constraints"),
             DriverError::SetupError => write!(f, "Could not setup connection"),
-            #[cfg(feature = "native-tls")]
             DriverError::TlsNotSupported => write!(
                 f,
                 "Client requires secure connection but server \
diff --git a/src/error/tls/mod.rs b/src/error/tls/mod.rs
new file mode 100644
index 00000000..b2db1695
--- /dev/null
+++ b/src/error/tls/mod.rs
@@ -0,0 +1,10 @@
+#![cfg(any(feature = "native-tls", feature = "rustls"))]
+
+pub mod native_tls_error;
+pub mod rustls_error;
+
+#[cfg(feature = "native-tls")]
+pub use native_tls_error::TlsError;
+
+#[cfg(feature = "rustls")]
+pub use rustls_error::TlsError;
diff --git a/src/error/tls/native_tls_error.rs b/src/error/tls/native_tls_error.rs
new file mode 100644
index 00000000..3a21e187
--- /dev/null
+++ b/src/error/tls/native_tls_error.rs
@@ -0,0 +1,45 @@
+#![cfg(feature = "native-tls")]
+
+use std::fmt::Display;
+
+#[derive(Debug)]
+pub enum TlsError {
+    TlsError(native_tls::Error),
+    TlsHandshakeError(native_tls::HandshakeError<std::net::TcpStream>),
+}
+
+impl From<TlsError> for super::super::Error {
+    fn from(err: TlsError) -> super::super::Error {
+        super::super::Error::TlsError(err)
+    }
+}
+
+impl From<native_tls::Error> for super::super::Error {
+    fn from(err: native_tls::Error) -> super::super::Error {
+        super::super::Error::TlsError(TlsError::TlsError(err))
+    }
+}
+
+impl From<native_tls::HandshakeError<std::net::TcpStream>> for super::super::Error {
+    fn from(err: native_tls::HandshakeError<std::net::TcpStream>) -> super::super::Error {
+        super::super::Error::TlsError(TlsError::TlsHandshakeError(err))
+    }
+}
+
+impl std::error::Error for TlsError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            TlsError::TlsError(e) => Some(e),
+            TlsError::TlsHandshakeError(e) => Some(e),
+        }
+    }
+}
+
+impl Display for TlsError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            TlsError::TlsError(e) => e.fmt(f),
+            TlsError::TlsHandshakeError(e) => e.fmt(f),
+        }
+    }
+}
diff --git a/src/error/tls/rustls_error.rs b/src/error/tls/rustls_error.rs
new file mode 100644
index 00000000..37f60d74
--- /dev/null
+++ b/src/error/tls/rustls_error.rs
@@ -0,0 +1,72 @@
+#![cfg(feature = "rustls")]
+
+use std::fmt::Display;
+
+#[derive(Debug)]
+pub enum TlsError {
+    Tls(rustls::Error),
+    Pki(webpki::Error),
+    InvalidDnsName(webpki::InvalidDnsNameError),
+}
+
+impl From<TlsError> for crate::Error {
+    fn from(e: TlsError) -> Self {
+        crate::Error::TlsError(e)
+    }
+}
+
+impl From<rustls::Error> for TlsError {
+    fn from(e: rustls::Error) -> Self {
+        TlsError::Tls(e)
+    }
+}
+
+impl From<webpki::InvalidDnsNameError> for TlsError {
+    fn from(e: webpki::InvalidDnsNameError) -> Self {
+        TlsError::InvalidDnsName(e)
+    }
+}
+
+impl From<webpki::Error> for TlsError {
+    fn from(e: webpki::Error) -> Self {
+        TlsError::Pki(e)
+    }
+}
+
+impl From<rustls::Error> for crate::Error {
+    fn from(e: rustls::Error) -> Self {
+        crate::Error::TlsError(e.into())
+    }
+}
+
+impl From<webpki::Error> for crate::Error {
+    fn from(e: webpki::Error) -> Self {
+        crate::Error::TlsError(e.into())
+    }
+}
+
+impl From<webpki::InvalidDnsNameError> for crate::Error {
+    fn from(e: webpki::InvalidDnsNameError) -> Self {
+        crate::Error::TlsError(e.into())
+    }
+}
+
+impl std::error::Error for TlsError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            TlsError::Tls(e) => Some(e),
+            TlsError::Pki(e) => Some(e),
+            TlsError::InvalidDnsName(e) => Some(e),
+        }
+    }
+}
+
+impl Display for TlsError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            TlsError::Tls(e) => e.fmt(f),
+            TlsError::Pki(e) => e.fmt(f),
+            TlsError::InvalidDnsName(e) => e.fmt(f),
+        }
+    }
+}
diff --git a/src/io/mod.rs b/src/io/mod.rs
index 6a69c9dc..4c29881a 100644
--- a/src/io/mod.rs
+++ b/src/io/mod.rs
@@ -10,37 +10,23 @@ use bufstream::BufStream;
 use io_enum::*;
 #[cfg(windows)]
 use named_pipe as np;
-#[cfg(feature = "native-tls")]
-use native_tls::{Certificate, Identity, TlsConnector, TlsStream};
 
 #[cfg(unix)]
 use std::os::unix;
 use std::{
-    fmt,
-    io,
+    fmt, io,
     net::{self, SocketAddr},
     time::Duration,
 };
 
-#[cfg(feature = "native-tls")]
-use std::{
-    fs::File,
-    io::Read as _
+use crate::error::{
+    DriverError::{ConnectTimeout, CouldNotConnect},
+    Error::DriverError,
+    Result as MyResult,
 };
 
-
-use crate::{
-    error::{
-        DriverError::{ConnectTimeout, CouldNotConnect},
-        Error::DriverError,
-        Result as MyResult,
-    },
-};
-
-#[cfg(feature = "native-tls")]
-use crate::SslOpts;
-
 mod tcp;
+mod tls;
 
 #[derive(Debug, Read, Write)]
 pub enum Stream {
@@ -147,68 +133,21 @@ impl Stream {
         }
     }
 
-    #[cfg(feature = "native-tls")]
-    pub fn make_secure(self, host: url::Host, ssl_opts: SslOpts) -> MyResult<Stream> {
-        if self.is_socket() {
-            // won't secure socket connection
-            return Ok(self);
-        }
-
-        let domain = match host {
-            url::Host::Domain(domain) => domain,
-            url::Host::Ipv4(ip) => ip.to_string(),
-            url::Host::Ipv6(ip) => ip.to_string(),
-        };
-
-
-        let mut builder = TlsConnector::builder();
-        if let Some(root_cert_path) = ssl_opts.root_cert_path() {
-            let mut root_cert_data = vec![];
-            let mut root_cert_file = File::open(root_cert_path)?;
-            root_cert_file.read_to_end(&mut root_cert_data)?;
-
-            let root_certs = Certificate::from_der(&*root_cert_data)
-                .map(|x| vec![x])
-                .or_else(|_| {
-                    pem::parse_many(&*root_cert_data)
-                        .iter()
-                        .map(pem::encode)
-                        .map(|s| Certificate::from_pem(s.as_bytes()))
-                        .collect()
-                })?;
-
-            for root_cert in root_certs {
-                builder.add_root_certificate(root_cert);
-            }
-        }
-        if let Some(pkcs12_path) = ssl_opts.pkcs12_path() {
-            let der = std::fs::read(pkcs12_path)?;
-            let identity = Identity::from_pkcs12(&*der, ssl_opts.password().unwrap_or(""))?;
-            builder.identity(identity);
-        }
-        builder.danger_accept_invalid_hostnames(ssl_opts.skip_domain_validation());
-        builder.danger_accept_invalid_certs(ssl_opts.accept_invalid_certs());
-        let tls_connector = builder.build()?;
-        match self {
-            Stream::TcpStream(tcp_stream) => match tcp_stream {
-                TcpStream::Insecure(insecure_stream) => {
-                    let inner = insecure_stream.into_inner().map_err(io::Error::from)?;
-                    let secure_stream = tls_connector.connect(&domain, inner)?;
-                    Ok(Stream::TcpStream(TcpStream::Secure(BufStream::new(
-                        secure_stream,
-                    ))))
-                }
-                TcpStream::Secure(_) => Ok(Stream::TcpStream(tcp_stream)),
-            },
-            _ => unreachable!(),
-        }
+    #[cfg(all(not(feature = "native-tls"), not(feature = "rustls")))]
+    pub fn make_secure(self, _host: url::Host, _ssl_opts: crate::SslOpts) -> MyResult<Stream> {
+        panic!(
+            "Client had asked for TLS connection but TLS support is disabled. \
+            Please enable one of the following features: [\"native-tls\", \"rustls\"]"
+        )
     }
 }
 
 #[derive(Read, Write)]
 pub enum TcpStream {
     #[cfg(feature = "native-tls")]
-    Secure(BufStream<TlsStream<net::TcpStream>>),
+    Secure(BufStream<native_tls::TlsStream<net::TcpStream>>),
+    #[cfg(feature = "rustls")]
+    Secure(BufStream<rustls::StreamOwned<rustls::ClientConnection, net::TcpStream>>),
     Insecure(BufStream<net::TcpStream>),
 }
 
@@ -217,6 +156,8 @@ impl fmt::Debug for TcpStream {
         match *self {
             #[cfg(feature = "native-tls")]
             TcpStream::Secure(ref s) => write!(f, "Secure stream {:?}", s),
+            #[cfg(feature = "rustls")]
+            TcpStream::Secure(ref s) => write!(f, "Secure stream {:?}", s),
             TcpStream::Insecure(ref s) => write!(f, "Insecure stream {:?}", s),
         }
     }
diff --git a/src/io/tls/mod.rs b/src/io/tls/mod.rs
new file mode 100644
index 00000000..92f5e7c2
--- /dev/null
+++ b/src/io/tls/mod.rs
@@ -0,0 +1,4 @@
+#![cfg(any(feature = "native-tls", feature = "rustls"))]
+
+mod native_tls_io;
+mod rustls_io;
diff --git a/src/io/tls/native_tls_io.rs b/src/io/tls/native_tls_io.rs
new file mode 100644
index 00000000..30af31b3
--- /dev/null
+++ b/src/io/tls/native_tls_io.rs
@@ -0,0 +1,70 @@
+#![cfg(feature = "native-tls")]
+
+use std::{
+    fs::File,
+    io::{self, Read},
+};
+
+use bufstream::BufStream;
+use native_tls::{Certificate, TlsConnector};
+
+use crate::{
+    io::{Stream, TcpStream},
+    Result, SslOpts,
+};
+
+impl Stream {
+    pub fn make_secure(self, host: url::Host, ssl_opts: SslOpts) -> Result<Stream> {
+        if self.is_socket() {
+            // won't secure socket connection
+            return Ok(self);
+        }
+
+        let domain = match host {
+            url::Host::Domain(domain) => domain,
+            url::Host::Ipv4(ip) => ip.to_string(),
+            url::Host::Ipv6(ip) => ip.to_string(),
+        };
+
+        let mut builder = TlsConnector::builder();
+        if let Some(root_cert_path) = ssl_opts.root_cert_path() {
+            let mut root_cert_data = vec![];
+            let mut root_cert_file = File::open(root_cert_path)?;
+            root_cert_file.read_to_end(&mut root_cert_data)?;
+
+            let root_certs = Certificate::from_der(&*root_cert_data)
+                .map(|x| vec![x])
+                .or_else(|_| {
+                    pem::parse_many(&*root_cert_data)
+                        .iter()
+                        .map(pem::encode)
+                        .map(|s| Certificate::from_pem(s.as_bytes()))
+                        .collect()
+                })?;
+
+            for root_cert in root_certs {
+                builder.add_root_certificate(root_cert);
+            }
+        }
+        if let Some(client_identity) = ssl_opts.client_identity() {
+            let identity = client_identity.load()?;
+            builder.identity(identity);
+        }
+        builder.danger_accept_invalid_hostnames(ssl_opts.skip_domain_validation());
+        builder.danger_accept_invalid_certs(ssl_opts.accept_invalid_certs());
+        let tls_connector = builder.build()?;
+        match self {
+            Stream::TcpStream(tcp_stream) => match tcp_stream {
+                TcpStream::Insecure(insecure_stream) => {
+                    let inner = insecure_stream.into_inner().map_err(io::Error::from)?;
+                    let secure_stream = tls_connector.connect(&domain, inner)?;
+                    Ok(Stream::TcpStream(TcpStream::Secure(BufStream::new(
+                        secure_stream,
+                    ))))
+                }
+                TcpStream::Secure(_) => Ok(Stream::TcpStream(tcp_stream)),
+            },
+            _ => unreachable!(),
+        }
+    }
+}
diff --git a/src/io/tls/rustls_io.rs b/src/io/tls/rustls_io.rs
new file mode 100644
index 00000000..9bc1099b
--- /dev/null
+++ b/src/io/tls/rustls_io.rs
@@ -0,0 +1,160 @@
+#![cfg(feature = "rustls-tls")]
+
+use std::{
+    convert::TryInto,
+    fs::File,
+    io::{self, Read},
+    sync::Arc,
+};
+
+use bufstream::BufStream;
+use rustls::{
+    client::{ServerCertVerifier, WebPkiVerifier},
+    Certificate, ClientConfig, OwnedTrustAnchor, RootCertStore,
+};
+use rustls_pemfile::certs;
+
+use crate::{
+    io::{Stream, TcpStream},
+    Result, SslOpts,
+};
+
+impl Stream {
+    pub fn make_secure(self, host: url::Host, ssl_opts: SslOpts) -> Result<Stream> {
+        if self.is_socket() {
+            // won't secure socket connection
+            return Ok(self);
+        }
+
+        let domain = match host {
+            url::Host::Domain(domain) => domain,
+            url::Host::Ipv4(ip) => ip.to_string(),
+            url::Host::Ipv6(ip) => ip.to_string(),
+        };
+
+        let mut root_store = RootCertStore::empty();
+        root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
+            OwnedTrustAnchor::from_subject_spki_name_constraints(
+                ta.subject,
+                ta.spki,
+                ta.name_constraints,
+            )
+        }));
+
+        if let Some(root_cert_path) = ssl_opts.root_cert_path() {
+            let mut root_cert_data = vec![];
+            let mut root_cert_file = File::open(root_cert_path)?;
+            root_cert_file.read_to_end(&mut root_cert_data)?;
+
+            let mut root_certs = Vec::new();
+            for cert in certs(&mut &*root_cert_data)? {
+                root_certs.push(Certificate(cert));
+            }
+
+            if root_certs.is_empty() && !root_cert_data.is_empty() {
+                root_certs.push(Certificate(root_cert_data));
+            }
+
+            for cert in &root_certs {
+                root_store.add(cert)?;
+            }
+        }
+
+        let config_builder = ClientConfig::builder()
+            .with_safe_defaults()
+            .with_root_certificates(root_store.clone());
+
+        let mut config = if let Some(identity) = ssl_opts.client_identity() {
+            let (cert_chain, priv_key) = identity.load()?;
+            config_builder.with_single_cert(cert_chain, priv_key)?
+        } else {
+            config_builder.with_no_client_auth()
+        };
+
+        let server_name = domain
+            .as_str()
+            .try_into()
+            .map_err(|_| webpki::InvalidDnsNameError)?;
+        let mut dangerous = config.dangerous();
+        let web_pki_verifier = WebPkiVerifier::new(root_store, None);
+        let dangerous_verifier = DangerousVerifier::new(
+            ssl_opts.accept_invalid_certs(),
+            ssl_opts.skip_domain_validation(),
+            web_pki_verifier,
+        );
+        dangerous.set_certificate_verifier(Arc::new(dangerous_verifier));
+
+        match self {
+            Stream::TcpStream(tcp_stream) => match tcp_stream {
+                TcpStream::Insecure(insecure_stream) => {
+                    let inner = insecure_stream
+                        .into_inner()
+                        .map_err(io::Error::from)
+                        .unwrap();
+                    let conn =
+                        rustls::ClientConnection::new(Arc::new(config), server_name).unwrap();
+                    let secure_stream = rustls::StreamOwned::new(conn, inner);
+                    Ok(Stream::TcpStream(TcpStream::Secure(BufStream::new(
+                        secure_stream,
+                    ))))
+                }
+                TcpStream::Secure(_) => Ok(Stream::TcpStream(tcp_stream)),
+            },
+            _ => unreachable!(),
+        }
+    }
+}
+
+struct DangerousVerifier {
+    accept_invalid_certs: bool,
+    skip_domain_validation: bool,
+    verifier: WebPkiVerifier,
+}
+
+impl DangerousVerifier {
+    fn new(
+        accept_invalid_certs: bool,
+        skip_domain_validation: bool,
+        verifier: WebPkiVerifier,
+    ) -> Self {
+        Self {
+            accept_invalid_certs,
+            skip_domain_validation,
+            verifier,
+        }
+    }
+}
+
+impl ServerCertVerifier for DangerousVerifier {
+    fn verify_server_cert(
+        &self,
+        end_entity: &Certificate,
+        intermediates: &[Certificate],
+        server_name: &rustls::ServerName,
+        scts: &mut dyn Iterator<Item = &[u8]>,
+        ocsp_response: &[u8],
+        now: std::time::SystemTime,
+    ) -> std::result::Result<rustls::client::ServerCertVerified, rustls::Error> {
+        if self.accept_invalid_certs {
+            Ok(rustls::client::ServerCertVerified::assertion())
+        } else {
+            match self.verifier.verify_server_cert(
+                end_entity,
+                intermediates,
+                server_name,
+                scts,
+                ocsp_response,
+                now,
+            ) {
+                Ok(assertion) => Ok(assertion),
+                Err(ref e)
+                    if e.to_string().contains("CertNotValidForName")
+                        && self.skip_domain_validation =>
+                {
+                    Ok(rustls::client::ServerCertVerified::assertion())
+                }
+                Err(e) => Err(e),
+            }
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index e460c0a0..6cb1d6cf 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -795,6 +795,10 @@ pub mod binlog {
     pub use crate::myc::binlog::{events, jsonb, jsondiff, row, value};
 }
 
+#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
+#[doc(inline)]
+pub use crate::conn::opts::ClientIdentity;
+
 #[doc(inline)]
 pub use crate::myc::packets::{session_state_change, SessionStateInfo};
 
@@ -874,7 +878,7 @@ macro_rules! def_database_url {
             }
             url
         } else {
-            "mysql://root:password@127.0.0.1:3307/mysql".into()
+            "mysql://root:password@localhost:3307/mysql".into()
         }
     };
 }


Test patch:
diff --git a/tests/ca-cert.pem b/tests/ca-cert.pem
deleted file mode 100644
index ad395ffa..00000000
--- a/tests/ca-cert.pem
+++ /dev/null
@@ -1,20 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDSDCCAjACCQCkUHWM/JQbaTANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJS
-VTEPMA0GA1UECAwGTW9zY293MQ8wDQYDVQQHDAZNb3Njb3cxEDAOBgNVBAoMB0ZT
-SSBSU0wxIjAgBgNVBAMMGVJ1c3QgbXlzcWwgZHJpdmVyIHRlc3QgQ0EwIBcNMjAw
-NjI5MDk0MzQ2WhgPMjE5OTEyMDQwOTQzNDZaMGUxCzAJBgNVBAYTAlJVMQ8wDQYD
-VQQIDAZNb3Njb3cxDzANBgNVBAcMBk1vc2NvdzEQMA4GA1UECgwHRlNJIFJTTDEi
-MCAGA1UEAwwZUnVzdCBteXNxbCBkcml2ZXIgdGVzdCBDQTCCASIwDQYJKoZIhvcN
-AQEBBQADggEPADCCAQoCggEBAMIKxR7MR16bxqAQoFJBh5UI2K147uAa43k9aLqd
-Vfw2fXG43CxXH6THDB2XvXwWfUax5i3QlIU13FC1D5TMEeVERhEWYwgfIxtibBMP
-IKFIvfzaFp+lsthmSNRuUX07MedeLNmHYvxZo5FFB4NA7C6v4mN9EGe4yyexK/gy
-71R2dvdqROJHzfUDu4XJTsraiZ85xPAS4cYpTqSESxb2yo2Z7anvvDpx30BMaPRk
-hYEcZ2AatyMQPO0ZlzYWuAzxG5A1EU/rRUdyaF/HnxZsCG4h/ZHl4VTAWKhoY2ca
-k1w9AeGD8joN8j5yCup+mNtGAKOdaE4E+ngLwcuiFuLp0AECAwEAATANBgkqhkiG
-9w0BAQsFAAOCAQEAQKSkaDGyMcqCga37/HA9YMkXuI6TnfRZnr2/IY68VR4GbO84
-IUap5oyIl9M8c/Qbe5DBlfPtK+qrx7LNWimTi9Gplh+yb9KfWHNqh1bUquf8Ri3D
-FA812mkuP7eOT1RtP8BqGy4yEBBiaIK/j3FIRk3CCOV1fohUOZ/y8VqftBCnkonH
-veyPwsOEVqPWM1NKMCorXjBdC9Eg5g1MIydnmPZxNm6eC6ApPZtD4vKaWhjAoL0w
-ovKSsMESn93uISW5GZmnYkj7TwHLKDK0mj/68LhRyC+qFjQpphdm+shzyHTjnkgJ
-+eCxbV7UxLkjC5z6ljpxpLlnoYFDRAepCjKvXg==
------END CERTIFICATE-----
diff --git a/tests/ca.crt b/tests/ca.crt
new file mode 100644
index 00000000..4bbbaf94
--- /dev/null
+++ b/tests/ca.crt
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEBDCCAuygAwIBAgIUBI+G157VcfoRku8+hbCfVFkDNocwDQYJKoZIhvcNAQEL
+BQAwgasxCzAJBgNVBAYTAlJVMQ8wDQYDVQQIDAZNb3Njb3MxDzANBgNVBAcMBk1v
+c2NvdzEQMA4GA1UECgwHRlNJIFJTTDEiMCAGA1UECwwZcnVzdC1teXNxbC1zaW1w
+bGUgdGVzdCBjYTEaMBgGA1UEAwwRcnVzdF9teXNxbF9zaW1wbGUxKDAmBgkqhkiG
+9w0BCQEWGXJvb3RfZW1haWxAcm9vdC5sb2NhbGhvc3QwHhcNMjExMDE5MDk1ODI1
+WhcNMjIxMDE5MDk1ODI1WjCBqzELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1vc2Nv
+czEPMA0GA1UEBwwGTW9zY293MRAwDgYDVQQKDAdGU0kgUlNMMSIwIAYDVQQLDBly
+dXN0LW15c3FsLXNpbXBsZSB0ZXN0IGNhMRowGAYDVQQDDBFydXN0X215c3FsX3Np
+bXBsZTEoMCYGCSqGSIb3DQEJARYZcm9vdF9lbWFpbEByb290LmxvY2FsaG9zdDCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMIKxR7MR16bxqAQoFJBh5UI
+2K147uAa43k9aLqdVfw2fXG43CxXH6THDB2XvXwWfUax5i3QlIU13FC1D5TMEeVE
+RhEWYwgfIxtibBMPIKFIvfzaFp+lsthmSNRuUX07MedeLNmHYvxZo5FFB4NA7C6v
+4mN9EGe4yyexK/gy71R2dvdqROJHzfUDu4XJTsraiZ85xPAS4cYpTqSESxb2yo2Z
+7anvvDpx30BMaPRkhYEcZ2AatyMQPO0ZlzYWuAzxG5A1EU/rRUdyaF/HnxZsCG4h
+/ZHl4VTAWKhoY2cak1w9AeGD8joN8j5yCup+mNtGAKOdaE4E+ngLwcuiFuLp0AEC
+AwEAAaMeMBwwGgYDVR0RBBMwEYcEfwAAAYIJbG9jYWxob3N0MA0GCSqGSIb3DQEB
+CwUAA4IBAQAjTbdm0y73dnIXI/2MprHp+NvRzsDJ2pVMLiw2L6cfSTOEyC13aBDm
+w4ssKG2P9H6Ezm/lWArQ3mFO2qzoan2OZID8zek7455ilUzP9VTNfWds5rh6qeN/
+Yq4BsPC8rLhTHoWW2jBDktCjlPLtLxBOBapE3JA4POv3SuOYP95wgxnnlZBKMAx0
+QyAEPYFlv2bfYxBka/6clEZpoFgfWWgFRNP67/trGRvdf+yTRkw/HSwE3vGrnv+k
+nESRVOxqLBaAdDtVCvqqEoE54fCXPAmyOzH/Vh/vFKuBX16WnwPRfqh4C0E5J07c
+vM6jjAQYMlejYh38X8R4yrFVbI4Plbqu
+-----END CERTIFICATE-----
diff --git a/tests/client-cert.pem b/tests/client-cert.pem
deleted file mode 100644
index 8c6da0fa..00000000
--- a/tests/client-cert.pem
+++ /dev/null
@@ -1,20 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDTDCCAjQCCQDTbgJSsvM7zTANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJS
-VTEPMA0GA1UECAwGTW9zY293MQ8wDQYDVQQHDAZNb3Njb3cxEDAOBgNVBAoMB0ZT
-SSBSU0wxIjAgBgNVBAMMGVJ1c3QgbXlzcWwgZHJpdmVyIHRlc3QgQ0EwIBcNMjAw
-NjI5MDk1MjIwWhgPMjE5OTEyMDQwOTUyMjBaMGkxCzAJBgNVBAYTAlJVMQ8wDQYD
-VQQIDAZNb3Njb3cxDzANBgNVBAcMBk1vc2NvdzEQMA4GA1UECgwHRlNJIFJTTDEm
-MCQGA1UEAwwdUnVzdCBteXNxbCBkcml2ZXIgdGVzdCBDbGllbnQwggEiMA0GCSqG
-SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgcITynCAG+50LjeUPc/5BuqUfrouuNvUw
-nyuRL7gkgVJooi9XfYch5caGTehpDoIaj4TydKJOg/juz0a82TzoAYUum8xtBVKW
-I/GUQauaJVw/LKyYL0WYdoRbrxdApIzG3B/j80MLWCwjCdPiN+bMXxUtpNAIsP0o
-+v6j/BpGXdwcvc/WoyeC61iwaayWljlf1Fb0LQDCQ//51Jea5wVbsFq4r5/06Vc4
-qWhkrDXcFjRqNPevPoMn4sr+pgC8AzQWOXnCN9VEaqg5M3POLGZsoup0B23y/Nea
-oBCYzL6DpbgLZmv6ChDmrmY+u89JxLi67CItvukJSo+Br32vKJCLAgMBAAEwDQYJ
-KoZIhvcNAQELBQADggEBAFP5LUeIn8SLfDxUs3QR5FDyFJJ2vaYrKZ+psp/3OtrP
-uPLIaSoKqcEXRzBI5giKaVslYQQSYLkfp4K8u8A7KoNJQaPDCrNSnAqCppjnkJQy
-HhC6ZxeCM+ctZWG5SDoIMH4088lOO/x4rIFDJwVWkksnRDKzI7VzYfolxbg4Y4ty
-U+q1uh7v2P4mTQnpxuCLqTHpymrN5ygky1XsbyKyNHO7pDrEbAAghNyA2tWm21JQ
-/+CeCWI34nN5Db3aSLNCd3a5Qj/ZblzU4efMmoNsupk7AS79IaLa+LmtQhUufUe2
-UJpbnECyCFdQjDbgOccLe3GhaSYJXKnSSlC7YLlyZjk=
------END CERTIFICATE-----
diff --git a/tests/client-identity.p12 b/tests/client-identity.p12
new file mode 100644
index 00000000..dcc65316
Binary files /dev/null and b/tests/client-identity.p12 differ
diff --git a/tests/client.crt b/tests/client.crt
new file mode 100644
index 00000000..7a8536bf
--- /dev/null
+++ b/tests/client.crt
@@ -0,0 +1,75 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 3 (0x3)
+        Signature Algorithm: sha256WithRSAEncryption
+        Issuer: C=RU, ST=Moscos, L=Moscow, O=FSI RSL, OU=rust-mysql-simple test ca, CN=rust_mysql_simple/emailAddress=root_email@root.localhost
+        Validity
+            Not Before: Oct 19 10:01:04 2021 GMT
+            Not After : Sep 25 10:01:04 2121 GMT
+        Subject: C=RU, ST=Moscow region, O=FSI RSL, OU=rust-mysql-simple test client cert, CN=localhost
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                RSA Public-Key: (2048 bit)
+                Modulus:
+                    00:a0:70:84:f2:9c:20:06:fb:9d:0b:8d:e5:0f:73:
+                    fe:41:ba:a5:1f:ae:8b:ae:36:f5:30:9f:2b:91:2f:
+                    b8:24:81:52:68:a2:2f:57:7d:87:21:e5:c6:86:4d:
+                    e8:69:0e:82:1a:8f:84:f2:74:a2:4e:83:f8:ee:cf:
+                    46:bc:d9:3c:e8:01:85:2e:9b:cc:6d:05:52:96:23:
+                    f1:94:41:ab:9a:25:5c:3f:2c:ac:98:2f:45:98:76:
+                    84:5b:af:17:40:a4:8c:c6:dc:1f:e3:f3:43:0b:58:
+                    2c:23:09:d3:e2:37:e6:cc:5f:15:2d:a4:d0:08:b0:
+                    fd:28:fa:fe:a3:fc:1a:46:5d:dc:1c:bd:cf:d6:a3:
+                    27:82:eb:58:b0:69:ac:96:96:39:5f:d4:56:f4:2d:
+                    00:c2:43:ff:f9:d4:97:9a:e7:05:5b:b0:5a:b8:af:
+                    9f:f4:e9:57:38:a9:68:64:ac:35:dc:16:34:6a:34:
+                    f7:af:3e:83:27:e2:ca:fe:a6:00:bc:03:34:16:39:
+                    79:c2:37:d5:44:6a:a8:39:33:73:ce:2c:66:6c:a2:
+                    ea:74:07:6d:f2:fc:d7:9a:a0:10:98:cc:be:83:a5:
+                    b8:0b:66:6b:fa:0a:10:e6:ae:66:3e:bb:cf:49:c4:
+                    b8:ba:ec:22:2d:be:e9:09:4a:8f:81:af:7d:af:28:
+                    90:8b
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                IP Address:127.0.0.1, DNS:localhost
+    Signature Algorithm: sha256WithRSAEncryption
+         4b:4b:2f:02:29:c5:32:32:6a:ce:3a:c6:fd:f4:b7:77:f9:59:
+         94:4d:35:21:01:dc:09:4d:ad:3d:ff:13:f3:23:4e:82:e6:07:
+         54:25:b3:f6:f9:83:80:d5:55:12:d1:7f:d6:21:f5:22:24:05:
+         3e:03:43:97:2b:81:bf:ce:01:54:5e:38:fa:fe:66:d2:4f:03:
+         bf:bd:e1:2c:50:5f:39:dc:5c:4f:bc:7b:5b:d1:2a:d1:dc:aa:
+         3b:e0:0c:9d:1b:6f:d4:5c:b8:ae:95:09:a1:6e:e2:f8:44:0f:
+         65:f1:a2:e0:be:fb:4e:bf:c7:3a:e5:70:9b:23:dd:54:99:01:
+         05:f0:15:a3:67:e2:cf:a5:1b:7a:44:76:f2:64:70:31:f0:ef:
+         34:40:6e:86:ef:be:81:a6:f7:34:46:ca:b9:76:8d:0c:dc:2c:
+         b0:50:81:7f:ba:cb:42:27:a3:15:c7:89:91:58:ea:39:74:32:
+         2c:7b:16:22:8e:14:8c:05:61:58:c7:9d:f6:9f:16:5a:df:1f:
+         fd:f9:1e:2b:68:73:1c:60:7b:c8:df:d3:e3:6a:d0:1f:29:74:
+         c4:98:a4:1d:5a:0d:4d:2f:0f:03:dd:5e:a8:03:da:4a:6e:12:
+         6b:65:e9:13:e0:c4:10:d1:85:c6:48:8e:eb:2e:52:c4:6f:ab:
+         c4:ce:b2:65
+-----BEGIN CERTIFICATE-----
+MIIDvzCCAqegAwIBAgIBAzANBgkqhkiG9w0BAQsFADCBqzELMAkGA1UEBhMCUlUx
+DzANBgNVBAgMBk1vc2NvczEPMA0GA1UEBwwGTW9zY293MRAwDgYDVQQKDAdGU0kg
+UlNMMSIwIAYDVQQLDBlydXN0LW15c3FsLXNpbXBsZSB0ZXN0IGNhMRowGAYDVQQD
+DBFydXN0X215c3FsX3NpbXBsZTEoMCYGCSqGSIb3DQEJARYZcm9vdF9lbWFpbEBy
+b290LmxvY2FsaG9zdDAgFw0yMTEwMTkxMDAxMDRaGA8yMTIxMDkyNTEwMDEwNFow
+eDELMAkGA1UEBhMCUlUxFjAUBgNVBAgMDU1vc2NvdyByZWdpb24xEDAOBgNVBAoM
+B0ZTSSBSU0wxKzApBgNVBAsMInJ1c3QtbXlzcWwtc2ltcGxlIHRlc3QgY2xpZW50
+IGNlcnQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAKBwhPKcIAb7nQuN5Q9z/kG6pR+ui6429TCfK5EvuCSBUmiiL1d9
+hyHlxoZN6GkOghqPhPJ0ok6D+O7PRrzZPOgBhS6bzG0FUpYj8ZRBq5olXD8srJgv
+RZh2hFuvF0CkjMbcH+PzQwtYLCMJ0+I35sxfFS2k0Aiw/Sj6/qP8GkZd3By9z9aj
+J4LrWLBprJaWOV/UVvQtAMJD//nUl5rnBVuwWrivn/TpVzipaGSsNdwWNGo0968+
+gyfiyv6mALwDNBY5ecI31URqqDkzc84sZmyi6nQHbfL815qgEJjMvoOluAtma/oK
+EOauZj67z0nEuLrsIi2+6QlKj4Gvfa8okIsCAwEAAaMeMBwwGgYDVR0RBBMwEYcE
+fwAAAYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQBLSy8CKcUyMmrOOsb9
+9Ld3+VmUTTUhAdwJTa09/xPzI06C5gdUJbP2+YOA1VUS0X/WIfUiJAU+A0OXK4G/
+zgFUXjj6/mbSTwO/veEsUF853FxPvHtb0SrR3Ko74AydG2/UXLiulQmhbuL4RA9l
+8aLgvvtOv8c65XCbI91UmQEF8BWjZ+LPpRt6RHbyZHAx8O80QG6G776Bpvc0Rsq5
+do0M3CywUIF/ustCJ6MVx4mRWOo5dDIsexYijhSMBWFYx532nxZa3x/9+R4raHMc
+YHvI39PjatAfKXTEmKQdWg1NLw8D3V6oA9pKbhJrZekT4MQQ0YXGSI7rLlLEb6vE
+zrJl
+-----END CERTIFICATE-----
diff --git a/tests/server-cert.pem b/tests/server-cert.pem
deleted file mode 100644
index 7638fb41..00000000
--- a/tests/server-cert.pem
+++ /dev/null
@@ -1,20 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDTDCCAjQCCQDTbgJSsvM7zjANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJS
-VTEPMA0GA1UECAwGTW9zY293MQ8wDQYDVQQHDAZNb3Njb3cxEDAOBgNVBAoMB0ZT
-SSBSU0wxIjAgBgNVBAMMGVJ1c3QgbXlzcWwgZHJpdmVyIHRlc3QgQ0EwIBcNMjAw
-NjI5MDk1MzQwWhgPMjE5OTEyMDQwOTUzNDBaMGkxCzAJBgNVBAYTAlJVMQ8wDQYD
-VQQIDAZNb3Njb3cxDzANBgNVBAcMBk1vc2NvdzEQMA4GA1UECgwHRlNJIFJTTDEm
-MCQGA1UEAwwdUnVzdCBteXNxbCBkcml2ZXIgdGVzdCBTZXJ2ZXIwggEiMA0GCSqG
-SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDbuLksnUS8wLLjJsdekXQfYU/YTuQCEdcS
-BL4d1eVFwGUW5no9RdqarS03rRZOhEBwKhPSwJKiy7ARmIN1+UcMT3U1j5zUiq6g
-4668tI3ZkbcUKdgjhmqNd6HTeD3yveM+JpN5fuo4Gk/0TGgZ5VWLvFzYjwuU6npk
-cM2khsoFo49ubWTgcanF+y30lIPPO8Yax/Qr31P0CkxQEFudRRf0hi4uUi2xWJVC
-RkV0j8Vnza2OQkx8o6TOecgLpc4+lNk37vwYPdqSdG7M5DoLZGmf1X+4mvvZ7fN/
-OYTAr+M/4HfW2bLjVmKlG4fUYHzGiFMQp3KVHKTJ3ZoJ5uplKz5TAgMBAAEwDQYJ
-KoZIhvcNAQELBQADggEBAKTnuJXuDMwezrF5ktLj6CSxQJWBkcuTOivYwjL062lD
-fn57Rgpt16uAVbstI+f3BlMSIruaGHpYFetIx47TeJrSfjZvlqGU1D6Mc8Y+D8Iw
-XvuAnQmDxZB8VRJvk1W+UkfYdq+Vkao6eKBcZNkpVIn5OGEUggjPxusvnR1kbfBh
-x2rtU6nzkzFr48RRfadmGmbj85q/dDOe3o7rzZIJEAwARql1G5yWQb34poyGg0EC
-lndzkVJ/Xw+obrYkyzlgjuDQNLcc+EFSwyNcXvzaO+XKNw+/mo4PaAa39DMSnmPZ
-6saUFJ8obDZN6YP2WEat+zurKCl+o8Tbo1EKqPh0pus=
------END CERTIFICATE-----
diff --git a/tests/server.crt b/tests/server.crt
new file mode 100644
index 00000000..4ef800ca
--- /dev/null
+++ b/tests/server.crt
@@ -0,0 +1,75 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 2 (0x2)
+        Signature Algorithm: sha256WithRSAEncryption
+        Issuer: C=RU, ST=Moscos, L=Moscow, O=FSI RSL, OU=rust-mysql-simple test ca, CN=rust_mysql_simple/emailAddress=root_email@root.localhost
+        Validity
+            Not Before: Oct 19 10:00:49 2021 GMT
+            Not After : Sep 25 10:00:49 2121 GMT
+        Subject: C=RU, ST=Moscow region, O=FSI RSL, OU=rust-mysql-simple test server cert, CN=localhost
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                RSA Public-Key: (2048 bit)
+                Modulus:
+                    00:db:b8:b9:2c:9d:44:bc:c0:b2:e3:26:c7:5e:91:
+                    74:1f:61:4f:d8:4e:e4:02:11:d7:12:04:be:1d:d5:
+                    e5:45:c0:65:16:e6:7a:3d:45:da:9a:ad:2d:37:ad:
+                    16:4e:84:40:70:2a:13:d2:c0:92:a2:cb:b0:11:98:
+                    83:75:f9:47:0c:4f:75:35:8f:9c:d4:8a:ae:a0:e3:
+                    ae:bc:b4:8d:d9:91:b7:14:29:d8:23:86:6a:8d:77:
+                    a1:d3:78:3d:f2:bd:e3:3e:26:93:79:7e:ea:38:1a:
+                    4f:f4:4c:68:19:e5:55:8b:bc:5c:d8:8f:0b:94:ea:
+                    7a:64:70:cd:a4:86:ca:05:a3:8f:6e:6d:64:e0:71:
+                    a9:c5:fb:2d:f4:94:83:cf:3b:c6:1a:c7:f4:2b:df:
+                    53:f4:0a:4c:50:10:5b:9d:45:17:f4:86:2e:2e:52:
+                    2d:b1:58:95:42:46:45:74:8f:c5:67:cd:ad:8e:42:
+                    4c:7c:a3:a4:ce:79:c8:0b:a5:ce:3e:94:d9:37:ee:
+                    fc:18:3d:da:92:74:6e:cc:e4:3a:0b:64:69:9f:d5:
+                    7f:b8:9a:fb:d9:ed:f3:7f:39:84:c0:af:e3:3f:e0:
+                    77:d6:d9:b2:e3:56:62:a5:1b:87:d4:60:7c:c6:88:
+                    53:10:a7:72:95:1c:a4:c9:dd:9a:09:e6:ea:65:2b:
+                    3e:53
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                IP Address:127.0.0.1, DNS:localhost
+    Signature Algorithm: sha256WithRSAEncryption
+         49:30:ef:c5:99:00:50:bb:5e:09:35:ad:d1:dd:10:21:a7:43:
+         7d:5c:da:05:80:0b:45:1e:79:db:88:fc:ec:a8:41:c2:15:83:
+         27:98:4f:47:44:58:4f:56:84:53:5b:c8:15:3c:82:14:57:d5:
+         e2:4f:49:b9:1c:4b:45:59:79:6d:a7:95:7d:e0:1c:1a:2e:20:
+         23:e4:ee:ac:46:1c:09:45:3f:96:bc:68:09:c0:e1:99:9a:7e:
+         51:1e:1a:e5:48:4f:dd:1a:32:1b:ec:6c:17:f4:35:e8:c7:0b:
+         bb:1c:2f:48:cf:8c:26:96:46:c4:44:f9:d6:25:44:0b:ee:78:
+         1b:81:40:4f:db:ea:51:53:7c:03:aa:3a:23:d1:73:d2:3c:f5:
+         33:ae:85:83:b7:63:0c:89:f8:9b:02:64:df:73:9f:5b:45:47:
+         05:fb:86:ed:14:35:e3:90:a8:12:32:5a:7a:05:7e:f8:7d:68:
+         2c:a6:ad:d4:5d:59:34:53:e0:a2:33:1b:ee:d9:c5:0f:68:31:
+         4b:88:6d:00:f6:bf:23:18:62:0d:5c:de:ba:e7:31:24:c0:13:
+         26:4c:85:5d:d6:76:0e:e5:1d:0f:50:52:09:75:c1:58:b5:0c:
+         d1:0e:12:bf:9b:f6:32:89:b4:cd:05:54:a0:dc:46:ba:8e:bd:
+         ce:9b:27:6b
+-----BEGIN CERTIFICATE-----
+MIIDvzCCAqegAwIBAgIBAjANBgkqhkiG9w0BAQsFADCBqzELMAkGA1UEBhMCUlUx
+DzANBgNVBAgMBk1vc2NvczEPMA0GA1UEBwwGTW9zY293MRAwDgYDVQQKDAdGU0kg
+UlNMMSIwIAYDVQQLDBlydXN0LW15c3FsLXNpbXBsZSB0ZXN0IGNhMRowGAYDVQQD
+DBFydXN0X215c3FsX3NpbXBsZTEoMCYGCSqGSIb3DQEJARYZcm9vdF9lbWFpbEBy
+b290LmxvY2FsaG9zdDAgFw0yMTEwMTkxMDAwNDlaGA8yMTIxMDkyNTEwMDA0OVow
+eDELMAkGA1UEBhMCUlUxFjAUBgNVBAgMDU1vc2NvdyByZWdpb24xEDAOBgNVBAoM
+B0ZTSSBSU0wxKzApBgNVBAsMInJ1c3QtbXlzcWwtc2ltcGxlIHRlc3Qgc2VydmVy
+IGNlcnQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBANu4uSydRLzAsuMmx16RdB9hT9hO5AIR1xIEvh3V5UXAZRbmej1F
+2pqtLTetFk6EQHAqE9LAkqLLsBGYg3X5RwxPdTWPnNSKrqDjrry0jdmRtxQp2COG
+ao13odN4PfK94z4mk3l+6jgaT/RMaBnlVYu8XNiPC5TqemRwzaSGygWjj25tZOBx
+qcX7LfSUg887xhrH9CvfU/QKTFAQW51FF/SGLi5SLbFYlUJGRXSPxWfNrY5CTHyj
+pM55yAulzj6U2Tfu/Bg92pJ0bszkOgtkaZ/Vf7ia+9nt8385hMCv4z/gd9bZsuNW
+YqUbh9RgfMaIUxCncpUcpMndmgnm6mUrPlMCAwEAAaMeMBwwGgYDVR0RBBMwEYcE
+fwAAAYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQBJMO/FmQBQu14JNa3R
+3RAhp0N9XNoFgAtFHnnbiPzsqEHCFYMnmE9HRFhPVoRTW8gVPIIUV9XiT0m5HEtF
+WXltp5V94BwaLiAj5O6sRhwJRT+WvGgJwOGZmn5RHhrlSE/dGjIb7GwX9DXoxwu7
+HC9Iz4wmlkbERPnWJUQL7ngbgUBP2+pRU3wDqjoj0XPSPPUzroWDt2MMifibAmTf
+c59bRUcF+4btFDXjkKgSMlp6BX74fWgspq3UXVk0U+CiMxvu2cUPaDFLiG0A9r8j
+GGINXN665zEkwBMmTIVd1nYO5R0PUFIJdcFYtQzRDhK/m/YyibTNBVSg3Ea6jr3O
+mydr
+-----END CERTIFICATE-----