Skip to main content

openmls/group/
group_context.rs

1//! # Group Context
2
3use openmls_traits::crypto::OpenMlsCrypto;
4use openmls_traits::types::Ciphersuite;
5
6use super::*;
7#[cfg(feature = "extensions-draft-08")]
8use crate::component::{ComponentId, ComponentType, ComponentsList};
9#[cfg(feature = "extensions-draft-08")]
10use crate::extensions::AppDataDictionary;
11use crate::{
12    error::LibraryError,
13    framing::{mls_auth_content::AuthenticatedContent, ConfirmedTranscriptHashInput},
14    versions::ProtocolVersion,
15};
16
17/// 8.1 Group Context
18///
19///```c
20/// struct {
21///     ProtocolVersion version = mls10;
22///     CipherSuite cipher_suite;
23///     opaque group_id<V>;
24///     uint64 epoch;
25///     opaque tree_hash<V>;
26///     opaque confirmed_transcript_hash<V>;
27///     Extension extensions<V>;
28/// } GroupContext;
29///
30/// The [`GroupContext`] is a state object maintained which summarizes the group
31/// state agreed upon by each member of the group.
32///```
33#[derive(
34    Debug,
35    Clone,
36    PartialEq,
37    Eq,
38    Serialize,
39    Deserialize,
40    TlsSerialize,
41    TlsDeserialize,
42    TlsDeserializeBytes,
43    TlsSize,
44)]
45pub struct GroupContext {
46    protocol_version: ProtocolVersion,
47    ciphersuite: Ciphersuite,
48    group_id: GroupId,
49    epoch: GroupEpoch,
50    tree_hash: VLBytes,
51    confirmed_transcript_hash: VLBytes,
52    extensions: Extensions<Self>,
53}
54
55#[cfg(any(feature = "test-utils", test))]
56impl GroupContext {
57    pub(crate) fn set_epoch(&mut self, epoch: GroupEpoch) {
58        self.epoch = epoch;
59    }
60
61    /// Set the ciphersuite
62    #[cfg(test)]
63    pub(crate) fn set_ciphersuite(&mut self, ciphersuite: Ciphersuite) {
64        self.ciphersuite = ciphersuite;
65    }
66}
67
68impl GroupContext {
69    /// Create a new group context
70    pub(crate) fn new(
71        ciphersuite: Ciphersuite,
72        group_id: GroupId,
73        epoch: impl Into<GroupEpoch>,
74        tree_hash: Vec<u8>,
75        confirmed_transcript_hash: Vec<u8>,
76        extensions: Extensions<Self>,
77    ) -> Self {
78        GroupContext {
79            ciphersuite,
80            protocol_version: ProtocolVersion::Mls10,
81            group_id,
82            epoch: epoch.into(),
83            tree_hash: tree_hash.into(),
84            confirmed_transcript_hash: confirmed_transcript_hash.into(),
85            extensions,
86        }
87    }
88
89    #[cfg(feature = "extensions-draft-08")]
90    pub(crate) fn app_data_dict(&self) -> Option<&AppDataDictionary> {
91        self.extensions
92            .app_data_dictionary()
93            .map(|extension| extension.dictionary())
94    }
95
96    /// Whether Safe AAD framing is active for messages in this group.
97    ///
98    /// Returns `true` exactly when the `app_data_dictionary` GroupContext
99    /// extension contains a `safe_aad` component (the value bytes are not
100    /// inspected here, an empty `ComponentsList` is still "present").
101    /// Per the Safe AAD spec section 4.9, when this is `true` the
102    /// `authenticated_data` field always starts with the `SafeAAD` struct.
103    #[cfg(feature = "extensions-draft-08")]
104    pub fn safe_aad_required(&self) -> bool {
105        let safe_aad_id = ComponentId::from(ComponentType::SafeAad);
106        self.app_data_dict()
107            .is_some_and(|dict| dict.contains(&safe_aad_id))
108    }
109
110    /// The list of [`ComponentId`]s required to be understood by every member
111    /// for Safe AAD purposes.
112    ///
113    /// Returns `Ok(None)` when the `safe_aad` component is absent from the
114    /// `app_data_dictionary` GroupContext extension, `Ok(Some(list))` when it
115    /// is present and parses cleanly (the list may be empty), and an error
116    /// when the component value fails to decode as a `ComponentsList`.
117    #[cfg(feature = "extensions-draft-08")]
118    pub fn safe_aad_required_components(
119        &self,
120    ) -> Result<Option<Vec<ComponentId>>, crate::framing::SafeAadError> {
121        let safe_aad_id = ComponentId::from(ComponentType::SafeAad);
122        let Some(dict) = self.app_data_dict() else {
123            return Ok(None);
124        };
125        let Some(raw) = dict.get(&safe_aad_id) else {
126            return Ok(None);
127        };
128        use tls_codec::Deserialize as _;
129        let list = ComponentsList::tls_deserialize_exact(raw)
130            .map_err(|err| crate::framing::SafeAadError::Codec(err.to_string()))?;
131        Ok(Some(list.into_ids()))
132    }
133
134    /// Create the `GroupContext` needed upon creation of a new group.
135    pub(crate) fn create_initial_group_context(
136        ciphersuite: Ciphersuite,
137        group_id: GroupId,
138        tree_hash: Vec<u8>,
139        extensions: Extensions<Self>,
140    ) -> Self {
141        // Note: Confirmed transcript hash is "The zero-length octet string."
142        Self::new(ciphersuite, group_id, 0, tree_hash, vec![], extensions)
143    }
144
145    /// Increment the current [`GroupEpoch`] by one.
146    pub(crate) fn increment_epoch(&mut self) {
147        self.epoch.increment()
148    }
149
150    /// Update the current tree hash to the new value
151    pub(crate) fn update_tree_hash(&mut self, new_tree_hash: Vec<u8>) {
152        self.tree_hash = new_tree_hash.into()
153    }
154
155    /// Update the confirmed transcript hash using the given
156    /// `interim_transcript_hash`, as well as the `commit_content`.
157    pub(crate) fn update_confirmed_transcript_hash(
158        &mut self,
159        crypto: &impl OpenMlsCrypto,
160        interim_transcript_hash: &[u8],
161        authenticated_content: &AuthenticatedContent,
162    ) -> Result<(), LibraryError> {
163        let confirmed_transcript_hash = {
164            let input = ConfirmedTranscriptHashInput::try_from(authenticated_content)
165                .map_err(|_| LibraryError::custom("PublicMessage did not contain a commit"))?;
166
167            input.calculate_confirmed_transcript_hash(
168                crypto,
169                self.ciphersuite,
170                interim_transcript_hash,
171            )?
172        };
173
174        self.confirmed_transcript_hash = confirmed_transcript_hash.into();
175
176        Ok(())
177    }
178
179    /// Return the protocol version.
180    pub fn protocol_version(&self) -> ProtocolVersion {
181        self.protocol_version
182    }
183
184    /// Return the ciphersuite.
185    pub fn ciphersuite(&self) -> Ciphersuite {
186        self.ciphersuite
187    }
188
189    /// Return the group ID.
190    pub fn group_id(&self) -> &GroupId {
191        &self.group_id
192    }
193
194    /// Return the epoch.
195    pub fn epoch(&self) -> GroupEpoch {
196        self.epoch
197    }
198
199    /// Return the tree hash.
200    pub fn tree_hash(&self) -> &[u8] {
201        self.tree_hash.as_slice()
202    }
203
204    /// Return the confirmed transcript hash.
205    pub fn confirmed_transcript_hash(&self) -> &[u8] {
206        self.confirmed_transcript_hash.as_slice()
207    }
208
209    pub(crate) fn set_extensions(&mut self, extensions: Extensions<GroupContext>) {
210        self.extensions = extensions;
211    }
212
213    /// Return the extensions.
214    pub fn extensions(&self) -> &Extensions<GroupContext> {
215        &self.extensions
216    }
217
218    /// Get the required capabilities extension.
219    pub fn required_capabilities(&self) -> Option<&RequiredCapabilitiesExtension> {
220        self.extensions.required_capabilities()
221    }
222}