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