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 node_cert = node_params
146 .signed_by(&node_key, &ca_cert, &ca_key)
147 .map_err(|e| ZamError::Config(format!("node cert signing failed: {e}")))?;
148
149 Ok(GeneratedCredentials {
150 ca_cert_pem: ca_cert.pem(),
151 ca_key_pem: ca_key.serialize_pem(),
152 node_cert_pem: node_cert.pem(),
153 node_key_pem: node_key.serialize_pem(),
154 })
155}
156
157pub struct SignedNodeCredentials {
159 pub ca_cert_pem: String,
161 pub node_cert_pem: String,
163 pub node_key_pem: String,
165}
166
167pub fn sign_node_cert(ca_cert_pem: &str, ca_key_pem: &str) -> ZamResult<SignedNodeCredentials> {
178 let ca_key = rcgen::KeyPair::from_pem(ca_key_pem)
179 .map_err(|e| ZamError::Config(format!("parse CA key: {e}")))?;
180
181 let mut ca_params = rcgen::CertificateParams::new(vec!["ZamSync CA".to_string()])
185 .map_err(|e| ZamError::Config(format!("CA params: {e}")))?;
186 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
187
188 let ca_cert = ca_params
189 .self_signed(&ca_key)
190 .map_err(|e| ZamError::Config(format!("reconstruct CA cert: {e}")))?;
191
192 let node_key = rcgen::KeyPair::generate()
193 .map_err(|e| ZamError::Config(format!("node key generation failed: {e}")))?;
194
195 let node_params = rcgen::CertificateParams::new(vec!["zamsync.local".to_string()])
196 .map_err(|e| ZamError::Config(format!("node params: {e}")))?;
197
198 let node_cert = node_params
199 .signed_by(&node_key, &ca_cert, &ca_key)
200 .map_err(|e| ZamError::Config(format!("node cert signing failed: {e}")))?;
201
202 Ok(SignedNodeCredentials {
203 ca_cert_pem: ca_cert_pem.to_owned(),
204 node_cert_pem: node_cert.pem(),
205 node_key_pem: node_key.serialize_pem(),
206 })
207}
208
209pub fn install_crypto_provider() {
211 let _ = rustls::crypto::ring::default_provider().install_default();
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_sign_node_cert_produces_valid_chain() {
220 install_crypto_provider();
221
222 let hub = generate_credentials().expect("hub keygen failed");
223
224 let clinic_a =
226 sign_node_cert(&hub.ca_cert_pem, &hub.ca_key_pem).expect("clinic_a signing failed");
227 let clinic_b =
228 sign_node_cert(&hub.ca_cert_pem, &hub.ca_key_pem).expect("clinic_b signing failed");
229
230 assert_eq!(clinic_a.ca_cert_pem, hub.ca_cert_pem);
232 assert_eq!(clinic_b.ca_cert_pem, hub.ca_cert_pem);
233
234 assert_ne!(clinic_a.node_cert_pem, clinic_b.node_cert_pem);
236 assert_ne!(clinic_a.node_key_pem, clinic_b.node_key_pem);
237
238 let hub_tls = TlsConfig::from_pem(
240 hub.node_cert_pem.clone(),
241 hub.node_key_pem.clone(),
242 hub.ca_cert_pem.clone(),
243 );
244 let clinic_a_tls = TlsConfig::from_pem(
245 clinic_a.node_cert_pem.clone(),
246 clinic_a.node_key_pem.clone(),
247 clinic_a.ca_cert_pem.clone(),
248 );
249
250 hub_tls.server_config().expect("hub server_config failed");
252 clinic_a_tls
253 .client_config()
254 .expect("clinic_a client_config failed");
255 }
256
257 #[test]
258 fn test_rogue_node_with_own_ca_rejected() {
259 install_crypto_provider();
260
261 let hub = generate_credentials().expect("hub keygen");
262 let rogue = generate_credentials().expect("rogue keygen");
263
264 let hub_tls = TlsConfig::from_pem(
266 hub.node_cert_pem.clone(),
267 hub.node_key_pem.clone(),
268 hub.ca_cert_pem.clone(),
269 );
270
271 let rogue_tls = TlsConfig::from_pem(
273 rogue.node_cert_pem.clone(),
274 rogue.node_key_pem.clone(),
275 hub.ca_cert_pem.clone(), );
277
278 hub_tls.server_config().expect("hub server_config failed");
280
281 let hub_ca_der = rustls_pemfile::certs(&mut hub.ca_cert_pem.as_bytes())
285 .collect::<Result<Vec<_>, _>>()
286 .expect("parse hub CA");
287 let rogue_cert_der = rustls_pemfile::certs(&mut rogue.node_cert_pem.as_bytes())
288 .collect::<Result<Vec<_>, _>>()
289 .expect("parse rogue cert");
290
291 let mut root_store = rustls::RootCertStore::empty();
292 root_store.add(hub_ca_der[0].clone()).expect("add hub CA");
293
294 let verifier =
296 rustls::server::WebPkiClientVerifier::builder(std::sync::Arc::new(root_store))
297 .build()
298 .expect("build verifier");
299
300 let now = rustls::pki_types::UnixTime::now();
302 let result = verifier.verify_client_cert(&rogue_cert_der[0], &[], now);
303 assert!(
304 result.is_err(),
305 "rogue cert must be rejected by hub CA verifier"
306 );
307
308 rogue_tls
311 .client_config()
312 .expect("client config builds -- rejection happens at handshake");
313 }
314
315 #[test]
318 fn test_expired_cert_rejected_at_handshake() {
319 install_crypto_provider();
320
321 let ca_key = rcgen::KeyPair::generate().expect("CA key");
322 let mut ca_params =
323 rcgen::CertificateParams::new(vec!["ZamSync CA".to_string()]).expect("CA params");
324 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
325 let ca_cert = ca_params.self_signed(&ca_key).expect("CA self-sign");
326
327 let node_key = rcgen::KeyPair::generate().expect("node key");
329 let mut node_params =
330 rcgen::CertificateParams::new(vec!["zamsync.local".to_string()]).expect("node params");
331 node_params.not_before =
332 time::OffsetDateTime::from_unix_timestamp(0).expect("epoch start");
333 node_params.not_after =
334 time::OffsetDateTime::from_unix_timestamp(86400).expect("epoch + 1 day");
335 let expired_cert = node_params
336 .signed_by(&node_key, &ca_cert, &ca_key)
337 .expect("sign expired cert");
338
339 let ca_pem = ca_cert.pem();
340 let expired_pem = expired_cert.pem();
341
342 let ca_der: Vec<_> = rustls_pemfile::certs(&mut ca_pem.as_bytes())
343 .collect::<Result<Vec<_>, _>>()
344 .expect("parse CA DER");
345 let expired_der: Vec<_> = rustls_pemfile::certs(&mut expired_pem.as_bytes())
346 .collect::<Result<Vec<_>, _>>()
347 .expect("parse expired cert DER");
348
349 let mut root_store = rustls::RootCertStore::empty();
350 root_store.add(ca_der[0].clone()).expect("add CA to root store");
351
352 let verifier =
353 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}