1use core::fmt::Debug;
2use std::mem;
3
4#[cfg(feature = "extensions-draft-08")]
5use openmls_traits::crypto::OpenMlsCrypto;
6use openmls_traits::storage::StorageProvider as _;
7use serde::{Deserialize, Serialize};
8use tls_codec::Serialize as _;
9
10use super::proposal_store::{
11 QueuedAddProposal, QueuedPskProposal, QueuedRemoveProposal, QueuedUpdateProposal,
12};
13
14use super::{
15 super::errors::*, load_psks, Credential, Extension, GroupContext, GroupEpochSecrets, GroupId,
16 JoinerSecret, KeySchedule, LeafNode, LibraryError, MessageSecrets, MlsGroup, OpenMlsProvider,
17 Proposal, ProposalQueue, PskSecret, QueuedProposal, Sender,
18};
19use crate::group::diff::PublicGroupDiff;
20use crate::prelude::{Commit, LeafNodeIndex};
21#[cfg(feature = "extensions-draft-08")]
22use crate::schedule::application_export_tree::ApplicationExportTree;
23
24use crate::{
25 ciphersuite::{hash_ref::ProposalRef, Secret},
26 framing::mls_auth_content::AuthenticatedContent,
27 group::public_group::{
28 diff::{apply_proposals::ApplyProposalsValues, StagedPublicGroupDiff},
29 staged_commit::PublicStagedCommitState,
30 },
31 schedule::{CommitSecret, EpochAuthenticator, EpochSecretsResult, InitSecret, PreSharedKeyId},
32 treesync::node::encryption_keys::EncryptionKeyPair,
33};
34
35#[cfg(feature = "extensions-draft-08")]
36use super::proposal_store::{QueuedAppDataUpdateProposal, QueuedAppEphemeralProposal};
37#[cfg(feature = "extensions-draft-08")]
38use crate::prelude::processing::AppDataUpdates;
39
40impl MlsGroup {
41 fn derive_epoch_secrets(
42 &self,
43 provider: &impl OpenMlsProvider,
44 apply_proposals_values: ApplyProposalsValues,
45 epoch_secrets: &GroupEpochSecrets,
46 commit_secret: CommitSecret,
47 serialized_provisional_group_context: &[u8],
48 ) -> Result<EpochSecretsResult, StageCommitError> {
49 let joiner_secret = if let Some(ref external_init_proposal) =
52 apply_proposals_values.external_init_proposal_option
53 {
54 let external_priv = epoch_secrets
56 .external_secret()
57 .derive_external_keypair(provider.crypto(), self.ciphersuite())
58 .map_err(LibraryError::unexpected_crypto_error)?
59 .private;
60 let init_secret = InitSecret::from_kem_output(
61 provider.crypto(),
62 self.ciphersuite(),
63 self.version(),
64 &external_priv,
65 external_init_proposal.kem_output(),
66 )?;
67 JoinerSecret::new(
68 provider.crypto(),
69 self.ciphersuite(),
70 commit_secret,
71 &init_secret,
72 serialized_provisional_group_context,
73 )
74 .map_err(LibraryError::unexpected_crypto_error)?
75 } else {
76 JoinerSecret::new(
77 provider.crypto(),
78 self.ciphersuite(),
79 commit_secret,
80 epoch_secrets.init_secret(),
81 serialized_provisional_group_context,
82 )
83 .map_err(LibraryError::unexpected_crypto_error)?
84 };
85
86 let psk_secret = {
89 let psks: Vec<(&PreSharedKeyId, Secret)> = load_psks(
90 provider.storage(),
91 &self.resumption_psk_store,
92 &apply_proposals_values.presharedkeys,
93 )?;
94
95 PskSecret::new(provider.crypto(), self.ciphersuite(), psks)?
96 };
97
98 let mut key_schedule = KeySchedule::init(
100 self.ciphersuite(),
101 provider.crypto(),
102 &joiner_secret,
103 psk_secret,
104 )?;
105
106 key_schedule
107 .add_context(provider.crypto(), serialized_provisional_group_context)
108 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
109 Ok(key_schedule
110 .epoch_secrets(provider.crypto(), self.ciphersuite())
111 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?)
112 }
113
114 pub(crate) fn stage_commit(
153 &self,
154 mls_content: &AuthenticatedContent,
155 old_epoch_keypairs: Vec<EncryptionKeyPair>,
156 leaf_node_keypairs: Vec<EncryptionKeyPair>,
157 provider: &impl OpenMlsProvider,
158 ) -> Result<StagedCommit, StageCommitError> {
159 if let Sender::Member(member) = mls_content.sender() {
161 if member == &self.own_leaf_index() {
162 return Err(StageCommitError::OwnCommit);
163 }
164 }
165
166 let (commit, proposal_queue, sender_index) = self
167 .public_group
168 .validate_commit(mls_content, provider.crypto())?;
169
170 let mut diff = self.public_group.empty_diff();
173
174 #[cfg(not(feature = "extensions-draft-08"))]
175 let apply_proposals_values =
176 diff.apply_proposals(&proposal_queue, self.own_leaf_index())?;
177
178 #[cfg(feature = "extensions-draft-08")]
179 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
180 &proposal_queue,
181 self.own_leaf_index(),
182 None,
183 )?;
184 self.stage_applied_proposal_values(
185 apply_proposals_values,
186 diff,
187 commit,
188 proposal_queue,
189 sender_index,
190 mls_content,
191 old_epoch_keypairs,
192 leaf_node_keypairs,
193 provider,
194 )
195 }
196
197 #[cfg(feature = "extensions-draft-08")]
198 pub(crate) fn stage_commit_with_app_data_updates(
199 &self,
200 mls_content: &AuthenticatedContent,
201 old_epoch_keypairs: Vec<EncryptionKeyPair>,
202 leaf_node_keypairs: Vec<EncryptionKeyPair>,
203 app_data_dict_updates: Option<AppDataUpdates>,
204 provider: &impl OpenMlsProvider,
205 ) -> Result<StagedCommit, StageCommitError> {
206 if let Sender::Member(member) = mls_content.sender() {
208 if member == &self.own_leaf_index() {
209 return Err(StageCommitError::OwnCommit);
210 }
211 }
212
213 let (commit, proposal_queue, sender_index) = self
214 .public_group
215 .validate_commit(mls_content, provider.crypto())?;
216
217 let mut diff = self.public_group.empty_diff();
220
221 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
222 &proposal_queue,
223 self.own_leaf_index(),
224 app_data_dict_updates,
225 )?;
226
227 self.stage_applied_proposal_values(
228 apply_proposals_values,
229 diff,
230 commit,
231 proposal_queue,
232 sender_index,
233 mls_content,
234 old_epoch_keypairs,
235 leaf_node_keypairs,
236 provider,
237 )
238 }
239
240 #[allow(clippy::too_many_arguments)]
241 fn stage_applied_proposal_values(
242 &self,
243 apply_proposals_values: ApplyProposalsValues,
244 mut diff: PublicGroupDiff,
245 commit: &Commit,
246 proposal_queue: ProposalQueue,
247 sender_index: LeafNodeIndex,
248 mls_content: &AuthenticatedContent,
249 old_epoch_keypairs: Vec<EncryptionKeyPair>,
250 leaf_node_keypairs: Vec<EncryptionKeyPair>,
251 provider: &impl OpenMlsProvider,
252 ) -> Result<StagedCommit, StageCommitError> {
253 let ciphersuite = self.ciphersuite();
254 let (commit_secret, new_keypairs, new_leaf_keypair_option, update_path_leaf_node) =
256 if let Some(path) = commit.path.clone() {
257 diff.apply_received_update_path(
260 provider.crypto(),
261 ciphersuite,
262 sender_index,
263 &path,
264 )?;
265
266 diff.update_group_context(
268 provider.crypto(),
269 apply_proposals_values.extensions.clone(),
270 )?;
271
272 if apply_proposals_values.self_removed {
274 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
276 let staged_state = PublicStagedCommitState::new(
277 staged_diff,
278 commit.path.as_ref().map(|path| path.leaf_node().clone()),
279 );
280 let staged_commit = StagedCommit::new(
281 proposal_queue,
282 StagedCommitState::PublicState(Box::new(staged_state)),
283 );
284 return Ok(staged_commit);
285 }
286
287 let decryption_keypairs: Vec<&EncryptionKeyPair> = old_epoch_keypairs
288 .iter()
289 .chain(leaf_node_keypairs.iter())
290 .collect();
291
292 let (new_keypairs, commit_secret) = diff.decrypt_path(
295 provider.crypto(),
296 &decryption_keypairs,
297 self.own_leaf_index(),
298 sender_index,
299 path.nodes(),
300 &apply_proposals_values.exclusion_list(),
301 )?;
302
303 let new_leaf_keypair_option = if let Some(leaf) = diff.leaf(self.own_leaf_index()) {
309 leaf_node_keypairs.into_iter().find_map(|keypair| {
310 if leaf.encryption_key() == keypair.public_key() {
311 Some(keypair)
312 } else {
313 None
314 }
315 })
316 } else {
317 debug_assert!(false);
319 None
320 };
321
322 let update_path_leaf_node = Some(path.leaf_node().clone());
326 debug_assert_eq!(diff.leaf(sender_index), path.leaf_node().into());
327
328 (
329 commit_secret,
330 new_keypairs,
331 new_leaf_keypair_option,
332 update_path_leaf_node,
333 )
334 } else {
335 if apply_proposals_values.path_required {
336 return Err(StageCommitError::RequiredPathNotFound);
338 }
339
340 diff.update_group_context(
342 provider.crypto(),
343 apply_proposals_values.extensions.clone(),
344 )?;
345
346 (CommitSecret::zero_secret(ciphersuite), vec![], None, None)
347 };
348
349 diff.update_confirmed_transcript_hash(provider.crypto(), mls_content)?;
351
352 let received_confirmation_tag = mls_content
353 .confirmation_tag()
354 .ok_or(StageCommitError::ConfirmationTagMissing)?;
355
356 let serialized_provisional_group_context = diff
357 .group_context()
358 .tls_serialize_detached()
359 .map_err(LibraryError::missing_bound_check)?;
360
361 let EpochSecretsResult {
362 epoch_secrets,
363 #[cfg(feature = "extensions-draft-08")]
364 application_exporter,
365 } = self.derive_epoch_secrets(
366 provider,
367 apply_proposals_values,
368 self.group_epoch_secrets(),
369 commit_secret,
370 &serialized_provisional_group_context,
371 )?;
372 let (provisional_group_secrets, provisional_message_secrets) = epoch_secrets.split_secrets(
373 serialized_provisional_group_context,
374 diff.tree_size(),
375 self.own_leaf_index(),
376 );
377
378 let own_confirmation_tag = provisional_message_secrets
381 .confirmation_key()
382 .tag(
383 provider.crypto(),
384 self.ciphersuite(),
385 diff.group_context().confirmed_transcript_hash(),
386 )
387 .map_err(LibraryError::unexpected_crypto_error)?;
388 if &own_confirmation_tag != received_confirmation_tag {
389 log::error!("Confirmation tag mismatch");
390 log_crypto!(trace, " Got: {:x?}", received_confirmation_tag);
391 log_crypto!(trace, " Expected: {:x?}", own_confirmation_tag);
392 if !crate::skip_validation::is_disabled::confirmation_tag() {
399 return Err(StageCommitError::ConfirmationTagMismatch);
400 }
401 }
402
403 diff.update_interim_transcript_hash(ciphersuite, provider.crypto(), own_confirmation_tag)?;
404
405 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
406 #[cfg(feature = "extensions-draft-08")]
407 let application_export_tree = ApplicationExportTree::new(application_exporter);
408 let staged_commit_state =
409 StagedCommitState::GroupMember(Box::new(MemberStagedCommitState::new(
410 provisional_group_secrets,
411 provisional_message_secrets,
412 staged_diff,
413 new_keypairs,
414 new_leaf_keypair_option,
415 update_path_leaf_node,
416 #[cfg(feature = "extensions-draft-08")]
417 application_export_tree,
418 )));
419 let staged_commit = StagedCommit::new(proposal_queue, staged_commit_state);
420
421 Ok(staged_commit)
422 }
423
424 pub(crate) fn merge_commit<Provider: OpenMlsProvider>(
430 &mut self,
431 provider: &Provider,
432 staged_commit: StagedCommit,
433 ) -> Result<(), MergeCommitError<Provider::StorageError>> {
434 let old_epoch_keypairs = self
437 .read_epoch_keypairs(provider.storage())
438 .map_err(MergeCommitError::StorageError)?;
439 match staged_commit.state {
440 StagedCommitState::PublicState(staged_state) => {
441 self.public_group
442 .merge_diff(staged_state.into_staged_diff());
443 self.store(provider.storage())
444 .map_err(MergeCommitError::StorageError)?;
445 Ok(())
446 }
447 StagedCommitState::GroupMember(state) => {
448 let past_epoch = self.context().epoch();
450 let leaves = self.public_group().members().collect();
452 self.group_epoch_secrets = state.group_epoch_secrets;
455
456 let mut message_secrets = state.message_secrets;
458 mem::swap(
459 &mut message_secrets,
460 self.message_secrets_store.message_secrets_mut(),
461 );
462 self.message_secrets_store
463 .add(past_epoch, message_secrets, leaves);
464
465 #[cfg(feature = "extensions-draft-08")]
467 {
468 if let Some(application_export_tree) = state.application_export_tree {
472 use openmls_traits::storage::StorageProvider as _;
475 provider
476 .storage()
477 .write_application_export_tree(
478 self.group_id(),
479 &application_export_tree,
480 )
481 .map_err(MergeCommitError::StorageError)?;
482
483 self.application_export_tree = Some(application_export_tree);
484 }
485 }
486
487 self.public_group.merge_diff(state.staged_diff);
488
489 let leaf_keypair = if let Some(keypair) = &state.new_leaf_keypair_option {
490 vec![keypair.clone()]
491 } else {
492 vec![]
493 };
494
495 let new_owned_encryption_keys = self
497 .public_group()
498 .owned_encryption_keys(self.own_leaf_index());
499 let epoch_keypairs: Vec<EncryptionKeyPair> = old_epoch_keypairs
501 .into_iter()
502 .chain(state.new_keypairs)
503 .chain(leaf_keypair)
504 .filter(|keypair| new_owned_encryption_keys.contains(keypair.public_key()))
505 .collect();
506
507 debug_assert_eq!(new_owned_encryption_keys.len(), epoch_keypairs.len());
509 if new_owned_encryption_keys.len() != epoch_keypairs.len() {
510 return Err(LibraryError::custom(
511 "We should have all the private key material we need.",
512 )
513 .into());
514 }
515
516 let storage = provider.storage();
518 let group_id = self.group_id();
519
520 self.public_group
521 .store(storage)
522 .map_err(MergeCommitError::StorageError)?;
523 storage
524 .write_group_epoch_secrets(group_id, &self.group_epoch_secrets)
525 .map_err(MergeCommitError::StorageError)?;
526 storage
527 .write_message_secrets(group_id, &self.message_secrets_store)
528 .map_err(MergeCommitError::StorageError)?;
529
530 self.store_epoch_keypairs(storage, epoch_keypairs.as_slice())
532 .map_err(MergeCommitError::StorageError)?;
533
534 self.delete_previous_epoch_keypairs(storage)
536 .map_err(MergeCommitError::StorageError)?;
537 if let Some(keypair) = state.new_leaf_keypair_option {
538 keypair
539 .delete(storage)
540 .map_err(MergeCommitError::StorageError)?;
541 }
542
543 storage
545 .clear_proposal_queue::<GroupId, ProposalRef>(group_id)
546 .map_err(MergeCommitError::StorageError)?;
547 self.proposal_store_mut().empty();
548
549 Ok(())
550 }
551 }
552 }
553}
554
555#[derive(Debug, Serialize, Deserialize)]
556#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
557pub(crate) enum StagedCommitState {
558 PublicState(Box<PublicStagedCommitState>),
559 GroupMember(Box<MemberStagedCommitState>),
561}
562
563#[derive(Debug, Serialize, Deserialize)]
565#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
566pub struct StagedCommit {
567 pub staged_proposal_queue: ProposalQueue,
569 pub(super) state: StagedCommitState,
571}
572
573impl StagedCommit {
574 pub(crate) fn new(staged_proposal_queue: ProposalQueue, state: StagedCommitState) -> Self {
577 StagedCommit {
578 staged_proposal_queue,
579 state,
580 }
581 }
582
583 pub fn add_proposals(&self) -> impl Iterator<Item = QueuedAddProposal<'_>> {
585 self.staged_proposal_queue.add_proposals()
586 }
587
588 pub fn remove_proposals(&self) -> impl Iterator<Item = QueuedRemoveProposal<'_>> {
590 self.staged_proposal_queue.remove_proposals()
591 }
592
593 pub fn update_proposals(&self) -> impl Iterator<Item = QueuedUpdateProposal<'_>> {
595 self.staged_proposal_queue.update_proposals()
596 }
597
598 pub fn psk_proposals(&self) -> impl Iterator<Item = QueuedPskProposal<'_>> {
600 self.staged_proposal_queue.psk_proposals()
601 }
602
603 #[cfg(feature = "extensions-draft-08")]
604 pub fn queued_app_ephemeral_proposals(
607 &self,
608 ) -> impl Iterator<Item = QueuedAppEphemeralProposal<'_>> {
609 self.staged_proposal_queue.app_ephemeral_proposals()
610 }
611 #[cfg(feature = "extensions-draft-08")]
613 pub fn app_data_update_proposals(
616 &self,
617 ) -> impl Iterator<Item = QueuedAppDataUpdateProposal<'_>> {
618 self.staged_proposal_queue.app_data_update_proposals()
619 }
620
621 pub fn queued_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
623 self.staged_proposal_queue.queued_proposals()
624 }
625
626 pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
628 match self.state {
629 StagedCommitState::PublicState(ref public_state) => {
630 public_state.update_path_leaf_node()
631 }
632 StagedCommitState::GroupMember(ref group_member_state) => {
633 group_member_state.update_path_leaf_node.as_ref()
634 }
635 }
636 }
637
638 pub fn credentials_to_verify(&self) -> impl Iterator<Item = &Credential> {
640 let update_path_leaf_node_cred = if let Some(node) = self.update_path_leaf_node() {
641 vec![node.credential()]
642 } else {
643 vec![]
644 };
645
646 update_path_leaf_node_cred
647 .into_iter()
648 .chain(
649 self.queued_proposals()
650 .flat_map(|proposal: &QueuedProposal| match proposal.proposal() {
651 Proposal::Update(update_proposal) => {
652 vec![update_proposal.leaf_node().credential()].into_iter()
653 }
654 Proposal::Add(add_proposal) => {
655 vec![add_proposal.key_package().leaf_node().credential()].into_iter()
656 }
657 Proposal::GroupContextExtensions(gce_proposal) => gce_proposal
658 .extensions()
659 .iter()
660 .flat_map(|extension| {
661 match extension {
662 Extension::ExternalSenders(external_senders) => {
663 external_senders
664 .iter()
665 .map(|external_sender| external_sender.credential())
666 .collect()
667 }
668 _ => vec![],
669 }
670 .into_iter()
671 })
672 .collect::<Vec<_>>()
679 .into_iter(),
680 _ => vec![].into_iter(),
681 }),
682 )
683 }
684
685 pub fn self_removed(&self) -> bool {
688 matches!(self.state, StagedCommitState::PublicState(_))
689 }
690
691 pub fn group_context(&self) -> &GroupContext {
693 match self.state {
694 StagedCommitState::PublicState(ref ps) => ps.staged_diff().group_context(),
695 StagedCommitState::GroupMember(ref gm) => gm.group_context(),
696 }
697 }
698 pub(crate) fn into_state(self) -> StagedCommitState {
700 self.state
701 }
702
703 pub fn epoch_authenticator(&self) -> Option<&EpochAuthenticator> {
707 if let StagedCommitState::GroupMember(ref gm) = self.state {
708 Some(gm.group_epoch_secrets.epoch_authenticator())
709 } else {
710 None
711 }
712 }
713
714 #[cfg(feature = "extensions-draft-08")]
715 pub(crate) fn safe_export_secret(
716 &mut self,
717 crypto: &impl OpenMlsCrypto,
718 component_id: u16,
719 ) -> Result<Vec<u8>, StagedSafeExportSecretError> {
720 let ciphersuite = self.group_context().ciphersuite();
721 let StagedCommitState::GroupMember(ref mut staged_commit) = self.state else {
722 return Err(StagedSafeExportSecretError::NotGroupMember);
723 };
724 let Some(application_export_tree) = staged_commit.application_export_tree.as_mut() else {
725 return Err(StagedSafeExportSecretError::Unsupported);
726 };
727 let secret =
728 application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
729 Ok(secret.as_slice().to_vec())
730 }
731}
732
733#[derive(Debug, Serialize, Deserialize)]
735#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
736pub(crate) struct MemberStagedCommitState {
737 group_epoch_secrets: GroupEpochSecrets,
738 message_secrets: MessageSecrets,
739 staged_diff: StagedPublicGroupDiff,
740 new_keypairs: Vec<EncryptionKeyPair>,
741 new_leaf_keypair_option: Option<EncryptionKeyPair>,
742 update_path_leaf_node: Option<LeafNode>,
743 #[cfg(feature = "extensions-draft-08")]
744 #[serde(default)]
745 application_export_tree: Option<ApplicationExportTree>,
748}
749
750impl MemberStagedCommitState {
751 pub(crate) fn new(
752 group_epoch_secrets: GroupEpochSecrets,
753 message_secrets: MessageSecrets,
754 staged_diff: StagedPublicGroupDiff,
755 new_keypairs: Vec<EncryptionKeyPair>,
756 new_leaf_keypair_option: Option<EncryptionKeyPair>,
757 update_path_leaf_node: Option<LeafNode>,
758 #[cfg(feature = "extensions-draft-08")] application_export_tree: ApplicationExportTree,
759 ) -> Self {
760 Self {
761 group_epoch_secrets,
762 message_secrets,
763 staged_diff,
764 new_keypairs,
765 new_leaf_keypair_option,
766 update_path_leaf_node,
767 #[cfg(feature = "extensions-draft-08")]
768 application_export_tree: Some(application_export_tree),
769 }
770 }
771
772 pub(crate) fn group_context(&self) -> &GroupContext {
774 self.staged_diff.group_context()
775 }
776}