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 =
205 MessageSecretsStore::new_with_secret(config.max_past_epochs, message_secrets);
206
207 let external_init_proposal = Proposal::ExternalInit(ExternalInitProposal::from(kem_output));
208
209 let serialized_context = group_context
211 .tls_serialize_detached()
212 .map_err(LibraryError::missing_bound_check)?;
213 let mut queued_proposals = Vec::new();
214 for message in proposals {
215 if message.content_type() != ContentType::Proposal {
216 continue; }
218 let decrypted_message = DecryptedMessage::from_inbound_public_message(
219 message,
220 None,
221 serialized_context.clone(),
222 provider.crypto(),
223 ciphersuite,
224 )?;
225 let unverified_message = public_group.parse_message(decrypted_message, None)?;
226 let (verified_message, _credential) = unverified_message.verify(
227 ciphersuite,
228 provider.crypto(),
229 ProtocolVersion::default(),
230 )?;
231 let queued_proposal = QueuedProposal::from_authenticated_content(
232 ciphersuite,
233 provider.crypto(),
234 verified_message,
235 ProposalOrRefType::Reference,
236 )?;
237 if queued_proposal.proposal().is_type(ProposalType::SelfRemove) {
239 queued_proposals.push(queued_proposal);
240 }
241 }
242
243 let inline_proposals = [external_init_proposal].into_iter();
244
245 let our_signature_key = credential_with_key.signature_key.as_slice();
248 let remove_proposal = public_group.members().find_map(|member| {
249 (member.signature_key == our_signature_key).then_some(Proposal::Remove(
250 RemoveProposal {
251 removed: member.index,
252 },
253 ))
254 });
255
256 let inline_proposals = inline_proposals
257 .chain(remove_proposal)
258 .map(|p| {
259 QueuedProposal::from_proposal_and_sender(
260 ciphersuite,
261 provider.crypto(),
262 p,
263 &Sender::NewMemberCommit,
264 )
265 })
266 .collect::<Result<Vec<_>, _>>()?;
267
268 queued_proposals.extend(inline_proposals);
269
270 let own_leaf_index = public_group.leftmost_free_index(queued_proposals.iter())?;
271
272 let original_wire_format_policy = config.wire_format_policy;
273
274 config.wire_format_policy = PURE_PLAINTEXT_WIRE_FORMAT_POLICY;
279
280 let mut mls_group = MlsGroup {
281 mls_group_config: config,
282 own_leaf_nodes: vec![],
283 aad: vec![],
284 group_state: MlsGroupState::Operational,
285 public_group,
286 group_epoch_secrets,
287 own_leaf_index,
288 message_secrets_store,
289 resumption_psk_store: ResumptionPskStore::new(32),
290 };
291
292 let proposal_store = mls_group.proposal_store_mut();
294 for queued_proposal in queued_proposals {
295 proposal_store.add(queued_proposal);
296 }
297
298 let mut commit_builder = CommitBuilder::<'_, Initial, MlsGroup>::new(mls_group);
299
300 commit_builder.stage.force_self_update = true;
301 commit_builder.stage.external_commit_info = Some(ExternalCommitInfo {
302 wire_format_policy: original_wire_format_policy,
303 credential: credential_with_key.clone(),
304 aad,
305 });
306 let leaf_node_parameters = LeafNodeParameters::builder()
307 .with_credential_with_key(credential_with_key)
308 .build();
309 commit_builder.stage.leaf_node_parameters = leaf_node_parameters;
310
311 Ok(commit_builder)
312 }
313}
314
315impl<'a> CommitBuilder<'a, Initial, MlsGroup> {
317 pub fn add_psk_proposal(mut self, proposal: PreSharedKeyProposal) -> Self {
319 self.stage
320 .own_proposals
321 .push(Proposal::PreSharedKey(proposal));
322 self
323 }
324
325 pub fn add_psk_proposals(
328 mut self,
329 proposals: impl IntoIterator<Item = PreSharedKeyProposal>,
330 ) -> Self {
331 self.stage
332 .own_proposals
333 .extend(proposals.into_iter().map(Proposal::PreSharedKey));
334 self
335 }
336}
337
338impl CommitBuilder<'_, super::Complete, MlsGroup> {
340 pub fn finalize<Provider: OpenMlsProvider>(
346 self,
347 provider: &Provider,
348 ) -> Result<
349 (MlsGroup, super::CommitMessageBundle),
350 ExternalCommitBuilderFinalizeError<Provider::StorageError>,
351 > {
352 let Self {
353 mut group,
354 stage:
355 super::Complete {
356 result: create_commit_result,
357 original_wire_format_policy,
358 },
359 ..
360 } = self;
361
362 let mls_message = group.content_to_mls_message(create_commit_result.commit, provider)?;
364
365 group.reset_aad();
366
367 if let Some(wire_format_policy) = original_wire_format_policy {
369 group.mls_group_config.wire_format_policy = wire_format_policy;
370 }
371
372 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
375 create_commit_result.staged_commit,
376 )));
377
378 group.merge_pending_commit(provider)?;
379
380 let bundle = super::CommitMessageBundle {
381 version: group.version(),
382 commit: mls_message,
383 welcome: create_commit_result.welcome_option,
384 group_info: create_commit_result.group_info,
385 };
386
387 Ok((group, bundle))
388 }
389}