1extern crate alloc;
23use alloc::vec::Vec;
24use core::time::Duration;
25
26use zerodds_rtps::error::WireError;
27use zerodds_rtps::message_builder::OutboundDatagram;
28use zerodds_rtps::reader_proxy::ReaderProxy;
29use zerodds_rtps::wire_types::{EntityId, Guid, GuidPrefix, Locator, VendorId};
30use zerodds_rtps::writer_proxy::WriterProxy;
31
32use crate::capabilities::PeerCapabilities;
33use crate::security::stateless::{StatelessMessageReader, StatelessMessageWriter};
34use crate::security::volatile_secure::{VolatileSecureMessageReader, VolatileSecureMessageWriter};
35use crate::spdp::DiscoveredParticipant;
36
37#[cfg(feature = "std")]
38use alloc::collections::BTreeMap;
39#[cfg(feature = "std")]
40use alloc::sync::Arc;
41#[cfg(feature = "std")]
42use std::sync::Mutex;
43
44#[cfg(feature = "std")]
45use zerodds_security::authentication::{
46 AuthenticationPlugin, HandshakeHandle, HandshakeStepOutcome, IdentityHandle, SharedSecretHandle,
47};
48#[cfg(feature = "std")]
49use zerodds_security::error::{SecurityError, SecurityErrorKind, SecurityResult};
50#[cfg(feature = "std")]
51use zerodds_security::generic_message::{MessageIdentity, ParticipantGenericMessage, class_id};
52#[cfg(feature = "std")]
53use zerodds_security::token::DataHolder;
54
55#[cfg(feature = "std")]
59pub type HandshakeStepResult = (
60 Vec<OutboundDatagram>,
61 Option<(IdentityHandle, SharedSecretHandle)>,
62);
63
64#[cfg(feature = "std")]
68fn lock_auth<'a>(
69 auth: &'a Arc<Mutex<dyn AuthenticationPlugin + 'static>>,
70) -> SecurityResult<std::sync::MutexGuard<'a, dyn AuthenticationPlugin + 'static>> {
71 auth.lock()
72 .map_err(|_| SecurityError::new(SecurityErrorKind::Internal, "auth mutex poisoned"))
73}
74
75#[cfg(feature = "std")]
82#[derive(Debug)]
83struct PeerHandshake {
84 remote_identity: IdentityHandle,
86 remote_guid: [u8; 16],
88 handshake: Option<HandshakeHandle>,
90 next_sn: i64,
92 secret: Option<SharedSecretHandle>,
94 last_request: Option<ParticipantGenericMessage>,
99}
100
101pub struct SecurityBuiltinStack {
103 local_prefix: GuidPrefix,
104 pub stateless_writer: StatelessMessageWriter,
106 pub stateless_reader: StatelessMessageReader,
108 pub volatile_writer: VolatileSecureMessageWriter,
110 pub volatile_reader: VolatileSecureMessageReader,
112 #[cfg(feature = "std")]
118 auth: Option<Arc<Mutex<dyn AuthenticationPlugin>>>,
119 #[cfg(feature = "std")]
121 local_identity: Option<IdentityHandle>,
122 #[cfg(feature = "std")]
124 local_guid: [u8; 16],
125 #[cfg(feature = "std")]
127 handshakes: BTreeMap<GuidPrefix, PeerHandshake>,
128 #[cfg(feature = "std")]
133 remote_vendors: BTreeMap<GuidPrefix, VendorId>,
134 #[cfg(feature = "std")]
141 last_handshake_resend: Duration,
142}
143
144impl core::fmt::Debug for SecurityBuiltinStack {
145 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
146 let mut dbg = f.debug_struct("SecurityBuiltinStack");
147 dbg.field("local_prefix", &self.local_prefix)
148 .field("stateless_writer", &self.stateless_writer)
149 .field("stateless_reader", &self.stateless_reader)
150 .field("volatile_writer", &self.volatile_writer)
151 .field("volatile_reader", &self.volatile_reader);
152 #[cfg(feature = "std")]
153 dbg.field("auth", &self.auth.is_some())
154 .field("local_identity", &self.local_identity)
155 .field("handshakes", &self.handshakes);
156 dbg.finish()
157 }
158}
159
160impl SecurityBuiltinStack {
161 #[must_use]
164 pub fn new(local_prefix: GuidPrefix, vendor_id: VendorId) -> Self {
165 Self {
166 local_prefix,
167 stateless_writer: StatelessMessageWriter::new(local_prefix, vendor_id),
168 stateless_reader: StatelessMessageReader::new(local_prefix, vendor_id),
169 volatile_writer: VolatileSecureMessageWriter::new(local_prefix, vendor_id),
170 volatile_reader: VolatileSecureMessageReader::new(local_prefix, vendor_id),
171 #[cfg(feature = "std")]
172 auth: None,
173 #[cfg(feature = "std")]
174 local_identity: None,
175 #[cfg(feature = "std")]
176 local_guid: [0; 16],
177 #[cfg(feature = "std")]
178 handshakes: BTreeMap::new(),
179 #[cfg(feature = "std")]
180 remote_vendors: BTreeMap::new(),
181 #[cfg(feature = "std")]
182 last_handshake_resend: Duration::ZERO,
183 }
184 }
185
186 #[cfg(feature = "std")]
195 #[must_use]
196 pub fn with_auth(
197 local_prefix: GuidPrefix,
198 vendor_id: VendorId,
199 auth: Arc<Mutex<dyn AuthenticationPlugin>>,
200 local_identity: IdentityHandle,
201 local_guid: [u8; 16],
202 ) -> Self {
203 Self {
204 local_prefix,
205 stateless_writer: StatelessMessageWriter::new(local_prefix, vendor_id),
206 stateless_reader: StatelessMessageReader::new(local_prefix, vendor_id),
207 volatile_writer: VolatileSecureMessageWriter::new(local_prefix, vendor_id),
208 volatile_reader: VolatileSecureMessageReader::new(local_prefix, vendor_id),
209 auth: Some(auth),
210 local_identity: Some(local_identity),
211 local_guid,
212 handshakes: BTreeMap::new(),
213 remote_vendors: BTreeMap::new(),
214 last_handshake_resend: Duration::ZERO,
215 }
216 }
217
218 #[must_use]
220 pub fn local_prefix(&self) -> GuidPrefix {
221 self.local_prefix
222 }
223
224 pub fn handle_remote_endpoints(&mut self, peer: &DiscoveredParticipant) {
234 if peer.sender_prefix == self.local_prefix {
235 return;
236 }
237 let caps = PeerCapabilities::from_bits(peer.data.builtin_endpoint_set);
238 if !caps.has_stateless_auth && !caps.has_volatile_secure {
239 return;
240 }
241 let unicast: Vec<Locator> = peer
242 .data
243 .metatraffic_unicast_locator
244 .or(peer.data.default_unicast_locator)
245 .into_iter()
246 .collect();
247 let remote_prefix = peer.sender_prefix;
248
249 if caps.has_stateless_auth {
250 self.stateless_writer.add_reader_proxy(ReaderProxy::new(
251 Guid::new(
252 remote_prefix,
253 EntityId::BUILTIN_PARTICIPANT_STATELESS_MESSAGE_READER,
254 ),
255 unicast.clone(),
256 Vec::new(),
257 false,
258 ));
259 self.stateless_reader.add_writer_proxy(WriterProxy::new(
260 Guid::new(
261 remote_prefix,
262 EntityId::BUILTIN_PARTICIPANT_STATELESS_MESSAGE_WRITER,
263 ),
264 unicast.clone(),
265 Vec::new(),
266 false,
267 ));
268 }
269
270 if caps.has_volatile_secure {
271 self.volatile_writer.add_reader_proxy(ReaderProxy::new(
272 Guid::new(
273 remote_prefix,
274 EntityId::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER,
275 ),
276 unicast.clone(),
277 Vec::new(),
278 true,
279 ));
280 self.volatile_reader.add_writer_proxy(WriterProxy::new(
281 Guid::new(
282 remote_prefix,
283 EntityId::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER,
284 ),
285 unicast,
286 Vec::new(),
287 true,
288 ));
289 }
290 }
291
292 pub fn on_participant_lost(&mut self, prefix: GuidPrefix) -> (usize, usize) {
296 let mut stateless = 0usize;
297 let mut volatile = 0usize;
298 #[cfg(feature = "std")]
299 self.handshakes.remove(&prefix);
300 if self
301 .stateless_writer
302 .remove_reader_proxy(Guid::new(
303 prefix,
304 EntityId::BUILTIN_PARTICIPANT_STATELESS_MESSAGE_READER,
305 ))
306 .is_some()
307 {
308 stateless += 1;
309 }
310 self.stateless_reader.remove_writer_proxy(Guid::new(
311 prefix,
312 EntityId::BUILTIN_PARTICIPANT_STATELESS_MESSAGE_WRITER,
313 ));
314 if self
315 .volatile_writer
316 .remove_reader_proxy(Guid::new(
317 prefix,
318 EntityId::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER,
319 ))
320 .is_some()
321 {
322 volatile += 1;
323 }
324 self.volatile_reader.remove_writer_proxy(Guid::new(
325 prefix,
326 EntityId::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER,
327 ));
328 (stateless, volatile)
329 }
330
331 pub fn poll(&mut self, now: Duration) -> Result<Vec<OutboundDatagram>, WireError> {
338 let mut out = Vec::new();
339 out.extend(self.volatile_writer.tick(now)?);
340 out.extend(self.volatile_reader.tick_outbound(now)?);
341 #[cfg(feature = "std")]
349 if now.saturating_sub(self.last_handshake_resend) >= Duration::from_millis(500) {
350 self.last_handshake_resend = now;
351 let pending: Vec<ParticipantGenericMessage> = self
352 .handshakes
353 .values()
354 .filter(|p| p.secret.is_none())
355 .filter_map(|p| p.last_request.clone())
356 .collect();
357 for msg in &pending {
358 out.extend(self.stateless_writer.write(msg)?);
359 }
360 }
361 Ok(out)
362 }
363
364 #[cfg(feature = "std")]
368 #[must_use]
369 pub fn peer_secret(&self, remote_prefix: GuidPrefix) -> Option<SharedSecretHandle> {
370 self.handshakes.get(&remote_prefix).and_then(|p| p.secret)
371 }
372
373 #[cfg(feature = "std")]
379 #[must_use]
380 pub fn completed_peer_prefixes(&self) -> Vec<GuidPrefix> {
381 self.handshakes
382 .iter()
383 .filter(|(_, p)| p.secret.is_some())
384 .map(|(prefix, _)| *prefix)
385 .collect()
386 }
387
388 #[cfg(feature = "std")]
412 pub fn note_remote_vendor(&mut self, remote_prefix: GuidPrefix, vendor: VendorId) {
413 self.remote_vendors.insert(remote_prefix, vendor);
414 }
415
416 #[cfg(feature = "std")]
420 pub fn begin_handshake_with(
421 &mut self,
422 remote_prefix: GuidPrefix,
423 remote_guid: [u8; 16],
424 remote_identity_token: &[u8],
425 ) -> SecurityResult<Vec<OutboundDatagram>> {
426 let (auth, local_identity) = match (self.auth.clone(), self.local_identity) {
427 (Some(a), Some(id)) => (a, id),
428 _ => return Ok(Vec::new()),
429 };
430 if let Some(peer) = self.handshakes.get(&remote_prefix) {
438 if peer.secret.is_none() {
439 if let Some(req) = peer.last_request.clone() {
440 return self.stateless_writer.write(&req).map_err(wire_to_security);
441 }
442 }
443 return Ok(Vec::new());
444 }
445 let is_initiator = self.local_guid < remote_guid;
450
451 let (remote_identity, request) = {
452 let mut plugin = lock_auth(&auth)?;
453 plugin.set_algo_nul_terminate(
456 self.remote_vendors.get(&remote_prefix) == Some(&VendorId::OPENDDS),
457 );
458 let remote_identity = plugin.validate_remote_identity(
459 local_identity,
460 remote_guid,
461 remote_identity_token,
462 )?;
463 let request = if is_initiator {
464 Some(plugin.begin_handshake_request(local_identity, remote_identity)?)
465 } else {
466 None
467 };
468 (remote_identity, request)
469 };
470
471 let mut peer = PeerHandshake {
472 remote_identity,
473 remote_guid,
474 handshake: None,
475 next_sn: 1,
476 secret: None,
477 last_request: None,
478 };
479
480 let mut datagrams = Vec::new();
481 if let Some((handle, outcome)) = request {
482 peer.handshake = Some(handle);
483 if let HandshakeStepOutcome::SendMessage { token } = outcome {
484 let msg = peer.build_message(
491 self.local_guid,
492 class_id::AUTH,
493 token,
494 MessageIdentity::default(),
495 )?;
496 peer.last_request = Some(msg.clone());
498 datagrams = self
499 .stateless_writer
500 .write(&msg)
501 .map_err(wire_to_security)?;
502 }
503 }
504 self.handshakes.insert(remote_prefix, peer);
505 Ok(datagrams)
506 }
507
508 #[cfg(feature = "std")]
512 #[must_use]
513 pub fn handshake_peer_count(&self) -> usize {
514 self.handshakes.len()
515 }
516
517 #[cfg(feature = "std")]
540 pub fn on_stateless_message(
541 &mut self,
542 remote_prefix: GuidPrefix,
543 msg: &ParticipantGenericMessage,
544 ) -> SecurityResult<HandshakeStepResult> {
545 let (auth, local_identity) = match (self.auth.clone(), self.local_identity) {
546 (Some(a), Some(id)) => (a, id),
547 _ => return Ok((Vec::new(), None)),
548 };
549 let local_guid = self.local_guid;
550 if msg.message_class_id == class_id::AUTH_REQUEST {
558 return Ok((Vec::new(), None));
559 }
560 let is_request = msg.message_class_id == class_id::AUTH
567 && msg.related_message_identity == MessageIdentity::default();
568 let mut peer = match self.handshakes.remove(&remote_prefix) {
569 Some(p) => p,
570 None => {
571 if !is_request {
580 return Ok((Vec::new(), None));
581 }
582 PeerHandshake {
583 remote_identity: local_identity,
588 remote_guid: msg.message_identity.source_guid,
590 handshake: None,
591 next_sn: 1,
592 secret: None,
593 last_request: None,
594 }
595 }
596 };
597 let token = match msg.message_data.first() {
598 Some(dh) => dh.to_cdr_le(),
599 None => {
600 self.handshakes.insert(remote_prefix, peer);
601 return Ok((Vec::new(), None));
602 }
603 };
604
605 if is_request && peer.handshake.is_some() {
611 let resend = peer.last_request.clone();
612 self.handshakes.insert(remote_prefix, peer);
613 return match resend {
614 Some(r) => self
615 .stateless_writer
616 .write(&r)
617 .map(|d| (d, None))
618 .map_err(wire_to_security),
619 None => Ok((Vec::new(), None)),
620 };
621 }
622
623 let outcome = {
624 let mut plugin = lock_auth(&auth)?;
625 plugin.set_algo_nul_terminate(
628 self.remote_vendors.get(&remote_prefix) == Some(&VendorId::OPENDDS),
629 );
630 if is_request {
631 let (handle, outcome) =
632 plugin.begin_handshake_reply(local_identity, peer.remote_identity, &token)?;
633 peer.handshake = Some(handle);
634 outcome
635 } else {
636 match peer.handshake {
637 Some(handle) => plugin.process_handshake(handle, &token)?,
638 None => {
639 drop(plugin);
640 self.handshakes.insert(remote_prefix, peer);
641 return Ok((Vec::new(), None));
642 }
643 }
644 }
645 };
646
647 let mut datagrams = Vec::new();
648 if let HandshakeStepOutcome::SendMessage { token: out_token } = outcome {
649 let response = peer.build_message(
650 local_guid,
651 class_id::AUTH,
652 out_token,
653 msg.message_identity.clone(),
654 )?;
655 peer.last_request = Some(response.clone());
658 datagrams = self
659 .stateless_writer
660 .write(&response)
661 .map_err(wire_to_security)?;
662 }
663
664 let mut completed = None;
669 if peer.secret.is_none() {
670 if let Some(handle) = peer.handshake {
671 let secret = lock_auth(&auth)?.shared_secret(handle).ok();
672 if let Some(secret) = secret {
673 peer.secret = Some(secret);
674 completed = Some((peer.remote_identity, secret));
675 }
676 }
677 }
678
679 self.handshakes.insert(remote_prefix, peer);
680 Ok((datagrams, completed))
681 }
682}
683
684#[cfg(feature = "std")]
686fn wire_to_security(_e: WireError) -> zerodds_security::error::SecurityError {
687 zerodds_security::error::SecurityError::new(
688 zerodds_security::error::SecurityErrorKind::BadArgument,
689 "stateless handshake: wire encode failed",
690 )
691}
692
693#[cfg(feature = "std")]
694impl PeerHandshake {
695 fn build_message(
699 &mut self,
700 local_guid: [u8; 16],
701 message_class: &str,
702 token: Vec<u8>,
703 related: MessageIdentity,
704 ) -> SecurityResult<ParticipantGenericMessage> {
705 let sequence_number = self.next_sn;
706 self.next_sn = self.next_sn.saturating_add(1);
707 let holder = DataHolder::from_cdr_le(&token)?;
711 Ok(ParticipantGenericMessage {
712 message_identity: MessageIdentity {
713 source_guid: local_guid,
714 sequence_number,
715 },
716 related_message_identity: related,
717 destination_participant_key: self.remote_guid,
718 destination_endpoint_key: [0; 16],
719 source_endpoint_key: [0; 16],
720 message_class_id: message_class.into(),
721 message_data: alloc::vec![holder],
722 })
723 }
724}
725
726#[cfg(test)]
727#[allow(clippy::expect_used, clippy::unwrap_used)]
728mod tests {
729 use super::*;
730 use zerodds_rtps::participant_data::{
731 Duration as DdsDuration, ParticipantBuiltinTopicData, endpoint_flag,
732 };
733 use zerodds_rtps::wire_types::ProtocolVersion;
734 use zerodds_security::generic_message::{MessageIdentity, ParticipantGenericMessage, class_id};
735 use zerodds_security::token::DataHolder;
736
737 fn local_prefix() -> GuidPrefix {
741 GuidPrefix::from_bytes([1; 12])
742 }
743 fn remote_prefix() -> GuidPrefix {
744 GuidPrefix::from_bytes([2; 12])
745 }
746
747 fn remote_with(flags: u32) -> DiscoveredParticipant {
748 DiscoveredParticipant {
749 sender_prefix: remote_prefix(),
750 sender_vendor: VendorId::ZERODDS,
751 data: ParticipantBuiltinTopicData {
752 guid: Guid::new(remote_prefix(), EntityId::PARTICIPANT),
753 protocol_version: ProtocolVersion::V2_5,
754 vendor_id: VendorId::ZERODDS,
755 default_unicast_locator: Some(Locator::udp_v4([127, 0, 0, 99], 7411)),
756 default_multicast_locator: None,
757 metatraffic_unicast_locator: None,
758 metatraffic_multicast_locator: None,
759 domain_id: None,
760 builtin_endpoint_set: flags,
761 lease_duration: DdsDuration::from_secs(30),
762 user_data: alloc::vec::Vec::new(),
763 properties: Default::default(),
764 identity_token: None,
765 permissions_token: None,
766 identity_status_token: None,
767 sig_algo_info: None,
768 kx_algo_info: None,
769 sym_cipher_algo_info: None,
770 participant_security_info: None,
771 },
772 }
773 }
774
775 fn sample_stateless_msg() -> ParticipantGenericMessage {
776 ParticipantGenericMessage {
777 message_identity: MessageIdentity {
778 source_guid: [0xAA; 16],
779 sequence_number: 1,
780 },
781 related_message_identity: MessageIdentity::default(),
782 destination_participant_key: [0xBB; 16],
783 destination_endpoint_key: [0; 16],
784 source_endpoint_key: [0xCC; 16],
785 message_class_id: class_id::AUTH_REQUEST.into(),
786 message_data: alloc::vec![DataHolder::new("DDS:Auth:PKI-DH:1.2+AuthReq")],
787 }
788 }
789
790 #[test]
791 fn new_stack_has_zero_proxies_everywhere() {
792 let s = SecurityBuiltinStack::new(local_prefix(), VendorId::ZERODDS);
793 assert_eq!(s.stateless_writer.reader_proxy_count(), 0);
794 assert_eq!(s.stateless_reader.writer_proxy_count(), 0);
795 assert_eq!(s.volatile_writer.reader_proxy_count(), 0);
796 assert_eq!(s.volatile_reader.writer_proxy_count(), 0);
797 assert_eq!(s.local_prefix(), local_prefix());
798 }
799
800 #[test]
801 fn handle_remote_endpoints_with_all_bits_wires_all_four() {
802 let mut s = SecurityBuiltinStack::new(local_prefix(), VendorId::ZERODDS);
803 let flags = endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER
804 | endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER
805 | endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER
806 | endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER;
807 s.handle_remote_endpoints(&remote_with(flags));
808 assert_eq!(s.stateless_writer.reader_proxy_count(), 1);
809 assert_eq!(s.stateless_reader.writer_proxy_count(), 1);
810 assert_eq!(s.volatile_writer.reader_proxy_count(), 1);
811 assert_eq!(s.volatile_reader.writer_proxy_count(), 1);
812 }
813
814 #[test]
815 fn handle_remote_endpoints_with_only_stateless_bits_skips_volatile() {
816 let mut s = SecurityBuiltinStack::new(local_prefix(), VendorId::ZERODDS);
817 let flags = endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER
818 | endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER;
819 s.handle_remote_endpoints(&remote_with(flags));
820 assert_eq!(s.stateless_writer.reader_proxy_count(), 1);
821 assert_eq!(s.stateless_reader.writer_proxy_count(), 1);
822 assert_eq!(s.volatile_writer.reader_proxy_count(), 0);
823 assert_eq!(s.volatile_reader.writer_proxy_count(), 0);
824 }
825
826 #[test]
827 fn handle_remote_endpoints_with_no_security_bits_is_noop() {
828 let mut s = SecurityBuiltinStack::new(local_prefix(), VendorId::ZERODDS);
829 let flags = endpoint_flag::ALL_STANDARD;
830 s.handle_remote_endpoints(&remote_with(flags));
831 assert_eq!(s.stateless_writer.reader_proxy_count(), 0);
832 assert_eq!(s.volatile_writer.reader_proxy_count(), 0);
833 }
834
835 #[test]
836 fn self_discovery_is_ignored() {
837 let mut s = SecurityBuiltinStack::new(local_prefix(), VendorId::ZERODDS);
838 let mut peer = remote_with(endpoint_flag::ALL_SECURE);
839 peer.sender_prefix = local_prefix();
840 s.handle_remote_endpoints(&peer);
841 assert_eq!(s.stateless_writer.reader_proxy_count(), 0);
842 }
843
844 #[test]
845 fn handle_remote_endpoints_is_idempotent_on_repeat_announcement() {
846 let mut s = SecurityBuiltinStack::new(local_prefix(), VendorId::ZERODDS);
847 let flags = endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER
848 | endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER;
849 s.handle_remote_endpoints(&remote_with(flags));
850 s.handle_remote_endpoints(&remote_with(flags));
851 assert_eq!(s.stateless_writer.reader_proxy_count(), 1);
852 }
853
854 #[test]
855 fn on_participant_lost_clears_proxies() {
856 let mut s = SecurityBuiltinStack::new(local_prefix(), VendorId::ZERODDS);
857 let flags = endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER
858 | endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER
859 | endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER
860 | endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER;
861 s.handle_remote_endpoints(&remote_with(flags));
862 let (sl, vol) = s.on_participant_lost(remote_prefix());
863 assert_eq!(sl, 1);
864 assert_eq!(vol, 1);
865 assert_eq!(s.stateless_writer.reader_proxy_count(), 0);
866 assert_eq!(s.volatile_writer.reader_proxy_count(), 0);
867 }
868
869 #[test]
870 fn poll_on_empty_stack_returns_no_datagrams() {
871 let mut s = SecurityBuiltinStack::new(local_prefix(), VendorId::ZERODDS);
872 let dgs = s.poll(Duration::from_secs(1)).unwrap();
873 assert!(dgs.is_empty());
874 }
875
876 #[test]
877 fn end_to_end_stateless_message_loopback_between_stacks() {
878 let mut a = SecurityBuiltinStack::new(local_prefix(), VendorId::ZERODDS);
879 let mut b = SecurityBuiltinStack::new(remote_prefix(), VendorId::ZERODDS);
880 let flags = endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER
881 | endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER;
882 a.handle_remote_endpoints(&remote_with_prefix(remote_prefix(), flags));
884 b.handle_remote_endpoints(&remote_with_prefix(local_prefix(), flags));
886
887 let msg = sample_stateless_msg();
888 let dgs = a.stateless_writer.write(&msg).unwrap();
889 assert_eq!(dgs.len(), 1);
890 let received = b.stateless_reader.handle_datagram(&dgs[0].bytes).unwrap();
891 assert_eq!(received.len(), 1);
892 assert_eq!(received[0], msg);
893 }
894
895 fn remote_with_prefix(prefix: GuidPrefix, flags: u32) -> DiscoveredParticipant {
896 let mut peer = remote_with(flags);
897 peer.sender_prefix = prefix;
898 peer.data.guid = Guid::new(prefix, EntityId::PARTICIPANT);
899 peer
900 }
901
902 #[cfg(feature = "std")]
908 #[allow(clippy::type_complexity)]
909 fn mint_ca_and_two_leafs() -> (Vec<u8>, (Vec<u8>, Vec<u8>), (Vec<u8>, Vec<u8>)) {
910 use alloc::string::String;
911 use rcgen::{CertificateParams, KeyPair};
912 let mut ca_params = CertificateParams::new(std::vec![String::from("Common CA")]).unwrap();
913 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
914 let ca_key = KeyPair::generate().unwrap();
915 let ca_cert = ca_params.self_signed(&ca_key).unwrap();
916 let ca_pem = ca_cert.pem().into_bytes();
917
918 let mint_leaf = |name: &str| -> (Vec<u8>, Vec<u8>) {
919 let mut params = CertificateParams::new(std::vec![String::from(name)]).unwrap();
920 params.is_ca = rcgen::IsCa::NoCa;
921 let key = KeyPair::generate().unwrap();
922 let cert = params.signed_by(&key, &ca_cert, &ca_key).unwrap();
923 (cert.pem().into_bytes(), key.serialize_pem().into_bytes())
924 };
925 (ca_pem, mint_leaf("alice"), mint_leaf("bob"))
926 }
927
928 #[cfg(feature = "std")]
929 fn cert_der_from_pem(pem: &[u8]) -> Vec<u8> {
930 use rustls_pki_types::CertificateDer;
931 use rustls_pki_types::pem::PemObject;
932 CertificateDer::pem_slice_iter(pem)
933 .next()
934 .unwrap()
935 .unwrap()
936 .as_ref()
937 .to_vec()
938 }
939
940 #[cfg(feature = "std")]
941 #[test]
942 fn full_pki_handshake_between_two_stacks_yields_shared_secret() {
943 use alloc::sync::Arc;
944 use std::sync::Mutex;
945 use zerodds_security::authentication::AuthenticationPlugin;
946 use zerodds_security_pki::{IdentityConfig, PkiAuthenticationPlugin};
947
948 let (ca_pem, (a_cert, a_key), (b_cert, b_key)) = mint_ca_and_two_leafs();
949
950 let a_guid = Guid::new(local_prefix(), EntityId::PARTICIPANT).to_bytes();
951 let b_guid = Guid::new(remote_prefix(), EntityId::PARTICIPANT).to_bytes();
952
953 let a_pki = Arc::new(Mutex::new(PkiAuthenticationPlugin::new()));
955 let b_pki = Arc::new(Mutex::new(PkiAuthenticationPlugin::new()));
956 let a_local = a_pki
957 .lock()
958 .unwrap()
959 .validate_with_config(
960 IdentityConfig {
961 identity_cert_pem: a_cert.clone(),
962 identity_ca_pem: ca_pem.clone(),
963 identity_key_pem: Some(a_key),
964 },
965 a_guid,
966 )
967 .unwrap();
968 let b_local = b_pki
969 .lock()
970 .unwrap()
971 .validate_with_config(
972 IdentityConfig {
973 identity_cert_pem: b_cert.clone(),
974 identity_ca_pem: ca_pem,
975 identity_key_pem: Some(b_key),
976 },
977 b_guid,
978 )
979 .unwrap();
980
981 let a_auth: Arc<Mutex<dyn AuthenticationPlugin>> = a_pki.clone();
982 let b_auth: Arc<Mutex<dyn AuthenticationPlugin>> = b_pki.clone();
983 let mut a = SecurityBuiltinStack::with_auth(
984 local_prefix(),
985 VendorId::ZERODDS,
986 a_auth,
987 a_local,
988 a_guid,
989 );
990 let mut b = SecurityBuiltinStack::with_auth(
991 remote_prefix(),
992 VendorId::ZERODDS,
993 b_auth,
994 b_local,
995 b_guid,
996 );
997
998 let flags = endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER
999 | endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER;
1000 a.handle_remote_endpoints(&remote_with_prefix(remote_prefix(), flags));
1001 b.handle_remote_endpoints(&remote_with_prefix(local_prefix(), flags));
1002
1003 let mut in_flight = a
1006 .begin_handshake_with(remote_prefix(), b_guid, &cert_der_from_pem(&b_cert))
1007 .unwrap();
1008 let from_b = b
1009 .begin_handshake_with(local_prefix(), a_guid, &cert_der_from_pem(&a_cert))
1010 .unwrap();
1011 assert!(
1012 from_b.is_empty(),
1013 "B is the replier (larger prefix), must not initiate"
1014 );
1015 assert_eq!(in_flight.len(), 1, "A sends exactly one AUTH_REQUEST");
1016
1017 let mut a_secret = None;
1020 let mut b_secret = None;
1021 let mut deliver_to_b = true; for _ in 0..6 {
1023 if in_flight.is_empty() {
1024 break;
1025 }
1026 let datagram = in_flight.remove(0);
1027 let (target, target_prefix) = if deliver_to_b {
1028 (&mut b, local_prefix())
1029 } else {
1030 (&mut a, remote_prefix())
1031 };
1032 let msgs = target
1033 .stateless_reader
1034 .handle_datagram(&datagram.bytes)
1035 .unwrap();
1036 assert_eq!(msgs.len(), 1, "ein generic-message pro Datagram");
1037 let (out, completed) = target
1038 .on_stateless_message(target_prefix, &msgs[0])
1039 .unwrap();
1040 if let Some((_id, secret)) = completed {
1041 if deliver_to_b {
1042 b_secret = Some(secret);
1043 } else {
1044 a_secret = Some(secret);
1045 }
1046 }
1047 in_flight = out;
1048 deliver_to_b = !deliver_to_b;
1049 }
1050
1051 let a_secret = a_secret.expect("A must derive a secret");
1052 let b_secret = b_secret.expect("B must derive a secret");
1053 let a_bytes = a_pki
1054 .lock()
1055 .unwrap()
1056 .secret_bytes(a_secret)
1057 .unwrap()
1058 .to_vec();
1059 let b_bytes = b_pki
1060 .lock()
1061 .unwrap()
1062 .secret_bytes(b_secret)
1063 .unwrap()
1064 .to_vec();
1065 assert_eq!(a_bytes.len(), 32);
1066 assert_eq!(a_bytes, b_bytes, "both stacks derive the same secret");
1067 }
1068
1069 #[cfg(feature = "std")]
1070 #[test]
1071 fn smaller_local_guid_initiates_handshake_cyclone_compatible() {
1072 use alloc::sync::Arc;
1077 use std::sync::Mutex;
1078 use zerodds_security::authentication::AuthenticationPlugin;
1079 use zerodds_security_pki::{IdentityConfig, PkiAuthenticationPlugin};
1080
1081 let (ca_pem, (big_cert, big_key), (small_cert, small_key)) = mint_ca_and_two_leafs();
1082 let big_prefix = GuidPrefix::from_bytes([0x09; 12]);
1083 let small_prefix = GuidPrefix::from_bytes([0x01; 12]);
1084 let big_guid = Guid::new(big_prefix, EntityId::PARTICIPANT).to_bytes();
1085 let small_guid = Guid::new(small_prefix, EntityId::PARTICIPANT).to_bytes();
1086 assert!(big_guid > small_guid, "test setup: big must be > small");
1087
1088 let mk = |cert: Vec<u8>, key: Vec<u8>, prefix: GuidPrefix, guid: [u8; 16]| {
1089 let pki = Arc::new(Mutex::new(PkiAuthenticationPlugin::new()));
1090 let local = pki
1091 .lock()
1092 .unwrap()
1093 .validate_with_config(
1094 IdentityConfig {
1095 identity_cert_pem: cert,
1096 identity_ca_pem: ca_pem.clone(),
1097 identity_key_pem: Some(key),
1098 },
1099 guid,
1100 )
1101 .unwrap();
1102 let auth: Arc<Mutex<dyn AuthenticationPlugin>> = pki.clone();
1103 SecurityBuiltinStack::with_auth(prefix, VendorId::ZERODDS, auth, local, guid)
1104 };
1105
1106 let flags = endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER
1107 | endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER;
1108
1109 let mut small = mk(small_cert.clone(), small_key, small_prefix, small_guid);
1111 small.handle_remote_endpoints(&remote_with_prefix(big_prefix, flags));
1112 let from_small = small
1113 .begin_handshake_with(big_prefix, big_guid, &cert_der_from_pem(&big_cert))
1114 .unwrap();
1115 assert_eq!(
1116 from_small.len(),
1117 1,
1118 "smaller local GUID MUST send AUTH_REQUEST (initiator, cyclone convention)"
1119 );
1120
1121 let mut big = mk(big_cert, big_key, big_prefix, big_guid);
1123 big.handle_remote_endpoints(&remote_with_prefix(small_prefix, flags));
1124 let from_big = big
1125 .begin_handshake_with(small_prefix, small_guid, &cert_der_from_pem(&small_cert))
1126 .unwrap();
1127 assert!(
1128 from_big.is_empty(),
1129 "larger local GUID MUST wait (replier), must not initiate"
1130 );
1131 }
1132
1133 #[test]
1134 fn initiator_resends_auth_request_on_repeated_begin_while_incomplete() {
1135 use alloc::sync::Arc;
1141 use std::sync::Mutex;
1142 use zerodds_security::authentication::AuthenticationPlugin;
1143 use zerodds_security_pki::{IdentityConfig, PkiAuthenticationPlugin};
1144
1145 let (ca_pem, (a_cert, a_key), (b_cert, _b_key)) = mint_ca_and_two_leafs();
1146 let a_guid = Guid::new(local_prefix(), EntityId::PARTICIPANT).to_bytes();
1147 let b_guid = Guid::new(remote_prefix(), EntityId::PARTICIPANT).to_bytes();
1148
1149 let a_pki = Arc::new(Mutex::new(PkiAuthenticationPlugin::new()));
1150 let a_local = a_pki
1151 .lock()
1152 .unwrap()
1153 .validate_with_config(
1154 IdentityConfig {
1155 identity_cert_pem: a_cert.clone(),
1156 identity_ca_pem: ca_pem,
1157 identity_key_pem: Some(a_key),
1158 },
1159 a_guid,
1160 )
1161 .unwrap();
1162 let a_auth: Arc<Mutex<dyn AuthenticationPlugin>> = a_pki.clone();
1163 let mut a = SecurityBuiltinStack::with_auth(
1164 local_prefix(),
1165 VendorId::ZERODDS,
1166 a_auth,
1167 a_local,
1168 a_guid,
1169 );
1170
1171 let flags = endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER
1172 | endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER;
1173 a.handle_remote_endpoints(&remote_with_prefix(remote_prefix(), flags));
1174
1175 let b_token = cert_der_from_pem(&b_cert);
1176 let first = a
1177 .begin_handshake_with(remote_prefix(), b_guid, &b_token)
1178 .unwrap();
1179 assert_eq!(first.len(), 1, "first beacon: AUTH_REQUEST sent");
1180
1181 let resend = a
1184 .begin_handshake_with(remote_prefix(), b_guid, &b_token)
1185 .unwrap();
1186 assert_eq!(
1187 resend.len(),
1188 1,
1189 "second beacon: AUTH_REQUEST must be sent again (resend, not idempotent-empty)"
1190 );
1191 }
1192
1193 #[test]
1194 fn lost_reply_recovered_by_resend_yields_matching_secret() {
1195 use alloc::sync::Arc;
1200 use std::sync::Mutex;
1201 use zerodds_security::authentication::AuthenticationPlugin;
1202 use zerodds_security_pki::{IdentityConfig, PkiAuthenticationPlugin};
1203
1204 let (ca_pem, (a_cert, a_key), (b_cert, b_key)) = mint_ca_and_two_leafs();
1205 let a_guid = Guid::new(local_prefix(), EntityId::PARTICIPANT).to_bytes();
1206 let b_guid = Guid::new(remote_prefix(), EntityId::PARTICIPANT).to_bytes();
1207 let a_pki = Arc::new(Mutex::new(PkiAuthenticationPlugin::new()));
1208 let b_pki = Arc::new(Mutex::new(PkiAuthenticationPlugin::new()));
1209 let a_local = a_pki
1210 .lock()
1211 .unwrap()
1212 .validate_with_config(
1213 IdentityConfig {
1214 identity_cert_pem: a_cert.clone(),
1215 identity_ca_pem: ca_pem.clone(),
1216 identity_key_pem: Some(a_key),
1217 },
1218 a_guid,
1219 )
1220 .unwrap();
1221 let b_local = b_pki
1222 .lock()
1223 .unwrap()
1224 .validate_with_config(
1225 IdentityConfig {
1226 identity_cert_pem: b_cert.clone(),
1227 identity_ca_pem: ca_pem,
1228 identity_key_pem: Some(b_key),
1229 },
1230 b_guid,
1231 )
1232 .unwrap();
1233 let a_auth: Arc<Mutex<dyn AuthenticationPlugin>> = a_pki.clone();
1234 let b_auth: Arc<Mutex<dyn AuthenticationPlugin>> = b_pki.clone();
1235 let mut a = SecurityBuiltinStack::with_auth(
1236 local_prefix(),
1237 VendorId::ZERODDS,
1238 a_auth,
1239 a_local,
1240 a_guid,
1241 );
1242 let mut b = SecurityBuiltinStack::with_auth(
1243 remote_prefix(),
1244 VendorId::ZERODDS,
1245 b_auth,
1246 b_local,
1247 b_guid,
1248 );
1249 let flags = endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER
1250 | endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER;
1251 a.handle_remote_endpoints(&remote_with_prefix(remote_prefix(), flags));
1252 b.handle_remote_endpoints(&remote_with_prefix(local_prefix(), flags));
1253
1254 let mut a_secret = None;
1255 let mut b_secret = None;
1256
1257 let from_b = b
1260 .begin_handshake_with(local_prefix(), a_guid, &cert_der_from_pem(&a_cert))
1261 .unwrap();
1262 assert!(from_b.is_empty(), "B is the replier, does not initiate");
1263
1264 let req = a
1266 .begin_handshake_with(remote_prefix(), b_guid, &cert_der_from_pem(&b_cert))
1267 .unwrap();
1268 let m = b.stateless_reader.handle_datagram(&req[0].bytes).unwrap();
1269 let (_lost, b_c0) = b.on_stateless_message(local_prefix(), &m[0]).unwrap();
1270 b_secret = b_secret.or(b_c0.map(|x| x.1));
1271
1272 let req2 = a
1274 .begin_handshake_with(remote_prefix(), b_guid, &cert_der_from_pem(&b_cert))
1275 .unwrap();
1276 assert_eq!(req2.len(), 1, "AUTH_REQUEST must be resent");
1277
1278 let m2 = b.stateless_reader.handle_datagram(&req2[0].bytes).unwrap();
1280 let (reply, b_c1) = b.on_stateless_message(local_prefix(), &m2[0]).unwrap();
1281 b_secret = b_secret.or(b_c1.map(|x| x.1));
1282 assert_eq!(reply.len(), 1, "B resends the cached reply");
1283
1284 let mut in_flight = reply;
1285 let mut deliver_to_b = false; for _ in 0..6 {
1287 if in_flight.is_empty() {
1288 break;
1289 }
1290 let dg = in_flight.remove(0);
1291 let (target, target_prefix) = if deliver_to_b {
1292 (&mut b, local_prefix())
1293 } else {
1294 (&mut a, remote_prefix())
1295 };
1296 let msgs = target.stateless_reader.handle_datagram(&dg.bytes).unwrap();
1297 let (out, completed) = target
1298 .on_stateless_message(target_prefix, &msgs[0])
1299 .unwrap();
1300 if let Some((_id, secret)) = completed {
1301 if deliver_to_b {
1302 b_secret = Some(secret);
1303 } else {
1304 a_secret = Some(secret);
1305 }
1306 }
1307 in_flight = out;
1308 deliver_to_b = !deliver_to_b;
1309 }
1310
1311 let a_secret = a_secret.expect("A must derive a secret");
1312 let b_secret = b_secret.expect("B must derive a secret");
1313 let a_bytes = a_pki
1314 .lock()
1315 .unwrap()
1316 .secret_bytes(a_secret)
1317 .unwrap()
1318 .to_vec();
1319 let b_bytes = b_pki
1320 .lock()
1321 .unwrap()
1322 .secret_bytes(b_secret)
1323 .unwrap()
1324 .to_vec();
1325 assert_eq!(
1326 a_bytes, b_bytes,
1327 "secret after reply-loss+resend must match (B must NOT have regenerated)"
1328 );
1329 }
1330
1331 #[test]
1332 fn replier_handles_auth_request_without_prior_discovery() {
1333 use alloc::sync::Arc;
1340 use std::sync::Mutex;
1341 use zerodds_security::authentication::AuthenticationPlugin;
1342 use zerodds_security_pki::{IdentityConfig, PkiAuthenticationPlugin};
1343
1344 let (ca_pem, (a_cert, a_key), (b_cert, b_key)) = mint_ca_and_two_leafs();
1345 let a_guid = Guid::new(local_prefix(), EntityId::PARTICIPANT).to_bytes();
1346 let b_guid = Guid::new(remote_prefix(), EntityId::PARTICIPANT).to_bytes();
1347 let a_pki = Arc::new(Mutex::new(PkiAuthenticationPlugin::new()));
1348 let b_pki = Arc::new(Mutex::new(PkiAuthenticationPlugin::new()));
1349 let a_local = a_pki
1350 .lock()
1351 .unwrap()
1352 .validate_with_config(
1353 IdentityConfig {
1354 identity_cert_pem: a_cert.clone(),
1355 identity_ca_pem: ca_pem.clone(),
1356 identity_key_pem: Some(a_key),
1357 },
1358 a_guid,
1359 )
1360 .unwrap();
1361 let b_local = b_pki
1362 .lock()
1363 .unwrap()
1364 .validate_with_config(
1365 IdentityConfig {
1366 identity_cert_pem: b_cert.clone(),
1367 identity_ca_pem: ca_pem,
1368 identity_key_pem: Some(b_key),
1369 },
1370 b_guid,
1371 )
1372 .unwrap();
1373 let a_auth: Arc<Mutex<dyn AuthenticationPlugin>> = a_pki.clone();
1374 let b_auth: Arc<Mutex<dyn AuthenticationPlugin>> = b_pki.clone();
1375 let mut a = SecurityBuiltinStack::with_auth(
1376 local_prefix(),
1377 VendorId::ZERODDS,
1378 a_auth,
1379 a_local,
1380 a_guid,
1381 );
1382 let mut b = SecurityBuiltinStack::with_auth(
1383 remote_prefix(),
1384 VendorId::ZERODDS,
1385 b_auth,
1386 b_local,
1387 b_guid,
1388 );
1389 let flags = endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_WRITER
1390 | endpoint_flag::PARTICIPANT_STATELESS_MESSAGE_READER;
1391 a.handle_remote_endpoints(&remote_with_prefix(remote_prefix(), flags));
1392 b.handle_remote_endpoints(&remote_with_prefix(local_prefix(), flags));
1393
1394 let mut in_flight = a
1397 .begin_handshake_with(remote_prefix(), b_guid, &cert_der_from_pem(&b_cert))
1398 .unwrap();
1399 assert_eq!(in_flight.len(), 1, "A sends AUTH_REQUEST");
1400
1401 let mut a_secret = None;
1402 let mut b_secret = None;
1403 let mut deliver_to_b = true;
1404 for _ in 0..6 {
1405 if in_flight.is_empty() {
1406 break;
1407 }
1408 let dg = in_flight.remove(0);
1409 let (target, target_prefix) = if deliver_to_b {
1410 (&mut b, local_prefix())
1411 } else {
1412 (&mut a, remote_prefix())
1413 };
1414 let msgs = target.stateless_reader.handle_datagram(&dg.bytes).unwrap();
1415 let (out, completed) = target
1416 .on_stateless_message(target_prefix, &msgs[0])
1417 .unwrap();
1418 if let Some((_id, secret)) = completed {
1419 if deliver_to_b {
1420 b_secret = Some(secret);
1421 } else {
1422 a_secret = Some(secret);
1423 }
1424 }
1425 in_flight = out;
1426 deliver_to_b = !deliver_to_b;
1427 }
1428
1429 let a_secret = a_secret.expect("A derives a secret");
1430 let b_secret = b_secret.expect("B (without prior discovery) derives a secret");
1431 let a_bytes = a_pki
1432 .lock()
1433 .unwrap()
1434 .secret_bytes(a_secret)
1435 .unwrap()
1436 .to_vec();
1437 let b_bytes = b_pki
1438 .lock()
1439 .unwrap()
1440 .secret_bytes(b_secret)
1441 .unwrap()
1442 .to_vec();
1443 assert_eq!(
1444 a_bytes, b_bytes,
1445 "secret must match even though B processed the request without prior discovery"
1446 );
1447 }
1448
1449 #[test]
1450 fn end_to_end_volatile_secure_handshake_via_reliable_loop() {
1451 let mut a = SecurityBuiltinStack::new(local_prefix(), VendorId::ZERODDS);
1458 let mut b = SecurityBuiltinStack::new(remote_prefix(), VendorId::ZERODDS);
1459 let flags = endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER
1460 | endpoint_flag::PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER;
1461 a.handle_remote_endpoints(&remote_with_prefix(remote_prefix(), flags));
1462 b.handle_remote_endpoints(&remote_with_prefix(local_prefix(), flags));
1463
1464 let mut msg = sample_stateless_msg();
1465 msg.message_class_id = class_id::PARTICIPANT_CRYPTO_TOKENS.into();
1466
1467 let dgs = a.volatile_writer.write(&msg).unwrap();
1468 assert_eq!(dgs.len(), 1, "ein Datagram pro Reader-Proxy");
1469 let parsed = zerodds_rtps::datagram::decode_datagram(&dgs[0].bytes).unwrap();
1471 let mut received_msgs = Vec::new();
1472 for sub in parsed.submessages {
1473 if let zerodds_rtps::datagram::ParsedSubmessage::Data(d) = sub {
1474 if d.reader_id == EntityId::BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER {
1475 received_msgs.extend(
1476 b.volatile_reader
1477 .handle_data(parsed.header.guid_prefix, &d)
1478 .unwrap(),
1479 );
1480 }
1481 }
1482 }
1483 assert_eq!(received_msgs.len(), 1);
1484 assert_eq!(received_msgs[0], msg);
1485
1486 let outbound = b
1488 .volatile_reader
1489 .tick_outbound(Duration::from_millis(500))
1490 .unwrap();
1491 assert!(
1493 !outbound.is_empty(),
1494 "reader should send an initial ACKNACK"
1495 );
1496 }
1497}