reddb_server/server/
tls.rs1use std::io::{self, BufReader, Write};
20use std::net::TcpStream;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24use rustls::pki_types::CertificateDer;
25use rustls::server::WebPkiClientVerifier;
26use rustls::{RootCertStore, ServerConfig, ServerConnection, StreamOwned};
27
28#[derive(Debug, Clone)]
30pub struct HttpTlsConfig {
31 pub cert_path: PathBuf,
33 pub key_path: PathBuf,
35 pub client_ca_path: Option<PathBuf>,
40}
41
42pub fn build_server_config(
46 config: &HttpTlsConfig,
47) -> Result<Arc<ServerConfig>, Box<dyn std::error::Error>> {
48 let _ = rustls::crypto::ring::default_provider().install_default();
49
50 let cert_pem = std::fs::read(&config.cert_path)
51 .map_err(|err| format!("read TLS cert {}: {err}", config.cert_path.display()))?;
52 let key_pem = std::fs::read(&config.key_path)
53 .map_err(|err| format!("read TLS key {}: {err}", config.key_path.display()))?;
54
55 let certs = rustls_pemfile::certs(&mut BufReader::new(&cert_pem[..]))
56 .collect::<Result<Vec<_>, _>>()
57 .map_err(|err| format!("decode cert PEM: {err}"))?;
58 if certs.is_empty() {
59 return Err("TLS cert PEM contained no certificates".into());
60 }
61 let key = rustls_pemfile::private_key(&mut BufReader::new(&key_pem[..]))
62 .map_err(|err| format!("decode key PEM: {err}"))?
63 .ok_or("TLS key PEM contained no private key")?;
64
65 let fingerprint = sha256_fingerprint_hex(&certs[0]);
67 tracing::info!(
68 target: "reddb::http_tls",
69 cert = %config.cert_path.display(),
70 sha256 = %fingerprint,
71 "HTTP TLS certificate loaded"
72 );
73
74 let builder = ServerConfig::builder();
75 let mut server_config = if let Some(ca_path) = &config.client_ca_path {
76 let ca_pem = std::fs::read(ca_path)
77 .map_err(|err| format!("read mTLS client CA {}: {err}", ca_path.display()))?;
78 let mut roots = RootCertStore::empty();
79 let ca_certs: Vec<CertificateDer<'static>> =
80 rustls_pemfile::certs(&mut BufReader::new(&ca_pem[..]))
81 .collect::<Result<Vec<_>, _>>()
82 .map_err(|err| format!("decode mTLS client CA PEM: {err}"))?;
83 if ca_certs.is_empty() {
84 return Err("mTLS client CA PEM contained no certificates".into());
85 }
86 for cert in ca_certs {
87 roots.add(cert)?;
88 }
89 let verifier = WebPkiClientVerifier::builder(Arc::new(roots))
90 .build()
91 .map_err(|err| format!("build mTLS client verifier: {err}"))?;
92 tracing::info!(
93 target: "reddb::http_tls",
94 ca = %ca_path.display(),
95 "HTTP mTLS enabled — clients must present a cert chaining to this CA"
96 );
97 builder
98 .with_client_cert_verifier(verifier)
99 .with_single_cert(certs, key)
100 .map_err(|err| format!("install TLS cert/key: {err}"))?
101 } else {
102 builder
103 .with_no_client_auth()
104 .with_single_cert(certs, key)
105 .map_err(|err| format!("install TLS cert/key: {err}"))?
106 };
107
108 server_config.alpn_protocols = vec![b"http/1.1".to_vec()];
112
113 Ok(Arc::new(server_config))
114}
115
116pub fn auto_generate_dev_cert(dir: &Path) -> Result<HttpTlsConfig, Box<dyn std::error::Error>> {
123 let dev_flag = std::env::var("RED_HTTP_TLS_DEV").unwrap_or_default();
124 if !matches!(dev_flag.as_str(), "1" | "true" | "yes" | "on") {
125 return Err(
126 "refusing to auto-generate HTTP TLS cert: set RED_HTTP_TLS_DEV=1 to opt into self-signed dev certs"
127 .into(),
128 );
129 }
130
131 let cert_path = dir.join("http-tls-cert.pem");
132 let key_path = dir.join("http-tls-key.pem");
133
134 if cert_path.exists() && key_path.exists() {
135 tracing::info!(
136 target: "reddb::http_tls",
137 cert = %cert_path.display(),
138 "HTTP TLS dev: reusing existing self-signed cert"
139 );
140 return Ok(HttpTlsConfig {
141 cert_path,
142 key_path,
143 client_ca_path: None,
144 });
145 }
146
147 let (cert_pem, key_pem) = generate_self_signed("localhost")?;
148 std::fs::create_dir_all(dir)?;
149 std::fs::write(&cert_path, &cert_pem)?;
150 std::fs::write(&key_path, &key_pem)?;
151 #[cfg(unix)]
152 {
153 use std::os::unix::fs::PermissionsExt;
154 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))?;
155 }
156 tracing::warn!(
157 target: "reddb::http_tls",
158 cert = %cert_path.display(),
159 "HTTP TLS dev: generated SELF-SIGNED cert (NOT FOR PRODUCTION)"
160 );
161 Ok(HttpTlsConfig {
162 cert_path,
163 key_path,
164 client_ca_path: None,
165 })
166}
167
168fn generate_self_signed(hostname: &str) -> Result<(String, String), Box<dyn std::error::Error>> {
174 crate::wire::tls::generate_self_signed_dev_cert(hostname, "RedDB HTTP", None)
175}
176
177fn sha256_fingerprint_hex(cert: &CertificateDer<'_>) -> String {
178 let digest = crate::crypto::sha256(cert.as_ref());
179 let mut out = String::with_capacity(64 + 31);
180 for (i, byte) in digest.iter().enumerate() {
181 if i > 0 {
182 out.push(':');
185 }
186 let _ = std::fmt::Write::write_fmt(&mut out, format_args!("{:02x}", byte));
187 }
188 out
189}
190
191pub fn accept_tls(
195 config: Arc<ServerConfig>,
196 tcp: TcpStream,
197) -> io::Result<StreamOwned<ServerConnection, TcpStream>> {
198 let conn = ServerConnection::new(config)
199 .map_err(|err| io::Error::other(format!("rustls server: {err}")))?;
200 let mut stream = StreamOwned::new(conn, tcp);
201 let _ = stream.flush();
204 Ok(stream)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 fn env_lock() -> &'static std::sync::Mutex<()> {
215 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
216 LOCK.get_or_init(|| std::sync::Mutex::new(()))
217 }
218
219 #[test]
220 fn fingerprint_format() {
221 let cert = CertificateDer::from(vec![0u8; 8]);
223 let fp = sha256_fingerprint_hex(&cert);
224 assert_eq!(fp.len(), 64 + 31);
226 assert!(fp.chars().all(|c| c == ':' || c.is_ascii_hexdigit()));
227 }
228
229 #[test]
230 fn auto_generate_refuses_without_dev_flag() {
231 let _g = env_lock().lock();
232 let dir = std::env::temp_dir().join(format!(
233 "reddb-http-tls-test-{}-{}",
234 std::process::id(),
235 std::time::SystemTime::now()
236 .duration_since(std::time::UNIX_EPOCH)
237 .unwrap()
238 .as_nanos()
239 ));
240 unsafe {
242 std::env::remove_var("RED_HTTP_TLS_DEV");
243 }
244 let err = auto_generate_dev_cert(&dir).unwrap_err();
245 assert!(err.to_string().contains("RED_HTTP_TLS_DEV"));
246 }
247
248 #[test]
249 fn auto_generate_with_dev_flag_writes_cert() {
250 let _g = env_lock().lock();
251 let dir = std::env::temp_dir().join(format!(
252 "reddb-http-tls-dev-{}-{}",
253 std::process::id(),
254 std::time::SystemTime::now()
255 .duration_since(std::time::UNIX_EPOCH)
256 .unwrap()
257 .as_nanos()
258 ));
259 unsafe {
260 std::env::set_var("RED_HTTP_TLS_DEV", "1");
261 }
262 let cfg = auto_generate_dev_cert(&dir).expect("should generate");
263 assert!(cfg.cert_path.exists());
264 assert!(cfg.key_path.exists());
265 unsafe {
266 std::env::remove_var("RED_HTTP_TLS_DEV");
267 }
268 let _ = std::fs::remove_dir_all(&dir);
269 }
270}