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-08")]
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 ) -> Result<(&'a Commit, ProposalQueue, LeafNodeIndex), StageCommitError> {
54 let ciphersuite = self.ciphersuite();
55
56 if mls_content.epoch() != self.group_context().epoch() {
59 log::error!(
60 "Epoch mismatch. Got {:?}, expected {:?}",
61 mls_content.epoch(),
62 self.group_context().epoch()
63 );
64 return Err(StageCommitError::EpochMismatch);
65 }
66
67 let commit = match mls_content.content() {
69 FramedContentBody::Commit(commit) => commit,
70 _ => return Err(StageCommitError::WrongPlaintextContentType),
71 };
72
73 let sender = mls_content.sender();
74
75 if sender == &Sender::NewMemberCommit {
76 let Some(path) = &commit.path else {
79 return Err(ExternalCommitValidationError::NoPath.into());
80 };
81
82 let leaf_nodes_supports_group_context_extensions = path
86 .leaf_node()
87 .capabilities()
88 .contains_extensions(self.group_context().extensions());
89
90 if !leaf_nodes_supports_group_context_extensions {
91 return Err(
92 ExternalCommitValidationError::UnsupportedGroupContextExtensions.into(),
93 );
94 }
95
96 if commit.proposals.iter().any(|proposal| {
100 let ProposalOrRef::Reference(proposal_ref) = proposal else {
101 return false;
102 };
103 !self.proposal_store.proposals().any(|p| {
106 p.proposal_reference_ref() == proposal_ref.as_ref()
107 && p.proposal().is_type(ProposalType::SelfRemove)
108 })
109 }) {
110 return Err(ExternalCommitValidationError::ReferencedProposal.into());
111 }
112
113 let number_of_remove_proposals = commit
114 .proposals
115 .iter()
116 .filter(|prop| prop.as_proposal().filter(|p| p.is_remove()).is_some())
117 .count();
118
119 if number_of_remove_proposals > 1 {
121 return Err(ExternalCommitValidationError::MultipleExternalInitProposals.into());
122 }
123 }
124
125 let proposal_queue = ProposalQueue::from_committed_proposals(
129 ciphersuite,
130 crypto,
131 commit.proposals.as_slice().to_vec(),
132 self.proposal_store(),
133 sender,
134 )
135 .map_err(|e| {
136 log::error!("Error building the proposal queue for the commit ({e:?})");
137 match e {
138 FromCommittedProposalsError::LibraryError(e) => StageCommitError::LibraryError(e),
139 FromCommittedProposalsError::ProposalNotFound => StageCommitError::MissingProposal,
140 FromCommittedProposalsError::SelfRemoval => StageCommitError::AttemptedSelfRemoval,
141 FromCommittedProposalsError::DuplicatePskId(psk_id) => {
142 StageCommitError::DuplicatePskId(psk_id)
143 }
144 }
145 })?;
146
147 if let Some(update_path) = &commit.path {
149 self.validate_leaf_node(update_path.leaf_node())?;
150
151 let leaf_node_supports_group_context_extensions = update_path
155 .leaf_node()
156 .capabilities()
157 .contains_extensions(self.group_context().extensions());
158
159 if !leaf_node_supports_group_context_extensions {
160 return Err(LeafNodeValidationError::UnsupportedExtensions.into());
161 }
162 }
163
164 self.validate_key_uniqueness(&proposal_queue, Some(commit))?;
175 self.validate_add_proposals(&proposal_queue)?;
177 self.validate_capabilities(&proposal_queue)?;
180 self.validate_remove_proposals(&proposal_queue)?;
183 self.validate_proposal_type_support(&proposal_queue)?;
186 self.validate_group_context_extensions_proposal(&proposal_queue)?;
189
190 #[cfg(feature = "extensions-draft-08")]
191 self.validate_app_data_update_proposals_and_group_context(&proposal_queue)?;
192
193 self.validate_pre_shared_key_proposals(&proposal_queue)?;
197
198 match sender {
199 Sender::Member(committer_leaf_index) => {
200 self.validate_update_proposals(&proposal_queue, *committer_leaf_index)?;
204
205 self.validate_no_external_init_proposals(&proposal_queue)?;
206 }
207 Sender::External(_) => {
208 return Err(StageCommitError::SenderTypeExternal);
210 }
211 Sender::NewMemberProposal => {
212 return Err(StageCommitError::SenderTypeNewMemberProposal);
214 }
215 Sender::NewMemberCommit => {
216 self.validate_external_commit(&proposal_queue)?;
220 }
221 }
222
223 let sender_index = match sender {
225 Sender::Member(leaf_index) => *leaf_index,
226 Sender::NewMemberCommit => {
227 self.leftmost_free_index(proposal_queue.queued_proposals())?
228 }
229 _ => {
230 return Err(StageCommitError::SenderTypeExternal);
231 }
232 };
233
234 Ok((commit, proposal_queue, sender_index))
235 }
236
237 fn validate_no_external_init_proposals(
240 &self,
241 proposal_queue: &ProposalQueue,
242 ) -> Result<(), ProposalValidationError> {
243 for proposal in proposal_queue.queued_proposals() {
244 if matches!(
245 proposal.proposal().proposal_type(),
246 ProposalType::ExternalInit
247 ) {
248 return Err(ProposalValidationError::ExternalInitProposalInRegularCommit);
249 }
250 }
251
252 Ok(())
253 }
254
255 pub(crate) fn stage_commit(
292 &self,
293 mls_content: &AuthenticatedContent,
294 crypto: &impl OpenMlsCrypto,
295 ) -> Result<StagedCommit, StageCommitError> {
296 let (commit, proposal_queue, sender_index) = self.validate_commit(mls_content, crypto)?;
297
298 let staged_diff = self.stage_diff(mls_content, &proposal_queue, sender_index, crypto)?;
299 let staged_state = PublicStagedCommitState {
300 staged_diff,
301 update_path_leaf_node: commit.path.as_ref().map(|p| p.leaf_node().clone()),
302 };
303
304 let staged_commit_state = StagedCommitState::PublicState(Box::new(staged_state));
305
306 Ok(StagedCommit::new(proposal_queue, staged_commit_state))
307 }
308
309 #[cfg(feature = "extensions-draft-08")]
310 pub(crate) fn stage_commit_with_app_data_updates(
311 &self,
312 mls_content: &AuthenticatedContent,
313 crypto: &impl OpenMlsCrypto,
314 app_data_dict_updates: Option<AppDataUpdates>,
315 ) -> Result<StagedCommit, StageCommitError> {
316 let (commit, proposal_queue, sender_index) = self.validate_commit(mls_content, crypto)?;
317
318 let staged_diff = self.stage_diff_with_app_data_updates(
319 mls_content,
320 &proposal_queue,
321 sender_index,
322 crypto,
323 app_data_dict_updates,
324 )?;
325 let staged_state = PublicStagedCommitState {
326 staged_diff,
327 update_path_leaf_node: commit.path.as_ref().map(|p| p.leaf_node().clone()),
328 };
329
330 let staged_commit_state = StagedCommitState::PublicState(Box::new(staged_state));
331
332 Ok(StagedCommit::new(proposal_queue, staged_commit_state))
333 }
334
335 fn stage_diff(
336 &self,
337 mls_content: &AuthenticatedContent,
338 proposal_queue: &ProposalQueue,
339 sender_index: LeafNodeIndex,
340 crypto: &impl OpenMlsCrypto,
341 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
342 let mut diff = self.empty_diff();
343
344 let apply_proposals_values = diff.apply_proposals(proposal_queue, None)?;
345
346 self.stage_diff_internal(
347 mls_content,
348 apply_proposals_values,
349 diff,
350 sender_index,
351 crypto,
352 )
353 }
354
355 #[cfg(feature = "extensions-draft-08")]
356 fn stage_diff_with_app_data_updates(
357 &self,
358 mls_content: &AuthenticatedContent,
359 proposal_queue: &ProposalQueue,
360 sender_index: LeafNodeIndex,
361 crypto: &impl OpenMlsCrypto,
362 app_data_dict_updates: Option<AppDataUpdates>,
363 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
364 let mut diff = self.empty_diff();
365
366 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
367 proposal_queue,
368 None,
369 app_data_dict_updates,
370 )?;
371
372 self.stage_diff_internal(
373 mls_content,
374 apply_proposals_values,
375 diff,
376 sender_index,
377 crypto,
378 )
379 }
380
381 fn stage_diff_internal(
382 &self,
383 mls_content: &AuthenticatedContent,
384 apply_proposals_values: ApplyProposalsValues,
385 mut diff: PublicGroupDiff,
386 sender_index: LeafNodeIndex,
387 crypto: &impl OpenMlsCrypto,
388 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
389 let ciphersuite = self.ciphersuite();
390
391 let commit = match mls_content.content() {
392 FramedContentBody::Commit(commit) => commit,
393 _ => return Err(StageCommitError::WrongPlaintextContentType),
394 };
395
396 if let Some(update_path) = &commit.path {
398 diff.apply_received_update_path(crypto, ciphersuite, sender_index, update_path)?;
401 } else if apply_proposals_values.path_required {
402 return Err(StageCommitError::RequiredPathNotFound);
405 };
406
407 diff.update_group_context(crypto, apply_proposals_values.extensions)?;
409
410 diff.update_confirmed_transcript_hash(crypto, mls_content)?;
412
413 let received_confirmation_tag = mls_content
414 .confirmation_tag()
415 .ok_or(StageCommitError::ConfirmationTagMissing)?;
416
417 diff.update_interim_transcript_hash(
420 ciphersuite,
421 crypto,
422 received_confirmation_tag.clone(),
423 )?;
424
425 let staged_diff = diff.into_staged_diff(crypto, ciphersuite)?;
426
427 Ok(staged_diff)
428 }
429
430 pub fn merge_commit<Storage: PublicStorageProvider>(
432 &mut self,
433 storage: &Storage,
434 staged_commit: StagedCommit,
435 ) -> Result<(), MergeCommitError<Storage::Error>> {
436 match staged_commit.into_state() {
437 StagedCommitState::PublicState(staged_state) => {
438 self.merge_diff(staged_state.staged_diff);
439 }
440 StagedCommitState::GroupMember(_) => (),
441 }
442
443 self.proposal_store.empty();
444 storage
445 .clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())
446 .map_err(MergeCommitError::StorageError)?;
447 self.store(storage).map_err(MergeCommitError::StorageError)
448 }
449}