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