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