Skip to main content

zerodds_rtps/
endpoint_security_info.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Endpoint-Security-Info Wire-Format fuer `PID_ENDPOINT_SECURITY_INFO`
4//! (0x1004, DDS-Security 1.1 §7.4.1.5).
5//!
6//! Zwei u32-Bitmasken:
7//! ```text
8//!   u32  endpoint_security_attributes          (masks §7.4.1.5)
9//!   u32  plugin_endpoint_security_attributes   (masks §7.4.1.6)
10//! ```
11//!
12//! Beide Masken verwenden das MSB als `IS_VALID`-Flag: ein
13//! Receiver darf die Werte nur interpretieren, wenn das Bit gesetzt
14//! ist (sonst → als "no security info" behandeln).
15//!
16//! Dieses Modul ist **Wire-only** — Policy-Entscheidungen (Match
17//! Writer ↔ Reader, Encryption-Level-Enforcement) gehoeren in den
18//! `security-runtime`-Crate.
19
20use crate::error::WireError;
21
22// ============================================================================
23// EndpointSecurityAttributes (DDS-Security 1.1 §7.4.1.5 Tabelle 32)
24// ============================================================================
25
26/// Bit-Masks fuer `endpoint_security_attributes`.
27pub mod attrs {
28    /// Set wenn die Bits ueberhaupt interpretiert werden duerfen.
29    /// Receiver muss diesen Bit pruefen, sonst Werte ignorieren.
30    pub const IS_VALID: u32 = 0x8000_0000;
31    /// Read-Access-Control ist fuer dieses Endpoint aktiv.
32    pub const IS_READ_PROTECTED: u32 = 0x0000_0001;
33    /// Write-Access-Control ist aktiv.
34    pub const IS_WRITE_PROTECTED: u32 = 0x0000_0002;
35    /// SEDP-Discovery fuer dieses Endpoint wird geschuetzt.
36    pub const IS_DISCOVERY_PROTECTED: u32 = 0x0000_0004;
37    /// Submessage-Level-Protection (SEC_PREFIX wrapping) aktiv.
38    pub const IS_SUBMESSAGE_PROTECTED: u32 = 0x0000_0008;
39    /// Payload-Level-Protection (SEC_BODY-Encoding) aktiv.
40    pub const IS_PAYLOAD_PROTECTED: u32 = 0x0000_0010;
41    /// Key-Attribute (z.B. Partition-Keys) werden geschuetzt.
42    pub const IS_KEY_PROTECTED: u32 = 0x0000_0020;
43    /// Liveliness-Submessages werden authentifiziert.
44    pub const IS_LIVELINESS_PROTECTED: u32 = 0x0000_0040;
45}
46
47// ============================================================================
48// PluginEndpointSecurityAttributes (§7.4.1.6 Tabelle 33)
49// ============================================================================
50
51/// Bit-Masks fuer `plugin_endpoint_security_attributes`.
52///
53/// Diese zweite Maske ist Plugin-spezifisch: sie sagt *was konkret*
54/// an Protection passiert, waehrend [`attrs`] sagt *welche Klasse* von
55/// Schutz aktiv ist.
56pub mod plugin_attrs {
57    /// Set wenn die Plugin-Bits gesetzt sind.
58    pub const IS_VALID: u32 = 0x8000_0000;
59    /// Submessage ist AEAD-verschluesselt (nicht nur signiert).
60    pub const IS_SUBMESSAGE_ENCRYPTED: u32 = 0x0000_0001;
61    /// Submessage traegt Origin-Authentication-Tag (receiver-spezifisch).
62    pub const IS_SUBMESSAGE_ORIGIN_AUTHENTICATED: u32 = 0x0000_0002;
63    /// Payload (SEC_BODY) ist verschluesselt (sonst nur authentifiziert).
64    pub const IS_PAYLOAD_ENCRYPTED: u32 = 0x0000_0004;
65}
66
67// ============================================================================
68// EndpointSecurityInfo-Struct
69// ============================================================================
70
71/// Wire-Repraesentation von `PID_ENDPOINT_SECURITY_INFO`.
72///
73/// Die rohen Masken werden unmodifiziert durchgereicht — die
74/// Policy-Layer-Konversion (z.B. "is_submessage_protected bedeutet
75/// ProtectionLevel::Sign/Encrypt") sitzt im `security-runtime`.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
77pub struct EndpointSecurityInfo {
78    /// Standard-Endpoint-Security-Attribute (siehe [`attrs`]).
79    pub endpoint_security_attributes: u32,
80    /// Plugin-spezifische Endpoint-Security-Attribute (siehe
81    /// [`plugin_attrs`]).
82    pub plugin_endpoint_security_attributes: u32,
83}
84
85impl EndpointSecurityInfo {
86    /// Wire-Size (2 * u32).
87    pub const WIRE_SIZE: usize = 8;
88
89    /// `true` wenn der Spec-konforme `IS_VALID`-Bit in beiden Masken
90    /// gesetzt ist. Andernfalls soll der Receiver die Werte ignorieren
91    /// (§7.4.1.5 Satz 2).
92    #[must_use]
93    pub const fn is_valid(&self) -> bool {
94        (self.endpoint_security_attributes & attrs::IS_VALID) != 0
95            && (self.plugin_endpoint_security_attributes & plugin_attrs::IS_VALID) != 0
96    }
97
98    /// Builder fuer ein "plain-Legacy"-Endpoint (alle Bits 0 ausser
99    /// den IS_VALID-Flags) — entspricht: der Peer unterstuetzt die
100    /// Security-PID, will aber keinen Schutz fuer dieses Endpoint.
101    #[must_use]
102    pub const fn plain() -> Self {
103        Self {
104            endpoint_security_attributes: attrs::IS_VALID,
105            plugin_endpoint_security_attributes: plugin_attrs::IS_VALID,
106        }
107    }
108
109    /// `true` wenn Submessage-Level-Protection gesetzt ist.
110    #[must_use]
111    pub const fn is_submessage_protected(&self) -> bool {
112        (self.endpoint_security_attributes & attrs::IS_SUBMESSAGE_PROTECTED) != 0
113    }
114
115    /// `true` wenn Payload-Level-Protection gesetzt ist.
116    #[must_use]
117    pub const fn is_payload_protected(&self) -> bool {
118        (self.endpoint_security_attributes & attrs::IS_PAYLOAD_PROTECTED) != 0
119    }
120
121    /// `true` wenn Plugin AEAD-Encryption fuer Submessages anmeldet.
122    #[must_use]
123    pub const fn is_submessage_encrypted(&self) -> bool {
124        (self.plugin_endpoint_security_attributes & plugin_attrs::IS_SUBMESSAGE_ENCRYPTED) != 0
125    }
126
127    /// `true` wenn Plugin Origin-Authentication-Tag meldet (Stufe 7
128    /// Receiver-Specific-MACs).
129    #[must_use]
130    pub const fn is_submessage_origin_authenticated(&self) -> bool {
131        (self.plugin_endpoint_security_attributes
132            & plugin_attrs::IS_SUBMESSAGE_ORIGIN_AUTHENTICATED)
133            != 0
134    }
135
136    /// `true` wenn Plugin Payload-Ciphertext meldet.
137    #[must_use]
138    pub const fn is_payload_encrypted(&self) -> bool {
139        (self.plugin_endpoint_security_attributes & plugin_attrs::IS_PAYLOAD_ENCRYPTED) != 0
140    }
141
142    /// Encode zu 8 Byte (2 * u32 LE oder BE).
143    #[must_use]
144    pub fn to_bytes(&self, little_endian: bool) -> [u8; Self::WIRE_SIZE] {
145        let mut out = [0u8; Self::WIRE_SIZE];
146        let (a, p) = if little_endian {
147            (
148                self.endpoint_security_attributes.to_le_bytes(),
149                self.plugin_endpoint_security_attributes.to_le_bytes(),
150            )
151        } else {
152            (
153                self.endpoint_security_attributes.to_be_bytes(),
154                self.plugin_endpoint_security_attributes.to_be_bytes(),
155            )
156        };
157        out[..4].copy_from_slice(&a);
158        out[4..].copy_from_slice(&p);
159        out
160    }
161
162    /// Decode aus 8 Byte (Value eines `PID_ENDPOINT_SECURITY_INFO`-
163    /// Parameters).
164    ///
165    /// # Errors
166    /// `UnexpectedEof` wenn Input < 8 Byte.
167    pub fn from_bytes(bytes: &[u8], little_endian: bool) -> Result<Self, WireError> {
168        if bytes.len() < Self::WIRE_SIZE {
169            return Err(WireError::UnexpectedEof {
170                needed: Self::WIRE_SIZE,
171                offset: 0,
172            });
173        }
174        let mut a = [0u8; 4];
175        a.copy_from_slice(&bytes[..4]);
176        let mut p = [0u8; 4];
177        p.copy_from_slice(&bytes[4..8]);
178        let (attrs_raw, plugin_raw) = if little_endian {
179            (u32::from_le_bytes(a), u32::from_le_bytes(p))
180        } else {
181            (u32::from_be_bytes(a), u32::from_be_bytes(p))
182        };
183        Ok(Self {
184            endpoint_security_attributes: attrs_raw,
185            plugin_endpoint_security_attributes: plugin_raw,
186        })
187    }
188}
189
190// ============================================================================
191// Tests
192// ============================================================================
193
194#[cfg(test)]
195mod tests {
196    #![allow(clippy::expect_used, clippy::unwrap_used)]
197    use super::*;
198
199    #[test]
200    fn plain_is_valid_with_no_protection_bits() {
201        let p = EndpointSecurityInfo::plain();
202        assert!(p.is_valid());
203        assert!(!p.is_submessage_protected());
204        assert!(!p.is_payload_protected());
205        assert!(!p.is_submessage_encrypted());
206        assert!(!p.is_payload_encrypted());
207        assert!(!p.is_submessage_origin_authenticated());
208    }
209
210    #[test]
211    fn default_is_not_valid() {
212        let d = EndpointSecurityInfo::default();
213        assert!(
214            !d.is_valid(),
215            "default == alle null == is_valid muss false sein"
216        );
217    }
218
219    #[test]
220    fn roundtrip_le() {
221        let info = EndpointSecurityInfo {
222            endpoint_security_attributes: attrs::IS_VALID
223                | attrs::IS_SUBMESSAGE_PROTECTED
224                | attrs::IS_PAYLOAD_PROTECTED,
225            plugin_endpoint_security_attributes: plugin_attrs::IS_VALID
226                | plugin_attrs::IS_SUBMESSAGE_ENCRYPTED
227                | plugin_attrs::IS_PAYLOAD_ENCRYPTED,
228        };
229        let bytes = info.to_bytes(true);
230        let decoded = EndpointSecurityInfo::from_bytes(&bytes, true).unwrap();
231        assert_eq!(decoded, info);
232    }
233
234    #[test]
235    fn roundtrip_be() {
236        let info = EndpointSecurityInfo {
237            endpoint_security_attributes: attrs::IS_VALID | attrs::IS_SUBMESSAGE_PROTECTED,
238            plugin_endpoint_security_attributes: plugin_attrs::IS_VALID
239                | plugin_attrs::IS_SUBMESSAGE_ENCRYPTED,
240        };
241        let bytes = info.to_bytes(false);
242        let decoded = EndpointSecurityInfo::from_bytes(&bytes, false).unwrap();
243        assert_eq!(decoded, info);
244    }
245
246    #[test]
247    fn wire_size_is_eight_bytes() {
248        let info = EndpointSecurityInfo::plain();
249        assert_eq!(info.to_bytes(true).len(), 8);
250    }
251
252    #[test]
253    fn decode_rejects_short_input() {
254        let err = EndpointSecurityInfo::from_bytes(&[0u8; 7], true).unwrap_err();
255        assert!(matches!(err, WireError::UnexpectedEof { .. }));
256    }
257
258    #[test]
259    fn encoded_bytes_le_match_spec_layout() {
260        let info = EndpointSecurityInfo {
261            endpoint_security_attributes: 0x8000_0008,
262            plugin_endpoint_security_attributes: 0x8000_0001,
263        };
264        let bytes = info.to_bytes(true);
265        // attrs LE: 08 00 00 80; plugin LE: 01 00 00 80
266        assert_eq!(bytes, [0x08, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80]);
267    }
268
269    #[test]
270    fn protection_bit_accessors_read_correctly() {
271        let info = EndpointSecurityInfo {
272            endpoint_security_attributes: attrs::IS_VALID | attrs::IS_SUBMESSAGE_PROTECTED,
273            plugin_endpoint_security_attributes: plugin_attrs::IS_VALID
274                | plugin_attrs::IS_SUBMESSAGE_ENCRYPTED
275                | plugin_attrs::IS_SUBMESSAGE_ORIGIN_AUTHENTICATED,
276        };
277        assert!(info.is_submessage_protected());
278        assert!(!info.is_payload_protected());
279        assert!(info.is_submessage_encrypted());
280        assert!(info.is_submessage_origin_authenticated());
281        assert!(!info.is_payload_encrypted());
282    }
283
284    #[test]
285    fn is_valid_requires_both_masks() {
286        let only_attrs = EndpointSecurityInfo {
287            endpoint_security_attributes: attrs::IS_VALID,
288            plugin_endpoint_security_attributes: 0,
289        };
290        assert!(!only_attrs.is_valid());
291
292        let only_plugin = EndpointSecurityInfo {
293            endpoint_security_attributes: 0,
294            plugin_endpoint_security_attributes: plugin_attrs::IS_VALID,
295        };
296        assert!(!only_plugin.is_valid());
297
298        let both = EndpointSecurityInfo::plain();
299        assert!(both.is_valid());
300    }
301}