zond_engine/core/models/port/
security.rs1use std::time::{Duration, SystemTime};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Security {
17 tls_version: Option<String>,
19
20 cipher_suite: Option<String>,
22
23 alpn: Vec<String>,
25
26 certificate: Option<CertificateInfo>,
28}
29
30impl Security {
31 pub fn new() -> Self {
33 Self {
34 tls_version: None,
35 cipher_suite: None,
36 alpn: Vec::new(),
37 certificate: None,
38 }
39 }
40
41 pub fn tls_version(&self) -> Option<&str> {
43 self.tls_version.as_deref()
44 }
45
46 pub fn cipher_suite(&self) -> Option<&str> {
48 self.cipher_suite.as_deref()
49 }
50
51 pub fn alpn(&self) -> &[String] {
53 &self.alpn
54 }
55
56 pub fn certificate(&self) -> Option<&CertificateInfo> {
58 self.certificate.as_ref()
59 }
60
61 pub fn with_tls_version(mut self, version: impl Into<String>) -> Self {
63 self.tls_version = Some(version.into());
64 self
65 }
66
67 pub fn with_cipher_suite(mut self, cipher: impl Into<String>) -> Self {
69 self.cipher_suite = Some(cipher.into());
70 self
71 }
72
73 pub fn add_alpn(mut self, protocol: impl Into<String>) -> Self {
75 let proto_str = protocol.into();
76 if !self.alpn.contains(&proto_str) {
77 self.alpn.push(proto_str);
78 }
79 self
80 }
81
82 pub fn with_certificate(mut self, cert: CertificateInfo) -> Self {
84 self.certificate = Some(cert);
85 self
86 }
87
88 pub fn merge(&mut self, other: Security) {
93 if self.tls_version.is_none() {
94 self.tls_version = other.tls_version;
95 }
96 if self.cipher_suite.is_none() {
97 self.cipher_suite = other.cipher_suite;
98 }
99 if self.certificate.is_none() {
100 self.certificate = other.certificate;
101 }
102
103 for protocol in other.alpn {
105 if !self.alpn.contains(&protocol) {
106 self.alpn.push(protocol);
107 }
108 }
109 }
110
111 pub fn is_cert_valid(&self) -> bool {
114 self.is_cert_valid_at(SystemTime::now())
115 }
116
117 pub fn is_cert_valid_at(&self, target_time: SystemTime) -> bool {
119 self.certificate
120 .as_ref()
121 .is_some_and(|c| target_time >= c.validity_start() && target_time <= c.validity_end())
122 }
123
124 pub fn is_cert_expiring(&self, threshold: Duration) -> bool {
137 let now = SystemTime::now();
138 self.certificate.as_ref().is_some_and(|c| {
139 if now < c.validity_start() || now > c.validity_end() {
141 return false;
142 }
143 c.validity_end() < now + threshold
145 })
146 }
147}
148
149impl Default for Security {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct CertificateInfo {
158 common_name: String,
160
161 sans: Vec<String>,
163
164 issuer: String,
166
167 validity_start: SystemTime,
169
170 validity_end: SystemTime,
172
173 pubkey_type: String,
175
176 pubkey_bits: u32,
178
179 fingerprint_sha256: String,
181}
182
183impl CertificateInfo {
184 #[allow(clippy::too_many_arguments)]
186 pub fn new(
187 common_name: impl Into<String>,
188 sans: Vec<String>,
189 issuer: impl Into<String>,
190 validity_start: SystemTime,
191 validity_end: SystemTime,
192 pubkey_type: impl Into<String>,
193 pubkey_bits: u32,
194 fingerprint_sha256: impl Into<String>,
195 ) -> Self {
196 Self {
197 common_name: common_name.into(),
198 sans,
199 issuer: issuer.into(),
200 validity_start,
201 validity_end,
202 pubkey_type: pubkey_type.into(),
203 pubkey_bits,
204 fingerprint_sha256: fingerprint_sha256.into(),
205 }
206 }
207
208 pub fn common_name(&self) -> &str {
210 &self.common_name
211 }
212
213 pub fn sans(&self) -> &[String] {
215 &self.sans
216 }
217
218 pub fn issuer(&self) -> &str {
220 &self.issuer
221 }
222
223 pub fn validity_start(&self) -> SystemTime {
225 self.validity_start
226 }
227
228 pub fn validity_end(&self) -> SystemTime {
230 self.validity_end
231 }
232
233 pub fn pubkey_type(&self) -> &str {
235 &self.pubkey_type
236 }
237
238 pub fn pubkey_bits(&self) -> u32 {
240 self.pubkey_bits
241 }
242
243 pub fn fingerprint_sha256(&self) -> &str {
245 &self.fingerprint_sha256
246 }
247}
248
249#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn mock_cert(start_offset: i64, end_offset: i64) -> CertificateInfo {
263 let now = SystemTime::now();
264
265 let start = if start_offset < 0 {
266 now - Duration::from_secs(start_offset.unsigned_abs())
267 } else {
268 now + Duration::from_secs(start_offset as u64)
269 };
270
271 let end = if end_offset < 0 {
272 now - Duration::from_secs(end_offset.unsigned_abs())
273 } else {
274 now + Duration::from_secs(end_offset as u64)
275 };
276
277 CertificateInfo::new(
278 "test.local",
279 vec!["*.test.local".into()],
280 "Local CA",
281 start,
282 end,
283 "RSA",
284 2048,
285 "deadbeef",
286 )
287 }
288
289 #[test]
290 fn security_builder_pattern() {
291 let sec = Security::new()
292 .with_tls_version("TLSv1.3")
293 .with_cipher_suite("TLS_AES_256_GCM_SHA384")
294 .add_alpn("h2")
295 .add_alpn("http/1.1");
296
297 assert_eq!(sec.tls_version(), Some("TLSv1.3"));
298 assert_eq!(sec.cipher_suite(), Some("TLS_AES_256_GCM_SHA384"));
299 assert_eq!(sec.alpn().len(), 2);
300 }
301
302 #[test]
303 fn security_merge_logic() {
304 let mut s1 = Security::new()
305 .with_tls_version("TLSv1.2")
306 .add_alpn("http/1.1");
307
308 let s2 = Security::new()
309 .with_cipher_suite("AES128-GCM")
310 .add_alpn("h2")
311 .add_alpn("http/1.1"); s1.merge(s2);
314
315 assert_eq!(s1.tls_version(), Some("TLSv1.2"));
316 assert_eq!(s1.cipher_suite(), Some("AES128-GCM"));
317 assert_eq!(s1.alpn().len(), 2);
318 assert!(s1.alpn().contains(&"h2".to_string()));
319 }
320
321 #[test]
322 fn certificate_validity_lifecycle() {
323 let valid_cert = mock_cert(-864000, 864000);
325 let sec_valid = Security::new().with_certificate(valid_cert);
326
327 assert!(sec_valid.is_cert_valid());
328 assert!(!sec_valid.is_cert_expiring(Duration::from_secs(86400 * 5)));
330 assert!(sec_valid.is_cert_expiring(Duration::from_secs(86400 * 15)));
332
333 let expired_cert = mock_cert(-864000, -432000);
335 let sec_expired = Security::new().with_certificate(expired_cert);
336
337 assert!(!sec_expired.is_cert_valid());
338 assert!(!sec_expired.is_cert_expiring(Duration::from_secs(86400 * 30)));
340
341 let future_cert = mock_cert(86400, 864000);
343 let sec_future = Security::new().with_certificate(future_cert);
344
345 assert!(!sec_future.is_cert_valid());
346 }
347}