1use std::path::Path;
2use std::sync::Arc;
3
4use rustls::pki_types::{CertificateDer, PrivateKeyDer};
5use zamsync_core::{ZamError, ZamResult};
6
7pub struct TlsConfig {
13 cert_pem: Vec<u8>,
14 key_pem: Vec<u8>,
15 ca_pem: Vec<u8>,
16}
17
18impl TlsConfig {
19 pub fn from_files(
25 cert_path: impl AsRef<Path>,
26 key_path: impl AsRef<Path>,
27 ca_path: impl AsRef<Path>,
28 ) -> ZamResult<Self> {
29 Ok(Self {
30 cert_pem: std::fs::read(cert_path)?,
31 key_pem: std::fs::read(key_path)?,
32 ca_pem: std::fs::read(ca_path)?,
33 })
34 }
35
36 pub fn from_pem(cert_pem: String, key_pem: String, ca_pem: String) -> Self {
38 Self {
39 cert_pem: cert_pem.into_bytes(),
40 key_pem: key_pem.into_bytes(),
41 ca_pem: ca_pem.into_bytes(),
42 }
43 }
44
45 fn load_cert(&self) -> ZamResult<CertificateDer<'static>> {
46 rustls_pemfile::certs(&mut self.cert_pem.as_slice())
47 .collect::<Result<Vec<_>, _>>()?
48 .into_iter()
49 .next()
50 .ok_or_else(|| ZamError::Config("no certificate in cert file".into()))
51 }
52
53 fn load_key(&self) -> ZamResult<PrivateKeyDer<'static>> {
54 rustls_pemfile::private_key(&mut self.key_pem.as_slice())?
55 .ok_or_else(|| ZamError::Config("no private key in key file".into()))
56 }
57
58 fn load_ca(&self) -> ZamResult<CertificateDer<'static>> {
59 rustls_pemfile::certs(&mut self.ca_pem.as_slice())
60 .collect::<Result<Vec<_>, _>>()?
61 .into_iter()
62 .next()
63 .ok_or_else(|| ZamError::Config("no certificate in CA file".into()))
64 }
65
66 pub(crate) fn server_config(&self) -> ZamResult<Arc<rustls::ServerConfig>> {
68 let cert = self.load_cert()?;
69 let key = self.load_key()?;
70 let ca = self.load_ca()?;
71
72 let mut client_roots = rustls::RootCertStore::empty();
73 client_roots
74 .add(ca)
75 .map_err(|e| ZamError::Config(format!("invalid CA cert: {e}")))?;
76
77 let client_verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(client_roots))
78 .build()
79 .map_err(|e| ZamError::Config(format!("client verifier: {e}")))?;
80
81 let config = rustls::ServerConfig::builder()
82 .with_client_cert_verifier(client_verifier)
83 .with_single_cert(vec![cert], key)
84 .map_err(|e| ZamError::Config(format!("server TLS config: {e}")))?;
85
86 Ok(Arc::new(config))
87 }
88
89 pub(crate) fn client_config(&self) -> ZamResult<Arc<rustls::ClientConfig>> {
91 let cert = self.load_cert()?;
92 let key = self.load_key()?;
93 let ca = self.load_ca()?;
94
95 let mut server_roots = rustls::RootCertStore::empty();
96 server_roots
97 .add(ca)
98 .map_err(|e| ZamError::Config(format!("invalid CA cert: {e}")))?;
99
100 let config = rustls::ClientConfig::builder()
101 .with_root_certificates(server_roots)
102 .with_client_auth_cert(vec![cert], key)
103 .map_err(|e| ZamError::Config(format!("client TLS config: {e}")))?;
104
105 Ok(Arc::new(config))
106 }
107}
108
109pub struct GeneratedCredentials {
111 pub ca_cert_pem: String,
113 pub ca_key_pem: String,
115 pub node_cert_pem: String,
117 pub node_key_pem: String,
119}
120
121pub fn generate_credentials() -> ZamResult<GeneratedCredentials> {
128 let ca_key = rcgen::KeyPair::generate()
129 .map_err(|e| ZamError::Config(format!("CA key generation failed: {e}")))?;
130
131 let mut ca_params = rcgen::CertificateParams::new(vec!["ZamSync CA".to_string()])
132 .map_err(|e| ZamError::Config(format!("CA params: {e}")))?;
133 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
134
135 let ca_cert = ca_params
136 .self_signed(&ca_key)
137 .map_err(|e| ZamError::Config(format!("CA self-sign failed: {e}")))?;
138
139 let node_key = rcgen::KeyPair::generate()
140 .map_err(|e| ZamError::Config(format!("node key generation failed: {e}")))?;
141
142 let node_params = rcgen::CertificateParams::new(vec!["zamsync.local".to_string()])
143 .map_err(|e| ZamError::Config(format!("node params: {e}")))?;
144
145 let ca_key_pem = ca_key.serialize_pem();
146 let ca_issuer = rcgen::Issuer::from_params(&ca_params, ca_key);
147 let node_cert = node_params
148 .signed_by(&node_key, &ca_issuer)
149 .map_err(|e| ZamError::Config(format!("node cert signing failed: {e}")))?;
150
151 Ok(GeneratedCredentials {
152 ca_cert_pem: ca_cert.pem(),
153 ca_key_pem,
154 node_cert_pem: node_cert.pem(),
155 node_key_pem: node_key.serialize_pem(),
156 })
157}
158
159pub struct SignedNodeCredentials {
161 pub ca_cert_pem: String,
163 pub node_cert_pem: String,
165 pub node_key_pem: String,
167}
168
169pub fn sign_node_cert(ca_cert_pem: &str, ca_key_pem: &str) -> ZamResult<SignedNodeCredentials> {
180 let ca_key = rcgen::KeyPair::from_pem(ca_key_pem)
181 .map_err(|e| ZamError::Config(format!("parse CA key: {e}")))?;
182
183 let mut ca_params = rcgen::CertificateParams::new(vec!["ZamSync CA".to_string()])
187 .map_err(|e| ZamError::Config(format!("CA params: {e}")))?;
188 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
189
190 let node_key = rcgen::KeyPair::generate()
191 .map_err(|e| ZamError::Config(format!("node key generation failed: {e}")))?;
192
193 let node_params = rcgen::CertificateParams::new(vec!["zamsync.local".to_string()])
194 .map_err(|e| ZamError::Config(format!("node params: {e}")))?;
195
196 let ca_issuer = rcgen::Issuer::from_params(&ca_params, ca_key);
197 let node_cert = node_params
198 .signed_by(&node_key, &ca_issuer)
199 .map_err(|e| ZamError::Config(format!("node cert signing failed: {e}")))?;
200
201 Ok(SignedNodeCredentials {
202 ca_cert_pem: ca_cert_pem.to_owned(),
203 node_cert_pem: node_cert.pem(),
204 node_key_pem: node_key.serialize_pem(),
205 })
206}
207
208pub fn install_crypto_provider() {
210 let _ = rustls::crypto::ring::default_provider().install_default();
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_sign_node_cert_produces_valid_chain() {
219 install_crypto_provider();
220
221 let hub = generate_credentials().expect("hub keygen failed");
222
223 let clinic_a =
225 sign_node_cert(&hub.ca_cert_pem, &hub.ca_key_pem).expect("clinic_a signing failed");
226 let clinic_b =
227 sign_node_cert(&hub.ca_cert_pem, &hub.ca_key_pem).expect("clinic_b signing failed");
228
229 assert_eq!(clinic_a.ca_cert_pem, hub.ca_cert_pem);
231 assert_eq!(clinic_b.ca_cert_pem, hub.ca_cert_pem);
232
233 assert_ne!(clinic_a.node_cert_pem, clinic_b.node_cert_pem);
235 assert_ne!(clinic_a.node_key_pem, clinic_b.node_key_pem);
236
237 let hub_tls = TlsConfig::from_pem(
239 hub.node_cert_pem.clone(),
240 hub.node_key_pem.clone(),
241 hub.ca_cert_pem.clone(),
242 );
243 let clinic_a_tls = TlsConfig::from_pem(
244 clinic_a.node_cert_pem.clone(),
245 clinic_a.node_key_pem.clone(),
246 clinic_a.ca_cert_pem.clone(),
247 );
248
249 hub_tls.server_config().expect("hub server_config failed");
251 clinic_a_tls
252 .client_config()
253 .expect("clinic_a client_config failed");
254 }
255
256 #[test]
257 fn test_rogue_node_with_own_ca_rejected() {
258 install_crypto_provider();
259
260 let hub = generate_credentials().expect("hub keygen");
261 let rogue = generate_credentials().expect("rogue keygen");
262
263 let hub_tls = TlsConfig::from_pem(
265 hub.node_cert_pem.clone(),
266 hub.node_key_pem.clone(),
267 hub.ca_cert_pem.clone(),
268 );
269
270 let rogue_tls = TlsConfig::from_pem(
272 rogue.node_cert_pem.clone(),
273 rogue.node_key_pem.clone(),
274 hub.ca_cert_pem.clone(), );
276
277 hub_tls.server_config().expect("hub server_config failed");
279
280 let hub_ca_der = rustls_pemfile::certs(&mut hub.ca_cert_pem.as_bytes())
284 .collect::<Result<Vec<_>, _>>()
285 .expect("parse hub CA");
286 let rogue_cert_der = rustls_pemfile::certs(&mut rogue.node_cert_pem.as_bytes())
287 .collect::<Result<Vec<_>, _>>()
288 .expect("parse rogue cert");
289
290 let mut root_store = rustls::RootCertStore::empty();
291 root_store.add(hub_ca_der[0].clone()).expect("add hub CA");
292
293 let verifier =
295 rustls::server::WebPkiClientVerifier::builder(std::sync::Arc::new(root_store))
296 .build()
297 .expect("build verifier");
298
299 let now = rustls::pki_types::UnixTime::now();
301 let result = verifier.verify_client_cert(&rogue_cert_der[0], &[], now);
302 assert!(
303 result.is_err(),
304 "rogue cert must be rejected by hub CA verifier"
305 );
306
307 rogue_tls
310 .client_config()
311 .expect("client config builds -- rejection happens at handshake");
312 }
313
314 #[test]
317 fn test_expired_cert_rejected_at_handshake() {
318 install_crypto_provider();
319
320 let ca_key = rcgen::KeyPair::generate().expect("CA key");
321 let mut ca_params =
322 rcgen::CertificateParams::new(vec!["ZamSync CA".to_string()]).expect("CA params");
323 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
324 let ca_cert = ca_params.self_signed(&ca_key).expect("CA self-sign");
325
326 let node_key = rcgen::KeyPair::generate().expect("node key");
328 let mut node_params =
329 rcgen::CertificateParams::new(vec!["zamsync.local".to_string()]).expect("node params");
330 node_params.not_before = time::OffsetDateTime::from_unix_timestamp(0).expect("epoch start");
331 node_params.not_after =
332 time::OffsetDateTime::from_unix_timestamp(86400).expect("epoch + 1 day");
333 let ca_issuer = rcgen::Issuer::from_params(&ca_params, ca_key);
334 let expired_cert = node_params
335 .signed_by(&node_key, &ca_issuer)
336 .expect("sign expired cert");
337
338 let ca_pem = ca_cert.pem();
339 let expired_pem = expired_cert.pem();
340
341 let ca_der: Vec<_> = rustls_pemfile::certs(&mut ca_pem.as_bytes())
342 .collect::<Result<Vec<_>, _>>()
343 .expect("parse CA DER");
344 let expired_der: Vec<_> = rustls_pemfile::certs(&mut expired_pem.as_bytes())
345 .collect::<Result<Vec<_>, _>>()
346 .expect("parse expired cert DER");
347
348 let mut root_store = rustls::RootCertStore::empty();
349 root_store
350 .add(ca_der[0].clone())
351 .expect("add CA to root store");
352
353 let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
354 .build()
355 .expect("build verifier");
356
357 let now = rustls::pki_types::UnixTime::now();
359 let result = verifier.verify_client_cert(&expired_der[0], &[], now);
360 assert!(
361 result.is_err(),
362 "expired certificate must be rejected; got Ok instead"
363 );
364 }
365}