1 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 // Copyright by contributors to this project.
3 // SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5 use crate::{
6 client::MlsError,
7 group::{proposal_filter::ProposalBundle, Sender},
8 key_package::{validate_key_package_properties, KeyPackage},
9 protocol_version::ProtocolVersion,
10 time::MlsTime,
11 tree_kem::{
12 leaf_node_validator::{LeafNodeValidator, ValidationContext},
13 node::LeafIndex,
14 TreeKemPublic,
15 },
16 CipherSuiteProvider, ExtensionList,
17 };
18
19 use crate::tree_kem::leaf_node::LeafNode;
20
21 use super::ProposalInfo;
22
23 use crate::extension::{MlsExtension, RequiredCapabilitiesExt};
24
25 #[cfg(feature = "by_ref_proposal")]
26 use crate::extension::ExternalSendersExt;
27
28 use mls_rs_core::error::IntoAnyError;
29
30 use alloc::vec::Vec;
31 use mls_rs_core::{identity::IdentityProvider, psk::PreSharedKeyStorage};
32
33 use crate::group::{ExternalInit, ProposalType, RemoveProposal};
34
35 #[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
36 use crate::group::proposal::PreSharedKeyProposal;
37
38 #[cfg(feature = "psk")]
39 use crate::group::{JustPreSharedKeyID, ResumptionPSKUsage, ResumptionPsk};
40
41 #[cfg(all(feature = "std", feature = "psk"))]
42 use std::collections::HashSet;
43
44 #[cfg(feature = "by_ref_proposal")]
45 use super::filtering::{apply_strategy, filter_out_invalid_proposers, FilterStrategy};
46
47 #[cfg(feature = "custom_proposal")]
48 use super::filtering::filter_out_unsupported_custom_proposals;
49
50 #[derive(Debug)]
51 pub(crate) struct ProposalApplier<'a, C, P, CSP> {
52 pub original_tree: &'a TreeKemPublic,
53 pub protocol_version: ProtocolVersion,
54 pub cipher_suite_provider: &'a CSP,
55 pub original_group_extensions: &'a ExtensionList,
56 pub external_leaf: Option<&'a LeafNode>,
57 pub identity_provider: &'a C,
58 pub psk_storage: &'a P,
59 #[cfg(feature = "by_ref_proposal")]
60 pub group_id: &'a [u8],
61 }
62
63 #[derive(Debug)]
64 pub(crate) struct ApplyProposalsOutput {
65 pub(crate) new_tree: TreeKemPublic,
66 pub(crate) indexes_of_added_kpkgs: Vec<LeafIndex>,
67 pub(crate) external_init_index: Option<LeafIndex>,
68 #[cfg(feature = "by_ref_proposal")]
69 pub(crate) applied_proposals: ProposalBundle,
70 pub(crate) new_context_extensions: Option<ExtensionList>,
71 }
72
73 impl<'a, C, P, CSP> ProposalApplier<'a, C, P, CSP>
74 where
75 C: IdentityProvider,
76 P: PreSharedKeyStorage,
77 CSP: CipherSuiteProvider,
78 {
79 #[allow(clippy::too_many_arguments)]
new( original_tree: &'a TreeKemPublic, protocol_version: ProtocolVersion, cipher_suite_provider: &'a CSP, original_group_extensions: &'a ExtensionList, external_leaf: Option<&'a LeafNode>, identity_provider: &'a C, psk_storage: &'a P, #[cfg(feature = "by_ref_proposal")] group_id: &'a [u8], ) -> Self80 pub(crate) fn new(
81 original_tree: &'a TreeKemPublic,
82 protocol_version: ProtocolVersion,
83 cipher_suite_provider: &'a CSP,
84 original_group_extensions: &'a ExtensionList,
85 external_leaf: Option<&'a LeafNode>,
86 identity_provider: &'a C,
87 psk_storage: &'a P,
88 #[cfg(feature = "by_ref_proposal")] group_id: &'a [u8],
89 ) -> Self {
90 Self {
91 original_tree,
92 protocol_version,
93 cipher_suite_provider,
94 original_group_extensions,
95 external_leaf,
96 identity_provider,
97 psk_storage,
98 #[cfg(feature = "by_ref_proposal")]
99 group_id,
100 }
101 }
102
103 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
apply_proposals( &self, #[cfg(feature = "by_ref_proposal")] strategy: FilterStrategy, commit_sender: &Sender, #[cfg(not(feature = "by_ref_proposal"))] proposals: &ProposalBundle, #[cfg(feature = "by_ref_proposal")] proposals: ProposalBundle, commit_time: Option<MlsTime>, ) -> Result<ApplyProposalsOutput, MlsError>104 pub(crate) async fn apply_proposals(
105 &self,
106 #[cfg(feature = "by_ref_proposal")] strategy: FilterStrategy,
107 commit_sender: &Sender,
108 #[cfg(not(feature = "by_ref_proposal"))] proposals: &ProposalBundle,
109 #[cfg(feature = "by_ref_proposal")] proposals: ProposalBundle,
110 commit_time: Option<MlsTime>,
111 ) -> Result<ApplyProposalsOutput, MlsError> {
112 let output = match commit_sender {
113 Sender::Member(sender) => {
114 self.apply_proposals_from_member(
115 #[cfg(feature = "by_ref_proposal")]
116 strategy,
117 LeafIndex(*sender),
118 proposals,
119 commit_time,
120 )
121 .await
122 }
123 Sender::NewMemberCommit => {
124 self.apply_proposals_from_new_member(proposals, commit_time)
125 .await
126 }
127 #[cfg(feature = "by_ref_proposal")]
128 Sender::External(_) => Err(MlsError::ExternalSenderCannotCommit),
129 #[cfg(feature = "by_ref_proposal")]
130 Sender::NewMemberProposal => Err(MlsError::ExternalSenderCannotCommit),
131 }?;
132
133 #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))]
134 let mut output = output;
135
136 #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))]
137 filter_out_unsupported_custom_proposals(
138 &mut output.applied_proposals,
139 &output.new_tree,
140 strategy,
141 )?;
142
143 #[cfg(all(not(feature = "by_ref_proposal"), feature = "custom_proposal"))]
144 filter_out_unsupported_custom_proposals(proposals, &output.new_tree)?;
145
146 Ok(output)
147 }
148
149 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
150 // The lint below is triggered by the `proposals` parameter which may or may not be a borrow.
151 #[allow(clippy::needless_borrow)]
apply_proposals_from_new_member( &self, #[cfg(not(feature = "by_ref_proposal"))] proposals: &ProposalBundle, #[cfg(feature = "by_ref_proposal")] proposals: ProposalBundle, commit_time: Option<MlsTime>, ) -> Result<ApplyProposalsOutput, MlsError>152 async fn apply_proposals_from_new_member(
153 &self,
154 #[cfg(not(feature = "by_ref_proposal"))] proposals: &ProposalBundle,
155 #[cfg(feature = "by_ref_proposal")] proposals: ProposalBundle,
156 commit_time: Option<MlsTime>,
157 ) -> Result<ApplyProposalsOutput, MlsError> {
158 let external_leaf = self
159 .external_leaf
160 .ok_or(MlsError::ExternalCommitMustHaveNewLeaf)?;
161
162 ensure_exactly_one_external_init(&proposals)?;
163
164 ensure_at_most_one_removal_for_self(
165 &proposals,
166 external_leaf,
167 self.original_tree,
168 self.identity_provider,
169 self.original_group_extensions,
170 )
171 .await?;
172
173 ensure_proposals_in_external_commit_are_allowed(&proposals)?;
174 ensure_no_proposal_by_ref(&proposals)?;
175
176 #[cfg(feature = "by_ref_proposal")]
177 let mut proposals = filter_out_invalid_proposers(FilterStrategy::IgnoreNone, proposals)?;
178
179 filter_out_invalid_psks(
180 #[cfg(feature = "by_ref_proposal")]
181 FilterStrategy::IgnoreNone,
182 self.cipher_suite_provider,
183 #[cfg(feature = "by_ref_proposal")]
184 &mut proposals,
185 #[cfg(not(feature = "by_ref_proposal"))]
186 proposals,
187 self.psk_storage,
188 )
189 .await?;
190
191 let mut output = self
192 .apply_proposal_changes(
193 #[cfg(feature = "by_ref_proposal")]
194 FilterStrategy::IgnoreNone,
195 proposals,
196 commit_time,
197 )
198 .await?;
199
200 output.external_init_index = Some(
201 insert_external_leaf(
202 &mut output.new_tree,
203 external_leaf.clone(),
204 self.identity_provider,
205 self.original_group_extensions,
206 )
207 .await?,
208 );
209
210 Ok(output)
211 }
212
213 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
apply_proposals_with_new_capabilities( &self, #[cfg(feature = "by_ref_proposal")] strategy: FilterStrategy, #[cfg(not(feature = "by_ref_proposal"))] proposals: &ProposalBundle, #[cfg(feature = "by_ref_proposal")] proposals: ProposalBundle, group_context_extensions_proposal: ProposalInfo<ExtensionList>, commit_time: Option<MlsTime>, ) -> Result<ApplyProposalsOutput, MlsError> where C: IdentityProvider,214 pub(super) async fn apply_proposals_with_new_capabilities(
215 &self,
216 #[cfg(feature = "by_ref_proposal")] strategy: FilterStrategy,
217 #[cfg(not(feature = "by_ref_proposal"))] proposals: &ProposalBundle,
218 #[cfg(feature = "by_ref_proposal")] proposals: ProposalBundle,
219 group_context_extensions_proposal: ProposalInfo<ExtensionList>,
220 commit_time: Option<MlsTime>,
221 ) -> Result<ApplyProposalsOutput, MlsError>
222 where
223 C: IdentityProvider,
224 {
225 #[cfg(feature = "by_ref_proposal")]
226 let mut proposals_clone = proposals.clone();
227
228 // Apply adds, updates etc. in the context of new extensions
229 let output = self
230 .apply_tree_changes(
231 #[cfg(feature = "by_ref_proposal")]
232 strategy,
233 proposals,
234 &group_context_extensions_proposal.proposal,
235 commit_time,
236 )
237 .await?;
238
239 // Verify that capabilities and extensions are supported after modifications.
240 // TODO: The newly inserted nodes have already been validated by `apply_tree_changes`
241 // above. We should investigate if there is an easy way to avoid the double check.
242 let must_check = group_context_extensions_proposal
243 .proposal
244 .has_extension(RequiredCapabilitiesExt::extension_type());
245
246 #[cfg(feature = "by_ref_proposal")]
247 let must_check = must_check
248 || group_context_extensions_proposal
249 .proposal
250 .has_extension(ExternalSendersExt::extension_type());
251
252 let new_capabilities_supported = if must_check {
253 let leaf_validator = LeafNodeValidator::new(
254 self.cipher_suite_provider,
255 self.identity_provider,
256 Some(&group_context_extensions_proposal.proposal),
257 );
258
259 output
260 .new_tree
261 .non_empty_leaves()
262 .try_for_each(|(_, leaf)| {
263 leaf_validator.validate_required_capabilities(leaf)?;
264
265 #[cfg(feature = "by_ref_proposal")]
266 leaf_validator.validate_external_senders_ext_credentials(leaf)?;
267
268 Ok(())
269 })
270 } else {
271 Ok(())
272 };
273
274 let new_extensions_supported = group_context_extensions_proposal
275 .proposal
276 .iter()
277 .map(|extension| extension.extension_type)
278 .filter(|&ext_type| !ext_type.is_default())
279 .find(|ext_type| {
280 !output
281 .new_tree
282 .non_empty_leaves()
283 .all(|(_, leaf)| leaf.capabilities.extensions.contains(ext_type))
284 })
285 .map_or(Ok(()), |ext| Err(MlsError::UnsupportedGroupExtension(ext)));
286
287 #[cfg(not(feature = "by_ref_proposal"))]
288 {
289 new_capabilities_supported.and(new_extensions_supported)?;
290 Ok(output)
291 }
292
293 #[cfg(feature = "by_ref_proposal")]
294 // If extensions are good, return `Ok`. If not and the strategy is to filter, remove the group
295 // context extensions proposal and try applying all proposals again in the context of the old
296 // extensions. Else, return an error.
297 match new_capabilities_supported.and(new_extensions_supported) {
298 Ok(()) => Ok(output),
299 Err(e) => {
300 if strategy.ignore(group_context_extensions_proposal.is_by_reference()) {
301 proposals_clone.group_context_extensions.clear();
302
303 self.apply_tree_changes(
304 strategy,
305 proposals_clone,
306 self.original_group_extensions,
307 commit_time,
308 )
309 .await
310 } else {
311 Err(e)
312 }
313 }
314 }
315 }
316
317 #[cfg(any(mls_build_async, not(feature = "rayon")))]
318 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
validate_new_node<Ip: IdentityProvider, Cp: CipherSuiteProvider>( &self, leaf_node_validator: &LeafNodeValidator<'_, Ip, Cp>, key_package: &KeyPackage, commit_time: Option<MlsTime>, ) -> Result<(), MlsError>319 pub async fn validate_new_node<Ip: IdentityProvider, Cp: CipherSuiteProvider>(
320 &self,
321 leaf_node_validator: &LeafNodeValidator<'_, Ip, Cp>,
322 key_package: &KeyPackage,
323 commit_time: Option<MlsTime>,
324 ) -> Result<(), MlsError> {
325 leaf_node_validator
326 .check_if_valid(&key_package.leaf_node, ValidationContext::Add(commit_time))
327 .await?;
328
329 validate_key_package_properties(
330 key_package,
331 self.protocol_version,
332 self.cipher_suite_provider,
333 )
334 .await
335 }
336
337 #[cfg(all(not(mls_build_async), feature = "rayon"))]
validate_new_node<Ip: IdentityProvider, Cp: CipherSuiteProvider>( &self, leaf_node_validator: &LeafNodeValidator<'_, Ip, Cp>, key_package: &KeyPackage, commit_time: Option<MlsTime>, ) -> Result<(), MlsError>338 pub fn validate_new_node<Ip: IdentityProvider, Cp: CipherSuiteProvider>(
339 &self,
340 leaf_node_validator: &LeafNodeValidator<'_, Ip, Cp>,
341 key_package: &KeyPackage,
342 commit_time: Option<MlsTime>,
343 ) -> Result<(), MlsError> {
344 let (a, b) = rayon::join(
345 || {
346 leaf_node_validator
347 .check_if_valid(&key_package.leaf_node, ValidationContext::Add(commit_time))
348 },
349 || {
350 validate_key_package_properties(
351 key_package,
352 self.protocol_version,
353 self.cipher_suite_provider,
354 )
355 },
356 );
357 a?;
358 b
359 }
360 }
361
362 #[cfg(feature = "psk")]
363 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
filter_out_invalid_psks<P, CP>( #[cfg(feature = "by_ref_proposal")] strategy: FilterStrategy, cipher_suite_provider: &CP, #[cfg(not(feature = "by_ref_proposal"))] proposals: &ProposalBundle, #[cfg(feature = "by_ref_proposal")] proposals: &mut ProposalBundle, psk_storage: &P, ) -> Result<(), MlsError> where P: PreSharedKeyStorage, CP: CipherSuiteProvider,364 pub(crate) async fn filter_out_invalid_psks<P, CP>(
365 #[cfg(feature = "by_ref_proposal")] strategy: FilterStrategy,
366 cipher_suite_provider: &CP,
367 #[cfg(not(feature = "by_ref_proposal"))] proposals: &ProposalBundle,
368 #[cfg(feature = "by_ref_proposal")] proposals: &mut ProposalBundle,
369 psk_storage: &P,
370 ) -> Result<(), MlsError>
371 where
372 P: PreSharedKeyStorage,
373 CP: CipherSuiteProvider,
374 {
375 let kdf_extract_size = cipher_suite_provider.kdf_extract_size();
376
377 #[cfg(feature = "std")]
378 let mut ids_seen = HashSet::new();
379
380 #[cfg(not(feature = "std"))]
381 let mut ids_seen = Vec::new();
382
383 #[cfg(feature = "by_ref_proposal")]
384 let mut bad_indices = Vec::new();
385
386 for i in 0..proposals.psk_proposals().len() {
387 let p = &proposals.psks[i];
388
389 let valid = matches!(
390 p.proposal.psk.key_id,
391 JustPreSharedKeyID::External(_)
392 | JustPreSharedKeyID::Resumption(ResumptionPsk {
393 usage: ResumptionPSKUsage::Application,
394 ..
395 })
396 );
397
398 let nonce_length = p.proposal.psk.psk_nonce.0.len();
399 let nonce_valid = nonce_length == kdf_extract_size;
400
401 #[cfg(feature = "std")]
402 let is_new_id = ids_seen.insert(p.proposal.psk.clone());
403
404 #[cfg(not(feature = "std"))]
405 let is_new_id = !ids_seen.contains(&p.proposal.psk);
406
407 let external_id_is_valid = match &p.proposal.psk.key_id {
408 JustPreSharedKeyID::External(id) => psk_storage
409 .contains(id)
410 .await
411 .map_err(|e| MlsError::PskStoreError(e.into_any_error()))
412 .and_then(|found| {
413 if found {
414 Ok(())
415 } else {
416 Err(MlsError::MissingRequiredPsk)
417 }
418 }),
419 JustPreSharedKeyID::Resumption(_) => Ok(()),
420 };
421
422 #[cfg(not(feature = "by_ref_proposal"))]
423 if !valid {
424 return Err(MlsError::InvalidTypeOrUsageInPreSharedKeyProposal);
425 } else if !nonce_valid {
426 return Err(MlsError::InvalidPskNonceLength);
427 } else if !is_new_id {
428 return Err(MlsError::DuplicatePskIds);
429 } else if external_id_is_valid.is_err() {
430 return external_id_is_valid;
431 }
432
433 #[cfg(feature = "by_ref_proposal")]
434 {
435 let res = if !valid {
436 Err(MlsError::InvalidTypeOrUsageInPreSharedKeyProposal)
437 } else if !nonce_valid {
438 Err(MlsError::InvalidPskNonceLength)
439 } else if !is_new_id {
440 Err(MlsError::DuplicatePskIds)
441 } else {
442 external_id_is_valid
443 };
444
445 if !apply_strategy(strategy, p.is_by_reference(), res)? {
446 bad_indices.push(i)
447 }
448 }
449
450 #[cfg(not(feature = "std"))]
451 ids_seen.push(p.proposal.psk.clone());
452 }
453
454 #[cfg(feature = "by_ref_proposal")]
455 bad_indices
456 .into_iter()
457 .rev()
458 .for_each(|i| proposals.remove::<PreSharedKeyProposal>(i));
459
460 Ok(())
461 }
462
463 #[cfg(not(feature = "psk"))]
464 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
filter_out_invalid_psks<P, CP>( #[cfg(feature = "by_ref_proposal")] _: FilterStrategy, _: &CP, #[cfg(not(feature = "by_ref_proposal"))] _: &ProposalBundle, #[cfg(feature = "by_ref_proposal")] _: &mut ProposalBundle, _: &P, ) -> Result<(), MlsError> where P: PreSharedKeyStorage, CP: CipherSuiteProvider,465 pub(crate) async fn filter_out_invalid_psks<P, CP>(
466 #[cfg(feature = "by_ref_proposal")] _: FilterStrategy,
467 _: &CP,
468 #[cfg(not(feature = "by_ref_proposal"))] _: &ProposalBundle,
469 #[cfg(feature = "by_ref_proposal")] _: &mut ProposalBundle,
470 _: &P,
471 ) -> Result<(), MlsError>
472 where
473 P: PreSharedKeyStorage,
474 CP: CipherSuiteProvider,
475 {
476 Ok(())
477 }
478
ensure_exactly_one_external_init(proposals: &ProposalBundle) -> Result<(), MlsError>479 fn ensure_exactly_one_external_init(proposals: &ProposalBundle) -> Result<(), MlsError> {
480 (proposals.by_type::<ExternalInit>().count() == 1)
481 .then_some(())
482 .ok_or(MlsError::ExternalCommitMustHaveExactlyOneExternalInit)
483 }
484
485 /// Non-default proposal types are by default allowed. Custom MlsRules may disallow
486 /// specific custom proposals in external commits
ensure_proposals_in_external_commit_are_allowed( proposals: &ProposalBundle, ) -> Result<(), MlsError>487 fn ensure_proposals_in_external_commit_are_allowed(
488 proposals: &ProposalBundle,
489 ) -> Result<(), MlsError> {
490 let supported_default_types = [
491 ProposalType::EXTERNAL_INIT,
492 ProposalType::REMOVE,
493 ProposalType::PSK,
494 ];
495
496 let unsupported_type = proposals
497 .proposal_types()
498 .find(|ty| !supported_default_types.contains(ty) && ProposalType::DEFAULT.contains(ty));
499
500 match unsupported_type {
501 Some(kind) => Err(MlsError::InvalidProposalTypeInExternalCommit(kind)),
502 None => Ok(()),
503 }
504 }
505
506 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
ensure_at_most_one_removal_for_self<C>( proposals: &ProposalBundle, external_leaf: &LeafNode, tree: &TreeKemPublic, identity_provider: &C, extensions: &ExtensionList, ) -> Result<(), MlsError> where C: IdentityProvider,507 async fn ensure_at_most_one_removal_for_self<C>(
508 proposals: &ProposalBundle,
509 external_leaf: &LeafNode,
510 tree: &TreeKemPublic,
511 identity_provider: &C,
512 extensions: &ExtensionList,
513 ) -> Result<(), MlsError>
514 where
515 C: IdentityProvider,
516 {
517 let mut removals = proposals.by_type::<RemoveProposal>();
518
519 match (removals.next(), removals.next()) {
520 (Some(removal), None) => {
521 ensure_removal_is_for_self(
522 &removal.proposal,
523 external_leaf,
524 tree,
525 identity_provider,
526 extensions,
527 )
528 .await
529 }
530 (Some(_), Some(_)) => Err(MlsError::ExternalCommitWithMoreThanOneRemove),
531 (None, _) => Ok(()),
532 }
533 }
534
535 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
ensure_removal_is_for_self<C>( removal: &RemoveProposal, external_leaf: &LeafNode, tree: &TreeKemPublic, identity_provider: &C, extensions: &ExtensionList, ) -> Result<(), MlsError> where C: IdentityProvider,536 async fn ensure_removal_is_for_self<C>(
537 removal: &RemoveProposal,
538 external_leaf: &LeafNode,
539 tree: &TreeKemPublic,
540 identity_provider: &C,
541 extensions: &ExtensionList,
542 ) -> Result<(), MlsError>
543 where
544 C: IdentityProvider,
545 {
546 let existing_signing_id = &tree.get_leaf_node(removal.to_remove)?.signing_identity;
547
548 identity_provider
549 .valid_successor(
550 existing_signing_id,
551 &external_leaf.signing_identity,
552 extensions,
553 )
554 .await
555 .map_err(|e| MlsError::IdentityProviderError(e.into_any_error()))?
556 .then_some(())
557 .ok_or(MlsError::ExternalCommitRemovesOtherIdentity)
558 }
559
560 /// Non-default by-ref proposal types are by default allowed. Custom MlsRules may disallow
561 /// specific custom by-ref proposals.
ensure_no_proposal_by_ref(proposals: &ProposalBundle) -> Result<(), MlsError>562 fn ensure_no_proposal_by_ref(proposals: &ProposalBundle) -> Result<(), MlsError> {
563 proposals
564 .iter_proposals()
565 .all(|p| !ProposalType::DEFAULT.contains(&p.proposal.proposal_type()) || p.is_by_value())
566 .then_some(())
567 .ok_or(MlsError::OnlyMembersCanCommitProposalsByRef)
568 }
569
570 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
insert_external_leaf<I: IdentityProvider>( tree: &mut TreeKemPublic, leaf_node: LeafNode, identity_provider: &I, extensions: &ExtensionList, ) -> Result<LeafIndex, MlsError>571 async fn insert_external_leaf<I: IdentityProvider>(
572 tree: &mut TreeKemPublic,
573 leaf_node: LeafNode,
574 identity_provider: &I,
575 extensions: &ExtensionList,
576 ) -> Result<LeafIndex, MlsError> {
577 tree.add_leaf(leaf_node, identity_provider, extensions, None)
578 .await
579 }
580