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>> {
89 let cert_path = dir.join("wire-tls-cert.pem");
90 let key_path = dir.join("wire-tls-key.pem");
91
92 if cert_path.exists() && key_path.exists() {
94 tracing::info!(cert = %cert_path.display(), "wire TLS: reusing existing cert");
95 return Ok(WireTlsConfig {
96 cert_path,
97 key_path,
98 });
99 }
100
101 tracing::info!("wire TLS: generating self-signed certificate");
102 let (cert_pem, key_pem) = generate_self_signed_cert("localhost")?;
103
104 std::fs::create_dir_all(dir)?;
105 std::fs::write(&cert_path, &cert_pem)?;
106 std::fs::write(&key_path, &key_pem)?;
107
108 #[cfg(unix)]
110 {
111 use std::os::unix::fs::PermissionsExt;
112 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))?;
113 }
114
115 tracing::info!(
116 cert = %cert_path.display(),
117 key = %key_path.display(),
118 "wire TLS: wrote self-signed cert"
119 );
120
121 Ok(WireTlsConfig {
122 cert_path,
123 key_path,
124 })
125}
126
127pub fn build_tls_acceptor(
129 config: &WireTlsConfig,
130) -> Result<TlsAcceptor, Box<dyn std::error::Error>> {
131 let _ = rustls::crypto::ring::default_provider().install_default();
133
134 let cert_pem = std::fs::read(&config.cert_path)?;
135 let key_pem = std::fs::read(&config.key_path)?;
136
137 let certs = rustls_pemfile::certs(&mut io::BufReader::new(&cert_pem[..]))
138 .collect::<Result<Vec<_>, _>>()?;
139 let key = rustls_pemfile::private_key(&mut io::BufReader::new(&key_pem[..]))?
140 .ok_or("no private key found in PEM file")?;
141
142 let server_config = ServerConfig::builder()
143 .with_no_client_auth()
144 .with_single_cert(certs, key)?;
145
146 Ok(TlsAcceptor::from(Arc::new(server_config)))
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn subject_cn_and_orgs(cert_pem: &str) -> (Vec<String>, Vec<String>) {
156 let der = rustls_pemfile::certs(&mut io::BufReader::new(cert_pem.as_bytes()))
157 .next()
158 .expect("one certificate in PEM")
159 .expect("valid certificate PEM");
160 let (_, parsed) =
161 x509_parser::parse_x509_certificate(der.as_ref()).expect("parse generated X.509");
162 let subject = parsed.subject();
163 let cns = subject
164 .iter_common_name()
165 .filter_map(|cn| cn.as_str().ok().map(str::to_string))
166 .collect();
167 let orgs = subject
168 .iter_organization()
169 .filter_map(|o| o.as_str().ok().map(str::to_string))
170 .collect();
171 (cns, orgs)
172 }
173
174 #[test]
175 fn wire_cert_keeps_wire_cn_and_reddb_org() {
176 let (cert_pem, key_pem) =
177 generate_self_signed_cert("localhost").expect("generate wire cert");
178 assert!(key_pem.contains("PRIVATE KEY"), "key PEM emitted");
179 let (cns, orgs) = subject_cn_and_orgs(&cert_pem);
180 assert_eq!(cns, vec!["RedDB Wire localhost".to_string()]);
181 assert_eq!(orgs, vec!["RedDB".to_string()]);
182 }
183
184 #[test]
185 fn http_cert_keeps_http_cn_and_no_org() {
186 let (cert_pem, _key) = generate_self_signed_dev_cert("localhost", "RedDB HTTP", None)
190 .expect("generate http cert");
191 let (cns, orgs) = subject_cn_and_orgs(&cert_pem);
192 assert_eq!(cns, vec!["RedDB HTTP localhost".to_string()]);
193 assert!(orgs.is_empty(), "HTTP cert must not carry an Organization");
194 }
195}