openmls/group/mls_group/commit_builder/
external_commits.rs1use thiserror::Error;
2use tls_codec::Serialize as _;
3
4#[cfg(doc)]
5use super::CommitMessageBundle;
6
7use crate::{
8 binary_tree::LeafNodeIndex,
9 credentials::CredentialWithKey,
10 error::LibraryError,
11 framing::{ContentType, DecryptedMessage, PublicMessageIn, Sender},
12 group::{
13 commit_builder::{CommitBuilder, ExternalCommitInfo, Initial},
14 past_secrets::MessageSecretsStore,
15 public_group::errors::CreationFromExternalError,
16 ExternalCommitBuilderFinalizeError, LeafNodeLifetimePolicy, MlsGroup, MlsGroupJoinConfig,
17 MlsGroupState, PendingCommitState, ProposalStore, PublicGroup, QueuedProposal,
18 ValidationError, PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
19 },
20 messages::{
21 group_info::VerifiableGroupInfo,
22 proposals::{
23 ExternalInitProposal, PreSharedKeyProposal, Proposal, ProposalOrRefType, ProposalType,
24 RemoveProposal,
25 },
26 },
27 schedule::{psk::store::ResumptionPskStore, EpochSecrets, InitSecret},
28 storage::OpenMlsProvider,
29 treesync::{LeafNodeParameters, RatchetTreeIn},
30 versions::ProtocolVersion,
31};
32
33#[derive(Debug, Error)]
35pub enum ExternalCommitBuilderError<StorageError> {
36 #[error(transparent)]
38 LibraryError(#[from] LibraryError),
39 #[error("No ratchet tree available to build initial tree.")]
41 MissingRatchetTree,
42 #[error("No external_pub extension available to join group by external commit.")]
44 MissingExternalPub,
45 #[error("We don't support the ciphersuite of the group we are trying to join.")]
47 UnsupportedCiphersuite,
48 #[error(transparent)]
51 PublicGroupError(#[from] CreationFromExternalError<StorageError>),
52 #[error("An error occurred when writing group to storage.")]
54 StorageError(StorageError),
55 #[error("Error validating proposals: {0}")]
57 InvalidProposal(#[from] ValidationError),
58}
59
60#[derive(Default)]
69pub struct ExternalCommitBuilder {
70 proposals: Vec<PublicMessageIn>,
71 ratchet_tree: Option<RatchetTreeIn>,
72 config: MlsGroupJoinConfig,
73 validate_lifetimes: LeafNodeLifetimePolicy,
74 aad: Vec<u8>,
75}
76
77impl MlsGroup {
78 pub fn external_commit_builder() -> ExternalCommitBuilder {
80 ExternalCommitBuilder::new()
81 }
82}
83
84impl ExternalCommitBuilder {
85 pub fn new() -> Self {
87 Self::default()
88 }
89
90 pub fn with_proposals(mut self, proposals: Vec<PublicMessageIn>) -> Self {
93 self.proposals = proposals;
94 self
95 }
96
97 pub fn with_ratchet_tree(mut self, ratchet_tree: RatchetTreeIn) -> Self {
102 self.ratchet_tree = Some(ratchet_tree);
103 self
104 }
105
106 pub fn with_config(mut self, config: MlsGroupJoinConfig) -> Self {
111 self.config = config;
112 self
113 }
114
115 pub fn with_aad(mut self, aad: Vec<u8>) -> Self {
118 self.aad = aad;
119 self
120 }
121
122 pub fn skip_lifetime_validation(mut self) -> Self {
127 self.validate_lifetimes = LeafNodeLifetimePolicy::Skip;
128 self
129 }
130
131 pub fn build_group<Provider: OpenMlsProvider>(
137 self,
138 provider: &Provider,
139 verifiable_group_info: VerifiableGroupInfo,
140 credential_with_key: CredentialWithKey,
141 ) -> Result<
142 CommitBuilder<'_, Initial, MlsGroup>,
143 ExternalCommitBuilderError<Provider::StorageError>,
144 > {
145 let ExternalCommitBuilder {
146 proposals,
147 ratchet_tree,
148 mut config,
149 aad,
150 validate_lifetimes,
151 } = self;
152
153 let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
157 Some(extension) => extension.ratchet_tree().clone(),
158 None => match ratchet_tree {
159 Some(ratchet_tree) => ratchet_tree,
160 None => return Err(ExternalCommitBuilderError::MissingRatchetTree),
161 },
162 };
163
164 let (public_group, group_info) = PublicGroup::from_ratchet_tree(
165 provider.crypto(),
166 ratchet_tree,
167 verifiable_group_info,
168 ProposalStore::new(),
169 validate_lifetimes,
170 )?;
171 let group_context = public_group.group_context();
172
173 let external_pub = group_info
175 .extensions()
176 .external_pub()
177 .ok_or(ExternalCommitBuilderError::MissingExternalPub)?
178 .external_pub();
179
180 let (init_secret, kem_output) = InitSecret::from_group_context(
181 provider.crypto(),
182 group_context,
183 external_pub.as_slice(),
184 )
185 .map_err(|_| ExternalCommitBuilderError::UnsupportedCiphersuite)?;
186
187 let ciphersuite = group_context.ciphersuite();
191 let epoch_secrets =
192 EpochSecrets::with_init_secret(provider.crypto(), ciphersuite, init_secret)
193 .map_err(LibraryError::unexpected_crypto_error)?;
194 let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
195 group_context
196 .tls_serialize_detached()
197 .map_err(LibraryError::missing_bound_check)?,
198 public_group.tree_size(),
199 LeafNodeIndex::new(0u32),
203 );
204 let message_secrets_store = MessageSecretsStore::new_with_secret(
205 config.past_epoch_deletion_policy(),
206 message_secrets,
207 );
208
209 let external_init_proposal =
210 Proposal::external_init(ExternalInitProposal::from(kem_output));
211
212 let serialized_context = group_context
214 .tls_serialize_detached()
215 .map_err(LibraryError::missing_bound_check)?;
216 let mut queued_proposals = Vec::new();
217 for message in proposals {
218 if message.content_type() != ContentType::Proposal {
219 continue; }
221 let decrypted_message = DecryptedMessage::from_inbound_public_message(
222 message,
223 None,
224 serialized_context.clone(),
225 provider.crypto(),
226 ciphersuite,
227 )?;
228 let unverified_message = public_group.parse_message(decrypted_message, None)?;
229 let (verified_message, _credential) = unverified_message.verify(
230 ciphersuite,
231 provider.crypto(),
232 ProtocolVersion::default(),
233 )?;
234 let queued_proposal = QueuedProposal::from_authenticated_content(
235 ciphersuite,
236 provider.crypto(),
237 verified_message,
238 ProposalOrRefType::Reference,
239 )?;
240 if queued_proposal.proposal().is_type(ProposalType::SelfRemove) {
242 queued_proposals.push(queued_proposal);
243 }
244 }
245
246 let inline_proposals = [external_init_proposal].into_iter();
247
248 let our_signature_key = credential_with_key.signature_key.as_slice();
251 let remove_proposal = public_group.members().find_map(|member| {
252 (member.signature_key == our_signature_key).then_some(Proposal::remove(
253 RemoveProposal {
254 removed: member.index,
255 },
256 ))
257 });
258
259 let inline_proposals = inline_proposals
260 .chain(remove_proposal)
261 .map(|p| {
262 QueuedProposal::from_proposal_and_sender(
263 ciphersuite,
264 provider.crypto(),
265 p,
266 &Sender::NewMemberCommit,
267 )
268 })
269 .collect::<Result<Vec<_>, _>>()?;
270
271 queued_proposals.extend(inline_proposals);
272
273 let own_leaf_index = public_group.leftmost_free_index(queued_proposals.iter())?;
274
275 let original_wire_format_policy = config.wire_format_policy;
276
277 config.wire_format_policy = PURE_PLAINTEXT_WIRE_FORMAT_POLICY;
282
283 let mut mls_group = MlsGroup {
284 mls_group_config: config,
285 own_leaf_nodes: vec![],
286 aad: vec![],
287 group_state: MlsGroupState::Operational,
288 public_group,
289 group_epoch_secrets,
290 own_leaf_index,
291 message_secrets_store,
292 resumption_psk_store: ResumptionPskStore::new(32),
293 #[cfg(feature = "extensions-draft-08")]
296 application_export_tree: None,
297 };
298
299 let proposal_store = mls_group.proposal_store_mut();
301 for queued_proposal in queued_proposals {
302 proposal_store.add(queued_proposal);
303 }
304
305 let mut commit_builder = CommitBuilder::<'_, Initial, MlsGroup>::new(mls_group);
306
307 commit_builder.stage.force_self_update = true;
308 commit_builder.stage.external_commit_info = Some(ExternalCommitInfo {
309 wire_format_policy: original_wire_format_policy,
310 credential: credential_with_key.clone(),
311 aad,
312 });
313 let leaf_node_parameters = LeafNodeParameters::builder()
314 .with_credential_with_key(credential_with_key)
315 .build();
316 commit_builder.stage.leaf_node_parameters = leaf_node_parameters;
317
318 Ok(commit_builder)
319 }
320}
321
322impl<'a> CommitBuilder<'a, Initial, MlsGroup> {
324 pub fn add_psk_proposal(mut self, proposal: PreSharedKeyProposal) -> Self {
326 self.stage.own_proposals.push(Proposal::psk(proposal));
327 self
328 }
329
330 pub fn add_psk_proposals(
333 mut self,
334 proposals: impl IntoIterator<Item = PreSharedKeyProposal>,
335 ) -> Self {
336 self.stage
337 .own_proposals
338 .extend(proposals.into_iter().map(Proposal::psk));
339 self
340 }
341}
342
343impl CommitBuilder<'_, super::Complete, MlsGroup> {
345 pub fn finalize<Provider: OpenMlsProvider>(
351 self,
352 provider: &Provider,
353 ) -> Result<
354 (MlsGroup, super::CommitMessageBundle),
355 ExternalCommitBuilderFinalizeError<Provider::StorageError>,
356 > {
357 let Self {
358 mut group,
359 stage:
360 super::Complete {
361 result: create_commit_result,
362 original_wire_format_policy,
363 },
364 ..
365 } = self;
366
367 let mls_message = group.content_to_mls_message(create_commit_result.commit, provider)?;
369
370 group.reset_aad();
371
372 if let Some(wire_format_policy) = original_wire_format_policy {
374 group.mls_group_config.wire_format_policy = wire_format_policy;
375 }
376
377 group
379 .store(provider.storage())
380 .map_err(ExternalCommitBuilderFinalizeError::StorageError)?;
381
382 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
385 create_commit_result.staged_commit,
386 )));
387
388 group.merge_pending_commit(provider)?;
389
390 let bundle = super::CommitMessageBundle {
391 version: group.version(),
392 commit: mls_message,
393 welcome: create_commit_result.welcome_option,
394 group_info: create_commit_result.group_info,
395 };
396
397 Ok((group, bundle))
398 }
399}