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
8use crate::{
9 binary_tree::LeafNodeIndex,
10 credentials::CredentialWithKey,
11 error::LibraryError,
12 framing::{ContentType, DecryptedMessage, PublicMessageIn, Sender},
13 group::{
14 commit_builder::{CommitBuilder, ExternalCommitInfo, Initial},
15 past_secrets::MessageSecretsStore,
16 public_group::errors::CreationFromExternalError,
17 ExternalCommitBuilderFinalizeError, LeafNodeLifetimePolicy, MlsGroup, MlsGroupJoinConfig,
18 MlsGroupState, PendingCommitState, ProposalStore, PublicGroup, QueuedProposal,
19 ValidationError, PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
20 },
21 messages::{
22 group_info::VerifiableGroupInfo,
23 proposals::{
24 ExternalInitProposal, PreSharedKeyProposal, Proposal, ProposalOrRefType, ProposalType,
25 RemoveProposal,
26 },
27 },
28 schedule::{psk::store::ResumptionPskStore, EpochSecrets, InitSecret},
29 storage::OpenMlsProvider,
30 treesync::{LeafNodeParameters, RatchetTreeIn},
31 versions::ProtocolVersion,
32};
33
34#[derive(Debug, Error)]
36pub enum ExternalCommitBuilderError<StorageError> {
37 #[error(transparent)]
39 LibraryError(#[from] LibraryError),
40 #[error("No ratchet tree available to build initial tree.")]
42 MissingRatchetTree,
43 #[error("No external_pub extension available to join group by external commit.")]
45 MissingExternalPub,
46 #[error("Ciphersuite {0:?} of the group we are trying to join is not supported by the crypto provider.")]
48 UnsupportedCiphersuite(Ciphersuite),
49 #[error(transparent)]
52 PublicGroupError(#[from] CreationFromExternalError<StorageError>),
53 #[error("An error occurred when writing group to storage.")]
55 StorageError(StorageError),
56 #[error("Error validating proposals: {0}")]
58 InvalidProposal(#[from] ValidationError),
59}
60
61#[derive(Default)]
70pub struct ExternalCommitBuilder {
71 proposals: Vec<PublicMessageIn>,
72 ratchet_tree: Option<RatchetTreeIn>,
73 config: MlsGroupJoinConfig,
74 validate_lifetimes: LeafNodeLifetimePolicy,
75 aad: Vec<u8>,
76}
77
78impl MlsGroup {
79 pub fn external_commit_builder() -> ExternalCommitBuilder {
81 ExternalCommitBuilder::new()
82 }
83}
84
85impl ExternalCommitBuilder {
86 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn with_proposals(mut self, proposals: Vec<PublicMessageIn>) -> Self {
94 self.proposals = proposals;
95 self
96 }
97
98 pub fn with_ratchet_tree(mut self, ratchet_tree: RatchetTreeIn) -> Self {
103 self.ratchet_tree = Some(ratchet_tree);
104 self
105 }
106
107 pub fn with_config(mut self, config: MlsGroupJoinConfig) -> Self {
112 self.config = config;
113 self
114 }
115
116 pub fn with_aad(mut self, aad: Vec<u8>) -> Self {
119 self.aad = aad;
120 self
121 }
122
123 pub fn skip_lifetime_validation(mut self) -> Self {
128 self.validate_lifetimes = LeafNodeLifetimePolicy::Skip;
129 self
130 }
131
132 pub fn build_group<Provider: OpenMlsProvider>(
138 self,
139 provider: &Provider,
140 verifiable_group_info: VerifiableGroupInfo,
141 credential_with_key: CredentialWithKey,
142 ) -> Result<
143 CommitBuilder<'_, Initial, MlsGroup>,
144 ExternalCommitBuilderError<Provider::StorageError>,
145 > {
146 let ExternalCommitBuilder {
147 proposals,
148 ratchet_tree,
149 mut config,
150 aad,
151 validate_lifetimes,
152 } = self;
153
154 let group_ciphersuite = verifiable_group_info.ciphersuite();
155 provider
156 .crypto()
157 .supports(group_ciphersuite)
158 .map_err(|_| ExternalCommitBuilderError::UnsupportedCiphersuite(group_ciphersuite))?;
159
160 let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
164 Some(extension) => extension.ratchet_tree().clone(),
165 None => match ratchet_tree {
166 Some(ratchet_tree) => ratchet_tree,
167 None => return Err(ExternalCommitBuilderError::MissingRatchetTree),
168 },
169 };
170
171 let (public_group, group_info) = PublicGroup::from_ratchet_tree(
172 provider.crypto(),
173 ratchet_tree,
174 verifiable_group_info,
175 ProposalStore::new(),
176 validate_lifetimes,
177 )?;
178 let group_context = public_group.group_context();
179
180 let external_pub = group_info
182 .extensions()
183 .external_pub()
184 .ok_or(ExternalCommitBuilderError::MissingExternalPub)?
185 .external_pub();
186
187 let (init_secret, kem_output) = InitSecret::from_group_context(
188 provider.crypto(),
189 group_context,
190 external_pub.as_slice(),
191 )
192 .map_err(|_| {
193 ExternalCommitBuilderError::UnsupportedCiphersuite(group_context.ciphersuite())
194 })?;
195
196 let ciphersuite = group_context.ciphersuite();
200 let epoch_secrets =
201 EpochSecrets::with_init_secret(provider.crypto(), ciphersuite, init_secret)
202 .map_err(LibraryError::unexpected_crypto_error)?;
203 let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
204 group_context
205 .tls_serialize_detached()
206 .map_err(LibraryError::missing_bound_check)?,
207 public_group.tree_size(),
208 LeafNodeIndex::new(0u32),
212 );
213 let message_secrets_store = MessageSecretsStore::new_with_secret(
214 config.past_epoch_deletion_policy(),
215 message_secrets,
216 );
217
218 let external_init_proposal =
219 Proposal::external_init(ExternalInitProposal::from(kem_output));
220
221 let serialized_context = group_context
223 .tls_serialize_detached()
224 .map_err(LibraryError::missing_bound_check)?;
225 let mut queued_proposals = Vec::new();
226 for message in proposals {
227 if message.content_type() != ContentType::Proposal {
228 continue; }
230 let decrypted_message = DecryptedMessage::from_inbound_public_message(
231 message,
232 None,
233 serialized_context.clone(),
234 provider.crypto(),
235 ciphersuite,
236 )?;
237 let unverified_message = public_group.parse_message(decrypted_message, None)?;
238 let verified = unverified_message.verify(
239 ciphersuite,
240 provider.crypto(),
241 ProtocolVersion::default(),
242 )?;
243 let queued_proposal = QueuedProposal::from_authenticated_content(
244 ciphersuite,
245 provider.crypto(),
246 verified.content,
247 ProposalOrRefType::Reference,
248 )?;
249 if queued_proposal.proposal().is_type(ProposalType::SelfRemove) {
251 queued_proposals.push(queued_proposal);
252 }
253 }
254
255 let inline_proposals = [external_init_proposal].into_iter();
256
257 let our_signature_key = credential_with_key.signature_key.as_slice();
260 let remove_proposal = public_group.members().find_map(|member| {
261 (member.signature_key == our_signature_key).then_some(Proposal::remove(
262 RemoveProposal {
263 removed: member.index,
264 },
265 ))
266 });
267
268 let inline_proposals = inline_proposals
269 .chain(remove_proposal)
270 .map(|p| {
271 QueuedProposal::from_proposal_and_sender(
272 ciphersuite,
273 provider.crypto(),
274 p,
275 &Sender::NewMemberCommit,
276 )
277 })
278 .collect::<Result<Vec<_>, _>>()?;
279
280 queued_proposals.extend(inline_proposals);
281
282 let own_leaf_index = public_group.leftmost_free_index(queued_proposals.iter())?;
283
284 let original_wire_format_policy = config.wire_format_policy;
285
286 config.wire_format_policy = PURE_PLAINTEXT_WIRE_FORMAT_POLICY;
291
292 let mut mls_group = MlsGroup {
293 mls_group_config: config,
294 own_leaf_nodes: vec![],
295 aad: vec![],
296 #[cfg(feature = "extensions-draft")]
297 safe_aad: crate::framing::SafeAad::empty(),
298 group_state: MlsGroupState::Operational,
299 public_group,
300 group_epoch_secrets,
301 own_leaf_index,
302 message_secrets_store,
303 resumption_psk_store: ResumptionPskStore::new(32),
304 #[cfg(feature = "extensions-draft")]
307 application_export_tree: None,
308 };
309
310 let proposal_store = mls_group.proposal_store_mut();
312 for queued_proposal in queued_proposals {
313 proposal_store.add(queued_proposal);
314 }
315
316 let mut commit_builder = CommitBuilder::<'_, Initial, MlsGroup>::new(mls_group);
317
318 commit_builder.stage.force_self_update = true;
319 commit_builder.stage.external_commit_info = Some(ExternalCommitInfo {
320 wire_format_policy: original_wire_format_policy,
321 credential: credential_with_key.clone(),
322 aad,
323 });
324 let leaf_node_parameters = LeafNodeParameters::builder()
325 .with_credential_with_key(credential_with_key)
326 .build();
327 commit_builder.stage.leaf_node_parameters = leaf_node_parameters;
328
329 Ok(commit_builder)
330 }
331}
332
333impl<'a> CommitBuilder<'a, Initial, MlsGroup> {
335 pub fn add_psk_proposal(mut self, proposal: PreSharedKeyProposal) -> Self {
337 self.stage.own_proposals.push(Proposal::psk(proposal));
338 self
339 }
340
341 pub fn add_psk_proposals(
344 mut self,
345 proposals: impl IntoIterator<Item = PreSharedKeyProposal>,
346 ) -> Self {
347 self.stage
348 .own_proposals
349 .extend(proposals.into_iter().map(Proposal::psk));
350 self
351 }
352
353 #[cfg(feature = "extensions-draft")]
355 pub fn add_app_data_update_proposal(
356 mut self,
357 proposal: crate::messages::proposals::AppDataUpdateProposal,
358 ) -> Self {
359 self.stage
360 .own_proposals
361 .push(Proposal::AppDataUpdate(Box::new(proposal)));
362 self
363 }
364}
365
366impl CommitBuilder<'_, super::Complete, MlsGroup> {
368 pub fn finalize<Provider: OpenMlsProvider>(
374 self,
375 provider: &Provider,
376 ) -> Result<
377 (MlsGroup, super::CommitMessageBundle),
378 ExternalCommitBuilderFinalizeError<Provider::StorageError>,
379 > {
380 let Self {
381 mut group,
382 stage:
383 super::Complete {
384 result: create_commit_result,
385 original_wire_format_policy,
386 },
387 ..
388 } = self;
389
390 let mls_message = group
394 .content_to_mls_message(create_commit_result.commit, provider)?
395 .message;
396
397 group.reset_aad();
398
399 if let Some(wire_format_policy) = original_wire_format_policy {
401 group.mls_group_config.wire_format_policy = wire_format_policy;
402 }
403
404 group
406 .store(provider.storage())
407 .map_err(ExternalCommitBuilderFinalizeError::StorageError)?;
408
409 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
412 create_commit_result.staged_commit,
413 )));
414
415 group.merge_pending_commit(provider)?;
416
417 let bundle = super::CommitMessageBundle {
418 version: group.version(),
419 commit: mls_message,
420 welcome: create_commit_result.welcome_option,
421 group_info: create_commit_result.group_info,
422 #[cfg(feature = "virtual-clients-draft")]
423 confirmation: None,
424 };
425
426 Ok((group, bundle))
427 }
428}