use crate::Result;
use rustls::{ClientConfig, RootCertStore};
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::Arc;
use tokio_rustls::TlsConnector;
pub struct MtlsConfig {
pub cert_chain: Vec<CertificateDer<'static>>,
pub private_key: PrivateKeyDer<'static>,
}
impl Clone for MtlsConfig {
fn clone(&self) -> Self {
Self {
cert_chain: self.cert_chain.clone(),
private_key: self.private_key.clone_key(),
}
}
}
impl MtlsConfig {
pub fn from_separate_files<P: AsRef<Path>>(
cert_path: P,
key_path: P,
_key_password: Option<&str>,
) -> Result<Self> {
let cert_file =
File::open(cert_path.as_ref()).map_err(|e| crate::error::TlsError::MtlsError {
message: format!("Failed to open certificate file: {}", e),
})?;
let mut cert_reader = BufReader::new(cert_file);
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
.collect::<std::result::Result<Vec<_>, std::io::Error>>()
.map_err(|e| crate::error::TlsError::MtlsError {
message: format!("Failed to parse certificates: {}", e),
})?;
if certs.is_empty() {
crate::tls_bail!("No certificates found in certificate file");
}
let key_file =
File::open(key_path.as_ref()).map_err(|e| crate::error::TlsError::MtlsError {
message: format!("Failed to open private key file: {}", e),
})?;
let mut key_reader = BufReader::new(key_file);
let mut keys = Vec::new();
for item in rustls_pemfile::read_all(&mut key_reader) {
match item {
Ok(rustls_pemfile::Item::Pkcs8Key(key)) => {
keys.push(PrivateKeyDer::Pkcs8(key));
}
Ok(rustls_pemfile::Item::Pkcs1Key(key)) => {
keys.push(PrivateKeyDer::Pkcs1(key));
}
Ok(rustls_pemfile::Item::Sec1Key(key)) => {
keys.push(PrivateKeyDer::Sec1(key));
}
_ => {}
}
}
if keys.is_empty() {
crate::tls_bail!("No private key found in key file");
}
Ok(Self {
cert_chain: certs,
private_key: keys.into_iter().next().ok_or_else(|| {
anyhow::anyhow!(
"No private key found in key file (should have been caught earlier)"
)
})?,
})
}
pub fn from_pem_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path.as_ref()).map_err(|e| crate::error::TlsError::MtlsError {
message: format!("Failed to open mTLS PEM file: {}", e),
})?;
let mut reader = BufReader::new(file);
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
.collect::<std::result::Result<Vec<_>, std::io::Error>>()
.map_err(|e| crate::error::TlsError::MtlsError {
message: format!("Failed to parse certificates from PEM: {}", e),
})?;
if certs.is_empty() {
crate::tls_bail!("No certificates found in PEM file");
}
let file = File::open(path.as_ref())?;
let mut reader = BufReader::new(file);
let mut keys = Vec::new();
for item in rustls_pemfile::read_all(&mut reader) {
match item {
Ok(rustls_pemfile::Item::Pkcs8Key(key)) => {
keys.push(PrivateKeyDer::Pkcs8(key));
}
Ok(rustls_pemfile::Item::Pkcs1Key(key)) => {
keys.push(PrivateKeyDer::Pkcs1(key));
}
Ok(rustls_pemfile::Item::Sec1Key(key)) => {
keys.push(PrivateKeyDer::Sec1(key));
}
_ => {}
}
}
if keys.is_empty() {
crate::tls_bail!("No private key found in PEM file");
}
if keys.len() > 1 {
eprintln!("Warning: Multiple private keys found in PEM file, using the first one");
}
Ok(Self {
cert_chain: certs,
private_key: keys.into_iter().next().ok_or_else(|| {
anyhow::anyhow!(
"No private key found in key file (should have been caught earlier)"
)
})?,
})
}
pub fn build_tls_connector(&self) -> Result<TlsConnector> {
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config =
ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into())
.with_safe_default_protocol_versions()
.map_err(|e| crate::error::TlsError::MtlsError {
message: format!("Failed to set protocol versions: {}", e),
})?
.with_root_certificates(root_store)
.with_client_auth_cert(self.cert_chain.clone(), self.private_key.clone_key())
.map_err(|e| crate::error::TlsError::MtlsError {
message: format!("Failed to build TLS config with client auth: {}", e),
})?;
Ok(TlsConnector::from(Arc::new(config)))
}
pub fn build_tls_connector_with_roots(
&self,
root_store: RootCertStore,
) -> Result<TlsConnector> {
let config =
ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into())
.with_safe_default_protocol_versions()
.map_err(|e| crate::error::TlsError::MtlsError {
message: format!("Failed to set protocol versions: {}", e),
})?
.with_root_certificates(root_store)
.with_client_auth_cert(self.cert_chain.clone(), self.private_key.clone_key())
.map_err(|e| crate::error::TlsError::MtlsError {
message: format!("Failed to build TLS config with client auth: {}", e),
})?;
Ok(TlsConnector::from(Arc::new(config)))
}
}
pub fn build_standard_tls_connector() -> TlsConnector {
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config =
ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into())
.with_safe_default_protocol_versions()
.expect("Failed to set protocol versions")
.with_root_certificates(root_store)
.with_no_client_auth();
TlsConnector::from(Arc::new(config))
}
pub fn build_tls_connector_with_roots(root_store: RootCertStore) -> TlsConnector {
let config =
ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into())
.with_safe_default_protocol_versions()
.expect("Failed to set protocol versions")
.with_root_certificates(root_store)
.with_no_client_auth();
TlsConnector::from(Arc::new(config))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_mtls_config_parsing() {
let mut temp_file = NamedTempFile::new().expect("test assertion should succeed");
let pem_data = r#"-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJAKHHCgVZU6RqMA0GCSqGSIb3DQEBCwUAMBkxFzAVBgNVBAMMDnRl
c3QtY2VydGlmaWNhdGUwHhcNMjMwMTAxMDAwMDAwWhcNMjQwMTAxMDAwMDAwWjAZ
MRcwFQYDVQQDDA50ZXN0LWNlcnRpZmljYXRlMFwwDQYJKoZIhvcNAQEBBQADSwAw
SAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/9X0ULi
C8owp8JKwHQ5XPY0cZIhQRhZQIcCAwEAATANBgkqhkiG9w0BAQsFAANBAJ3+FJLb
jvSPe2pxLGJBqf0LcPB2s6yKLKCjFPPdWHMQUxu1u6OsUVaLEWakj0TJzO5RW0Kg
jY0qL8fStJpDQTc=
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEA0smE8eEhOpBs+SUb
eJsJXHAYxF+n+/heqF91Ad1xV8dqno1DlaiT/1fRQuILyjCnwkrAdDlc9jRxkiFB
GFlAhwIDAQABAkEAiNeuOSngRKRqzGBuqLEawO0pPNSaFH1qFjJCvzCiPjJLp5P2
2YFy5t3O1UBQA5T3w9pFBwXfJ0rBmA9mM7H0AQIhAP1/hzQMN/vJpfGYp/MVWQXP
9kRjjxQ1PpmvHSRqNK0PAiEA1I7H0zCJFCVq3x8pWfKx3cV/XYXJ7lkOmqvphqDT
OQECIEk9KlW7J7hFNa9TCw6N3Kv0K1yVphQBhOvCF3OzRq1vAiEAuAhKNSMsZ7vQ
z2H2IWYh2fKcFO6s7AYL6G0KOm6KQAECIBNKDX+4tN7R4x/pNMxsqHrG4k5pZGRq
KHvHJKYnrKyB
-----END PRIVATE KEY-----"#;
write!(temp_file, "{}", pem_data).expect("test assertion should succeed");
temp_file.flush().expect("test assertion should succeed");
let result = MtlsConfig::from_pem_file(temp_file.path());
if let Ok(config) = result {
assert!(!config.cert_chain.is_empty());
}
}
#[test]
fn test_build_standard_connector() {
let connector = build_standard_tls_connector();
assert!(std::mem::size_of_val(&connector) > 0);
}
}