1use openmls_traits::{
5 crypto::OpenMlsCrypto, random::OpenMlsRand, signatures::Signer, storage::StorageProvider as _,
6};
7use tls_codec::Serialize as _;
8
9use crate::{
10 binary_tree::LeafNodeIndex,
11 ciphersuite::{signable::Signable as _, Secret},
12 group::{
13 create_commit::CommitType, diff::compute_path::PathComputationResult,
14 CommitBuilderStageError, CreateCommitError, Extension, Extensions, ExternalPubExtension,
15 ProposalQueue, ProposalQueueError, QueuedProposal, RatchetTreeExtension, StagedCommit,
16 },
17 key_packages::KeyPackage,
18 messages::{
19 group_info::{GroupInfo, GroupInfoTBS},
20 Commit, Welcome,
21 },
22 prelude::{LeafNodeParameters, LibraryError, NewSignerBundle},
23 schedule::{
24 psk::{load_psks, PskSecret},
25 JoinerSecret, KeySchedule, PreSharedKeyId,
26 },
27 storage::{OpenMlsProvider, StorageProvider},
28 versions::ProtocolVersion,
29};
30
31#[cfg(doc)]
32use super::MlsGroupJoinConfig;
33
34use super::{
35 mls_auth_content::AuthenticatedContent,
36 staged_commit::{MemberStagedCommitState, StagedCommitState},
37 AddProposal, CreateCommitResult, GroupContextExtensionProposal, MlsGroup, MlsGroupState,
38 MlsMessageOut, PendingCommitState, Proposal, RemoveProposal, Sender,
39};
40
41pub struct Initial {
43 own_proposals: Vec<Proposal>,
44 force_self_update: bool,
45 leaf_node_parameters: LeafNodeParameters,
46 create_group_info: bool,
47
48 consume_proposal_store: bool,
51}
52
53impl Default for Initial {
54 fn default() -> Self {
55 Initial {
56 consume_proposal_store: true,
57 force_self_update: false,
58 leaf_node_parameters: LeafNodeParameters::default(),
59 create_group_info: false,
60 own_proposals: vec![],
61 }
62 }
63}
64
65pub struct LoadedPsks {
67 own_proposals: Vec<Proposal>,
68 force_self_update: bool,
69 leaf_node_parameters: LeafNodeParameters,
70 create_group_info: bool,
71
72 consume_proposal_store: bool,
75 psks: Vec<(PreSharedKeyId, Secret)>,
76}
77
78pub struct Complete {
80 result: CreateCommitResult,
81}
82
83#[derive(Debug)]
121pub struct CommitBuilder<'a, T> {
122 group: &'a mut MlsGroup,
125
126 stage: T,
128}
129
130impl<'a, T> CommitBuilder<'a, T> {
131 pub(crate) fn replace_stage<NextStage>(
132 self,
133 next_stage: NextStage,
134 ) -> (T, CommitBuilder<'a, NextStage>) {
135 self.map_stage(|prev_stage| (prev_stage, next_stage))
136 }
137
138 pub(crate) fn into_stage<NextStage>(
139 self,
140 next_stage: NextStage,
141 ) -> CommitBuilder<'a, NextStage> {
142 self.replace_stage(next_stage).1
143 }
144
145 pub(crate) fn take_stage(self) -> (T, CommitBuilder<'a, ()>) {
146 self.replace_stage(())
147 }
148
149 pub(crate) fn map_stage<NextStage, Aux, F: FnOnce(T) -> (Aux, NextStage)>(
150 self,
151 f: F,
152 ) -> (Aux, CommitBuilder<'a, NextStage>) {
153 let Self { group, stage } = self;
154
155 let (aux, stage) = f(stage);
156
157 (aux, CommitBuilder { group, stage })
158 }
159
160 #[cfg(feature = "fork-resolution")]
161 pub(crate) fn stage(&self) -> &T {
162 &self.stage
163 }
164}
165
166impl MlsGroup {
167 pub fn commit_builder(&mut self) -> CommitBuilder<Initial> {
169 CommitBuilder::new(self)
170 }
171}
172
173impl<'a> CommitBuilder<'a, Initial> {
174 pub fn new(group: &'a mut MlsGroup) -> Self {
176 let stage = Initial {
177 create_group_info: group.configuration().use_ratchet_tree_extension,
178 ..Default::default()
179 };
180 Self { group, stage }
181 }
182
183 pub fn consume_proposal_store(mut self, consume_proposal_store: bool) -> Self {
186 self.stage.consume_proposal_store = consume_proposal_store;
187 self
188 }
189
190 pub fn create_group_info(mut self, create_group_info: bool) -> Self {
193 self.stage.create_group_info = create_group_info;
194 self
195 }
196
197 pub fn force_self_update(mut self, force_self_update: bool) -> Self {
199 self.stage.force_self_update = force_self_update;
200 self
201 }
202
203 pub fn add_proposal(mut self, proposal: Proposal) -> Self {
205 self.stage.own_proposals.push(proposal);
206 self
207 }
208
209 pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
211 self.stage.own_proposals.extend(proposals);
212 self
213 }
214
215 pub fn leaf_node_parameters(mut self, leaf_node_parameters: LeafNodeParameters) -> Self {
218 self.stage.leaf_node_parameters = leaf_node_parameters;
219 self
220 }
221
222 pub fn propose_adds(mut self, key_packages: impl IntoIterator<Item = KeyPackage>) -> Self {
225 self.stage.own_proposals.extend(
226 key_packages
227 .into_iter()
228 .map(|key_package| Proposal::Add(AddProposal { key_package })),
229 );
230 self
231 }
232
233 pub fn propose_removals(mut self, removed: impl IntoIterator<Item = LeafNodeIndex>) -> Self {
234 self.stage.own_proposals.extend(
235 removed
236 .into_iter()
237 .map(|removed| Proposal::Remove(RemoveProposal { removed })),
238 );
239 self
240 }
241
242 pub fn propose_group_context_extensions(mut self, extensions: Extensions) -> Self {
243 self.stage
244 .own_proposals
245 .push(Proposal::GroupContextExtensions(
246 GroupContextExtensionProposal::new(extensions),
247 ));
248 self
249 }
250
251 pub fn load_psks<Storage: StorageProvider>(
253 self,
254 storage: &'a Storage,
255 ) -> Result<CommitBuilder<'a, LoadedPsks>, CreateCommitError> {
256 let psk_ids: Vec<_> = self
257 .stage
258 .own_proposals
259 .iter()
260 .chain(
261 self.group
262 .proposal_store()
263 .proposals()
264 .map(|queued_proposal| queued_proposal.proposal()),
265 )
266 .filter_map(|proposal| match proposal {
267 Proposal::PreSharedKey(psk_proposal) => Some(psk_proposal.clone().into_psk_id()),
268 _ => None,
269 })
270 .collect();
271
272 let psks = load_psks(storage, &self.group.resumption_psk_store, &psk_ids)?
274 .into_iter()
275 .map(|(psk_id_ref, key)| (psk_id_ref.clone(), key))
276 .collect();
277
278 Ok(self
279 .map_stage(|stage| {
280 (
281 (),
282 LoadedPsks {
283 own_proposals: stage.own_proposals,
284 psks,
285 force_self_update: stage.force_self_update,
286 leaf_node_parameters: stage.leaf_node_parameters,
287 consume_proposal_store: stage.consume_proposal_store,
288 create_group_info: stage.create_group_info,
289 },
290 )
291 })
292 .1)
293 }
294}
295
296impl<'a> CommitBuilder<'a, LoadedPsks> {
297 pub fn build<S: Signer>(
301 self,
302 rand: &impl OpenMlsRand,
303 crypto: &impl OpenMlsCrypto,
304 signer: &S,
305 f: impl FnMut(&QueuedProposal) -> bool,
306 ) -> Result<CommitBuilder<'a, Complete>, CreateCommitError> {
307 self.build_internal(rand, crypto, signer, None::<NewSignerBundle<'_, S>>, f)
308 }
309
310 pub fn build_with_new_signer<S: Signer>(
318 self,
319 rand: &impl OpenMlsRand,
320 crypto: &impl OpenMlsCrypto,
321 old_signer: &impl Signer,
322 new_signer: NewSignerBundle<'_, S>,
323 f: impl FnMut(&QueuedProposal) -> bool,
324 ) -> Result<CommitBuilder<'a, Complete>, CreateCommitError> {
325 self.build_internal(rand, crypto, old_signer, Some(new_signer), f)
326 }
327
328 fn build_internal<S: Signer>(
329 self,
330 rand: &impl OpenMlsRand,
331 crypto: &impl OpenMlsCrypto,
332 old_signer: &impl Signer,
333 new_signer: Option<NewSignerBundle<'_, S>>,
334 f: impl FnMut(&QueuedProposal) -> bool,
335 ) -> Result<CommitBuilder<'a, Complete>, CreateCommitError> {
336 let ciphersuite = self.group.ciphersuite();
337 let sender = Sender::build_member(self.group.own_leaf_index());
338 let (mut cur_stage, builder) = self.take_stage();
339 let psks = cur_stage.psks;
340
341 let own_proposals: Vec<_> = cur_stage
344 .own_proposals
345 .into_iter()
346 .map(|proposal| {
347 QueuedProposal::from_proposal_and_sender(ciphersuite, crypto, proposal, &sender)
348 })
349 .collect::<Result<_, _>>()?;
350
351 let group_proposal_store_queue = builder
354 .group
355 .pending_proposals()
356 .filter(|_| cur_stage.consume_proposal_store)
357 .cloned();
358
359 let proposal_queue = group_proposal_store_queue.chain(own_proposals).filter(f);
363
364 let (proposal_queue, contains_own_updates) =
365 ProposalQueue::filter_proposals_without_inline(
366 proposal_queue,
367 builder.group.own_leaf_index,
368 )
369 .map_err(|e| match e {
370 ProposalQueueError::LibraryError(e) => e.into(),
371 ProposalQueueError::ProposalNotFound => CreateCommitError::MissingProposal,
372 ProposalQueueError::UpdateFromExternalSender
373 | ProposalQueueError::SelfRemoveFromNonMember => {
374 CreateCommitError::WrongProposalSenderType
375 }
376 })?;
377
378 builder
383 .group
384 .public_group
385 .validate_proposal_type_support(&proposal_queue)?;
386 builder
391 .group
392 .public_group
393 .validate_key_uniqueness(&proposal_queue, None)?;
394 builder
396 .group
397 .public_group
398 .validate_add_proposals(&proposal_queue)?;
399 builder
402 .group
403 .public_group
404 .validate_capabilities(&proposal_queue)?;
405 builder
408 .group
409 .public_group
410 .validate_remove_proposals(&proposal_queue)?;
411 builder
412 .group
413 .public_group
414 .validate_pre_shared_key_proposals(&proposal_queue)?;
415 builder
420 .group
421 .public_group
422 .validate_update_proposals(&proposal_queue, builder.group.own_leaf_index())?;
423
424 builder
427 .group
428 .public_group
429 .validate_group_context_extensions_proposal(&proposal_queue)?;
430
431 let ciphersuite = builder.group.ciphersuite();
432 let sender = Sender::build_member(builder.group.own_leaf_index());
433 let proposal_reference_list = proposal_queue.commit_list();
434
435 let mut diff = builder.group.public_group.empty_diff();
437
438 let apply_proposals_values =
440 diff.apply_proposals(&proposal_queue, builder.group.own_leaf_index())?;
441 if apply_proposals_values.self_removed {
442 return Err(CreateCommitError::CannotRemoveSelf);
443 }
444
445 let path_computation_result =
446 if apply_proposals_values.path_required
448 || contains_own_updates
449 || cur_stage.force_self_update
450 || !cur_stage.leaf_node_parameters.is_empty()
451 {
452 if let Some(new_signer) = new_signer {
456 if let Some(credential_with_key) =
457 cur_stage.leaf_node_parameters.credential_with_key()
458 {
459 if credential_with_key != &new_signer.credential_with_key {
460 return Err(CreateCommitError::InvalidLeafNodeParameters);
461 }
462 }
463 cur_stage.leaf_node_parameters.set_credential_with_key(
464 new_signer.credential_with_key,
465 );
466 diff.compute_path(
467 rand,
468 crypto,
469 builder.group.own_leaf_index(),
470 apply_proposals_values.exclusion_list(),
471 &CommitType::Member,
472 &cur_stage.leaf_node_parameters,
473 new_signer.signer,
474 apply_proposals_values.extensions.clone()
475 )?
476 } else {
477 diff.compute_path(
478 rand,
479 crypto,
480 builder.group.own_leaf_index(),
481 apply_proposals_values.exclusion_list(),
482 &CommitType::Member,
483 &cur_stage.leaf_node_parameters,
484 old_signer,
485 apply_proposals_values.extensions.clone()
486 )?
487 }
488 } else {
489 diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
492 PathComputationResult::default()
493 };
494
495 let update_path_leaf_node = path_computation_result
496 .encrypted_path
497 .as_ref()
498 .map(|path| path.leaf_node().clone());
499
500 let commit = Commit {
502 proposals: proposal_reference_list,
503 path: path_computation_result.encrypted_path,
504 };
505
506 let mut authenticated_content = AuthenticatedContent::commit(
508 builder.group.framing_parameters(),
509 sender,
510 commit,
511 builder.group.public_group.group_context(),
512 old_signer,
513 )?;
514
515 diff.update_confirmed_transcript_hash(crypto, &authenticated_content)?;
517
518 let serialized_provisional_group_context = diff
519 .group_context()
520 .tls_serialize_detached()
521 .map_err(LibraryError::missing_bound_check)?;
522
523 let joiner_secret = JoinerSecret::new(
524 crypto,
525 ciphersuite,
526 path_computation_result.commit_secret,
527 builder.group.group_epoch_secrets().init_secret(),
528 &serialized_provisional_group_context,
529 )
530 .map_err(LibraryError::unexpected_crypto_error)?;
531
532 let psk_secret = { PskSecret::new(crypto, ciphersuite, psks)? };
534
535 let mut key_schedule = KeySchedule::init(ciphersuite, crypto, &joiner_secret, psk_secret)?;
537
538 let serialized_provisional_group_context = diff
539 .group_context()
540 .tls_serialize_detached()
541 .map_err(LibraryError::missing_bound_check)?;
542
543 let welcome_secret = key_schedule
544 .welcome(crypto, builder.group.ciphersuite())
545 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
546 key_schedule
547 .add_context(crypto, &serialized_provisional_group_context)
548 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
549 let provisional_epoch_secrets = key_schedule
550 .epoch_secrets(crypto, builder.group.ciphersuite())
551 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
552
553 let confirmation_tag = provisional_epoch_secrets
555 .confirmation_key()
556 .tag(
557 crypto,
558 builder.group.ciphersuite(),
559 diff.group_context().confirmed_transcript_hash(),
560 )
561 .map_err(LibraryError::unexpected_crypto_error)?;
562
563 authenticated_content.set_confirmation_tag(confirmation_tag.clone());
565
566 diff.update_interim_transcript_hash(ciphersuite, crypto, confirmation_tag.clone())?;
567
568 let needs_welcome = !apply_proposals_values.invitation_list.is_empty();
570
571 let needs_group_info = needs_welcome || cur_stage.create_group_info;
575
576 let group_info = if !needs_group_info {
577 None
578 } else {
579 let external_pub = provisional_epoch_secrets
581 .external_secret()
582 .derive_external_keypair(crypto, ciphersuite)
583 .map_err(LibraryError::unexpected_crypto_error)?
584 .public;
585 let external_pub_extension =
586 Extension::ExternalPub(ExternalPubExtension::new(external_pub.into()));
587
588 let extensions: Extensions = if builder.group.configuration().use_ratchet_tree_extension
590 {
591 Extensions::from_vec(vec![
592 Extension::RatchetTree(RatchetTreeExtension::new(diff.export_ratchet_tree())),
593 external_pub_extension,
594 ])?
595 } else {
596 Extensions::single(external_pub_extension)
597 };
598
599 let group_info_tbs = {
601 GroupInfoTBS::new(
602 diff.group_context().clone(),
603 extensions,
604 confirmation_tag,
605 builder.group.own_leaf_index(),
606 )
607 };
608 Some(group_info_tbs.sign(old_signer)?)
610 };
611
612 let welcome_option = if !needs_welcome {
613 None
614 } else {
615 let (welcome_key, welcome_nonce) = welcome_secret
617 .derive_welcome_key_nonce(crypto, builder.group.ciphersuite())
618 .map_err(LibraryError::unexpected_crypto_error)?;
619 let encrypted_group_info = welcome_key
620 .aead_seal(
621 crypto,
622 group_info
623 .as_ref()
624 .ok_or_else(|| LibraryError::custom("GroupInfo was not computed"))?
625 .tls_serialize_detached()
626 .map_err(LibraryError::missing_bound_check)?
627 .as_slice(),
628 &[],
629 &welcome_nonce,
630 )
631 .map_err(LibraryError::unexpected_crypto_error)?;
632
633 let encrypted_secrets = diff.encrypt_group_secrets(
636 &joiner_secret,
637 apply_proposals_values.invitation_list,
638 path_computation_result.plain_path.as_deref(),
639 &apply_proposals_values.presharedkeys,
640 &encrypted_group_info,
641 crypto,
642 builder.group.own_leaf_index(),
643 )?;
644
645 let welcome = Welcome::new(ciphersuite, encrypted_secrets, encrypted_group_info);
647 Some(welcome)
648 };
649
650 let (provisional_group_epoch_secrets, provisional_message_secrets) =
651 provisional_epoch_secrets.split_secrets(
652 serialized_provisional_group_context,
653 diff.tree_size(),
654 builder.group.own_leaf_index(),
655 );
656
657 let staged_commit_state = MemberStagedCommitState::new(
658 provisional_group_epoch_secrets,
659 provisional_message_secrets,
660 diff.into_staged_diff(crypto, ciphersuite)?,
661 path_computation_result.new_keypairs,
662 None,
665 update_path_leaf_node,
666 );
667 let staged_commit = StagedCommit::new(
668 proposal_queue,
669 StagedCommitState::GroupMember(Box::new(staged_commit_state)),
670 );
671
672 Ok(builder.into_stage(Complete {
673 result: CreateCommitResult {
674 commit: authenticated_content,
675 welcome_option,
676 staged_commit,
677 group_info: group_info.filter(|_| cur_stage.create_group_info),
678 },
679 }))
680 }
681}
682
683impl CommitBuilder<'_, Complete> {
684 #[cfg(test)]
685 pub(crate) fn commit_result(self) -> CreateCommitResult {
686 self.stage.result
687 }
688
689 pub fn stage_commit<Provider: OpenMlsProvider>(
691 self,
692 provider: &Provider,
693 ) -> Result<CommitMessageBundle, CommitBuilderStageError<Provider::StorageError>> {
694 let Self {
695 group,
696 stage: Complete {
697 result: create_commit_result,
698 },
699 ..
700 } = self;
701
702 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
705 create_commit_result.staged_commit,
706 )));
707
708 provider
709 .storage()
710 .write_group_state(group.group_id(), &group.group_state)
711 .map_err(CommitBuilderStageError::KeyStoreError)?;
712
713 group.reset_aad();
714
715 let mls_message = group.content_to_mls_message(create_commit_result.commit, provider)?;
721
722 Ok(CommitMessageBundle {
723 version: group.version(),
724 commit: mls_message,
725 welcome: create_commit_result.welcome_option,
726 group_info: create_commit_result.group_info,
727 })
728 }
729}
730
731#[derive(Debug, Clone)]
734pub struct CommitMessageBundle {
735 version: ProtocolVersion,
736 commit: MlsMessageOut,
737 welcome: Option<Welcome>,
738 group_info: Option<GroupInfo>,
739}
740
741#[cfg(test)]
742impl CommitMessageBundle {
743 pub fn new(
744 version: ProtocolVersion,
745 commit: MlsMessageOut,
746 welcome: Option<Welcome>,
747 group_info: Option<GroupInfo>,
748 ) -> Self {
749 Self {
750 version,
751 commit,
752 welcome,
753 group_info,
754 }
755 }
756}
757
758impl CommitMessageBundle {
759 pub fn commit(&self) -> &MlsMessageOut {
763 &self.commit
764 }
765
766 pub fn welcome(&self) -> Option<&Welcome> {
769 self.welcome.as_ref()
770 }
771
772 pub fn to_welcome_msg(&self) -> Option<MlsMessageOut> {
775 self.welcome
776 .as_ref()
777 .map(|welcome| MlsMessageOut::from_welcome(welcome.clone(), self.version))
778 }
779
780 pub fn group_info(&self) -> Option<&GroupInfo> {
784 self.group_info.as_ref()
785 }
786
787 pub fn contents(&self) -> (&MlsMessageOut, Option<&Welcome>, Option<&GroupInfo>) {
790 (
791 &self.commit,
792 self.welcome.as_ref(),
793 self.group_info.as_ref(),
794 )
795 }
796
797 pub fn into_commit(self) -> MlsMessageOut {
801 self.commit
802 }
803
804 pub fn into_welcome(self) -> Option<Welcome> {
808 self.welcome
809 }
810
811 pub fn into_welcome_msg(self) -> Option<MlsMessageOut> {
814 self.welcome
815 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version))
816 }
817
818 pub fn into_group_info(self) -> Option<GroupInfo> {
823 self.group_info
824 }
825
826 pub fn into_group_info_msg(self) -> Option<MlsMessageOut> {
828 self.group_info.map(|group_info| group_info.into())
829 }
830
831 pub fn into_contents(self) -> (MlsMessageOut, Option<Welcome>, Option<GroupInfo>) {
834 (self.commit, self.welcome, self.group_info)
835 }
836
837 pub fn into_messages(self) -> (MlsMessageOut, Option<MlsMessageOut>, Option<MlsMessageOut>) {
840 (
841 self.commit,
842 self.welcome
843 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version)),
844 self.group_info.map(|group_info| group_info.into()),
845 )
846 }
847}
848
849impl IntoIterator for CommitMessageBundle {
850 type Item = MlsMessageOut;
851
852 type IntoIter = core::iter::Chain<
853 core::iter::Chain<
854 core::option::IntoIter<MlsMessageOut>,
855 core::option::IntoIter<MlsMessageOut>,
856 >,
857 core::option::IntoIter<MlsMessageOut>,
858 >;
859
860 fn into_iter(self) -> Self::IntoIter {
861 let welcome = self.to_welcome_msg();
862 let group_info = self.group_info.map(|group_info| group_info.into());
863
864 Some(self.commit)
865 .into_iter()
866 .chain(welcome)
867 .chain(group_info)
868 }
869}