openmls/test_utils/test_framework/
client.rs1use 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)]
36pub struct Client<Provider: OpenMlsProvider> {
42 pub identity: Vec<u8>,
44 pub credentials: HashMap<Ciphersuite, CredentialWithKey>,
46 pub provider: Provider,
47 pub groups: RwLock<HashMap<GroupId, MlsGroup>>,
48}
49
50#[allow(clippy::await_holding_lock)]
53impl<Provider: OpenMlsProvider> Client<Provider> {
54 #[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 #[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 #[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 #[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 group_state
174 .clear_pending_commit(self.provider.storage())
175 .await?;
176 }
177 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 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 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 #[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 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 #[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 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 #[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 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 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}