Skip to main content

zerodds_security/
mock.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Mock-Plugins fuer Tests.
5//!
6//! Die Mocks akzeptieren **jeden** Peer und simulieren einen
7//! Handshake in genau zwei Schritten. Niemals fuer Produktion — sie
8//! liefern keine echte Crypto.
9//!
10//! Zweck:
11//! 1. Das SPI-Interface gegen einen tatsaechlich funktionierenden Flow
12//!    validieren (Signature-Checks, Handshake-State-Machine).
13//! 2. DCPS-Layer kann ab v1.4 gegen den Mock sub-testen, bevor der
14//!    Produktions-Plugin fertig ist.
15//!
16//! zerodds-lint: allow no_dyn_in_safe
17//! (Tests instanziieren `Box<dyn AuthenticationPlugin>`.)
18
19extern crate alloc;
20
21use alloc::collections::BTreeMap;
22use alloc::vec::Vec;
23
24#[cfg(feature = "std")]
25use alloc::borrow::ToOwned;
26#[cfg(feature = "std")]
27use alloc::string::String;
28use core::sync::atomic::{AtomicU64, Ordering};
29
30use crate::access_control::{AccessControlPlugin, AccessDecision, PermissionsHandle};
31use crate::authentication::{
32    AuthenticationPlugin, HandshakeHandle, HandshakeStepOutcome, IdentityHandle, SharedSecretHandle,
33};
34use crate::data_tagging::{DataTag, DataTaggingPlugin};
35use crate::error::{SecurityError, SecurityErrorKind, SecurityResult};
36use crate::logging::{LogLevel, LoggingPlugin};
37use crate::properties::PropertyList;
38
39// ============================================================================
40// MockAuthenticationPlugin
41// ============================================================================
42
43/// Mock-Implementation — akzeptiert alles, Handshake-Step-Count
44/// hard-coded.
45#[derive(Debug, Default)]
46pub struct MockAuthenticationPlugin {
47    next_handle: AtomicU64,
48    handshakes: BTreeMap<HandshakeHandle, SharedSecretHandle>,
49}
50
51impl MockAuthenticationPlugin {
52    /// Konstruktor.
53    #[must_use]
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    fn next_id(&self) -> u64 {
59        // `fetch_add` auf AtomicU64 — kein `&mut self` benoetigt,
60        // daher koennen `validate_*` read-only-Semantik implizieren.
61        self.next_handle.fetch_add(1, Ordering::Relaxed) + 1
62    }
63}
64
65impl AuthenticationPlugin for MockAuthenticationPlugin {
66    fn validate_local_identity(
67        &mut self,
68        _props: &PropertyList,
69        _participant_guid: [u8; 16],
70    ) -> SecurityResult<IdentityHandle> {
71        Ok(IdentityHandle(self.next_id()))
72    }
73
74    fn validate_remote_identity(
75        &mut self,
76        _local: IdentityHandle,
77        _remote_participant_guid: [u8; 16],
78        _remote_auth_token: &[u8],
79    ) -> SecurityResult<IdentityHandle> {
80        Ok(IdentityHandle(self.next_id()))
81    }
82
83    fn begin_handshake_request(
84        &mut self,
85        _initiator: IdentityHandle,
86        _replier: IdentityHandle,
87    ) -> SecurityResult<(HandshakeHandle, HandshakeStepOutcome)> {
88        let h = HandshakeHandle(self.next_id());
89        // Mock-Handshake: Request-Token ist ein Fixtext.
90        Ok((
91            h,
92            HandshakeStepOutcome::SendMessage {
93                token: b"MOCK-REQUEST".to_vec(),
94            },
95        ))
96    }
97
98    fn begin_handshake_reply(
99        &mut self,
100        _replier: IdentityHandle,
101        _initiator: IdentityHandle,
102        request_token: &[u8],
103    ) -> SecurityResult<(HandshakeHandle, HandshakeStepOutcome)> {
104        if request_token != b"MOCK-REQUEST" {
105            return Err(SecurityError::new(
106                SecurityErrorKind::AuthenticationFailed,
107                "mock: unerwartetes Request-Token",
108            ));
109        }
110        let h = HandshakeHandle(self.next_id());
111        // Reply-Token, nach dessen Empfang der Initiator den Handshake
112        // abschliessen kann.
113        Ok((
114            h,
115            HandshakeStepOutcome::SendMessage {
116                token: b"MOCK-REPLY".to_vec(),
117            },
118        ))
119    }
120
121    fn process_handshake(
122        &mut self,
123        handshake: HandshakeHandle,
124        token: &[u8],
125    ) -> SecurityResult<HandshakeStepOutcome> {
126        if token == b"MOCK-REPLY" {
127            let secret = SharedSecretHandle(self.next_id());
128            self.handshakes.insert(handshake, secret);
129            return Ok(HandshakeStepOutcome::Complete { secret });
130        }
131        if token == b"MOCK-FINAL-ACK" {
132            // Replier-Seite abgeschlossen.
133            let secret = self
134                .handshakes
135                .get(&handshake)
136                .copied()
137                .unwrap_or(SharedSecretHandle(self.next_id()));
138            return Ok(HandshakeStepOutcome::Complete { secret });
139        }
140        Err(SecurityError::new(
141            SecurityErrorKind::AuthenticationFailed,
142            "mock: unbekanntes handshake-token",
143        ))
144    }
145
146    fn shared_secret(&self, handshake: HandshakeHandle) -> SecurityResult<SharedSecretHandle> {
147        self.handshakes.get(&handshake).copied().ok_or_else(|| {
148            SecurityError::new(
149                SecurityErrorKind::BadArgument,
150                "mock: handshake-handle unbekannt",
151            )
152        })
153    }
154
155    fn plugin_class_id(&self) -> &str {
156        "DDS:Auth:Mock"
157    }
158}
159
160// ============================================================================
161// MockAccessControlPlugin — jedes Topic erlaubt (Permit-Everything).
162// ============================================================================
163
164/// Mock-Access-Control: erlaubt alles. Nur fuer Tests.
165#[derive(Debug, Default)]
166pub struct MockAccessControlPlugin {
167    next_handle: AtomicU64,
168}
169
170impl MockAccessControlPlugin {
171    /// Konstruktor.
172    #[must_use]
173    pub fn new() -> Self {
174        Self::default()
175    }
176
177    fn next_id(&self) -> u64 {
178        self.next_handle.fetch_add(1, Ordering::Relaxed) + 1
179    }
180}
181
182impl AccessControlPlugin for MockAccessControlPlugin {
183    fn validate_local_permissions(
184        &mut self,
185        _local: IdentityHandle,
186        _participant_guid: [u8; 16],
187        _props: &PropertyList,
188    ) -> SecurityResult<PermissionsHandle> {
189        Ok(PermissionsHandle(self.next_id()))
190    }
191
192    fn validate_remote_permissions(
193        &mut self,
194        _local: IdentityHandle,
195        _remote: IdentityHandle,
196        _remote_permissions_token: &[u8],
197        _remote_credential: &[u8],
198    ) -> SecurityResult<PermissionsHandle> {
199        Ok(PermissionsHandle(self.next_id()))
200    }
201
202    fn check_create_datawriter(
203        &self,
204        _p: PermissionsHandle,
205        _topic: &str,
206    ) -> SecurityResult<AccessDecision> {
207        Ok(AccessDecision::Permit)
208    }
209
210    fn check_create_datareader(
211        &self,
212        _p: PermissionsHandle,
213        _topic: &str,
214    ) -> SecurityResult<AccessDecision> {
215        Ok(AccessDecision::Permit)
216    }
217
218    fn check_remote_datawriter_match(
219        &self,
220        _l: PermissionsHandle,
221        _r: PermissionsHandle,
222        _topic: &str,
223    ) -> SecurityResult<AccessDecision> {
224        Ok(AccessDecision::Permit)
225    }
226
227    fn check_remote_datareader_match(
228        &self,
229        _l: PermissionsHandle,
230        _r: PermissionsHandle,
231        _topic: &str,
232    ) -> SecurityResult<AccessDecision> {
233        Ok(AccessDecision::Permit)
234    }
235
236    fn plugin_class_id(&self) -> &str {
237        "DDS:Access:Mock"
238    }
239}
240
241// ============================================================================
242// MockLoggingPlugin — sammelt Events in einem Vec fuer Test-Assertions
243// ============================================================================
244
245/// Ein Log-Eintrag — fuer Test-Assertions gesammelt.
246#[cfg(feature = "std")]
247#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct MockLogEntry {
249    /// Severity.
250    pub level: LogLevel,
251    /// Participant-GUID (16 octets).
252    pub participant: [u8; 16],
253    /// Category-String.
254    pub category: String,
255    /// Message.
256    pub message: String,
257}
258
259/// Shared Sink-Typ — `Arc<Mutex<Vec<MockLogEntry>>>`.
260#[cfg(feature = "std")]
261pub type MockLogSink = std::sync::Arc<std::sync::Mutex<Vec<MockLogEntry>>>;
262
263/// Mock-Logger: sammelt alle Events in einem `MockLogSink`, damit Tests
264/// die Events nachtraeglich inspizieren koennen.
265#[cfg(feature = "std")]
266pub struct MockLoggingPlugin {
267    sink: MockLogSink,
268}
269
270#[cfg(feature = "std")]
271impl MockLoggingPlugin {
272    /// Konstruktor.
273    #[must_use]
274    pub fn new(sink: MockLogSink) -> Self {
275        Self { sink }
276    }
277}
278
279#[cfg(feature = "std")]
280impl LoggingPlugin for MockLoggingPlugin {
281    fn log(&self, level: LogLevel, participant: [u8; 16], category: &str, message: &str) {
282        if let Ok(mut v) = self.sink.lock() {
283            v.push(MockLogEntry {
284                level,
285                participant,
286                category: category.to_owned(),
287                message: message.to_owned(),
288            });
289        }
290    }
291
292    fn plugin_class_id(&self) -> &str {
293        "DDS:Logging:Mock"
294    }
295}
296
297// ============================================================================
298// MockDataTaggingPlugin — minimaler Tag-Store fuer Tests
299// ============================================================================
300
301/// Mock-DataTagging-Plugin: speichert Tag-Listen pro Endpoint-GUID in
302/// einer In-Memory-Map. Liefert auf Unknown-GUID einen leeren Vec
303/// (Spec-konformer Default).
304#[derive(Debug, Default)]
305pub struct MockDataTaggingPlugin {
306    tags: BTreeMap<[u8; 16], Vec<DataTag>>,
307}
308
309impl MockDataTaggingPlugin {
310    /// Konstruktor.
311    #[must_use]
312    pub fn new() -> Self {
313        Self::default()
314    }
315}
316
317impl DataTaggingPlugin for MockDataTaggingPlugin {
318    fn set_tags(&mut self, endpoint_guid: [u8; 16], tags: Vec<DataTag>) {
319        self.tags.insert(endpoint_guid, tags);
320    }
321
322    fn get_tags(&self, endpoint_guid: [u8; 16]) -> Vec<DataTag> {
323        self.tags.get(&endpoint_guid).cloned().unwrap_or_default()
324    }
325
326    fn plugin_class_id(&self) -> &str {
327        "DDS:Tagging:Mock"
328    }
329}
330
331// ============================================================================
332// Tests
333// ============================================================================
334
335#[cfg(test)]
336#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn mock_authentication_end_to_end_handshake() {
342        // Zwei Plugins simulieren zwei Participants.
343        let mut alice = MockAuthenticationPlugin::new();
344        let mut bob = MockAuthenticationPlugin::new();
345
346        let alice_id = alice
347            .validate_local_identity(&PropertyList::new(), [0xAA; 16])
348            .expect("alice identity");
349        let bob_id = bob
350            .validate_local_identity(&PropertyList::new(), [0xBB; 16])
351            .expect("bob identity");
352
353        // Alice sieht Bob via SPDP.
354        let bob_remote_at_alice = alice
355            .validate_remote_identity(alice_id, [0xBB; 16], b"mock-bob-token")
356            .expect("bob-remote");
357        let alice_remote_at_bob = bob
358            .validate_remote_identity(bob_id, [0xAA; 16], b"mock-alice-token")
359            .expect("alice-remote");
360
361        // Alice startet Handshake.
362        let (alice_h, outcome1) = alice
363            .begin_handshake_request(alice_id, bob_remote_at_alice)
364            .expect("request");
365        let request_token = match outcome1 {
366            HandshakeStepOutcome::SendMessage { token } => token,
367            other => panic!("erwartet SendMessage, got {other:?}"),
368        };
369
370        // Bob antwortet.
371        let (bob_h, outcome2) = bob
372            .begin_handshake_reply(bob_id, alice_remote_at_bob, &request_token)
373            .expect("reply");
374        let reply_token = match outcome2 {
375            HandshakeStepOutcome::SendMessage { token } => token,
376            other => panic!("erwartet SendMessage, got {other:?}"),
377        };
378
379        // Alice verarbeitet Reply → Complete.
380        let outcome3 = alice
381            .process_handshake(alice_h, &reply_token)
382            .expect("proc");
383        let alice_secret = match outcome3 {
384            HandshakeStepOutcome::Complete { secret } => secret,
385            other => panic!("erwartet Complete, got {other:?}"),
386        };
387
388        // Bob-Seite: final-ack abschliessen.
389        let outcome4 = bob
390            .process_handshake(bob_h, b"MOCK-FINAL-ACK")
391            .expect("proc bob");
392        assert!(matches!(outcome4, HandshakeStepOutcome::Complete { .. }));
393
394        // Secret-Handle auf Alice-Seite ist queryable.
395        let fetched = alice.shared_secret(alice_h).expect("fetch");
396        assert_eq!(fetched, alice_secret);
397    }
398
399    #[test]
400    fn mock_access_control_permits_everything() {
401        let mut ac = MockAccessControlPlugin::new();
402        let local = IdentityHandle(1);
403        let perms = ac
404            .validate_local_permissions(local, [0xAA; 16], &PropertyList::new())
405            .expect("perms");
406        assert!(
407            ac.check_create_datawriter(perms, "Chatter")
408                .unwrap()
409                .is_permitted()
410        );
411        assert!(
412            ac.check_create_datareader(perms, "Chatter")
413                .unwrap()
414                .is_permitted()
415        );
416    }
417
418    #[test]
419    fn auth_plugin_can_be_boxed_as_trait_object() {
420        let plugin: Box<dyn AuthenticationPlugin> = Box::new(MockAuthenticationPlugin::new());
421        assert_eq!(plugin.plugin_class_id(), "DDS:Auth:Mock");
422    }
423
424    #[test]
425    fn mock_data_tagging_set_get_roundtrip() {
426        let mut tagger = MockDataTaggingPlugin::new();
427        let g = [0xAB; 16];
428        let tags = alloc::vec![DataTag {
429            name: "classification".into(),
430            value: "secret".into(),
431        }];
432        tagger.set_tags(g, tags.clone());
433        assert_eq!(tagger.get_tags(g), tags);
434        assert!(tagger.get_tags([0xCD; 16]).is_empty());
435        assert_eq!(tagger.plugin_class_id(), "DDS:Tagging:Mock");
436    }
437
438    #[cfg(feature = "std")]
439    #[test]
440    fn mock_logging_captures_events() {
441        use std::sync::{Arc, Mutex};
442        let sink = Arc::new(Mutex::new(Vec::new()));
443        let logger = MockLoggingPlugin::new(Arc::clone(&sink));
444        logger.log(LogLevel::Critical, [0u8; 16], "auth.failed", "bad cert");
445        let captured = sink.lock().unwrap();
446        assert_eq!(captured.len(), 1);
447        assert_eq!(captured[0].level, LogLevel::Critical);
448        assert_eq!(captured[0].category, "auth.failed");
449        assert_eq!(captured[0].message, "bad cert");
450    }
451}