Skip to main content

zerodds_security_rtps/
header_aad.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! RTPS-Header-AAD fuer SRTPS-Wrapping — DDS-Security 1.2 §7.4.6.6 + §8.1.
5//!
6//! Wenn `rtps_protection_kind != NONE`, MUSS der vollstaendige
7//! RTPS-Header (20 Bytes) zur AAD (Authenticated-Additional-Data)
8//! hinzugefuegt werden. Damit ist der Header gegen Tampering
9//! geschuetzt — ein Angreifer kann nicht den Sender-GuidPrefix
10//! aendern, ohne dass der GCM-Tag invalid wird.
11
12use alloc::vec::Vec;
13
14/// Wire-Size eines RTPS-Headers (Spec §8.3.5.1: 4 magic + 2 vendor +
15/// 2 version + 12 GuidPrefix = 20 Bytes).
16pub const RTPS_HEADER_LEN: usize = 20;
17
18/// Baut den AAD-Slot fuer einen `SRTPS_PREFIX`-gewrapten Datagram-
19/// Schutz. Spec §7.4.6.6:
20///
21/// ```text
22///   AAD = transformation_kind ||
23///         transformation_key_id ||
24///         session_id ||
25///         reserved-4 ||
26///         RTPS-Header[0..20]
27/// ```
28///
29/// `transformation_*` und `session_id` kommen aus dem `CryptoHeader`
30/// der SRTPS_PREFIX-Submessage; den RTPS-Header liefert der Caller
31/// als 20-Byte-Slice.
32///
33/// # Errors
34/// Static-String wenn `rtps_header_bytes.len() < 20`.
35pub fn build_rtps_header_aad(
36    transformation_kind: [u8; 4],
37    transformation_key_id: [u8; 4],
38    session_id: [u8; 4],
39    rtps_header_bytes: &[u8],
40) -> Result<Vec<u8>, &'static str> {
41    if rtps_header_bytes.len() < RTPS_HEADER_LEN {
42        return Err("rtps header < 20 bytes");
43    }
44    let mut out = Vec::with_capacity(16 + RTPS_HEADER_LEN);
45    out.extend_from_slice(&transformation_kind);
46    out.extend_from_slice(&transformation_key_id);
47    out.extend_from_slice(&session_id);
48    out.extend_from_slice(&[0u8; 4]); // reserved
49    out.extend_from_slice(&rtps_header_bytes[..RTPS_HEADER_LEN]);
50    Ok(out)
51}
52
53/// Spec §7.4.7.8/9: AAD fuer SubmessageProtection ist der
54/// `SEC_PREFIX`-Submessage-Header (vor dem CryptoHeader) plus die
55/// Crypto-Header-Bytes selbst.
56#[must_use]
57pub fn build_submessage_aad(
58    transformation_kind: [u8; 4],
59    transformation_key_id: [u8; 4],
60    session_id: [u8; 4],
61    sec_prefix_header_bytes: &[u8],
62) -> Vec<u8> {
63    let mut out = Vec::with_capacity(16 + sec_prefix_header_bytes.len());
64    out.extend_from_slice(&transformation_kind);
65    out.extend_from_slice(&transformation_key_id);
66    out.extend_from_slice(&session_id);
67    out.extend_from_slice(&[0u8; 4]); // reserved
68    out.extend_from_slice(sec_prefix_header_bytes);
69    out
70}
71
72#[cfg(test)]
73#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn rtps_header_aad_round_trip() {
79        let kind = [0, 0, 0, 0x02];
80        let key_id = [1, 2, 3, 4];
81        let sid = [10, 20, 30, 40];
82        let aad = build_rtps_header_aad(kind, key_id, sid, &[0xCAu8; 20]).unwrap();
83        assert_eq!(aad.len(), 16 + 20);
84        assert_eq!(&aad[0..4], &kind);
85        assert_eq!(&aad[4..8], &key_id);
86        assert_eq!(&aad[8..12], &sid);
87        assert_eq!(&aad[12..16], &[0, 0, 0, 0]);
88        assert_eq!(&aad[16..36], &[0xCA; 20]);
89    }
90
91    #[test]
92    fn rtps_header_aad_short_buffer_rejected() {
93        assert!(build_rtps_header_aad([0; 4], [0; 4], [0; 4], &[0; 10]).is_err());
94    }
95
96    #[test]
97    fn submessage_aad_includes_prefix_header() {
98        let aad = build_submessage_aad([0, 0, 0, 0x04], [1; 4], [2; 4], &[0xDE, 0xAD, 0xBE, 0xEF]);
99        assert_eq!(aad.len(), 16 + 4);
100        assert_eq!(&aad[16..20], &[0xDE, 0xAD, 0xBE, 0xEF]);
101    }
102
103    #[test]
104    fn rtps_header_len_matches_spec() {
105        // Spec §8.3.5.1: 4 magic + 2 vendor + 2 version + 12 GuidPrefix.
106        assert_eq!(RTPS_HEADER_LEN, 20);
107    }
108
109    #[test]
110    fn aad_changes_with_kind() {
111        let aad1 = build_rtps_header_aad([0, 0, 0, 2], [0; 4], [0; 4], &[0; 20]).unwrap();
112        let aad2 = build_rtps_header_aad([0, 0, 0, 4], [0; 4], [0; 4], &[0; 20]).unwrap();
113        assert_ne!(aad1, aad2);
114    }
115
116    #[test]
117    fn aad_changes_with_session_id() {
118        let aad1 = build_rtps_header_aad([0; 4], [0; 4], [1; 4], &[0; 20]).unwrap();
119        let aad2 = build_rtps_header_aad([0; 4], [0; 4], [2; 4], &[0; 20]).unwrap();
120        assert_ne!(aad1, aad2);
121    }
122
123    #[test]
124    fn aad_changes_with_rtps_header_content() {
125        let aad1 = build_rtps_header_aad([0; 4], [0; 4], [0; 4], &[0xAA; 20]).unwrap();
126        let aad2 = build_rtps_header_aad([0; 4], [0; 4], [0; 4], &[0xBB; 20]).unwrap();
127        assert_ne!(aad1, aad2);
128    }
129}