openmls/group/public_group/
staged_commit.rs1use super::{super::errors::*, *};
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};
12
13#[derive(Debug, Serialize, Deserialize)]
14#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
15pub struct PublicStagedCommitState {
16 pub(super) staged_diff: StagedPublicGroupDiff,
17 pub(super) update_path_leaf_node: Option<LeafNode>,
18}
19
20impl PublicStagedCommitState {
21 pub fn new(
22 staged_diff: StagedPublicGroupDiff,
23 update_path_leaf_node: Option<LeafNode>,
24 ) -> Self {
25 Self {
26 staged_diff,
27 update_path_leaf_node,
28 }
29 }
30
31 pub(crate) fn into_staged_diff(self) -> StagedPublicGroupDiff {
32 self.staged_diff
33 }
34
35 pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
36 self.update_path_leaf_node.as_ref()
37 }
38
39 pub fn staged_diff(&self) -> &StagedPublicGroupDiff {
40 &self.staged_diff
41 }
42}
43
44impl PublicGroup {
45 pub(crate) fn validate_commit<'a>(
46 &self,
47 mls_content: &'a AuthenticatedContent,
48 crypto: &impl OpenMlsCrypto,
49 ) -> Result<(&'a Commit, ProposalQueue, LeafNodeIndex), StageCommitError> {
50 let ciphersuite = self.ciphersuite();
51
52 if mls_content.epoch() != self.group_context().epoch() {
55 log::error!(
56 "Epoch mismatch. Got {:?}, expected {:?}",
57 mls_content.epoch(),
58 self.group_context().epoch()
59 );
60 return Err(StageCommitError::EpochMismatch);
61 }
62
63 let commit = match mls_content.content() {
65 FramedContentBody::Commit(commit) => commit,
66 _ => return Err(StageCommitError::WrongPlaintextContentType),
67 };
68
69 let sender = mls_content.sender();
70
71 if sender == &Sender::NewMemberCommit {
72 if commit.path.is_none() {
75 return Err(ExternalCommitValidationError::NoPath.into());
76 }
77
78 if commit.proposals.iter().any(|proposal| {
82 let ProposalOrRef::Reference(proposal_ref) = proposal else {
83 return false;
84 };
85 !self.proposal_store.proposals().any(|p| {
88 p.proposal_reference_ref() == proposal_ref.as_ref()
89 && p.proposal().is_type(ProposalType::SelfRemove)
90 })
91 }) {
92 return Err(ExternalCommitValidationError::ReferencedProposal.into());
93 }
94
95 let number_of_remove_proposals = commit
96 .proposals
97 .iter()
98 .filter(|prop| prop.as_proposal().filter(|p| p.is_remove()).is_some())
99 .count();
100
101 if number_of_remove_proposals > 1 {
103 return Err(ExternalCommitValidationError::MultipleExternalInitProposals.into());
104 }
105 }
106
107 let proposal_queue = ProposalQueue::from_committed_proposals(
111 ciphersuite,
112 crypto,
113 commit.proposals.as_slice().to_vec(),
114 self.proposal_store(),
115 sender,
116 )
117 .map_err(|e| {
118 log::error!("Error building the proposal queue for the commit ({e:?})");
119 match e {
120 FromCommittedProposalsError::LibraryError(e) => StageCommitError::LibraryError(e),
121 FromCommittedProposalsError::ProposalNotFound => StageCommitError::MissingProposal,
122 FromCommittedProposalsError::SelfRemoval => StageCommitError::AttemptedSelfRemoval,
123 }
124 })?;
125
126 if let Some(update_path) = &commit.path {
128 self.validate_leaf_node(update_path.leaf_node())?;
129 }
130
131 self.validate_key_uniqueness(&proposal_queue, Some(commit))?;
139 self.validate_add_proposals(&proposal_queue)?;
141 self.validate_capabilities(&proposal_queue)?;
144 self.validate_remove_proposals(&proposal_queue)?;
147 self.validate_proposal_type_support(&proposal_queue)?;
150 self.validate_group_context_extensions_proposal(&proposal_queue)?;
153 self.validate_pre_shared_key_proposals(&proposal_queue)?;
157
158 match sender {
159 Sender::Member(leaf_index) => {
160 self.validate_update_proposals(&proposal_queue, *leaf_index)?;
164
165 self.validate_no_external_init_proposals(&proposal_queue)?;
166 }
167 Sender::External(_) => {
168 return Err(StageCommitError::SenderTypeExternal);
170 }
171 Sender::NewMemberProposal => {
172 return Err(StageCommitError::SenderTypeNewMemberProposal);
174 }
175 Sender::NewMemberCommit => {
176 self.validate_external_commit(&proposal_queue)?;
180 }
181 }
182
183 let sender_index = match sender {
185 Sender::Member(leaf_index) => *leaf_index,
186 Sender::NewMemberCommit => {
187 self.leftmost_free_index(proposal_queue.queued_proposals())?
188 }
189 _ => {
190 return Err(StageCommitError::SenderTypeExternal);
191 }
192 };
193
194 Ok((commit, proposal_queue, sender_index))
195 }
196
197 fn validate_no_external_init_proposals(
200 &self,
201 proposal_queue: &ProposalQueue,
202 ) -> Result<(), ProposalValidationError> {
203 for proposal in proposal_queue.queued_proposals() {
204 if matches!(
205 proposal.proposal().proposal_type(),
206 ProposalType::ExternalInit
207 ) {
208 return Err(ProposalValidationError::ExternalInitProposalInRegularCommit);
209 }
210 }
211
212 Ok(())
213 }
214
215 pub(crate) fn stage_commit(
252 &self,
253 mls_content: &AuthenticatedContent,
254 crypto: &impl OpenMlsCrypto,
255 ) -> Result<StagedCommit, StageCommitError> {
256 let (commit, proposal_queue, sender_index) = self.validate_commit(mls_content, crypto)?;
257
258 let staged_diff = self.stage_diff(mls_content, &proposal_queue, sender_index, crypto)?;
259 let staged_state = PublicStagedCommitState {
260 staged_diff,
261 update_path_leaf_node: commit.path.as_ref().map(|p| p.leaf_node().clone()),
262 };
263
264 let staged_commit_state = StagedCommitState::PublicState(Box::new(staged_state));
265
266 Ok(StagedCommit::new(proposal_queue, staged_commit_state))
267 }
268
269 fn stage_diff(
270 &self,
271 mls_content: &AuthenticatedContent,
272 proposal_queue: &ProposalQueue,
273 sender_index: LeafNodeIndex,
274 crypto: &impl OpenMlsCrypto,
275 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
276 let ciphersuite = self.ciphersuite();
277 let mut diff = self.empty_diff();
278
279 let apply_proposals_values = diff.apply_proposals(proposal_queue, None)?;
280
281 let commit = match mls_content.content() {
282 FramedContentBody::Commit(commit) => commit,
283 _ => return Err(StageCommitError::WrongPlaintextContentType),
284 };
285
286 if let Some(update_path) = &commit.path {
288 diff.apply_received_update_path(crypto, ciphersuite, sender_index, update_path)?;
291 } else if apply_proposals_values.path_required {
292 return Err(StageCommitError::RequiredPathNotFound);
295 };
296
297 diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
299
300 diff.update_confirmed_transcript_hash(crypto, mls_content)?;
302
303 let received_confirmation_tag = mls_content
304 .confirmation_tag()
305 .ok_or(StageCommitError::ConfirmationTagMissing)?;
306
307 diff.update_interim_transcript_hash(
310 ciphersuite,
311 crypto,
312 received_confirmation_tag.clone(),
313 )?;
314
315 let staged_diff = diff.into_staged_diff(crypto, ciphersuite)?;
316
317 Ok(staged_diff)
318 }
319
320 pub fn merge_commit<Storage: PublicStorageProvider>(
322 &mut self,
323 storage: &Storage,
324 staged_commit: StagedCommit,
325 ) -> Result<(), MergeCommitError<Storage::Error>> {
326 match staged_commit.into_state() {
327 StagedCommitState::PublicState(staged_state) => {
328 self.merge_diff(staged_state.staged_diff);
329 }
330 StagedCommitState::GroupMember(_) => (),
331 }
332
333 self.proposal_store.empty();
334 storage
335 .clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())
336 .map_err(MergeCommitError::StorageError)?;
337 self.store(storage).map_err(MergeCommitError::StorageError)
338 }
339}