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_submessage_multi, encode_secured_submessage_multi,
29};
30
31use crate::gate::SecurityGateError;
32use crate::policy::{NetInterface, ProtectionLevel};
33
34#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum InboundVerdict {
49 Accept(Vec<u8>),
52 Malformed,
56 LegacyBlocked,
60 PolicyViolation(String),
64 CryptoError(String),
67}
68
69impl InboundVerdict {
70 #[must_use]
72 pub const fn is_accept(&self) -> bool {
73 matches!(self, Self::Accept(_))
74 }
75
76 #[must_use]
79 pub fn category(&self) -> &'static str {
80 match self {
81 Self::Accept(_) => "inbound.accept",
82 Self::Malformed => "inbound.malformed",
83 Self::LegacyBlocked => "inbound.legacy_blocked",
84 Self::PolicyViolation(_) => "inbound.policy_violation",
85 Self::CryptoError(_) => "inbound.crypto_error",
86 }
87 }
88}
89
90pub type PeerKey = [u8; 12];
93
94#[derive(Clone)]
98pub struct SharedSecurityGate {
99 inner: Arc<Mutex<GateInner>>,
100}
101
102impl core::fmt::Debug for SharedSecurityGate {
103 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104 match self.inner.lock() {
106 Ok(g) => write!(
107 f,
108 "SharedSecurityGate {{ domain_id: {}, peers: {}, local_registered: {} }}",
109 g.domain_id,
110 g.peers.len(),
111 g.local.is_some()
112 ),
113 Err(_) => write!(f, "SharedSecurityGate {{ <poisoned> }}"),
114 }
115 }
116}
117
118struct GateInner {
119 domain_id: u32,
120 governance: Governance,
121 crypto: Box<dyn CryptographicPlugin>,
122 local: Option<CryptoHandle>,
123 peers: BTreeMap<PeerKey, CryptoHandle>,
127}
128
129#[must_use]
137pub fn peer_key_to_id(pk: &PeerKey) -> u32 {
138 let mut buf = [0u8; 4];
139 buf.copy_from_slice(&pk[..4]);
140 u32::from_le_bytes(buf)
141}
142
143fn poisoned<T>(_: PoisonError<T>) -> SecurityGateError {
144 SecurityGateError::Crypto(zerodds_security::error::SecurityError::new(
145 zerodds_security::error::SecurityErrorKind::Internal,
146 "security-runtime: mutex poisoned",
147 ))
148}
149
150impl SharedSecurityGate {
151 #[must_use]
153 pub fn new(
154 domain_id: u32,
155 governance: Governance,
156 crypto: Box<dyn CryptographicPlugin>,
157 ) -> Self {
158 Self {
159 inner: Arc::new(Mutex::new(GateInner {
160 domain_id,
161 governance,
162 crypto,
163 local: None,
164 peers: BTreeMap::new(),
165 })),
166 }
167 }
168
169 fn with_inner<R>(
170 &self,
171 f: impl FnOnce(&mut GateInner) -> Result<R, SecurityGateError>,
172 ) -> Result<R, SecurityGateError> {
173 let mut g = self.inner.lock().map_err(poisoned)?;
174 f(&mut g)
175 }
176
177 pub fn domain_id(&self) -> Result<u32, SecurityGateError> {
179 self.with_inner(|g| Ok(g.domain_id))
180 }
181
182 pub fn message_protection(&self) -> Result<ProtectionKind, SecurityGateError> {
185 self.with_inner(|g| {
186 Ok(g.governance
187 .find_domain_rule(g.domain_id)
188 .map(|r| r.rtps_protection_kind)
189 .unwrap_or(ProtectionKind::None))
190 })
191 }
192
193 pub fn data_protection(&self) -> Result<ProtectionLevel, SecurityGateError> {
204 self.with_inner(|g| {
205 let kind = g
206 .governance
207 .find_domain_rule(g.domain_id)
208 .and_then(|r| r.topic_rules.first())
209 .map(|t| t.data_protection_kind)
210 .unwrap_or(ProtectionKind::None);
211 Ok(ProtectionLevel::from_protection_kind(kind))
212 })
213 }
214
215 pub fn metadata_protection(&self) -> Result<ProtectionLevel, SecurityGateError> {
225 self.with_inner(|g| {
226 let kind = g
227 .governance
228 .find_domain_rule(g.domain_id)
229 .and_then(|r| r.topic_rules.first())
230 .map(|t| t.metadata_protection_kind)
231 .unwrap_or(ProtectionKind::None);
232 Ok(ProtectionLevel::from_protection_kind(kind))
233 })
234 }
235
236 pub fn discovery_protection(&self) -> Result<ProtectionLevel, SecurityGateError> {
246 self.with_inner(|g| {
247 let kind = g
248 .governance
249 .find_domain_rule(g.domain_id)
250 .map(|r| r.discovery_protection_kind)
251 .unwrap_or(ProtectionKind::None);
252 Ok(ProtectionLevel::from_protection_kind(kind))
253 })
254 }
255
256 pub fn topic_discovery_protected(&self) -> Result<bool, SecurityGateError> {
268 self.with_inner(|g| {
269 Ok(g.governance
270 .find_domain_rule(g.domain_id)
271 .and_then(|r| r.topic_rules.first())
272 .map(|t| t.enable_discovery_protection)
273 .unwrap_or(false))
274 })
275 }
276
277 pub fn topic_read_protected(&self) -> Result<bool, SecurityGateError> {
285 self.with_inner(|g| {
286 Ok(g.governance
287 .find_domain_rule(g.domain_id)
288 .and_then(|r| r.topic_rules.first())
289 .map(|t| t.enable_read_access_control)
290 .unwrap_or(false))
291 })
292 }
293
294 pub fn topic_write_protected(&self) -> Result<bool, SecurityGateError> {
301 self.with_inner(|g| {
302 Ok(g.governance
303 .find_domain_rule(g.domain_id)
304 .and_then(|r| r.topic_rules.first())
305 .map(|t| t.enable_write_access_control)
306 .unwrap_or(false))
307 })
308 }
309
310 pub fn rtps_protection(&self) -> Result<ProtectionLevel, SecurityGateError> {
318 self.with_inner(|g| {
319 let kind = g
320 .governance
321 .find_domain_rule(g.domain_id)
322 .map(|r| r.rtps_protection_kind)
323 .unwrap_or(ProtectionKind::None);
324 Ok(ProtectionLevel::from_protection_kind(kind))
325 })
326 }
327
328 pub fn liveliness_protection(&self) -> Result<ProtectionLevel, SecurityGateError> {
334 self.with_inner(|g| {
335 let kind = g
336 .governance
337 .find_domain_rule(g.domain_id)
338 .map(|r| r.liveliness_protection_kind)
339 .unwrap_or(ProtectionKind::None);
340 Ok(ProtectionLevel::from_protection_kind(kind))
341 })
342 }
343
344 pub fn ensure_local(&self) -> Result<CryptoHandle, SecurityGateError> {
349 self.with_inner(|g| {
350 if let Some(h) = g.local {
351 return Ok(h);
352 }
353 let h = g
354 .crypto
355 .register_local_participant(IdentityHandle(1), &[])
356 .map_err(SecurityGateError::CryptoSetup)?;
357 g.local = Some(h);
358 Ok(h)
359 })
360 }
361
362 pub fn local_token(&self) -> Result<Vec<u8>, SecurityGateError> {
367 let local = self.ensure_local()?;
368 self.with_inner(|g| {
369 g.crypto
370 .create_local_participant_crypto_tokens(local, CryptoHandle(0))
371 .map_err(SecurityGateError::Crypto)
372 })
373 }
374
375 pub fn register_remote_with_token(
382 &self,
383 remote_identity: IdentityHandle,
384 shared_secret: SharedSecretHandle,
385 token: &[u8],
386 ) -> Result<CryptoHandle, SecurityGateError> {
387 let local = self.ensure_local()?;
388 self.with_inner(|g| {
389 let slot = g
390 .crypto
391 .register_matched_remote_participant(local, remote_identity, shared_secret)
392 .map_err(SecurityGateError::CryptoSetup)?;
393 g.crypto
394 .set_remote_participant_crypto_tokens(local, slot, token)
395 .map_err(SecurityGateError::Crypto)?;
396 Ok(slot)
397 })
398 }
399
400 pub fn register_remote_by_guid(
411 &self,
412 peer_key: PeerKey,
413 remote_identity: IdentityHandle,
414 shared_secret: SharedSecretHandle,
415 token: &[u8],
416 ) -> Result<CryptoHandle, SecurityGateError> {
417 {
420 let g = self.inner.lock().map_err(poisoned)?;
421 if let Some(h) = g.peers.get(&peer_key) {
422 return Ok(*h);
423 }
424 }
425 let slot = self.register_remote_with_token(remote_identity, shared_secret, token)?;
426 self.with_inner(|g| {
427 g.peers.insert(peer_key, slot);
428 Ok(())
429 })?;
430 Ok(slot)
431 }
432
433 pub fn register_remote_by_guid_from_secret(
444 &self,
445 peer_key: PeerKey,
446 remote_identity: IdentityHandle,
447 shared_secret: SharedSecretHandle,
448 ) -> Result<CryptoHandle, SecurityGateError> {
449 {
450 let g = self.inner.lock().map_err(poisoned)?;
451 if let Some(h) = g.peers.get(&peer_key) {
452 return Ok(*h);
453 }
454 }
455 let local = self.ensure_local()?;
456 self.with_inner(|g| {
457 let slot = g
458 .crypto
459 .register_matched_remote_participant(local, remote_identity, shared_secret)
460 .map_err(SecurityGateError::CryptoSetup)?;
461 g.peers.insert(peer_key, slot);
462 Ok(slot)
463 })
464 }
465
466 pub fn set_remote_data_token_by_guid(
474 &self,
475 peer_key: &PeerKey,
476 token: &[u8],
477 ) -> Result<(), SecurityGateError> {
478 let local = self.ensure_local()?;
479 self.with_inner(|g| {
480 let slot = g.peers.get(peer_key).copied().ok_or_else(|| {
481 SecurityGateError::PolicyViolation(alloc::format!(
482 "set_remote_data_token: peer {peer_key:?} not registered"
483 ))
484 })?;
485 g.crypto
486 .set_remote_participant_crypto_tokens(local, slot, token)
487 .map_err(SecurityGateError::Crypto)
488 })
489 }
490
491 pub fn transform_kx_outbound_for(
499 &self,
500 peer_key: &PeerKey,
501 plaintext: &[u8],
502 ) -> Result<Vec<u8>, SecurityGateError> {
503 self.with_inner(|g| {
504 let slot = g.peers.get(peer_key).copied().ok_or_else(|| {
505 SecurityGateError::PolicyViolation(alloc::format!(
506 "transform_kx_outbound: peer {peer_key:?} not registered"
507 ))
508 })?;
509 g.crypto
510 .encode_kx_submessage(slot, plaintext, &[])
511 .map_err(SecurityGateError::Crypto)
512 })
513 }
514
515 pub fn transform_kx_inbound_from(
521 &self,
522 peer_key: &PeerKey,
523 wire: &[u8],
524 ) -> Result<Vec<u8>, SecurityGateError> {
525 self.with_inner(|g| {
526 let slot = g.peers.get(peer_key).copied().ok_or_else(|| {
527 SecurityGateError::PolicyViolation(alloc::format!(
528 "transform_kx_inbound: peer {peer_key:?} not registered"
529 ))
530 })?;
531 g.crypto
532 .decode_kx_submessage(slot, wire, &[])
533 .map_err(SecurityGateError::Crypto)
534 })
535 }
536
537 pub fn encode_kx_datawriter_for(
544 &self,
545 peer_key: &PeerKey,
546 data_submessage: &[u8],
547 ) -> Result<Vec<u8>, SecurityGateError> {
548 self.with_inner(|g| {
549 let slot = g.peers.get(peer_key).copied().ok_or_else(|| {
550 SecurityGateError::PolicyViolation(alloc::format!(
551 "encode_kx_datawriter: peer {peer_key:?} not registered"
552 ))
553 })?;
554 g.crypto
555 .encode_kx_datawriter_submessage(slot, data_submessage)
556 .map_err(SecurityGateError::Crypto)
557 })
558 }
559
560 pub fn decode_kx_datawriter_from(
566 &self,
567 peer_key: &PeerKey,
568 wire: &[u8],
569 ) -> Result<Vec<u8>, SecurityGateError> {
570 self.with_inner(|g| {
571 let slot = g.peers.get(peer_key).copied().ok_or_else(|| {
572 SecurityGateError::PolicyViolation(alloc::format!(
573 "decode_kx_datawriter: peer {peer_key:?} not registered"
574 ))
575 })?;
576 g.crypto
577 .decode_kx_datawriter_submessage(slot, wire)
578 .map_err(SecurityGateError::Crypto)
579 })
580 }
581
582 pub fn encode_data_datawriter_local(
590 &self,
591 data_submessage: &[u8],
592 ) -> Result<Vec<u8>, SecurityGateError> {
593 let local = self.ensure_local()?;
594 self.with_inner(|g| {
595 g.crypto
596 .encode_data_datawriter_submessage(local, data_submessage)
597 .map_err(SecurityGateError::Crypto)
598 })
599 }
600
601 pub fn register_local_endpoint(
616 &self,
617 is_writer: bool,
618 ) -> Result<CryptoHandle, SecurityGateError> {
619 let local = self.ensure_local()?;
620 self.with_inner(|g| {
621 g.crypto
622 .register_local_endpoint(local, is_writer, &[])
623 .map_err(SecurityGateError::CryptoSetup)
624 })
625 }
626
627 pub fn create_endpoint_token(
634 &self,
635 endpoint: CryptoHandle,
636 ) -> Result<Vec<u8>, SecurityGateError> {
637 self.with_inner(|g| {
638 g.crypto
639 .create_local_participant_crypto_tokens(endpoint, CryptoHandle(0))
640 .map_err(SecurityGateError::Crypto)
641 })
642 }
643
644 #[must_use]
655 pub fn endpoint_payload_token(&self, endpoint: CryptoHandle) -> Option<alloc::vec::Vec<u8>> {
656 self.with_inner(|g| Ok(g.crypto.endpoint_payload_token(endpoint)))
657 .ok()
658 .flatten()
659 }
660
661 pub fn install_remote_endpoint_token(&self, token: &[u8]) -> Result<(), SecurityGateError> {
665 let local = self.ensure_local()?;
666 self.with_inner(|g| {
667 g.crypto
668 .set_remote_participant_crypto_tokens(local, CryptoHandle(0), token)
669 .map_err(SecurityGateError::Crypto)
670 })
671 }
672
673 pub fn encode_data_datawriter_by_handle(
679 &self,
680 endpoint: CryptoHandle,
681 data_submessage: &[u8],
682 ) -> Result<Vec<u8>, SecurityGateError> {
683 self.with_inner(|g| {
684 g.crypto
685 .encode_data_datawriter_submessage(endpoint, data_submessage)
686 .map_err(SecurityGateError::Crypto)
687 })
688 }
689
690 pub fn decode_data_datawriter_from(
696 &self,
697 peer_key: &PeerKey,
698 wire: &[u8],
699 ) -> Result<Vec<u8>, SecurityGateError> {
700 self.with_inner(|g| {
701 let slot = g.peers.get(peer_key).copied().ok_or_else(|| {
702 SecurityGateError::PolicyViolation(alloc::format!(
703 "decode_data_datawriter: peer {peer_key:?} not registered"
704 ))
705 })?;
706 g.crypto
707 .decode_data_datawriter_submessage(slot, wire)
708 .map_err(SecurityGateError::Crypto)
709 })
710 }
711
712 pub fn decode_data_by_key_id(&self, wire: &[u8]) -> Result<Vec<u8>, SecurityGateError> {
721 self.with_inner(|g| {
722 g.crypto
723 .decode_data_by_key_id(wire)
724 .map_err(SecurityGateError::Crypto)
725 })
726 }
727
728 pub fn encode_serialized_payload(
736 &self,
737 endpoint: CryptoHandle,
738 payload: &[u8],
739 ) -> Result<Vec<u8>, SecurityGateError> {
740 self.with_inner(|g| {
741 g.crypto
742 .encode_serialized_payload(endpoint, payload)
743 .map_err(SecurityGateError::Crypto)
744 })
745 }
746
747 pub fn decode_serialized_payload(&self, encoded: &[u8]) -> Result<Vec<u8>, SecurityGateError> {
752 self.with_inner(|g| {
753 g.crypto
754 .decode_serialized_payload(encoded)
755 .map_err(SecurityGateError::Crypto)
756 })
757 }
758
759 pub fn authenticated_peer_prefixes(&self) -> Vec<PeerKey> {
765 self.with_inner(|g| Ok(g.peers.keys().copied().collect()))
766 .unwrap_or_default()
767 }
768
769 pub fn decode_serialized_payload_from(
777 &self,
778 peer_key: &PeerKey,
779 encoded: &[u8],
780 ) -> Result<Vec<u8>, SecurityGateError> {
781 self.with_inner(|g| {
782 let slot = g.peers.get(peer_key).copied().ok_or_else(|| {
783 SecurityGateError::PolicyViolation(alloc::format!(
784 "decode_serialized_payload: peer {peer_key:?} not registered"
785 ))
786 })?;
787 g.crypto
788 .decode_serialized_payload_with(slot, encoded)
789 .map_err(SecurityGateError::Crypto)
790 })
791 }
792
793 pub fn decode_serialized_payload_kx(
802 &self,
803 peer_key: &PeerKey,
804 encoded: &[u8],
805 ) -> Result<Vec<u8>, SecurityGateError> {
806 self.with_inner(|g| {
807 let slot = g.peers.get(peer_key).copied().ok_or_else(|| {
808 SecurityGateError::PolicyViolation(alloc::format!(
809 "decode_serialized_payload_kx: peer {peer_key:?} not registered"
810 ))
811 })?;
812 g.crypto
813 .decode_serialized_payload_kx(slot, encoded)
814 .map_err(SecurityGateError::Crypto)
815 })
816 }
817
818 pub fn forget_remote(&self, peer_key: &PeerKey) -> Result<(), SecurityGateError> {
821 self.with_inner(|g| {
822 g.peers.remove(peer_key);
823 Ok(())
824 })
825 }
826
827 pub fn slot_for(&self, peer_key: &PeerKey) -> Result<Option<CryptoHandle>, SecurityGateError> {
829 self.with_inner(|g| Ok(g.peers.get(peer_key).copied()))
830 }
831
832 pub fn transform_inbound_from(
843 &self,
844 peer_key: &PeerKey,
845 wire: &[u8],
846 ) -> Result<Vec<u8>, SecurityGateError> {
847 let looks_secured = wire.len() > RTPS_HEADER_LEN && wire[RTPS_HEADER_LEN] == SRTPS_PREFIX;
848 let kind = self.message_protection()?;
849 if !looks_secured {
850 return if matches!(kind, ProtectionKind::None) {
853 Ok(wire.to_vec())
854 } else {
855 Err(SecurityGateError::PolicyViolation(alloc::format!(
856 "domain requires {kind:?}, got a plain rtps message"
857 )))
858 };
859 }
860 let slot = self.slot_for(peer_key)?.ok_or_else(|| {
861 SecurityGateError::PolicyViolation(alloc::format!(
862 "unknown peer {peer_key:?} sends SRTPS_PREFIX"
863 ))
864 })?;
865 self.transform_inbound(slot, wire)
866 }
867
868 pub fn transform_outbound(&self, message: &[u8]) -> Result<Vec<u8>, SecurityGateError> {
873 match self.message_protection()? {
874 ProtectionKind::None => Ok(message.to_vec()),
875 _ => {
876 let local = self.ensure_local()?;
877 self.with_inner(|g| {
880 g.crypto
881 .encode_rtps_message_cyclone(local, message)
882 .map_err(SecurityGateError::Crypto)
883 })
884 }
885 }
886 }
887
888 pub fn transform_outbound_group(
905 &self,
906 peer_keys: &[PeerKey],
907 plaintext: &[u8],
908 ) -> Result<Vec<u8>, SecurityGateError> {
909 let local = self.ensure_local()?;
910 let bindings: Vec<(CryptoHandle, u32)> = self.with_inner(|g| {
917 let mut out = Vec::with_capacity(peer_keys.len());
918 for pk in peer_keys {
919 let h = g.peers.get(pk).copied().ok_or_else(|| {
920 SecurityGateError::PolicyViolation(alloc::format!(
921 "transform_outbound_group: peer {pk:?} not registered"
922 ))
923 })?;
924 out.push((h, peer_key_to_id(pk)));
925 }
926 Ok(out)
927 })?;
928 self.with_inner(|g| {
929 encode_secured_submessage_multi(&*g.crypto, local, &bindings, plaintext)
930 .map_err(SecurityGateError::from)
931 })
932 }
933
934 pub fn transform_inbound_group(
947 &self,
948 sender_peer_key: &PeerKey,
949 own_peer_key: &PeerKey,
950 wire: &[u8],
951 ) -> Result<Vec<u8>, SecurityGateError> {
952 let sender_slot = self.slot_for(sender_peer_key)?.ok_or_else(|| {
953 SecurityGateError::PolicyViolation(alloc::format!(
954 "transform_inbound_group: unknown sender {sender_peer_key:?}"
955 ))
956 })?;
957 let own_local = self.ensure_local()?;
961 let own_id = peer_key_to_id(own_peer_key);
962 self.with_inner(|g| {
963 decode_secured_submessage_multi(
964 &*g.crypto,
965 sender_slot,
966 sender_slot,
967 own_id,
968 own_local,
969 wire,
970 )
971 .map_err(SecurityGateError::from)
972 })
973 }
974
975 pub fn transform_outbound_for(
997 &self,
998 _peer_key: &PeerKey,
999 message: &[u8],
1000 level: ProtectionLevel,
1001 ) -> Result<Vec<u8>, SecurityGateError> {
1002 match level {
1003 ProtectionLevel::None => Ok(message.to_vec()),
1004 ProtectionLevel::Sign | ProtectionLevel::Encrypt => {
1005 let local = self.ensure_local()?;
1006 self.with_inner(|g| {
1014 g.crypto
1015 .encode_rtps_message_cyclone(local, message)
1016 .map_err(SecurityGateError::Crypto)
1017 })
1018 }
1019 }
1020 }
1021
1022 pub fn allow_unauthenticated(&self) -> Result<bool, SecurityGateError> {
1026 self.with_inner(|g| {
1027 Ok(g.governance
1028 .find_domain_rule(g.domain_id)
1029 .map(|r| r.allow_unauthenticated_participants)
1030 .unwrap_or(false))
1031 })
1032 }
1033
1034 #[must_use]
1059 pub fn classify_inbound(&self, bytes: &[u8], iface: &NetInterface) -> InboundVerdict {
1060 if bytes.len() < RTPS_HEADER_LEN + 8 {
1061 return InboundVerdict::Malformed;
1062 }
1063 let mut peer_key = [0u8; 12];
1064 peer_key.copy_from_slice(&bytes[8..20]);
1065
1066 let looks_secured = bytes.len() > RTPS_HEADER_LEN && bytes[RTPS_HEADER_LEN] == SRTPS_PREFIX;
1067 let kind = match self.message_protection() {
1068 Ok(k) => k,
1069 Err(e) => {
1070 return InboundVerdict::CryptoError(alloc::format!("gate lookup failed: {e:?}"));
1071 }
1072 };
1073
1074 if looks_secured {
1075 return match self.transform_inbound_from(&peer_key, bytes) {
1076 Ok(clear) => InboundVerdict::Accept(clear),
1077 Err(SecurityGateError::PolicyViolation(msg)) => {
1078 InboundVerdict::PolicyViolation(msg)
1079 }
1080 Err(e) => InboundVerdict::CryptoError(alloc::format!("{e:?}")),
1081 };
1082 }
1083
1084 if matches!(kind, ProtectionKind::None) {
1086 return InboundVerdict::Accept(bytes.to_vec());
1087 }
1088 if matches!(iface, NetInterface::Loopback | NetInterface::LocalHost) {
1092 return InboundVerdict::Accept(bytes.to_vec());
1093 }
1094 match self.allow_unauthenticated() {
1097 Ok(true) => InboundVerdict::Accept(bytes.to_vec()),
1098 Ok(false) => InboundVerdict::LegacyBlocked,
1099 Err(e) => InboundVerdict::CryptoError(alloc::format!("gate lookup failed: {e:?}")),
1100 }
1101 }
1102
1103 pub fn transform_inbound(
1112 &self,
1113 remote_slot: CryptoHandle,
1114 wire: &[u8],
1115 ) -> Result<Vec<u8>, SecurityGateError> {
1116 let looks_secured = wire.len() > RTPS_HEADER_LEN && wire[RTPS_HEADER_LEN] == SRTPS_PREFIX;
1117 let kind = self.message_protection()?;
1118 match (kind, looks_secured) {
1119 (ProtectionKind::None, false) => Ok(wire.to_vec()),
1120 (_, true) => self.with_inner(|g| {
1121 let _ = remote_slot;
1124 g.crypto
1125 .decode_rtps_message_cyclone(wire)
1126 .map_err(SecurityGateError::Crypto)
1127 }),
1128 (_, false) => Err(SecurityGateError::PolicyViolation(alloc::format!(
1129 "domain requires {kind:?}, got a plain rtps message"
1130 ))),
1131 }
1132 }
1133}
1134
1135#[cfg(test)]
1136#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
1137mod tests {
1138 use super::*;
1139 use std::thread;
1140 use zerodds_security_crypto::AesGcmCryptoPlugin;
1141 use zerodds_security_permissions::parse_governance_xml;
1142
1143 const GOV_RTPS: &str = r#"
1144<domain_access_rules>
1145 <domain_rule>
1146 <domains><id>0</id></domains>
1147 <rtps_protection_kind>ENCRYPT</rtps_protection_kind>
1148 <topic_access_rules><topic_rule><topic_expression>*</topic_expression></topic_rule></topic_access_rules>
1149 </domain_rule>
1150</domain_access_rules>
1151"#;
1152
1153 fn fake_msg(body: &[u8]) -> Vec<u8> {
1154 let mut m = Vec::with_capacity(20 + body.len());
1155 m.extend_from_slice(b"RTPS\x02\x05\x01\x02");
1156 m.extend_from_slice(&[0u8; 12]);
1157 m.extend_from_slice(body);
1158 m
1159 }
1160
1161 #[test]
1162 fn outbound_none_is_passthrough() {
1163 let gov = parse_governance_xml(
1164 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>"#,
1165 )
1166 .unwrap();
1167 let gate = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
1168 let msg = fake_msg(b"x");
1169 assert_eq!(gate.transform_outbound(&msg).unwrap(), msg);
1170 }
1171
1172 #[test]
1173 fn per_endpoint_datawriter_token_roundtrips_by_key_id() {
1174 let alice = SharedSecurityGate::new(
1180 0,
1181 parse_governance_xml(GOV_RTPS).unwrap(),
1182 Box::new(AesGcmCryptoPlugin::new()),
1183 );
1184 let bob = SharedSecurityGate::new(
1185 0,
1186 parse_governance_xml(GOV_RTPS).unwrap(),
1187 Box::new(AesGcmCryptoPlugin::new()),
1188 );
1189
1190 let aw = alice.register_local_endpoint(true).unwrap();
1192 let token = alice.create_endpoint_token(aw).unwrap();
1194 bob.install_remote_endpoint_token(&token).unwrap();
1195
1196 let plain = fake_msg(b"[DATA:per-endpoint]");
1198 let wire = alice.encode_data_datawriter_by_handle(aw, &plain).unwrap();
1199 let back = bob.decode_data_by_key_id(&wire).unwrap();
1201 assert_eq!(back, plain, "per-endpoint key round-trips via key_id");
1202 }
1203
1204 #[test]
1205 fn topic_discovery_protected_reads_topic_rule_flag() {
1206 const GOV_DISC: &str = r#"
1212<domain_access_rules>
1213 <domain_rule>
1214 <domains><id>0</id></domains>
1215 <discovery_protection_kind>ENCRYPT</discovery_protection_kind>
1216 <topic_access_rules><topic_rule>
1217 <topic_expression>*</topic_expression>
1218 <enable_discovery_protection>true</enable_discovery_protection>
1219 </topic_rule></topic_access_rules>
1220 </domain_rule>
1221</domain_access_rules>
1222"#;
1223 let on = SharedSecurityGate::new(
1224 0,
1225 parse_governance_xml(GOV_DISC).unwrap(),
1226 Box::new(AesGcmCryptoPlugin::new()),
1227 );
1228 assert!(
1229 on.topic_discovery_protected().unwrap(),
1230 "enable_discovery_protection=true ⟹ endpoint IS_DISCOVERY_PROTECTED"
1231 );
1232 let off = SharedSecurityGate::new(
1234 0,
1235 parse_governance_xml(GOV_RTPS).unwrap(),
1236 Box::new(AesGcmCryptoPlugin::new()),
1237 );
1238 assert!(!off.topic_discovery_protected().unwrap());
1239 }
1240
1241 #[test]
1242 fn e2e_alice_bob_with_shared_gate() {
1243 let alice = SharedSecurityGate::new(
1244 0,
1245 parse_governance_xml(GOV_RTPS).unwrap(),
1246 Box::new(AesGcmCryptoPlugin::new()),
1247 );
1248 let bob = SharedSecurityGate::new(
1249 0,
1250 parse_governance_xml(GOV_RTPS).unwrap(),
1251 Box::new(AesGcmCryptoPlugin::new()),
1252 );
1253 let alice_token = alice.local_token().unwrap();
1254 let bob_view_of_alice = bob
1255 .register_remote_with_token(IdentityHandle(1), SharedSecretHandle(1), &alice_token)
1256 .unwrap();
1257
1258 let plain = fake_msg(b"[DATA:shared]");
1259 let wire = alice.transform_outbound(&plain).unwrap();
1260 let back = bob.transform_inbound(bob_view_of_alice, &wire).unwrap();
1261 assert_eq!(back, plain);
1262 }
1263
1264 #[test]
1265 fn clone_shares_same_plugin_instance() {
1266 let gate1 = SharedSecurityGate::new(
1270 0,
1271 parse_governance_xml(GOV_RTPS).unwrap(),
1272 Box::new(AesGcmCryptoPlugin::new()),
1273 );
1274 let gate2 = gate1.clone();
1275 let t1 = gate1.local_token().unwrap();
1276 let t2 = gate2.local_token().unwrap();
1277 assert_eq!(t1, t2, "both clones see the same local slot");
1278 }
1279
1280 #[test]
1281 fn concurrent_transform_is_thread_safe() {
1282 let alice = SharedSecurityGate::new(
1283 0,
1284 parse_governance_xml(GOV_RTPS).unwrap(),
1285 Box::new(AesGcmCryptoPlugin::new()),
1286 );
1287 let mut handles = Vec::new();
1288 for i in 0..8 {
1289 let g = alice.clone();
1290 handles.push(thread::spawn(move || {
1291 let m = fake_msg(alloc::format!("[DATA:{i}]").as_bytes());
1292 let _ = g.transform_outbound(&m).unwrap();
1295 }));
1296 }
1297 for h in handles {
1298 h.join().unwrap();
1299 }
1300 }
1301
1302 #[test]
1303 fn plain_inbound_on_protected_domain_is_policy_violation() {
1304 let gate = SharedSecurityGate::new(
1305 0,
1306 parse_governance_xml(GOV_RTPS).unwrap(),
1307 Box::new(AesGcmCryptoPlugin::new()),
1308 );
1309 let plain = fake_msg(b"nope");
1310 let err = gate
1311 .transform_inbound(CryptoHandle(99), &plain)
1312 .unwrap_err();
1313 assert!(matches!(err, SecurityGateError::PolicyViolation(_)));
1314 }
1315
1316 #[test]
1317 fn domain_id_reflects_constructor() {
1318 let gate = SharedSecurityGate::new(
1319 7,
1320 parse_governance_xml(GOV_RTPS).unwrap(),
1321 Box::new(AesGcmCryptoPlugin::new()),
1322 );
1323 assert_eq!(gate.domain_id().unwrap(), 7);
1324 }
1325
1326 fn build_pair() -> (SharedSecurityGate, SharedSecurityGate) {
1331 let alice = SharedSecurityGate::new(
1332 0,
1333 parse_governance_xml(GOV_RTPS).unwrap(),
1334 Box::new(AesGcmCryptoPlugin::new()),
1335 );
1336 let bob = SharedSecurityGate::new(
1337 0,
1338 parse_governance_xml(GOV_RTPS).unwrap(),
1339 Box::new(AesGcmCryptoPlugin::new()),
1340 );
1341 (alice, bob)
1342 }
1343
1344 #[test]
1345 fn register_remote_by_guid_is_idempotent() {
1346 let (alice, bob) = build_pair();
1347 let alice_prefix: PeerKey = [0xAA; 12];
1348 let atoken = alice.local_token().unwrap();
1349 let slot1 = bob
1350 .register_remote_by_guid(
1351 alice_prefix,
1352 IdentityHandle(1),
1353 SharedSecretHandle(1),
1354 &atoken,
1355 )
1356 .unwrap();
1357 let slot2 = bob
1358 .register_remote_by_guid(
1359 alice_prefix,
1360 IdentityHandle(1),
1361 SharedSecretHandle(1),
1362 &atoken,
1363 )
1364 .unwrap();
1365 assert_eq!(slot1, slot2, "idempotent: same guid prefix → same slot");
1366 }
1367
1368 #[test]
1369 fn transform_inbound_from_looks_up_slot_by_guid() {
1370 let (alice, bob) = build_pair();
1371 let alice_prefix: PeerKey = [0xAA; 12];
1372 let atoken = alice.local_token().unwrap();
1373 bob.register_remote_by_guid(
1374 alice_prefix,
1375 IdentityHandle(1),
1376 SharedSecretHandle(1),
1377 &atoken,
1378 )
1379 .unwrap();
1380
1381 let msg = fake_msg(b"[DATA:guid-lookup]");
1382 let wire = alice.transform_outbound(&msg).unwrap();
1383 let back = bob.transform_inbound_from(&alice_prefix, &wire).unwrap();
1384 assert_eq!(back, msg);
1385 }
1386
1387 #[test]
1388 fn transform_inbound_from_unknown_peer_is_policy_violation() {
1389 let (alice, bob) = build_pair();
1390 let msg = fake_msg(b"x");
1392 let wire = alice.transform_outbound(&msg).unwrap();
1393 let err = bob.transform_inbound_from(&[0xCC; 12], &wire).unwrap_err();
1394 assert!(matches!(err, SecurityGateError::PolicyViolation(_)));
1395 }
1396
1397 #[test]
1398 fn multi_peer_mapping_routes_correctly() {
1399 let gov = parse_governance_xml(GOV_RTPS).unwrap();
1402 let alice = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
1403 let charlie = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
1404 let bob = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
1405
1406 let alice_prefix: PeerKey = [1u8; 12];
1407 let charlie_prefix: PeerKey = [3u8; 12];
1408
1409 bob.register_remote_by_guid(
1410 alice_prefix,
1411 IdentityHandle(1),
1412 SharedSecretHandle(1),
1413 &alice.local_token().unwrap(),
1414 )
1415 .unwrap();
1416 bob.register_remote_by_guid(
1417 charlie_prefix,
1418 IdentityHandle(3),
1419 SharedSecretHandle(3),
1420 &charlie.local_token().unwrap(),
1421 )
1422 .unwrap();
1423
1424 let m_alice = fake_msg(b"from-alice");
1425 let m_charlie = fake_msg(b"from-charlie");
1426 let w_alice = alice.transform_outbound(&m_alice).unwrap();
1427 let w_charlie = charlie.transform_outbound(&m_charlie).unwrap();
1428
1429 assert_eq!(
1430 bob.transform_inbound_from(&alice_prefix, &w_alice).unwrap(),
1431 m_alice
1432 );
1433 assert_eq!(
1434 bob.transform_inbound_from(&charlie_prefix, &w_charlie)
1435 .unwrap(),
1436 m_charlie
1437 );
1438 }
1439
1440 #[test]
1441 fn tampered_wire_fails_tag_verify() {
1442 let (alice, bob) = build_pair();
1450 let alice_prefix: PeerKey = [1u8; 12];
1451 bob.register_remote_by_guid(
1452 alice_prefix,
1453 IdentityHandle(1),
1454 SharedSecretHandle(1),
1455 &alice.local_token().unwrap(),
1456 )
1457 .unwrap();
1458
1459 let msg = fake_msg(b"from-alice");
1460 let wire = alice.transform_outbound(&msg).unwrap();
1461 assert_eq!(
1463 bob.transform_inbound_from(&alice_prefix, &wire).unwrap(),
1464 msg
1465 );
1466
1467 assert!(
1472 wire.len() > 30,
1473 "SRTPS wire too short for the tamper offset"
1474 );
1475 let mut tampered = wire.clone();
1476 let idx = tampered.len() - 6;
1477 tampered[idx] ^= 0xff;
1478 let err = bob
1479 .transform_inbound_from(&alice_prefix, &tampered)
1480 .unwrap_err();
1481 assert!(matches!(
1482 err,
1483 SecurityGateError::Wrapper(_) | SecurityGateError::Crypto(_)
1484 ));
1485 }
1486
1487 #[test]
1492 fn group_transform_one_ciphertext_three_macs_each_reader_decodes() {
1493 let gov = parse_governance_xml(GOV_RTPS).unwrap();
1497 let alice = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
1498 let bob = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
1499 let charlie = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
1500 let dave = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
1501
1502 let bob_prefix: PeerKey = [0xB1; 12];
1503 let charlie_prefix: PeerKey = [0xC1; 12];
1504 let dave_prefix: PeerKey = [0xD1; 12];
1505 let alice_prefix: PeerKey = [0xA1; 12];
1506
1507 alice
1510 .register_remote_by_guid(
1511 bob_prefix,
1512 IdentityHandle(1),
1513 SharedSecretHandle(1),
1514 &bob.local_token().unwrap(),
1515 )
1516 .unwrap();
1517 alice
1518 .register_remote_by_guid(
1519 charlie_prefix,
1520 IdentityHandle(2),
1521 SharedSecretHandle(2),
1522 &charlie.local_token().unwrap(),
1523 )
1524 .unwrap();
1525 alice
1526 .register_remote_by_guid(
1527 dave_prefix,
1528 IdentityHandle(3),
1529 SharedSecretHandle(3),
1530 &dave.local_token().unwrap(),
1531 )
1532 .unwrap();
1533
1534 for recv in [&bob, &charlie, &dave] {
1538 recv.register_remote_by_guid(
1539 alice_prefix,
1540 IdentityHandle(10),
1541 SharedSecretHandle(10),
1542 &alice.local_token().unwrap(),
1543 )
1544 .unwrap();
1545 }
1546
1547 let plain = b"hetero-broadcast-e2e";
1549 let wire = alice
1550 .transform_outbound_group(&[bob_prefix, charlie_prefix, dave_prefix], plain)
1551 .unwrap();
1552
1553 let out_bob = bob
1556 .transform_inbound_group(&alice_prefix, &bob_prefix, &wire)
1557 .unwrap();
1558 let out_charlie = charlie
1559 .transform_inbound_group(&alice_prefix, &charlie_prefix, &wire)
1560 .unwrap();
1561 let out_dave = dave
1562 .transform_inbound_group(&alice_prefix, &dave_prefix, &wire)
1563 .unwrap();
1564 assert_eq!(out_bob, plain);
1565 assert_eq!(out_charlie, plain);
1566 assert_eq!(out_dave, plain);
1567 }
1568
1569 #[test]
1570 fn group_transform_rogue_receiver_without_mac_rejects() {
1571 let gov = parse_governance_xml(GOV_RTPS).unwrap();
1577 let alice = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
1578 let bob = SharedSecurityGate::new(0, gov.clone(), Box::new(AesGcmCryptoPlugin::new()));
1579 let eve = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
1580
1581 let alice_prefix: PeerKey = [0xA2; 12];
1582 let bob_prefix: PeerKey = [0xB2; 12];
1583 alice
1584 .register_remote_by_guid(
1585 bob_prefix,
1586 IdentityHandle(1),
1587 SharedSecretHandle(1),
1588 &bob.local_token().unwrap(),
1589 )
1590 .unwrap();
1591 eve.register_remote_by_guid(
1593 alice_prefix,
1594 IdentityHandle(10),
1595 SharedSecretHandle(10),
1596 &alice.local_token().unwrap(),
1597 )
1598 .unwrap();
1599
1600 let wire = alice
1601 .transform_outbound_group(&[bob_prefix], b"confidential")
1602 .unwrap();
1603
1604 let eve_prefix: PeerKey = [0xEE; 12];
1605 let err = eve
1606 .transform_inbound_group(&alice_prefix, &eve_prefix, &wire)
1607 .unwrap_err();
1608 assert!(
1609 matches!(
1610 err,
1611 SecurityGateError::Crypto(_) | SecurityGateError::Wrapper(_)
1612 ),
1613 "Eve without a MAC entry must drop, got: {err:?}"
1614 );
1615 }
1616
1617 #[test]
1618 fn group_transform_unknown_peer_is_policy_violation() {
1619 let alice = SharedSecurityGate::new(
1622 0,
1623 parse_governance_xml(GOV_RTPS).unwrap(),
1624 Box::new(AesGcmCryptoPlugin::new()),
1625 );
1626 let unregistered: PeerKey = [0x99; 12];
1627 let err = alice
1628 .transform_outbound_group(&[unregistered], b"x")
1629 .unwrap_err();
1630 assert!(matches!(err, SecurityGateError::PolicyViolation(_)));
1631 }
1632
1633 #[test]
1634 fn forget_remote_removes_mapping() {
1635 let (alice, bob) = build_pair();
1636 let alice_prefix: PeerKey = [0xAA; 12];
1637 bob.register_remote_by_guid(
1638 alice_prefix,
1639 IdentityHandle(1),
1640 SharedSecretHandle(1),
1641 &alice.local_token().unwrap(),
1642 )
1643 .unwrap();
1644 assert!(bob.slot_for(&alice_prefix).unwrap().is_some());
1645 bob.forget_remote(&alice_prefix).unwrap();
1646 assert!(bob.slot_for(&alice_prefix).unwrap().is_none());
1647 }
1648
1649 #[test]
1654 fn transform_outbound_for_none_is_passthrough_even_on_protected_domain() {
1655 let gate = SharedSecurityGate::new(
1659 0,
1660 parse_governance_xml(GOV_RTPS).unwrap(),
1661 Box::new(AesGcmCryptoPlugin::new()),
1662 );
1663 let peer_key: PeerKey = [0xBB; 12];
1664 let msg = fake_msg(b"[plain-for-legacy]");
1665 let out = gate
1666 .transform_outbound_for(&peer_key, &msg, ProtectionLevel::None)
1667 .unwrap();
1668 assert_eq!(out, msg, "None level must passthrough byte-identical");
1669 }
1670
1671 #[test]
1672 fn transform_outbound_for_encrypt_produces_srtps_wire() {
1673 let gate = SharedSecurityGate::new(
1674 0,
1675 parse_governance_xml(GOV_RTPS).unwrap(),
1676 Box::new(AesGcmCryptoPlugin::new()),
1677 );
1678 let peer_key: PeerKey = [0xCC; 12];
1679 let msg = fake_msg(b"[enc-for-secure]");
1680 let wire = gate
1681 .transform_outbound_for(&peer_key, &msg, ProtectionLevel::Encrypt)
1682 .unwrap();
1683 assert!(wire.len() > msg.len());
1686 assert_eq!(wire[RTPS_HEADER_LEN], SRTPS_PREFIX);
1687 }
1688
1689 #[test]
1690 fn transform_outbound_for_sign_also_uses_srtps_encoder() {
1691 let gate = SharedSecurityGate::new(
1694 0,
1695 parse_governance_xml(GOV_RTPS).unwrap(),
1696 Box::new(AesGcmCryptoPlugin::new()),
1697 );
1698 let peer_key: PeerKey = [0xDD; 12];
1699 let msg = fake_msg(b"[sig-for-fast]");
1700 let wire = gate
1701 .transform_outbound_for(&peer_key, &msg, ProtectionLevel::Sign)
1702 .unwrap();
1703 assert_ne!(wire, msg, "Sign must not be byte-identical to plain");
1704 assert_eq!(wire[RTPS_HEADER_LEN], SRTPS_PREFIX);
1705 }
1706
1707 #[test]
1708 fn transform_outbound_for_heterogeneous_three_readers() {
1709 let gate = SharedSecurityGate::new(
1712 0,
1713 parse_governance_xml(GOV_RTPS).unwrap(),
1714 Box::new(AesGcmCryptoPlugin::new()),
1715 );
1716 let msg = fake_msg(b"[broadcast]");
1717 let legacy = gate
1718 .transform_outbound_for(&[1; 12], &msg, ProtectionLevel::None)
1719 .unwrap();
1720 let fast = gate
1721 .transform_outbound_for(&[2; 12], &msg, ProtectionLevel::Sign)
1722 .unwrap();
1723 let secure = gate
1724 .transform_outbound_for(&[3; 12], &msg, ProtectionLevel::Encrypt)
1725 .unwrap();
1726 assert_eq!(legacy, msg, "the legacy reader gets plain");
1727 assert_ne!(fast, msg, "the fast reader gets SRTPS-wrapped");
1728 assert_ne!(secure, msg, "the secure reader gets SRTPS-wrapped");
1729 assert_ne!(fast, secure, "per-reader encoding must differ each time");
1733 }
1734
1735 const GOV_NONE: &str = r#"
1740<domain_access_rules>
1741 <domain_rule>
1742 <domains><id>0</id></domains>
1743 <rtps_protection_kind>NONE</rtps_protection_kind>
1744 <topic_access_rules><topic_rule><topic_expression>*</topic_expression></topic_rule></topic_access_rules>
1745 </domain_rule>
1746</domain_access_rules>
1747"#;
1748 const GOV_ENCRYPT_ALLOW_UNAUTH: &str = r#"
1749<domain_access_rules>
1750 <domain_rule>
1751 <domains><id>0</id></domains>
1752 <allow_unauthenticated_participants>TRUE</allow_unauthenticated_participants>
1753 <rtps_protection_kind>ENCRYPT</rtps_protection_kind>
1754 <topic_access_rules><topic_rule><topic_expression>*</topic_expression></topic_rule></topic_access_rules>
1755 </domain_rule>
1756</domain_access_rules>
1757"#;
1758
1759 #[test]
1760 fn allow_unauthenticated_default_false_without_element() {
1761 let gate = SharedSecurityGate::new(
1762 0,
1763 parse_governance_xml(GOV_RTPS).unwrap(),
1764 Box::new(AesGcmCryptoPlugin::new()),
1765 );
1766 assert!(!gate.allow_unauthenticated().unwrap());
1767 }
1768
1769 #[test]
1770 fn allow_unauthenticated_reads_true_when_set() {
1771 let gate = SharedSecurityGate::new(
1772 0,
1773 parse_governance_xml(GOV_ENCRYPT_ALLOW_UNAUTH).unwrap(),
1774 Box::new(AesGcmCryptoPlugin::new()),
1775 );
1776 assert!(gate.allow_unauthenticated().unwrap());
1777 }
1778
1779 #[test]
1780 fn allow_unauthenticated_defaults_false_for_unknown_domain() {
1781 let gate = SharedSecurityGate::new(
1782 99,
1783 parse_governance_xml(GOV_RTPS).unwrap(),
1784 Box::new(AesGcmCryptoPlugin::new()),
1785 );
1786 assert!(!gate.allow_unauthenticated().unwrap());
1787 }
1788
1789 #[test]
1790 fn classify_inbound_rejects_truncated_datagram() {
1791 let gate = SharedSecurityGate::new(
1792 0,
1793 parse_governance_xml(GOV_NONE).unwrap(),
1794 Box::new(AesGcmCryptoPlugin::new()),
1795 );
1796 let verdict = gate.classify_inbound(&[0u8; 10], &NetInterface::Wan);
1797 assert_eq!(verdict, InboundVerdict::Malformed);
1798 assert_eq!(verdict.category(), "inbound.malformed");
1799 }
1800
1801 #[test]
1802 fn classify_inbound_plain_on_none_domain_accepts() {
1803 let gate = SharedSecurityGate::new(
1804 0,
1805 parse_governance_xml(GOV_NONE).unwrap(),
1806 Box::new(AesGcmCryptoPlugin::new()),
1807 );
1808 let msg = fake_msg(b"[plain-hello]");
1809 match gate.classify_inbound(&msg, &NetInterface::Wan) {
1810 InboundVerdict::Accept(out) => assert_eq!(out, msg),
1811 other => panic!("expected Accept, got {other:?}"),
1812 }
1813 }
1814
1815 #[test]
1816 fn classify_inbound_plain_on_protected_domain_is_legacy_blocked() {
1817 let gate = SharedSecurityGate::new(
1819 0,
1820 parse_governance_xml(GOV_RTPS).unwrap(),
1821 Box::new(AesGcmCryptoPlugin::new()),
1822 );
1823 let msg = fake_msg(b"[legacy-on-encrypted]");
1824 let verdict = gate.classify_inbound(&msg, &NetInterface::Wan);
1825 assert_eq!(verdict, InboundVerdict::LegacyBlocked);
1826 assert_eq!(verdict.category(), "inbound.legacy_blocked");
1827 assert!(!verdict.is_accept());
1828 }
1829
1830 #[test]
1831 fn classify_inbound_plain_on_protected_domain_with_allow_unauth_accepts() {
1832 let gate = SharedSecurityGate::new(
1835 0,
1836 parse_governance_xml(GOV_ENCRYPT_ALLOW_UNAUTH).unwrap(),
1837 Box::new(AesGcmCryptoPlugin::new()),
1838 );
1839 let msg = fake_msg(b"[legacy-allowed]");
1840 match gate.classify_inbound(&msg, &NetInterface::Wan) {
1841 InboundVerdict::Accept(out) => assert_eq!(out, msg),
1842 other => panic!("expected Accept (allow_unauthenticated=true), got {other:?}"),
1843 }
1844 }
1845
1846 #[test]
1847 fn classify_inbound_plain_on_loopback_accepts_even_on_protected_domain() {
1848 let gate = SharedSecurityGate::new(
1852 0,
1853 parse_governance_xml(GOV_RTPS).unwrap(),
1854 Box::new(AesGcmCryptoPlugin::new()),
1855 );
1856 let msg = fake_msg(b"[loopback-plain]");
1857 match gate.classify_inbound(&msg, &NetInterface::Loopback) {
1858 InboundVerdict::Accept(out) => assert_eq!(out, msg),
1859 other => panic!("expected Loopback-Accept, got {other:?}"),
1860 }
1861 }
1862
1863 #[test]
1864 fn classify_inbound_srtps_from_unknown_peer_is_policy_violation() {
1865 let (alice, bob) = build_pair();
1867 let msg = fake_msg(b"[from-unknown]");
1870 let wire = alice.transform_outbound(&msg).unwrap();
1871 let verdict = bob.classify_inbound(&wire, &NetInterface::Wan);
1872 assert!(
1873 matches!(verdict, InboundVerdict::PolicyViolation(_)),
1874 "expected PolicyViolation, got {verdict:?}"
1875 );
1876 assert_eq!(verdict.category(), "inbound.policy_violation");
1877 }
1878
1879 #[test]
1880 fn classify_inbound_srtps_from_known_peer_accepts() {
1881 let (alice, bob) = build_pair();
1882 let alice_prefix: PeerKey = [0xAA; 12];
1883 bob.register_remote_by_guid(
1884 alice_prefix,
1885 IdentityHandle(1),
1886 SharedSecretHandle(1),
1887 &alice.local_token().unwrap(),
1888 )
1889 .unwrap();
1890 let msg = fake_msg(b"[authed-peer]");
1891 let mut hdr_msg = Vec::with_capacity(msg.len());
1894 hdr_msg.extend_from_slice(b"RTPS\x02\x05\x01\x02");
1895 hdr_msg.extend_from_slice(&alice_prefix);
1896 hdr_msg.extend_from_slice(b"payload-body");
1897 let wire = alice.transform_outbound(&hdr_msg).unwrap();
1898 match bob.classify_inbound(&wire, &NetInterface::Wan) {
1899 InboundVerdict::Accept(_) => {}
1900 other => panic!("expected Accept, got {other:?}"),
1901 }
1902 }
1903
1904 #[test]
1905 fn classify_inbound_srtps_with_wrong_key_is_crypto_error() {
1906 let (alice, bob) = build_pair();
1910 let gov = parse_governance_xml(GOV_RTPS).unwrap();
1911 let charlie = SharedSecurityGate::new(0, gov, Box::new(AesGcmCryptoPlugin::new()));
1912
1913 let alice_prefix: PeerKey = [0xAA; 12];
1914 bob.register_remote_by_guid(
1915 alice_prefix,
1916 IdentityHandle(1),
1917 SharedSecretHandle(1),
1918 &alice.local_token().unwrap(),
1919 )
1920 .unwrap();
1921
1922 let mut body = Vec::new();
1924 body.extend_from_slice(b"RTPS\x02\x05\x01\x02");
1925 body.extend_from_slice(&alice_prefix);
1926 body.extend_from_slice(b"mitm-try");
1927 let spoofed = charlie.transform_outbound(&body).unwrap();
1928
1929 let verdict = bob.classify_inbound(&spoofed, &NetInterface::Wan);
1930 assert!(
1931 matches!(verdict, InboundVerdict::CryptoError(_)),
1932 "expected CryptoError, got {verdict:?}"
1933 );
1934 assert_eq!(verdict.category(), "inbound.crypto_error");
1935 }
1936
1937 #[test]
1938 fn transform_outbound_for_is_decodable_with_registered_token() {
1939 let (alice, bob) = build_pair();
1942 let alice_prefix: PeerKey = [0xAA; 12];
1943 bob.register_remote_by_guid(
1944 alice_prefix,
1945 IdentityHandle(1),
1946 SharedSecretHandle(1),
1947 &alice.local_token().unwrap(),
1948 )
1949 .unwrap();
1950 let msg = fake_msg(b"[hetero-e2e]");
1951 let wire = alice
1952 .transform_outbound_for(&[9; 12], &msg, ProtectionLevel::Encrypt)
1953 .unwrap();
1954 let back = bob.transform_inbound_from(&alice_prefix, &wire).unwrap();
1955 assert_eq!(back, msg);
1956 }
1957
1958 struct FixedSecret;
1964 impl zerodds_security::authentication::SharedSecretProvider for FixedSecret {
1965 fn get_shared_secret(
1966 &self,
1967 _h: zerodds_security::authentication::SharedSecretHandle,
1968 ) -> Option<Vec<u8>> {
1969 Some(alloc::vec![0x77u8; 32])
1970 }
1971 }
1972
1973 fn build_kx_pair() -> (SharedSecurityGate, SharedSecurityGate) {
1974 let mk = || {
1975 SharedSecurityGate::new(
1976 0,
1977 parse_governance_xml(GOV_RTPS).unwrap(),
1978 Box::new(AesGcmCryptoPlugin::with_secret_provider(
1979 zerodds_security_crypto::Suite::Aes128Gcm,
1980 Arc::new(FixedSecret)
1981 as Arc<dyn zerodds_security::authentication::SharedSecretProvider>,
1982 )),
1983 )
1984 };
1985 (mk(), mk())
1986 }
1987
1988 #[test]
1989 fn kx_channel_round_trips_through_gate() {
1990 let (alice, bob) = build_kx_pair();
1991 let alice_prefix: PeerKey = [0xAA; 12];
1992 let bob_prefix: PeerKey = [0xBB; 12];
1993 alice
1994 .register_remote_by_guid_from_secret(
1995 bob_prefix,
1996 IdentityHandle(2),
1997 SharedSecretHandle(1),
1998 )
1999 .unwrap();
2000 bob.register_remote_by_guid_from_secret(
2001 alice_prefix,
2002 IdentityHandle(1),
2003 SharedSecretHandle(1),
2004 )
2005 .unwrap();
2006 let token_blob = b"participant-crypto-token-payload";
2008 let wire = alice
2009 .transform_kx_outbound_for(&bob_prefix, token_blob)
2010 .unwrap();
2011 let back = bob.transform_kx_inbound_from(&alice_prefix, &wire).unwrap();
2012 assert_eq!(back, token_blob);
2013 }
2014
2015 #[test]
2016 fn data_round_trips_via_token_after_kx_register() {
2017 let (alice, bob) = build_kx_pair();
2018 let alice_prefix: PeerKey = [0xAA; 12];
2019 let bob_prefix: PeerKey = [0xBB; 12];
2020 alice
2022 .register_remote_by_guid_from_secret(
2023 bob_prefix,
2024 IdentityHandle(2),
2025 SharedSecretHandle(1),
2026 )
2027 .unwrap();
2028 bob.register_remote_by_guid_from_secret(
2029 alice_prefix,
2030 IdentityHandle(1),
2031 SharedSecretHandle(1),
2032 )
2033 .unwrap();
2034 bob.set_remote_data_token_by_guid(&alice_prefix, &alice.local_token().unwrap())
2036 .unwrap();
2037 let msg = fake_msg(b"[secured-user-data]");
2039 let wire = alice
2040 .transform_outbound_for(&bob_prefix, &msg, ProtectionLevel::Encrypt)
2041 .unwrap();
2042 let back = bob.transform_inbound_from(&alice_prefix, &wire).unwrap();
2043 assert_eq!(back, msg);
2044 }
2045}