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>> {
169 use rcgen::{CertificateParams, KeyPair};
170 let mut params = CertificateParams::new(vec![hostname.to_string()])?;
171 params.distinguished_name.push(
172 rcgen::DnType::CommonName,
173 rcgen::DnValue::Utf8String(format!("RedDB HTTP {hostname}")),
174 );
175 params
176 .subject_alt_names
177 .push(rcgen::SanType::DnsName(hostname.try_into()?));
178 if hostname != "localhost" {
179 params
180 .subject_alt_names
181 .push(rcgen::SanType::DnsName("localhost".try_into()?));
182 }
183 params
184 .subject_alt_names
185 .push(rcgen::SanType::IpAddress(std::net::IpAddr::V4(
186 std::net::Ipv4Addr::LOCALHOST,
187 )));
188 let key_pair = KeyPair::generate()?;
189 let cert = params.self_signed(&key_pair)?;
190 Ok((cert.pem(), key_pair.serialize_pem()))
191}
192
193fn sha256_fingerprint_hex(cert: &CertificateDer<'_>) -> String {
194 let digest = crate::crypto::sha256(cert.as_ref());
195 let mut out = String::with_capacity(64 + 31);
196 for (i, byte) in digest.iter().enumerate() {
197 if i > 0 {
198 out.push(':');
201 }
202 let _ = std::fmt::Write::write_fmt(&mut out, format_args!("{:02x}", byte));
203 }
204 out
205}
206
207pub fn accept_tls(
211 config: Arc<ServerConfig>,
212 tcp: TcpStream,
213) -> io::Result<StreamOwned<ServerConnection, TcpStream>> {
214 let conn = ServerConnection::new(config)
215 .map_err(|err| io::Error::other(format!("rustls server: {err}")))?;
216 let mut stream = StreamOwned::new(conn, tcp);
217 let _ = stream.flush();
220 Ok(stream)
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 fn env_lock() -> &'static std::sync::Mutex<()> {
231 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
232 LOCK.get_or_init(|| std::sync::Mutex::new(()))
233 }
234
235 #[test]
236 fn fingerprint_format() {
237 let cert = CertificateDer::from(vec![0u8; 8]);
239 let fp = sha256_fingerprint_hex(&cert);
240 assert_eq!(fp.len(), 64 + 31);
242 assert!(fp.chars().all(|c| c == ':' || c.is_ascii_hexdigit()));
243 }
244
245 #[test]
246 fn auto_generate_refuses_without_dev_flag() {
247 let _g = env_lock().lock();
248 let dir = std::env::temp_dir().join(format!(
249 "reddb-http-tls-test-{}-{}",
250 std::process::id(),
251 std::time::SystemTime::now()
252 .duration_since(std::time::UNIX_EPOCH)
253 .unwrap()
254 .as_nanos()
255 ));
256 unsafe {
258 std::env::remove_var("RED_HTTP_TLS_DEV");
259 }
260 let err = auto_generate_dev_cert(&dir).unwrap_err();
261 assert!(err.to_string().contains("RED_HTTP_TLS_DEV"));
262 }
263
264 #[test]
265 fn auto_generate_with_dev_flag_writes_cert() {
266 let _g = env_lock().lock();
267 let dir = std::env::temp_dir().join(format!(
268 "reddb-http-tls-dev-{}-{}",
269 std::process::id(),
270 std::time::SystemTime::now()
271 .duration_since(std::time::UNIX_EPOCH)
272 .unwrap()
273 .as_nanos()
274 ));
275 unsafe {
276 std::env::set_var("RED_HTTP_TLS_DEV", "1");
277 }
278 let cfg = auto_generate_dev_cert(&dir).expect("should generate");
279 assert!(cfg.cert_path.exists());
280 assert!(cfg.key_path.exists());
281 unsafe {
282 std::env::remove_var("RED_HTTP_TLS_DEV");
283 }
284 let _ = std::fs::remove_dir_all(&dir);
285 }
286}