core_crypto/mls/conversation/conversation_guard/
commit.rs

1//! The methods in this module all produce or handle commits.
2
3use openmls::prelude::KeyPackageIn;
4
5use crate::mls::conversation::{ConversationWithMls as _, Error};
6use crate::mls::credential::CredentialBundle;
7use crate::prelude::MlsCredentialType;
8use crate::{
9    LeafError, MlsError, MlsTransportResponse, RecursiveError,
10    e2e_identity::init_certificates::NewCrlDistributionPoint,
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.mls_client().await?;
33                let backend = self.mls_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            .central()
49            .await?
50            .mls_transport()
51            .await
52            .map_err(RecursiveError::root("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.mls_client().await?;
57        let backend = self.mls_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<NewCrlDistributionPoint> {
104        let backend = self.mls_provider().await?;
105        let credential = self.credential_bundle().await?;
106        let signer = credential.signature_key();
107        let mut conversation = self.conversation_mut().await;
108
109        // No need to also check pending proposals since they should already have been scanned while decrypting the proposal message
110        let crl_dps = extract_crl_uris_from_credentials(key_packages.iter().filter_map(|kp| {
111            let mls_credential = kp.credential().mls_credential();
112            matches!(mls_credential, openmls::prelude::MlsCredentialType::X509(_)).then_some(mls_credential)
113        }))
114        .map_err(RecursiveError::mls_credential("extracting crl uris from credentials"))?;
115        let crl_new_distribution_points = get_new_crl_distribution_points(&backend, crl_dps)
116            .await
117            .map_err(RecursiveError::mls_credential("getting new crl distribution points"))?;
118
119        let (commit, welcome, group_info) = conversation
120            .group
121            .add_members(&backend, signer, key_packages)
122            .await
123            .map_err(MlsError::wrap("group add members"))?;
124
125        // commit requires an optional welcome
126        let welcome = Some(welcome);
127        let group_info = Self::group_info(group_info)?;
128
129        conversation
130            .persist_group_when_changed(&backend.keystore(), false)
131            .await?;
132
133        // we don't need the conversation anymore, but we do need to mutably borrow `self` again
134        drop(conversation);
135
136        let commit = MlsCommitBundle {
137            commit,
138            welcome,
139            group_info,
140        };
141
142        self.send_and_merge_commit(commit).await?;
143
144        Ok(crl_new_distribution_points)
145    }
146
147    /// Removes clients from the group/conversation.
148    ///
149    /// # Arguments
150    /// * `id` - group/conversation id
151    /// * `clients` - list of client ids to be removed from the group
152    pub async fn remove_members(&mut self, clients: &[ClientId]) -> Result<()> {
153        let backend = self.mls_provider().await?;
154        let credential = self.credential_bundle().await?;
155        let signer = credential.signature_key();
156        let mut conversation = self.inner.write().await;
157
158        let members = conversation
159            .group
160            .members()
161            .filter_map(|kp| {
162                clients
163                    .iter()
164                    .any(move |client_id| client_id.as_slice() == kp.credential.identity())
165                    .then_some(kp.index)
166            })
167            .collect::<Vec<_>>();
168
169        let (commit, welcome, group_info) = conversation
170            .group
171            .remove_members(&backend, signer, &members)
172            .await
173            .map_err(MlsError::wrap("group remove members"))?;
174
175        let group_info = Self::group_info(group_info)?;
176
177        conversation
178            .persist_group_when_changed(&backend.keystore(), false)
179            .await?;
180
181        // we don't need the conversation anymore, but we do need to mutably borrow `self` again
182        drop(conversation);
183
184        self.send_and_merge_commit(MlsCommitBundle {
185            commit,
186            welcome,
187            group_info,
188        })
189        .await
190    }
191
192    /// Self updates the KeyPackage and automatically commits. Pending proposals will be commited.
193    ///
194    /// # Arguments
195    /// * `conversation_id` - the group/conversation id
196    ///
197    /// see [MlsCentral::update_keying_material]
198    pub async fn update_key_material(&mut self) -> Result<()> {
199        let client = self.mls_client().await?;
200        let backend = self.mls_provider().await?;
201        let mut conversation = self.inner.write().await;
202        let commit = conversation
203            .update_keying_material(&client, &backend, None, None)
204            .await?;
205        drop(conversation);
206        self.send_and_merge_commit(commit).await
207    }
208
209    /// Send a commit in a conversation for changing the credential. Requires first
210    /// having enrolled a new X509 certificate with either
211    /// [crate::context::CentralContext::e2ei_new_activation_enrollment] or
212    /// [crate::context::CentralContext::e2ei_new_rotate_enrollment] and having saved it with
213    /// [crate::context::CentralContext::save_x509_credential].
214    pub async fn e2ei_rotate(&mut self, cb: Option<&CredentialBundle>) -> Result<()> {
215        let client = &self.mls_client().await?;
216        let backend = &self.mls_provider().await?;
217        let mut conversation = self.inner.write().await;
218
219        let cb = match cb {
220            Some(cb) => cb,
221            None => &client
222                .find_most_recent_credential_bundle(
223                    conversation.ciphersuite().signature_algorithm(),
224                    MlsCredentialType::X509,
225                )
226                .await
227                .map_err(RecursiveError::mls_client("finding most recent x509 credential bundle"))?,
228        };
229
230        let mut leaf_node = conversation
231            .group
232            .own_leaf()
233            .ok_or(LeafError::InternalMlsError)?
234            .clone();
235        leaf_node.set_credential_with_key(cb.to_mls_credential_with_key());
236
237        let commit = conversation
238            .update_keying_material(client, backend, Some(cb), Some(leaf_node))
239            .await?;
240        // we don't need the conversation anymore, but we do need to mutably borrow `self` again
241        drop(conversation);
242
243        self.send_and_merge_commit(commit).await
244    }
245
246    /// Commits all pending proposals of the group
247    pub async fn commit_pending_proposals(&mut self) -> Result<()> {
248        let client = self.mls_client().await?;
249        let backend = self.mls_provider().await?;
250        let mut conversation = self.inner.write().await;
251        let commit = conversation.commit_pending_proposals(&client, &backend).await?;
252        drop(conversation);
253        let Some(commit) = commit else {
254            return Ok(());
255        };
256        self.send_and_merge_commit(commit).await
257    }
258}