rabbitmq-backup-core 0.1.0

Core engine for RabbitMQ backup and restore operations
Documentation
//! TLS configuration for AMQP connections.
//!
//! Ported from kafka-backup's `kafka/tls.rs` — same pattern:
//! custom CA cert loading with webpki-roots fallback, optional mTLS.

use std::fs::File;
use std::io::BufReader;
use std::path::Path;

use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
use tokio_rustls::rustls::{ClientConfig, RootCertStore};
use tracing::debug;

use crate::config::TlsConfig;
use crate::error::{Error, Result};

/// Build a TLS ClientConfig from our TlsConfig.
pub fn build_tls_config(config: &TlsConfig) -> Result<ClientConfig> {
    let root_store = build_root_store(config.ca_cert.as_deref())?;

    let tls_config = match (&config.client_cert, &config.client_key) {
        (Some(cert_path), Some(key_path)) => {
            debug!(
                "Configuring mTLS with cert={}, key={}",
                cert_path.display(),
                key_path.display()
            );
            let certs = load_certificates(cert_path)?;
            let key = load_private_key(key_path)?;

            ClientConfig::builder()
                .with_root_certificates(root_store)
                .with_client_auth_cert(certs, key)
                .map_err(|e| {
                    Error::Authentication(format!(
                        "Failed to configure client authentication: {}",
                        e
                    ))
                })?
        }
        (Some(cert_path), None) => {
            return Err(Error::Config(format!(
                "client_cert ({}) provided without client_key. Both required for mTLS.",
                cert_path.display()
            )));
        }
        (None, Some(key_path)) => {
            return Err(Error::Config(format!(
                "client_key ({}) provided without client_cert. Both required for mTLS.",
                key_path.display()
            )));
        }
        (None, None) => {
            debug!("Configuring TLS without client authentication");
            ClientConfig::builder()
                .with_root_certificates(root_store)
                .with_no_client_auth()
        }
    };

    Ok(tls_config)
}

fn build_root_store(ca_path: Option<&Path>) -> Result<RootCertStore> {
    match ca_path {
        Some(path) => {
            let certs = load_certificates(path)?;
            let mut root_store = RootCertStore::empty();
            for cert in certs {
                root_store.add(cert).map_err(|e| {
                    Error::Authentication(format!("Failed to add CA certificate: {}", e))
                })?;
            }
            if root_store.is_empty() {
                return Err(Error::Authentication(format!(
                    "No valid certificates found in {}",
                    path.display()
                )));
            }
            debug!(
                "Loaded {} CA certificate(s) from {}",
                root_store.len(),
                path.display()
            );
            Ok(root_store)
        }
        None => {
            debug!("Using webpki-roots for TLS verification");
            Ok(RootCertStore {
                roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
            })
        }
    }
}

fn load_certificates(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
    let file = File::open(path)
        .map_err(|e| Error::Authentication(format!("Failed to open {}: {}", path.display(), e)))?;
    let mut reader = BufReader::new(file);
    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
        .collect::<std::result::Result<Vec<_>, _>>()
        .map_err(|e| {
            Error::Authentication(format!(
                "Failed to parse certificates from {}: {}",
                path.display(),
                e
            ))
        })?;
    if certs.is_empty() {
        return Err(Error::Authentication(format!(
            "No certificates found in {}",
            path.display()
        )));
    }
    debug!(
        "Loaded {} certificate(s) from {}",
        certs.len(),
        path.display()
    );
    Ok(certs)
}

fn load_private_key(path: &Path) -> Result<PrivateKeyDer<'static>> {
    let file = File::open(path)
        .map_err(|e| Error::Authentication(format!("Failed to open {}: {}", path.display(), e)))?;
    let mut reader = BufReader::new(file);
    rustls_pemfile::private_key(&mut reader)
        .map_err(|e| {
            Error::Authentication(format!(
                "Failed to parse private key from {}: {}",
                path.display(),
                e
            ))
        })?
        .ok_or_else(|| Error::Authentication(format!("No private key found in {}", path.display())))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_build_root_store_webpki_fallback() {
        let store = build_root_store(None).unwrap();
        assert!(!store.is_empty());
    }

    #[test]
    fn test_build_root_store_missing_file() {
        let result = build_root_store(Some(Path::new("/nonexistent/ca.pem")));
        assert!(result.is_err());
    }

    #[test]
    fn test_mtls_requires_both_cert_and_key() {
        let config = TlsConfig {
            enabled: true,
            ca_cert: None,
            client_cert: Some("/path/to/cert.pem".into()),
            client_key: None,
        };
        let result = build_tls_config(&config);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("client_key"));
    }
}