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 must not reduce offered_protection"
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!(base.has_valid_cert, "sticky: true must not become false");
323 }
324
325 #[test]
326 fn merge_supported_suites_replaces_when_update_has_any() {
327 let mut base = PeerCapabilities {
328 supported_suites: alloc::vec![SuiteHint::Aes128Gcm],
329 ..Default::default()
330 };
331 base.merge_update(&PeerCapabilities {
332 supported_suites: alloc::vec![SuiteHint::Aes256Gcm, SuiteHint::HmacSha256],
333 ..Default::default()
334 });
335 assert_eq!(
336 base.supported_suites,
337 alloc::vec![SuiteHint::Aes256Gcm, SuiteHint::HmacSha256]
338 );
339 }
340
341 #[test]
344 fn cache_new_is_empty() {
345 let c = PeerCache::new();
346 assert!(c.is_empty());
347 assert_eq!(c.len(), 0);
348 }
349
350 #[test]
351 fn cache_insert_and_get() {
352 let mut c = PeerCache::new();
353 let key: PeerKey = [1; 12];
354 c.insert(key, caps_secure());
355 assert_eq!(c.len(), 1);
356 assert_eq!(c.get(&key).unwrap(), &caps_secure());
357 }
358
359 #[test]
360 fn cache_insert_overwrites() {
361 let mut c = PeerCache::new();
362 let key: PeerKey = [2; 12];
363 c.insert(key, PeerCapabilities::default());
364 c.insert(key, caps_secure());
365 assert_eq!(c.get(&key).unwrap(), &caps_secure());
366 assert_eq!(c.len(), 1);
367 }
368
369 #[test]
370 fn cache_update_partial_inserts_when_missing() {
371 let mut c = PeerCache::new();
372 let key: PeerKey = [3; 12];
373 c.update_partial(key, &caps_secure());
374 assert_eq!(c.get(&key).unwrap(), &caps_secure());
375 }
376
377 #[test]
378 fn cache_update_partial_merges_into_existing() {
379 let mut c = PeerCache::new();
380 let key: PeerKey = [4; 12];
381 c.insert(
382 key,
383 PeerCapabilities {
384 auth_plugin_class: Some("DDS:Auth:PKI-DH:1.2".to_string()),
385 offered_protection: ProtectionLevel::Sign,
386 ..Default::default()
387 },
388 );
389 c.update_partial(
390 key,
391 &PeerCapabilities {
392 offered_protection: ProtectionLevel::Encrypt,
393 has_valid_cert: true,
394 ..Default::default()
395 },
396 );
397 let merged = c.get(&key).unwrap();
398 assert_eq!(merged.offered_protection, ProtectionLevel::Encrypt);
399 assert!(merged.has_valid_cert);
400 assert_eq!(
401 merged.auth_plugin_class.as_deref(),
402 Some("DDS:Auth:PKI-DH:1.2"),
403 "pre-existing fields are preserved"
404 );
405 }
406
407 #[test]
408 fn cache_forget_removes_and_returns_caps() {
409 let mut c = PeerCache::new();
410 let key: PeerKey = [5; 12];
411 c.insert(key, caps_secure());
412 let removed = c.forget(&key);
413 assert_eq!(removed, Some(caps_secure()));
414 assert!(c.get(&key).is_none());
415 assert!(c.is_empty());
416 }
417
418 #[test]
419 fn cache_forget_unknown_returns_none() {
420 let mut c = PeerCache::new();
421 assert!(c.forget(&[9; 12]).is_none());
422 }
423
424 #[test]
425 fn cache_iter_yields_all_entries() {
426 let mut c = PeerCache::new();
427 c.insert([1; 12], caps_secure());
428 c.insert([2; 12], PeerCapabilities::default());
429 let count = c.iter().count();
430 assert_eq!(count, 2);
431 }
432}