use std::io::Error;
#[cfg(feature = "tls-rustls")]
use rustls::RootCertStore;
#[cfg(feature = "tls-rustls")]
use rustls::pki_types::pem::PemObject;
#[cfg(feature = "tls-rustls")]
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use crate::connection::TlsConnParams;
use crate::types::redacted_if_set;
use crate::{Client, ConnectionAddr, ConnectionInfo, ErrorKind, RedisError, RedisResult};
#[derive(Clone)]
pub struct ClientTlsConfig {
pub client_cert: Vec<u8>,
pub client_key: Vec<u8>,
}
impl std::fmt::Debug for ClientTlsConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
client_cert: _,
client_key: _,
} = self;
f.debug_struct("ClientTlsConfig")
.field("client_cert", &"<redacted>")
.field("client_key", &"<redacted>")
.finish()
}
}
#[derive(Clone)]
pub struct TlsCertificates {
pub client_tls: Option<ClientTlsConfig>,
pub root_cert: Option<Vec<u8>>,
}
impl std::fmt::Debug for TlsCertificates {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
client_tls,
root_cert,
} = self;
f.debug_struct("TlsCertificates")
.field("client_tls", &redacted_if_set(client_tls))
.field("root_cert", &redacted_if_set(root_cert))
.finish()
}
}
pub(crate) fn inner_build_with_tls(
mut connection_info: ConnectionInfo,
certificates: &TlsCertificates,
) -> RedisResult<Client> {
let tls_params = retrieve_tls_certificates(certificates)?;
connection_info.addr = if let ConnectionAddr::TcpTls {
host,
port,
insecure,
..
} = connection_info.addr
{
ConnectionAddr::TcpTls {
host,
port,
insecure,
tls_params: Some(tls_params),
}
} else {
return Err(RedisError::from((
ErrorKind::InvalidClientConfig,
"Constructing a TLS client requires a URL with the `rediss://` scheme",
)));
};
Ok(Client { connection_info })
}
#[cfg(feature = "tls-rustls")]
pub(crate) fn retrieve_tls_certificates(
certificates: &TlsCertificates,
) -> RedisResult<TlsConnParams> {
let TlsCertificates {
client_tls,
root_cert,
} = certificates;
let client_tls_params = if let Some(ClientTlsConfig {
client_cert,
client_key,
}) = client_tls
{
let client_cert_chain = CertificateDer::pem_slice_iter(client_cert)
.collect::<Result<Vec<_>, _>>()
.map_err(|err| {
Error::other(format!(
"Unable to parse client certificate chain PEM: {err}"
))
})?;
let client_key = PrivateKeyDer::from_pem_slice(client_key).map_err(|err| {
Error::other(format!(
"Unable to extract private key from PEM file: {err}"
))
})?;
Some(ClientTlsParams {
client_cert_chain,
client_key,
})
} else {
None
};
let root_cert_store = if let Some(root_cert) = root_cert {
let mut root_cert_store = RootCertStore::empty();
for result in CertificateDer::pem_slice_iter(root_cert) {
let cert = result.map_err(|err| {
Error::other(format!("Unable to parse root certificate PEM: {err}"))
})?;
if root_cert_store.add(cert).is_err() {
return Err(Error::other("Unable to parse TLS trust anchors").into());
}
}
Some(root_cert_store)
} else {
None
};
Ok(TlsConnParams {
client_tls_params,
root_cert_store,
#[cfg(any(
all(feature = "tls-native-tls", not(feature = "tls-rustls")),
all(feature = "tokio-native-tls-comp", not(feature = "tokio-rustls-comp")),
all(feature = "smol-native-tls-comp", not(feature = "smol-rustls-comp"))
))]
native_tls_params: parse_native_tls_params(certificates)?,
#[cfg(any(feature = "tls-rustls-insecure", feature = "tls-native-tls"))]
danger_accept_invalid_hostnames: false,
})
}
#[cfg(all(feature = "tls-native-tls", not(feature = "tls-rustls")))]
pub(crate) fn retrieve_tls_certificates(
certificates: &TlsCertificates,
) -> RedisResult<TlsConnParams> {
Ok(TlsConnParams {
native_tls_params: parse_native_tls_params(certificates)?,
danger_accept_invalid_hostnames: false,
})
}
#[cfg(any(
all(feature = "tls-native-tls", not(feature = "tls-rustls")),
all(feature = "tokio-native-tls-comp", not(feature = "tokio-rustls-comp")),
all(feature = "smol-native-tls-comp", not(feature = "smol-rustls-comp"))
))]
#[derive(Clone)]
pub(crate) struct NativeTlsParams {
pub(crate) identity: Option<native_tls::Identity>,
pub(crate) root_certs: Vec<native_tls::Certificate>,
}
#[cfg(any(
all(feature = "tls-native-tls", not(feature = "tls-rustls")),
all(feature = "tokio-native-tls-comp", not(feature = "tokio-rustls-comp")),
all(feature = "smol-native-tls-comp", not(feature = "smol-rustls-comp"))
))]
impl std::fmt::Debug for NativeTlsParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NativeTlsParams")
.field("identity", &redacted_if_set(&self.identity))
.field("root_certs", &self.root_certs.len())
.finish()
}
}
#[cfg(any(
all(feature = "tls-native-tls", not(feature = "tls-rustls")),
all(feature = "tokio-native-tls-comp", not(feature = "tokio-rustls-comp")),
all(feature = "smol-native-tls-comp", not(feature = "smol-rustls-comp"))
))]
pub(crate) fn parse_native_tls_params(
certificates: &TlsCertificates,
) -> RedisResult<Option<NativeTlsParams>> {
const PEM_BEGIN: &str = "-----BEGIN CERTIFICATE-----";
const PEM_END: &str = "-----END CERTIFICATE-----";
let TlsCertificates {
client_tls,
root_cert,
} = certificates;
if client_tls.is_none() && root_cert.is_none() {
return Ok(None);
}
let identity = client_tls
.as_ref()
.map(
|ClientTlsConfig {
client_cert,
client_key,
}| {
native_tls::Identity::from_pkcs8(client_cert, client_key).map_err(|err| {
Error::other(format!("Unable to parse client identity PEM: {err}"))
})
},
)
.transpose()?;
let root_certs = match root_cert {
Some(pem) => {
let text = std::str::from_utf8(pem).map_err(|err| {
Error::other(format!("Root certificate PEM is not valid UTF-8: {err}"))
})?;
let certs = text
.split_inclusive(PEM_END)
.filter_map(|block| block.find(PEM_BEGIN).map(|start| &block[start..]))
.map(|block| {
native_tls::Certificate::from_pem(block.as_bytes()).map_err(|err| {
Error::other(format!("Unable to parse root certificate PEM: {err}"))
})
})
.collect::<Result<Vec<_>, _>>()?;
if certs.is_empty() {
return Err(Error::other("Root certificate PEM contains no certificates").into());
}
certs
}
None => Vec::new(),
};
Ok(Some(NativeTlsParams {
identity,
root_certs,
}))
}
#[cfg(feature = "tls-rustls")]
#[derive(Debug)]
pub struct ClientTlsParams {
pub(crate) client_cert_chain: Vec<CertificateDer<'static>>,
pub(crate) client_key: PrivateKeyDer<'static>,
}
#[cfg(feature = "tls-rustls")]
impl Clone for ClientTlsParams {
fn clone(&self) -> Self {
use PrivateKeyDer::*;
Self {
client_cert_chain: self.client_cert_chain.clone(),
client_key: match &self.client_key {
Pkcs1(key) => Pkcs1(key.secret_pkcs1_der().to_vec().into()),
Pkcs8(key) => Pkcs8(key.secret_pkcs8_der().to_vec().into()),
Sec1(key) => Sec1(key.secret_sec1_der().to_vec().into()),
_ => unreachable!(),
},
}
}
}