Skip to main content

reddb_server/wire/
tls.rs

1/// Wire Protocol TLS support
2///
3/// Provides:
4/// - Auto-generated self-signed certificates for dev mode
5/// - TLS acceptor configuration from cert/key files
6/// - TLS-wrapped TCP listener
7use std::io;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use rustls::ServerConfig;
12use tokio_rustls::TlsAcceptor;
13
14/// TLS configuration for the wire protocol.
15#[derive(Debug, Clone)]
16pub struct WireTlsConfig {
17    /// Path to PEM certificate file
18    pub cert_path: PathBuf,
19    /// Path to PEM private key file
20    pub key_path: PathBuf,
21}
22
23/// Generate a self-signed certificate for development.
24/// Returns (cert_pem, key_pem) as strings.
25pub fn generate_self_signed_cert(
26    hostname: &str,
27) -> Result<(String, String), Box<dyn std::error::Error>> {
28    use rcgen::{CertificateParams, KeyPair};
29
30    let mut params = CertificateParams::new(vec![hostname.to_string()])?;
31    params.distinguished_name.push(
32        rcgen::DnType::CommonName,
33        rcgen::DnValue::Utf8String(format!("RedDB Wire {hostname}")),
34    );
35    params.distinguished_name.push(
36        rcgen::DnType::OrganizationName,
37        rcgen::DnValue::Utf8String("RedDB".to_string()),
38    );
39
40    // Add localhost + IP SANs for dev
41    params
42        .subject_alt_names
43        .push(rcgen::SanType::DnsName(hostname.try_into()?));
44    if hostname != "localhost" {
45        params
46            .subject_alt_names
47            .push(rcgen::SanType::DnsName("localhost".try_into()?));
48    }
49    // 127.0.0.1
50    params
51        .subject_alt_names
52        .push(rcgen::SanType::IpAddress(std::net::IpAddr::V4(
53            std::net::Ipv4Addr::LOCALHOST,
54        )));
55
56    let key_pair = KeyPair::generate()?;
57    let cert = params.self_signed(&key_pair)?;
58
59    Ok((cert.pem(), key_pair.serialize_pem()))
60}
61
62/// Generate self-signed cert and write to files in the given directory.
63/// Returns the WireTlsConfig pointing to the written files.
64pub fn auto_generate_cert(dir: &Path) -> Result<WireTlsConfig, Box<dyn std::error::Error>> {
65    let cert_path = dir.join("wire-tls-cert.pem");
66    let key_path = dir.join("wire-tls-key.pem");
67
68    // If files already exist, reuse them
69    if cert_path.exists() && key_path.exists() {
70        tracing::info!(cert = %cert_path.display(), "wire TLS: reusing existing cert");
71        return Ok(WireTlsConfig {
72            cert_path,
73            key_path,
74        });
75    }
76
77    tracing::info!("wire TLS: generating self-signed certificate");
78    let (cert_pem, key_pem) = generate_self_signed_cert("localhost")?;
79
80    std::fs::create_dir_all(dir)?;
81    std::fs::write(&cert_path, &cert_pem)?;
82    std::fs::write(&key_path, &key_pem)?;
83
84    // Restrict key file permissions on Unix
85    #[cfg(unix)]
86    {
87        use std::os::unix::fs::PermissionsExt;
88        std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))?;
89    }
90
91    tracing::info!(
92        cert = %cert_path.display(),
93        key = %key_path.display(),
94        "wire TLS: wrote self-signed cert"
95    );
96
97    Ok(WireTlsConfig {
98        cert_path,
99        key_path,
100    })
101}
102
103/// Build a TLS acceptor from cert and key PEM files.
104pub fn build_tls_acceptor(
105    config: &WireTlsConfig,
106) -> Result<TlsAcceptor, Box<dyn std::error::Error>> {
107    // Ensure the ring crypto provider is installed
108    let _ = rustls::crypto::ring::default_provider().install_default();
109
110    let cert_pem = std::fs::read(&config.cert_path)?;
111    let key_pem = std::fs::read(&config.key_path)?;
112
113    let certs = rustls_pemfile::certs(&mut io::BufReader::new(&cert_pem[..]))
114        .collect::<Result<Vec<_>, _>>()?;
115    let key = rustls_pemfile::private_key(&mut io::BufReader::new(&key_pem[..]))?
116        .ok_or("no private key found in PEM file")?;
117
118    let server_config = ServerConfig::builder()
119        .with_no_client_auth()
120        .with_single_cert(certs, key)?;
121
122    Ok(TlsAcceptor::from(Arc::new(server_config)))
123}