Skip to main content

openmls/framing/
safe_aad.rs

1//! Safe Additional Authenticated Data (Safe AAD) framing.
2//!
3//! Implements the wire format and validation rules from
4//! <https://datatracker.ietf.org/doc/html/draft-ietf-mls-extensions> Section 4.9.
5//!
6//! ```tls
7//! struct {
8//!   ComponentID component_id;
9//!   opaque aad_item_data<V>;
10//! } SafeAADItem;
11//!
12//! struct {
13//!   SafeAADItem aad_items<V>;
14//! } SafeAAD;
15//! ```
16//!
17//! Items in a [`SafeAad`] are sorted in strictly-increasing order of
18//! `component_id`. Duplicates and misordering are rejected on both construction
19//! and deserialization.
20
21use serde::{Deserialize, Serialize};
22use std::io::Read;
23use tls_codec::{
24    Deserialize as TlsDeserializeTrait, DeserializeBytes as TlsDeserializeBytesTrait,
25    Serialize as TlsSerializeTrait, TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize,
26    VLBytes,
27};
28
29use crate::component::ComponentId;
30
31/// Errors that can occur when building or parsing a [`SafeAad`].
32#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)]
33pub enum SafeAadError {
34    /// Two items share the same [`ComponentId`].
35    #[error("duplicate component id in SafeAAD: {0}")]
36    DuplicateComponentId(ComponentId),
37    /// Items are not sorted in strictly-increasing order by [`ComponentId`].
38    #[error("SafeAAD items are not sorted by component id in increasing order")]
39    ItemsNotSortedAscending,
40    /// Encoding or decoding failure.
41    #[error("codec error: {0}")]
42    Codec(String),
43}
44
45/// A single Safe AAD entry tagged by [`ComponentId`].
46///
47/// ```tls
48/// struct {
49///   ComponentID component_id;
50///   opaque aad_item_data<V>;
51/// } SafeAADItem;
52/// ```
53#[derive(
54    Clone,
55    Debug,
56    PartialEq,
57    Eq,
58    Serialize,
59    Deserialize,
60    TlsSerialize,
61    TlsDeserialize,
62    TlsDeserializeBytes,
63    TlsSize,
64)]
65pub struct SafeAadItem {
66    component_id: ComponentId,
67    aad_item_data: VLBytes,
68}
69
70impl SafeAadItem {
71    /// Create a new [`SafeAadItem`].
72    pub fn new(component_id: ComponentId, data: Vec<u8>) -> Self {
73        Self {
74            component_id,
75            aad_item_data: data.into(),
76        }
77    }
78
79    /// The [`ComponentId`] this item is tagged with.
80    pub fn component_id(&self) -> ComponentId {
81        self.component_id
82    }
83
84    /// The bytes carried by this item.
85    pub fn data(&self) -> &[u8] {
86        self.aad_item_data.as_slice()
87    }
88}
89
90/// A Safe AAD struct as it appears at the beginning of an MLS message's
91/// `authenticated_data` field when negotiated for the group.
92#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TlsSerialize, TlsSize)]
93pub struct SafeAad {
94    aad_items: Vec<SafeAadItem>,
95}
96
97impl SafeAad {
98    /// Build a [`SafeAad`] from a list of items.
99    ///
100    /// Returns an error if items are not sorted in strictly-increasing
101    /// [`ComponentId`] order or if any [`ComponentId`] appears more than once.
102    pub fn from_items(items: Vec<SafeAadItem>) -> Result<Self, SafeAadError> {
103        Self::validate(&items)?;
104        Ok(Self { aad_items: items })
105    }
106
107    /// Build an empty [`SafeAad`].
108    pub fn empty() -> Self {
109        Self {
110            aad_items: Vec::new(),
111        }
112    }
113
114    /// Returns all items.
115    pub fn items(&self) -> &[SafeAadItem] {
116        &self.aad_items
117    }
118
119    /// Look up the data carried for a given [`ComponentId`].
120    ///
121    /// Returns `None` if there is no item tagged with that id.
122    pub fn get(&self, component_id: ComponentId) -> Option<&[u8]> {
123        // The list is sorted by construction, so a binary search is correct
124        // and cheap.
125        self.aad_items
126            .binary_search_by_key(&component_id, SafeAadItem::component_id)
127            .ok()
128            .map(|index| self.aad_items[index].data())
129    }
130
131    /// Insert `item`, replacing any item already tagged with its
132    /// [`ComponentId`]. The sorted-unique invariant is maintained.
133    #[cfg(feature = "virtual-clients-draft")]
134    pub(crate) fn upsert(&mut self, item: SafeAadItem) {
135        match self
136            .aad_items
137            .binary_search_by_key(&item.component_id(), SafeAadItem::component_id)
138        {
139            Ok(index) => self.aad_items[index] = item,
140            Err(index) => self.aad_items.insert(index, item),
141        }
142    }
143
144    /// Returns true if there are no items.
145    pub fn is_empty(&self) -> bool {
146        self.aad_items.is_empty()
147    }
148
149    /// Number of items.
150    pub fn len(&self) -> usize {
151        self.aad_items.len()
152    }
153
154    fn validate(items: &[SafeAadItem]) -> Result<(), SafeAadError> {
155        let mut previous: Option<ComponentId> = None;
156        for item in items {
157            if let Some(prev) = previous {
158                if item.component_id == prev {
159                    return Err(SafeAadError::DuplicateComponentId(item.component_id));
160                }
161                if item.component_id < prev {
162                    return Err(SafeAadError::ItemsNotSortedAscending);
163                }
164            }
165            previous = Some(item.component_id);
166        }
167        Ok(())
168    }
169}
170
171impl TlsDeserializeTrait for SafeAad {
172    fn tls_deserialize<R: Read>(bytes: &mut R) -> Result<Self, tls_codec::Error> {
173        let aad_items = Vec::<SafeAadItem>::tls_deserialize(bytes)?;
174        SafeAad::from_items(aad_items)
175            .map_err(|err| tls_codec::Error::DecodingError(err.to_string()))
176    }
177}
178
179impl TlsDeserializeBytesTrait for SafeAad {
180    fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), tls_codec::Error> {
181        let (aad_items, rest) = Vec::<SafeAadItem>::tls_deserialize_bytes(bytes)?;
182        let aad = SafeAad::from_items(aad_items)
183            .map_err(|err| tls_codec::Error::DecodingError(err.to_string()))?;
184        Ok((aad, rest))
185    }
186}
187
188/// Build the bytes that go into `authenticated_data` for an outgoing message
189/// when Safe AAD is required: the TLS-serialized [`SafeAad`] followed by the
190/// caller-supplied tail bytes.
191pub(crate) fn assemble_authenticated_data(
192    safe_aad: &SafeAad,
193    tail: &[u8],
194) -> Result<Vec<u8>, SafeAadError> {
195    let mut out = safe_aad
196        .tls_serialize_detached()
197        .map_err(|err| SafeAadError::Codec(err.to_string()))?;
198    out.extend_from_slice(tail);
199    Ok(out)
200}
201
202/// Parse the [`SafeAad`] prefix from `authenticated_data` bytes when Safe AAD
203/// is required for the group. Returns the parsed struct and the length of the
204/// consumed prefix.
205pub(crate) fn parse_authenticated_data_prefix(
206    bytes: &[u8],
207) -> Result<(SafeAad, usize), SafeAadError> {
208    let (parsed, remainder) = SafeAad::tls_deserialize_bytes(bytes)
209        .map_err(|err| SafeAadError::Codec(err.to_string()))?;
210    let prefix_len = bytes.len() - remainder.len();
211    Ok((parsed, prefix_len))
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use tls_codec::{Deserialize, Serialize};
218
219    fn item(id: ComponentId, data: &[u8]) -> SafeAadItem {
220        SafeAadItem::new(id, data.to_vec())
221    }
222
223    #[test]
224    fn roundtrip_non_empty() {
225        let safe_aad = SafeAad::from_items(vec![
226            item(1, b"first"),
227            item(7, b""),
228            item(42, b"last item bytes"),
229        ])
230        .unwrap();
231
232        let bytes = safe_aad.tls_serialize_detached().unwrap();
233        let parsed = SafeAad::tls_deserialize_exact(&bytes).unwrap();
234
235        assert_eq!(parsed, safe_aad);
236        let reserialized = parsed.tls_serialize_detached().unwrap();
237        assert_eq!(reserialized, bytes);
238    }
239
240    #[test]
241    fn empty_is_length_prefix_only() {
242        let safe_aad = SafeAad::empty();
243        let bytes = safe_aad.tls_serialize_detached().unwrap();
244
245        // The TLS encoding of a zero-length `<V>` vector is a single zero byte.
246        assert_eq!(bytes, vec![0x00]);
247
248        let parsed = SafeAad::tls_deserialize_exact(&bytes).unwrap();
249        assert!(parsed.is_empty());
250    }
251
252    #[test]
253    fn from_items_rejects_duplicates() {
254        let err = SafeAad::from_items(vec![item(3, b"a"), item(3, b"b")]).unwrap_err();
255        assert_eq!(err, SafeAadError::DuplicateComponentId(3));
256    }
257
258    #[test]
259    fn from_items_rejects_misordered() {
260        let err = SafeAad::from_items(vec![item(9, b""), item(2, b"")]).unwrap_err();
261        assert_eq!(err, SafeAadError::ItemsNotSortedAscending);
262    }
263
264    #[test]
265    fn deserialize_rejects_misordered() {
266        // Hand-craft TLS bytes for two items that are out of order. The derived
267        // serializer would normally refuse to emit these, so we build the bytes
268        // directly from items wrapped in a plain `Vec`.
269        let raw_items: Vec<SafeAadItem> = vec![item(5, b"x"), item(1, b"y")];
270        let raw_bytes = raw_items.tls_serialize_detached().unwrap();
271
272        let err = SafeAad::tls_deserialize_exact(&raw_bytes).unwrap_err();
273        match err {
274            tls_codec::Error::DecodingError(message) => {
275                assert!(
276                    message.contains("not sorted"),
277                    "unexpected error message: {message}"
278                );
279            }
280            other => panic!("unexpected error variant: {other:?}"),
281        }
282    }
283
284    #[test]
285    fn deserialize_rejects_duplicates() {
286        let raw_items: Vec<SafeAadItem> = vec![item(4, b""), item(4, b"")];
287        let raw_bytes = raw_items.tls_serialize_detached().unwrap();
288
289        let err = SafeAad::tls_deserialize_exact(&raw_bytes).unwrap_err();
290        match err {
291            tls_codec::Error::DecodingError(message) => {
292                assert!(
293                    message.contains("duplicate"),
294                    "unexpected error message: {message}"
295                );
296            }
297            other => panic!("unexpected error variant: {other:?}"),
298        }
299    }
300
301    #[test]
302    fn boundary_component_ids() {
303        let safe_aad = SafeAad::from_items(vec![item(0, b"min"), item(u16::MAX, b"max")]).unwrap();
304
305        let bytes = safe_aad.tls_serialize_detached().unwrap();
306        let parsed = SafeAad::tls_deserialize_exact(&bytes).unwrap();
307
308        assert_eq!(parsed.get(0), Some(b"min".as_slice()));
309        assert_eq!(parsed.get(u16::MAX), Some(b"max".as_slice()));
310    }
311
312    #[test]
313    fn get_returns_none_for_missing() {
314        let safe_aad = SafeAad::from_items(vec![item(1, b"a"), item(10, b"b")]).unwrap();
315        assert_eq!(safe_aad.get(5), None);
316        assert_eq!(safe_aad.get(1), Some(b"a".as_slice()));
317        assert_eq!(safe_aad.get(10), Some(b"b".as_slice()));
318    }
319
320    /// `upsert` has to keep the list sorted and free of duplicates, since
321    /// [`SafeAad::get`] binary-searches it and serialization would otherwise
322    /// emit bytes the parser rejects.
323    #[cfg(feature = "virtual-clients-draft")]
324    #[test]
325    fn upsert_inserts_and_replaces_in_order() {
326        let mut safe_aad = SafeAad::from_items(vec![item(1, b"a"), item(10, b"c")]).unwrap();
327
328        safe_aad.upsert(item(5, b"b"));
329        safe_aad.upsert(item(20, b"d"));
330        safe_aad.upsert(item(0, b"first"));
331        // An existing component id is replaced rather than duplicated.
332        safe_aad.upsert(item(10, b"replaced"));
333
334        let ids: Vec<ComponentId> = safe_aad
335            .items()
336            .iter()
337            .map(SafeAadItem::component_id)
338            .collect();
339        assert_eq!(ids, vec![0, 1, 5, 10, 20]);
340        assert_eq!(safe_aad.get(10), Some(b"replaced".as_slice()));
341
342        let bytes = safe_aad.tls_serialize_detached().unwrap();
343        assert_eq!(SafeAad::tls_deserialize_exact(&bytes).unwrap(), safe_aad);
344    }
345
346    #[test]
347    fn assemble_and_parse_authenticated_data_roundtrip() {
348        let safe_aad =
349            SafeAad::from_items(vec![item(2, b"safe-aad-data"), item(8, b"more")]).unwrap();
350        let tail = b"caller tail bytes";
351
352        let combined = assemble_authenticated_data(&safe_aad, tail).unwrap();
353
354        let (parsed, prefix_len) = parse_authenticated_data_prefix(&combined).unwrap();
355        assert_eq!(parsed, safe_aad);
356        assert_eq!(&combined[prefix_len..], tail);
357    }
358}