Skip to main content

openmls/test_utils/test_framework/
client.rs

1//! This module provides the `Client` datastructure, which contains the state
2//! associated with a client in the context of MLS, along with functions to have
3//! that client perform certain MLS operations.
4use std::{collections::HashMap, sync::RwLock};
5
6use commit_builder::CommitMessageBundle;
7use openmls_basic_credential::SignatureKeyPair;
8use openmls_traits::{
9    types::{Ciphersuite, HpkeKeyPair, SignatureScheme},
10    OpenMlsProvider as _,
11};
12use tls_codec::{Deserialize, Serialize};
13
14use super::OpenMlsRustCrypto;
15
16use crate::{
17    binary_tree::array_representation::LeafNodeIndex,
18    ciphersuite::hash_ref::KeyPackageRef,
19    credentials::*,
20    extensions::*,
21    framing::*,
22    group::*,
23    key_packages::*,
24    messages::{group_info::GroupInfo, *},
25    storage::OpenMlsProvider,
26    treesync::{
27        node::{leaf_node::Capabilities, Node},
28        LeafNode, LeafNodeParameters, RatchetTree, RatchetTreeIn,
29    },
30    versions::ProtocolVersion,
31};
32
33use super::{errors::ClientError, ActionType};
34
35#[derive(Debug)]
36/// The client contains the necessary state for a client in the context of MLS.
37/// It contains the group states, as well as a reference to a `KeyStore`
38/// containing its `CredentialWithKey`s. The `key_package_bundles` field
39/// contains generated `KeyPackageBundle`s that are waiting to be used for new
40/// groups.
41pub struct Client<Provider: OpenMlsProvider> {
42    /// Name of the client.
43    pub identity: Vec<u8>,
44    /// Ciphersuites supported by the client.
45    pub credentials: HashMap<Ciphersuite, CredentialWithKey>,
46    pub provider: Provider,
47    pub groups: RwLock<HashMap<GroupId, MlsGroup>>,
48}
49
50// The framework runs one client operation at a time, so no other task waits on
51// the group lock while a guard is held across an await.
52#[allow(clippy::await_holding_lock)]
53impl<Provider: OpenMlsProvider> Client<Provider> {
54    /// Generate a fresh key package and return it.
55    /// The first ciphersuite determines the
56    /// credential used to generate the `KeyPackage`.
57    #[openmls_traits::maybe_async]
58    pub async fn get_fresh_key_package(
59        &self,
60        ciphersuite: Ciphersuite,
61    ) -> Result<KeyPackage, ClientError<Provider::StorageError>> {
62        let credential_with_key = self
63            .credentials
64            .get(&ciphersuite)
65            .ok_or(ClientError::CiphersuiteNotSupported)?;
66        let keys = SignatureKeyPair::read(
67            self.provider.storage(),
68            credential_with_key.signature_key.as_slice(),
69            ciphersuite.signature_algorithm(),
70        )
71        .await
72        .unwrap();
73
74        let key_package = KeyPackage::builder()
75            .leaf_node_capabilities(
76                crate::test_utils::minimal_capabilities_for(ciphersuite).build(),
77            )
78            .build(
79                ciphersuite,
80                &self.provider,
81                &keys,
82                credential_with_key.clone(),
83            )
84            .await
85            .unwrap();
86
87        Ok(key_package.key_package)
88    }
89
90    /// Create a group with the given [MlsGroupCreateConfig] and [Ciphersuite], and return the created [GroupId].
91    ///
92    /// Returns an error if the client doesn't support the `ciphersuite`.
93    #[openmls_traits::maybe_async]
94    pub async fn create_group(
95        &self,
96        mls_group_create_config: MlsGroupCreateConfig,
97        ciphersuite: Ciphersuite,
98    ) -> Result<GroupId, ClientError<Provider::StorageError>> {
99        let credential_with_key = self
100            .credentials
101            .get(&ciphersuite)
102            .ok_or(ClientError::CiphersuiteNotSupported);
103        let credential_with_key = credential_with_key?;
104        let signer = SignatureKeyPair::read(
105            self.provider.storage(),
106            credential_with_key.signature_key.as_slice(),
107            ciphersuite.signature_algorithm(),
108        )
109        .await
110        .unwrap();
111
112        let group_state = MlsGroup::new(
113            &self.provider,
114            &signer,
115            &mls_group_create_config,
116            credential_with_key.clone(),
117        )
118        .await?;
119        let group_id = group_state.group_id().clone();
120        self.groups
121            .write()
122            .expect("An unexpected error occurred.")
123            .insert(group_state.group_id().clone(), group_state);
124        Ok(group_id)
125    }
126
127    /// Join a group based on the given `welcome` and `ratchet_tree`. The group
128    /// is created with the given `MlsGroupCreateConfig`. Throws an error if no
129    /// `KeyPackage` exists matching the `Welcome`, if the client doesn't
130    /// support the ciphersuite, or if an error occurs processing the `Welcome`.
131    #[openmls_traits::maybe_async]
132    pub async fn join_group(
133        &self,
134        mls_group_config: MlsGroupJoinConfig,
135        welcome: Welcome,
136        ratchet_tree: Option<RatchetTreeIn>,
137    ) -> Result<(), ClientError<Provider::StorageError>> {
138        let staged_join = StagedWelcome::new_from_welcome(
139            &self.provider,
140            &mls_group_config,
141            welcome,
142            ratchet_tree,
143        )
144        .await?;
145        let new_group = staged_join.into_group(&self.provider).await?;
146        self.groups
147            .write()
148            .expect("An unexpected error occurred.")
149            .insert(new_group.group_id().to_owned(), new_group);
150        Ok(())
151    }
152
153    /// Have the client process the given messages. Returns an error if an error
154    /// occurs during message processing or if no group exists for one of the
155    /// messages.
156    #[openmls_traits::maybe_async]
157    pub async fn receive_messages_for_group<AS: Fn(&Credential) -> bool>(
158        &self,
159        message: &ProtocolMessage,
160        sender_id: &[u8],
161        authentication_service: &AS,
162    ) -> Result<(), ClientError<Provider::StorageError>> {
163        let mut group_states = self.groups.write().expect("An unexpected error occurred.");
164        let group_id = message.group_id();
165        let group_state = group_states
166            .get_mut(group_id)
167            .ok_or(ClientError::NoMatchingGroup)?;
168        if sender_id == self.identity && message.content_type() == ContentType::Commit {
169            group_state.merge_pending_commit(&self.provider).await?
170        } else {
171            if message.content_type() == ContentType::Commit {
172                // Clear any potential pending commits.
173                group_state
174                    .clear_pending_commit(self.provider.storage())
175                    .await?;
176            }
177            // Process the message.
178            let processed_message = group_state
179                .process_message(&self.provider, message.clone())
180                .await
181                .map_err(ClientError::ProcessMessageError)?;
182
183            match processed_message.into_content() {
184                ProcessedMessageContent::ApplicationMessage(_) => {}
185                ProcessedMessageContent::ProposalMessage(staged_proposal) => {
186                    group_state
187                        .store_pending_proposal(self.provider.storage(), *staged_proposal)
188                        .await?;
189                }
190                ProcessedMessageContent::ExternalJoinProposalMessage(staged_proposal) => {
191                    group_state
192                        .store_pending_proposal(self.provider.storage(), *staged_proposal)
193                        .await?;
194                }
195                ProcessedMessageContent::StagedCommitMessage(staged_commit) => {
196                    for credential in staged_commit.credentials_to_verify() {
197                        if !authentication_service(credential) {
198                            println!(
199                                "authentication service callback denied credential {credential:?}"
200                            );
201                            return Err(ClientError::NoMatchingCredential);
202                        }
203                    }
204                    group_state
205                        .merge_staged_commit(&self.provider, *staged_commit)
206                        .await?;
207                }
208                ProcessedMessageContent::OwnPendingCommit => {
209                    group_state.merge_pending_commit(&self.provider).await?;
210                }
211                // Own PrivateMessages echoed by the DS cannot be decrypted, so
212                // skip them.
213                ProcessedMessageContent::OwnPrivateMessage => {}
214                #[cfg(feature = "extensions-draft")]
215                ProcessedMessageContent::UnresolvedAppDataCommit(_) => {
216                    unimplemented!("this test framework does not handle AppDataUpdate proposals")
217                }
218            }
219        }
220
221        Ok(())
222    }
223
224    /// Get the credential and the index of each group member of the group with
225    /// the given id. Returns an error if no group exists with the given group
226    /// id.
227    pub fn get_members_of_group(
228        &self,
229        group_id: &GroupId,
230    ) -> Result<Vec<Member>, ClientError<Provider::StorageError>> {
231        let groups = self.groups.read().expect("An unexpected error occurred.");
232        let group = groups.get(group_id).ok_or(ClientError::NoMatchingGroup)?;
233        let members = group.members().collect();
234        Ok(members)
235    }
236
237    /// Have the client either propose or commit (depending on the
238    /// `action_type`) a self update in the group with the given group id.
239    /// Optionally, a `HpkeKeyPair` can be provided, which the client will
240    /// update their leaf with. Returns an error if no group with the given
241    /// group id can be found or if an error occurs while creating the update.
242    #[allow(clippy::type_complexity)]
243    #[openmls_traits::maybe_async]
244    pub async fn self_update(
245        &self,
246        action_type: ActionType,
247        group_id: &GroupId,
248        leaf_node_parameters: LeafNodeParameters,
249    ) -> Result<
250        (MlsMessageOut, Option<Welcome>, Option<GroupInfo>),
251        ClientError<Provider::StorageError>,
252    > {
253        let mut groups = self.groups.write().expect("An unexpected error occurred.");
254        let group = groups
255            .get_mut(group_id)
256            .ok_or(ClientError::NoMatchingGroup)?;
257        // Get the signature public key to read the signer from the
258        // key store.
259        let signature_pk = group.own_leaf().unwrap().signature_key();
260        let signer = SignatureKeyPair::read(
261            self.provider.storage(),
262            signature_pk.as_slice(),
263            group.ciphersuite().signature_algorithm(),
264        )
265        .await
266        .unwrap();
267        let (msg, welcome_option, group_info) = match action_type {
268            ActionType::Commit => {
269                let bundle = group
270                    .self_update(&self.provider, &signer, LeafNodeParameters::default())
271                    .await?;
272
273                let welcome = bundle.to_welcome_msg();
274                let (msg, _, group_info) = bundle.into_contents();
275
276                (msg, welcome, group_info)
277            }
278            ActionType::Proposal => {
279                let (msg, _) = group
280                    .propose_self_update(&self.provider, &signer, leaf_node_parameters)
281                    .await?;
282
283                (msg, None, None)
284            }
285        };
286        Ok((
287            msg,
288            welcome_option.map(|w| w.into_welcome().expect("Unexpected message type.")),
289            group_info,
290        ))
291    }
292
293    /// Have the client either propose or commit (depending on the
294    /// `action_type`) adding the clients with the given `KeyPackage`s to the
295    /// group with the given group id. Returns an error if no group with the
296    /// given group id can be found or if an error occurs while performing the
297    /// add operation.
298    #[allow(clippy::type_complexity)]
299    #[openmls_traits::maybe_async]
300    pub async fn add_members(
301        &self,
302        action_type: ActionType,
303        group_id: &GroupId,
304        key_packages: &[KeyPackage],
305    ) -> Result<
306        (Vec<MlsMessageOut>, Option<Welcome>, Option<GroupInfo>),
307        ClientError<Provider::StorageError>,
308    > {
309        let mut groups = self.groups.write().expect("An unexpected error occurred.");
310        let group = groups
311            .get_mut(group_id)
312            .ok_or(ClientError::NoMatchingGroup)?;
313        // Get the signature public key to read the signer from the
314        // key store.
315        let signature_pk = group.own_leaf().unwrap().signature_key();
316        let signer = SignatureKeyPair::read(
317            self.provider.storage(),
318            signature_pk.as_slice(),
319            group.ciphersuite().signature_algorithm(),
320        )
321        .await
322        .unwrap();
323        let action_results = match action_type {
324            ActionType::Commit => {
325                let (messages, welcome_message, group_info) = group
326                    .add_members(&self.provider, &signer, key_packages)
327                    .await?;
328                (
329                    vec![messages],
330                    Some(
331                        welcome_message
332                            .into_welcome()
333                            .expect("Unexpected message type."),
334                    ),
335                    group_info,
336                )
337            }
338            ActionType::Proposal => {
339                let mut messages = Vec::new();
340                for key_package in key_packages {
341                    let message = group
342                        .propose_add_member(&self.provider, &signer, key_package)
343                        .await
344                        .map(|(out, _)| out)?;
345                    messages.push(message);
346                }
347                (messages, None, None)
348            }
349        };
350        Ok(action_results)
351    }
352
353    /// Have the client either propose or commit (depending on the
354    /// `action_type`) removing the clients with the given indices from the
355    /// group with the given group id. Returns an error if no group with the
356    /// given group id can be found or if an error occurs while performing the
357    /// remove operation.
358    #[allow(clippy::type_complexity)]
359    #[openmls_traits::maybe_async]
360    pub async fn remove_members(
361        &self,
362        action_type: ActionType,
363        group_id: &GroupId,
364        targets: &[LeafNodeIndex],
365    ) -> Result<
366        (Vec<MlsMessageOut>, Option<Welcome>, Option<GroupInfo>),
367        ClientError<Provider::StorageError>,
368    > {
369        let mut groups = self.groups.write().expect("An unexpected error occurred.");
370        let group = groups
371            .get_mut(group_id)
372            .ok_or(ClientError::NoMatchingGroup)?;
373        // Get the signature public key to read the signer from the
374        // key store.
375        let signature_pk = group.own_leaf().unwrap().signature_key();
376        let signer = SignatureKeyPair::read(
377            self.provider.storage(),
378            signature_pk.as_slice(),
379            group.ciphersuite().signature_algorithm(),
380        )
381        .await
382        .unwrap();
383        let action_results = match action_type {
384            ActionType::Commit => {
385                let (message, welcome_option, group_info) = group
386                    .remove_members(&self.provider, &signer, targets)
387                    .await?;
388                (
389                    vec![message],
390                    welcome_option.map(|w| w.into_welcome().expect("Unexpected message type.")),
391                    group_info,
392                )
393            }
394            ActionType::Proposal => {
395                let mut messages = Vec::new();
396                for target in targets {
397                    let message = group
398                        .propose_remove_member(&self.provider, &signer, *target)
399                        .await
400                        .map(|(out, _)| out)?;
401                    messages.push(message);
402                }
403                (messages, None, None)
404            }
405        };
406        Ok(action_results)
407    }
408
409    /// Get the identity of this client in the given group.
410    pub fn identity(&self, group_id: &GroupId) -> Option<Vec<u8>> {
411        let groups = self.groups.read().unwrap();
412        let group = groups.get(group_id).unwrap();
413        let leaf = group.own_leaf();
414        leaf.map(|l| {
415            let credential = BasicCredential::try_from(l.credential().clone()).unwrap();
416            credential.identity().to_vec()
417        })
418    }
419}