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::{proposals::ProposalOrRef, Commit},
8};
9
10#[derive(Debug, Serialize, Deserialize)]
11#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
12pub struct PublicStagedCommitState {
13 pub(super) staged_diff: StagedPublicGroupDiff,
14 pub(super) update_path_leaf_node: Option<LeafNode>,
15}
16
17impl PublicStagedCommitState {
18 pub fn new(
19 staged_diff: StagedPublicGroupDiff,
20 update_path_leaf_node: Option<LeafNode>,
21 ) -> Self {
22 Self {
23 staged_diff,
24 update_path_leaf_node,
25 }
26 }
27
28 pub(crate) fn into_staged_diff(self) -> StagedPublicGroupDiff {
29 self.staged_diff
30 }
31
32 pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
33 self.update_path_leaf_node.as_ref()
34 }
35
36 pub fn staged_diff(&self) -> &StagedPublicGroupDiff {
37 &self.staged_diff
38 }
39}
40
41impl PublicGroup {
42 pub(crate) fn validate_commit<'a>(
43 &self,
44 mls_content: &'a AuthenticatedContent,
45 crypto: &impl OpenMlsCrypto,
46 ) -> Result<(&'a Commit, ProposalQueue, LeafNodeIndex), StageCommitError> {
47 let ciphersuite = self.ciphersuite();
48
49 if mls_content.epoch() != self.group_context().epoch() {
52 log::error!(
53 "Epoch mismatch. Got {:?}, expected {:?}",
54 mls_content.epoch(),
55 self.group_context().epoch()
56 );
57 return Err(StageCommitError::EpochMismatch);
58 }
59
60 let commit = match mls_content.content() {
62 FramedContentBody::Commit(commit) => commit,
63 _ => return Err(StageCommitError::WrongPlaintextContentType),
64 };
65
66 let sender = mls_content.sender();
67
68 if sender == &Sender::NewMemberCommit {
69 if commit.path.is_none() {
72 return Err(ExternalCommitValidationError::NoPath.into());
73 }
74
75 if commit
78 .proposals
79 .iter()
80 .any(|proposal| matches!(proposal, ProposalOrRef::Reference(_)))
81 {
82 return Err(ExternalCommitValidationError::ReferencedProposal.into());
83 }
84
85 let number_of_remove_proposals = commit
86 .proposals
87 .iter()
88 .filter(|prop| matches!(prop, ProposalOrRef::Proposal(Proposal::Remove(_))))
89 .count();
90
91 if number_of_remove_proposals > 1 {
93 return Err(ExternalCommitValidationError::MultipleExternalInitProposals.into());
94 }
95 }
96
97 let proposal_queue = ProposalQueue::from_committed_proposals(
101 ciphersuite,
102 crypto,
103 commit.proposals.as_slice().to_vec(),
104 self.proposal_store(),
105 sender,
106 )
107 .map_err(|e| {
108 log::error!("Error building the proposal queue for the commit ({e:?})");
109 match e {
110 FromCommittedProposalsError::LibraryError(e) => StageCommitError::LibraryError(e),
111 FromCommittedProposalsError::ProposalNotFound => StageCommitError::MissingProposal,
112 FromCommittedProposalsError::SelfRemoval => StageCommitError::AttemptedSelfRemoval,
113 }
114 })?;
115
116 if let Some(update_path) = &commit.path {
118 self.validate_leaf_node(update_path.leaf_node())?;
119 }
120
121 self.validate_key_uniqueness(&proposal_queue, Some(commit))?;
129 self.validate_add_proposals(&proposal_queue)?;
131 self.validate_capabilities(&proposal_queue)?;
134 self.validate_remove_proposals(&proposal_queue)?;
137 self.validate_proposal_type_support(&proposal_queue)?;
140 self.validate_group_context_extensions_proposal(&proposal_queue)?;
143 self.validate_pre_shared_key_proposals(&proposal_queue)?;
147
148 match sender {
149 Sender::Member(leaf_index) => {
150 self.validate_update_proposals(&proposal_queue, *leaf_index)?;
154
155 self.validate_no_external_init_proposals(&proposal_queue)?;
156 }
157 Sender::External(_) => {
158 return Err(StageCommitError::SenderTypeExternal);
160 }
161 Sender::NewMemberProposal => {
162 return Err(StageCommitError::SenderTypeNewMemberProposal);
164 }
165 Sender::NewMemberCommit => {
166 self.validate_external_commit(&proposal_queue)?;
170 }
171 }
172
173 let sender_index = match sender {
175 Sender::Member(leaf_index) => *leaf_index,
176 Sender::NewMemberCommit => {
177 let inline_proposals = commit.proposals.iter().filter_map(|p| {
178 if let ProposalOrRef::Proposal(inline_proposal) = p {
179 Some(Some(inline_proposal))
180 } else {
181 None
182 }
183 });
184 self.leftmost_free_index(inline_proposals)?
185 }
186 _ => {
187 return Err(StageCommitError::SenderTypeExternal);
188 }
189 };
190
191 Ok((commit, proposal_queue, sender_index))
192 }
193
194 fn validate_no_external_init_proposals(
197 &self,
198 proposal_queue: &ProposalQueue,
199 ) -> Result<(), ProposalValidationError> {
200 for proposal in proposal_queue.queued_proposals() {
201 if matches!(
202 proposal.proposal().proposal_type(),
203 ProposalType::ExternalInit
204 ) {
205 return Err(ProposalValidationError::ExternalInitProposalInRegularCommit);
206 }
207 }
208
209 Ok(())
210 }
211
212 pub(crate) fn stage_commit(
249 &self,
250 mls_content: &AuthenticatedContent,
251 crypto: &impl OpenMlsCrypto,
252 ) -> Result<StagedCommit, StageCommitError> {
253 let (commit, proposal_queue, sender_index) = self.validate_commit(mls_content, crypto)?;
254
255 let staged_diff = self.stage_diff(mls_content, &proposal_queue, sender_index, crypto)?;
256 let staged_state = PublicStagedCommitState {
257 staged_diff,
258 update_path_leaf_node: commit.path.as_ref().map(|p| p.leaf_node().clone()),
259 };
260
261 let staged_commit_state = StagedCommitState::PublicState(Box::new(staged_state));
262
263 Ok(StagedCommit::new(proposal_queue, staged_commit_state))
264 }
265
266 fn stage_diff(
267 &self,
268 mls_content: &AuthenticatedContent,
269 proposal_queue: &ProposalQueue,
270 sender_index: LeafNodeIndex,
271 crypto: &impl OpenMlsCrypto,
272 ) -> Result<StagedPublicGroupDiff, StageCommitError> {
273 let ciphersuite = self.ciphersuite();
274 let mut diff = self.empty_diff();
275
276 let apply_proposals_values = diff.apply_proposals(proposal_queue, None)?;
277
278 let commit = match mls_content.content() {
279 FramedContentBody::Commit(commit) => commit,
280 _ => return Err(StageCommitError::WrongPlaintextContentType),
281 };
282
283 if let Some(update_path) = &commit.path {
285 diff.apply_received_update_path(crypto, ciphersuite, sender_index, update_path)?;
288 } else if apply_proposals_values.path_required {
289 return Err(StageCommitError::RequiredPathNotFound);
292 };
293
294 diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
296
297 diff.update_confirmed_transcript_hash(crypto, mls_content)?;
299
300 let received_confirmation_tag = mls_content
301 .confirmation_tag()
302 .ok_or(StageCommitError::ConfirmationTagMissing)?;
303
304 diff.update_interim_transcript_hash(
307 ciphersuite,
308 crypto,
309 received_confirmation_tag.clone(),
310 )?;
311
312 let staged_diff = diff.into_staged_diff(crypto, ciphersuite)?;
313
314 Ok(staged_diff)
315 }
316
317 pub fn merge_commit<Storage: PublicStorageProvider>(
319 &mut self,
320 storage: &Storage,
321 staged_commit: StagedCommit,
322 ) -> Result<(), MergeCommitError<Storage::Error>> {
323 match staged_commit.into_state() {
324 StagedCommitState::PublicState(staged_state) => {
325 self.merge_diff(staged_state.staged_diff);
326 }
327 StagedCommitState::GroupMember(_) => (),
328 }
329
330 self.proposal_store.empty();
331 storage
332 .clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())
333 .map_err(MergeCommitError::StorageError)?;
334 self.store(storage).map_err(MergeCommitError::StorageError)
335 }
336}