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-08")]
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-08")]
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-08")]
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 proposal = self.$group_fun(self.framing_parameters(), value, signer)?;
96
97            let queued_proposal = QueuedProposal::from_authenticated_content(
98                self.ciphersuite(),
99                provider.crypto(),
100                proposal.clone(),
101                $ref_or_value,
102            )?;
103            let proposal_ref = queued_proposal.proposal_reference();
104
105            log::trace!("Storing proposal in queue {:?}", queued_proposal);
106            provider
107                .storage()
108                .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
109                .map_err(ProposalError::StorageError)?;
110            self.proposal_store_mut().add(queued_proposal);
111
112            let mls_message = self.content_to_mls_message(proposal, provider)?;
113
114            self.reset_aad();
115            Ok((mls_message, proposal_ref))
116        }
117    };
118}
119
120impl MlsGroup {
121    impl_propose_fun!(
122        propose_add_member_by_value,
123        KeyPackage,
124        create_add_proposal,
125        ProposalOrRefType::Proposal
126    );
127
128    impl_propose_fun!(
129        propose_remove_member_by_value,
130        LeafNodeIndex,
131        create_remove_proposal,
132        ProposalOrRefType::Proposal
133    );
134
135    impl_propose_fun!(
136        propose_external_psk,
137        PreSharedKeyId,
138        create_presharedkey_proposal,
139        ProposalOrRefType::Reference
140    );
141
142    impl_propose_fun!(
143        propose_external_psk_by_value,
144        PreSharedKeyId,
145        create_presharedkey_proposal,
146        ProposalOrRefType::Proposal
147    );
148
149    impl_propose_fun!(
150        propose_custom_proposal_by_value,
151        CustomProposal,
152        create_custom_proposal,
153        ProposalOrRefType::Proposal
154    );
155
156    impl_propose_fun!(
157        propose_custom_proposal_by_reference,
158        CustomProposal,
159        create_custom_proposal,
160        ProposalOrRefType::Reference
161    );
162
163    /// Generate a proposal
164    pub fn propose<Provider: OpenMlsProvider>(
165        &mut self,
166        provider: &Provider,
167        signer: &impl Signer,
168        propose: Propose,
169        ref_or_value: ProposalOrRefType,
170    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
171        match propose {
172            Propose::Add(key_package) => match ref_or_value {
173                ProposalOrRefType::Proposal => {
174                    self.propose_add_member_by_value(provider, signer, key_package)
175                }
176                ProposalOrRefType::Reference => self
177                    .propose_add_member(provider, signer, &key_package)
178                    .map_err(|e| e.into()),
179            },
180
181            Propose::Update(leaf_node_parameters) => match ref_or_value {
182                ProposalOrRefType::Proposal => self
183                    .propose_self_update(provider, signer, leaf_node_parameters)
184                    .map_err(|e| e.into()),
185                ProposalOrRefType::Reference => self
186                    .propose_self_update(provider, signer, leaf_node_parameters)
187                    .map_err(|e| e.into()),
188            },
189
190            Propose::Remove(leaf_index) => match ref_or_value {
191                ProposalOrRefType::Proposal => self.propose_remove_member_by_value(
192                    provider,
193                    signer,
194                    LeafNodeIndex::new(leaf_index),
195                ),
196                ProposalOrRefType::Reference => self
197                    .propose_remove_member(provider, signer, LeafNodeIndex::new(leaf_index))
198                    .map_err(|e| e.into()),
199            },
200
201            Propose::RemoveCredential(credential) => match ref_or_value {
202                ProposalOrRefType::Proposal => {
203                    self.propose_remove_member_by_credential_by_value(provider, signer, &credential)
204                }
205                ProposalOrRefType::Reference => self
206                    .propose_remove_member_by_credential(provider, signer, &credential)
207                    .map_err(|e| e.into()),
208            },
209            Propose::PreSharedKey(psk_id) => match psk_id.psk() {
210                crate::schedule::Psk::External(_) => match ref_or_value {
211                    ProposalOrRefType::Proposal => {
212                        self.propose_external_psk_by_value(provider, signer, psk_id)
213                    }
214                    ProposalOrRefType::Reference => {
215                        self.propose_external_psk(provider, signer, psk_id)
216                    }
217                },
218                crate::schedule::Psk::Resumption(_) => Err(ProposalError::LibraryError(
219                    LibraryError::custom("Invalid PSk argument"),
220                )),
221            },
222            Propose::ReInit {
223                group_id: _,
224                version: _,
225                ciphersuite: _,
226                extensions: _,
227            } => Err(ProposalError::LibraryError(LibraryError::custom(
228                "Unsupported proposal type ReInit",
229            ))),
230            Propose::ExternalInit(_) => Err(ProposalError::LibraryError(LibraryError::custom(
231                "Unsupported proposal type ExternalInit",
232            ))),
233            Propose::GroupContextExtensions(_) => Err(ProposalError::LibraryError(
234                LibraryError::custom("Unsupported proposal type GroupContextExtensions"),
235            )),
236            // extensions-draft-08
237            #[cfg(feature = "extensions-draft-08")]
238            Propose::UpdateAppDataComponent {
239                component_id,
240                update,
241            } => self.propose_app_data_update(
242                provider,
243                signer,
244                component_id,
245                AppDataUpdateOperation::Update(update.into()),
246            ),
247            #[cfg(feature = "extensions-draft-08")]
248            Propose::RemoveAppDataComponent { component_id } => self.propose_app_data_update(
249                provider,
250                signer,
251                component_id,
252                AppDataUpdateOperation::Remove,
253            ),
254
255            // custom
256            Propose::Custom(custom_proposal) => match ref_or_value {
257                ProposalOrRefType::Proposal => {
258                    self.propose_custom_proposal_by_value(provider, signer, custom_proposal)
259                }
260                ProposalOrRefType::Reference => {
261                    self.propose_custom_proposal_by_reference(provider, signer, custom_proposal)
262                }
263            },
264        }
265    }
266
267    /// Creates proposals to add members to the group.
268    ///
269    /// Returns an error if there is a pending commit.
270    pub fn propose_add_member<Provider: OpenMlsProvider>(
271        &mut self,
272        provider: &Provider,
273        signer: &impl Signer,
274        key_package: &KeyPackage,
275    ) -> Result<(MlsMessageOut, ProposalRef), ProposeAddMemberError<Provider::StorageError>> {
276        self.is_operational()?;
277
278        let add_proposal = self
279            .create_add_proposal(self.framing_parameters(), key_package.clone(), signer)
280            .map_err(|e| match e {
281                CreateAddProposalError::LibraryError(e) => e.into(),
282                CreateAddProposalError::LeafNodeValidation(error) => {
283                    ProposeAddMemberError::LeafNodeValidation(error)
284                }
285            })?;
286
287        let proposal = QueuedProposal::from_authenticated_content_by_ref(
288            self.ciphersuite(),
289            provider.crypto(),
290            add_proposal.clone(),
291        )?;
292        let proposal_ref = proposal.proposal_reference();
293        provider
294            .storage()
295            .queue_proposal(self.group_id(), &proposal_ref, &proposal)
296            .map_err(ProposeAddMemberError::StorageError)?;
297        self.proposal_store_mut().add(proposal);
298
299        let mls_message = self.content_to_mls_message(add_proposal, provider)?;
300
301        self.reset_aad();
302        Ok((mls_message, proposal_ref))
303    }
304
305    /// Creates proposals to remove members from the group.
306    /// The `member` has to be the member's leaf index.
307    ///
308    /// Returns an error if there is a pending commit.
309    pub fn propose_remove_member<Provider: OpenMlsProvider>(
310        &mut self,
311        provider: &Provider,
312        signer: &impl Signer,
313        member: LeafNodeIndex,
314    ) -> Result<(MlsMessageOut, ProposalRef), ProposeRemoveMemberError<Provider::StorageError>>
315    {
316        self.is_operational()?;
317
318        let remove_proposal = self
319            .create_remove_proposal(self.framing_parameters(), member, signer)
320            .map_err(|_| ProposeRemoveMemberError::UnknownMember)?;
321
322        let proposal = QueuedProposal::from_authenticated_content_by_ref(
323            self.ciphersuite(),
324            provider.crypto(),
325            remove_proposal.clone(),
326        )?;
327        let proposal_ref = proposal.proposal_reference();
328        provider
329            .storage()
330            .queue_proposal(self.group_id(), &proposal_ref, &proposal)
331            .map_err(ProposeRemoveMemberError::StorageError)?;
332        self.proposal_store_mut().add(proposal);
333
334        let mls_message = self.content_to_mls_message(remove_proposal, provider)?;
335
336        self.reset_aad();
337        Ok((mls_message, proposal_ref))
338    }
339
340    /// Creates proposals to remove members from the group.
341    /// The `member` has to be the member's credential.
342    ///
343    /// Returns an error if there is a pending commit.
344    pub fn propose_remove_member_by_credential<Provider: OpenMlsProvider>(
345        &mut self,
346        provider: &Provider,
347        signer: &impl Signer,
348        member: &Credential,
349    ) -> Result<(MlsMessageOut, ProposalRef), ProposeRemoveMemberError<Provider::StorageError>>
350    {
351        // Find the user for the credential first.
352        let member_index = self
353            .public_group()
354            .members()
355            .find(|m| &m.credential == member)
356            .map(|m| m.index);
357
358        if let Some(member_index) = member_index {
359            self.propose_remove_member(provider, signer, member_index)
360        } else {
361            Err(ProposeRemoveMemberError::UnknownMember)
362        }
363    }
364
365    /// Creates proposals to remove members from the group.
366    /// The `member` has to be the member's credential.
367    ///
368    /// Returns an error if there is a pending commit.
369    pub fn propose_remove_member_by_credential_by_value<Provider: OpenMlsProvider>(
370        &mut self,
371        provider: &Provider,
372        signer: &impl Signer,
373        member: &Credential,
374    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
375        // Find the user for the credential first.
376        let member_index = self
377            .public_group()
378            .members()
379            .find(|m| &m.credential == member)
380            .map(|m| m.index);
381
382        if let Some(member_index) = member_index {
383            self.propose_remove_member_by_value(provider, signer, member_index)
384        } else {
385            Err(ProposalError::ProposeRemoveMemberError(
386                ProposeRemoveMemberError::UnknownMember,
387            ))
388        }
389    }
390
391    /// Creates a proposals with a new set of `extensions` for the group context.
392    ///
393    /// Returns an error when the group does not support all the required capabilities
394    /// in the new `extensions`.
395    pub fn propose_group_context_extensions<Provider: OpenMlsProvider>(
396        &mut self,
397        provider: &Provider,
398        extensions: Extensions<GroupContext>,
399        signer: &impl Signer,
400    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
401        self.is_operational()?;
402
403        let proposal = self.create_group_context_ext_proposal::<Provider>(
404            self.framing_parameters(),
405            extensions,
406            signer,
407        )?;
408
409        let queued_proposal = QueuedProposal::from_authenticated_content_by_ref(
410            self.ciphersuite(),
411            provider.crypto(),
412            proposal.clone(),
413        )?;
414
415        let proposal_ref = queued_proposal.proposal_reference();
416        provider
417            .storage()
418            .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
419            .map_err(ProposalError::StorageError)?;
420        self.proposal_store_mut().add(queued_proposal);
421
422        let mls_message = self.content_to_mls_message(proposal, provider)?;
423
424        self.reset_aad();
425        Ok((mls_message, proposal_ref))
426    }
427
428    /// Updates Group Context Extensions
429    ///
430    /// Commits to the Group Context Extension inline proposal using the [`Extensions`]
431    ///
432    /// Returns an error when the group does not support all the required capabilities
433    /// in the new `extensions` or if there is a pending commit.
434    //// FIXME: #1217
435    #[allow(clippy::type_complexity)]
436    pub fn update_group_context_extensions<Provider: OpenMlsProvider>(
437        &mut self,
438        provider: &Provider,
439        extensions: Extensions<GroupContext>,
440        signer: &impl Signer,
441    ) -> Result<
442        (MlsMessageOut, Option<MlsMessageOut>, Option<GroupInfo>),
443        CreateGroupContextExtProposalError<Provider::StorageError>,
444    > {
445        self.is_operational()?;
446
447        // Build and stage Commit containing GroupContextExtensions proposal
448        let bundle = self
449            .commit_builder()
450            .propose_group_context_extensions(extensions)?
451            .load_psks(provider.storage())?
452            .build(provider.rand(), provider.crypto(), signer, |_| true)?
453            .stage_commit(provider)?;
454
455        // Extract messages and convert Welcome to MlsMessageOut
456        let (commit, welcome, group_info) = bundle.into_contents();
457        let welcome = welcome.map(|welcome| MlsMessageOut::from_welcome(welcome, self.version()));
458
459        Ok((commit, welcome, group_info))
460    }
461
462    /// Updates the AppDataDictionary
463    #[cfg(feature = "extensions-draft-08")]
464    pub fn propose_app_data_update<Provider: OpenMlsProvider>(
465        &mut self,
466        provider: &Provider,
467        signer: &impl Signer,
468        component_id: ComponentId,
469        operation: AppDataUpdateOperation,
470    ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
471        self.is_operational()?;
472
473        let proposal = self.create_app_data_update_proposal(
474            self.framing_parameters(),
475            component_id,
476            operation,
477            signer,
478        )?;
479
480        let queued_proposal = QueuedProposal::from_authenticated_content(
481            self.ciphersuite(),
482            provider.crypto(),
483            proposal.clone(),
484            ProposalOrRefType::Proposal,
485        )?;
486        let proposal_ref = queued_proposal.proposal_reference();
487
488        log::trace!("Storing proposal in queue {:?}", queued_proposal);
489        provider
490            .storage()
491            .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
492            .map_err(ProposalError::StorageError)?;
493        self.proposal_store_mut().add(queued_proposal);
494
495        let mls_message = self.content_to_mls_message(proposal, provider)?;
496
497        self.reset_aad();
498        Ok((mls_message, proposal_ref))
499    }
500
501    /// Removes a specific proposal from the store.
502    pub fn remove_pending_proposal<Storage: StorageProvider>(
503        &mut self,
504        storage: &Storage,
505        proposal_ref: &ProposalRef,
506    ) -> Result<(), RemoveProposalError<Storage::Error>> {
507        storage
508            .remove_proposal(self.group_id(), proposal_ref)
509            .map_err(RemoveProposalError::Storage)?;
510        self.proposal_store_mut()
511            .remove(proposal_ref)
512            .ok_or(RemoveProposalError::ProposalNotFound)
513    }
514
515    // === Create handshake messages ===
516
517    // 12.1.1. Add
518    // struct {
519    //     KeyPackage key_package;
520    // } Add;
521    pub(crate) fn create_add_proposal(
522        &self,
523        framing_parameters: FramingParameters,
524        joiner_key_package: KeyPackage,
525        signer: &impl Signer,
526    ) -> Result<AuthenticatedContent, CreateAddProposalError> {
527        if let Some(required_capabilities) = self.required_capabilities() {
528            joiner_key_package
529                .leaf_node()
530                .capabilities()
531                .supports_required_capabilities(required_capabilities)?;
532        }
533        let add_proposal = AddProposal {
534            key_package: joiner_key_package,
535        };
536        let proposal = Proposal::add(add_proposal);
537        AuthenticatedContent::member_proposal(
538            framing_parameters,
539            self.own_leaf_index(),
540            proposal,
541            self.context(),
542            signer,
543        )
544        .map_err(|e| e.into())
545    }
546
547    // 12.1.2. Update
548    // struct {
549    //     LeafNode leaf_node;
550    // } Update;
551    pub(crate) fn create_update_proposal(
552        &self,
553        framing_parameters: FramingParameters,
554        // XXX: There's no need to own this. The [`UpdateProposal`] should
555        //      operate on a reference to make this more efficient.
556        leaf_node: LeafNode,
557        signer: &impl Signer,
558    ) -> Result<AuthenticatedContent, LibraryError> {
559        let update_proposal = UpdateProposal { leaf_node };
560        let proposal = Proposal::update(update_proposal);
561        AuthenticatedContent::member_proposal(
562            framing_parameters,
563            self.own_leaf_index(),
564            proposal,
565            self.context(),
566            signer,
567        )
568    }
569
570    // 12.1.3. Remove
571    // struct {
572    //     uint32 removed;
573    // } Remove;
574    pub(crate) fn create_remove_proposal(
575        &self,
576        framing_parameters: FramingParameters,
577        removed: LeafNodeIndex,
578        signer: &impl Signer,
579    ) -> Result<AuthenticatedContent, ValidationError> {
580        if self.public_group().leaf(removed).is_none() {
581            return Err(ValidationError::UnknownMember);
582        }
583        let remove_proposal = RemoveProposal { removed };
584        let proposal = Proposal::remove(remove_proposal);
585        AuthenticatedContent::member_proposal(
586            framing_parameters,
587            self.own_leaf_index(),
588            proposal,
589            self.context(),
590            signer,
591        )
592        .map_err(ValidationError::LibraryError)
593    }
594
595    /// Create a SelfRemove proposal. Note that SelfRemove proposals are always
596    /// sent as PublicMessages.
597    pub(crate) fn create_self_remove_proposal(
598        &self,
599        aad: &[u8],
600        signer: &impl Signer,
601    ) -> Result<AuthenticatedContent, LibraryError> {
602        let proposal = Proposal::SelfRemove;
603        let framing_parameters = FramingParameters::new(aad, WireFormat::PublicMessage);
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.4. PreSharedKey
614    // struct {
615    //     PreSharedKeyID psk;
616    // } PreSharedKey;
617    // TODO: #751
618    pub(crate) fn create_presharedkey_proposal(
619        &self,
620        framing_parameters: FramingParameters,
621        psk: PreSharedKeyId,
622        signer: &impl Signer,
623    ) -> Result<AuthenticatedContent, LibraryError> {
624        let presharedkey_proposal = PreSharedKeyProposal::new(psk);
625        let proposal = Proposal::psk(presharedkey_proposal);
626        AuthenticatedContent::member_proposal(
627            framing_parameters,
628            self.own_leaf_index(),
629            proposal,
630            self.context(),
631            signer,
632        )
633    }
634
635    #[cfg(feature = "extensions-draft-08")]
636    pub(crate) fn create_app_data_update_proposal(
637        &self,
638        framing_parameters: FramingParameters,
639        component_id: ComponentId,
640        operation: AppDataUpdateOperation,
641        signer: &impl Signer,
642    ) -> Result<AuthenticatedContent, LibraryError> {
643        let proposal = Proposal::AppDataUpdate(Box::new(AppDataUpdateProposal::new(
644            component_id,
645            operation,
646        )));
647        AuthenticatedContent::member_proposal(
648            framing_parameters,
649            self.own_leaf_index(),
650            proposal,
651            self.context(),
652            signer,
653        )
654    }
655
656    pub(crate) fn create_custom_proposal(
657        &self,
658        framing_parameters: FramingParameters,
659        custom_proposal: CustomProposal,
660        signer: &impl Signer,
661    ) -> Result<AuthenticatedContent, LibraryError> {
662        let proposal = Proposal::custom(custom_proposal);
663        AuthenticatedContent::member_proposal(
664            framing_parameters,
665            self.own_leaf_index(),
666            proposal,
667            self.context(),
668            signer,
669        )
670    }
671}