zerodds_bridge_security/
tls.rs1use std::fs::File;
15use std::io::BufReader;
16use std::path::Path;
17use std::sync::Arc;
18
19use rustls::ServerConfig;
20use rustls_pemfile::Item;
21use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
22
23#[derive(Debug)]
25pub enum TlsConfigError {
26 CertFileRead(String),
28 KeyFileRead(String),
30 NoCertificateInPem,
32 NoSupportedPrivateKeyInPem,
34 Rustls(String),
37}
38
39impl core::fmt::Display for TlsConfigError {
40 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41 match self {
42 Self::CertFileRead(m) => write!(f, "cert file read: {m}"),
43 Self::KeyFileRead(m) => write!(f, "key file read: {m}"),
44 Self::NoCertificateInPem => f.write_str("PEM had no CERTIFICATE block"),
45 Self::NoSupportedPrivateKeyInPem => {
46 f.write_str("PEM had no PKCS#8 / RSA / EC private key")
47 }
48 Self::Rustls(m) => write!(f, "rustls build: {m}"),
49 }
50 }
51}
52
53impl std::error::Error for TlsConfigError {}
54
55pub fn load_server_config(
62 cert_pem_path: &Path,
63 key_pem_path: &Path,
64) -> Result<Arc<ServerConfig>, TlsConfigError> {
65 let certs = read_certs(cert_pem_path)?;
66 let key = read_private_key(key_pem_path)?;
67 let provider = rustls::crypto::ring::default_provider();
68 let cfg = ServerConfig::builder_with_provider(Arc::new(provider))
69 .with_safe_default_protocol_versions()
70 .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?
71 .with_no_client_auth()
72 .with_single_cert(certs, key)
73 .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?;
74 Ok(Arc::new(cfg))
75}
76
77pub fn load_server_config_with_client_auth(
87 cert_pem_path: &Path,
88 key_pem_path: &Path,
89 client_ca_pem_path: &Path,
90) -> Result<Arc<ServerConfig>, TlsConfigError> {
91 let certs = read_certs(cert_pem_path)?;
92 let key = read_private_key(key_pem_path)?;
93 let client_cas = read_certs(client_ca_pem_path)?;
94
95 let mut roots = rustls::RootCertStore::empty();
96 for c in client_cas {
97 roots
98 .add(c)
99 .map_err(|e| TlsConfigError::Rustls(format!("client CA add: {e}")))?;
100 }
101 let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
102 .build()
103 .map_err(|e| TlsConfigError::Rustls(format!("client verifier: {e}")))?;
104
105 let provider = rustls::crypto::ring::default_provider();
106 let cfg = ServerConfig::builder_with_provider(Arc::new(provider))
107 .with_safe_default_protocol_versions()
108 .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?
109 .with_client_cert_verifier(verifier)
110 .with_single_cert(certs, key)
111 .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?;
112 Ok(Arc::new(cfg))
113}
114
115pub(crate) fn read_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, TlsConfigError> {
116 let f = File::open(path)
117 .map_err(|e| TlsConfigError::CertFileRead(format!("{}: {e}", path.display())))?;
118 let mut br = BufReader::new(f);
119 let mut out = Vec::new();
120 for item in rustls_pemfile::read_all(&mut br) {
121 let item = item.map_err(|e| TlsConfigError::CertFileRead(format!("{e}")))?;
122 if let Item::X509Certificate(d) = item {
123 out.push(d);
124 }
125 }
126 if out.is_empty() {
127 return Err(TlsConfigError::NoCertificateInPem);
128 }
129 Ok(out)
130}
131
132pub(crate) fn read_private_key(path: &Path) -> Result<PrivateKeyDer<'static>, TlsConfigError> {
133 let f = File::open(path)
134 .map_err(|e| TlsConfigError::KeyFileRead(format!("{}: {e}", path.display())))?;
135 let mut br = BufReader::new(f);
136 for item in rustls_pemfile::read_all(&mut br) {
137 let item = item.map_err(|e| TlsConfigError::KeyFileRead(format!("{e}")))?;
138 match item {
139 Item::Pkcs8Key(k) => return Ok(PrivateKeyDer::Pkcs8(k)),
140 Item::Pkcs1Key(k) => return Ok(PrivateKeyDer::Pkcs1(k)),
141 Item::Sec1Key(k) => return Ok(PrivateKeyDer::Sec1(k)),
142 _ => {}
143 }
144 }
145 let _ = PrivatePkcs8KeyDer::from(Vec::<u8>::new()); Err(TlsConfigError::NoSupportedPrivateKeyInPem)
147}
148
149#[cfg(test)]
150#[allow(clippy::expect_used, clippy::unwrap_used)]
151mod tests {
152 use super::*;
153 use std::io::Write;
154
155 fn write_temp(name: &str, body: &[u8]) -> std::path::PathBuf {
156 let dir =
157 std::env::temp_dir().join(format!("zd-bridge-sec-{}-{}", name, std::process::id()));
158 let _ = std::fs::create_dir_all(&dir);
159 let p = dir.join(name);
160 let mut f = std::fs::File::create(&p).unwrap();
161 f.write_all(body).unwrap();
162 p
163 }
164
165 fn gen_self_signed() -> (String, String) {
166 let ck = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
167 (ck.cert.pem(), ck.key_pair.serialize_pem())
168 }
169
170 #[test]
171 fn load_self_signed_cert_succeeds() {
172 let (cert_pem, key_pem) = gen_self_signed();
173 let c = write_temp("cert.pem", cert_pem.as_bytes());
174 let k = write_temp("key.pem", key_pem.as_bytes());
175 let cfg = load_server_config(&c, &k).expect("ServerConfig");
176 assert!(Arc::strong_count(&cfg) >= 1);
178 }
179
180 #[test]
181 fn missing_cert_file_returns_err() {
182 let p = std::path::PathBuf::from("/no/such/file.pem");
183 let err = load_server_config(&p, &p).unwrap_err();
184 assert!(matches!(err, TlsConfigError::CertFileRead(_)));
185 }
186
187 #[test]
188 fn empty_pem_rejected_as_no_cert() {
189 let c = write_temp(
190 "empty.pem",
191 b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n",
192 );
193 let k = c.clone();
194 let err = load_server_config(&c, &k).unwrap_err();
195 assert!(matches!(err, TlsConfigError::NoCertificateInPem));
196 }
197
198 #[test]
199 fn key_pem_without_supported_block_rejected() {
200 let (cert_pem, _) = gen_self_signed();
201 let c = write_temp("c2.pem", cert_pem.as_bytes());
202 let k = write_temp(
203 "k2.pem",
204 b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n",
205 );
206 let err = load_server_config(&c, &k).unwrap_err();
207 assert!(matches!(err, TlsConfigError::NoSupportedPrivateKeyInPem));
208 }
209
210 #[test]
211 fn mtls_config_loads_with_client_ca() {
212 let (cert_pem, key_pem) = gen_self_signed();
213 let c = write_temp("c3.pem", cert_pem.as_bytes());
214 let k = write_temp("k3.pem", key_pem.as_bytes());
215 let ca = write_temp("ca.pem", cert_pem.as_bytes());
218 let cfg = load_server_config_with_client_auth(&c, &k, &ca).expect("mtls cfg");
219 assert!(Arc::strong_count(&cfg) >= 1);
220 }
221}