openmls/group/public_group/
staged_commit.rs1use super::{super::errors::*, diff::apply_proposals::ApplyProposalsValues, *};
2use crate::{
3 framing::{mls_auth_content::AuthenticatedContent, mls_content::FramedContentBody, Sender},
4 group::{
5 mls_group::staged_commit::StagedCommitState, proposal_store::ProposalQueue, StagedCommit,
6 },
7 messages::{
8 proposals::{ProposalOrRef, ProposalType},
9 Commit,
10 },
11 treesync::errors::LeafNodeValidationError,
12};
13
14#[cfg(feature = "extensions-draft")]
15use crate::prelude::processing::AppDataUpdates;
16
17#[derive(Debug, Serialize, Deserialize)]
18#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
19pub struct PublicStagedCommitState {
20 pub(crate) staged_diff: StagedPublicGroupDiff,
21 pub(super) update_path_leaf_node: Option<LeafNode>,
22}
23
24impl PublicStagedCommitState {
25 pub fn new(
26 staged_diff: StagedPublicGroupDiff,
27 update_path_leaf_node: Option<LeafNode>,
28 ) -> Self {
29 Self {
30 staged_diff,
31 update_path_leaf_node,
32 }
33 }
34
35 pub(crate) fn into_staged_diff(self) -> StagedPublicGroupDiff {
36 self.staged_diff
37 }
38
39 pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
40 self.update_path_leaf_node.as_ref()
41 }
42
43 pub fn staged_diff(&self) -> &StagedPublicGroupDiff {
44 &self.staged_diff
45 }
46}
47
48impl PublicGroup {
49 pub(crate) fn validate_commit<'a>(
50 &self,
51 mls_content: &'a AuthenticatedContent,
52 crypto: &impl OpenMlsCrypto,
53 lifetime_policy: LeafNodeLifetimePolicy,
54 ) -> Result<(&'a Commit, ProposalQueue, LeafNodeIndex), StageCommitError> {
55 let ciphersuite = self.ciphersuite();
56
57 if mls_content.epoch() != self.group_context().epoch() {
60 log::error!(
61 "Epoch mismatch. Got {:?}, expected {:?}",
62 mls_content.epoch(),
63 self.group_context().epoch()
64 );
65 return Err(StageCommitError::EpochMismatch);
66 }
67
68 let commit = match mls_content.content() {
70 FramedContentBody::Commit(commit) => commit,
71 _ => return Err(StageCommitError::WrongPlaintextContentType),
72 };
73
74 let sender = mls_content.sender();
75
76 if sender == &Sender::NewMemberCommit {
77 let Some(path) = &commit.path else {
80 return Err(ExternalCommitValidationError::NoPath.into());
81 };
82
83 let leaf_nodes_supports_group_context_extensions = path
87 .leaf_node()
88 .capabilities()
89 .contains_extensions(self.group_context().extensions());
90
91 if !leaf_nodes_supports_group_context_extensions {
92 return Err(
93 ExternalCommitValidationError::UnsupportedGroupContextExtensions.into(),
94 );
95 }
96
97 if commit.proposals.iter().any(|proposal| {
101 let ProposalOrRef::Reference(proposal_ref) = proposal else {
102 return false;
103 };
104 !self.proposal_store.proposals().any(|p| {
107 p.proposal_reference_ref() == proposal_ref.as_ref()
108 && p.proposal().is_type(ProposalType::SelfRemove)
109 })
110 }) {
111 return Err(ExternalCommitValidationError::ReferencedProposal.into());
112 }
113
114 let number_of_remove_proposals = commit
115 .proposals
116 .iter()
117 .filter(|prop| prop.as_proposal().filter(|p| p.is_remove()).is_some())
118 .count();
119
120 if number_of_remove_proposals > 1 {
122 return Err(ExternalCommitValidationError::MultipleExternalInitProposals.into());
123 }
124 }
125
126 let proposal_queue = ProposalQueue::from_committed_proposals(
130 ciphersuite,
131 crypto,
132 commit.proposals.as_slice().to_vec(),
133 self.proposal_store(),
134 sender,
135 )
136 .map_err(|e| {
137 log::error!("Error building the proposal queue for the commit ({e:?})");
138 match e {
139 FromCommittedProposalsError::LibraryError(e) => StageCommitError::LibraryError(e),
140 FromCommittedProposalsError::ProposalNotFound => StageCommitError::MissingProposal,
141 FromCommittedProposalsError::SelfRemoval => StageCommitError::AttemptedSelfRemoval,
142 FromCommittedProposalsError::DuplicatePskId(psk_id) => {
143 StageCommitError::DuplicatePskId(psk_id)
144 }
145 }
146 })?;
147
148 if let Some(update_path) = &commit.path {
150 self.validate_leaf_node(update_path.leaf_node(), LeafNodeLifetimePolicy::Verify)?;
152
153 let leaf_node_supports_group_context_extensions = update_path
157 .leaf_node()
158 .capabilities()
159 .contains_extensions(self.group_context().extensions());
160
161 if !leaf_node_supports_group_context_extensions {
162 return Err(LeafNodeValidationError::UnsupportedExtensions.into());
163 }
164 }
165
166 self.validate_key_uniqueness(
177 &proposal_queue,
178 Some(commit),
179 sender,
180 commit
181 .path
182 .as_ref()
183 .map(|path| path.leaf_node().signature_key()),
184 )?;
185 self.validate_add_proposals(&proposal_queue, lifetime_policy)?;
187 self.validate_capabilities(&proposal_queue)?;
190 self.validate_remove_proposals(&proposal_queue)?;
193 self.validate_proposal_type_support(&proposal_queue)?;
196 self.validate_group_context_extensions_proposal(&proposal_queue)?;
199
200 #[cfg(feature = "extensions-draft")]
201 self.validate_app_data_update_proposals_and_group_context(&proposal_queue)?;
202
203 self.validate_pre_shared_key_proposals(&proposal_queue)?;
207
208 match sender {
209 Sender::Member(committer_leaf_index) => {
210 self.validate_update_proposals(&proposal_queue, *committer_leaf_index)?;
214
215 self.validate_no_external_init_proposals(&proposal_queue)?;
216
217 self.validate_reinit_proposals(&proposal_queue)?;
221 }
222 Sender::External(_) => {
223 return Err(StageCommitError::SenderTypeExternal);
225 }
226 Sender::NewMemberProposal => {
227 return Err(StageCommitError::SenderTypeNewMemberProposal);
229 }
230 Sender::NewMemberCommit => {
231 self.validate_external_commit(&proposal_queue)?;
235 }
236 }
237
238 let sender_index = match sender {
240 Sender::Member(leaf_index) => *leaf_index,
241 Sender::NewMemberCommit => {
242 self.leftmost_free_index(proposal_queue.queued_proposals())?
243 }
244 _ => {
245 return Err(StageCommitError::SenderTypeExternal);
246 }
247 };
248
249 Ok((commit, proposal_queue, sender_index))
250 }
251
252 fn validate_no_external_init_proposals(
255 &self,
256 proposal_queue: &ProposalQueue,
257 ) -> Result<(), ProposalValidationError> {
258 for proposal in proposal_queue.queued_proposals() {
259 if matches!(
260 proposal.proposal().proposal_type(),
261 ProposalType::ExternalInit
262 ) {
263 return Err(ProposalValidationError::ExternalInitProposalInRegularCommit);
264 }
265 }
266
267 Ok(())
268 }
269
270 pub(crate) fn validate_reinit_proposals(
277 &self,
278 proposal_queue: &ProposalQueue,
279 ) -> Result<(), ProposalValidationError> {
280 let Some(reinit) = proposal_queue.reinit_proposal() else {
281 return Ok(());
282 };
283
284 if proposal_queue.queued_proposals().count() != 1 {
286 return Err(ProposalValidationError::ReInitProposalNotAlone);
287 }
288
289 if !matches!(reinit.version(), ProtocolVersion::Mls10) {
292 return Err(ProposalValidationError::ReInitUnsupportedVersion);
293 }
294 if reinit.version() < self.group_context.protocol_version() {
295 return Err(ProposalValidationError::ReInitDowngrade);
296 }
297
298 Ok(())
299 }
300
301 pub(crate) fn stage_commit(
336 &self,
337 mls_content: &AuthenticatedContent,
338 crypto: &impl OpenMlsCrypto,
339 lifetime_policy: LeafNodeLifetimePolicy,
340 ) -> Result<StagedCommit, StageCommitError> {
341 let (commit, proposal_queue, sender_index) =
342 self.validate_commit(mls_content, crypto, lifetime_policy)?;
343
344 let staged_diff = self.stage_diff(mls_content, &proposal_queue, sender_index, crypto)?;
345 let staged_state = PublicStagedCommitState {
346 staged_diff,
347 update_path_leaf_node: commit.path.as_ref().map(|p| p.leaf_node().clone()),
348 };
349
350 let staged_commit_state = StagedCommitState::PublicState(Box::new(staged_state));
351
352 Ok(StagedCommit::new(
353 proposal_queue,
354 staged_commit_state,
355 #[cfg(feature = "virtual-clients-draft")]
356 None,
357 ))
358 }
359
360 #[cfg(feature = "extensions-draft")]
361 pub(crate) fn stage_commit_with_app_data_updates(
362 &self,
363 mls_content: &AuthenticatedContent,
364 crypto: &impl OpenMlsCrypto,
365 app_data_dict_updates: Option<AppDataUpdates>,
366 lifetime_policy: LeafNodeLifetimePolicy,
367 ) -> Result<StagedCommit, StageCommitError> {
368 let (commit, proposal_queue, sender_index) =
369 self.validate_commit(mls_content, crypto, lifetime_policy)?;
370
371 let staged_diff = self.stage_diff_with_app_data_updates(
372 mls_content,
373 &proposal_queue,
374 sender_index,
375 crypto,
376 app_data_dict_updates,
377 )?;
378 let staged_state = PublicStagedCommitState {
379 staged_diff,
380 update_path_leaf_node: commit.path.as_ref().map(|p| p.leaf_node().clone()),
381 };
382
383 let staged_commit_state = StagedCommitState::PublicState(Box::new(staged_state));
384
385 Ok(StagedCommit::new(
386 proposal_queue,
387 staged_commit_state,
388 #[cfg(feature = "virtual-clients-draft")]
389 None,
390 ))
391 }
392
393 fn stage_diff(
394 &self,
395 mls_content: &AuthenticatedContent,
396 proposal_queue: &ProposalQueue,
397 sender_index: LeafNodeIndex,
398 crypto: &impl OpenMlsCrypto,
399 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
400 let mut diff = self.empty_diff();
401
402 let apply_proposals_values = diff.apply_proposals(proposal_queue, None)?;
403
404 self.stage_diff_internal(
405 mls_content,
406 apply_proposals_values,
407 diff,
408 sender_index,
409 crypto,
410 )
411 }
412
413 #[cfg(feature = "extensions-draft")]
414 fn stage_diff_with_app_data_updates(
415 &self,
416 mls_content: &AuthenticatedContent,
417 proposal_queue: &ProposalQueue,
418 sender_index: LeafNodeIndex,
419 crypto: &impl OpenMlsCrypto,
420 app_data_dict_updates: Option<AppDataUpdates>,
421 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
422 let mut diff = self.empty_diff();
423
424 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
425 proposal_queue,
426 None,
427 app_data_dict_updates,
428 )?;
429
430 self.stage_diff_internal(
431 mls_content,
432 apply_proposals_values,
433 diff,
434 sender_index,
435 crypto,
436 )
437 }
438
439 fn stage_diff_internal(
440 &self,
441 mls_content: &AuthenticatedContent,
442 apply_proposals_values: ApplyProposalsValues,
443 mut diff: PublicGroupDiff,
444 sender_index: LeafNodeIndex,
445 crypto: &impl OpenMlsCrypto,
446 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
447 let ciphersuite = self.ciphersuite();
448
449 let commit = match mls_content.content() {
450 FramedContentBody::Commit(commit) => commit,
451 _ => return Err(StageCommitError::WrongPlaintextContentType),
452 };
453
454 if let Some(update_path) = &commit.path {
456 diff.apply_received_update_path(crypto, ciphersuite, sender_index, update_path)?;
459 } else if apply_proposals_values.path_required {
460 return Err(StageCommitError::RequiredPathNotFound);
463 };
464
465 diff.update_group_context(crypto, apply_proposals_values.extensions)?;
467
468 diff.update_confirmed_transcript_hash(crypto, mls_content)?;
470
471 let received_confirmation_tag = mls_content
472 .confirmation_tag()
473 .ok_or(StageCommitError::ConfirmationTagMissing)?;
474
475 diff.update_interim_transcript_hash(
478 ciphersuite,
479 crypto,
480 received_confirmation_tag.clone(),
481 )?;
482
483 let staged_diff = diff.into_staged_diff(crypto, ciphersuite)?;
484
485 Ok(staged_diff)
486 }
487
488 #[openmls_traits::maybe_async]
490 pub async fn merge_commit<Storage: PublicStorageProvider>(
491 &mut self,
492 storage: &Storage,
493 staged_commit: StagedCommit,
494 ) -> Result<(), MergeCommitError<Storage::Error>> {
495 match staged_commit.into_state() {
496 StagedCommitState::PublicState(staged_state) => {
497 self.merge_diff(staged_state.staged_diff);
498 }
499 StagedCommitState::GroupMember(_) => (),
500 }
501
502 self.proposal_store.empty();
503 storage
504 .clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())
505 .await
506 .map_err(MergeCommitError::StorageError)?;
507 self.store(storage)
508 .await
509 .map_err(MergeCommitError::StorageError)
510 }
511}