Skip to main content

zerodds_security/
security_topic_qos.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Security-Builtin-Topic QoS-Profile — DDS-Security 1.2 §7.5.3 + §7.5.4.
5//!
6//! Spec §7.5.3 (`DCPSParticipantStatelessMessage`):
7//! * Reliability: `BEST_EFFORT`
8//! * Durability: `VOLATILE`
9//! * History: `KEEP_LAST(1)`
10//! * Lifespan: `INFINITE`
11//!
12//! Spec §7.5.4 (`DCPSParticipantVolatileMessageSecure`):
13//! * Reliability: `RELIABLE`
14//! * Durability: `VOLATILE`
15//! * History: `KEEP_ALL`
16//! * Lifespan: `INFINITE`
17//! * Crypto: Receiver-Specific-MACs MUST be enabled
18//!   (`participant_crypto_handle`-Encryption mit pro-Empfaenger
19//!   distinkter Macs).
20
21use core::fmt;
22
23/// `BuiltinSecurityTopicProfile` — beschreibt das Spec-konforme
24/// QoS-Profile fuer einen Security-Builtin-Topic.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct BuiltinSecurityTopicProfile {
27    /// Reliability-Kind (Best-Effort vs Reliable).
28    pub reliability: ReliabilityKind,
29    /// Durability-Kind (immer Volatile fuer Security-Builtins).
30    pub durability: DurabilityKind,
31    /// History-Kind.
32    pub history: HistoryKind,
33    /// `true` wenn Receiver-Specific-MACs Pflicht sind.
34    pub require_receiver_specific_macs: bool,
35}
36
37/// Reliability-Kind (Spec DDS 1.4 §2.2.3.14).
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ReliabilityKind {
40    /// `BEST_EFFORT`.
41    BestEffort,
42    /// `RELIABLE`.
43    Reliable,
44}
45
46/// Durability-Kind (Spec DDS 1.4 §2.2.3.4).
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum DurabilityKind {
49    /// `VOLATILE` (default fuer Security-Builtins).
50    Volatile,
51    /// `TRANSIENT_LOCAL`.
52    TransientLocal,
53}
54
55/// History-Kind (Spec DDS 1.4 §2.2.3.18).
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum HistoryKind {
58    /// `KEEP_LAST(depth)`.
59    KeepLast(u32),
60    /// `KEEP_ALL`.
61    KeepAll,
62}
63
64impl fmt::Display for ReliabilityKind {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str(match self {
67            Self::BestEffort => "BEST_EFFORT",
68            Self::Reliable => "RELIABLE",
69        })
70    }
71}
72
73/// Spec §7.5.3 `DCPSParticipantStatelessMessage`-Topic-Profile.
74///
75/// Wird vom Authentication-Plugin fuer Pre-Handshake-Token-Exchange
76/// (`AuthRequestMessageToken`) und Handshake-Sequenz (Request/Reply/
77/// Final) verwendet. BEST_EFFORT weil die Tokens self-contained sind
78/// (jeder Token traegt eine eigene Challenge — Replays sind via
79/// future_challenge-Echo verhindert).
80#[must_use]
81pub fn stateless_message_profile() -> BuiltinSecurityTopicProfile {
82    BuiltinSecurityTopicProfile {
83        reliability: ReliabilityKind::BestEffort,
84        durability: DurabilityKind::Volatile,
85        history: HistoryKind::KeepLast(1),
86        require_receiver_specific_macs: false,
87    }
88}
89
90/// Spec §7.5.4 `DCPSParticipantVolatileMessageSecure`-Topic-Profile.
91///
92/// Wird vom Crypto-Plugin fuer Crypto-Token-Exchange verwendet
93/// (Participant/DataWriter/DataReader-Crypto-Tokens). RELIABLE weil
94/// fehlende Tokens zu permanenten Decryption-Fehlern fuehren — wir
95/// muessen Tokens bis zur Bestaetigung wiederholen koennen.
96/// `require_receiver_specific_macs = true` ist Spec §7.5.4-Pflicht:
97/// jeder Empfaenger MUSS einen distinkten MAC sehen, sonst koennte
98/// ein Empfaenger einen Token-Sample, den er empfaengt, an einen
99/// anderen Empfaenger relaiien.
100#[must_use]
101pub fn volatile_message_secure_profile() -> BuiltinSecurityTopicProfile {
102    BuiltinSecurityTopicProfile {
103        reliability: ReliabilityKind::Reliable,
104        durability: DurabilityKind::Volatile,
105        history: HistoryKind::KeepAll,
106        require_receiver_specific_macs: true,
107    }
108}
109
110/// Validiert dass ein vorgeschlagenes Profile mit dem Spec-konformen
111/// Profile fuer einen Security-Builtin-Topic uebereinstimmt.
112///
113/// # Errors
114/// `&'static str` mit Fehlerursache.
115pub fn validate_security_topic_profile(
116    topic_name: &str,
117    actual: BuiltinSecurityTopicProfile,
118) -> Result<(), &'static str> {
119    let expected = match topic_name {
120        crate::generic_message::TOPIC_STATELESS_MESSAGE => stateless_message_profile(),
121        crate::generic_message::TOPIC_VOLATILE_MESSAGE_SECURE => volatile_message_secure_profile(),
122        _ => return Err("not a security builtin topic"),
123    };
124    if actual.reliability != expected.reliability {
125        return Err("reliability mismatch");
126    }
127    if actual.durability != expected.durability {
128        return Err("durability mismatch");
129    }
130    if actual.history != expected.history {
131        return Err("history mismatch");
132    }
133    if actual.require_receiver_specific_macs != expected.require_receiver_specific_macs {
134        return Err("receiver-specific-MAC requirement mismatch");
135    }
136    Ok(())
137}
138
139#[cfg(test)]
140#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn stateless_profile_is_best_effort() {
146        let p = stateless_message_profile();
147        assert_eq!(p.reliability, ReliabilityKind::BestEffort);
148        assert_eq!(p.durability, DurabilityKind::Volatile);
149        assert_eq!(p.history, HistoryKind::KeepLast(1));
150        assert!(!p.require_receiver_specific_macs);
151    }
152
153    #[test]
154    fn volatile_secure_is_reliable_with_receiver_macs() {
155        let p = volatile_message_secure_profile();
156        assert_eq!(p.reliability, ReliabilityKind::Reliable);
157        assert_eq!(p.history, HistoryKind::KeepAll);
158        assert!(p.require_receiver_specific_macs);
159    }
160
161    #[test]
162    fn validate_known_topic_with_matching_profile() {
163        validate_security_topic_profile(
164            crate::generic_message::TOPIC_STATELESS_MESSAGE,
165            stateless_message_profile(),
166        )
167        .unwrap();
168        validate_security_topic_profile(
169            crate::generic_message::TOPIC_VOLATILE_MESSAGE_SECURE,
170            volatile_message_secure_profile(),
171        )
172        .unwrap();
173    }
174
175    #[test]
176    fn validate_rejects_wrong_reliability_on_volatile_topic() {
177        let mut wrong = volatile_message_secure_profile();
178        wrong.reliability = ReliabilityKind::BestEffort;
179        let err = validate_security_topic_profile(
180            crate::generic_message::TOPIC_VOLATILE_MESSAGE_SECURE,
181            wrong,
182        )
183        .unwrap_err();
184        assert_eq!(err, "reliability mismatch");
185    }
186
187    #[test]
188    fn validate_rejects_missing_receiver_macs() {
189        let mut wrong = volatile_message_secure_profile();
190        wrong.require_receiver_specific_macs = false;
191        let err = validate_security_topic_profile(
192            crate::generic_message::TOPIC_VOLATILE_MESSAGE_SECURE,
193            wrong,
194        )
195        .unwrap_err();
196        assert_eq!(err, "receiver-specific-MAC requirement mismatch");
197    }
198
199    #[test]
200    fn validate_rejects_unknown_topic() {
201        let err =
202            validate_security_topic_profile("Other", stateless_message_profile()).unwrap_err();
203        assert_eq!(err, "not a security builtin topic");
204    }
205
206    #[test]
207    fn reliability_display_matches_spec() {
208        assert_eq!(
209            alloc::format!("{}", ReliabilityKind::BestEffort),
210            "BEST_EFFORT"
211        );
212        assert_eq!(alloc::format!("{}", ReliabilityKind::Reliable), "RELIABLE");
213    }
214}