Skip to main content

zerodds_security_runtime/
builtin_topics.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! C3.4-b — API-Bridge fuer die DDS-Security 1.2 §7.5.3/§7.5.4 Builtin-
5//! Topics (`DCPSParticipantStatelessMessage` + `DCPSParticipantVolatileMessage-
6//! Secure`). Wraps das Spec-Datenmodell aus `zerodds_security::generic_message`
7//! in eine DCPS-fertige Form:
8//!
9//! - 4-byte PL_CDR-Encapsulation-Header (Spec RTPS 2.5 §10) vor den
10//!   Bytes — gleiche Hülle wie `ParticipantBuiltinTopicData`-DATA-
11//!   Submessages.
12//! - QoS-Defaults pro Topic (Spec §7.5.3 BestEffort, §7.5.4 Reliable +
13//!   VOLATILE + KEEP_ALL).
14//!
15//! **Was hier nicht passiert (C3.4-c):** Tatsaechliche DataWriter/
16//! DataReader-Erzeugung im DCPS-Runtime. Der Caller nutzt diese
17//! Helpers, um die Wire-Bytes ueber einen Standard-RawBytes-DataWriter
18//! mit den passenden EntityIds (siehe `zerodds_rtps::wire_types::EntityId::
19//! BUILTIN_PARTICIPANT_STATELESS_MESSAGE_*` aus C3.8) zu pushen.
20
21use alloc::vec::Vec;
22
23use zerodds_qos::{
24    DurabilityKind, DurabilityQosPolicy, HistoryKind, HistoryQosPolicy, ReliabilityKind,
25    ReliabilityQosPolicy,
26};
27use zerodds_security::error::{SecurityError, SecurityErrorKind, SecurityResult};
28
29/// Schicht-neutraler QoS-Trio fuer die zwei Builtin-Topics. Caller im
30/// DCPS-Layer mappt diese auf seine `DataWriterQos`/`DataReaderQos`-
31/// Aggregat-Typen.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct BuiltinTopicQos {
34    /// `RELIABILITY`-Policy.
35    pub reliability: ReliabilityQosPolicy,
36    /// `DURABILITY`-Policy.
37    pub durability: DurabilityQosPolicy,
38    /// `HISTORY`-Policy.
39    pub history: HistoryQosPolicy,
40}
41use zerodds_security::generic_message::ParticipantGenericMessage;
42
43/// CDR-LE Encapsulation-Kind (Spec RTPS 2.5 §10.2). Gleiche 4-Byte-Hülle
44/// wie ParticipantBuiltinTopicData (CDR_LE statt PL_CDR_LE — die
45/// ParticipantGenericMessage ist eine **strukturierte CDR**, nicht
46/// ParameterList).
47pub const ENCAPSULATION_CDR_LE: [u8; 2] = [0x00, 0x01];
48
49/// Encapsulation-Header-Laenge (Spec §10.1: 2 byte kind + 2 byte options).
50pub const ENCAPSULATION_HEADER_LEN: usize = 4;
51
52/// Encoded eine `ParticipantGenericMessage` als
53/// `serialized_payload`-Bytes fuer eine DATA-Submessage (mit 4-byte
54/// CDR-LE-Encapsulation-Header + XCDR1-Body).
55#[must_use]
56pub fn encode_generic_message(msg: &ParticipantGenericMessage) -> Vec<u8> {
57    let body = msg.to_cdr_le();
58    let mut out = Vec::with_capacity(ENCAPSULATION_HEADER_LEN + body.len());
59    out.extend_from_slice(&ENCAPSULATION_CDR_LE);
60    out.extend_from_slice(&[0, 0]); // options (Spec: must be 0)
61    out.extend_from_slice(&body);
62    out
63}
64
65/// Decoded eine `ParticipantGenericMessage` aus
66/// `serialized_payload`-Bytes (mit 4-byte Encapsulation-Header).
67///
68/// # Errors
69/// `BadArgument` wenn der Encapsulation-Header fehlt oder ein anderes
70/// Kind als CDR_LE / CDR_BE traegt; CDR-Decode-Fehler werden
71/// durchgereicht.
72pub fn decode_generic_message(bytes: &[u8]) -> SecurityResult<ParticipantGenericMessage> {
73    if bytes.len() < ENCAPSULATION_HEADER_LEN {
74        return Err(SecurityError::new(
75            SecurityErrorKind::BadArgument,
76            "generic_message: encapsulation-header truncated",
77        ));
78    }
79    let kind = [bytes[0], bytes[1]];
80    if kind != ENCAPSULATION_CDR_LE && kind != [0x00, 0x00] {
81        return Err(SecurityError::new(
82            SecurityErrorKind::BadArgument,
83            "generic_message: only CDR_LE encapsulation supported",
84        ));
85    }
86    // Skip 2 byte options.
87    ParticipantGenericMessage::from_cdr_le(&bytes[ENCAPSULATION_HEADER_LEN..])
88}
89
90/// Spec §7.5.3 — BestEffort-Reliability fuer DCPSParticipantStateless-
91/// Message-Topic. Stateless = kein Sequence-Tracking, jede DATA-
92/// Submessage ist standalone.
93#[must_use]
94pub fn stateless_message_qos() -> BuiltinTopicQos {
95    BuiltinTopicQos {
96        reliability: ReliabilityQosPolicy {
97            kind: ReliabilityKind::BestEffort,
98            ..ReliabilityQosPolicy::default()
99        },
100        durability: DurabilityQosPolicy {
101            kind: DurabilityKind::Volatile,
102        },
103        history: HistoryQosPolicy {
104            kind: HistoryKind::KeepAll,
105            depth: 0,
106        },
107    }
108}
109
110/// Spec §7.5.4 Tab.19/20 — Reliable + VOLATILE + KEEP_ALL fuer
111/// DCPSParticipantVolatileMessageSecure-Topic.
112#[must_use]
113pub fn volatile_secure_qos() -> BuiltinTopicQos {
114    BuiltinTopicQos {
115        reliability: ReliabilityQosPolicy {
116            kind: ReliabilityKind::Reliable,
117            ..ReliabilityQosPolicy::default()
118        },
119        durability: DurabilityQosPolicy {
120            kind: DurabilityKind::Volatile,
121        },
122        history: HistoryQosPolicy {
123            kind: HistoryKind::KeepAll,
124            depth: 0,
125        },
126    }
127}
128
129#[cfg(test)]
130#[allow(clippy::expect_used, clippy::unwrap_used)]
131mod tests {
132    use super::*;
133    use zerodds_security::generic_message::{MessageIdentity, class_id};
134    use zerodds_security::token::DataHolder;
135
136    fn sample_msg() -> ParticipantGenericMessage {
137        ParticipantGenericMessage {
138            message_identity: MessageIdentity {
139                source_guid: [0xAA; 16],
140                sequence_number: 1,
141            },
142            related_message_identity: MessageIdentity::default(),
143            destination_participant_key: [0xBB; 16],
144            destination_endpoint_key: [0; 16],
145            source_endpoint_key: [0xCC; 16],
146            message_class_id: class_id::AUTH_REQUEST.to_string(),
147            message_data: vec![DataHolder::new("DDS:Auth:PKI-DH:1.2+AuthReq")],
148        }
149    }
150
151    #[test]
152    fn encode_starts_with_cdr_le_encapsulation() {
153        let msg = sample_msg();
154        let bytes = encode_generic_message(&msg);
155        assert_eq!(&bytes[..4], &[0x00, 0x01, 0x00, 0x00]);
156    }
157
158    #[test]
159    fn encode_decode_roundtrip() {
160        let msg = sample_msg();
161        let bytes = encode_generic_message(&msg);
162        let back = decode_generic_message(&bytes).unwrap();
163        assert_eq!(msg, back);
164    }
165
166    #[test]
167    fn decode_rejects_unknown_encapsulation() {
168        let bytes = vec![0x00, 0x99, 0, 0, 0, 0, 0, 0];
169        let err = decode_generic_message(&bytes).unwrap_err();
170        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
171    }
172
173    #[test]
174    fn decode_rejects_truncated() {
175        let bytes = vec![0x00, 0x01];
176        let err = decode_generic_message(&bytes).unwrap_err();
177        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
178    }
179
180    #[test]
181    fn stateless_qos_is_best_effort_volatile_keep_all() {
182        let q = stateless_message_qos();
183        assert_eq!(q.reliability.kind, ReliabilityKind::BestEffort);
184        assert_eq!(q.durability.kind, DurabilityKind::Volatile);
185        assert_eq!(q.history.kind, HistoryKind::KeepAll);
186    }
187
188    #[test]
189    fn volatile_secure_qos_is_reliable_volatile_keep_all() {
190        let q = volatile_secure_qos();
191        assert_eq!(q.reliability.kind, ReliabilityKind::Reliable);
192        assert_eq!(q.durability.kind, DurabilityKind::Volatile);
193        assert_eq!(q.history.kind, HistoryKind::KeepAll);
194    }
195
196    #[test]
197    fn stateless_and_volatile_qos_differ() {
198        // Spec §7.5.3 vs §7.5.4 — die zwei Topics MÜSSEN unterschiedliche
199        // Reliability haben, sonst hat sich jemand verlesen.
200        assert_ne!(
201            stateless_message_qos().reliability.kind,
202            volatile_secure_qos().reliability.kind
203        );
204    }
205
206    #[test]
207    fn full_handshake_token_through_bridge() {
208        // E2E: Auth-Plugin baut HandshakeRequest → ParticipantGeneric-
209        // Message → encapsulated bytes; Empfanger dekodiert wieder
210        // zum DataHolder.
211        let token = DataHolder::new("DDS:Auth:PKI-DH:1.2+AuthReq")
212            .with_property("c.dsign_algo", "ECDSA-SHA256")
213            .with_binary_property("c.id", vec![0x30, 0x82, 0x01, 0x23]);
214        let msg = ParticipantGenericMessage {
215            message_identity: MessageIdentity {
216                source_guid: [0xAA; 16],
217                sequence_number: 1,
218            },
219            destination_participant_key: [0xBB; 16],
220            source_endpoint_key: [0xCC; 16],
221            message_class_id: class_id::AUTH_REQUEST.to_string(),
222            message_data: vec![token.clone()],
223            ..ParticipantGenericMessage::default()
224        };
225        let wire = encode_generic_message(&msg);
226        let decoded = decode_generic_message(&wire).unwrap();
227        assert_eq!(decoded.message_data.len(), 1);
228        assert_eq!(decoded.message_data[0], token);
229    }
230}