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
81 .proposals
82 .iter()
83 .any(|proposal| matches!(proposal, ProposalOrRef::Reference(_)))
84 {
85 return Err(ExternalCommitValidationError::ReferencedProposal.into());
86 }
87
88 let number_of_remove_proposals = commit
89 .proposals
90 .iter()
91 .filter(|prop| matches!(prop, ProposalOrRef::Proposal(Proposal::Remove(_))))
92 .count();
93
94 if number_of_remove_proposals > 1 {
96 return Err(ExternalCommitValidationError::MultipleExternalInitProposals.into());
97 }
98 }
99
100 let proposal_queue = ProposalQueue::from_committed_proposals(
104 ciphersuite,
105 crypto,
106 commit.proposals.as_slice().to_vec(),
107 self.proposal_store(),
108 sender,
109 )
110 .map_err(|e| {
111 log::error!("Error building the proposal queue for the commit ({e:?})");
112 match e {
113 FromCommittedProposalsError::LibraryError(e) => StageCommitError::LibraryError(e),
114 FromCommittedProposalsError::ProposalNotFound => StageCommitError::MissingProposal,
115 FromCommittedProposalsError::SelfRemoval => StageCommitError::AttemptedSelfRemoval,
116 }
117 })?;
118
119 if let Some(update_path) = &commit.path {
121 self.validate_leaf_node(update_path.leaf_node())?;
122 }
123
124 self.validate_key_uniqueness(&proposal_queue, Some(commit))?;
132 self.validate_add_proposals(&proposal_queue)?;
134 self.validate_capabilities(&proposal_queue)?;
137 self.validate_remove_proposals(&proposal_queue)?;
140 self.validate_proposal_type_support(&proposal_queue)?;
143 self.validate_group_context_extensions_proposal(&proposal_queue)?;
146 self.validate_pre_shared_key_proposals(&proposal_queue)?;
150
151 match sender {
152 Sender::Member(leaf_index) => {
153 self.validate_update_proposals(&proposal_queue, *leaf_index)?;
157
158 self.validate_no_external_init_proposals(&proposal_queue)?;
159 }
160 Sender::External(_) => {
161 return Err(StageCommitError::SenderTypeExternal);
163 }
164 Sender::NewMemberProposal => {
165 return Err(StageCommitError::SenderTypeNewMemberProposal);
167 }
168 Sender::NewMemberCommit => {
169 self.validate_external_commit(&proposal_queue)?;
173 }
174 }
175
176 let sender_index = match sender {
178 Sender::Member(leaf_index) => *leaf_index,
179 Sender::NewMemberCommit => {
180 self.leftmost_free_index(iter::empty(), proposal_queue.queued_proposals())?
181 }
182 _ => {
183 return Err(StageCommitError::SenderTypeExternal);
184 }
185 };
186
187 Ok((commit, proposal_queue, sender_index))
188 }
189
190 fn validate_no_external_init_proposals(
193 &self,
194 proposal_queue: &ProposalQueue,
195 ) -> Result<(), ProposalValidationError> {
196 for proposal in proposal_queue.queued_proposals() {
197 if matches!(
198 proposal.proposal().proposal_type(),
199 ProposalType::ExternalInit
200 ) {
201 return Err(ProposalValidationError::ExternalInitProposalInRegularCommit);
202 }
203 }
204
205 Ok(())
206 }
207
208 pub(crate) fn stage_commit(
245 &self,
246 mls_content: &AuthenticatedContent,
247 crypto: &impl OpenMlsCrypto,
248 ) -> Result<StagedCommit, StageCommitError> {
249 let (commit, proposal_queue, sender_index) = self.validate_commit(mls_content, crypto)?;
250
251 let staged_diff = self.stage_diff(mls_content, &proposal_queue, sender_index, crypto)?;
252 let staged_state = PublicStagedCommitState {
253 staged_diff,
254 update_path_leaf_node: commit.path.as_ref().map(|p| p.leaf_node().clone()),
255 };
256
257 let staged_commit_state = StagedCommitState::PublicState(Box::new(staged_state));
258
259 Ok(StagedCommit::new(proposal_queue, staged_commit_state))
260 }
261
262 fn stage_diff(
263 &self,
264 mls_content: &AuthenticatedContent,
265 proposal_queue: &ProposalQueue,
266 sender_index: LeafNodeIndex,
267 crypto: &impl OpenMlsCrypto,
268 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
269 let ciphersuite = self.ciphersuite();
270 let mut diff = self.empty_diff();
271
272 let apply_proposals_values = diff.apply_proposals(proposal_queue, None)?;
273
274 let commit = match mls_content.content() {
275 FramedContentBody::Commit(commit) => commit,
276 _ => return Err(StageCommitError::WrongPlaintextContentType),
277 };
278
279 if let Some(update_path) = &commit.path {
281 diff.apply_received_update_path(crypto, ciphersuite, sender_index, update_path)?;
284 } else if apply_proposals_values.path_required {
285 return Err(StageCommitError::RequiredPathNotFound);
288 };
289
290 diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
292
293 diff.update_confirmed_transcript_hash(crypto, mls_content)?;
295
296 let received_confirmation_tag = mls_content
297 .confirmation_tag()
298 .ok_or(StageCommitError::ConfirmationTagMissing)?;
299
300 diff.update_interim_transcript_hash(
303 ciphersuite,
304 crypto,
305 received_confirmation_tag.clone(),
306 )?;
307
308 let staged_diff = diff.into_staged_diff(crypto, ciphersuite)?;
309
310 Ok(staged_diff)
311 }
312
313 pub fn merge_commit<Storage: PublicStorageProvider>(
315 &mut self,
316 storage: &Storage,
317 staged_commit: StagedCommit,
318 ) -> Result<(), MergeCommitError<Storage::Error>> {
319 match staged_commit.into_state() {
320 StagedCommitState::PublicState(staged_state) => {
321 self.merge_diff(staged_state.staged_diff);
322 }
323 StagedCommitState::GroupMember(_) => (),
324 }
325
326 self.proposal_store.empty();
327 storage
328 .clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())
329 .map_err(MergeCommitError::StorageError)?;
330 self.store(storage).map_err(MergeCommitError::StorageError)
331 }
332}