1use std::io;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use rustls::ServerConfig;
12use tokio_rustls::TlsAcceptor;
13
14#[derive(Debug, Clone)]
16pub struct WireTlsConfig {
17 pub cert_path: PathBuf,
19 pub key_path: PathBuf,
21}
22
23pub fn generate_self_signed_cert(
31 hostname: &str,
32) -> Result<(String, String), Box<dyn std::error::Error>> {
33 generate_self_signed_dev_cert(hostname, "RedDB Wire", Some("RedDB"))
34}
35
36pub(crate) fn generate_self_signed_dev_cert(
46 hostname: &str,
47 cn_label: &str,
48 org: Option<&str>,
49) -> Result<(String, String), Box<dyn std::error::Error>> {
50 use rcgen::{CertificateParams, KeyPair};
51
52 let mut params = CertificateParams::new(vec![hostname.to_string()])?;
53 params.distinguished_name.push(
54 rcgen::DnType::CommonName,
55 rcgen::DnValue::Utf8String(format!("{cn_label} {hostname}")),
56 );
57 if let Some(org) = org {
58 params.distinguished_name.push(
59 rcgen::DnType::OrganizationName,
60 rcgen::DnValue::Utf8String(org.to_string()),
61 );
62 }
63
64 params
66 .subject_alt_names
67 .push(rcgen::SanType::DnsName(hostname.try_into()?));
68 if hostname != "localhost" {
69 params
70 .subject_alt_names
71 .push(rcgen::SanType::DnsName("localhost".try_into()?));
72 }
73 params
75 .subject_alt_names
76 .push(rcgen::SanType::IpAddress(std::net::IpAddr::V4(
77 std::net::Ipv4Addr::LOCALHOST,
78 )));
79
80 let key_pair = KeyPair::generate()?;
81 let cert = params.self_signed(&key_pair)?;
82
83 Ok((cert.pem(), key_pair.serialize_pem()))
84}
85
86pub fn auto_generate_cert(dir: &Path) -> Result<WireTlsConfig, Box<dyn std::error::Error>> {
95 let dev_flag = std::env::var("RED_WIRE_TLS_DEV").unwrap_or_default();
96 if !matches!(dev_flag.as_str(), "1" | "true" | "yes" | "on") {
97 return Err(
98 "refusing to auto-generate wire TLS cert: set RED_WIRE_TLS_DEV=1 to opt into self-signed dev certs"
99 .into(),
100 );
101 }
102
103 let cert_path = dir.join("wire-tls-cert.pem");
104 let key_path = dir.join("wire-tls-key.pem");
105
106 if cert_path.exists() && key_path.exists() {
108 tracing::info!(cert = %cert_path.display(), "wire TLS: reusing existing cert");
109 return Ok(WireTlsConfig {
110 cert_path,
111 key_path,
112 });
113 }
114
115 tracing::info!("wire TLS: generating self-signed certificate");
116 let (cert_pem, key_pem) = generate_self_signed_cert("localhost")?;
117
118 std::fs::create_dir_all(dir)?;
119 std::fs::write(&cert_path, &cert_pem)?;
120 std::fs::write(&key_path, &key_pem)?;
121
122 #[cfg(unix)]
124 {
125 use std::os::unix::fs::PermissionsExt;
126 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))?;
127 }
128
129 tracing::info!(
130 cert = %cert_path.display(),
131 key = %key_path.display(),
132 "wire TLS: wrote self-signed cert"
133 );
134
135 Ok(WireTlsConfig {
136 cert_path,
137 key_path,
138 })
139}
140
141pub fn build_tls_acceptor(
143 config: &WireTlsConfig,
144) -> Result<TlsAcceptor, Box<dyn std::error::Error>> {
145 let _ = rustls::crypto::ring::default_provider().install_default();
147
148 let cert_pem = std::fs::read(&config.cert_path)?;
149 let key_pem = std::fs::read(&config.key_path)?;
150
151 let certs = rustls_pemfile::certs(&mut io::BufReader::new(&cert_pem[..]))
152 .collect::<Result<Vec<_>, _>>()?;
153 let key = rustls_pemfile::private_key(&mut io::BufReader::new(&key_pem[..]))?
154 .ok_or("no private key found in PEM file")?;
155
156 let server_config = ServerConfig::builder()
157 .with_no_client_auth()
158 .with_single_cert(certs, key)?;
159
160 Ok(TlsAcceptor::from(Arc::new(server_config)))
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 fn subject_cn_and_orgs(cert_pem: &str) -> (Vec<String>, Vec<String>) {
170 let der = rustls_pemfile::certs(&mut io::BufReader::new(cert_pem.as_bytes()))
171 .next()
172 .expect("one certificate in PEM")
173 .expect("valid certificate PEM");
174 let (_, parsed) =
175 x509_parser::parse_x509_certificate(der.as_ref()).expect("parse generated X.509");
176 let subject = parsed.subject();
177 let cns = subject
178 .iter_common_name()
179 .filter_map(|cn| cn.as_str().ok().map(str::to_string))
180 .collect();
181 let orgs = subject
182 .iter_organization()
183 .filter_map(|o| o.as_str().ok().map(str::to_string))
184 .collect();
185 (cns, orgs)
186 }
187
188 #[test]
189 fn wire_cert_keeps_wire_cn_and_reddb_org() {
190 let (cert_pem, key_pem) =
191 generate_self_signed_cert("localhost").expect("generate wire cert");
192 assert!(key_pem.contains("PRIVATE KEY"), "key PEM emitted");
193 let (cns, orgs) = subject_cn_and_orgs(&cert_pem);
194 assert_eq!(cns, vec!["RedDB Wire localhost".to_string()]);
195 assert_eq!(orgs, vec!["RedDB".to_string()]);
196 }
197
198 #[test]
199 fn http_cert_keeps_http_cn_and_no_org() {
200 let (cert_pem, _key) = generate_self_signed_dev_cert("localhost", "RedDB HTTP", None)
204 .expect("generate http cert");
205 let (cns, orgs) = subject_cn_and_orgs(&cert_pem);
206 assert_eq!(cns, vec!["RedDB HTTP localhost".to_string()]);
207 assert!(orgs.is_empty(), "HTTP cert must not carry an Organization");
208 }
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 auto_generate_refuses_without_dev_flag() {
220 let _g = env_lock().lock();
221 let dir = std::env::temp_dir().join(format!(
222 "reddb-wire-tls-test-{}-{}",
223 std::process::id(),
224 std::time::SystemTime::now()
225 .duration_since(std::time::UNIX_EPOCH)
226 .unwrap()
227 .as_nanos()
228 ));
229 unsafe {
231 std::env::remove_var("RED_WIRE_TLS_DEV");
232 }
233 let err = auto_generate_cert(&dir).unwrap_err();
234 assert!(err.to_string().contains("RED_WIRE_TLS_DEV"));
235 }
236
237 #[test]
238 fn auto_generate_with_dev_flag_writes_cert() {
239 let _g = env_lock().lock();
240 let dir = std::env::temp_dir().join(format!(
241 "reddb-wire-tls-dev-{}-{}",
242 std::process::id(),
243 std::time::SystemTime::now()
244 .duration_since(std::time::UNIX_EPOCH)
245 .unwrap()
246 .as_nanos()
247 ));
248 unsafe {
249 std::env::set_var("RED_WIRE_TLS_DEV", "1");
250 }
251 let cfg = auto_generate_cert(&dir).expect("should generate");
252 assert!(cfg.cert_path.exists());
253 assert!(cfg.key_path.exists());
254 unsafe {
255 std::env::remove_var("RED_WIRE_TLS_DEV");
256 }
257 let _ = std::fs::remove_dir_all(&dir);
258 }
259}