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    pub async fn update_key_material(&mut self) -> Result<()> {
196        let commit = self.update_key_material_inner(None, None).await?;
197        self.send_and_merge_commit(commit).await
198    }
199
200    /// Send a commit in a conversation for changing the credential. Requires first
201    /// having enrolled a new X509 certificate with either
202    /// [crate::transaction_context::TransactionContext::e2ei_new_activation_enrollment] or
203    /// [crate::transaction_context::TransactionContext::e2ei_new_rotate_enrollment] and having saved it with
204    /// [crate::transaction_context::TransactionContext::save_x509_credential].
205    pub async fn e2ei_rotate(&mut self, cb: Option<&CredentialBundle>) -> Result<()> {
206        let client = &self.session().await?;
207        let conversation = self.conversation().await;
208
209        let cb = match cb {
210            Some(cb) => cb,
211            None => &client
212                .find_most_recent_credential_bundle(
213                    conversation.ciphersuite().signature_algorithm(),
214                    MlsCredentialType::X509,
215                )
216                .await
217                .map_err(RecursiveError::mls_client("finding most recent x509 credential bundle"))?,
218        };
219
220        let mut leaf_node = conversation
221            .group
222            .own_leaf()
223            .ok_or(LeafError::InternalMlsError)?
224            .clone();
225        leaf_node.set_credential_with_key(cb.to_mls_credential_with_key());
226
227        // we don't need the conversation anymore, but we do need to mutably borrow `self` again
228        drop(conversation);
229
230        let commit = self.update_key_material_inner(Some(cb), Some(leaf_node)).await?;
231
232        self.send_and_merge_commit(commit).await
233    }
234
235    pub(crate) async fn update_key_material_inner(
236        &mut self,
237        cb: Option<&CredentialBundle>,
238        leaf_node: Option<LeafNode>,
239    ) -> Result<MlsCommitBundle> {
240        self.ensure_no_pending_commit().await?;
241        let session = &self.session().await?;
242        let backend = &self.crypto_provider().await?;
243        let mut conversation = self.conversation_mut().await;
244        let cb = match cb {
245            None => &conversation.find_most_recent_credential_bundle(session).await?,
246            Some(cb) => cb,
247        };
248        let (commit, welcome, group_info) = conversation
249            .group
250            .explicit_self_update(backend, &cb.signature_key, leaf_node)
251            .await
252            .map_err(MlsError::wrap("group self update"))?;
253
254        // We should always have ratchet tree extension turned on hence GroupInfo should always be present
255        let group_info = group_info.ok_or(LeafError::MissingGroupInfo)?;
256        let group_info = MlsGroupInfoBundle::try_new_full_plaintext(group_info)?;
257
258        conversation
259            .persist_group_when_changed(&backend.keystore(), false)
260            .await?;
261
262        Ok(MlsCommitBundle {
263            welcome,
264            commit,
265            group_info,
266        })
267    }
268
269    /// Commits all pending proposals of the group
270    pub async fn commit_pending_proposals(&mut self) -> Result<()> {
271        self.ensure_no_pending_commit().await?;
272        let client = self.session().await?;
273        let backend = self.crypto_provider().await?;
274        let mut conversation = self.inner.write().await;
275        let commit = conversation.commit_pending_proposals(&client, &backend).await?;
276        drop(conversation);
277        let Some(commit) = commit else {
278            return Ok(());
279        };
280        self.send_and_merge_commit(commit).await
281    }
282}