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