Skip to main content

openmls/group/mls_group/
proposal.rs

1use openmls_traits::{signatures::Signer, storage::StorageProvider as _, types::Ciphersuite};
2
3use super::{
4    errors::{ProposalError, ProposeAddMemberError, ProposeRemoveMemberError, RemoveProposalError},
5    AddProposal, CreateGroupContextExtProposalError, CustomProposal, FramingParameters, MlsGroup,
6    PreSharedKeyProposal, Proposal, QueuedProposal, RemoveProposal, UpdateProposal, WireFormat,
7};
8use crate::{
9    binary_tree::LeafNodeIndex,
10    ciphersuite::hash_ref::ProposalRef,
11    credentials::Credential,
12    error::LibraryError,
13    extensions::Extensions,
14    framing::{mls_auth_content::AuthenticatedContent, MlsMessageOut},
15    group::{errors::CreateAddProposalError, GroupContext, GroupId, ValidationError},
16    key_packages::KeyPackage,
17    messages::{group_info::GroupInfo, proposals::ProposalOrRefType},
18    schedule::PreSharedKeyId,
19    storage::{OpenMlsProvider, StorageProvider},
20    treesync::{LeafNode, LeafNodeParameters},
21    versions::ProtocolVersion,
22};
23
24#[cfg(feature = "extensions-draft")]
25use crate::{
26    component::ComponentId,
27    messages::proposals::{AppDataUpdateOperation, AppDataUpdateProposal},
28};
29
30/// Helper for building a proposal based on the raw values.
31#[derive(Debug, PartialEq, Clone)]
32pub enum Propose {
33    /// An add proposal requires a key package of the addee.
34    Add(KeyPackage),
35
36    /// An update proposal requires a new leaf node.
37    Update(LeafNodeParameters),
38
39    /// A remove proposal consists of the leaf index of the leaf to be removed.
40    Remove(u32),
41
42    /// A remove proposal for the leaf with the credential.
43    RemoveCredential(Credential),
44
45    /// A PSK proposal gets a pre shared key id.
46    PreSharedKey(PreSharedKeyId),
47
48    /// A re-init proposal gets the [`GroupId`], [`ProtocolVersion`], [`Ciphersuite`], and [`Extensions`].
49    ReInit {
50        group_id: GroupId,
51        version: ProtocolVersion,
52        ciphersuite: Ciphersuite,
53        extensions: Extensions<GroupContext>,
54    },
55
56    /// An external init proposal gets the raw bytes from the KEM output.
57    ExternalInit(Vec<u8>),
58
59    /// Propose adding new group context extensions.
60    GroupContextExtensions(Extensions<GroupContext>),
61
62    #[cfg(feature = "extensions-draft")]
63    /// Propose an update to a component in the [`AppDataDictionary`]
64    UpdateAppDataComponent {
65        /// The component_id to update in the dictionary
66        component_id: ComponentId,
67        /// The data representing the update
68        update: Vec<u8>,
69    },
70    #[cfg(feature = "extensions-draft")]
71    /// Propose removal of a component in the [`AppDataDictionary`]
72    RemoveAppDataComponent {
73        /// The component_id to remove in the dictionary
74        component_id: ComponentId,
75    },
76
77    /// A custom proposal with semantics to be implemented by the application.
78    Custom(CustomProposal),
79}
80
81macro_rules! impl_propose_fun {
82    ($name:ident, $value_ty:ty, $group_fun:ident, $ref_or_value:expr) => {
83        // TODO: Documentation wrong.
84        /// Creates proposals to add an external PSK to the key schedule.
85        ///
86        /// Returns an error if there is a pending commit.
87        pub fn $name<Provider: OpenMlsProvider>(
88            &mut self,
89            provider: &Provider,
90            signer: &impl Signer,
91            value: $value_ty,
92        ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
93            self.is_operational()?;
94
95            let aad = self.outgoing_authenticated_data()?;
96            let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
97            let proposal = self.$group_fun(framing_parameters, value, signer)?;
98
99            let queued_proposal = QueuedProposal::from_authenticated_content(
100                self.ciphersuite(),
101                provider.crypto(),
102                proposal.clone(),
103                $ref_or_value,
104            )?;
105            let proposal_ref = queued_proposal.proposal_reference();
106
107            log::trace!("Storing proposal in queue {:?}", queued_proposal);
108            provider
109                .storage()
110                .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
111                .map_err(ProposalError::StorageError)?;
112            self.proposal_store_mut().add(queued_proposal);
113
114            let mls_message = self.content_to_mls_message(proposal, provider)?;
115
116            self.reset_aad();
117            Ok((mls_message, proposal_ref))
118        }
119    };
120}
121
122impl MlsGroup {
123    impl_propose_fun!(
124        propose_add_member_by_value,
125        KeyPackage,
126        create_add_proposal,
127        ProposalOrRefType::Proposal
128    );
129
130    impl_propose_fun!(
131        propose_remove_member_by_value,
132        LeafNodeIndex,
133        create_remove_proposal,
134        ProposalOrRefType::Proposal
135    );
136
137    impl_propose_fun!(
138        propose_pre_shared_key,
139        PreSharedKeyId,
140        create_presharedkey_proposal,
141        ProposalOrRefType::Reference
142    );
143
144    impl_propose_fun!(
145        propose_pre_shared_key_by_value,
146        PreSharedKeyId,
147        create_presharedkey_proposal,
148        ProposalOrRefType::Proposal
149    );
150
151    /// Creates proposals to add a non-resumption PSK to the key schedule.
152    #[deprecated(
153        note = "Renamed to `propose_pre_shared_key`; works for any non-resumption PSK, not just external"
154    )]
155    pub fn propose_external_psk<Provider: OpenMlsProvider>(
156        &mut self,
157        provider: &Provider,
158        signer: &impl Signer,
159        value: PreSharedKeyId,
160    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
161        self.propose_pre_shared_key(provider, signer, value)
162    }
163
164    /// Creates proposals to add a non-resumption PSK to the key schedule by value.
165    #[deprecated(
166        note = "Renamed to `propose_pre_shared_key_by_value`; works for any non-resumption PSK, not just external"
167    )]
168    pub fn propose_external_psk_by_value<Provider: OpenMlsProvider>(
169        &mut self,
170        provider: &Provider,
171        signer: &impl Signer,
172        value: PreSharedKeyId,
173    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
174        self.propose_pre_shared_key_by_value(provider, signer, value)
175    }
176
177    impl_propose_fun!(
178        propose_custom_proposal_by_value,
179        CustomProposal,
180        create_custom_proposal,
181        ProposalOrRefType::Proposal
182    );
183
184    impl_propose_fun!(
185        propose_custom_proposal_by_reference,
186        CustomProposal,
187        create_custom_proposal,
188        ProposalOrRefType::Reference
189    );
190
191    /// Generate a proposal
192    pub fn propose<Provider: OpenMlsProvider>(
193        &mut self,
194        provider: &Provider,
195        signer: &impl Signer,
196        propose: Propose,
197        ref_or_value: ProposalOrRefType,
198    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
199        match propose {
200            Propose::Add(key_package) => match ref_or_value {
201                ProposalOrRefType::Proposal => {
202                    self.propose_add_member_by_value(provider, signer, key_package)
203                }
204                ProposalOrRefType::Reference => self
205                    .propose_add_member(provider, signer, &key_package)
206                    .map_err(|e| e.into()),
207            },
208
209            Propose::Update(leaf_node_parameters) => match ref_or_value {
210                ProposalOrRefType::Proposal => self
211                    .propose_self_update(provider, signer, leaf_node_parameters)
212                    .map_err(|e| e.into()),
213                ProposalOrRefType::Reference => self
214                    .propose_self_update(provider, signer, leaf_node_parameters)
215                    .map_err(|e| e.into()),
216            },
217
218            Propose::Remove(leaf_index) => match ref_or_value {
219                ProposalOrRefType::Proposal => self.propose_remove_member_by_value(
220                    provider,
221                    signer,
222                    LeafNodeIndex::new(leaf_index),
223                ),
224                ProposalOrRefType::Reference => self
225                    .propose_remove_member(provider, signer, LeafNodeIndex::new(leaf_index))
226                    .map_err(|e| e.into()),
227            },
228
229            Propose::RemoveCredential(credential) => match ref_or_value {
230                ProposalOrRefType::Proposal => {
231                    self.propose_remove_member_by_credential_by_value(provider, signer, &credential)
232                }
233                ProposalOrRefType::Reference => self
234                    .propose_remove_member_by_credential(provider, signer, &credential)
235                    .map_err(|e| e.into()),
236            },
237            Propose::PreSharedKey(psk_id) => {
238                match psk_id.psk() {
239                    crate::schedule::Psk::External(_) => {}
240                    #[cfg(feature = "extensions-draft")]
241                    crate::schedule::Psk::Application(_) => {}
242                    crate::schedule::Psk::Resumption(_) => {
243                        return Err(ProposalError::LibraryError(LibraryError::custom(
244                            "Invalid PSk argument",
245                        )))
246                    }
247                };
248                match ref_or_value {
249                    ProposalOrRefType::Proposal => {
250                        self.propose_pre_shared_key_by_value(provider, signer, psk_id)
251                    }
252                    ProposalOrRefType::Reference => {
253                        self.propose_pre_shared_key(provider, signer, psk_id)
254                    }
255                }
256            }
257            Propose::ReInit {
258                group_id: _,
259                version: _,
260                ciphersuite: _,
261                extensions: _,
262            } => Err(ProposalError::LibraryError(LibraryError::custom(
263                "Unsupported proposal type ReInit",
264            ))),
265            Propose::ExternalInit(_) => Err(ProposalError::LibraryError(LibraryError::custom(
266                "Unsupported proposal type ExternalInit",
267            ))),
268            Propose::GroupContextExtensions(_) => Err(ProposalError::LibraryError(
269                LibraryError::custom("Unsupported proposal type GroupContextExtensions"),
270            )),
271            // extensions-draft
272            #[cfg(feature = "extensions-draft")]
273            Propose::UpdateAppDataComponent {
274                component_id,
275                update,
276            } => self.propose_app_data_update(
277                provider,
278                signer,
279                component_id,
280                AppDataUpdateOperation::Update(update.into()),
281            ),
282            #[cfg(feature = "extensions-draft")]
283            Propose::RemoveAppDataComponent { component_id } => self.propose_app_data_update(
284                provider,
285                signer,
286                component_id,
287                AppDataUpdateOperation::Remove,
288            ),
289
290            // custom
291            Propose::Custom(custom_proposal) => match ref_or_value {
292                ProposalOrRefType::Proposal => {
293                    self.propose_custom_proposal_by_value(provider, signer, custom_proposal)
294                }
295                ProposalOrRefType::Reference => {
296                    self.propose_custom_proposal_by_reference(provider, signer, custom_proposal)
297                }
298            },
299        }
300    }
301
302    /// Creates proposals to add members to the group.
303    ///
304    /// Returns an error if there is a pending commit.
305    pub fn propose_add_member<Provider: OpenMlsProvider>(
306        &mut self,
307        provider: &Provider,
308        signer: &impl Signer,
309        key_package: &KeyPackage,
310    ) -> Result<(MlsMessageOut, ProposalRef), ProposeAddMemberError<Provider::StorageError>> {
311        self.is_operational()?;
312
313        let aad = self.outgoing_authenticated_data()?;
314        let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
315        let add_proposal = self
316            .create_add_proposal(framing_parameters, key_package.clone(), signer)
317            .map_err(|e| match e {
318                CreateAddProposalError::LibraryError(e) => e.into(),
319                CreateAddProposalError::LeafNodeValidation(error) => {
320                    ProposeAddMemberError::LeafNodeValidation(error)
321                }
322            })?;
323
324        let proposal = QueuedProposal::from_authenticated_content_by_ref(
325            self.ciphersuite(),
326            provider.crypto(),
327            add_proposal.clone(),
328        )?;
329        let proposal_ref = proposal.proposal_reference();
330        provider
331            .storage()
332            .queue_proposal(self.group_id(), &proposal_ref, &proposal)
333            .map_err(ProposeAddMemberError::StorageError)?;
334        self.proposal_store_mut().add(proposal);
335
336        let mls_message = self.content_to_mls_message(add_proposal, provider)?;
337
338        self.reset_aad();
339        Ok((mls_message, proposal_ref))
340    }
341
342    /// Creates proposals to remove members from the group.
343    /// The `member` has to be the member's leaf index.
344    ///
345    /// Returns an error if there is a pending commit.
346    pub fn propose_remove_member<Provider: OpenMlsProvider>(
347        &mut self,
348        provider: &Provider,
349        signer: &impl Signer,
350        member: LeafNodeIndex,
351    ) -> Result<(MlsMessageOut, ProposalRef), ProposeRemoveMemberError<Provider::StorageError>>
352    {
353        self.is_operational()?;
354
355        let aad = self.outgoing_authenticated_data()?;
356        let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
357        let remove_proposal = self
358            .create_remove_proposal(framing_parameters, member, signer)
359            .map_err(|_| ProposeRemoveMemberError::UnknownMember)?;
360
361        let proposal = QueuedProposal::from_authenticated_content_by_ref(
362            self.ciphersuite(),
363            provider.crypto(),
364            remove_proposal.clone(),
365        )?;
366        let proposal_ref = proposal.proposal_reference();
367        provider
368            .storage()
369            .queue_proposal(self.group_id(), &proposal_ref, &proposal)
370            .map_err(ProposeRemoveMemberError::StorageError)?;
371        self.proposal_store_mut().add(proposal);
372
373        let mls_message = self.content_to_mls_message(remove_proposal, provider)?;
374
375        self.reset_aad();
376        Ok((mls_message, proposal_ref))
377    }
378
379    /// Creates proposals to remove members from the group.
380    /// The `member` has to be the member's credential.
381    ///
382    /// Returns an error if there is a pending commit.
383    pub fn propose_remove_member_by_credential<Provider: OpenMlsProvider>(
384        &mut self,
385        provider: &Provider,
386        signer: &impl Signer,
387        member: &Credential,
388    ) -> Result<(MlsMessageOut, ProposalRef), ProposeRemoveMemberError<Provider::StorageError>>
389    {
390        // Find the user for the credential first.
391        let member_index = self
392            .public_group()
393            .members()
394            .find(|m| &m.credential == member)
395            .map(|m| m.index);
396
397        if let Some(member_index) = member_index {
398            self.propose_remove_member(provider, signer, member_index)
399        } else {
400            Err(ProposeRemoveMemberError::UnknownMember)
401        }
402    }
403
404    /// Creates proposals to remove members from the group.
405    /// The `member` has to be the member's credential.
406    ///
407    /// Returns an error if there is a pending commit.
408    pub fn propose_remove_member_by_credential_by_value<Provider: OpenMlsProvider>(
409        &mut self,
410        provider: &Provider,
411        signer: &impl Signer,
412        member: &Credential,
413    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
414        // Find the user for the credential first.
415        let member_index = self
416            .public_group()
417            .members()
418            .find(|m| &m.credential == member)
419            .map(|m| m.index);
420
421        if let Some(member_index) = member_index {
422            self.propose_remove_member_by_value(provider, signer, member_index)
423        } else {
424            Err(ProposalError::ProposeRemoveMemberError(
425                ProposeRemoveMemberError::UnknownMember,
426            ))
427        }
428    }
429
430    /// Creates a proposals with a new set of `extensions` for the group context.
431    ///
432    /// Returns an error when the group does not support all the required capabilities
433    /// in the new `extensions`.
434    pub fn propose_group_context_extensions<Provider: OpenMlsProvider>(
435        &mut self,
436        provider: &Provider,
437        extensions: Extensions<GroupContext>,
438        signer: &impl Signer,
439    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
440        self.is_operational()?;
441
442        let aad = self.outgoing_authenticated_data()?;
443        let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
444        let proposal = self.create_group_context_ext_proposal::<Provider>(
445            framing_parameters,
446            extensions,
447            signer,
448        )?;
449
450        let queued_proposal = QueuedProposal::from_authenticated_content_by_ref(
451            self.ciphersuite(),
452            provider.crypto(),
453            proposal.clone(),
454        )?;
455
456        let proposal_ref = queued_proposal.proposal_reference();
457        provider
458            .storage()
459            .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
460            .map_err(ProposalError::StorageError)?;
461        self.proposal_store_mut().add(queued_proposal);
462
463        let mls_message = self.content_to_mls_message(proposal, provider)?;
464
465        self.reset_aad();
466        Ok((mls_message, proposal_ref))
467    }
468
469    /// Updates Group Context Extensions
470    ///
471    /// Commits to the Group Context Extension inline proposal using the [`Extensions`]
472    ///
473    /// Returns an error when the group does not support all the required capabilities
474    /// in the new `extensions` or if there is a pending commit.
475    //// FIXME: #1217
476    #[allow(clippy::type_complexity)]
477    pub fn update_group_context_extensions<Provider: OpenMlsProvider>(
478        &mut self,
479        provider: &Provider,
480        extensions: Extensions<GroupContext>,
481        signer: &impl Signer,
482    ) -> Result<
483        (MlsMessageOut, Option<MlsMessageOut>, Option<GroupInfo>),
484        CreateGroupContextExtProposalError<Provider::StorageError>,
485    > {
486        self.is_operational()?;
487
488        // Build and stage Commit containing GroupContextExtensions proposal
489        let bundle = self
490            .commit_builder()
491            .propose_group_context_extensions(extensions)?
492            .load_psks(provider.storage())?
493            .build(provider.rand(), provider.crypto(), signer, |_| true)?
494            .stage_commit(provider)?;
495
496        // Extract messages and convert Welcome to MlsMessageOut
497        let (commit, welcome, group_info) = bundle.into_contents();
498        let welcome = welcome.map(|welcome| MlsMessageOut::from_welcome(welcome, self.version()));
499
500        Ok((commit, welcome, group_info))
501    }
502
503    /// Updates the AppDataDictionary
504    #[cfg(feature = "extensions-draft")]
505    pub fn propose_app_data_update<Provider: OpenMlsProvider>(
506        &mut self,
507        provider: &Provider,
508        signer: &impl Signer,
509        component_id: ComponentId,
510        operation: AppDataUpdateOperation,
511    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
512        self.is_operational()?;
513
514        let aad = self.outgoing_authenticated_data()?;
515        let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
516        let proposal = self.create_app_data_update_proposal(
517            framing_parameters,
518            component_id,
519            operation,
520            signer,
521        )?;
522
523        let queued_proposal = QueuedProposal::from_authenticated_content(
524            self.ciphersuite(),
525            provider.crypto(),
526            proposal.clone(),
527            ProposalOrRefType::Proposal,
528        )?;
529        let proposal_ref = queued_proposal.proposal_reference();
530
531        log::trace!("Storing proposal in queue {:?}", queued_proposal);
532        provider
533            .storage()
534            .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
535            .map_err(ProposalError::StorageError)?;
536        self.proposal_store_mut().add(queued_proposal);
537
538        let mls_message = self.content_to_mls_message(proposal, provider)?;
539
540        self.reset_aad();
541        Ok((mls_message, proposal_ref))
542    }
543
544    /// Removes a specific proposal from the store.
545    pub fn remove_pending_proposal<Storage: StorageProvider>(
546        &mut self,
547        storage: &Storage,
548        proposal_ref: &ProposalRef,
549    ) -> Result<(), RemoveProposalError<Storage::Error>> {
550        storage
551            .remove_proposal(self.group_id(), proposal_ref)
552            .map_err(RemoveProposalError::Storage)?;
553        self.proposal_store_mut()
554            .remove(proposal_ref)
555            .ok_or(RemoveProposalError::ProposalNotFound)
556    }
557
558    // === Create handshake messages ===
559
560    // 12.1.1. Add
561    // struct {
562    //     KeyPackage key_package;
563    // } Add;
564    pub(crate) fn create_add_proposal(
565        &self,
566        framing_parameters: FramingParameters,
567        joiner_key_package: KeyPackage,
568        signer: &impl Signer,
569    ) -> Result<AuthenticatedContent, CreateAddProposalError> {
570        if let Some(required_capabilities) = self.required_capabilities() {
571            joiner_key_package
572                .leaf_node()
573                .capabilities()
574                .supports_required_capabilities(required_capabilities)?;
575        }
576        let add_proposal = AddProposal {
577            key_package: joiner_key_package,
578        };
579        let proposal = Proposal::add(add_proposal);
580        AuthenticatedContent::member_proposal(
581            framing_parameters,
582            self.own_leaf_index(),
583            proposal,
584            self.context(),
585            signer,
586        )
587        .map_err(|e| e.into())
588    }
589
590    // 12.1.2. Update
591    // struct {
592    //     LeafNode leaf_node;
593    // } Update;
594    pub(crate) fn create_update_proposal(
595        &self,
596        framing_parameters: FramingParameters,
597        // XXX: There's no need to own this. The [`UpdateProposal`] should
598        //      operate on a reference to make this more efficient.
599        leaf_node: LeafNode,
600        signer: &impl Signer,
601    ) -> Result<AuthenticatedContent, LibraryError> {
602        let update_proposal = UpdateProposal { leaf_node };
603        let proposal = Proposal::update(update_proposal);
604        AuthenticatedContent::member_proposal(
605            framing_parameters,
606            self.own_leaf_index(),
607            proposal,
608            self.context(),
609            signer,
610        )
611    }
612
613    // 12.1.3. Remove
614    // struct {
615    //     uint32 removed;
616    // } Remove;
617    pub(crate) fn create_remove_proposal(
618        &self,
619        framing_parameters: FramingParameters,
620        removed: LeafNodeIndex,
621        signer: &impl Signer,
622    ) -> Result<AuthenticatedContent, ValidationError> {
623        if self.public_group().leaf(removed).is_none() {
624            return Err(ValidationError::UnknownMember);
625        }
626        let remove_proposal = RemoveProposal { removed };
627        let proposal = Proposal::remove(remove_proposal);
628        AuthenticatedContent::member_proposal(
629            framing_parameters,
630            self.own_leaf_index(),
631            proposal,
632            self.context(),
633            signer,
634        )
635        .map_err(ValidationError::LibraryError)
636    }
637
638    /// Create a SelfRemove proposal. Note that SelfRemove proposals are always
639    /// sent as PublicMessages.
640    pub(crate) fn create_self_remove_proposal(
641        &self,
642        aad: &[u8],
643        signer: &impl Signer,
644    ) -> Result<AuthenticatedContent, LibraryError> {
645        let proposal = Proposal::SelfRemove;
646        let framing_parameters = FramingParameters::new(aad, WireFormat::PublicMessage);
647        AuthenticatedContent::member_proposal(
648            framing_parameters,
649            self.own_leaf_index(),
650            proposal,
651            self.context(),
652            signer,
653        )
654    }
655
656    // 12.1.4. PreSharedKey
657    // struct {
658    //     PreSharedKeyID psk;
659    // } PreSharedKey;
660    // TODO: #751
661    pub(crate) fn create_presharedkey_proposal(
662        &self,
663        framing_parameters: FramingParameters,
664        psk: PreSharedKeyId,
665        signer: &impl Signer,
666    ) -> Result<AuthenticatedContent, LibraryError> {
667        let presharedkey_proposal = PreSharedKeyProposal::new(psk);
668        let proposal = Proposal::psk(presharedkey_proposal);
669        AuthenticatedContent::member_proposal(
670            framing_parameters,
671            self.own_leaf_index(),
672            proposal,
673            self.context(),
674            signer,
675        )
676    }
677
678    #[cfg(feature = "extensions-draft")]
679    pub(crate) fn create_app_data_update_proposal(
680        &self,
681        framing_parameters: FramingParameters,
682        component_id: ComponentId,
683        operation: AppDataUpdateOperation,
684        signer: &impl Signer,
685    ) -> Result<AuthenticatedContent, LibraryError> {
686        let proposal = Proposal::AppDataUpdate(Box::new(AppDataUpdateProposal::new(
687            component_id,
688            operation,
689        )));
690        AuthenticatedContent::member_proposal(
691            framing_parameters,
692            self.own_leaf_index(),
693            proposal,
694            self.context(),
695            signer,
696        )
697    }
698
699    pub(crate) fn create_custom_proposal(
700        &self,
701        framing_parameters: FramingParameters,
702        custom_proposal: CustomProposal,
703        signer: &impl Signer,
704    ) -> Result<AuthenticatedContent, LibraryError> {
705        let proposal = Proposal::custom(custom_proposal);
706        AuthenticatedContent::member_proposal(
707            framing_parameters,
708            self.own_leaf_index(),
709            proposal,
710            self.context(),
711            signer,
712        )
713    }
714}