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(
307 proposal_queue,
308 staged_commit_state,
309 #[cfg(feature = "virtual-clients-draft")]
310 None,
311 ))
312 }
313
314 #[cfg(feature = "extensions-draft-08")]
315 pub(crate) fn stage_commit_with_app_data_updates(
316 &self,
317 mls_content: &AuthenticatedContent,
318 crypto: &impl OpenMlsCrypto,
319 app_data_dict_updates: Option<AppDataUpdates>,
320 ) -> Result<StagedCommit, StageCommitError> {
321 let (commit, proposal_queue, sender_index) = self.validate_commit(mls_content, crypto)?;
322
323 let staged_diff = self.stage_diff_with_app_data_updates(
324 mls_content,
325 &proposal_queue,
326 sender_index,
327 crypto,
328 app_data_dict_updates,
329 )?;
330 let staged_state = PublicStagedCommitState {
331 staged_diff,
332 update_path_leaf_node: commit.path.as_ref().map(|p| p.leaf_node().clone()),
333 };
334
335 let staged_commit_state = StagedCommitState::PublicState(Box::new(staged_state));
336
337 Ok(StagedCommit::new(
338 proposal_queue,
339 staged_commit_state,
340 #[cfg(feature = "virtual-clients-draft")]
341 None,
342 ))
343 }
344
345 fn stage_diff(
346 &self,
347 mls_content: &AuthenticatedContent,
348 proposal_queue: &ProposalQueue,
349 sender_index: LeafNodeIndex,
350 crypto: &impl OpenMlsCrypto,
351 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
352 let mut diff = self.empty_diff();
353
354 let apply_proposals_values = diff.apply_proposals(proposal_queue, None)?;
355
356 self.stage_diff_internal(
357 mls_content,
358 apply_proposals_values,
359 diff,
360 sender_index,
361 crypto,
362 )
363 }
364
365 #[cfg(feature = "extensions-draft-08")]
366 fn stage_diff_with_app_data_updates(
367 &self,
368 mls_content: &AuthenticatedContent,
369 proposal_queue: &ProposalQueue,
370 sender_index: LeafNodeIndex,
371 crypto: &impl OpenMlsCrypto,
372 app_data_dict_updates: Option<AppDataUpdates>,
373 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
374 let mut diff = self.empty_diff();
375
376 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
377 proposal_queue,
378 None,
379 app_data_dict_updates,
380 )?;
381
382 self.stage_diff_internal(
383 mls_content,
384 apply_proposals_values,
385 diff,
386 sender_index,
387 crypto,
388 )
389 }
390
391 fn stage_diff_internal(
392 &self,
393 mls_content: &AuthenticatedContent,
394 apply_proposals_values: ApplyProposalsValues,
395 mut diff: PublicGroupDiff,
396 sender_index: LeafNodeIndex,
397 crypto: &impl OpenMlsCrypto,
398 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
399 let ciphersuite = self.ciphersuite();
400
401 let commit = match mls_content.content() {
402 FramedContentBody::Commit(commit) => commit,
403 _ => return Err(StageCommitError::WrongPlaintextContentType),
404 };
405
406 if let Some(update_path) = &commit.path {
408 diff.apply_received_update_path(crypto, ciphersuite, sender_index, update_path)?;
411 } else if apply_proposals_values.path_required {
412 return Err(StageCommitError::RequiredPathNotFound);
415 };
416
417 diff.update_group_context(crypto, apply_proposals_values.extensions)?;
419
420 diff.update_confirmed_transcript_hash(crypto, mls_content)?;
422
423 let received_confirmation_tag = mls_content
424 .confirmation_tag()
425 .ok_or(StageCommitError::ConfirmationTagMissing)?;
426
427 diff.update_interim_transcript_hash(
430 ciphersuite,
431 crypto,
432 received_confirmation_tag.clone(),
433 )?;
434
435 let staged_diff = diff.into_staged_diff(crypto, ciphersuite)?;
436
437 Ok(staged_diff)
438 }
439
440 pub fn merge_commit<Storage: PublicStorageProvider>(
442 &mut self,
443 storage: &Storage,
444 staged_commit: StagedCommit,
445 ) -> Result<(), MergeCommitError<Storage::Error>> {
446 match staged_commit.into_state() {
447 StagedCommitState::PublicState(staged_state) => {
448 self.merge_diff(staged_state.staged_diff);
449 }
450 StagedCommitState::GroupMember(_) => (),
451 }
452
453 self.proposal_store.empty();
454 storage
455 .clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())
456 .map_err(MergeCommitError::StorageError)?;
457 self.store(storage).map_err(MergeCommitError::StorageError)
458 }
459}