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
50impl<Provider: OpenMlsProvider> Client<Provider> {
51    /// Generate a fresh key package and return it.
52    /// The first ciphersuite determines the
53    /// credential used to generate the `KeyPackage`.
54    pub fn get_fresh_key_package(
55        &self,
56        ciphersuite: Ciphersuite,
57    ) -> Result<KeyPackage, ClientError<Provider::StorageError>> {
58        let credential_with_key = self
59            .credentials
60            .get(&ciphersuite)
61            .ok_or(ClientError::CiphersuiteNotSupported)?;
62        let keys = SignatureKeyPair::read(
63            self.provider.storage(),
64            credential_with_key.signature_key.as_slice(),
65            ciphersuite.signature_algorithm(),
66        )
67        .unwrap();
68
69        let key_package = KeyPackage::builder()
70            .build(
71                ciphersuite,
72                &self.provider,
73                &keys,
74                credential_with_key.clone(),
75            )
76            .unwrap();
77
78        Ok(key_package.key_package)
79    }
80
81    /// Create a group with the given [MlsGroupCreateConfig] and [Ciphersuite], and return the created [GroupId].
82    ///
83    /// Returns an error if the client doesn't support the `ciphersuite`.
84    pub fn create_group(
85        &self,
86        mls_group_create_config: MlsGroupCreateConfig,
87        ciphersuite: Ciphersuite,
88    ) -> Result<GroupId, ClientError<Provider::StorageError>> {
89        let credential_with_key = self
90            .credentials
91            .get(&ciphersuite)
92            .ok_or(ClientError::CiphersuiteNotSupported);
93        let credential_with_key = credential_with_key?;
94        let signer = SignatureKeyPair::read(
95            self.provider.storage(),
96            credential_with_key.signature_key.as_slice(),
97            ciphersuite.signature_algorithm(),
98        )
99        .unwrap();
100
101        let group_state = MlsGroup::new(
102            &self.provider,
103            &signer,
104            &mls_group_create_config,
105            credential_with_key.clone(),
106        )?;
107        let group_id = group_state.group_id().clone();
108        self.groups
109            .write()
110            .expect("An unexpected error occurred.")
111            .insert(group_state.group_id().clone(), group_state);
112        Ok(group_id)
113    }
114
115    /// Join a group based on the given `welcome` and `ratchet_tree`. The group
116    /// is created with the given `MlsGroupCreateConfig`. Throws an error if no
117    /// `KeyPackage` exists matching the `Welcome`, if the client doesn't
118    /// support the ciphersuite, or if an error occurs processing the `Welcome`.
119    pub fn join_group(
120        &self,
121        mls_group_config: MlsGroupJoinConfig,
122        welcome: Welcome,
123        ratchet_tree: Option<RatchetTreeIn>,
124    ) -> Result<(), ClientError<Provider::StorageError>> {
125        let staged_join = StagedWelcome::new_from_welcome(
126            &self.provider,
127            &mls_group_config,
128            welcome,
129            ratchet_tree,
130        )?;
131        let new_group = staged_join.into_group(&self.provider)?;
132        self.groups
133            .write()
134            .expect("An unexpected error occurred.")
135            .insert(new_group.group_id().to_owned(), new_group);
136        Ok(())
137    }
138
139    /// Have the client process the given messages. Returns an error if an error
140    /// occurs during message processing or if no group exists for one of the
141    /// messages.
142    pub fn receive_messages_for_group<AS: Fn(&Credential) -> bool>(
143        &self,
144        message: &ProtocolMessage,
145        sender_id: &[u8],
146        authentication_service: &AS,
147    ) -> Result<(), ClientError<Provider::StorageError>> {
148        let mut group_states = self.groups.write().expect("An unexpected error occurred.");
149        let group_id = message.group_id();
150        let group_state = group_states
151            .get_mut(group_id)
152            .ok_or(ClientError::NoMatchingGroup)?;
153        if sender_id == self.identity && message.content_type() == ContentType::Commit {
154            group_state.merge_pending_commit(&self.provider)?
155        } else {
156            if message.content_type() == ContentType::Commit {
157                // Clear any potential pending commits.
158                group_state.clear_pending_commit(self.provider.storage())?;
159            }
160            // Process the message.
161            let processed_message = group_state
162                .process_message(&self.provider, message.clone())
163                .map_err(ClientError::ProcessMessageError)?;
164
165            match processed_message.into_content() {
166                ProcessedMessageContent::ApplicationMessage(_) => {}
167                ProcessedMessageContent::ProposalMessage(staged_proposal) => {
168                    group_state
169                        .store_pending_proposal(self.provider.storage(), *staged_proposal)?;
170                }
171                ProcessedMessageContent::ExternalJoinProposalMessage(staged_proposal) => {
172                    group_state
173                        .store_pending_proposal(self.provider.storage(), *staged_proposal)?;
174                }
175                ProcessedMessageContent::StagedCommitMessage(staged_commit) => {
176                    for credential in staged_commit.credentials_to_verify() {
177                        if !authentication_service(credential) {
178                            println!(
179                                "authentication service callback denied credential {credential:?}"
180                            );
181                            return Err(ClientError::NoMatchingCredential);
182                        }
183                    }
184                    group_state.merge_staged_commit(&self.provider, *staged_commit)?;
185                }
186                ProcessedMessageContent::OwnPendingCommit => {
187                    group_state.merge_pending_commit(&self.provider)?;
188                }
189                // Own PrivateMessages echoed by the DS cannot be decrypted, so
190                // skip them.
191                ProcessedMessageContent::OwnPrivateMessage => {}
192                #[cfg(feature = "extensions-draft")]
193                ProcessedMessageContent::UnresolvedAppDataCommit(_) => {
194                    unimplemented!("this test framework does not handle AppDataUpdate proposals")
195                }
196            }
197        }
198
199        Ok(())
200    }
201
202    /// Get the credential and the index of each group member of the group with
203    /// the given id. Returns an error if no group exists with the given group
204    /// id.
205    pub fn get_members_of_group(
206        &self,
207        group_id: &GroupId,
208    ) -> Result<Vec<Member>, ClientError<Provider::StorageError>> {
209        let groups = self.groups.read().expect("An unexpected error occurred.");
210        let group = groups.get(group_id).ok_or(ClientError::NoMatchingGroup)?;
211        let members = group.members().collect();
212        Ok(members)
213    }
214
215    /// Have the client either propose or commit (depending on the
216    /// `action_type`) a self update in the group with the given group id.
217    /// Optionally, a `HpkeKeyPair` can be provided, which the client will
218    /// update their leaf with. Returns an error if no group with the given
219    /// group id can be found or if an error occurs while creating the update.
220    #[allow(clippy::type_complexity)]
221    pub fn self_update(
222        &self,
223        action_type: ActionType,
224        group_id: &GroupId,
225        leaf_node_parameters: LeafNodeParameters,
226    ) -> Result<
227        (MlsMessageOut, Option<Welcome>, Option<GroupInfo>),
228        ClientError<Provider::StorageError>,
229    > {
230        let mut groups = self.groups.write().expect("An unexpected error occurred.");
231        let group = groups
232            .get_mut(group_id)
233            .ok_or(ClientError::NoMatchingGroup)?;
234        // Get the signature public key to read the signer from the
235        // key store.
236        let signature_pk = group.own_leaf().unwrap().signature_key();
237        let signer = SignatureKeyPair::read(
238            self.provider.storage(),
239            signature_pk.as_slice(),
240            group.ciphersuite().signature_algorithm(),
241        )
242        .unwrap();
243        let (msg, welcome_option, group_info) = match action_type {
244            ActionType::Commit => {
245                let bundle =
246                    group.self_update(&self.provider, &signer, LeafNodeParameters::default())?;
247
248                let welcome = bundle.to_welcome_msg();
249                let (msg, _, group_info) = bundle.into_contents();
250
251                (msg, welcome, group_info)
252            }
253            ActionType::Proposal => {
254                let (msg, _) =
255                    group.propose_self_update(&self.provider, &signer, leaf_node_parameters)?;
256
257                (msg, None, None)
258            }
259        };
260        Ok((
261            msg,
262            welcome_option.map(|w| w.into_welcome().expect("Unexpected message type.")),
263            group_info,
264        ))
265    }
266
267    /// Have the client either propose or commit (depending on the
268    /// `action_type`) adding the clients with the given `KeyPackage`s to the
269    /// group with the given group id. Returns an error if no group with the
270    /// given group id can be found or if an error occurs while performing the
271    /// add operation.
272    #[allow(clippy::type_complexity)]
273    pub fn add_members(
274        &self,
275        action_type: ActionType,
276        group_id: &GroupId,
277        key_packages: &[KeyPackage],
278    ) -> Result<
279        (Vec<MlsMessageOut>, Option<Welcome>, Option<GroupInfo>),
280        ClientError<Provider::StorageError>,
281    > {
282        let mut groups = self.groups.write().expect("An unexpected error occurred.");
283        let group = groups
284            .get_mut(group_id)
285            .ok_or(ClientError::NoMatchingGroup)?;
286        // Get the signature public key to read the signer from the
287        // key store.
288        let signature_pk = group.own_leaf().unwrap().signature_key();
289        let signer = SignatureKeyPair::read(
290            self.provider.storage(),
291            signature_pk.as_slice(),
292            group.ciphersuite().signature_algorithm(),
293        )
294        .unwrap();
295        let action_results = match action_type {
296            ActionType::Commit => {
297                let (messages, welcome_message, group_info) =
298                    group.add_members(&self.provider, &signer, key_packages)?;
299                (
300                    vec![messages],
301                    Some(
302                        welcome_message
303                            .into_welcome()
304                            .expect("Unexpected message type."),
305                    ),
306                    group_info,
307                )
308            }
309            ActionType::Proposal => {
310                let mut messages = Vec::new();
311                for key_package in key_packages {
312                    let message = group
313                        .propose_add_member(&self.provider, &signer, key_package)
314                        .map(|(out, _)| out)?;
315                    messages.push(message);
316                }
317                (messages, None, None)
318            }
319        };
320        Ok(action_results)
321    }
322
323    /// Have the client either propose or commit (depending on the
324    /// `action_type`) removing the clients with the given indices from the
325    /// group with the given group id. Returns an error if no group with the
326    /// given group id can be found or if an error occurs while performing the
327    /// remove operation.
328    #[allow(clippy::type_complexity)]
329    pub fn remove_members(
330        &self,
331        action_type: ActionType,
332        group_id: &GroupId,
333        targets: &[LeafNodeIndex],
334    ) -> Result<
335        (Vec<MlsMessageOut>, Option<Welcome>, Option<GroupInfo>),
336        ClientError<Provider::StorageError>,
337    > {
338        let mut groups = self.groups.write().expect("An unexpected error occurred.");
339        let group = groups
340            .get_mut(group_id)
341            .ok_or(ClientError::NoMatchingGroup)?;
342        // Get the signature public key to read the signer from the
343        // key store.
344        let signature_pk = group.own_leaf().unwrap().signature_key();
345        let signer = SignatureKeyPair::read(
346            self.provider.storage(),
347            signature_pk.as_slice(),
348            group.ciphersuite().signature_algorithm(),
349        )
350        .unwrap();
351        let action_results = match action_type {
352            ActionType::Commit => {
353                let (message, welcome_option, group_info) =
354                    group.remove_members(&self.provider, &signer, targets)?;
355                (
356                    vec![message],
357                    welcome_option.map(|w| w.into_welcome().expect("Unexpected message type.")),
358                    group_info,
359                )
360            }
361            ActionType::Proposal => {
362                let mut messages = Vec::new();
363                for target in targets {
364                    let message = group
365                        .propose_remove_member(&self.provider, &signer, *target)
366                        .map(|(out, _)| out)?;
367                    messages.push(message);
368                }
369                (messages, None, None)
370            }
371        };
372        Ok(action_results)
373    }
374
375    /// Get the identity of this client in the given group.
376    pub fn identity(&self, group_id: &GroupId) -> Option<Vec<u8>> {
377        let groups = self.groups.read().unwrap();
378        let group = groups.get(group_id).unwrap();
379        let leaf = group.own_leaf();
380        leaf.map(|l| {
381            let credential = BasicCredential::try_from(l.credential().clone()).unwrap();
382            credential.identity().to_vec()
383        })
384    }
385}