1use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
2use thiserror::Error;
3use tls_codec::Serialize as _;
4
5#[cfg(doc)]
6use super::CommitMessageBundle;
7#[cfg(doc)]
8use crate::treesync::LeafNodeParameters;
9
10use crate::{
11 binary_tree::LeafNodeIndex,
12 credentials::CredentialWithKey,
13 error::LibraryError,
14 framing::{ContentType, DecryptedMessage, PublicMessageIn, Sender},
15 group::{
16 commit_builder::{CommitBuilder, ExternalCommitInfo, Initial},
17 past_secrets::MessageSecretsStore,
18 public_group::errors::CreationFromExternalError,
19 ExternalCommitBuilderFinalizeError, GroupContext, LeafNodeLifetimePolicy, MlsGroup,
20 MlsGroupJoinConfig, MlsGroupState, PendingCommitState, ProposalStore, PublicGroup,
21 QueuedProposal, ValidationError, PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
22 },
23 messages::{
24 group_info::{GroupInfo, VerifiableGroupInfo},
25 proposals::{
26 ExternalInitProposal, PreSharedKeyProposal, Proposal, ProposalOrRefType, ProposalType,
27 RemoveProposal,
28 },
29 },
30 schedule::{psk::store::ResumptionPskStore, EpochSecrets, InitSecret},
31 storage::OpenMlsProvider,
32 treesync::RatchetTreeIn,
33 versions::ProtocolVersion,
34};
35
36#[derive(Debug, Error)]
38pub enum ExternalCommitBuilderError<StorageError> {
39 #[error(transparent)]
41 LibraryError(#[from] LibraryError),
42 #[error("No ratchet tree available to build initial tree.")]
44 MissingRatchetTree,
45 #[error("No external_pub extension available to join group by external commit.")]
47 MissingExternalPub,
48 #[error("Ciphersuite {0:?} of the group we are trying to join is not supported by the crypto provider.")]
50 UnsupportedCiphersuite(Ciphersuite),
51 #[error(transparent)]
54 PublicGroupError(#[from] CreationFromExternalError<StorageError>),
55 #[error("An error occurred when writing group to storage.")]
57 StorageError(StorageError),
58 #[error("Error validating proposals: {0}")]
60 InvalidProposal(#[from] ValidationError),
61}
62
63#[derive(Default)]
78pub struct ExternalCommitBuilder {
79 proposals: Vec<PublicMessageIn>,
80 ratchet_tree: Option<RatchetTreeIn>,
81 config: MlsGroupJoinConfig,
82 validate_lifetimes: LeafNodeLifetimePolicy,
83 aad: Vec<u8>,
84 #[cfg(feature = "virtual-clients-draft")]
87 emulation_group: bool,
88}
89
90impl MlsGroup {
91 pub fn external_commit_builder() -> ExternalCommitBuilder {
93 ExternalCommitBuilder::new()
94 }
95}
96
97impl ExternalCommitBuilder {
98 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn with_proposals(mut self, proposals: Vec<PublicMessageIn>) -> Self {
106 self.proposals = proposals;
107 self
108 }
109
110 pub fn with_ratchet_tree(mut self, ratchet_tree: RatchetTreeIn) -> Self {
115 self.ratchet_tree = Some(ratchet_tree);
116 self
117 }
118
119 pub fn with_config(mut self, config: MlsGroupJoinConfig) -> Self {
124 self.config = config;
125 self
126 }
127
128 pub fn with_aad(mut self, aad: Vec<u8>) -> Self {
132 self.aad = aad;
133 self
134 }
135
136 #[cfg(feature = "virtual-clients-draft")]
147 pub fn emulation_group(mut self, emulation_group: bool) -> Self {
148 self.emulation_group = emulation_group;
149 self
150 }
151
152 pub fn skip_lifetime_validation(mut self) -> Self {
157 self.validate_lifetimes = LeafNodeLifetimePolicy::Skip;
158 self
159 }
160
161 pub fn leaf_node_lifetime_policy(mut self, lifetime_policy: LeafNodeLifetimePolicy) -> Self {
165 self.validate_lifetimes = lifetime_policy;
166 self
167 }
168
169 pub fn build_group<Provider: OpenMlsProvider>(
178 self,
179 provider: &Provider,
180 verifiable_group_info: VerifiableGroupInfo,
181 credential_with_key: CredentialWithKey,
182 ) -> Result<
183 CommitBuilder<'_, Initial, MlsGroup>,
184 ExternalCommitBuilderError<Provider::StorageError>,
185 > {
186 self.verify_group(provider, verifiable_group_info)?
187 .build_group(provider, credential_with_key)
188 }
189
190 pub fn verify_group<Provider: OpenMlsProvider>(
198 self,
199 provider: &Provider,
200 verifiable_group_info: VerifiableGroupInfo,
201 ) -> Result<VerifiedExternalCommitBuilder, ExternalCommitBuilderError<Provider::StorageError>>
202 {
203 let ExternalCommitBuilder {
204 proposals,
205 ratchet_tree,
206 config,
207 aad,
208 validate_lifetimes,
209 #[cfg(feature = "virtual-clients-draft")]
210 emulation_group,
211 } = self;
212
213 let group_ciphersuite = verifiable_group_info.ciphersuite();
214 provider
215 .crypto()
216 .supports(group_ciphersuite)
217 .map_err(|_| ExternalCommitBuilderError::UnsupportedCiphersuite(group_ciphersuite))?;
218
219 let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
221 Some(extension) => extension.ratchet_tree().clone(),
222 None => match ratchet_tree {
223 Some(ratchet_tree) => ratchet_tree,
224 None => return Err(ExternalCommitBuilderError::MissingRatchetTree),
225 },
226 };
227
228 let (public_group, group_info) = PublicGroup::from_ratchet_tree(
229 provider.crypto(),
230 ratchet_tree,
231 verifiable_group_info,
232 ProposalStore::new(),
233 validate_lifetimes,
234 )?;
235
236 Ok(VerifiedExternalCommitBuilder {
237 public_group,
238 group_info,
239 proposals,
240 config,
241 aad,
242 #[cfg(feature = "virtual-clients-draft")]
243 emulation_group,
244 })
245 }
246}
247
248pub struct VerifiedExternalCommitBuilder {
253 public_group: PublicGroup,
254 group_info: GroupInfo,
255 proposals: Vec<PublicMessageIn>,
256 config: MlsGroupJoinConfig,
257 aad: Vec<u8>,
258 #[cfg(feature = "virtual-clients-draft")]
259 emulation_group: bool,
260}
261
262impl VerifiedExternalCommitBuilder {
263 pub fn group_info(&self) -> &GroupInfo {
265 &self.group_info
266 }
267
268 pub fn group_context(&self) -> &GroupContext {
270 self.public_group.group_context()
271 }
272
273 pub fn build_group<Provider: OpenMlsProvider>(
279 self,
280 provider: &Provider,
281 credential_with_key: CredentialWithKey,
282 ) -> Result<
283 CommitBuilder<'_, Initial, MlsGroup>,
284 ExternalCommitBuilderError<Provider::StorageError>,
285 > {
286 let VerifiedExternalCommitBuilder {
287 public_group,
288 group_info,
289 proposals,
290 mut config,
291 aad,
292 #[cfg(feature = "virtual-clients-draft")]
293 emulation_group,
294 } = self;
295
296 let group_context = public_group.group_context();
297
298 let external_pub = group_info
300 .extensions()
301 .external_pub()
302 .ok_or(ExternalCommitBuilderError::MissingExternalPub)?
303 .external_pub();
304
305 let (init_secret, kem_output) = InitSecret::from_group_context(
306 provider.crypto(),
307 group_context,
308 external_pub.as_slice(),
309 )
310 .map_err(|_| {
311 ExternalCommitBuilderError::UnsupportedCiphersuite(group_context.ciphersuite())
312 })?;
313
314 let ciphersuite = group_context.ciphersuite();
318 let epoch_secrets =
319 EpochSecrets::with_init_secret(provider.crypto(), ciphersuite, init_secret)
320 .map_err(LibraryError::unexpected_crypto_error)?;
321 let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
322 group_context
323 .tls_serialize_detached()
324 .map_err(LibraryError::missing_bound_check)?,
325 public_group.tree_size(),
326 LeafNodeIndex::new(0u32),
330 );
331 let message_secrets_store = MessageSecretsStore::new_with_secret(
332 config.past_epoch_deletion_policy(),
333 message_secrets,
334 );
335
336 let external_init_proposal =
337 Proposal::external_init(ExternalInitProposal::from(kem_output));
338
339 let serialized_context = group_context
341 .tls_serialize_detached()
342 .map_err(LibraryError::missing_bound_check)?;
343 let mut queued_proposals = Vec::new();
344 for message in proposals {
345 if message.content_type() != ContentType::Proposal {
346 continue; }
348 let decrypted_message = DecryptedMessage::from_inbound_public_message(
349 message,
350 None,
351 serialized_context.clone(),
352 provider.crypto(),
353 ciphersuite,
354 )?;
355 let unverified_message = public_group.parse_message(decrypted_message, None)?;
356 let verified = unverified_message.verify(
357 ciphersuite,
358 provider.crypto(),
359 ProtocolVersion::default(),
360 LeafNodeLifetimePolicy::Verify,
362 )?;
363 let queued_proposal = QueuedProposal::from_authenticated_content(
364 ciphersuite,
365 provider.crypto(),
366 verified.content,
367 ProposalOrRefType::Reference,
368 )?;
369 if queued_proposal.proposal().is_type(ProposalType::SelfRemove) {
371 queued_proposals.push(queued_proposal);
372 }
373 }
374
375 let inline_proposals = [external_init_proposal].into_iter();
376
377 let our_signature_key = credential_with_key.signature_key.as_slice();
380 let remove_proposal = public_group.members().find_map(|member| {
381 (member.signature_key == our_signature_key).then_some(Proposal::remove(
382 RemoveProposal {
383 removed: member.index,
384 },
385 ))
386 });
387
388 let inline_proposals = inline_proposals
389 .chain(remove_proposal)
390 .map(|p| {
391 QueuedProposal::from_proposal_and_sender(
392 ciphersuite,
393 provider.crypto(),
394 p,
395 &Sender::NewMemberCommit,
396 )
397 })
398 .collect::<Result<Vec<_>, _>>()?;
399
400 queued_proposals.extend(inline_proposals);
401
402 let own_leaf_index = public_group.leftmost_free_index(queued_proposals.iter())?;
403
404 let original_wire_format_policy = config.wire_format_policy;
405
406 config.wire_format_policy = PURE_PLAINTEXT_WIRE_FORMAT_POLICY;
411
412 let mut mls_group = MlsGroup {
413 mls_group_config: config,
414 own_leaf_nodes: vec![],
415 aad: vec![],
416 #[cfg(feature = "extensions-draft")]
417 safe_aad: crate::framing::SafeAad::empty(),
418 group_state: MlsGroupState::Operational,
419 public_group,
420 group_epoch_secrets,
421 own_leaf_index,
422 message_secrets_store,
423 resumption_psk_store: ResumptionPskStore::new(32),
424 #[cfg(feature = "extensions-draft")]
427 application_export_tree: None,
428 #[cfg(feature = "virtual-clients-draft")]
429 emulation_group,
430 };
431
432 let proposal_store = mls_group.proposal_store_mut();
434 for queued_proposal in queued_proposals {
435 proposal_store.add(queued_proposal);
436 }
437
438 let mut commit_builder = CommitBuilder::<'_, Initial, MlsGroup>::new(mls_group);
439
440 commit_builder.stage.force_self_update = true;
441 commit_builder.stage.bare_aad = Some(aad);
442 commit_builder.stage.external_commit_info = Some(ExternalCommitInfo {
443 wire_format_policy: original_wire_format_policy,
444 credential: credential_with_key,
445 });
446
447 Ok(commit_builder)
448 }
449}
450
451impl<'a> CommitBuilder<'a, Initial, MlsGroup> {
453 pub fn add_proposal(mut self, proposal: Proposal) -> Self {
464 self.stage.own_proposals.push(proposal);
465 self
466 }
467
468 pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
472 self.stage.own_proposals.extend(proposals);
473 self
474 }
475
476 pub fn add_psk_proposal(mut self, proposal: PreSharedKeyProposal) -> Self {
478 self.stage.own_proposals.push(Proposal::psk(proposal));
479 self
480 }
481
482 pub fn add_psk_proposals(
485 mut self,
486 proposals: impl IntoIterator<Item = PreSharedKeyProposal>,
487 ) -> Self {
488 self.stage
489 .own_proposals
490 .extend(proposals.into_iter().map(Proposal::psk));
491 self
492 }
493
494 #[cfg(feature = "extensions-draft")]
496 pub fn add_app_data_update_proposal(
497 mut self,
498 proposal: crate::messages::proposals::AppDataUpdateProposal,
499 ) -> Self {
500 self.stage
501 .own_proposals
502 .push(Proposal::AppDataUpdate(Box::new(proposal)));
503 self
504 }
505}
506
507impl CommitBuilder<'_, super::Complete, MlsGroup> {
509 #[openmls_traits::maybe_async]
515 pub async fn finalize<Provider: OpenMlsProvider>(
516 self,
517 provider: &Provider,
518 ) -> Result<
519 (MlsGroup, super::CommitMessageBundle),
520 ExternalCommitBuilderFinalizeError<Provider::StorageError>,
521 > {
522 let Self {
523 mut group,
524 stage:
525 super::Complete {
526 result: create_commit_result,
527 original_wire_format_policy,
528 },
529 ..
530 } = self;
531
532 let mls_message = group
536 .content_to_mls_message(create_commit_result.commit, provider)
537 .await?
538 .message;
539
540 group.reset_aad();
541
542 if let Some(wire_format_policy) = original_wire_format_policy {
544 group.mls_group_config.wire_format_policy = wire_format_policy;
545 }
546
547 group
549 .store(provider.storage())
550 .await
551 .map_err(ExternalCommitBuilderFinalizeError::StorageError)?;
552
553 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
556 create_commit_result.staged_commit,
557 )));
558
559 group.merge_pending_commit(provider).await?;
560
561 let bundle = super::CommitMessageBundle {
562 version: group.version(),
563 commit: mls_message,
564 welcome: create_commit_result.welcome_option,
565 group_info: create_commit_result.group_info,
566 #[cfg(feature = "virtual-clients-draft")]
567 confirmation: None,
568 };
569
570 Ok((group, bundle))
571 }
572}