1use openmls_traits::{signatures::Signer, storage::StorageProvider as _, types::Ciphersuite};
2
3use super::{
4 errors::{ProposalError, ProposeAddMemberError, ProposeRemoveMemberError, RemoveProposalError},
5 AddProposal, CreateGroupContextExtProposalError, CustomProposal, FramingParameters, MlsGroup,
6 PreSharedKeyProposal, Proposal, QueuedProposal, RemoveProposal, UpdateProposal, WireFormat,
7};
8use crate::{
9 binary_tree::LeafNodeIndex,
10 ciphersuite::hash_ref::ProposalRef,
11 credentials::Credential,
12 error::LibraryError,
13 extensions::Extensions,
14 framing::{mls_auth_content::AuthenticatedContent, MlsMessageOut},
15 group::{errors::CreateAddProposalError, GroupContext, GroupId, ValidationError},
16 key_packages::KeyPackage,
17 messages::{group_info::GroupInfo, proposals::ProposalOrRefType},
18 schedule::PreSharedKeyId,
19 storage::{OpenMlsProvider, StorageProvider},
20 treesync::{LeafNode, LeafNodeParameters},
21 versions::ProtocolVersion,
22};
23
24#[cfg(feature = "extensions-draft")]
25use crate::{
26 component::ComponentId,
27 messages::proposals::{AppDataUpdateOperation, AppDataUpdateProposal},
28};
29
30#[derive(Debug, PartialEq, Clone)]
32pub enum Propose {
33 Add(KeyPackage),
35
36 Update(LeafNodeParameters),
38
39 Remove(u32),
41
42 RemoveCredential(Credential),
44
45 PreSharedKey(PreSharedKeyId),
47
48 ReInit {
50 group_id: GroupId,
51 version: ProtocolVersion,
52 ciphersuite: Ciphersuite,
53 extensions: Extensions<GroupContext>,
54 },
55
56 ExternalInit(Vec<u8>),
58
59 GroupContextExtensions(Extensions<GroupContext>),
61
62 #[cfg(feature = "extensions-draft")]
63 UpdateAppDataComponent {
65 component_id: ComponentId,
67 update: Vec<u8>,
69 },
70 #[cfg(feature = "extensions-draft")]
71 RemoveAppDataComponent {
73 component_id: ComponentId,
75 },
76
77 Custom(CustomProposal),
79}
80
81macro_rules! impl_propose_fun {
82 ($name:ident, $value_ty:ty, $group_fun:ident, $ref_or_value:expr) => {
83 pub fn $name<Provider: OpenMlsProvider>(
88 &mut self,
89 provider: &Provider,
90 signer: &impl Signer,
91 value: $value_ty,
92 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
93 self.is_operational()?;
94
95 let aad = self.outgoing_authenticated_data()?;
96 let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
97 let proposal = self.$group_fun(framing_parameters, value, signer)?;
98
99 let queued_proposal = QueuedProposal::from_authenticated_content(
100 self.ciphersuite(),
101 provider.crypto(),
102 proposal.clone(),
103 $ref_or_value,
104 )?;
105 let proposal_ref = queued_proposal.proposal_reference();
106
107 log::trace!("Storing proposal in queue {:?}", queued_proposal);
108 provider
109 .storage()
110 .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
111 .map_err(ProposalError::StorageError)?;
112 self.proposal_store_mut().add(queued_proposal);
113
114 let mls_message = self.content_to_mls_message(proposal, provider)?;
115
116 self.reset_aad();
117 Ok((mls_message, proposal_ref))
118 }
119 };
120}
121
122impl MlsGroup {
123 impl_propose_fun!(
124 propose_add_member_by_value,
125 KeyPackage,
126 create_add_proposal,
127 ProposalOrRefType::Proposal
128 );
129
130 impl_propose_fun!(
131 propose_remove_member_by_value,
132 LeafNodeIndex,
133 create_remove_proposal,
134 ProposalOrRefType::Proposal
135 );
136
137 impl_propose_fun!(
138 propose_pre_shared_key,
139 PreSharedKeyId,
140 create_presharedkey_proposal,
141 ProposalOrRefType::Reference
142 );
143
144 impl_propose_fun!(
145 propose_pre_shared_key_by_value,
146 PreSharedKeyId,
147 create_presharedkey_proposal,
148 ProposalOrRefType::Proposal
149 );
150
151 #[deprecated(
153 note = "Renamed to `propose_pre_shared_key`; works for any non-resumption PSK, not just external"
154 )]
155 pub fn propose_external_psk<Provider: OpenMlsProvider>(
156 &mut self,
157 provider: &Provider,
158 signer: &impl Signer,
159 value: PreSharedKeyId,
160 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
161 self.propose_pre_shared_key(provider, signer, value)
162 }
163
164 #[deprecated(
166 note = "Renamed to `propose_pre_shared_key_by_value`; works for any non-resumption PSK, not just external"
167 )]
168 pub fn propose_external_psk_by_value<Provider: OpenMlsProvider>(
169 &mut self,
170 provider: &Provider,
171 signer: &impl Signer,
172 value: PreSharedKeyId,
173 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
174 self.propose_pre_shared_key_by_value(provider, signer, value)
175 }
176
177 impl_propose_fun!(
178 propose_custom_proposal_by_value,
179 CustomProposal,
180 create_custom_proposal,
181 ProposalOrRefType::Proposal
182 );
183
184 impl_propose_fun!(
185 propose_custom_proposal_by_reference,
186 CustomProposal,
187 create_custom_proposal,
188 ProposalOrRefType::Reference
189 );
190
191 pub fn propose<Provider: OpenMlsProvider>(
193 &mut self,
194 provider: &Provider,
195 signer: &impl Signer,
196 propose: Propose,
197 ref_or_value: ProposalOrRefType,
198 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
199 match propose {
200 Propose::Add(key_package) => match ref_or_value {
201 ProposalOrRefType::Proposal => {
202 self.propose_add_member_by_value(provider, signer, key_package)
203 }
204 ProposalOrRefType::Reference => self
205 .propose_add_member(provider, signer, &key_package)
206 .map_err(|e| e.into()),
207 },
208
209 Propose::Update(leaf_node_parameters) => match ref_or_value {
210 ProposalOrRefType::Proposal => self
211 .propose_self_update(provider, signer, leaf_node_parameters)
212 .map_err(|e| e.into()),
213 ProposalOrRefType::Reference => self
214 .propose_self_update(provider, signer, leaf_node_parameters)
215 .map_err(|e| e.into()),
216 },
217
218 Propose::Remove(leaf_index) => match ref_or_value {
219 ProposalOrRefType::Proposal => self.propose_remove_member_by_value(
220 provider,
221 signer,
222 LeafNodeIndex::new(leaf_index),
223 ),
224 ProposalOrRefType::Reference => self
225 .propose_remove_member(provider, signer, LeafNodeIndex::new(leaf_index))
226 .map_err(|e| e.into()),
227 },
228
229 Propose::RemoveCredential(credential) => match ref_or_value {
230 ProposalOrRefType::Proposal => {
231 self.propose_remove_member_by_credential_by_value(provider, signer, &credential)
232 }
233 ProposalOrRefType::Reference => self
234 .propose_remove_member_by_credential(provider, signer, &credential)
235 .map_err(|e| e.into()),
236 },
237 Propose::PreSharedKey(psk_id) => {
238 match psk_id.psk() {
239 crate::schedule::Psk::External(_) => {}
240 #[cfg(feature = "extensions-draft")]
241 crate::schedule::Psk::Application(_) => {}
242 crate::schedule::Psk::Resumption(_) => {
243 return Err(ProposalError::LibraryError(LibraryError::custom(
244 "Invalid PSk argument",
245 )))
246 }
247 };
248 match ref_or_value {
249 ProposalOrRefType::Proposal => {
250 self.propose_pre_shared_key_by_value(provider, signer, psk_id)
251 }
252 ProposalOrRefType::Reference => {
253 self.propose_pre_shared_key(provider, signer, psk_id)
254 }
255 }
256 }
257 Propose::ReInit {
258 group_id: _,
259 version: _,
260 ciphersuite: _,
261 extensions: _,
262 } => Err(ProposalError::LibraryError(LibraryError::custom(
263 "Unsupported proposal type ReInit",
264 ))),
265 Propose::ExternalInit(_) => Err(ProposalError::LibraryError(LibraryError::custom(
266 "Unsupported proposal type ExternalInit",
267 ))),
268 Propose::GroupContextExtensions(_) => Err(ProposalError::LibraryError(
269 LibraryError::custom("Unsupported proposal type GroupContextExtensions"),
270 )),
271 #[cfg(feature = "extensions-draft")]
273 Propose::UpdateAppDataComponent {
274 component_id,
275 update,
276 } => self.propose_app_data_update(
277 provider,
278 signer,
279 component_id,
280 AppDataUpdateOperation::Update(update.into()),
281 ),
282 #[cfg(feature = "extensions-draft")]
283 Propose::RemoveAppDataComponent { component_id } => self.propose_app_data_update(
284 provider,
285 signer,
286 component_id,
287 AppDataUpdateOperation::Remove,
288 ),
289
290 Propose::Custom(custom_proposal) => match ref_or_value {
292 ProposalOrRefType::Proposal => {
293 self.propose_custom_proposal_by_value(provider, signer, custom_proposal)
294 }
295 ProposalOrRefType::Reference => {
296 self.propose_custom_proposal_by_reference(provider, signer, custom_proposal)
297 }
298 },
299 }
300 }
301
302 pub fn propose_add_member<Provider: OpenMlsProvider>(
306 &mut self,
307 provider: &Provider,
308 signer: &impl Signer,
309 key_package: &KeyPackage,
310 ) -> Result<(MlsMessageOut, ProposalRef), ProposeAddMemberError<Provider::StorageError>> {
311 self.is_operational()?;
312
313 let aad = self.outgoing_authenticated_data()?;
314 let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
315 let add_proposal = self
316 .create_add_proposal(framing_parameters, key_package.clone(), signer)
317 .map_err(|e| match e {
318 CreateAddProposalError::LibraryError(e) => e.into(),
319 CreateAddProposalError::LeafNodeValidation(error) => {
320 ProposeAddMemberError::LeafNodeValidation(error)
321 }
322 })?;
323
324 let proposal = QueuedProposal::from_authenticated_content_by_ref(
325 self.ciphersuite(),
326 provider.crypto(),
327 add_proposal.clone(),
328 )?;
329 let proposal_ref = proposal.proposal_reference();
330 provider
331 .storage()
332 .queue_proposal(self.group_id(), &proposal_ref, &proposal)
333 .map_err(ProposeAddMemberError::StorageError)?;
334 self.proposal_store_mut().add(proposal);
335
336 let mls_message = self.content_to_mls_message(add_proposal, provider)?;
337
338 self.reset_aad();
339 Ok((mls_message, proposal_ref))
340 }
341
342 pub fn propose_remove_member<Provider: OpenMlsProvider>(
347 &mut self,
348 provider: &Provider,
349 signer: &impl Signer,
350 member: LeafNodeIndex,
351 ) -> Result<(MlsMessageOut, ProposalRef), ProposeRemoveMemberError<Provider::StorageError>>
352 {
353 self.is_operational()?;
354
355 let aad = self.outgoing_authenticated_data()?;
356 let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
357 let remove_proposal = self
358 .create_remove_proposal(framing_parameters, member, signer)
359 .map_err(|_| ProposeRemoveMemberError::UnknownMember)?;
360
361 let proposal = QueuedProposal::from_authenticated_content_by_ref(
362 self.ciphersuite(),
363 provider.crypto(),
364 remove_proposal.clone(),
365 )?;
366 let proposal_ref = proposal.proposal_reference();
367 provider
368 .storage()
369 .queue_proposal(self.group_id(), &proposal_ref, &proposal)
370 .map_err(ProposeRemoveMemberError::StorageError)?;
371 self.proposal_store_mut().add(proposal);
372
373 let mls_message = self.content_to_mls_message(remove_proposal, provider)?;
374
375 self.reset_aad();
376 Ok((mls_message, proposal_ref))
377 }
378
379 pub fn propose_remove_member_by_credential<Provider: OpenMlsProvider>(
384 &mut self,
385 provider: &Provider,
386 signer: &impl Signer,
387 member: &Credential,
388 ) -> Result<(MlsMessageOut, ProposalRef), ProposeRemoveMemberError<Provider::StorageError>>
389 {
390 let member_index = self
392 .public_group()
393 .members()
394 .find(|m| &m.credential == member)
395 .map(|m| m.index);
396
397 if let Some(member_index) = member_index {
398 self.propose_remove_member(provider, signer, member_index)
399 } else {
400 Err(ProposeRemoveMemberError::UnknownMember)
401 }
402 }
403
404 pub fn propose_remove_member_by_credential_by_value<Provider: OpenMlsProvider>(
409 &mut self,
410 provider: &Provider,
411 signer: &impl Signer,
412 member: &Credential,
413 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
414 let member_index = self
416 .public_group()
417 .members()
418 .find(|m| &m.credential == member)
419 .map(|m| m.index);
420
421 if let Some(member_index) = member_index {
422 self.propose_remove_member_by_value(provider, signer, member_index)
423 } else {
424 Err(ProposalError::ProposeRemoveMemberError(
425 ProposeRemoveMemberError::UnknownMember,
426 ))
427 }
428 }
429
430 pub fn propose_group_context_extensions<Provider: OpenMlsProvider>(
435 &mut self,
436 provider: &Provider,
437 extensions: Extensions<GroupContext>,
438 signer: &impl Signer,
439 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
440 self.is_operational()?;
441
442 let aad = self.outgoing_authenticated_data()?;
443 let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
444 let proposal = self.create_group_context_ext_proposal::<Provider>(
445 framing_parameters,
446 extensions,
447 signer,
448 )?;
449
450 let queued_proposal = QueuedProposal::from_authenticated_content_by_ref(
451 self.ciphersuite(),
452 provider.crypto(),
453 proposal.clone(),
454 )?;
455
456 let proposal_ref = queued_proposal.proposal_reference();
457 provider
458 .storage()
459 .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
460 .map_err(ProposalError::StorageError)?;
461 self.proposal_store_mut().add(queued_proposal);
462
463 let mls_message = self.content_to_mls_message(proposal, provider)?;
464
465 self.reset_aad();
466 Ok((mls_message, proposal_ref))
467 }
468
469 #[allow(clippy::type_complexity)]
477 pub fn update_group_context_extensions<Provider: OpenMlsProvider>(
478 &mut self,
479 provider: &Provider,
480 extensions: Extensions<GroupContext>,
481 signer: &impl Signer,
482 ) -> Result<
483 (MlsMessageOut, Option<MlsMessageOut>, Option<GroupInfo>),
484 CreateGroupContextExtProposalError<Provider::StorageError>,
485 > {
486 self.is_operational()?;
487
488 let bundle = self
490 .commit_builder()
491 .propose_group_context_extensions(extensions)?
492 .load_psks(provider.storage())?
493 .build(provider.rand(), provider.crypto(), signer, |_| true)?
494 .stage_commit(provider)?;
495
496 let (commit, welcome, group_info) = bundle.into_contents();
498 let welcome = welcome.map(|welcome| MlsMessageOut::from_welcome(welcome, self.version()));
499
500 Ok((commit, welcome, group_info))
501 }
502
503 #[cfg(feature = "extensions-draft")]
505 pub fn propose_app_data_update<Provider: OpenMlsProvider>(
506 &mut self,
507 provider: &Provider,
508 signer: &impl Signer,
509 component_id: ComponentId,
510 operation: AppDataUpdateOperation,
511 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
512 self.is_operational()?;
513
514 let aad = self.outgoing_authenticated_data()?;
515 let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
516 let proposal = self.create_app_data_update_proposal(
517 framing_parameters,
518 component_id,
519 operation,
520 signer,
521 )?;
522
523 let queued_proposal = QueuedProposal::from_authenticated_content(
524 self.ciphersuite(),
525 provider.crypto(),
526 proposal.clone(),
527 ProposalOrRefType::Proposal,
528 )?;
529 let proposal_ref = queued_proposal.proposal_reference();
530
531 log::trace!("Storing proposal in queue {:?}", queued_proposal);
532 provider
533 .storage()
534 .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
535 .map_err(ProposalError::StorageError)?;
536 self.proposal_store_mut().add(queued_proposal);
537
538 let mls_message = self.content_to_mls_message(proposal, provider)?;
539
540 self.reset_aad();
541 Ok((mls_message, proposal_ref))
542 }
543
544 pub fn remove_pending_proposal<Storage: StorageProvider>(
546 &mut self,
547 storage: &Storage,
548 proposal_ref: &ProposalRef,
549 ) -> Result<(), RemoveProposalError<Storage::Error>> {
550 storage
551 .remove_proposal(self.group_id(), proposal_ref)
552 .map_err(RemoveProposalError::Storage)?;
553 self.proposal_store_mut()
554 .remove(proposal_ref)
555 .ok_or(RemoveProposalError::ProposalNotFound)
556 }
557
558 pub(crate) fn create_add_proposal(
565 &self,
566 framing_parameters: FramingParameters,
567 joiner_key_package: KeyPackage,
568 signer: &impl Signer,
569 ) -> Result<AuthenticatedContent, CreateAddProposalError> {
570 if let Some(required_capabilities) = self.required_capabilities() {
571 joiner_key_package
572 .leaf_node()
573 .capabilities()
574 .supports_required_capabilities(required_capabilities)?;
575 }
576 let add_proposal = AddProposal {
577 key_package: joiner_key_package,
578 };
579 let proposal = Proposal::add(add_proposal);
580 AuthenticatedContent::member_proposal(
581 framing_parameters,
582 self.own_leaf_index(),
583 proposal,
584 self.context(),
585 signer,
586 )
587 .map_err(|e| e.into())
588 }
589
590 pub(crate) fn create_update_proposal(
595 &self,
596 framing_parameters: FramingParameters,
597 leaf_node: LeafNode,
600 signer: &impl Signer,
601 ) -> Result<AuthenticatedContent, LibraryError> {
602 let update_proposal = UpdateProposal { leaf_node };
603 let proposal = Proposal::update(update_proposal);
604 AuthenticatedContent::member_proposal(
605 framing_parameters,
606 self.own_leaf_index(),
607 proposal,
608 self.context(),
609 signer,
610 )
611 }
612
613 pub(crate) fn create_remove_proposal(
618 &self,
619 framing_parameters: FramingParameters,
620 removed: LeafNodeIndex,
621 signer: &impl Signer,
622 ) -> Result<AuthenticatedContent, ValidationError> {
623 if self.public_group().leaf(removed).is_none() {
624 return Err(ValidationError::UnknownMember);
625 }
626 let remove_proposal = RemoveProposal { removed };
627 let proposal = Proposal::remove(remove_proposal);
628 AuthenticatedContent::member_proposal(
629 framing_parameters,
630 self.own_leaf_index(),
631 proposal,
632 self.context(),
633 signer,
634 )
635 .map_err(ValidationError::LibraryError)
636 }
637
638 pub(crate) fn create_self_remove_proposal(
641 &self,
642 aad: &[u8],
643 signer: &impl Signer,
644 ) -> Result<AuthenticatedContent, LibraryError> {
645 let proposal = Proposal::SelfRemove;
646 let framing_parameters = FramingParameters::new(aad, WireFormat::PublicMessage);
647 AuthenticatedContent::member_proposal(
648 framing_parameters,
649 self.own_leaf_index(),
650 proposal,
651 self.context(),
652 signer,
653 )
654 }
655
656 pub(crate) fn create_presharedkey_proposal(
662 &self,
663 framing_parameters: FramingParameters,
664 psk: PreSharedKeyId,
665 signer: &impl Signer,
666 ) -> Result<AuthenticatedContent, LibraryError> {
667 let presharedkey_proposal = PreSharedKeyProposal::new(psk);
668 let proposal = Proposal::psk(presharedkey_proposal);
669 AuthenticatedContent::member_proposal(
670 framing_parameters,
671 self.own_leaf_index(),
672 proposal,
673 self.context(),
674 signer,
675 )
676 }
677
678 #[cfg(feature = "extensions-draft")]
679 pub(crate) fn create_app_data_update_proposal(
680 &self,
681 framing_parameters: FramingParameters,
682 component_id: ComponentId,
683 operation: AppDataUpdateOperation,
684 signer: &impl Signer,
685 ) -> Result<AuthenticatedContent, LibraryError> {
686 let proposal = Proposal::AppDataUpdate(Box::new(AppDataUpdateProposal::new(
687 component_id,
688 operation,
689 )));
690 AuthenticatedContent::member_proposal(
691 framing_parameters,
692 self.own_leaf_index(),
693 proposal,
694 self.context(),
695 signer,
696 )
697 }
698
699 pub(crate) fn create_custom_proposal(
700 &self,
701 framing_parameters: FramingParameters,
702 custom_proposal: CustomProposal,
703 signer: &impl Signer,
704 ) -> Result<AuthenticatedContent, LibraryError> {
705 let proposal = Proposal::custom(custom_proposal);
706 AuthenticatedContent::member_proposal(
707 framing_parameters,
708 self.own_leaf_index(),
709 proposal,
710 self.context(),
711 signer,
712 )
713 }
714}