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