Skip to main content

openmls/group/mls_group/
proposal.rs

1use openmls_traits::{signatures::Signer, storage::StorageProvider as _};
2
3#[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
4use super::CreateGroupContextExtProposalError;
5#[cfg(feature = "virtual-clients-draft")]
6use super::HandshakeConfirmationData;
7use super::{
8    errors::{ProposalError, ProposeAddMemberError, ProposeRemoveMemberError, RemoveProposalError},
9    AddProposal, CustomProposal, FramingParameters, HandshakeFramingOutput, MlsGroup,
10    PreSharedKeyProposal, Proposal, QueuedProposal, ReInitProposal, RemoveProposal, UpdateProposal,
11    WireFormat,
12};
13#[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
14use crate::messages::group_info::GroupInfo;
15use crate::{
16    binary_tree::LeafNodeIndex,
17    ciphersuite::hash_ref::ProposalRef,
18    credentials::{Credential, NewSignerBundle},
19    error::LibraryError,
20    extensions::Extensions,
21    framing::{mls_auth_content::AuthenticatedContent, MlsMessageOut},
22    group::{errors::CreateAddProposalError, GroupContext, ValidationError},
23    key_packages::KeyPackage,
24    messages::proposals::ProposalOrRefType,
25    schedule::PreSharedKeyId,
26    storage::{OpenMlsProvider, StorageProvider},
27    treesync::{LeafNode, LeafNodeParameters},
28};
29
30#[cfg(feature = "extensions-draft")]
31use crate::{
32    component::ComponentId,
33    messages::proposals::{AppDataUpdateOperation, AppDataUpdateProposal},
34};
35
36/// Helper for building a proposal based on the raw values.
37#[derive(Debug, PartialEq, Clone)]
38pub enum Propose {
39    /// An add proposal requires a key package of the addee.
40    Add(KeyPackage),
41
42    /// An update proposal requires a new leaf node.
43    ///
44    /// An update proposal is always queued by reference as per RFC 9420 a
45    /// member must not commit its own update proposal.
46    Update(LeafNodeParameters),
47
48    /// A remove proposal consists of the leaf index of the leaf to be removed.
49    Remove(u32),
50
51    /// A remove proposal for the leaf with the credential.
52    RemoveCredential(Credential),
53
54    /// A PSK proposal gets a pre shared key id.
55    PreSharedKey(PreSharedKeyId),
56
57    /// Propose adding new group context extensions.
58    GroupContextExtensions(Extensions<GroupContext>),
59
60    #[cfg(feature = "extensions-draft")]
61    /// Propose an update to a component in the
62    /// [`AppDataDictionary`](crate::extensions::AppDataDictionary)
63    UpdateAppDataComponent {
64        /// The component_id to update in the dictionary
65        component_id: ComponentId,
66        /// The data representing the update
67        update: Vec<u8>,
68    },
69    #[cfg(feature = "extensions-draft")]
70    /// Propose removal of a component in the
71    /// [`AppDataDictionary`](crate::extensions::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, $impl_name:ident, $value_ty:ty, $group_fun:ident, $ref_or_value:expr, $doc:expr) => {
83        /// Builds the proposal, queues it, and frames it, returning the framing
84        /// output so callers can surface the handshake confirmation data.
85        #[openmls_traits::maybe_async]
86        async fn $impl_name<Provider: OpenMlsProvider>(
87            &mut self,
88            provider: &Provider,
89            signer: &impl Signer,
90            value: $value_ty,
91        ) -> Result<(HandshakeFramingOutput, ProposalRef), ProposalError<Provider::StorageError>> {
92            self.is_operational()?;
93
94            let aad = self.outgoing_authenticated_data()?;
95            let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
96            let proposal = self.$group_fun(framing_parameters, value, signer)?;
97
98            let queued_proposal = QueuedProposal::from_authenticated_content(
99                self.ciphersuite(),
100                provider.crypto(),
101                proposal.clone(),
102                $ref_or_value,
103            )?;
104            let proposal_ref = queued_proposal.proposal_reference();
105
106            log::trace!("Storing proposal in queue {:?}", queued_proposal);
107            provider
108                .storage()
109                .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
110                .await
111                .map_err(ProposalError::StorageError)?;
112            self.proposal_store_mut().add(queued_proposal);
113
114            let framing = self.content_to_mls_message(proposal, provider).await?;
115
116            self.reset_aad();
117            Ok((framing, proposal_ref))
118        }
119
120        #[doc = $doc]
121        ///
122        /// Returns an error if there is a pending commit.
123        ///
124        /// Under the `virtual-clients-draft` feature this function is
125        /// unavailable. Use [`Self::propose_unconfirmed`], which retains the
126        /// handshake secret and returns the confirmation data.
127        #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
128        #[openmls_traits::maybe_async]
129        pub async fn $name<Provider: OpenMlsProvider>(
130            &mut self,
131            provider: &Provider,
132            signer: &impl Signer,
133            value: $value_ty,
134        ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
135            let (framing, proposal_ref) = self.$impl_name(provider, signer, value).await?;
136            Ok((framing.message, proposal_ref))
137        }
138    };
139}
140
141impl MlsGroup {
142    impl_propose_fun!(
143        propose_add_member_by_value,
144        propose_add_member_by_value_impl,
145        KeyPackage,
146        create_add_proposal,
147        ProposalOrRefType::Proposal,
148        "Creates a proposal to add a member to the group, committed by value."
149    );
150
151    impl_propose_fun!(
152        propose_remove_member_by_value,
153        propose_remove_member_by_value_impl,
154        LeafNodeIndex,
155        create_remove_proposal,
156        ProposalOrRefType::Proposal,
157        "Creates a proposal to remove a member from the group, committed by value."
158    );
159
160    impl_propose_fun!(
161        propose_pre_shared_key,
162        propose_pre_shared_key_impl,
163        PreSharedKeyId,
164        create_presharedkey_proposal,
165        ProposalOrRefType::Reference,
166        "Creates a proposal to add a pre-shared key to the key schedule, committed by reference."
167    );
168
169    impl_propose_fun!(
170        propose_pre_shared_key_by_value,
171        propose_pre_shared_key_by_value_impl,
172        PreSharedKeyId,
173        create_presharedkey_proposal,
174        ProposalOrRefType::Proposal,
175        "Creates a proposal to add a pre-shared key to the key schedule, committed by value."
176    );
177
178    /// Creates proposals to add a non-resumption PSK to the key schedule.
179    #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
180    #[deprecated(
181        note = "Renamed to `propose_pre_shared_key`; works for any non-resumption PSK, not just external"
182    )]
183    #[openmls_traits::maybe_async]
184    pub async fn propose_external_psk<Provider: OpenMlsProvider>(
185        &mut self,
186        provider: &Provider,
187        signer: &impl Signer,
188        value: PreSharedKeyId,
189    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
190        self.propose_pre_shared_key(provider, signer, value).await
191    }
192
193    /// Creates proposals to add a non-resumption PSK to the key schedule by value.
194    #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
195    #[deprecated(
196        note = "Renamed to `propose_pre_shared_key_by_value`; works for any non-resumption PSK, not just external"
197    )]
198    #[openmls_traits::maybe_async]
199    pub async fn propose_external_psk_by_value<Provider: OpenMlsProvider>(
200        &mut self,
201        provider: &Provider,
202        signer: &impl Signer,
203        value: PreSharedKeyId,
204    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
205        self.propose_pre_shared_key_by_value(provider, signer, value)
206            .await
207    }
208
209    impl_propose_fun!(
210        propose_custom_proposal_by_value,
211        propose_custom_proposal_by_value_impl,
212        CustomProposal,
213        create_custom_proposal,
214        ProposalOrRefType::Proposal,
215        "Creates a custom proposal, committed by value."
216    );
217
218    impl_propose_fun!(
219        propose_custom_proposal_by_reference,
220        propose_custom_proposal_by_reference_impl,
221        CustomProposal,
222        create_custom_proposal,
223        ProposalOrRefType::Reference,
224        "Creates a custom proposal, committed by reference."
225    );
226
227    /// Generate a proposal.
228    ///
229    /// Under the `virtual-clients-draft` feature this function is unavailable.
230    /// Use [`Self::propose_unconfirmed`], which retains the handshake secret and
231    /// returns the confirmation data.
232    #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
233    #[openmls_traits::maybe_async]
234    pub async fn propose<Provider: OpenMlsProvider>(
235        &mut self,
236        provider: &Provider,
237        signer: &impl Signer,
238        propose: Propose,
239        ref_or_value: ProposalOrRefType,
240    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
241        let (framing, proposal_ref) = self
242            .propose_dispatch(provider, signer, propose, ref_or_value)
243            .await?;
244        Ok((framing.message, proposal_ref))
245    }
246
247    /// Like [`Self::propose`], but retains the handshake secret and returns the
248    /// [`HandshakeConfirmationData`] alongside the framed proposal, so a
249    /// virtual client can confirm the proposal with
250    /// [`MlsGroup::confirm_handshake_message`] once the DS has accepted it. The
251    /// confirmation is `None` for a proposal framed as a plaintext
252    /// PublicMessage.
253    ///
254    /// [`MlsGroup::confirm_handshake_message`]: crate::group::MlsGroup::confirm_handshake_message
255    #[cfg(feature = "virtual-clients-draft")]
256    #[openmls_traits::maybe_async]
257    pub async fn propose_unconfirmed<Provider: OpenMlsProvider>(
258        &mut self,
259        provider: &Provider,
260        signer: &impl Signer,
261        propose: Propose,
262        ref_or_value: ProposalOrRefType,
263    ) -> Result<
264        (
265            MlsMessageOut,
266            ProposalRef,
267            Option<HandshakeConfirmationData>,
268        ),
269        ProposalError<Provider::StorageError>,
270    > {
271        let (framing, proposal_ref) = self
272            .propose_dispatch(provider, signer, propose, ref_or_value)
273            .await?;
274        Ok((framing.message, proposal_ref, framing.confirmation))
275    }
276
277    /// Shared dispatch for [`Self::propose`] and [`Self::propose_unconfirmed`].
278    /// Routes to the per-proposal-type helpers and returns the framing output
279    /// so the caller can decide whether to surface the confirmation data.
280    #[openmls_traits::maybe_async]
281    async fn propose_dispatch<Provider: OpenMlsProvider, S: Signer>(
282        &mut self,
283        provider: &Provider,
284        signer: &S,
285        propose: Propose,
286        ref_or_value: ProposalOrRefType,
287    ) -> Result<(HandshakeFramingOutput, ProposalRef), ProposalError<Provider::StorageError>> {
288        match propose {
289            Propose::Add(key_package) => match ref_or_value {
290                ProposalOrRefType::Proposal => {
291                    self.propose_add_member_by_value_impl(provider, signer, key_package)
292                        .await
293                }
294                ProposalOrRefType::Reference => self
295                    .propose_add_member_impl(provider, signer, &key_package)
296                    .await
297                    .map_err(|e| e.into()),
298            },
299
300            Propose::Update(leaf_node_parameters) => self
301                .propose_self_update_internal(
302                    provider,
303                    signer,
304                    None::<NewSignerBundle<'_, S>>,
305                    leaf_node_parameters,
306                )
307                .await
308                .map_err(|e| e.into()),
309
310            Propose::Remove(leaf_index) => match ref_or_value {
311                ProposalOrRefType::Proposal => {
312                    self.propose_remove_member_by_value_impl(
313                        provider,
314                        signer,
315                        LeafNodeIndex::new(leaf_index),
316                    )
317                    .await
318                }
319                ProposalOrRefType::Reference => self
320                    .propose_remove_member_impl(provider, signer, LeafNodeIndex::new(leaf_index))
321                    .await
322                    .map_err(|e| e.into()),
323            },
324
325            Propose::RemoveCredential(credential) => match ref_or_value {
326                ProposalOrRefType::Proposal => {
327                    self.propose_remove_member_by_credential_by_value_impl(
328                        provider,
329                        signer,
330                        &credential,
331                    )
332                    .await
333                }
334                ProposalOrRefType::Reference => self
335                    .propose_remove_member_by_credential_impl(provider, signer, &credential)
336                    .await
337                    .map_err(|e| e.into()),
338            },
339            Propose::PreSharedKey(psk_id) => {
340                match psk_id.psk() {
341                    crate::schedule::Psk::External(_) => {}
342                    #[cfg(feature = "extensions-draft")]
343                    crate::schedule::Psk::Application(_) => {}
344                    crate::schedule::Psk::Resumption(_) => {
345                        return Err(ProposalError::LibraryError(LibraryError::custom(
346                            "Invalid PSk argument",
347                        )))
348                    }
349                };
350                match ref_or_value {
351                    ProposalOrRefType::Proposal => {
352                        self.propose_pre_shared_key_by_value_impl(provider, signer, psk_id)
353                            .await
354                    }
355                    ProposalOrRefType::Reference => {
356                        self.propose_pre_shared_key_impl(provider, signer, psk_id)
357                            .await
358                    }
359                }
360            }
361            Propose::GroupContextExtensions(extensions) => {
362                self.propose_group_context_extensions_impl(
363                    provider,
364                    extensions,
365                    signer,
366                    ref_or_value,
367                )
368                .await
369            }
370            // extensions-draft
371            #[cfg(feature = "extensions-draft")]
372            Propose::UpdateAppDataComponent {
373                component_id,
374                update,
375            } => {
376                self.propose_app_data_update_impl(
377                    provider,
378                    signer,
379                    component_id,
380                    AppDataUpdateOperation::Update(update.into()),
381                    ref_or_value,
382                )
383                .await
384            }
385            #[cfg(feature = "extensions-draft")]
386            Propose::RemoveAppDataComponent { component_id } => {
387                self.propose_app_data_update_impl(
388                    provider,
389                    signer,
390                    component_id,
391                    AppDataUpdateOperation::Remove,
392                    ref_or_value,
393                )
394                .await
395            }
396
397            // custom
398            Propose::Custom(custom_proposal) => match ref_or_value {
399                ProposalOrRefType::Proposal => {
400                    self.propose_custom_proposal_by_value_impl(provider, signer, custom_proposal)
401                        .await
402                }
403                ProposalOrRefType::Reference => {
404                    self.propose_custom_proposal_by_reference_impl(
405                        provider,
406                        signer,
407                        custom_proposal,
408                    )
409                    .await
410                }
411            },
412        }
413    }
414
415    /// Creates proposals to add members to the group.
416    ///
417    /// Returns an error if there is a pending commit.
418    ///
419    /// Under the `virtual-clients-draft` feature this function is unavailable.
420    /// Use [`Self::propose_unconfirmed`], which retains the handshake secret and
421    /// returns the confirmation data.
422    #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
423    #[openmls_traits::maybe_async]
424    pub async fn propose_add_member<Provider: OpenMlsProvider>(
425        &mut self,
426        provider: &Provider,
427        signer: &impl Signer,
428        key_package: &KeyPackage,
429    ) -> Result<(MlsMessageOut, ProposalRef), ProposeAddMemberError<Provider::StorageError>> {
430        let (framing, proposal_ref) = self
431            .propose_add_member_impl(provider, signer, key_package)
432            .await?;
433        Ok((framing.message, proposal_ref))
434    }
435
436    #[openmls_traits::maybe_async]
437    async fn propose_add_member_impl<Provider: OpenMlsProvider>(
438        &mut self,
439        provider: &Provider,
440        signer: &impl Signer,
441        key_package: &KeyPackage,
442    ) -> Result<(HandshakeFramingOutput, ProposalRef), ProposeAddMemberError<Provider::StorageError>>
443    {
444        self.is_operational()?;
445
446        let aad = self.outgoing_authenticated_data()?;
447        let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
448        let add_proposal = self
449            .create_add_proposal(framing_parameters, key_package.clone(), signer)
450            .map_err(|e| match e {
451                CreateAddProposalError::LibraryError(e) => e.into(),
452                CreateAddProposalError::LeafNodeValidation(error) => {
453                    ProposeAddMemberError::LeafNodeValidation(error)
454                }
455            })?;
456
457        let proposal = QueuedProposal::from_authenticated_content_by_ref(
458            self.ciphersuite(),
459            provider.crypto(),
460            add_proposal.clone(),
461        )?;
462        let proposal_ref = proposal.proposal_reference();
463        provider
464            .storage()
465            .queue_proposal(self.group_id(), &proposal_ref, &proposal)
466            .await
467            .map_err(ProposeAddMemberError::StorageError)?;
468        self.proposal_store_mut().add(proposal);
469
470        let framing = self.content_to_mls_message(add_proposal, provider).await?;
471
472        self.reset_aad();
473        Ok((framing, proposal_ref))
474    }
475
476    /// Creates proposals to remove members from the group.
477    /// The `member` has to be the member's leaf index.
478    ///
479    /// Returns an error if there is a pending commit.
480    ///
481    /// Under the `virtual-clients-draft` feature this function is unavailable.
482    /// Use [`Self::propose_unconfirmed`], which retains the handshake secret and
483    /// returns the confirmation data.
484    #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
485    #[openmls_traits::maybe_async]
486    pub async fn propose_remove_member<Provider: OpenMlsProvider>(
487        &mut self,
488        provider: &Provider,
489        signer: &impl Signer,
490        member: LeafNodeIndex,
491    ) -> Result<(MlsMessageOut, ProposalRef), ProposeRemoveMemberError<Provider::StorageError>>
492    {
493        let (framing, proposal_ref) = self
494            .propose_remove_member_impl(provider, signer, member)
495            .await?;
496        Ok((framing.message, proposal_ref))
497    }
498
499    #[openmls_traits::maybe_async]
500    async fn propose_remove_member_impl<Provider: OpenMlsProvider>(
501        &mut self,
502        provider: &Provider,
503        signer: &impl Signer,
504        member: LeafNodeIndex,
505    ) -> Result<
506        (HandshakeFramingOutput, ProposalRef),
507        ProposeRemoveMemberError<Provider::StorageError>,
508    > {
509        self.is_operational()?;
510
511        let aad = self.outgoing_authenticated_data()?;
512        let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
513        let remove_proposal = self
514            .create_remove_proposal(framing_parameters, member, signer)
515            .map_err(|_| ProposeRemoveMemberError::UnknownMember)?;
516
517        let proposal = QueuedProposal::from_authenticated_content_by_ref(
518            self.ciphersuite(),
519            provider.crypto(),
520            remove_proposal.clone(),
521        )?;
522        let proposal_ref = proposal.proposal_reference();
523        provider
524            .storage()
525            .queue_proposal(self.group_id(), &proposal_ref, &proposal)
526            .await
527            .map_err(ProposeRemoveMemberError::StorageError)?;
528        self.proposal_store_mut().add(proposal);
529
530        let framing = self
531            .content_to_mls_message(remove_proposal, provider)
532            .await?;
533
534        self.reset_aad();
535        Ok((framing, proposal_ref))
536    }
537
538    /// Creates proposals to remove members from the group.
539    /// The `member` has to be the member's credential.
540    ///
541    /// Returns an error if there is a pending commit.
542    ///
543    /// Under the `virtual-clients-draft` feature this function is unavailable.
544    /// Use [`Self::propose_unconfirmed`], which retains the handshake secret and
545    /// returns the confirmation data.
546    #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
547    #[openmls_traits::maybe_async]
548    pub async fn propose_remove_member_by_credential<Provider: OpenMlsProvider>(
549        &mut self,
550        provider: &Provider,
551        signer: &impl Signer,
552        member: &Credential,
553    ) -> Result<(MlsMessageOut, ProposalRef), ProposeRemoveMemberError<Provider::StorageError>>
554    {
555        let (framing, proposal_ref) = self
556            .propose_remove_member_by_credential_impl(provider, signer, member)
557            .await?;
558        Ok((framing.message, proposal_ref))
559    }
560
561    #[openmls_traits::maybe_async]
562    async fn propose_remove_member_by_credential_impl<Provider: OpenMlsProvider>(
563        &mut self,
564        provider: &Provider,
565        signer: &impl Signer,
566        member: &Credential,
567    ) -> Result<
568        (HandshakeFramingOutput, ProposalRef),
569        ProposeRemoveMemberError<Provider::StorageError>,
570    > {
571        // Find the user for the credential first.
572        let member_index = self
573            .public_group()
574            .members()
575            .find(|m| &m.credential == member)
576            .map(|m| m.index);
577
578        if let Some(member_index) = member_index {
579            self.propose_remove_member_impl(provider, signer, member_index)
580                .await
581        } else {
582            Err(ProposeRemoveMemberError::UnknownMember)
583        }
584    }
585
586    /// Creates proposals to remove members from the group.
587    /// The `member` has to be the member's credential.
588    ///
589    /// Returns an error if there is a pending commit.
590    ///
591    /// Under the `virtual-clients-draft` feature this function is unavailable.
592    /// Use [`Self::propose_unconfirmed`], which retains the handshake secret and
593    /// returns the confirmation data.
594    #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
595    #[openmls_traits::maybe_async]
596    pub async fn propose_remove_member_by_credential_by_value<Provider: OpenMlsProvider>(
597        &mut self,
598        provider: &Provider,
599        signer: &impl Signer,
600        member: &Credential,
601    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
602        let (framing, proposal_ref) = self
603            .propose_remove_member_by_credential_by_value_impl(provider, signer, member)
604            .await?;
605        Ok((framing.message, proposal_ref))
606    }
607
608    #[openmls_traits::maybe_async]
609    async fn propose_remove_member_by_credential_by_value_impl<Provider: OpenMlsProvider>(
610        &mut self,
611        provider: &Provider,
612        signer: &impl Signer,
613        member: &Credential,
614    ) -> Result<(HandshakeFramingOutput, ProposalRef), ProposalError<Provider::StorageError>> {
615        // Find the user for the credential first.
616        let member_index = self
617            .public_group()
618            .members()
619            .find(|m| &m.credential == member)
620            .map(|m| m.index);
621
622        if let Some(member_index) = member_index {
623            self.propose_remove_member_by_value_impl(provider, signer, member_index)
624                .await
625        } else {
626            Err(ProposalError::ProposeRemoveMemberError(
627                ProposeRemoveMemberError::UnknownMember,
628            ))
629        }
630    }
631
632    /// Creates a proposals with a new set of `extensions` for the group context.
633    ///
634    /// Returns an error when the group does not support all the required capabilities
635    /// in the new `extensions`.
636    ///
637    /// Under the `virtual-clients-draft` feature this function is unavailable.
638    /// Use [`Self::propose_unconfirmed`] with
639    /// [`Propose::GroupContextExtensions`], which retains the handshake secret
640    /// and returns the confirmation data.
641    #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
642    #[openmls_traits::maybe_async]
643    pub async fn propose_group_context_extensions<Provider: OpenMlsProvider>(
644        &mut self,
645        provider: &Provider,
646        extensions: Extensions<GroupContext>,
647        signer: &impl Signer,
648    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
649        let (framing, proposal_ref) = self
650            .propose_group_context_extensions_impl(
651                provider,
652                extensions,
653                signer,
654                ProposalOrRefType::Reference,
655            )
656            .await?;
657        Ok((framing.message, proposal_ref))
658    }
659
660    #[openmls_traits::maybe_async]
661    async fn propose_group_context_extensions_impl<Provider: OpenMlsProvider>(
662        &mut self,
663        provider: &Provider,
664        extensions: Extensions<GroupContext>,
665        signer: &impl Signer,
666        ref_or_value: ProposalOrRefType,
667    ) -> Result<(HandshakeFramingOutput, ProposalRef), ProposalError<Provider::StorageError>> {
668        self.is_operational()?;
669
670        let aad = self.outgoing_authenticated_data()?;
671        let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
672        let proposal = self.create_group_context_ext_proposal::<Provider>(
673            framing_parameters,
674            extensions,
675            signer,
676        )?;
677
678        let queued_proposal = QueuedProposal::from_authenticated_content(
679            self.ciphersuite(),
680            provider.crypto(),
681            proposal.clone(),
682            ref_or_value,
683        )?;
684
685        let proposal_ref = queued_proposal.proposal_reference();
686        provider
687            .storage()
688            .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
689            .await
690            .map_err(ProposalError::StorageError)?;
691        self.proposal_store_mut().add(queued_proposal);
692
693        let framing = self.content_to_mls_message(proposal, provider).await?;
694
695        self.reset_aad();
696        Ok((framing, proposal_ref))
697    }
698
699    /// Creates proposals to reinitialize the group into a successor group with
700    /// the parameters described by `reinit_proposal` (a new group id, protocol
701    /// version, ciphersuite and group context extensions).
702    ///
703    /// Committing to a ReInit proposal suspends the group: once the commit is
704    /// merged, the group becomes inoperable and can only be used to seed the
705    /// successor group (see [`MlsGroupBuilder::reinit`] and
706    /// [`StagedWelcome::build_from_reinit`]).
707    ///
708    /// Returns an error if the group is not operational (e.g. there is a pending
709    /// commit).
710    ///
711    /// [`MlsGroupBuilder::reinit`]: crate::group::MlsGroupBuilder::reinit
712    /// [`StagedWelcome::build_from_reinit`]: crate::group::StagedWelcome::build_from_reinit
713    #[openmls_traits::maybe_async]
714    pub async fn propose_reinit<Provider: OpenMlsProvider>(
715        &mut self,
716        provider: &Provider,
717        reinit_proposal: ReInitProposal,
718        signer: &impl Signer,
719    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
720        self.is_operational()?;
721
722        let aad = self.outgoing_authenticated_data()?;
723        let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
724        let proposal = self.create_reinit_proposal(framing_parameters, reinit_proposal, signer)?;
725
726        let queued_proposal = QueuedProposal::from_authenticated_content(
727            self.ciphersuite(),
728            provider.crypto(),
729            proposal.clone(),
730            ProposalOrRefType::Reference,
731        )?;
732
733        let proposal_ref = queued_proposal.proposal_reference();
734        provider
735            .storage()
736            .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
737            .await
738            .map_err(ProposalError::StorageError)?;
739        self.proposal_store_mut().add(queued_proposal);
740
741        let framing = self.content_to_mls_message(proposal, provider).await?;
742
743        self.reset_aad();
744        Ok((framing.message, proposal_ref))
745    }
746
747    /// Updates Group Context Extensions
748    ///
749    /// Commits to the Group Context Extension inline proposal using the [`Extensions`]
750    ///
751    /// Returns an error when the group does not support all the required capabilities
752    /// in the new `extensions` or if there is a pending commit.
753    ///
754    /// Under the `virtual-clients-draft` feature this function is unavailable.
755    /// Use [`MlsGroup::commit_builder`], whose
756    /// [`CommitMessageBundle::confirmation`](crate::group::CommitMessageBundle::confirmation)
757    /// surfaces the handshake confirmation data.
758    //// FIXME: #1217
759    #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
760    #[allow(clippy::type_complexity)]
761    #[openmls_traits::maybe_async]
762    pub async fn update_group_context_extensions<Provider: OpenMlsProvider>(
763        &mut self,
764        provider: &Provider,
765        extensions: Extensions<GroupContext>,
766        signer: &impl Signer,
767    ) -> Result<
768        (MlsMessageOut, Option<MlsMessageOut>, Option<GroupInfo>),
769        CreateGroupContextExtProposalError<Provider::StorageError>,
770    > {
771        self.is_operational()?;
772
773        // Build and stage Commit containing GroupContextExtensions proposal
774        let bundle = self
775            .commit_builder()
776            .propose_group_context_extensions(extensions)?
777            .load_psks(provider.storage())
778            .await?
779            .build(provider.rand(), provider.crypto(), signer, |_| true)?
780            .stage_commit(provider)
781            .await?;
782
783        // Extract messages and convert Welcome to MlsMessageOut
784        let (commit, welcome, group_info) = bundle.into_contents();
785        let welcome = welcome.map(|welcome| MlsMessageOut::from_welcome(welcome, self.version()));
786
787        Ok((commit, welcome, group_info))
788    }
789
790    /// Updates the AppDataDictionary, committed by value.
791    ///
792    /// Under the `virtual-clients-draft` feature this function is unavailable.
793    /// Use [`Self::propose_unconfirmed`], which retains the handshake secret and
794    /// returns the confirmation data.
795    #[cfg(all(
796        feature = "extensions-draft",
797        any(not(feature = "virtual-clients-draft"), feature = "test-utils", test)
798    ))]
799    #[openmls_traits::maybe_async]
800    pub async fn propose_app_data_update<Provider: OpenMlsProvider>(
801        &mut self,
802        provider: &Provider,
803        signer: &impl Signer,
804        component_id: ComponentId,
805        operation: AppDataUpdateOperation,
806    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
807        let (framing, proposal_ref) = self
808            .propose_app_data_update_impl(
809                provider,
810                signer,
811                component_id,
812                operation,
813                ProposalOrRefType::Proposal,
814            )
815            .await?;
816        Ok((framing.message, proposal_ref))
817    }
818
819    #[cfg(feature = "extensions-draft")]
820    #[openmls_traits::maybe_async]
821    async fn propose_app_data_update_impl<Provider: OpenMlsProvider>(
822        &mut self,
823        provider: &Provider,
824        signer: &impl Signer,
825        component_id: ComponentId,
826        operation: AppDataUpdateOperation,
827        ref_or_value: ProposalOrRefType,
828    ) -> Result<(HandshakeFramingOutput, ProposalRef), ProposalError<Provider::StorageError>> {
829        self.is_operational()?;
830
831        let aad = self.outgoing_authenticated_data()?;
832        let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
833        let proposal = self.create_app_data_update_proposal(
834            framing_parameters,
835            component_id,
836            operation,
837            signer,
838        )?;
839
840        let queued_proposal = QueuedProposal::from_authenticated_content(
841            self.ciphersuite(),
842            provider.crypto(),
843            proposal.clone(),
844            ref_or_value,
845        )?;
846        let proposal_ref = queued_proposal.proposal_reference();
847
848        log::trace!("Storing proposal in queue {:?}", queued_proposal);
849        provider
850            .storage()
851            .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
852            .await
853            .map_err(ProposalError::StorageError)?;
854        self.proposal_store_mut().add(queued_proposal);
855
856        let framing = self.content_to_mls_message(proposal, provider).await?;
857
858        self.reset_aad();
859        Ok((framing, proposal_ref))
860    }
861
862    /// Removes a specific proposal from the store.
863    #[openmls_traits::maybe_async]
864    pub async fn remove_pending_proposal<Storage: StorageProvider>(
865        &mut self,
866        storage: &Storage,
867        proposal_ref: &ProposalRef,
868    ) -> Result<(), RemoveProposalError<Storage::Error>> {
869        storage
870            .remove_proposal(self.group_id(), proposal_ref)
871            .await
872            .map_err(RemoveProposalError::Storage)?;
873        self.proposal_store_mut()
874            .remove(proposal_ref)
875            .ok_or(RemoveProposalError::ProposalNotFound)
876    }
877
878    // === Create handshake messages ===
879
880    // 12.1.1. Add
881    // struct {
882    //     KeyPackage key_package;
883    // } Add;
884    pub(crate) fn create_add_proposal(
885        &self,
886        framing_parameters: FramingParameters,
887        joiner_key_package: KeyPackage,
888        signer: &impl Signer,
889    ) -> Result<AuthenticatedContent, CreateAddProposalError> {
890        if let Some(required_capabilities) = self.required_capabilities() {
891            joiner_key_package
892                .leaf_node()
893                .capabilities()
894                .supports_required_capabilities(required_capabilities)?;
895        }
896        let add_proposal = AddProposal {
897            key_package: joiner_key_package,
898        };
899        let proposal = Proposal::add(add_proposal);
900        AuthenticatedContent::member_proposal(
901            framing_parameters,
902            self.own_leaf_index(),
903            proposal,
904            self.context(),
905            signer,
906        )
907        .map_err(|e| e.into())
908    }
909
910    // 12.1.2. Update
911    // struct {
912    //     LeafNode leaf_node;
913    // } Update;
914    pub(crate) fn create_update_proposal(
915        &self,
916        framing_parameters: FramingParameters,
917        // XXX: There's no need to own this. The [`UpdateProposal`] should
918        //      operate on a reference to make this more efficient.
919        leaf_node: LeafNode,
920        signer: &impl Signer,
921    ) -> Result<AuthenticatedContent, LibraryError> {
922        let update_proposal = UpdateProposal { leaf_node };
923        let proposal = Proposal::update(update_proposal);
924        AuthenticatedContent::member_proposal(
925            framing_parameters,
926            self.own_leaf_index(),
927            proposal,
928            self.context(),
929            signer,
930        )
931    }
932
933    // 12.1.3. Remove
934    // struct {
935    //     uint32 removed;
936    // } Remove;
937    pub(crate) fn create_remove_proposal(
938        &self,
939        framing_parameters: FramingParameters,
940        removed: LeafNodeIndex,
941        signer: &impl Signer,
942    ) -> Result<AuthenticatedContent, ValidationError> {
943        if self.public_group().leaf(removed).is_none() {
944            return Err(ValidationError::UnknownMember);
945        }
946        let remove_proposal = RemoveProposal { removed };
947        let proposal = Proposal::remove(remove_proposal);
948        AuthenticatedContent::member_proposal(
949            framing_parameters,
950            self.own_leaf_index(),
951            proposal,
952            self.context(),
953            signer,
954        )
955        .map_err(ValidationError::LibraryError)
956    }
957
958    /// Create a SelfRemove proposal. Note that SelfRemove proposals are always
959    /// sent as PublicMessages.
960    pub(crate) fn create_self_remove_proposal(
961        &self,
962        aad: &[u8],
963        signer: &impl Signer,
964    ) -> Result<AuthenticatedContent, LibraryError> {
965        let proposal = Proposal::SelfRemove;
966        let framing_parameters = FramingParameters::new(aad, WireFormat::PublicMessage);
967        AuthenticatedContent::member_proposal(
968            framing_parameters,
969            self.own_leaf_index(),
970            proposal,
971            self.context(),
972            signer,
973        )
974    }
975
976    // 12.1.4. PreSharedKey
977    // struct {
978    //     PreSharedKeyID psk;
979    // } PreSharedKey;
980    pub(crate) fn create_presharedkey_proposal(
981        &self,
982        framing_parameters: FramingParameters,
983        psk: PreSharedKeyId,
984        signer: &impl Signer,
985    ) -> Result<AuthenticatedContent, LibraryError> {
986        let presharedkey_proposal = PreSharedKeyProposal::new(psk);
987        let proposal = Proposal::psk(presharedkey_proposal);
988        AuthenticatedContent::member_proposal(
989            framing_parameters,
990            self.own_leaf_index(),
991            proposal,
992            self.context(),
993            signer,
994        )
995    }
996
997    // 12.1.5. ReInit
998    // struct {
999    //     opaque group_id<V>;
1000    //     ProtocolVersion version;
1001    //     CipherSuite cipher_suite;
1002    //     Extension extensions<V>;
1003    // } ReInit;
1004    pub(crate) fn create_reinit_proposal(
1005        &self,
1006        framing_parameters: FramingParameters,
1007        reinit_proposal: ReInitProposal,
1008        signer: &impl Signer,
1009    ) -> Result<AuthenticatedContent, LibraryError> {
1010        let proposal = Proposal::re_init(reinit_proposal);
1011        AuthenticatedContent::member_proposal(
1012            framing_parameters,
1013            self.own_leaf_index(),
1014            proposal,
1015            self.context(),
1016            signer,
1017        )
1018    }
1019
1020    #[cfg(feature = "extensions-draft")]
1021    pub(crate) fn create_app_data_update_proposal(
1022        &self,
1023        framing_parameters: FramingParameters,
1024        component_id: ComponentId,
1025        operation: AppDataUpdateOperation,
1026        signer: &impl Signer,
1027    ) -> Result<AuthenticatedContent, LibraryError> {
1028        let proposal = Proposal::AppDataUpdate(Box::new(AppDataUpdateProposal::new(
1029            component_id,
1030            operation,
1031        )));
1032        AuthenticatedContent::member_proposal(
1033            framing_parameters,
1034            self.own_leaf_index(),
1035            proposal,
1036            self.context(),
1037            signer,
1038        )
1039    }
1040
1041    pub(crate) fn create_custom_proposal(
1042        &self,
1043        framing_parameters: FramingParameters,
1044        custom_proposal: CustomProposal,
1045        signer: &impl Signer,
1046    ) -> Result<AuthenticatedContent, LibraryError> {
1047        let proposal = Proposal::custom(custom_proposal);
1048        AuthenticatedContent::member_proposal(
1049            framing_parameters,
1050            self.own_leaf_index(),
1051            proposal,
1052            self.context(),
1053            signer,
1054        )
1055    }
1056}