core_crypto/mls/conversation/conversation_guard/
commit.rs

1//! The methods in this module all produce or handle commits.
2
3use openmls::prelude::{KeyPackageIn, LeafNode};
4
5use crate::mls::conversation::{ConversationWithMls as _, Error};
6use crate::mls::credential::CredentialBundle;
7use crate::prelude::{MlsCredentialType, MlsGroupInfoBundle};
8use crate::{
9    LeafError, MlsError, MlsTransportResponse, RecursiveError,
10    e2e_identity::NewCrlDistributionPoints,
11    mls::{
12        conversation::{ConversationGuard, Result, commit::MlsCommitBundle},
13        credential::crl::{extract_crl_uris_from_credentials, get_new_crl_distribution_points},
14    },
15    prelude::ClientId,
16};
17
18/// What to do with a commit after it has been sent via [crate::MlsTransport].
19#[derive(Clone, Copy, PartialEq, Eq)]
20pub(crate) enum TransportedCommitPolicy {
21    /// Accept and merge the commit.
22    Merge,
23    /// Do nothing, because intended operation was already done in one in intermediate processing.
24    None,
25}
26
27impl ConversationGuard {
28    async fn send_and_merge_commit(&mut self, commit: MlsCommitBundle) -> Result<()> {
29        match self.send_commit(commit).await {
30            Ok(TransportedCommitPolicy::None) => Ok(()),
31            Ok(TransportedCommitPolicy::Merge) => {
32                let client = self.session().await?;
33                let backend = self.crypto_provider().await?;
34                let mut conversation = self.inner.write().await;
35                conversation.commit_accepted(&client, &backend).await
36            }
37            Err(e @ Error::MessageRejected { .. }) => {
38                self.clear_pending_commit().await?;
39                Err(e)
40            }
41            Err(e) => Err(e),
42        }
43    }
44
45    /// Send the commit via [crate::MlsTransport] and handle the response.
46    async fn send_commit(&mut self, mut commit: MlsCommitBundle) -> Result<TransportedCommitPolicy> {
47        let transport = self
48            .context()
49            .await?
50            .mls_transport()
51            .await
52            .map_err(RecursiveError::transaction("getting mls transport"))?;
53        let transport = transport.as_ref().ok_or::<Error>(
54            RecursiveError::root("getting mls transport")(crate::Error::MlsTransportNotProvided).into(),
55        )?;
56        let client = self.session().await?;
57        let backend = self.crypto_provider().await?;
58
59        let inner = self.conversation().await;
60        let epoch_before_sending = inner.group().epoch().as_u64();
61        // Drop the lock to allow mutably borrowing self again.
62        drop(inner);
63
64        loop {
65            match transport
66                .send_commit_bundle(commit.clone())
67                .await
68                .map_err(RecursiveError::root("sending commit bundle"))?
69            {
70                MlsTransportResponse::Success => {
71                    return Ok(TransportedCommitPolicy::Merge);
72                }
73                MlsTransportResponse::Abort { reason } => {
74                    return Err(Error::MessageRejected { reason });
75                }
76                MlsTransportResponse::Retry => {
77                    let mut inner = self.conversation_mut().await;
78                    let epoch_after_sending = inner.group().epoch().as_u64();
79                    if epoch_before_sending == epoch_after_sending {
80                        // No intermediate commits have been processed before returning retry.
81                        // This will be the case, e.g., on network failure.
82                        // We can just send the exact same commit again.
83                        continue;
84                    }
85
86                    // The epoch has changed. I.e., a client originally tried sending a commit for an old epoch,
87                    // which was rejected by the DS.
88                    // Before returning `Retry`, the API consumer has fetched and merged all commits,
89                    // so the group state is up-to-date.
90                    // The original commit has been `renewed` to a pending proposal, unless the
91                    // intended operation was already done in one of the merged commits.
92                    let Some(commit_to_retry) = inner.commit_pending_proposals(&client, &backend).await? else {
93                        // The intended operation was already done in one of the merged commits.
94                        return Ok(TransportedCommitPolicy::None);
95                    };
96                    commit = commit_to_retry;
97                }
98            }
99        }
100    }
101
102    /// Adds new members to the group/conversation
103    pub async fn add_members(&mut self, key_packages: Vec<KeyPackageIn>) -> Result<NewCrlDistributionPoints> {
104        self.ensure_no_pending_commit().await?;
105        let backend = self.crypto_provider().await?;
106        let credential = self.credential_bundle().await?;
107        let signer = credential.signature_key();
108        let mut conversation = self.conversation_mut().await;
109
110        // No need to also check pending proposals since they should already have been scanned while decrypting the proposal message
111        let crl_dps = extract_crl_uris_from_credentials(key_packages.iter().filter_map(|kp| {
112            let mls_credential = kp.credential().mls_credential();
113            matches!(mls_credential, openmls::prelude::MlsCredentialType::X509(_)).then_some(mls_credential)
114        }))
115        .map_err(RecursiveError::mls_credential("extracting crl uris from credentials"))?;
116        let crl_new_distribution_points = get_new_crl_distribution_points(&backend, crl_dps)
117            .await
118            .map_err(RecursiveError::mls_credential("getting new crl distribution points"))?;
119
120        let (commit, welcome, group_info) = conversation
121            .group
122            .add_members(&backend, signer, key_packages)
123            .await
124            .map_err(MlsError::wrap("group add members"))?;
125
126        // commit requires an optional welcome
127        let welcome = Some(welcome);
128        let group_info = Self::group_info(group_info)?;
129
130        conversation
131            .persist_group_when_changed(&backend.keystore(), false)
132            .await?;
133
134        // we don't need the conversation anymore, but we do need to mutably borrow `self` again
135        drop(conversation);
136
137        let commit = MlsCommitBundle {
138            commit,
139            welcome,
140            group_info,
141        };
142
143        self.send_and_merge_commit(commit).await?;
144
145        Ok(crl_new_distribution_points)
146    }
147
148    /// Removes clients from the group/conversation.
149    ///
150    /// # Arguments
151    /// * `id` - group/conversation id
152    /// * `clients` - list of client ids to be removed from the group
153    pub async fn remove_members(&mut self, clients: &[ClientId]) -> Result<()> {
154        self.ensure_no_pending_commit().await?;
155        let backend = self.crypto_provider().await?;
156        let credential = self.credential_bundle().await?;
157        let signer = credential.signature_key();
158        let mut conversation = self.inner.write().await;
159
160        let members = conversation
161            .group
162            .members()
163            .filter_map(|kp| {
164                clients
165                    .iter()
166                    .any(move |client_id| client_id.as_slice() == kp.credential.identity())
167                    .then_some(kp.index)
168            })
169            .collect::<Vec<_>>();
170
171        let (commit, welcome, group_info) = conversation
172            .group
173            .remove_members(&backend, signer, &members)
174            .await
175            .map_err(MlsError::wrap("group remove members"))?;
176
177        let group_info = Self::group_info(group_info)?;
178
179        conversation
180            .persist_group_when_changed(&backend.keystore(), false)
181            .await?;
182
183        // we don't need the conversation anymore, but we do need to mutably borrow `self` again
184        drop(conversation);
185
186        self.send_and_merge_commit(MlsCommitBundle {
187            commit,
188            welcome,
189            group_info,
190        })
191        .await
192    }
193
194    /// Self updates the KeyPackage and automatically commits. Pending proposals will be commited.
195    ///
196    /// # Arguments
197    /// * `conversation_id` - the group/conversation id
198    ///
199    /// see [MlsCentral::update_keying_material]
200    pub async fn update_key_material(&mut self) -> Result<()> {
201        let commit = self.update_key_material_inner(None, None).await?;
202        self.send_and_merge_commit(commit).await
203    }
204
205    /// Send a commit in a conversation for changing the credential. Requires first
206    /// having enrolled a new X509 certificate with either
207    /// [crate::transaction_context::TransactionContext::e2ei_new_activation_enrollment] or
208    /// [crate::transaction_context::TransactionContext::e2ei_new_rotate_enrollment] and having saved it with
209    /// [crate::transaction_context::TransactionContext::save_x509_credential].
210    pub async fn e2ei_rotate(&mut self, cb: Option<&CredentialBundle>) -> Result<()> {
211        let client = &self.session().await?;
212        let conversation = self.conversation().await;
213
214        let cb = match cb {
215            Some(cb) => cb,
216            None => &client
217                .find_most_recent_credential_bundle(
218                    conversation.ciphersuite().signature_algorithm(),
219                    MlsCredentialType::X509,
220                )
221                .await
222                .map_err(RecursiveError::mls_client("finding most recent x509 credential bundle"))?,
223        };
224
225        let mut leaf_node = conversation
226            .group
227            .own_leaf()
228            .ok_or(LeafError::InternalMlsError)?
229            .clone();
230        leaf_node.set_credential_with_key(cb.to_mls_credential_with_key());
231
232        // we don't need the conversation anymore, but we do need to mutably borrow `self` again
233        drop(conversation);
234
235        let commit = self.update_key_material_inner(Some(cb), Some(leaf_node)).await?;
236
237        self.send_and_merge_commit(commit).await
238    }
239
240    pub(crate) async fn update_key_material_inner(
241        &mut self,
242        cb: Option<&CredentialBundle>,
243        leaf_node: Option<LeafNode>,
244    ) -> Result<MlsCommitBundle> {
245        self.ensure_no_pending_commit().await?;
246        let session = &self.session().await?;
247        let backend = &self.crypto_provider().await?;
248        let mut conversation = self.conversation_mut().await;
249        let cb = match cb {
250            None => &conversation.find_most_recent_credential_bundle(session).await?,
251            Some(cb) => cb,
252        };
253        let (commit, welcome, group_info) = conversation
254            .group
255            .explicit_self_update(backend, &cb.signature_key, leaf_node)
256            .await
257            .map_err(MlsError::wrap("group self update"))?;
258
259        // We should always have ratchet tree extension turned on hence GroupInfo should always be present
260        let group_info = group_info.ok_or(LeafError::MissingGroupInfo)?;
261        let group_info = MlsGroupInfoBundle::try_new_full_plaintext(group_info)?;
262
263        conversation
264            .persist_group_when_changed(&backend.keystore(), false)
265            .await?;
266
267        Ok(MlsCommitBundle {
268            welcome,
269            commit,
270            group_info,
271        })
272    }
273
274    /// Commits all pending proposals of the group
275    pub async fn commit_pending_proposals(&mut self) -> Result<()> {
276        self.ensure_no_pending_commit().await?;
277        let client = self.session().await?;
278        let backend = self.crypto_provider().await?;
279        let mut conversation = self.inner.write().await;
280        let commit = conversation.commit_pending_proposals(&client, &backend).await?;
281        drop(conversation);
282        let Some(commit) = commit else {
283            return Ok(());
284        };
285        self.send_and_merge_commit(commit).await
286    }
287}