1use alloc::boxed::Box;
20use alloc::collections::BTreeMap;
21use alloc::vec::Vec;
22use std::sync::{Arc, Mutex, PoisonError};
23
24use zerodds_security::authentication::{IdentityHandle, SharedSecretHandle};
25use zerodds_security::crypto::{CryptoHandle, CryptographicPlugin};
26use zerodds_security_permissions::{Governance, ProtectionKind};
27use zerodds_security_rtps::{
28 RTPS_HEADER_LEN, SRTPS_PREFIX, decode_secured_rtps_message, decode_secured_submessage_multi,
29 encode_secured_rtps_message, encode_secured_submessage_multi,
30};
31
32use crate::gate::SecurityGateError;
33use crate::policy::{NetInterface, ProtectionLevel};
34
35#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum InboundVerdict {
50 Accept(Vec<u8>),
53 Malformed,
57 LegacyBlocked,
61 PolicyViolation(String),
65 CryptoError(String),
68}
69
70impl InboundVerdict {
71 #[must_use]
73 pub const fn is_accept(&self) -> bool {
74 matches!(self, Self::Accept(_))
75 }
76
77 #[must_use]
81 pub fn category(&self) -> &'static str {
82 match self {
83 Self::Accept(_) => "inbound.accept",
84 Self::Malformed => "inbound.malformed",
85 Self::LegacyBlocked => "inbound.legacy_blocked",
86 Self::PolicyViolation(_) => "inbound.policy_violation",
87 Self::CryptoError(_) => "inbound.crypto_error",
88 }
89 }
90}
91
92pub type PeerKey = [u8; 12];
95
96#[derive(Clone)]
100pub struct SharedSecurityGate {
101 inner: Arc<Mutex<GateInner>>,
102}
103
104impl core::fmt::Debug for SharedSecurityGate {
105 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
106 match self.inner.lock() {
108 Ok(g) => write!(
109 f,
110 "SharedSecurityGate {{ domain_id: {}, peers: {}, local_registered: {} }}",
111 g.domain_id,
112 g.peers.len(),
113 g.local.is_some()
114 ),
115 Err(_) => write!(f, "SharedSecurityGate {{ <poisoned> }}"),
116 }
117 }
118}
119
120struct GateInner {
121 domain_id: u32,
122 governance: Governance,
123 crypto: Box<dyn CryptographicPlugin>,
124 local: Option<CryptoHandle>,
125 peers: BTreeMap<PeerKey, CryptoHandle>,
129}
130
131#[must_use]
139pub fn peer_key_to_id(pk: &PeerKey) -> u32 {
140 let mut buf = [0u8; 4];
141 buf.copy_from_slice(&pk[..4]);
142 u32::from_le_bytes(buf)
143}
144
145fn poisoned<T>(_: PoisonError<T>) -> SecurityGateError {
146 SecurityGateError::Crypto(zerodds_security::error::SecurityError::new(
147 zerodds_security::error::SecurityErrorKind::Internal,
148 "security-runtime: mutex poisoned",
149 ))
150}
151
152impl SharedSecurityGate {
153 #[must_use]
155 pub fn new(
156 domain_id: u32,
157 governance: Governance,
158 crypto: Box<dyn CryptographicPlugin>,
159 ) -> Self {
160 Self {
161 inner: Arc::new(Mutex::new(GateInner {
162 domain_id,
163 governance,
164 crypto,
165 local: None,
166 peers: BTreeMap::new(),
167 })),
168 }
169 }
170
171 fn with_inner<R>(
172 &self,
173 f: impl FnOnce(&mut GateInner) -> Result<R, SecurityGateError>,
174 ) -> Result<R, SecurityGateError> {
175 let mut g = self.inner.lock().map_err(poisoned)?;
176 f(&mut g)
177 }
178
179 pub fn domain_id(&self) -> Result<u32, SecurityGateError> {
181 self.with_inner(|g| Ok(g.domain_id))
182 }
183
184 pub fn message_protection(&self) -> Result<ProtectionKind, SecurityGateError> {
187 self.with_inner(|g| {
188 Ok(g.governance
189 .find_domain_rule(g.domain_id)
190 .map(|r| r.rtps_protection_kind)
191 .unwrap_or(ProtectionKind::None))
192 })
193 }
194
195 pub fn ensure_local(&self) -> Result<CryptoHandle, SecurityGateError> {
200 self.with_inner(|g| {
201 if let Some(h) = g.local {
202 return Ok(h);
203 }
204 let h = g
205 .crypto
206 .register_local_participant(IdentityHandle(1), &[])
207 .map_err(SecurityGateError::CryptoSetup)?;
208 g.local = Some(h);
209 Ok(h)
210 })
211 }
212
213 pub fn local_token(&self) -> Result<Vec<u8>, SecurityGateError> {
218 let local = self.ensure_local()?;
219 self.with_inner(|g| {
220 g.crypto
221 .create_local_participant_crypto_tokens(local, CryptoHandle(0))
222 .map_err(SecurityGateError::Crypto)
223 })
224 }
225
226 pub fn register_remote_with_token(
234 &self,
235 remote_identity: IdentityHandle,
236 shared_secret: SharedSecretHandle,
237 token: &[u8],
238 ) -> Result<CryptoHandle, SecurityGateError> {
239 let local = self.ensure_local()?;
240 self.with_inner(|g| {
241 let slot = g
242 .crypto
243 .register_matched_remote_participant(local, remote_identity, shared_secret)
244 .map_err(SecurityGateError::CryptoSetup)?;
245 g.crypto
246 .set_remote_participant_crypto_tokens(local, slot, token)
247 .map_err(SecurityGateError::Crypto)?;
248 Ok(slot)
249 })
250 }
251
252 pub fn register_remote_by_guid(
263 &self,
264 peer_key: PeerKey,
265 remote_identity: IdentityHandle,
266 shared_secret: SharedSecretHandle,
267 token: &[u8],
268 ) -> Result<CryptoHandle, SecurityGateError> {
269 {
272 let g = self.inner.lock().map_err(poisoned)?;
273 if let Some(h) = g.peers.get(&peer_key) {
274 return Ok(*h);
275 }
276 }
277 let slot = self.register_remote_with_token(remote_identity, shared_secret, token)?;
278 self.with_inner(|g| {
279 g.peers.insert(peer_key, slot);
280 Ok(())
281 })?;
282 Ok(slot)
283 }
284
285 pub fn forget_remote(&self, peer_key: &PeerKey) -> Result<(), SecurityGateError> {
288 self.with_inner(|g| {
289 g.peers.remove(peer_key);
290 Ok(())
291 })
292 }
293
294 pub fn slot_for(&self, peer_key: &PeerKey) -> Result<Option<CryptoHandle>, SecurityGateError> {
296 self.with_inner(|g| Ok(g.peers.get(peer_key).copied()))
297 }
298
299 pub fn transform_inbound_from(
310 &self,
311 peer_key: &PeerKey,
312 wire: &[u8],
313 ) -> Result<Vec<u8>, SecurityGateError> {
314 let looks_secured = wire.len() > RTPS_HEADER_LEN && wire[RTPS_HEADER_LEN] == SRTPS_PREFIX;
315 let kind = self.message_protection()?;
316 if !looks_secured {
317 return if matches!(kind, ProtectionKind::None) {
320 Ok(wire.to_vec())
321 } else {
322 Err(SecurityGateError::PolicyViolation(alloc::format!(
323 "domain verlangt {kind:?}, bekam plain-rtps-message"
324 )))
325 };
326 }
327 let slot = self.slot_for(peer_key)?.ok_or_else(|| {
328 SecurityGateError::PolicyViolation(alloc::format!(
329 "unbekannter peer {peer_key:?} sendet SRTPS_PREFIX"
330 ))
331 })?;
332 self.transform_inbound(slot, wire)
333 }
334
335 pub fn transform_outbound(&self, message: &[u8]) -> Result<Vec<u8>, SecurityGateError> {
340 match self.message_protection()? {
341 ProtectionKind::None => Ok(message.to_vec()),
342 _ => {
343 let local = self.ensure_local()?;
344 self.with_inner(|g| {
345 encode_secured_rtps_message(&*g.crypto, local, &[], message)
346 .map_err(SecurityGateError::from)
347 })
348 }
349 }
350 }
351
352 pub fn transform_outbound_group(
369 &self,
370 peer_keys: &[PeerKey],
371 plaintext: &[u8],
372 ) -> Result<Vec<u8>, SecurityGateError> {
373 let local = self.ensure_local()?;
374 let bindings: Vec<(CryptoHandle, u32)> = self.with_inner(|g| {
381 let mut out = Vec::with_capacity(peer_keys.len());
382 for pk in peer_keys {
383 let h = g.peers.get(pk).copied().ok_or_else(|| {
384 SecurityGateError::PolicyViolation(alloc::format!(
385 "transform_outbound_group: peer {pk:?} not registered"
386 ))
387 })?;
388 out.push((h, peer_key_to_id(pk)));
389 }
390 Ok(out)
391 })?;
392 self.with_inner(|g| {
393 encode_secured_submessage_multi(&*g.crypto, local, &bindings, plaintext)
394 .map_err(SecurityGateError::from)
395 })
396 }
397
398 pub fn transform_inbound_group(
411 &self,
412 sender_peer_key: &PeerKey,
413 own_peer_key: &PeerKey,
414 wire: &[u8],
415 ) -> Result<Vec<u8>, SecurityGateError> {
416 let sender_slot = self.slot_for(sender_peer_key)?.ok_or_else(|| {
417 SecurityGateError::PolicyViolation(alloc::format!(
418 "transform_inbound_group: unknown sender {sender_peer_key:?}"
419 ))
420 })?;
421 let own_local = self.ensure_local()?;
425 let own_id = peer_key_to_id(own_peer_key);
426 self.with_inner(|g| {
427 decode_secured_submessage_multi(
428 &*g.crypto,
429 sender_slot,
430 sender_slot,
431 own_id,
432 own_local,
433 wire,
434 )
435 .map_err(SecurityGateError::from)
436 })
437 }
438
439 pub fn transform_outbound_for(
461 &self,
462 _peer_key: &PeerKey,
463 message: &[u8],
464 level: ProtectionLevel,
465 ) -> Result<Vec<u8>, SecurityGateError> {
466 match level {
467 ProtectionLevel::None => Ok(message.to_vec()),
468 ProtectionLevel::Sign | ProtectionLevel::Encrypt => {
469 let local = self.ensure_local()?;
470 self.with_inner(|g| {
471 encode_secured_rtps_message(&*g.crypto, local, &[], message)
472 .map_err(SecurityGateError::from)
473 })
474 }
475 }
476 }
477
478 pub fn allow_unauthenticated(&self) -> Result<bool, SecurityGateError> {
482 self.with_inner(|g| {
483 Ok(g.governance
484 .find_domain_rule(g.domain_id)
485 .map(|r| r.allow_unauthenticated_participants)
486 .unwrap_or(false))
487 })
488 }
489
490 #[must_use]
515 pub fn classify_inbound(&self, bytes: &[u8], iface: &NetInterface) -> InboundVerdict {
516 if bytes.len() < RTPS_HEADER_LEN + 8 {
517 return InboundVerdict::Malformed;
518 }
519 let mut peer_key = [0u8; 12];
520 peer_key.copy_from_slice(&bytes[8..20]);
521
522 let looks_secured = bytes.len() > RTPS_HEADER_LEN && bytes[RTPS_HEADER_LEN] == SRTPS_PREFIX;
523 let kind = match self.message_protection() {
524 Ok(k) => k,
525 Err(e) => {
526 return InboundVerdict::CryptoError(alloc::format!("gate lookup failed: {e:?}"));
527 }
528 };
529
530 if looks_secured {
531 return match self.transform_inbound_from(&peer_key, bytes) {
532 Ok(clear) => InboundVerdict::Accept(clear),
533 Err(SecurityGateError::PolicyViolation(msg)) => {
534 InboundVerdict::PolicyViolation(msg)
535 }
536 Err(e) => InboundVerdict::CryptoError(alloc::format!("{e:?}")),
537 };
538 }
539
540 if matches!(kind, ProtectionKind::None) {
542 return InboundVerdict::Accept(bytes.to_vec());
543 }
544 if matches!(iface, NetInterface::Loopback | NetInterface::LocalHost) {
548 return InboundVerdict::Accept(bytes.to_vec());
549 }
550 match self.allow_unauthenticated() {
553 Ok(true) => InboundVerdict::Accept(bytes.to_vec()),
554 Ok(false) => InboundVerdict::LegacyBlocked,
555 Err(e) => InboundVerdict::CryptoError(alloc::format!("gate lookup failed: {e:?}")),
556 }
557 }
558
559 pub fn transform_inbound(
568 &self,
569 remote_slot: CryptoHandle,
570 wire: &[u8],
571 ) -> Result<Vec<u8>, SecurityGateError> {
572 let looks_secured = wire.len() > RTPS_HEADER_LEN && wire[RTPS_HEADER_LEN] == SRTPS_PREFIX;
573 let kind = self.message_protection()?;
574 match (kind, looks_secured) {
575 (ProtectionKind::None, false) => Ok(wire.to_vec()),
576 (_, true) => self.with_inner(|g| {
577 decode_secured_rtps_message(&*g.crypto, remote_slot, remote_slot, wire)
578 .map_err(SecurityGateError::from)
579 }),
580 (_, false) => Err(SecurityGateError::PolicyViolation(alloc::format!(
581 "domain verlangt {kind:?}, bekam plain-rtps-message"
582 ))),
583 }
584 }
585}
586
587#[cfg(test)]
588#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
589mod tests {
590 use super::*;
591 use std::thread;
592 use zerodds_security_crypto::AesGcmCryptoPlugin;
593 use zerodds_security_permissions::parse_governance_xml;
594
595 const GOV_RTPS: &str = r#"
596<domain_access_rules>
597 <domain_rule>
598 <domains><id>0</id></domains>
599 <rtps_protection_kind>ENCRYPT</rtps_protection_kind>
600 <topic_access_rules><topic_rule><topic_expression>*</topic_expression></topic_rule></topic_access_rules>
601 </domain_rule>
602</domain_access_rules>
603"#;
604
605 fn fake_msg(body: &[u8]) -> Vec<u8> {
606 let mut m = Vec::with_capacity(20 + body.len());
607 m.extend_from_slice(b"RTPS\x02\x05\x01\x02");
608 m.extend_from_slice(&[0u8; 12]);
609 m.extend_from_slice(body);
610 m
611 }
612
613 #[test]
614 fn outbound_none_is_passthrough() {
615 let gov = parse_governance_xml(
616 r#"<domain_access_rules><domain_rule><domains><id>0</id></domains><topic_access_rules><topic_rule><topic_expression>*</topic_expression></topic_rule></topic_access_rules></domain_rule></domain_access_rules>"#,
617 )
618 .unwrap();
619 let gate = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
620 let msg = fake_msg(b"x");
621 assert_eq!(gate.transform_outbound(&msg).unwrap(), msg);
622 }
623
624 #[test]
625 fn e2e_alice_bob_with_shared_gate() {
626 let alice = SharedSecurityGate::new(
627 0,
628 parse_governance_xml(GOV_RTPS).unwrap(),
629 Box::new(AesGcmCryptoPlugin::new()),
630 );
631 let bob = SharedSecurityGate::new(
632 0,
633 parse_governance_xml(GOV_RTPS).unwrap(),
634 Box::new(AesGcmCryptoPlugin::new()),
635 );
636 let alice_token = alice.local_token().unwrap();
637 let bob_view_of_alice = bob
638 .register_remote_with_token(IdentityHandle(1), SharedSecretHandle(1), &alice_token)
639 .unwrap();
640
641 let plain = fake_msg(b"[DATA:shared]");
642 let wire = alice.transform_outbound(&plain).unwrap();
643 let back = bob.transform_inbound(bob_view_of_alice, &wire).unwrap();
644 assert_eq!(back, plain);
645 }
646
647 #[test]
648 fn clone_shares_same_plugin_instance() {
649 let gate1 = SharedSecurityGate::new(
653 0,
654 parse_governance_xml(GOV_RTPS).unwrap(),
655 Box::new(AesGcmCryptoPlugin::new()),
656 );
657 let gate2 = gate1.clone();
658 let t1 = gate1.local_token().unwrap();
659 let t2 = gate2.local_token().unwrap();
660 assert_eq!(t1, t2, "beide Clones sehen den gleichen lokalen Slot");
661 }
662
663 #[test]
664 fn concurrent_transform_is_thread_safe() {
665 let alice = SharedSecurityGate::new(
666 0,
667 parse_governance_xml(GOV_RTPS).unwrap(),
668 Box::new(AesGcmCryptoPlugin::new()),
669 );
670 let mut handles = Vec::new();
671 for i in 0..8 {
672 let g = alice.clone();
673 handles.push(thread::spawn(move || {
674 let m = fake_msg(alloc::format!("[DATA:{i}]").as_bytes());
675 let _ = g.transform_outbound(&m).unwrap();
678 }));
679 }
680 for h in handles {
681 h.join().unwrap();
682 }
683 }
684
685 #[test]
686 fn plain_inbound_on_protected_domain_is_policy_violation() {
687 let gate = SharedSecurityGate::new(
688 0,
689 parse_governance_xml(GOV_RTPS).unwrap(),
690 Box::new(AesGcmCryptoPlugin::new()),
691 );
692 let plain = fake_msg(b"nope");
693 let err = gate
694 .transform_inbound(CryptoHandle(99), &plain)
695 .unwrap_err();
696 assert!(matches!(err, SecurityGateError::PolicyViolation(_)));
697 }
698
699 #[test]
700 fn domain_id_reflects_constructor() {
701 let gate = SharedSecurityGate::new(
702 7,
703 parse_governance_xml(GOV_RTPS).unwrap(),
704 Box::new(AesGcmCryptoPlugin::new()),
705 );
706 assert_eq!(gate.domain_id().unwrap(), 7);
707 }
708
709 fn build_pair() -> (SharedSecurityGate, SharedSecurityGate) {
714 let alice = SharedSecurityGate::new(
715 0,
716 parse_governance_xml(GOV_RTPS).unwrap(),
717 Box::new(AesGcmCryptoPlugin::new()),
718 );
719 let bob = SharedSecurityGate::new(
720 0,
721 parse_governance_xml(GOV_RTPS).unwrap(),
722 Box::new(AesGcmCryptoPlugin::new()),
723 );
724 (alice, bob)
725 }
726
727 #[test]
728 fn register_remote_by_guid_is_idempotent() {
729 let (alice, bob) = build_pair();
730 let alice_prefix: PeerKey = [0xAA; 12];
731 let atoken = alice.local_token().unwrap();
732 let slot1 = bob
733 .register_remote_by_guid(
734 alice_prefix,
735 IdentityHandle(1),
736 SharedSecretHandle(1),
737 &atoken,
738 )
739 .unwrap();
740 let slot2 = bob
741 .register_remote_by_guid(
742 alice_prefix,
743 IdentityHandle(1),
744 SharedSecretHandle(1),
745 &atoken,
746 )
747 .unwrap();
748 assert_eq!(
749 slot1, slot2,
750 "idempotent: gleicher guid-prefix → gleicher slot"
751 );
752 }
753
754 #[test]
755 fn transform_inbound_from_looks_up_slot_by_guid() {
756 let (alice, bob) = build_pair();
757 let alice_prefix: PeerKey = [0xAA; 12];
758 let atoken = alice.local_token().unwrap();
759 bob.register_remote_by_guid(
760 alice_prefix,
761 IdentityHandle(1),
762 SharedSecretHandle(1),
763 &atoken,
764 )
765 .unwrap();
766
767 let msg = fake_msg(b"[DATA:guid-lookup]");
768 let wire = alice.transform_outbound(&msg).unwrap();
769 let back = bob.transform_inbound_from(&alice_prefix, &wire).unwrap();
770 assert_eq!(back, msg);
771 }
772
773 #[test]
774 fn transform_inbound_from_unknown_peer_is_policy_violation() {
775 let (alice, bob) = build_pair();
776 let msg = fake_msg(b"x");
778 let wire = alice.transform_outbound(&msg).unwrap();
779 let err = bob.transform_inbound_from(&[0xCC; 12], &wire).unwrap_err();
780 assert!(matches!(err, SecurityGateError::PolicyViolation(_)));
781 }
782
783 #[test]
784 fn multi_peer_mapping_routes_correctly() {
785 let gov = parse_governance_xml(GOV_RTPS).unwrap();
788 let alice = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
789 let charlie = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
790 let bob = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
791
792 let alice_prefix: PeerKey = [1u8; 12];
793 let charlie_prefix: PeerKey = [3u8; 12];
794
795 bob.register_remote_by_guid(
796 alice_prefix,
797 IdentityHandle(1),
798 SharedSecretHandle(1),
799 &alice.local_token().unwrap(),
800 )
801 .unwrap();
802 bob.register_remote_by_guid(
803 charlie_prefix,
804 IdentityHandle(3),
805 SharedSecretHandle(3),
806 &charlie.local_token().unwrap(),
807 )
808 .unwrap();
809
810 let m_alice = fake_msg(b"from-alice");
811 let m_charlie = fake_msg(b"from-charlie");
812 let w_alice = alice.transform_outbound(&m_alice).unwrap();
813 let w_charlie = charlie.transform_outbound(&m_charlie).unwrap();
814
815 assert_eq!(
816 bob.transform_inbound_from(&alice_prefix, &w_alice).unwrap(),
817 m_alice
818 );
819 assert_eq!(
820 bob.transform_inbound_from(&charlie_prefix, &w_charlie)
821 .unwrap(),
822 m_charlie
823 );
824 }
825
826 #[test]
827 fn wrong_prefix_fails_tag_verify() {
828 let (alice, bob) = build_pair();
831 let gov = parse_governance_xml(GOV_RTPS).unwrap();
832 let charlie = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
833
834 let alice_prefix: PeerKey = [1u8; 12];
835 let charlie_prefix: PeerKey = [3u8; 12];
836 bob.register_remote_by_guid(
837 alice_prefix,
838 IdentityHandle(1),
839 SharedSecretHandle(1),
840 &alice.local_token().unwrap(),
841 )
842 .unwrap();
843 bob.register_remote_by_guid(
844 charlie_prefix,
845 IdentityHandle(3),
846 SharedSecretHandle(3),
847 &charlie.local_token().unwrap(),
848 )
849 .unwrap();
850
851 let msg = fake_msg(b"from-alice");
852 let wire = alice.transform_outbound(&msg).unwrap();
853 let err = bob
855 .transform_inbound_from(&charlie_prefix, &wire)
856 .unwrap_err();
857 assert!(matches!(
858 err,
859 SecurityGateError::Wrapper(_) | SecurityGateError::Crypto(_)
860 ));
861 }
862
863 #[test]
868 fn group_transform_one_ciphertext_three_macs_each_reader_decodes() {
869 let gov = parse_governance_xml(GOV_RTPS).unwrap();
873 let alice = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
874 let bob = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
875 let charlie = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
876 let dave = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
877
878 let bob_prefix: PeerKey = [0xB1; 12];
879 let charlie_prefix: PeerKey = [0xC1; 12];
880 let dave_prefix: PeerKey = [0xD1; 12];
881 let alice_prefix: PeerKey = [0xA1; 12];
882
883 alice
886 .register_remote_by_guid(
887 bob_prefix,
888 IdentityHandle(1),
889 SharedSecretHandle(1),
890 &bob.local_token().unwrap(),
891 )
892 .unwrap();
893 alice
894 .register_remote_by_guid(
895 charlie_prefix,
896 IdentityHandle(2),
897 SharedSecretHandle(2),
898 &charlie.local_token().unwrap(),
899 )
900 .unwrap();
901 alice
902 .register_remote_by_guid(
903 dave_prefix,
904 IdentityHandle(3),
905 SharedSecretHandle(3),
906 &dave.local_token().unwrap(),
907 )
908 .unwrap();
909
910 for recv in [&bob, &charlie, &dave] {
914 recv.register_remote_by_guid(
915 alice_prefix,
916 IdentityHandle(10),
917 SharedSecretHandle(10),
918 &alice.local_token().unwrap(),
919 )
920 .unwrap();
921 }
922
923 let plain = b"hetero-broadcast-e2e";
925 let wire = alice
926 .transform_outbound_group(&[bob_prefix, charlie_prefix, dave_prefix], plain)
927 .unwrap();
928
929 let out_bob = bob
932 .transform_inbound_group(&alice_prefix, &bob_prefix, &wire)
933 .unwrap();
934 let out_charlie = charlie
935 .transform_inbound_group(&alice_prefix, &charlie_prefix, &wire)
936 .unwrap();
937 let out_dave = dave
938 .transform_inbound_group(&alice_prefix, &dave_prefix, &wire)
939 .unwrap();
940 assert_eq!(out_bob, plain);
941 assert_eq!(out_charlie, plain);
942 assert_eq!(out_dave, plain);
943 }
944
945 #[test]
946 fn group_transform_rogue_receiver_without_mac_rejects() {
947 let gov = parse_governance_xml(GOV_RTPS).unwrap();
953 let alice = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
954 let bob = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
955 let eve = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
956
957 let alice_prefix: PeerKey = [0xA2; 12];
958 let bob_prefix: PeerKey = [0xB2; 12];
959 alice
960 .register_remote_by_guid(
961 bob_prefix,
962 IdentityHandle(1),
963 SharedSecretHandle(1),
964 &bob.local_token().unwrap(),
965 )
966 .unwrap();
967 eve.register_remote_by_guid(
969 alice_prefix,
970 IdentityHandle(10),
971 SharedSecretHandle(10),
972 &alice.local_token().unwrap(),
973 )
974 .unwrap();
975
976 let wire = alice
977 .transform_outbound_group(&[bob_prefix], b"confidential")
978 .unwrap();
979
980 let eve_prefix: PeerKey = [0xEE; 12];
981 let err = eve
982 .transform_inbound_group(&alice_prefix, &eve_prefix, &wire)
983 .unwrap_err();
984 assert!(
985 matches!(
986 err,
987 SecurityGateError::Crypto(_) | SecurityGateError::Wrapper(_)
988 ),
989 "Eve ohne MAC-Entry muss droppen, got: {err:?}"
990 );
991 }
992
993 #[test]
994 fn group_transform_unknown_peer_is_policy_violation() {
995 let alice = SharedSecurityGate::new(
998 0,
999 parse_governance_xml(GOV_RTPS).unwrap(),
1000 Box::new(AesGcmCryptoPlugin::new()),
1001 );
1002 let unregistered: PeerKey = [0x99; 12];
1003 let err = alice
1004 .transform_outbound_group(&[unregistered], b"x")
1005 .unwrap_err();
1006 assert!(matches!(err, SecurityGateError::PolicyViolation(_)));
1007 }
1008
1009 #[test]
1010 fn forget_remote_removes_mapping() {
1011 let (alice, bob) = build_pair();
1012 let alice_prefix: PeerKey = [0xAA; 12];
1013 bob.register_remote_by_guid(
1014 alice_prefix,
1015 IdentityHandle(1),
1016 SharedSecretHandle(1),
1017 &alice.local_token().unwrap(),
1018 )
1019 .unwrap();
1020 assert!(bob.slot_for(&alice_prefix).unwrap().is_some());
1021 bob.forget_remote(&alice_prefix).unwrap();
1022 assert!(bob.slot_for(&alice_prefix).unwrap().is_none());
1023 }
1024
1025 #[test]
1030 fn transform_outbound_for_none_is_passthrough_even_on_protected_domain() {
1031 let gate = SharedSecurityGate::new(
1035 0,
1036 parse_governance_xml(GOV_RTPS).unwrap(),
1037 Box::new(AesGcmCryptoPlugin::new()),
1038 );
1039 let peer_key: PeerKey = [0xBB; 12];
1040 let msg = fake_msg(b"[plain-for-legacy]");
1041 let out = gate
1042 .transform_outbound_for(&peer_key, &msg, ProtectionLevel::None)
1043 .unwrap();
1044 assert_eq!(out, msg, "None-Level muss byte-identisch passthrough sein");
1045 }
1046
1047 #[test]
1048 fn transform_outbound_for_encrypt_produces_srtps_wire() {
1049 let gate = SharedSecurityGate::new(
1050 0,
1051 parse_governance_xml(GOV_RTPS).unwrap(),
1052 Box::new(AesGcmCryptoPlugin::new()),
1053 );
1054 let peer_key: PeerKey = [0xCC; 12];
1055 let msg = fake_msg(b"[enc-for-secure]");
1056 let wire = gate
1057 .transform_outbound_for(&peer_key, &msg, ProtectionLevel::Encrypt)
1058 .unwrap();
1059 assert!(wire.len() > msg.len());
1062 assert_eq!(wire[RTPS_HEADER_LEN], SRTPS_PREFIX);
1063 }
1064
1065 #[test]
1066 fn transform_outbound_for_sign_also_uses_srtps_encoder() {
1067 let gate = SharedSecurityGate::new(
1070 0,
1071 parse_governance_xml(GOV_RTPS).unwrap(),
1072 Box::new(AesGcmCryptoPlugin::new()),
1073 );
1074 let peer_key: PeerKey = [0xDD; 12];
1075 let msg = fake_msg(b"[sig-for-fast]");
1076 let wire = gate
1077 .transform_outbound_for(&peer_key, &msg, ProtectionLevel::Sign)
1078 .unwrap();
1079 assert_ne!(wire, msg, "Sign darf nicht byte-identisch zu plain sein");
1080 assert_eq!(wire[RTPS_HEADER_LEN], SRTPS_PREFIX);
1081 }
1082
1083 #[test]
1084 fn transform_outbound_for_heterogeneous_three_readers() {
1085 let gate = SharedSecurityGate::new(
1088 0,
1089 parse_governance_xml(GOV_RTPS).unwrap(),
1090 Box::new(AesGcmCryptoPlugin::new()),
1091 );
1092 let msg = fake_msg(b"[broadcast]");
1093 let legacy = gate
1094 .transform_outbound_for(&[1; 12], &msg, ProtectionLevel::None)
1095 .unwrap();
1096 let fast = gate
1097 .transform_outbound_for(&[2; 12], &msg, ProtectionLevel::Sign)
1098 .unwrap();
1099 let secure = gate
1100 .transform_outbound_for(&[3; 12], &msg, ProtectionLevel::Encrypt)
1101 .unwrap();
1102 assert_eq!(legacy, msg, "Legacy-Reader bekommt plain");
1103 assert_ne!(fast, msg, "Fast-Reader bekommt SRTPS-wrapped");
1104 assert_ne!(secure, msg, "Secure-Reader bekommt SRTPS-wrapped");
1105 assert_ne!(fast, secure, "Per-Reader-Encoding muss je verschieden sein");
1109 }
1110
1111 const GOV_NONE: &str = r#"
1116<domain_access_rules>
1117 <domain_rule>
1118 <domains><id>0</id></domains>
1119 <rtps_protection_kind>NONE</rtps_protection_kind>
1120 <topic_access_rules><topic_rule><topic_expression>*</topic_expression></topic_rule></topic_access_rules>
1121 </domain_rule>
1122</domain_access_rules>
1123"#;
1124 const GOV_ENCRYPT_ALLOW_UNAUTH: &str = r#"
1125<domain_access_rules>
1126 <domain_rule>
1127 <domains><id>0</id></domains>
1128 <allow_unauthenticated_participants>TRUE</allow_unauthenticated_participants>
1129 <rtps_protection_kind>ENCRYPT</rtps_protection_kind>
1130 <topic_access_rules><topic_rule><topic_expression>*</topic_expression></topic_rule></topic_access_rules>
1131 </domain_rule>
1132</domain_access_rules>
1133"#;
1134
1135 #[test]
1136 fn allow_unauthenticated_default_false_without_element() {
1137 let gate = SharedSecurityGate::new(
1138 0,
1139 parse_governance_xml(GOV_RTPS).unwrap(),
1140 Box::new(AesGcmCryptoPlugin::new()),
1141 );
1142 assert!(!gate.allow_unauthenticated().unwrap());
1143 }
1144
1145 #[test]
1146 fn allow_unauthenticated_reads_true_when_set() {
1147 let gate = SharedSecurityGate::new(
1148 0,
1149 parse_governance_xml(GOV_ENCRYPT_ALLOW_UNAUTH).unwrap(),
1150 Box::new(AesGcmCryptoPlugin::new()),
1151 );
1152 assert!(gate.allow_unauthenticated().unwrap());
1153 }
1154
1155 #[test]
1156 fn allow_unauthenticated_defaults_false_for_unknown_domain() {
1157 let gate = SharedSecurityGate::new(
1158 99,
1159 parse_governance_xml(GOV_RTPS).unwrap(),
1160 Box::new(AesGcmCryptoPlugin::new()),
1161 );
1162 assert!(!gate.allow_unauthenticated().unwrap());
1163 }
1164
1165 #[test]
1166 fn classify_inbound_rejects_truncated_datagram() {
1167 let gate = SharedSecurityGate::new(
1168 0,
1169 parse_governance_xml(GOV_NONE).unwrap(),
1170 Box::new(AesGcmCryptoPlugin::new()),
1171 );
1172 let verdict = gate.classify_inbound(&[0u8; 10], &NetInterface::Wan);
1173 assert_eq!(verdict, InboundVerdict::Malformed);
1174 assert_eq!(verdict.category(), "inbound.malformed");
1175 }
1176
1177 #[test]
1178 fn classify_inbound_plain_on_none_domain_accepts() {
1179 let gate = SharedSecurityGate::new(
1180 0,
1181 parse_governance_xml(GOV_NONE).unwrap(),
1182 Box::new(AesGcmCryptoPlugin::new()),
1183 );
1184 let msg = fake_msg(b"[plain-hello]");
1185 match gate.classify_inbound(&msg, &NetInterface::Wan) {
1186 InboundVerdict::Accept(out) => assert_eq!(out, msg),
1187 other => panic!("expected Accept, got {other:?}"),
1188 }
1189 }
1190
1191 #[test]
1192 fn classify_inbound_plain_on_protected_domain_is_legacy_blocked() {
1193 let gate = SharedSecurityGate::new(
1195 0,
1196 parse_governance_xml(GOV_RTPS).unwrap(),
1197 Box::new(AesGcmCryptoPlugin::new()),
1198 );
1199 let msg = fake_msg(b"[legacy-on-encrypted]");
1200 let verdict = gate.classify_inbound(&msg, &NetInterface::Wan);
1201 assert_eq!(verdict, InboundVerdict::LegacyBlocked);
1202 assert_eq!(verdict.category(), "inbound.legacy_blocked");
1203 assert!(!verdict.is_accept());
1204 }
1205
1206 #[test]
1207 fn classify_inbound_plain_on_protected_domain_with_allow_unauth_accepts() {
1208 let gate = SharedSecurityGate::new(
1211 0,
1212 parse_governance_xml(GOV_ENCRYPT_ALLOW_UNAUTH).unwrap(),
1213 Box::new(AesGcmCryptoPlugin::new()),
1214 );
1215 let msg = fake_msg(b"[legacy-allowed]");
1216 match gate.classify_inbound(&msg, &NetInterface::Wan) {
1217 InboundVerdict::Accept(out) => assert_eq!(out, msg),
1218 other => panic!("expected Accept (allow_unauthenticated=true), got {other:?}"),
1219 }
1220 }
1221
1222 #[test]
1223 fn classify_inbound_plain_on_loopback_accepts_even_on_protected_domain() {
1224 let gate = SharedSecurityGate::new(
1228 0,
1229 parse_governance_xml(GOV_RTPS).unwrap(),
1230 Box::new(AesGcmCryptoPlugin::new()),
1231 );
1232 let msg = fake_msg(b"[loopback-plain]");
1233 match gate.classify_inbound(&msg, &NetInterface::Loopback) {
1234 InboundVerdict::Accept(out) => assert_eq!(out, msg),
1235 other => panic!("expected Loopback-Accept, got {other:?}"),
1236 }
1237 }
1238
1239 #[test]
1240 fn classify_inbound_srtps_from_unknown_peer_is_policy_violation() {
1241 let (alice, bob) = build_pair();
1243 let msg = fake_msg(b"[from-unknown]");
1246 let wire = alice.transform_outbound(&msg).unwrap();
1247 let verdict = bob.classify_inbound(&wire, &NetInterface::Wan);
1248 assert!(
1249 matches!(verdict, InboundVerdict::PolicyViolation(_)),
1250 "expected PolicyViolation, got {verdict:?}"
1251 );
1252 assert_eq!(verdict.category(), "inbound.policy_violation");
1253 }
1254
1255 #[test]
1256 fn classify_inbound_srtps_from_known_peer_accepts() {
1257 let (alice, bob) = build_pair();
1258 let alice_prefix: PeerKey = [0xAA; 12];
1259 bob.register_remote_by_guid(
1260 alice_prefix,
1261 IdentityHandle(1),
1262 SharedSecretHandle(1),
1263 &alice.local_token().unwrap(),
1264 )
1265 .unwrap();
1266 let msg = fake_msg(b"[authed-peer]");
1267 let mut hdr_msg = Vec::with_capacity(msg.len());
1270 hdr_msg.extend_from_slice(b"RTPS\x02\x05\x01\x02");
1271 hdr_msg.extend_from_slice(&alice_prefix);
1272 hdr_msg.extend_from_slice(b"payload-body");
1273 let wire = alice.transform_outbound(&hdr_msg).unwrap();
1274 match bob.classify_inbound(&wire, &NetInterface::Wan) {
1275 InboundVerdict::Accept(_) => {}
1276 other => panic!("expected Accept, got {other:?}"),
1277 }
1278 }
1279
1280 #[test]
1281 fn classify_inbound_srtps_with_wrong_key_is_crypto_error() {
1282 let (alice, bob) = build_pair();
1286 let gov = parse_governance_xml(GOV_RTPS).unwrap();
1287 let charlie = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
1288
1289 let alice_prefix: PeerKey = [0xAA; 12];
1290 bob.register_remote_by_guid(
1291 alice_prefix,
1292 IdentityHandle(1),
1293 SharedSecretHandle(1),
1294 &alice.local_token().unwrap(),
1295 )
1296 .unwrap();
1297
1298 let mut body = Vec::new();
1300 body.extend_from_slice(b"RTPS\x02\x05\x01\x02");
1301 body.extend_from_slice(&alice_prefix);
1302 body.extend_from_slice(b"mitm-try");
1303 let spoofed = charlie.transform_outbound(&body).unwrap();
1304
1305 let verdict = bob.classify_inbound(&spoofed, &NetInterface::Wan);
1306 assert!(
1307 matches!(verdict, InboundVerdict::CryptoError(_)),
1308 "expected CryptoError, got {verdict:?}"
1309 );
1310 assert_eq!(verdict.category(), "inbound.crypto_error");
1311 }
1312
1313 #[test]
1314 fn transform_outbound_for_is_decodable_with_registered_token() {
1315 let (alice, bob) = build_pair();
1318 let alice_prefix: PeerKey = [0xAA; 12];
1319 bob.register_remote_by_guid(
1320 alice_prefix,
1321 IdentityHandle(1),
1322 SharedSecretHandle(1),
1323 &alice.local_token().unwrap(),
1324 )
1325 .unwrap();
1326 let msg = fake_msg(b"[hetero-e2e]");
1327 let wire = alice
1328 .transform_outbound_for(&[9; 12], &msg, ProtectionLevel::Encrypt)
1329 .unwrap();
1330 let back = bob.transform_inbound_from(&alice_prefix, &wire).unwrap();
1331 assert_eq!(back, msg);
1332 }
1333}