Skip to main content

zerodds_security_rtps/
codec.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Wire-Format fuer Secure-Submessages.
5//!
6//! zerodds-lint: allow no_dyn_in_safe
7//! (Der Codec nimmt `&dyn CryptographicPlugin` entgegen, damit die
8//! Crypto-Implementation austauschbar bleibt — architektur-bedingt.)
9//!
10//! ```text
11//! Submessage-Header (4 bytes):
12//!   +---+---+-------+
13//!   | id|flg| length|
14//!   +---+---+-------+
15//!     u8  u8  u16 (LE wenn flg & 0x01 gesetzt)
16//! ```
17//!
18//! SEC_PREFIX:   id=0x31, body = TransformIdentifier (16 byte)
19//! SEC_BODY:     id=0x30, body = u32 length + ciphertext
20//! SEC_POSTFIX:  id=0x32, body = MAC-Liste fuer Receiver-Specific-MACs
21
22use alloc::vec::Vec;
23
24use zerodds_security::crypto::{CryptoHandle, CryptographicPlugin, ReceiverMac};
25use zerodds_security::error::SecurityError;
26
27/// SEC_PREFIX Submessage-ID (Spec §7.3.6.2).
28pub const SEC_PREFIX: u8 = 0x31;
29/// SEC_POSTFIX Submessage-ID (Spec §7.3.6.3).
30pub const SEC_POSTFIX: u8 = 0x32;
31/// SEC_BODY Submessage-ID (Spec §7.3.6.4).
32pub const SEC_BODY: u8 = 0x30;
33/// SRTPS_PREFIX Submessage-ID (Spec §7.3.6.5).
34pub const SRTPS_PREFIX: u8 = 0x33;
35/// SRTPS_POSTFIX Submessage-ID (Spec §7.3.6.6).
36pub const SRTPS_POSTFIX: u8 = 0x34;
37
38/// Endianness-Flag: `0x01` bedeutet Little-Endian in der Submessage.
39const FLAG_LE: u8 = 0x01;
40
41/// Fehler beim Kodieren/Dekodieren.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum SecurityRtpsError {
44    /// Input-Bytes zu kurz fuer die erwartete Submessage-Struktur.
45    Truncated(&'static str),
46    /// Submessage-ID passt nicht zum erwarteten Slot (z.B. SEC_PREFIX
47    /// fehlt oder SEC_BODY-ID falsch).
48    UnexpectedSubmessageId {
49        /// Position der Submessage im Container (0-indexed).
50        pos: usize,
51        /// Erwartete ID.
52        expected: u8,
53        /// Tatsaechlich gelesene ID.
54        got: u8,
55    },
56    /// Big-Endian-Sec-Submessage — Big-Endian-Sec-Submessage (Major-2.0-additive).
57    BigEndianNotSupported,
58    /// Ciphertext-Laenge im SEC_BODY stimmt nicht mit dem Submessage-
59    /// Length-Header ueberein (Wire-Tampering?).
60    InconsistentLength,
61    /// Crypto-Plugin-Fehler durchgereicht.
62    Crypto(SecurityError),
63}
64
65impl core::fmt::Display for SecurityRtpsError {
66    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
67        match self {
68            Self::Truncated(what) => write!(f, "secured submessage truncated at {what}"),
69            Self::UnexpectedSubmessageId { pos, expected, got } => write!(
70                f,
71                "secured submessage #{pos} id 0x{got:02x}, expected 0x{expected:02x}"
72            ),
73            Self::BigEndianNotSupported => write!(
74                f,
75                "big-endian SEC_* not supported (Single-Endianness-Pfad, LE per Default)"
76            ),
77            Self::InconsistentLength => write!(f, "SEC_BODY length header != payload"),
78            Self::Crypto(e) => write!(f, "crypto plugin: {e}"),
79        }
80    }
81}
82
83#[cfg(feature = "std")]
84impl std::error::Error for SecurityRtpsError {}
85
86impl From<SecurityError> for SecurityRtpsError {
87    fn from(e: SecurityError) -> Self {
88        Self::Crypto(e)
89    }
90}
91
92/// Kodiert ein plain-Submessage-Blob als **secured** Submessage-
93/// Sequenz (SEC_PREFIX + SEC_BODY + SEC_POSTFIX).
94///
95/// Der Crypto-Plugin liefert den eigentlichen Ciphertext; dieses
96/// Modul kuemmert sich nur ums Wire-Framing.
97///
98/// # Errors
99/// Weitergereichter Crypto-Error oder Laengen-Overflow (u16).
100pub fn encode_secured_submessage(
101    plugin: &dyn CryptographicPlugin,
102    local: CryptoHandle,
103    remote_list: &[CryptoHandle],
104    plaintext: &[u8],
105) -> Result<Vec<u8>, SecurityRtpsError> {
106    // SEC_PREFIX-Body: 16 byte TransformIdentifier (TransformKindId(4)
107    // + KeyId(4) + TransformId(8)). Aktuell alle Null — der dynamische
108    // Wert kommt mit der DCPS-RTPS-Handle-Map-Integration.
109    let sec_prefix_body = [0u8; 16];
110
111    // AAD-Extension per DDS-Security 1.2 §10.5.2 Tab.78 (Submessage-
112    // Protection): reserved-4 || SEC_PREFIX-CryptoHeader. Der Plugin's
113    // `mat.aad(extension)` prependet zusätzlich `transformation_kind ||
114    // key_id || session_id`. Damit ist der SEC_PREFIX-Header gegen
115    // Tampering geschützt.
116    let mut aad_extension = Vec::with_capacity(4 + 16);
117    aad_extension.extend_from_slice(&[0u8; 4]); // reserved-4
118    aad_extension.extend_from_slice(&sec_prefix_body);
119
120    // 1) Crypto-Plugin verschlüsselt mit Spec-konformer AAD.
121    let ciphertext = plugin.encrypt_submessage(local, remote_list, plaintext, &aad_extension)?;
122
123    // 2) SEC_PREFIX-Submessage schreiben.
124    let mut out = Vec::with_capacity(4 + 16 + 4 + 4 + ciphertext.len() + 4);
125    push_header(&mut out, SEC_PREFIX, 16);
126    out.extend_from_slice(&sec_prefix_body);
127
128    // 3) SEC_BODY: u32 length + ciphertext.
129    let ct_len = u32::try_from(ciphertext.len())
130        .map_err(|_| SecurityRtpsError::Truncated("ciphertext > u32"))?;
131    let body_len = u16::try_from(4 + ciphertext.len())
132        .map_err(|_| SecurityRtpsError::Truncated("SEC_BODY > u16"))?;
133    push_header(&mut out, SEC_BODY, body_len);
134    out.extend_from_slice(&ct_len.to_le_bytes());
135    out.extend_from_slice(&ciphertext);
136
137    // 4) SEC_POSTFIX: leer (Single-MAC-Pfad). Multi-MAC-Variante:
138    //    `encode_secured_submessage_multi`.
139    push_header(&mut out, SEC_POSTFIX, 0);
140
141    Ok(out)
142}
143
144/// Dekodiert eine Secure-Submessage-Sequenz und liefert den
145/// plaintext zurueck.
146///
147/// # Errors
148/// Bei Wire-Tampering (Submessage-IDs falsch, Laengen inkonsistent),
149/// Big-Endian, oder Crypto-Verify-Fail.
150pub fn decode_secured_submessage(
151    plugin: &dyn CryptographicPlugin,
152    local: CryptoHandle,
153    remote: CryptoHandle,
154    secured_bytes: &[u8],
155) -> Result<Vec<u8>, SecurityRtpsError> {
156    let mut cur = Cursor::new(secured_bytes);
157
158    // SEC_PREFIX. Body-Bytes (16 Byte TransformIdentifier) sind Teil
159    // der AAD-Extension — symmetrisch zum Encoder.
160    let (id, _flags, plen) = read_header(&mut cur, "SEC_PREFIX")?;
161    if id != SEC_PREFIX {
162        return Err(SecurityRtpsError::UnexpectedSubmessageId {
163            pos: 0,
164            expected: SEC_PREFIX,
165            got: id,
166        });
167    }
168    let sec_prefix_body = cur.read_bytes(plen as usize, "SEC_PREFIX body")?;
169    let mut aad_extension = Vec::with_capacity(4 + sec_prefix_body.len());
170    aad_extension.extend_from_slice(&[0u8; 4]);
171    aad_extension.extend_from_slice(sec_prefix_body);
172
173    // SEC_BODY.
174    let (id, _flags, blen) = read_header(&mut cur, "SEC_BODY header")?;
175    if id != SEC_BODY {
176        return Err(SecurityRtpsError::UnexpectedSubmessageId {
177            pos: 1,
178            expected: SEC_BODY,
179            got: id,
180        });
181    }
182    let ct_len_raw = cur.read_u32_le("SEC_BODY length")?;
183    if (ct_len_raw as usize) + 4 != (blen as usize) {
184        return Err(SecurityRtpsError::InconsistentLength);
185    }
186    let ciphertext = cur.read_bytes(ct_len_raw as usize, "SEC_BODY ciphertext")?;
187
188    // SEC_POSTFIX (leer im Single-Receiver-Modus; ID-Check gibt Wire-Integrity).
189    let (id, _flags, postlen) = read_header(&mut cur, "SEC_POSTFIX")?;
190    if id != SEC_POSTFIX {
191        return Err(SecurityRtpsError::UnexpectedSubmessageId {
192            pos: 2,
193            expected: SEC_POSTFIX,
194            got: id,
195        });
196    }
197    cur.skip(postlen as usize, "SEC_POSTFIX body")?;
198
199    let plain = plugin.decrypt_submessage(local, remote, ciphertext, &aad_extension)?;
200    Ok(plain)
201}
202
203fn push_header(out: &mut Vec<u8>, id: u8, length: u16) {
204    out.push(id);
205    out.push(FLAG_LE);
206    out.extend_from_slice(&length.to_le_bytes());
207}
208
209/// DoS-Cap fuer die MAC-Liste im SEC_POSTFIX. Jeder MAC ist 20 Bytes;
210/// 256 MACs = 5 KiB — ausreichend fuer Hetero-Deployments mit
211/// hundertschaft Readers pro Writer, aber weit unter RAM-Angriffs-
212/// Threshold.
213pub const MAX_RECEIVER_MACS: usize = 256;
214
215/// Encoded ein plain-Submessage-Blob als secured Sequenz MIT
216/// Receiver-Specific-MACs im SEC_POSTFIX(Spec §7.3.6.3).
217///
218/// Der Crypto-Plugin liefert einen gemeinsamen Ciphertext plus eine
219/// Liste von `(key_id, mac)`-Eintraegen, einer pro Reader.
220///
221/// # Wire-Layout SEC_POSTFIX (body)
222/// ```text
223///   u32  count
224///   [ u32 key_id ; u8 mac[16] ] * count     // 20 byte pro Eintrag
225/// ```
226///
227/// # Errors
228/// * `Crypto` durchgereicht vom Plugin.
229/// * `Truncated` wenn die MAC-Liste > `MAX_RECEIVER_MACS` ist oder
230///   der Ciphertext > `u32::MAX` / SEC_POSTFIX-body > `u16::MAX`.
231pub fn encode_secured_submessage_multi(
232    plugin: &dyn CryptographicPlugin,
233    local: CryptoHandle,
234    receivers: &[(CryptoHandle, u32)],
235    plaintext: &[u8],
236) -> Result<Vec<u8>, SecurityRtpsError> {
237    // SEC_PREFIX-Body + AAD-Extension (analog encode_secured_submessage).
238    let sec_prefix_body = [0u8; 16];
239    let mut aad_extension = Vec::with_capacity(4 + 16);
240    aad_extension.extend_from_slice(&[0u8; 4]);
241    aad_extension.extend_from_slice(&sec_prefix_body);
242
243    let (ciphertext, macs) =
244        plugin.encrypt_submessage_multi(local, receivers, plaintext, &aad_extension)?;
245    if macs.len() > MAX_RECEIVER_MACS {
246        return Err(SecurityRtpsError::Truncated(
247            "receiver-specific mac count exceeds cap",
248        ));
249    }
250
251    let postfix_body_len = 4usize.saturating_add(macs.len().saturating_mul(ReceiverMac::WIRE_SIZE));
252    let postfix_body_len_u16 = u16::try_from(postfix_body_len)
253        .map_err(|_| SecurityRtpsError::Truncated("SEC_POSTFIX > u16"))?;
254
255    let mut out = Vec::with_capacity(4 + 16 + 4 + 4 + ciphertext.len() + 4 + postfix_body_len);
256
257    // SEC_PREFIX (16 byte TransformIdentifier).
258    push_header(&mut out, SEC_PREFIX, 16);
259    out.extend_from_slice(&sec_prefix_body);
260
261    // SEC_BODY.
262    let ct_len = u32::try_from(ciphertext.len())
263        .map_err(|_| SecurityRtpsError::Truncated("ciphertext > u32"))?;
264    let body_len = u16::try_from(4 + ciphertext.len())
265        .map_err(|_| SecurityRtpsError::Truncated("SEC_BODY > u16"))?;
266    push_header(&mut out, SEC_BODY, body_len);
267    out.extend_from_slice(&ct_len.to_le_bytes());
268    out.extend_from_slice(&ciphertext);
269
270    // SEC_POSTFIX mit Multi-MAC-Payload.
271    push_header(&mut out, SEC_POSTFIX, postfix_body_len_u16);
272    let n =
273        u32::try_from(macs.len()).map_err(|_| SecurityRtpsError::Truncated("mac count > u32"))?;
274    out.extend_from_slice(&n.to_le_bytes());
275    for m in &macs {
276        out.extend_from_slice(&m.key_id.to_le_bytes());
277        out.extend_from_slice(&m.mac);
278    }
279
280    Ok(out)
281}
282
283/// Dekodiert eine Secure-Submessage-Sequenz MIT Multi-MAC-SEC_POSTFIX
284/// und liefert den plaintext zurueck.
285///
286/// `own_receiver_handle` identifiziert unsere eigene Empfaenger-
287/// Position in der MAC-Liste — der Plugin nutzt das, um den richtigen
288/// MAC-Eintrag zu finden und zu validieren (Spec §7.3.6.3).
289///
290/// Wenn die eingebettete MAC-Liste leer ist, wird auf den
291/// v1.4-Pfad `decode_secured_submessage` zurueckgefallen (Backward-
292/// Compat: ein Legacy-Sender hat nur `common_mac` im AEAD-Tag).
293///
294/// # Errors
295/// * `Crypto` bei MAC-Mismatch / AEAD-Verify-Fail.
296/// * `Truncated` bei zu kurzen Eingaben.
297pub fn decode_secured_submessage_multi(
298    plugin: &dyn CryptographicPlugin,
299    local: CryptoHandle,
300    remote: CryptoHandle,
301    own_key_id: u32,
302    own_mac_key_handle: CryptoHandle,
303    secured_bytes: &[u8],
304) -> Result<Vec<u8>, SecurityRtpsError> {
305    let mut cur = Cursor::new(secured_bytes);
306
307    // SEC_PREFIX. Body-Bytes (16 Byte TransformIdentifier) sind Teil
308    // der AAD-Extension — symmetrisch zum Encoder.
309    let (id, _flags, plen) = read_header(&mut cur, "SEC_PREFIX")?;
310    if id != SEC_PREFIX {
311        return Err(SecurityRtpsError::UnexpectedSubmessageId {
312            pos: 0,
313            expected: SEC_PREFIX,
314            got: id,
315        });
316    }
317    let sec_prefix_body = cur.read_bytes(plen as usize, "SEC_PREFIX body")?;
318    let mut aad_extension = Vec::with_capacity(4 + sec_prefix_body.len());
319    aad_extension.extend_from_slice(&[0u8; 4]);
320    aad_extension.extend_from_slice(sec_prefix_body);
321
322    // SEC_BODY.
323    let (id, _flags, blen) = read_header(&mut cur, "SEC_BODY header")?;
324    if id != SEC_BODY {
325        return Err(SecurityRtpsError::UnexpectedSubmessageId {
326            pos: 1,
327            expected: SEC_BODY,
328            got: id,
329        });
330    }
331    let ct_len_raw = cur.read_u32_le("SEC_BODY length")?;
332    if (ct_len_raw as usize) + 4 != (blen as usize) {
333        return Err(SecurityRtpsError::InconsistentLength);
334    }
335    let ciphertext = cur.read_bytes(ct_len_raw as usize, "SEC_BODY ciphertext")?;
336
337    // SEC_POSTFIX mit Multi-MAC-Payload.
338    let (id, _flags, postlen) = read_header(&mut cur, "SEC_POSTFIX")?;
339    if id != SEC_POSTFIX {
340        return Err(SecurityRtpsError::UnexpectedSubmessageId {
341            pos: 2,
342            expected: SEC_POSTFIX,
343            got: id,
344        });
345    }
346    let macs = if postlen == 0 {
347        Vec::new()
348    } else {
349        let count = cur.read_u32_le("SEC_POSTFIX mac count")? as usize;
350        if count > MAX_RECEIVER_MACS {
351            return Err(SecurityRtpsError::Truncated(
352                "SEC_POSTFIX mac count exceeds cap",
353            ));
354        }
355        let expected_body = 4usize.saturating_add(count.saturating_mul(ReceiverMac::WIRE_SIZE));
356        if expected_body != postlen as usize {
357            return Err(SecurityRtpsError::InconsistentLength);
358        }
359        let mut out = Vec::with_capacity(count);
360        for _ in 0..count {
361            let key_id = cur.read_u32_le("SEC_POSTFIX mac key_id")?;
362            let mac_bytes = cur.read_bytes(16, "SEC_POSTFIX mac body")?;
363            let mut mac = [0u8; 16];
364            mac.copy_from_slice(mac_bytes);
365            out.push(ReceiverMac { key_id, mac });
366        }
367        out
368    };
369
370    let plain = plugin.decrypt_submessage_with_receiver_mac(
371        local,
372        remote,
373        own_key_id,
374        own_mac_key_handle,
375        ciphertext,
376        &macs,
377        &aad_extension,
378    )?;
379    Ok(plain)
380}
381
382struct Cursor<'a> {
383    bytes: &'a [u8],
384    pos: usize,
385}
386
387impl<'a> Cursor<'a> {
388    fn new(bytes: &'a [u8]) -> Self {
389        Self { bytes, pos: 0 }
390    }
391
392    fn need(&self, n: usize, what: &'static str) -> Result<(), SecurityRtpsError> {
393        if self.pos + n > self.bytes.len() {
394            return Err(SecurityRtpsError::Truncated(what));
395        }
396        Ok(())
397    }
398
399    fn read_bytes(&mut self, n: usize, what: &'static str) -> Result<&'a [u8], SecurityRtpsError> {
400        self.need(n, what)?;
401        let out = &self.bytes[self.pos..self.pos + n];
402        self.pos += n;
403        Ok(out)
404    }
405
406    fn skip(&mut self, n: usize, what: &'static str) -> Result<(), SecurityRtpsError> {
407        self.need(n, what)?;
408        self.pos += n;
409        Ok(())
410    }
411
412    fn read_u32_le(&mut self, what: &'static str) -> Result<u32, SecurityRtpsError> {
413        self.need(4, what)?;
414        let mut b = [0u8; 4];
415        b.copy_from_slice(&self.bytes[self.pos..self.pos + 4]);
416        self.pos += 4;
417        Ok(u32::from_le_bytes(b))
418    }
419}
420
421fn read_header(
422    cur: &mut Cursor<'_>,
423    what: &'static str,
424) -> Result<(u8, u8, u16), SecurityRtpsError> {
425    cur.need(4, what)?;
426    let id = cur.bytes[cur.pos];
427    let flags = cur.bytes[cur.pos + 1];
428    if flags & FLAG_LE == 0 {
429        return Err(SecurityRtpsError::BigEndianNotSupported);
430    }
431    let mut l = [0u8; 2];
432    l.copy_from_slice(&cur.bytes[cur.pos + 2..cur.pos + 4]);
433    cur.pos += 4;
434    Ok((id, flags, u16::from_le_bytes(l)))
435}
436
437#[cfg(test)]
438#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
439mod tests {
440    use super::*;
441    use zerodds_security::authentication::{IdentityHandle, SharedSecretHandle};
442    use zerodds_security::error::SecurityErrorKind;
443    use zerodds_security_crypto::AesGcmCryptoPlugin;
444
445    fn make_plugin() -> (AesGcmCryptoPlugin, CryptoHandle, CryptoHandle) {
446        let mut p = AesGcmCryptoPlugin::new();
447        let local = p
448            .register_local_participant(IdentityHandle(1), &[])
449            .unwrap();
450        let remote = p
451            .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
452            .unwrap();
453        (p, local, remote)
454    }
455
456    #[test]
457    fn encode_produces_three_submessages() {
458        let (p, local, remote) = make_plugin();
459        let plain = b"plain-rtps-submessage-bytes";
460        let secured = encode_secured_submessage(&p, local, &[remote], plain).unwrap();
461        // Erste Bytes: SEC_PREFIX-Header.
462        assert_eq!(secured[0], SEC_PREFIX);
463        // Irgendwo in den folgenden Bytes steht SEC_BODY und SEC_POSTFIX.
464        assert!(secured.contains(&SEC_BODY));
465        assert!(secured.contains(&SEC_POSTFIX));
466    }
467
468    #[test]
469    fn roundtrip_matches_plaintext() {
470        let (p, local, remote) = make_plugin();
471        let plain = b"hello secure dds";
472        let secured = encode_secured_submessage(&p, local, &[remote], plain).unwrap();
473        let back = decode_secured_submessage(&p, local, remote, &secured).unwrap();
474        assert_eq!(back, plain);
475    }
476
477    #[test]
478    fn tampered_ciphertext_fails_verify() {
479        let (p, local, remote) = make_plugin();
480        let plain = b"0123456789abcdef";
481        let mut secured = encode_secured_submessage(&p, local, &[remote], plain).unwrap();
482
483        // SEC_BODY beginnt bei offset 4 (PREFIX header) + 16 (PREFIX body) =
484        // 20, dann 4 (BODY header) + 4 (u32 ct_len) = 28. Byte 30 liegt im
485        // ciphertext nach nonce.
486        secured[30 + 12] ^= 0x10;
487
488        let err = decode_secured_submessage(&p, local, remote, &secured).unwrap_err();
489        match err {
490            SecurityRtpsError::Crypto(e) => assert_eq!(e.kind, SecurityErrorKind::CryptoFailed),
491            other => panic!("expected Crypto, got {other:?}"),
492        }
493    }
494
495    #[test]
496    fn wrong_prefix_id_rejected() {
497        let (p, local, remote) = make_plugin();
498        let mut secured = encode_secured_submessage(&p, local, &[remote], b"abc").unwrap();
499        secured[0] = 0x15; // irgendein anderer Submessage-Typ
500        let err = decode_secured_submessage(&p, local, remote, &secured).unwrap_err();
501        assert!(matches!(
502            err,
503            SecurityRtpsError::UnexpectedSubmessageId {
504                pos: 0,
505                expected: SEC_PREFIX,
506                ..
507            }
508        ));
509    }
510
511    #[test]
512    fn big_endian_flag_rejected() {
513        let (p, local, remote) = make_plugin();
514        let mut secured = encode_secured_submessage(&p, local, &[remote], b"x").unwrap();
515        secured[1] = 0x00; // flags = BE
516        let err = decode_secured_submessage(&p, local, remote, &secured).unwrap_err();
517        assert!(matches!(err, SecurityRtpsError::BigEndianNotSupported));
518    }
519
520    #[test]
521    fn truncated_input_rejected() {
522        let (p, local, remote) = make_plugin();
523        let err = decode_secured_submessage(&p, local, remote, &[SEC_PREFIX, 0x01]).unwrap_err();
524        assert!(matches!(err, SecurityRtpsError::Truncated(_)));
525    }
526
527    #[test]
528    fn constants_match_spec() {
529        assert_eq!(SEC_BODY, 0x30);
530        assert_eq!(SEC_PREFIX, 0x31);
531        assert_eq!(SEC_POSTFIX, 0x32);
532        assert_eq!(SRTPS_PREFIX, 0x33);
533        assert_eq!(SRTPS_POSTFIX, 0x34);
534    }
535
536    // =======================================================================
537    // Multi-MAC-Encoding (Receiver-Specific-MACs)
538    // =======================================================================
539
540    /// Baut 3 Receiver-Slots mit jeweils eigenem random-master-key
541    /// (Simulation: pro Receiver ein eigener HMAC-Schluessel, wie er
542    /// aus getrennten SharedSecrets abgeleitet wuerde).
543    fn make_plugin_with_three_receivers() -> (
544        AesGcmCryptoPlugin,
545        CryptoHandle,
546        [CryptoHandle; 3],
547        [CryptoHandle; 3],
548    ) {
549        let mut p = AesGcmCryptoPlugin::new();
550        let sender = p
551            .register_local_participant(IdentityHandle(1), &[])
552            .unwrap();
553
554        // Pro Receiver: auf Sender-Seite ein eigener Handle (der
555        // spaeter als Multi-MAC-Key genutzt wird). Wir registrieren
556        // einfach 3 lokale Endpoints (jeder bekommt random key_material)
557        // und kopieren ihre Tokens auf die "Receiver-Seite" im selben
558        // Plugin.
559        let r1_sender = p.register_local_endpoint(sender, true, &[]).unwrap();
560        let r2_sender = p.register_local_endpoint(sender, true, &[]).unwrap();
561        let r3_sender = p.register_local_endpoint(sender, true, &[]).unwrap();
562
563        // Die gleichen Keys auf Receiver-Seite "registrieren" — in der
564        // realen Welt kommen sie via SharedSecret-Token-Austausch rein,
565        // hier im Unit-Test umgehen wir den Handshake indem wir die
566        // Tokens direkt zurueckspielen.
567        let t1 = p
568            .create_local_participant_crypto_tokens(r1_sender, CryptoHandle(0))
569            .unwrap();
570        let t2 = p
571            .create_local_participant_crypto_tokens(r2_sender, CryptoHandle(0))
572            .unwrap();
573        let t3 = p
574            .create_local_participant_crypto_tokens(r3_sender, CryptoHandle(0))
575            .unwrap();
576
577        let r1_recv = p
578            .register_matched_remote_participant(sender, IdentityHandle(2), SharedSecretHandle(1))
579            .unwrap();
580        let r2_recv = p
581            .register_matched_remote_participant(sender, IdentityHandle(3), SharedSecretHandle(2))
582            .unwrap();
583        let r3_recv = p
584            .register_matched_remote_participant(sender, IdentityHandle(4), SharedSecretHandle(3))
585            .unwrap();
586        p.set_remote_participant_crypto_tokens(sender, r1_recv, &t1)
587            .unwrap();
588        p.set_remote_participant_crypto_tokens(sender, r2_recv, &t2)
589            .unwrap();
590        p.set_remote_participant_crypto_tokens(sender, r3_recv, &t3)
591            .unwrap();
592
593        (
594            p,
595            sender,
596            [r1_sender, r2_sender, r3_sender],
597            [r1_recv, r2_recv, r3_recv],
598        )
599    }
600
601    fn bindings_with_ids(handles: &[CryptoHandle]) -> Vec<(CryptoHandle, u32)> {
602        // Im Unit-Test leiten wir key_id fuer den Receiver deterministisch
603        // aus dem Index ab (1001, 1002, 1003, ...). Realistisch kommt die
604        // ID aus dem Handshake.
605        handles
606            .iter()
607            .enumerate()
608            .map(|(i, h)| (*h, 1000u32 + (i as u32) + 1))
609            .collect()
610    }
611
612    #[test]
613    fn multi_mac_encode_produces_one_ciphertext_and_three_macs() {
614        let (p, sender, r_sender, _r_recv) = make_plugin_with_three_receivers();
615        let receivers = bindings_with_ids(&r_sender);
616        let plain = b"hetero-broadcast-with-3-macs";
617        let wire = encode_secured_submessage_multi(&p, sender, &receivers, plain).unwrap();
618
619        // SEC_POSTFIX-ID muss im Wire stehen.
620        let ptr = wire.windows(1).position(|w| w[0] == SEC_POSTFIX);
621        assert!(ptr.is_some());
622    }
623
624    #[test]
625    fn multi_mac_roundtrip_each_receiver_validates_own_mac() {
626        // DoD §Stufe 7 wortwoertlich: 3 Reader mit gleicher Suite,
627        // unterschiedlichen Tokens. Writer produziert ein Ciphertext +
628        // 3 MACs. Jeder Reader validiert seinen spezifischen MAC.
629        let (p, sender, r_sender, _r_recv) = make_plugin_with_three_receivers();
630        let receivers = bindings_with_ids(&r_sender);
631        let plain = b"multi-mac-dod";
632        let wire = encode_secured_submessage_multi(&p, sender, &receivers, plain).unwrap();
633
634        for (idx, (handle, key_id)) in receivers.iter().enumerate() {
635            let back = decode_secured_submessage_multi(&p, sender, sender, *key_id, *handle, &wire)
636                .unwrap_or_else(|e| panic!("receiver {idx} must decode: {e:?}"));
637            assert_eq!(back, plain);
638        }
639    }
640
641    #[test]
642    fn multi_mac_reader_without_matching_key_id_rejects() {
643        let (mut p, sender, r_sender, _r_recv) = make_plugin_with_three_receivers();
644        let receivers = bindings_with_ids(&r_sender);
645        let plain = b"rogue-attempt";
646        let wire = encode_secured_submessage_multi(&p, sender, &receivers, plain).unwrap();
647
648        // Unbekannter Receiver: 4. Slot mit key_id 9999 — NICHT in der
649        // MAC-Liste.
650        let foreign = p.register_local_endpoint(sender, true, &[]).unwrap();
651        let err =
652            decode_secured_submessage_multi(&p, sender, sender, 9999, foreign, &wire).unwrap_err();
653        match err {
654            SecurityRtpsError::Crypto(e) => assert_eq!(e.kind, SecurityErrorKind::CryptoFailed),
655            other => panic!("expected Crypto-Fail, got {other:?}"),
656        }
657    }
658
659    #[test]
660    fn multi_mac_tampered_ciphertext_fails_even_with_correct_key_id() {
661        let (p, sender, r_sender, _r_recv) = make_plugin_with_three_receivers();
662        let receivers = bindings_with_ids(&r_sender);
663        let plain = b"honest-plaintext";
664        let mut wire = encode_secured_submessage_multi(&p, sender, &receivers, plain).unwrap();
665
666        // Flip ein Byte im Ciphertext (~offset 32).
667        wire[32] ^= 0x20;
668
669        let (own_h, own_id) = receivers[0];
670        let err =
671            decode_secured_submessage_multi(&p, sender, sender, own_id, own_h, &wire).unwrap_err();
672        match err {
673            SecurityRtpsError::Crypto(e) => assert_eq!(e.kind, SecurityErrorKind::CryptoFailed),
674            other => panic!("expected Crypto-Fail, got {other:?}"),
675        }
676    }
677
678    #[test]
679    fn multi_mac_count_cap_enforced() {
680        let (p, sender, _r_sender, _r_recv) = make_plugin_with_three_receivers();
681        // Baue ein Wire mit malformem SEC_POSTFIX: count > MAX_RECEIVER_MACS.
682        // Wir konstruieren das manuell statt via Plugin, um den Cap
683        // im Decoder explizit zu triggern.
684        let ct = b"ciphertext-x"; // irgendwas
685        let mut wire = Vec::new();
686        // SEC_PREFIX
687        wire.push(SEC_PREFIX);
688        wire.push(FLAG_LE);
689        wire.extend_from_slice(&16u16.to_le_bytes());
690        wire.extend_from_slice(&[0u8; 16]);
691        // SEC_BODY
692        wire.push(SEC_BODY);
693        wire.push(FLAG_LE);
694        let body_len = 4 + ct.len() as u16;
695        wire.extend_from_slice(&body_len.to_le_bytes());
696        wire.extend_from_slice(&(ct.len() as u32).to_le_bytes());
697        wire.extend_from_slice(ct);
698        // SEC_POSTFIX mit "count" = MAX+1
699        wire.push(SEC_POSTFIX);
700        wire.push(FLAG_LE);
701        let bad_body_len = 4u16 + ((MAX_RECEIVER_MACS as u16 + 1) * 20);
702        wire.extend_from_slice(&bad_body_len.to_le_bytes());
703        wire.extend_from_slice(&((MAX_RECEIVER_MACS as u32) + 1).to_le_bytes());
704        // (die 20*N Bytes muessen gar nicht da sein — der Count-Check
705        //  laeuft vor dem Read)
706
707        let err =
708            decode_secured_submessage_multi(&p, sender, sender, 0, sender, &wire).unwrap_err();
709        assert!(matches!(err, SecurityRtpsError::Truncated(_)));
710    }
711
712    #[test]
713    fn multi_mac_empty_mac_list_falls_back_to_normal_decrypt() {
714        // Wenn der Sender via klassischem `encode_secured_submessage`
715        // encoded hat (SEC_POSTFIX leer), soll der Multi-Decoder
716        // trotzdem funktionieren — Backward-Compat.
717        let (p, sender, _, _) = make_plugin_with_three_receivers();
718        let plain = b"legacy-encoded-path";
719        let wire = encode_secured_submessage(&p, sender, &[sender], plain).unwrap();
720        let back = decode_secured_submessage_multi(&p, sender, sender, 0, sender, &wire).unwrap();
721        assert_eq!(back, plain);
722    }
723}