Skip to main content

zerodds_rtps/
security_algo_info.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! DDS-Security 1.2 §7.3.11-§7.3.15 — Algorithm-Info-Strukturen + PIDs
4//! 0x1010-0x1013 (C3.5-Rest).
5//!
6//! Spec verlangt drei neue Properties im SPDP-Announce, damit Peers
7//! ueber unterschiedliche Algorithm-Familien negotiate koennen:
8//!
9//! | PID    | Spec                       | Inhalt                                  |
10//! |--------|----------------------------|-----------------------------------------|
11//! | 0x1010 | §7.3.11                    | DigitalSignatureAlgorithmInfo (sig)     |
12//! | 0x1011 | §7.3.12                    | KeyEstablishmentAlgorithmInfo (kx)      |
13//! | 0x1012 | §7.3.13                    | SymmetricCipherAlgorithmInfo (sym)      |
14//! | 0x1013 | §7.3.15 (Endpoint-Level)   | EndpointSymmetricCipherAlgorithmInfo    |
15//!
16//! Bit-Konstanten (Spec §8.1 Tab.22 + §8.2 + §8.3, "CBIT" = CryptoBit):
17//!
18//! Symmetric (`SymmetricCipherBitId`):
19//! - bit 0 = AES128 (covers GCM + GMAC)
20//! - bit 1 = AES256 (covers GCM + GMAC)
21//!
22//! DigitalSignature (`DigitalSignatureBitId`):
23//! - bit 0 = RSASSA-PSS-MGF1SHA256+2048+SHA256
24//! - bit 1 = RSASSA-PKCS1-V1_5+2048+SHA256
25//! - bit 2 = ECDSA+P256+SHA256
26//! - bit 3 = ECDSA+P384+SHA384
27//!
28//! KeyEstablishment (`KeyEstablishmentBitId`):
29//! - bit 0 = DHE+MODP-2048-256
30//! - bit 1 = ECDHE-CEUM+P256
31//! - bit 2 = ECDHE-CEUM+P384
32//!
33//! Spec-Defaults (verwendet wenn ein Peer keine Algorithm-Info-PID
34//! schickt — Backwards-Compat mit DDS-Security 1.1):
35//! - sig.trust_chain: supported = required = (RSASSA_PSS | ECDSA_P256)
36//! - sig.message_auth: supported = required = (RSASSA_PSS | ECDSA_P256)
37//! - kx.shared_secret: supported = required = (DHE_MODP | ECDHE_P256)
38//! - sym.supported_mask: AES128 | AES256
39//! - sym.builtin_endpoints_required_mask: AES128
40//! - sym.builtin_kx_endpoints_required_mask: AES128
41//! - sym.user_endpoints_default_required_mask: AES128
42
43extern crate alloc;
44use alloc::vec::Vec;
45
46use crate::error::WireError;
47
48// ----------------------------------------------------------------------
49// Bit-Konstanten (Spec §8.1 Tab.22, §8.2, §8.3)
50// ----------------------------------------------------------------------
51
52/// Symmetric-Cipher-Bit-IDs (Spec §8.1).
53pub mod symmetric_bit {
54    /// AES-128 (GCM oder GMAC).
55    pub const AES128: u32 = 1 << 0;
56    /// AES-256 (GCM oder GMAC).
57    pub const AES256: u32 = 1 << 1;
58}
59
60/// Digital-Signature-Bit-IDs (Spec §8.2).
61pub mod digital_signature_bit {
62    /// RSASSA-PSS-MGF1SHA256+2048+SHA256.
63    pub const RSASSA_PSS_2048_SHA256: u32 = 1 << 0;
64    /// RSASSA-PKCS1-V1_5+2048+SHA256.
65    pub const RSASSA_PKCS1_V15_2048_SHA256: u32 = 1 << 1;
66    /// ECDSA+P256+SHA256.
67    pub const ECDSA_P256_SHA256: u32 = 1 << 2;
68    /// ECDSA+P384+SHA384.
69    pub const ECDSA_P384_SHA384: u32 = 1 << 3;
70}
71
72/// Key-Establishment-Bit-IDs (Spec §8.3).
73pub mod key_establishment_bit {
74    /// DHE+MODP-2048-256 (RFC 5114 Group).
75    pub const DHE_MODP_2048_256: u32 = 1 << 0;
76    /// ECDHE-CEUM+P256 (NIST P-256 ephemeral).
77    pub const ECDHE_CEUM_P256: u32 = 1 << 1;
78    /// ECDHE-CEUM+P384.
79    pub const ECDHE_CEUM_P384: u32 = 1 << 2;
80}
81
82// ----------------------------------------------------------------------
83// AlgorithmRequirements (Spec §7.3.10)
84// ----------------------------------------------------------------------
85
86/// Spec §7.3.10 — `AlgorithmRequirements { supported_mask, required_mask }`.
87/// Beide u32 BE auf der Wire (8 byte total).
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub struct AlgorithmRequirements {
90    /// Maske aller von diesem Participant unterstuetzten Algorithmen.
91    pub supported: u32,
92    /// Maske der Algorithmen, die der Participant *erfordert*. Eine
93    /// Compatibility-Pruefung (Spec §7.3.10) verlangt
94    /// `(remote.supported & local.required) == local.required`.
95    pub required: u32,
96}
97
98impl AlgorithmRequirements {
99    /// Wire-Size: 4 byte supported + 4 byte required = 8 byte.
100    pub const WIRE_SIZE: usize = 8;
101
102    /// Encode (4 byte u32 LE/BE × 2).
103    #[must_use]
104    pub fn to_bytes(&self, little_endian: bool) -> [u8; 8] {
105        let mut out = [0u8; 8];
106        if little_endian {
107            out[0..4].copy_from_slice(&self.supported.to_le_bytes());
108            out[4..8].copy_from_slice(&self.required.to_le_bytes());
109        } else {
110            out[0..4].copy_from_slice(&self.supported.to_be_bytes());
111            out[4..8].copy_from_slice(&self.required.to_be_bytes());
112        }
113        out
114    }
115
116    /// Decode aus 8 byte.
117    ///
118    /// # Errors
119    /// `ValueOutOfRange` bei Slice-Mismatch.
120    pub fn from_bytes(bytes: &[u8], little_endian: bool) -> Result<Self, WireError> {
121        if bytes.len() < 8 {
122            return Err(WireError::ValueOutOfRange {
123                message: "AlgorithmRequirements: < 8 bytes",
124            });
125        }
126        let mut s = [0u8; 4];
127        let mut r = [0u8; 4];
128        s.copy_from_slice(&bytes[0..4]);
129        r.copy_from_slice(&bytes[4..8]);
130        Ok(if little_endian {
131            Self {
132                supported: u32::from_le_bytes(s),
133                required: u32::from_le_bytes(r),
134            }
135        } else {
136            Self {
137                supported: u32::from_be_bytes(s),
138                required: u32::from_be_bytes(r),
139            }
140        })
141    }
142
143    /// Compatibility-Check (Spec §7.3.10): pruefe ob `remote.supported`
144    /// alle Bits von `self.required` enthaelt.
145    #[must_use]
146    pub fn is_compatible_with(&self, remote_supported: u32) -> bool {
147        (remote_supported & self.required) == self.required
148    }
149}
150
151// ----------------------------------------------------------------------
152// PID 0x1010 — ParticipantSecurityDigitalSignatureAlgorithmInfo
153// ----------------------------------------------------------------------
154
155/// Spec §7.3.11 — Sig-Algorithm-Info pro Participant.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct ParticipantSecurityDigitalSignatureAlgorithmInfo {
158    /// Trust-Chain-Algorithmen (Cert-Sig-Verify).
159    pub trust_chain: AlgorithmRequirements,
160    /// Message-Authentication-Algorithmen (Handshake-Signaturen, OCSP).
161    pub message_auth: AlgorithmRequirements,
162}
163
164impl ParticipantSecurityDigitalSignatureAlgorithmInfo {
165    /// Wire-Size: 16 byte (2 × `AlgorithmRequirements`).
166    pub const WIRE_SIZE: usize = 16;
167
168    /// Spec-Default (siehe Modul-Doku).
169    #[must_use]
170    pub fn spec_default() -> Self {
171        let mask = digital_signature_bit::RSASSA_PSS_2048_SHA256
172            | digital_signature_bit::ECDSA_P256_SHA256;
173        Self {
174            trust_chain: AlgorithmRequirements {
175                supported: mask,
176                required: mask,
177            },
178            message_auth: AlgorithmRequirements {
179                supported: mask,
180                required: mask,
181            },
182        }
183    }
184
185    /// Encode (16 byte).
186    #[must_use]
187    pub fn to_bytes(&self, little_endian: bool) -> [u8; 16] {
188        let mut out = [0u8; 16];
189        out[0..8].copy_from_slice(&self.trust_chain.to_bytes(little_endian));
190        out[8..16].copy_from_slice(&self.message_auth.to_bytes(little_endian));
191        out
192    }
193
194    /// Decode (16 byte).
195    ///
196    /// # Errors
197    /// `ValueOutOfRange` bei Slice-Mismatch.
198    pub fn from_bytes(bytes: &[u8], little_endian: bool) -> Result<Self, WireError> {
199        if bytes.len() < 16 {
200            return Err(WireError::ValueOutOfRange {
201                message: "DigitalSignatureAlgorithmInfo: < 16 bytes",
202            });
203        }
204        Ok(Self {
205            trust_chain: AlgorithmRequirements::from_bytes(&bytes[0..8], little_endian)?,
206            message_auth: AlgorithmRequirements::from_bytes(&bytes[8..16], little_endian)?,
207        })
208    }
209}
210
211// ----------------------------------------------------------------------
212// PID 0x1011 — ParticipantSecurityKeyEstablishmentAlgorithmInfo
213// ----------------------------------------------------------------------
214
215/// Spec §7.3.12 — Key-Establishment-Algorithm-Info pro Participant.
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct ParticipantSecurityKeyEstablishmentAlgorithmInfo {
218    /// Shared-Secret-Algorithmen (DH/ECDH).
219    pub shared_secret: AlgorithmRequirements,
220}
221
222impl ParticipantSecurityKeyEstablishmentAlgorithmInfo {
223    /// Wire-Size: 8 byte.
224    pub const WIRE_SIZE: usize = 8;
225
226    /// Spec-Default.
227    #[must_use]
228    pub fn spec_default() -> Self {
229        let mask =
230            key_establishment_bit::DHE_MODP_2048_256 | key_establishment_bit::ECDHE_CEUM_P256;
231        Self {
232            shared_secret: AlgorithmRequirements {
233                supported: mask,
234                required: mask,
235            },
236        }
237    }
238
239    /// Encode (8 byte).
240    #[must_use]
241    pub fn to_bytes(&self, little_endian: bool) -> [u8; 8] {
242        self.shared_secret.to_bytes(little_endian)
243    }
244
245    /// Decode (8 byte).
246    ///
247    /// # Errors
248    /// `ValueOutOfRange` bei Slice-Mismatch.
249    pub fn from_bytes(bytes: &[u8], little_endian: bool) -> Result<Self, WireError> {
250        Ok(Self {
251            shared_secret: AlgorithmRequirements::from_bytes(bytes, little_endian)?,
252        })
253    }
254}
255
256// ----------------------------------------------------------------------
257// PID 0x1012 — ParticipantSecuritySymmetricCipherAlgorithmInfo
258// ----------------------------------------------------------------------
259
260/// Spec §7.3.13 — Symmetric-Cipher-Algorithm-Info pro Participant.
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262pub struct ParticipantSecuritySymmetricCipherAlgorithmInfo {
263    /// Mask aller unterstuetzten Symmetric-Cipher-Familien
264    /// (siehe [`symmetric_bit`]).
265    pub supported_mask: u32,
266    /// Mask der Required-Algos fuer Builtin-Endpoints (Discovery,
267    /// Liveliness, Volatile, Stateless).
268    pub builtin_endpoints_required_mask: u32,
269    /// Mask der Required-Algos fuer Builtin-KX-Endpoints (Crypto-Token-
270    /// Exchange via VolatileMessageSecure).
271    pub builtin_kx_endpoints_required_mask: u32,
272    /// Default-Mask fuer User-Endpoints (kann von User-Endpoint-PID
273    /// 0x1013 ueberschrieben werden).
274    pub user_endpoints_default_required_mask: u32,
275}
276
277impl ParticipantSecuritySymmetricCipherAlgorithmInfo {
278    /// Wire-Size: 16 byte (4 × u32).
279    pub const WIRE_SIZE: usize = 16;
280
281    /// Spec-Default.
282    #[must_use]
283    pub fn spec_default() -> Self {
284        Self {
285            supported_mask: symmetric_bit::AES128 | symmetric_bit::AES256,
286            builtin_endpoints_required_mask: symmetric_bit::AES128,
287            builtin_kx_endpoints_required_mask: symmetric_bit::AES128,
288            user_endpoints_default_required_mask: symmetric_bit::AES128,
289        }
290    }
291
292    /// Encode (16 byte).
293    #[must_use]
294    pub fn to_bytes(&self, little_endian: bool) -> [u8; 16] {
295        let mut out = [0u8; 16];
296        let fields = [
297            self.supported_mask,
298            self.builtin_endpoints_required_mask,
299            self.builtin_kx_endpoints_required_mask,
300            self.user_endpoints_default_required_mask,
301        ];
302        for (i, v) in fields.iter().enumerate() {
303            let bytes = if little_endian {
304                v.to_le_bytes()
305            } else {
306                v.to_be_bytes()
307            };
308            out[i * 4..i * 4 + 4].copy_from_slice(&bytes);
309        }
310        out
311    }
312
313    /// Decode (16 byte).
314    ///
315    /// # Errors
316    /// `ValueOutOfRange` bei Slice-Mismatch.
317    pub fn from_bytes(bytes: &[u8], little_endian: bool) -> Result<Self, WireError> {
318        if bytes.len() < 16 {
319            return Err(WireError::ValueOutOfRange {
320                message: "SymmetricCipherAlgorithmInfo: < 16 bytes",
321            });
322        }
323        let read = |off: usize| -> u32 {
324            let mut a = [0u8; 4];
325            a.copy_from_slice(&bytes[off..off + 4]);
326            if little_endian {
327                u32::from_le_bytes(a)
328            } else {
329                u32::from_be_bytes(a)
330            }
331        };
332        Ok(Self {
333            supported_mask: read(0),
334            builtin_endpoints_required_mask: read(4),
335            builtin_kx_endpoints_required_mask: read(8),
336            user_endpoints_default_required_mask: read(12),
337        })
338    }
339}
340
341// ----------------------------------------------------------------------
342// PID 0x1013 — EndpointSecuritySymmetricCipherAlgorithmInfo
343// ----------------------------------------------------------------------
344
345/// Spec §7.3.15 — Symmetric-Cipher-Algorithm-Info pro Endpoint
346/// (DataWriter / DataReader). Wird in Pub/SubscriptionBuiltinTopicData
347/// gefuehrt — ueberschreibt den Participant-Default fuer diesen Slot.
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
349pub struct EndpointSecuritySymmetricCipherAlgorithmInfo {
350    /// Required-Mask fuer diesen Endpoint (siehe [`symmetric_bit`]).
351    pub required_mask: u32,
352}
353
354impl EndpointSecuritySymmetricCipherAlgorithmInfo {
355    /// Wire-Size: 4 byte.
356    pub const WIRE_SIZE: usize = 4;
357
358    /// Encode (4 byte).
359    #[must_use]
360    pub fn to_bytes(&self, little_endian: bool) -> [u8; 4] {
361        if little_endian {
362            self.required_mask.to_le_bytes()
363        } else {
364            self.required_mask.to_be_bytes()
365        }
366    }
367
368    /// Decode (4 byte).
369    ///
370    /// # Errors
371    /// `ValueOutOfRange` bei Slice-Mismatch.
372    pub fn from_bytes(bytes: &[u8], little_endian: bool) -> Result<Self, WireError> {
373        if bytes.len() < 4 {
374            return Err(WireError::ValueOutOfRange {
375                message: "EndpointSymmetricCipherAlgorithmInfo: < 4 bytes",
376            });
377        }
378        let mut a = [0u8; 4];
379        a.copy_from_slice(&bytes[0..4]);
380        Ok(Self {
381            required_mask: if little_endian {
382                u32::from_le_bytes(a)
383            } else {
384                u32::from_be_bytes(a)
385            },
386        })
387    }
388}
389
390/// Suppress-warning fuer `Vec`-Import (wird in Tests benutzt).
391#[allow(dead_code)]
392fn _vec_keepalive(v: Vec<u8>) -> Vec<u8> {
393    v
394}
395
396#[cfg(test)]
397#[allow(clippy::expect_used, clippy::unwrap_used)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn algorithm_requirements_roundtrip_le() {
403        let a = AlgorithmRequirements {
404            supported: 0xCAFE_BABE,
405            required: 0xDEAD_BEEF,
406        };
407        let bytes = a.to_bytes(true);
408        let back = AlgorithmRequirements::from_bytes(&bytes, true).unwrap();
409        assert_eq!(a, back);
410    }
411
412    #[test]
413    fn algorithm_requirements_roundtrip_be() {
414        let a = AlgorithmRequirements {
415            supported: 0x0102_0304,
416            required: 0x0506_0708,
417        };
418        let bytes = a.to_bytes(false);
419        let back = AlgorithmRequirements::from_bytes(&bytes, false).unwrap();
420        assert_eq!(a, back);
421    }
422
423    #[test]
424    fn algorithm_requirements_layout_be() {
425        // Spec-konforme BE: 4 byte supported + 4 byte required.
426        let a = AlgorithmRequirements {
427            supported: 0x01,
428            required: 0x02,
429        };
430        assert_eq!(a.to_bytes(false), [0, 0, 0, 0x01, 0, 0, 0, 0x02]);
431    }
432
433    #[test]
434    fn compatibility_check_strict() {
435        let local = AlgorithmRequirements {
436            supported: 0,
437            required: 0b101,
438        };
439        // Remote unterstuetzt alle 3 → kompatibel.
440        assert!(local.is_compatible_with(0b111));
441        // Remote unterstuetzt bit 0 + 2 (genau die required Bits) → ok.
442        assert!(local.is_compatible_with(0b101));
443        // Remote unterstuetzt bit 0 alleine → fehlt bit 2 → reject.
444        assert!(!local.is_compatible_with(0b001));
445        // Remote unterstuetzt nichts → reject.
446        assert!(!local.is_compatible_with(0));
447    }
448
449    #[test]
450    fn sig_info_spec_default() {
451        let d = ParticipantSecurityDigitalSignatureAlgorithmInfo::spec_default();
452        let expected = digital_signature_bit::RSASSA_PSS_2048_SHA256
453            | digital_signature_bit::ECDSA_P256_SHA256;
454        assert_eq!(d.trust_chain.supported, expected);
455        assert_eq!(d.trust_chain.required, expected);
456        assert_eq!(d.message_auth.supported, expected);
457        assert_eq!(d.message_auth.required, expected);
458    }
459
460    #[test]
461    fn sig_info_roundtrip() {
462        let d = ParticipantSecurityDigitalSignatureAlgorithmInfo::spec_default();
463        let bytes = d.to_bytes(true);
464        let back =
465            ParticipantSecurityDigitalSignatureAlgorithmInfo::from_bytes(&bytes, true).unwrap();
466        assert_eq!(d, back);
467    }
468
469    #[test]
470    fn kx_info_spec_default() {
471        let k = ParticipantSecurityKeyEstablishmentAlgorithmInfo::spec_default();
472        let expected =
473            key_establishment_bit::DHE_MODP_2048_256 | key_establishment_bit::ECDHE_CEUM_P256;
474        assert_eq!(k.shared_secret.supported, expected);
475        assert_eq!(k.shared_secret.required, expected);
476    }
477
478    #[test]
479    fn kx_info_roundtrip() {
480        let k = ParticipantSecurityKeyEstablishmentAlgorithmInfo::spec_default();
481        let bytes = k.to_bytes(true);
482        let back =
483            ParticipantSecurityKeyEstablishmentAlgorithmInfo::from_bytes(&bytes, true).unwrap();
484        assert_eq!(k, back);
485    }
486
487    #[test]
488    fn sym_info_spec_default() {
489        let s = ParticipantSecuritySymmetricCipherAlgorithmInfo::spec_default();
490        assert_eq!(
491            s.supported_mask,
492            symmetric_bit::AES128 | symmetric_bit::AES256
493        );
494        assert_eq!(s.builtin_endpoints_required_mask, symmetric_bit::AES128);
495        assert_eq!(s.builtin_kx_endpoints_required_mask, symmetric_bit::AES128);
496        assert_eq!(
497            s.user_endpoints_default_required_mask,
498            symmetric_bit::AES128
499        );
500    }
501
502    #[test]
503    fn sym_info_roundtrip() {
504        let s = ParticipantSecuritySymmetricCipherAlgorithmInfo::spec_default();
505        let bytes = s.to_bytes(true);
506        let back =
507            ParticipantSecuritySymmetricCipherAlgorithmInfo::from_bytes(&bytes, true).unwrap();
508        assert_eq!(s, back);
509    }
510
511    #[test]
512    fn endpoint_sym_info_roundtrip() {
513        let e = EndpointSecuritySymmetricCipherAlgorithmInfo {
514            required_mask: symmetric_bit::AES256,
515        };
516        let bytes = e.to_bytes(true);
517        let back = EndpointSecuritySymmetricCipherAlgorithmInfo::from_bytes(&bytes, true).unwrap();
518        assert_eq!(e, back);
519    }
520
521    #[test]
522    fn truncated_buffer_rejected() {
523        assert!(AlgorithmRequirements::from_bytes(&[1, 2, 3], true).is_err());
524        assert!(
525            ParticipantSecurityDigitalSignatureAlgorithmInfo::from_bytes(&[0u8; 15], true).is_err()
526        );
527        assert!(
528            ParticipantSecuritySymmetricCipherAlgorithmInfo::from_bytes(&[0u8; 15], true).is_err()
529        );
530        assert!(EndpointSecuritySymmetricCipherAlgorithmInfo::from_bytes(&[0u8; 3], true).is_err());
531    }
532
533    #[test]
534    fn spec_bit_constants_match() {
535        // Spec §8.1 Tab.22 + §8.2 + §8.3 — diese Konstanten duerfen
536        // NIE driften, sonst sieht ein Cyclone-Peer unsere Caps falsch.
537        assert_eq!(symmetric_bit::AES128, 0x01);
538        assert_eq!(symmetric_bit::AES256, 0x02);
539        assert_eq!(digital_signature_bit::RSASSA_PSS_2048_SHA256, 0x01);
540        assert_eq!(digital_signature_bit::RSASSA_PKCS1_V15_2048_SHA256, 0x02);
541        assert_eq!(digital_signature_bit::ECDSA_P256_SHA256, 0x04);
542        assert_eq!(digital_signature_bit::ECDSA_P384_SHA384, 0x08);
543        assert_eq!(key_establishment_bit::DHE_MODP_2048_256, 0x01);
544        assert_eq!(key_establishment_bit::ECDHE_CEUM_P256, 0x02);
545        assert_eq!(key_establishment_bit::ECDHE_CEUM_P384, 0x04);
546    }
547}