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