openmls/group/mls_group/commit_builder/
external_commits.rs1use 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, LeafNodeLifetimePolicy, MlsGroup, MlsGroupJoinConfig,
20 MlsGroupState, PendingCommitState, ProposalStore, PublicGroup, QueuedProposal,
21 ValidationError, PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
22 },
23 messages::{
24 group_info::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)]
74pub struct ExternalCommitBuilder {
75 proposals: Vec<PublicMessageIn>,
76 ratchet_tree: Option<RatchetTreeIn>,
77 config: MlsGroupJoinConfig,
78 validate_lifetimes: LeafNodeLifetimePolicy,
79 aad: Vec<u8>,
80 #[cfg(feature = "virtual-clients-draft")]
83 emulation_group: bool,
84}
85
86impl MlsGroup {
87 pub fn external_commit_builder() -> ExternalCommitBuilder {
89 ExternalCommitBuilder::new()
90 }
91}
92
93impl ExternalCommitBuilder {
94 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn with_proposals(mut self, proposals: Vec<PublicMessageIn>) -> Self {
102 self.proposals = proposals;
103 self
104 }
105
106 pub fn with_ratchet_tree(mut self, ratchet_tree: RatchetTreeIn) -> Self {
111 self.ratchet_tree = Some(ratchet_tree);
112 self
113 }
114
115 pub fn with_config(mut self, config: MlsGroupJoinConfig) -> Self {
120 self.config = config;
121 self
122 }
123
124 pub fn with_aad(mut self, aad: Vec<u8>) -> Self {
127 self.aad = aad;
128 self
129 }
130
131 #[cfg(feature = "virtual-clients-draft")]
142 pub fn emulation_group(mut self, emulation_group: bool) -> Self {
143 self.emulation_group = emulation_group;
144 self
145 }
146
147 pub fn skip_lifetime_validation(mut self) -> Self {
152 self.validate_lifetimes = LeafNodeLifetimePolicy::Skip;
153 self
154 }
155
156 pub fn build_group<Provider: OpenMlsProvider>(
162 self,
163 provider: &Provider,
164 verifiable_group_info: VerifiableGroupInfo,
165 credential_with_key: CredentialWithKey,
166 ) -> Result<
167 CommitBuilder<'_, Initial, MlsGroup>,
168 ExternalCommitBuilderError<Provider::StorageError>,
169 > {
170 let ExternalCommitBuilder {
171 proposals,
172 ratchet_tree,
173 mut config,
174 aad,
175 validate_lifetimes,
176 #[cfg(feature = "virtual-clients-draft")]
177 emulation_group,
178 } = self;
179
180 let group_ciphersuite = verifiable_group_info.ciphersuite();
181 provider
182 .crypto()
183 .supports(group_ciphersuite)
184 .map_err(|_| ExternalCommitBuilderError::UnsupportedCiphersuite(group_ciphersuite))?;
185
186 let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
190 Some(extension) => extension.ratchet_tree().clone(),
191 None => match ratchet_tree {
192 Some(ratchet_tree) => ratchet_tree,
193 None => return Err(ExternalCommitBuilderError::MissingRatchetTree),
194 },
195 };
196
197 let (public_group, group_info) = PublicGroup::from_ratchet_tree(
198 provider.crypto(),
199 ratchet_tree,
200 verifiable_group_info,
201 ProposalStore::new(),
202 validate_lifetimes,
203 )?;
204 let group_context = public_group.group_context();
205
206 let external_pub = group_info
208 .extensions()
209 .external_pub()
210 .ok_or(ExternalCommitBuilderError::MissingExternalPub)?
211 .external_pub();
212
213 let (init_secret, kem_output) = InitSecret::from_group_context(
214 provider.crypto(),
215 group_context,
216 external_pub.as_slice(),
217 )
218 .map_err(|_| {
219 ExternalCommitBuilderError::UnsupportedCiphersuite(group_context.ciphersuite())
220 })?;
221
222 let ciphersuite = group_context.ciphersuite();
226 let epoch_secrets =
227 EpochSecrets::with_init_secret(provider.crypto(), ciphersuite, init_secret)
228 .map_err(LibraryError::unexpected_crypto_error)?;
229 let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
230 group_context
231 .tls_serialize_detached()
232 .map_err(LibraryError::missing_bound_check)?,
233 public_group.tree_size(),
234 LeafNodeIndex::new(0u32),
238 );
239 let message_secrets_store = MessageSecretsStore::new_with_secret(
240 config.past_epoch_deletion_policy(),
241 message_secrets,
242 );
243
244 let external_init_proposal =
245 Proposal::external_init(ExternalInitProposal::from(kem_output));
246
247 let serialized_context = group_context
249 .tls_serialize_detached()
250 .map_err(LibraryError::missing_bound_check)?;
251 let mut queued_proposals = Vec::new();
252 for message in proposals {
253 if message.content_type() != ContentType::Proposal {
254 continue; }
256 let decrypted_message = DecryptedMessage::from_inbound_public_message(
257 message,
258 None,
259 serialized_context.clone(),
260 provider.crypto(),
261 ciphersuite,
262 )?;
263 let unverified_message = public_group.parse_message(decrypted_message, None)?;
264 let verified = unverified_message.verify(
265 ciphersuite,
266 provider.crypto(),
267 ProtocolVersion::default(),
268 )?;
269 let queued_proposal = QueuedProposal::from_authenticated_content(
270 ciphersuite,
271 provider.crypto(),
272 verified.content,
273 ProposalOrRefType::Reference,
274 )?;
275 if queued_proposal.proposal().is_type(ProposalType::SelfRemove) {
277 queued_proposals.push(queued_proposal);
278 }
279 }
280
281 let inline_proposals = [external_init_proposal].into_iter();
282
283 let our_signature_key = credential_with_key.signature_key.as_slice();
286 let remove_proposal = public_group.members().find_map(|member| {
287 (member.signature_key == our_signature_key).then_some(Proposal::remove(
288 RemoveProposal {
289 removed: member.index,
290 },
291 ))
292 });
293
294 let inline_proposals = inline_proposals
295 .chain(remove_proposal)
296 .map(|p| {
297 QueuedProposal::from_proposal_and_sender(
298 ciphersuite,
299 provider.crypto(),
300 p,
301 &Sender::NewMemberCommit,
302 )
303 })
304 .collect::<Result<Vec<_>, _>>()?;
305
306 queued_proposals.extend(inline_proposals);
307
308 let own_leaf_index = public_group.leftmost_free_index(queued_proposals.iter())?;
309
310 let original_wire_format_policy = config.wire_format_policy;
311
312 config.wire_format_policy = PURE_PLAINTEXT_WIRE_FORMAT_POLICY;
317
318 let mut mls_group = MlsGroup {
319 mls_group_config: config,
320 own_leaf_nodes: vec![],
321 aad: vec![],
322 #[cfg(feature = "extensions-draft")]
323 safe_aad: crate::framing::SafeAad::empty(),
324 group_state: MlsGroupState::Operational,
325 public_group,
326 group_epoch_secrets,
327 own_leaf_index,
328 message_secrets_store,
329 resumption_psk_store: ResumptionPskStore::new(32),
330 #[cfg(feature = "extensions-draft")]
333 application_export_tree: None,
334 #[cfg(feature = "virtual-clients-draft")]
335 emulation_group,
336 };
337
338 let proposal_store = mls_group.proposal_store_mut();
340 for queued_proposal in queued_proposals {
341 proposal_store.add(queued_proposal);
342 }
343
344 let mut commit_builder = CommitBuilder::<'_, Initial, MlsGroup>::new(mls_group);
345
346 commit_builder.stage.force_self_update = true;
347 commit_builder.stage.external_commit_info = Some(ExternalCommitInfo {
348 wire_format_policy: original_wire_format_policy,
349 credential: credential_with_key,
350 aad,
351 });
352
353 Ok(commit_builder)
354 }
355}
356
357impl<'a> CommitBuilder<'a, Initial, MlsGroup> {
359 pub fn add_proposal(mut self, proposal: Proposal) -> Self {
370 self.stage.own_proposals.push(proposal);
371 self
372 }
373
374 pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
378 self.stage.own_proposals.extend(proposals);
379 self
380 }
381
382 pub fn add_psk_proposal(mut self, proposal: PreSharedKeyProposal) -> Self {
384 self.stage.own_proposals.push(Proposal::psk(proposal));
385 self
386 }
387
388 pub fn add_psk_proposals(
391 mut self,
392 proposals: impl IntoIterator<Item = PreSharedKeyProposal>,
393 ) -> Self {
394 self.stage
395 .own_proposals
396 .extend(proposals.into_iter().map(Proposal::psk));
397 self
398 }
399
400 #[cfg(feature = "extensions-draft")]
402 pub fn add_app_data_update_proposal(
403 mut self,
404 proposal: crate::messages::proposals::AppDataUpdateProposal,
405 ) -> Self {
406 self.stage
407 .own_proposals
408 .push(Proposal::AppDataUpdate(Box::new(proposal)));
409 self
410 }
411}
412
413impl CommitBuilder<'_, super::Complete, MlsGroup> {
415 pub fn finalize<Provider: OpenMlsProvider>(
421 self,
422 provider: &Provider,
423 ) -> Result<
424 (MlsGroup, super::CommitMessageBundle),
425 ExternalCommitBuilderFinalizeError<Provider::StorageError>,
426 > {
427 let Self {
428 mut group,
429 stage:
430 super::Complete {
431 result: create_commit_result,
432 original_wire_format_policy,
433 },
434 ..
435 } = self;
436
437 let mls_message = group
441 .content_to_mls_message(create_commit_result.commit, provider)?
442 .message;
443
444 group.reset_aad();
445
446 if let Some(wire_format_policy) = original_wire_format_policy {
448 group.mls_group_config.wire_format_policy = wire_format_policy;
449 }
450
451 group
453 .store(provider.storage())
454 .map_err(ExternalCommitBuilderFinalizeError::StorageError)?;
455
456 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
459 create_commit_result.staged_commit,
460 )));
461
462 group.merge_pending_commit(provider)?;
463
464 let bundle = super::CommitMessageBundle {
465 version: group.version(),
466 commit: mls_message,
467 welcome: create_commit_result.welcome_option,
468 group_info: create_commit_result.group_info,
469 #[cfg(feature = "virtual-clients-draft")]
470 confirmation: None,
471 };
472
473 Ok((group, bundle))
474 }
475}