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 for the DDS-Security 1.2 §7.5.3/§7.5.4 builtin
5//! topics (`DCPSParticipantStatelessMessage` + `DCPSParticipantVolatileMessage-
6//! Secure`). Wraps the spec data model from `zerodds_security::generic_message`
7//! into a DCPS-ready form:
8//!
9//! - 4-byte PL_CDR encapsulation header (spec RTPS 2.5 §10) before the
10//!   bytes — the same envelope as `ParticipantBuiltinTopicData` DATA
11//!   submessages.
12//! - QoS defaults per topic (spec §7.5.3 BestEffort, §7.5.4 Reliable +
13//!   VOLATILE + KEEP_ALL).
14//!
15//! **What does not happen here (C3.4-c):** the actual DataWriter/
16//! DataReader creation in the DCPS runtime. The caller uses these
17//! helpers to push the wire bytes via a standard RawBytes DataWriter
18//! with the matching EntityIds (see `zerodds_rtps::wire_types::EntityId::
19//! BUILTIN_PARTICIPANT_STATELESS_MESSAGE_*` from C3.8).
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/// Layer-neutral QoS trio for the two builtin topics. The caller in the
30/// DCPS layer maps these onto its `DataWriterQos`/`DataReaderQos`
31/// aggregate types.
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). The same 4-byte envelope
44/// as ParticipantBuiltinTopicData (CDR_LE instead of PL_CDR_LE — the
45/// ParticipantGenericMessage is a **structured CDR**, not a
46/// ParameterList).
47pub const ENCAPSULATION_CDR_LE: [u8; 2] = [0x00, 0x01];
48
49/// Encapsulation header length (spec §10.1: 2 byte kind + 2 byte options).
50pub const ENCAPSULATION_HEADER_LEN: usize = 4;
51
52/// Encodes a `ParticipantGenericMessage` as
53/// `serialized_payload` bytes for a DATA submessage (with a 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/// Decodes a `ParticipantGenericMessage` from
66/// `serialized_payload` bytes (with a 4-byte encapsulation header).
67///
68/// # Errors
69/// `BadArgument` if the encapsulation header is missing or carries a
70/// kind other than CDR_LE / CDR_BE; CDR decode errors are
71/// passed through.
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 for the DCPSParticipantStateless-
91/// Message topic. Stateless = no sequence tracking, every DATA
92/// submessage is 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 for the
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 — the two topics MUST have different
199        // reliability, otherwise someone misread it.
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: the auth plugin builds a HandshakeRequest → ParticipantGeneric-
209        // Message → encapsulated bytes; the receiver decodes back
210        // to the 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}