zerodds_security_runtime/
caps.rs1use alloc::collections::BTreeMap;
23use alloc::string::String;
24use alloc::vec::Vec;
25
26use zerodds_security_pki::DelegationChain;
27
28use crate::policy::{ProtectionLevel, SuiteHint};
29use crate::shared::PeerKey;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub struct Validity {
39 pub not_before: i64,
41 pub not_after: i64,
43}
44
45impl Validity {
46 #[must_use]
48 pub const fn contains(&self, now: i64) -> bool {
49 now >= self.not_before && now <= self.not_after
50 }
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
65pub struct PeerCapabilities {
66 pub auth_plugin_class: Option<String>,
69 pub crypto_plugin_class: Option<String>,
71 pub access_plugin_class: Option<String>,
73 pub supported_suites: Vec<SuiteHint>,
75 pub offered_protection: ProtectionLevel,
77 pub has_valid_cert: bool,
80 pub validity_window: Option<Validity>,
82 pub vendor_hint: Option<String>,
85 pub cert_cn: Option<String>,
91 pub delegation_chain: Option<DelegationChain>,
97}
98
99impl PeerCapabilities {
100 pub fn merge_update(&mut self, other: &PeerCapabilities) {
112 if other.auth_plugin_class.is_some() {
113 self.auth_plugin_class = other.auth_plugin_class.clone();
114 }
115 if other.crypto_plugin_class.is_some() {
116 self.crypto_plugin_class = other.crypto_plugin_class.clone();
117 }
118 if other.access_plugin_class.is_some() {
119 self.access_plugin_class = other.access_plugin_class.clone();
120 }
121 if !other.supported_suites.is_empty() {
122 self.supported_suites = other.supported_suites.clone();
123 }
124 self.offered_protection = self.offered_protection.stronger(other.offered_protection);
125 if other.has_valid_cert {
126 self.has_valid_cert = true;
127 }
128 if other.validity_window.is_some() {
129 self.validity_window = other.validity_window;
130 }
131 if other.vendor_hint.is_some() {
132 self.vendor_hint = other.vendor_hint.clone();
133 }
134 if other.cert_cn.is_some() {
135 self.cert_cn = other.cert_cn.clone();
136 }
137 if other.delegation_chain.is_some() {
138 self.delegation_chain = other.delegation_chain.clone();
139 }
140 }
141}
142
143#[derive(Debug, Default, Clone)]
151pub struct PeerCache {
152 inner: BTreeMap<PeerKey, PeerCapabilities>,
153}
154
155impl PeerCache {
156 #[must_use]
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 #[must_use]
164 pub fn len(&self) -> usize {
165 self.inner.len()
166 }
167
168 #[must_use]
170 pub fn is_empty(&self) -> bool {
171 self.inner.is_empty()
172 }
173
174 pub fn insert(&mut self, key: PeerKey, caps: PeerCapabilities) {
178 self.inner.insert(key, caps);
179 }
180
181 #[must_use]
183 pub fn get(&self, key: &PeerKey) -> Option<&PeerCapabilities> {
184 self.inner.get(key)
185 }
186
187 pub fn update_partial(&mut self, key: PeerKey, update: &PeerCapabilities) {
192 self.inner
193 .entry(key)
194 .and_modify(|existing| existing.merge_update(update))
195 .or_insert_with(|| update.clone());
196 }
197
198 pub fn forget(&mut self, key: &PeerKey) -> Option<PeerCapabilities> {
201 self.inner.remove(key)
202 }
203
204 pub fn iter(&self) -> impl Iterator<Item = (&PeerKey, &PeerCapabilities)> {
206 self.inner.iter()
207 }
208}
209
210#[cfg(test)]
215#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
216mod tests {
217 use super::*;
218 use alloc::string::ToString;
219
220 fn caps_secure() -> PeerCapabilities {
221 PeerCapabilities {
222 auth_plugin_class: Some("DDS:Auth:PKI-DH:1.2".to_string()),
223 crypto_plugin_class: Some("DDS:Crypto:AES-GCM-GMAC:1.2".to_string()),
224 access_plugin_class: Some("DDS:Access:Permissions:1.2".to_string()),
225 supported_suites: alloc::vec![SuiteHint::Aes128Gcm, SuiteHint::Aes256Gcm],
226 offered_protection: ProtectionLevel::Encrypt,
227 has_valid_cert: true,
228 validity_window: Some(Validity {
229 not_before: 0,
230 not_after: 2_000_000_000,
231 }),
232 vendor_hint: Some("zerodds".to_string()),
233 cert_cn: Some("writer1.fast.example".to_string()),
234 delegation_chain: None,
235 }
236 }
237
238 #[test]
241 fn validity_contains_inside_window() {
242 let v = Validity {
243 not_before: 100,
244 not_after: 200,
245 };
246 assert!(v.contains(100));
247 assert!(v.contains(150));
248 assert!(v.contains(200));
249 }
250
251 #[test]
252 fn validity_rejects_outside_window() {
253 let v = Validity {
254 not_before: 100,
255 not_after: 200,
256 };
257 assert!(!v.contains(99));
258 assert!(!v.contains(201));
259 }
260
261 #[test]
264 fn merge_fills_empty_fields() {
265 let mut base = PeerCapabilities::default();
266 base.merge_update(&caps_secure());
267 assert_eq!(base, caps_secure());
268 }
269
270 #[test]
271 fn merge_preserves_existing_when_update_is_empty() {
272 let mut base = caps_secure();
273 let orig = base.clone();
274 base.merge_update(&PeerCapabilities::default());
275 assert_eq!(base, orig);
278 }
279
280 #[test]
281 fn merge_upgrades_protection_monotonically() {
282 let mut base = PeerCapabilities {
283 offered_protection: ProtectionLevel::Sign,
284 ..Default::default()
285 };
286 let upgrade = PeerCapabilities {
287 offered_protection: ProtectionLevel::Encrypt,
288 ..Default::default()
289 };
290 base.merge_update(&upgrade);
291 assert_eq!(base.offered_protection, ProtectionLevel::Encrypt);
292 }
293
294 #[test]
295 fn merge_does_not_downgrade_protection() {
296 let mut base = PeerCapabilities {
297 offered_protection: ProtectionLevel::Encrypt,
298 ..Default::default()
299 };
300 let downgrade = PeerCapabilities {
301 offered_protection: ProtectionLevel::Sign,
302 ..Default::default()
303 };
304 base.merge_update(&downgrade);
305 assert_eq!(
306 base.offered_protection,
307 ProtectionLevel::Encrypt,
308 "downgrade darf offered_protection nicht reduzieren"
309 );
310 }
311
312 #[test]
313 fn merge_has_valid_cert_is_sticky_true() {
314 let mut base = PeerCapabilities {
315 has_valid_cert: true,
316 ..Default::default()
317 };
318 base.merge_update(&PeerCapabilities {
319 has_valid_cert: false,
320 ..Default::default()
321 });
322 assert!(
323 base.has_valid_cert,
324 "sticky: true darf nicht zu false werden"
325 );
326 }
327
328 #[test]
329 fn merge_supported_suites_replaces_when_update_has_any() {
330 let mut base = PeerCapabilities {
331 supported_suites: alloc::vec![SuiteHint::Aes128Gcm],
332 ..Default::default()
333 };
334 base.merge_update(&PeerCapabilities {
335 supported_suites: alloc::vec![SuiteHint::Aes256Gcm, SuiteHint::HmacSha256],
336 ..Default::default()
337 });
338 assert_eq!(
339 base.supported_suites,
340 alloc::vec![SuiteHint::Aes256Gcm, SuiteHint::HmacSha256]
341 );
342 }
343
344 #[test]
347 fn cache_new_is_empty() {
348 let c = PeerCache::new();
349 assert!(c.is_empty());
350 assert_eq!(c.len(), 0);
351 }
352
353 #[test]
354 fn cache_insert_and_get() {
355 let mut c = PeerCache::new();
356 let key: PeerKey = [1; 12];
357 c.insert(key, caps_secure());
358 assert_eq!(c.len(), 1);
359 assert_eq!(c.get(&key).unwrap(), &caps_secure());
360 }
361
362 #[test]
363 fn cache_insert_overwrites() {
364 let mut c = PeerCache::new();
365 let key: PeerKey = [2; 12];
366 c.insert(key, PeerCapabilities::default());
367 c.insert(key, caps_secure());
368 assert_eq!(c.get(&key).unwrap(), &caps_secure());
369 assert_eq!(c.len(), 1);
370 }
371
372 #[test]
373 fn cache_update_partial_inserts_when_missing() {
374 let mut c = PeerCache::new();
375 let key: PeerKey = [3; 12];
376 c.update_partial(key, &caps_secure());
377 assert_eq!(c.get(&key).unwrap(), &caps_secure());
378 }
379
380 #[test]
381 fn cache_update_partial_merges_into_existing() {
382 let mut c = PeerCache::new();
383 let key: PeerKey = [4; 12];
384 c.insert(
385 key,
386 PeerCapabilities {
387 auth_plugin_class: Some("DDS:Auth:PKI-DH:1.2".to_string()),
388 offered_protection: ProtectionLevel::Sign,
389 ..Default::default()
390 },
391 );
392 c.update_partial(
393 key,
394 &PeerCapabilities {
395 offered_protection: ProtectionLevel::Encrypt,
396 has_valid_cert: true,
397 ..Default::default()
398 },
399 );
400 let merged = c.get(&key).unwrap();
401 assert_eq!(merged.offered_protection, ProtectionLevel::Encrypt);
402 assert!(merged.has_valid_cert);
403 assert_eq!(
404 merged.auth_plugin_class.as_deref(),
405 Some("DDS:Auth:PKI-DH:1.2"),
406 "pre-existing Felder bleiben erhalten"
407 );
408 }
409
410 #[test]
411 fn cache_forget_removes_and_returns_caps() {
412 let mut c = PeerCache::new();
413 let key: PeerKey = [5; 12];
414 c.insert(key, caps_secure());
415 let removed = c.forget(&key);
416 assert_eq!(removed, Some(caps_secure()));
417 assert!(c.get(&key).is_none());
418 assert!(c.is_empty());
419 }
420
421 #[test]
422 fn cache_forget_unknown_returns_none() {
423 let mut c = PeerCache::new();
424 assert!(c.forget(&[9; 12]).is_none());
425 }
426
427 #[test]
428 fn cache_iter_yields_all_entries() {
429 let mut c = PeerCache::new();
430 c.insert([1; 12], caps_secure());
431 c.insert([2; 12], PeerCapabilities::default());
432 let count = c.iter().count();
433 assert_eq!(count, 2);
434 }
435}