use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::Arc;
use rustls::crypto::ring::default_provider;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use rustls::{ClientConfig, RootCertStore};
use webpki_roots::TLS_SERVER_ROOTS;
pub struct TlsClientConfig {
pub(crate) root_certs: Option<Arc<RootCertStore>>,
pub(crate) use_webpki_roots: bool,
pub(crate) client_auth: Option<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
pub(crate) alpn_protocols: Vec<Vec<u8>>,
pub(crate) verify_server: bool,
}
impl Clone for TlsClientConfig {
fn clone(&self) -> Self {
Self {
root_certs: self.root_certs.clone(),
use_webpki_roots: self.use_webpki_roots,
client_auth: self
.client_auth
.as_ref()
.map(|(certs, key)| (certs.clone(), key.clone_key())),
alpn_protocols: self.alpn_protocols.clone(),
verify_server: self.verify_server,
}
}
}
impl Default for TlsClientConfig {
fn default() -> Self {
Self {
root_certs: None,
use_webpki_roots: true,
client_auth: None,
alpn_protocols: Vec::new(),
verify_server: true,
}
}
}
impl TlsClientConfig {
pub fn new() -> Self {
Self::default()
}
pub fn builder() -> TlsClientConfigBuilder {
TlsClientConfigBuilder::default()
}
pub(crate) fn build_client_config(&self) -> Result<ClientConfig, TlsClientConfigError> {
let provider = Arc::new(default_provider());
let mut root_store = RootCertStore::empty();
if self.use_webpki_roots {
root_store.extend(TLS_SERVER_ROOTS.iter().cloned());
}
if let Some(custom_roots) = &self.root_certs {
root_store.extend(custom_roots.roots.iter().cloned());
}
let builder = ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(|e| TlsClientConfigError::InvalidConfig(e.to_string()))?
.with_root_certificates(root_store);
let mut config = if let Some((cert_chain, key)) = &self.client_auth {
builder
.with_client_auth_cert(cert_chain.clone(), key.clone_key())
.map_err(|e| TlsClientConfigError::InvalidCertificate(e.to_string()))?
} else {
builder.with_no_client_auth()
};
if !self.alpn_protocols.is_empty() {
config.alpn_protocols = self.alpn_protocols.clone();
}
if !self.verify_server {
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification));
}
Ok(config)
}
}
#[derive(Default)]
pub struct TlsClientConfigBuilder {
root_certs: Option<Arc<RootCertStore>>,
use_webpki_roots: bool,
client_cert_chain: Option<Vec<CertificateDer<'static>>>,
client_private_key: Option<PrivateKeyDer<'static>>,
alpn_protocols: Vec<Vec<u8>>,
verify_server: bool,
}
impl TlsClientConfigBuilder {
pub fn new() -> Self {
Self {
use_webpki_roots: true,
verify_server: true,
..Default::default()
}
}
pub fn add_root_certificate(
mut self,
path: impl AsRef<Path>,
) -> Result<Self, TlsClientConfigError> {
let root_store = Self::load_root_certs(path)?;
self.root_certs = Some(match self.root_certs.take() {
Some(existing) => {
let mut merged = RootCertStore::empty();
merged.extend(existing.roots.iter().cloned());
merged.extend(root_store.roots.iter().cloned());
Arc::new(merged)
}
None => root_store,
});
Ok(self)
}
pub fn disable_webpki_roots(mut self) -> Self {
self.use_webpki_roots = false;
self
}
pub fn client_auth(
mut self,
cert_path: impl AsRef<Path>,
key_path: impl AsRef<Path>,
) -> Result<Self, TlsClientConfigError> {
let file = File::open(cert_path.as_ref()).map_err(|e| TlsClientConfigError::IoError(e))?;
let mut reader = BufReader::new(file);
let certs = rustls_pemfile::certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| TlsClientConfigError::InvalidCertificate(e.to_string()))?;
if certs.is_empty() {
return Err(TlsClientConfigError::InvalidCertificate(
"No certificates found in file".into(),
));
}
let file = File::open(key_path.as_ref()).map_err(|e| TlsClientConfigError::IoError(e))?;
let mut reader = BufReader::new(file);
let key = rustls_pemfile::private_key(&mut reader)
.map_err(|e| TlsClientConfigError::InvalidKey(e.to_string()))?
.ok_or_else(|| TlsClientConfigError::InvalidKey("No private key found".into()))?;
self.client_cert_chain = Some(certs);
self.client_private_key = Some(key);
Ok(self)
}
pub fn alpn_protocol(mut self, protocol: impl Into<Vec<u8>>) -> Self {
self.alpn_protocols.push(protocol.into());
self
}
pub fn alpn_protocols(mut self, protocols: &[&str]) -> Self {
for proto in protocols {
self.alpn_protocols.push(proto.as_bytes().to_vec());
}
self
}
pub fn danger_disable_verification(mut self) -> Self {
self.verify_server = false;
self
}
pub fn build(self) -> Result<TlsClientConfig, TlsClientConfigError> {
let client_auth = match (self.client_cert_chain, self.client_private_key) {
(Some(chain), Some(key)) => Some((chain, key)),
(None, None) => None,
_ => {
return Err(TlsClientConfigError::MissingField(
"both client certificate and key required for mTLS",
));
}
};
Ok(TlsClientConfig {
root_certs: self.root_certs,
use_webpki_roots: self.use_webpki_roots,
client_auth,
alpn_protocols: self.alpn_protocols,
verify_server: self.verify_server,
})
}
fn load_root_certs(path: impl AsRef<Path>) -> Result<Arc<RootCertStore>, TlsClientConfigError> {
let file = File::open(path.as_ref()).map_err(|e| TlsClientConfigError::IoError(e))?;
let mut reader = BufReader::new(file);
let certs = rustls_pemfile::certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| TlsClientConfigError::InvalidCertificate(e.to_string()))?;
let mut root_store = RootCertStore::empty();
let (added, _ignored) = root_store.add_parsable_certificates(certs);
if added == 0 {
return Err(TlsClientConfigError::InvalidCertificate(
"No valid CA certificates found".into(),
));
}
Ok(Arc::new(root_store))
}
}
#[derive(Debug)]
struct NoCertificateVerification;
impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ED25519,
]
}
}
#[derive(Debug)]
pub enum TlsClientConfigError {
IoError(std::io::Error),
InvalidCertificate(String),
InvalidKey(String),
InvalidConfig(String),
MissingField(&'static str),
}
impl std::fmt::Display for TlsClientConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IoError(e) => write!(f, "I/O error: {}", e),
Self::InvalidCertificate(e) => write!(f, "Invalid certificate: {}", e),
Self::InvalidKey(e) => write!(f, "Invalid private key: {}", e),
Self::InvalidConfig(e) => write!(f, "Invalid TLS configuration: {}", e),
Self::MissingField(field) => write!(f, "Missing required field: {}", field),
}
}
}
impl std::error::Error for TlsClientConfigError {}