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