Skip to main content

openmls/messages/
proposals_in.rs

1//! # Proposals
2//!
3//! This module defines all the different types of Proposals.
4
5use crate::{
6    ciphersuite::{hash_ref::ProposalRef, signable::Verifiable},
7    credentials::CredentialWithKey,
8    extensions::{AnyObject, Extensions},
9    framing::SenderContext,
10    group::{errors::ValidationError, LeafNodeLifetimePolicy},
11    key_packages::*,
12    prelude::InvalidExtensionError,
13    treesync::node::leaf_node::{LeafNodeIn, TreePosition, VerifiableLeafNode},
14    versions::ProtocolVersion,
15};
16
17use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
18use serde::{Deserialize, Serialize};
19use tls_codec::{TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize};
20
21use super::{
22    proposals::{
23        AddProposal, ExternalInitProposal, GroupContextExtensionProposal, PreSharedKeyProposal,
24        Proposal, ProposalOrRef, ProposalType, ReInitProposal, RemoveProposal, UpdateProposal,
25    },
26    CustomProposal,
27};
28
29#[cfg(feature = "extensions-draft")]
30use super::proposals::{AppDataUpdateProposal, AppEphemeralProposal};
31
32/// Proposal.
33///
34/// This `enum` contains the different proposals in its variants.
35///
36/// ```c
37/// // draft-ietf-mls-protocol-17
38/// struct {
39///     ProposalType msg_type;
40///     select (Proposal.msg_type) {
41///         case add:                      Add;
42///         case update:                   Update;
43///         case remove:                   Remove;
44///         case psk:                      PreSharedKey;
45///         case reinit:                   ReInit;
46///         case external_init:            ExternalInit;
47///         case group_context_extensions: GroupContextExtensions;
48///     };
49/// } Proposal;
50/// ```
51#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
52#[allow(missing_docs)]
53#[repr(u16)]
54pub enum ProposalIn {
55    Add(Box<AddProposalIn>),
56    Update(Box<UpdateProposalIn>),
57    Remove(Box<RemoveProposal>),
58    PreSharedKey(Box<PreSharedKeyProposal>),
59    ReInit(Box<ReInitProposal>),
60    ExternalInit(Box<ExternalInitProposal>),
61    GroupContextExtensions(Box<GroupContextExtensionProposalIn>),
62    // # Extensions
63    #[cfg(feature = "extensions-draft")]
64    AppDataUpdate(Box<AppDataUpdateProposal>),
65
66    // A SelfRemove proposal is an empty struct.
67    SelfRemove,
68    #[cfg(feature = "extensions-draft")]
69    AppEphemeral(Box<AppEphemeralProposal>),
70    Custom(Box<CustomProposal>),
71}
72
73impl ProposalIn {
74    /// Returns the proposal type.
75    pub fn proposal_type(&self) -> ProposalType {
76        match self {
77            ProposalIn::Add(_) => ProposalType::Add,
78            ProposalIn::Update(_) => ProposalType::Update,
79            ProposalIn::Remove(_) => ProposalType::Remove,
80            ProposalIn::PreSharedKey(_) => ProposalType::PreSharedKey,
81            ProposalIn::ReInit(_) => ProposalType::Reinit,
82            ProposalIn::ExternalInit(_) => ProposalType::ExternalInit,
83            ProposalIn::GroupContextExtensions(_) => ProposalType::GroupContextExtensions,
84            #[cfg(feature = "extensions-draft")]
85            ProposalIn::AppDataUpdate(_) => ProposalType::AppDataUpdate,
86            ProposalIn::SelfRemove => ProposalType::SelfRemove,
87            #[cfg(feature = "extensions-draft")]
88            ProposalIn::AppEphemeral(_) => ProposalType::AppEphemeral,
89            ProposalIn::Custom(custom_proposal) => {
90                ProposalType::Custom(custom_proposal.proposal_type())
91            }
92        }
93    }
94
95    /// Indicates whether a Commit containing this [ProposalIn] requires a path.
96    pub fn is_path_required(&self) -> bool {
97        self.proposal_type().is_path_required()
98    }
99
100    /// Returns a [`Proposal`] after successful validation.
101    pub(crate) fn validate(
102        self,
103        crypto: &impl OpenMlsCrypto,
104        ciphersuite: Ciphersuite,
105        sender_context: Option<SenderContext>,
106        protocol_version: ProtocolVersion,
107        lifetime_policy: LeafNodeLifetimePolicy,
108    ) -> Result<Proposal, ValidationError> {
109        Ok(match self {
110            ProposalIn::Add(add) => Proposal::Add(Box::new(add.validate(
111                crypto,
112                protocol_version,
113                ciphersuite,
114                lifetime_policy,
115            )?)),
116            ProposalIn::Update(update) => {
117                let sender_context =
118                    sender_context.ok_or(ValidationError::CommitterIncludedOwnUpdate)?;
119                Proposal::Update(Box::new(update.validate(
120                    crypto,
121                    ciphersuite,
122                    sender_context,
123                )?))
124            }
125            ProposalIn::Remove(remove) => Proposal::Remove(remove),
126            ProposalIn::PreSharedKey(psk) => Proposal::PreSharedKey(psk),
127            ProposalIn::ReInit(reinit) => Proposal::ReInit(reinit),
128            ProposalIn::ExternalInit(external_init) => Proposal::ExternalInit(external_init),
129            ProposalIn::GroupContextExtensions(group_context_extension) => {
130                Proposal::group_context_extensions(group_context_extension.validate()?)
131            }
132            #[cfg(feature = "extensions-draft")]
133            ProposalIn::AppDataUpdate(app_data_update) => Proposal::AppDataUpdate(app_data_update),
134            ProposalIn::SelfRemove => Proposal::SelfRemove,
135            #[cfg(feature = "extensions-draft")]
136            ProposalIn::AppEphemeral(app_ephemeral) => Proposal::AppEphemeral(app_ephemeral),
137            ProposalIn::Custom(custom) => Proposal::Custom(custom),
138        })
139    }
140}
141
142/// Add Proposal.
143///
144/// An Add proposal requests that a client with a specified [`KeyPackage`] be added to the group.
145///
146/// ```c
147/// // draft-ietf-mls-protocol-17
148/// struct {
149///     KeyPackage key_package;
150/// } Add;
151/// ```
152#[derive(
153    Debug,
154    PartialEq,
155    Clone,
156    Serialize,
157    Deserialize,
158    TlsSerialize,
159    TlsDeserialize,
160    TlsDeserializeBytes,
161    TlsSize,
162)]
163pub struct AddProposalIn {
164    key_package: KeyPackageIn,
165}
166
167impl AddProposalIn {
168    pub(crate) fn unverified_credential(&self) -> CredentialWithKey {
169        self.key_package.unverified_credential()
170    }
171
172    /// Returns a [`AddProposal`] after successful validation.
173    pub(crate) fn validate(
174        self,
175        crypto: &impl OpenMlsCrypto,
176        protocol_version: ProtocolVersion,
177        ciphersuite: Ciphersuite,
178        lifetime_policy: LeafNodeLifetimePolicy,
179    ) -> Result<AddProposal, ValidationError> {
180        if self.key_package.unverified_ciphersuite() != ciphersuite {
181            return Err(ValidationError::InvalidAddProposalCiphersuite);
182        }
183        let key_package = self.key_package.validate_with_lifetime_policy(
184            crypto,
185            protocol_version,
186            lifetime_policy,
187        )?;
188        Ok(AddProposal { key_package })
189    }
190}
191
192/// Update Proposal.
193///
194/// An Update proposal is a similar mechanism to [`AddProposalIn`] with the distinction that it
195/// replaces the sender's leaf node instead of adding a new leaf to the tree.
196///
197/// ```c
198/// // draft-ietf-mls-protocol-17
199/// struct {
200///     LeafNode leaf_node;
201/// } Update;
202/// ```
203#[derive(
204    Debug,
205    PartialEq,
206    Eq,
207    Clone,
208    Serialize,
209    Deserialize,
210    TlsDeserialize,
211    TlsDeserializeBytes,
212    TlsSerialize,
213    TlsSize,
214)]
215pub struct UpdateProposalIn {
216    leaf_node: LeafNodeIn,
217}
218
219impl UpdateProposalIn {
220    /// Returns a [`UpdateProposal`] after successful validation.
221    pub(crate) fn validate(
222        self,
223        crypto: &impl OpenMlsCrypto,
224        ciphersuite: Ciphersuite,
225        sender_context: SenderContext,
226    ) -> Result<UpdateProposal, ValidationError> {
227        let leaf_node = match self.leaf_node.into_verifiable_leaf_node() {
228            VerifiableLeafNode::Update(mut leaf_node) => {
229                let tree_position = match sender_context {
230                    SenderContext::Member((group_id, leaf_index)) => {
231                        TreePosition::new(group_id, leaf_index)
232                    }
233                    _ => return Err(ValidationError::InvalidSenderType),
234                };
235                leaf_node.add_tree_position(tree_position);
236                let pk = &leaf_node
237                    .signature_key()
238                    .clone()
239                    .into_signature_public_key_enriched(ciphersuite.signature_algorithm());
240
241                leaf_node
242                    .verify(crypto, pk)
243                    .map_err(|_| ValidationError::InvalidLeafNodeSignature)?
244            }
245            _ => return Err(ValidationError::InvalidLeafNodeSourceType),
246        };
247
248        Ok(UpdateProposal { leaf_node })
249    }
250}
251
252// Crate-only types
253
254/// Type of Proposal, either by value or by reference.
255#[derive(
256    Debug,
257    PartialEq,
258    Clone,
259    Serialize,
260    Deserialize,
261    TlsSerialize,
262    TlsDeserialize,
263    TlsDeserializeBytes,
264    TlsSize,
265)]
266#[repr(u8)]
267#[allow(missing_docs)]
268pub enum ProposalOrRefIn {
269    #[tls_codec(discriminant = 1)]
270    Proposal(Box<ProposalIn>),
271    Reference(Box<ProposalRef>),
272}
273
274impl ProposalOrRefIn {
275    /// Returns a [`ProposalOrRef`] after successful validation.
276    pub(crate) fn validate(
277        self,
278        crypto: &impl OpenMlsCrypto,
279        ciphersuite: Ciphersuite,
280        protocol_version: ProtocolVersion,
281        lifetime_policy: LeafNodeLifetimePolicy,
282    ) -> Result<ProposalOrRef, ValidationError> {
283        Ok(match self {
284            ProposalOrRefIn::Proposal(proposal_in) => {
285                ProposalOrRef::Proposal(Box::new(proposal_in.validate(
286                    crypto,
287                    ciphersuite,
288                    None,
289                    protocol_version,
290                    lifetime_policy,
291                )?))
292            }
293            ProposalOrRefIn::Reference(reference) => ProposalOrRef::Reference(reference),
294        })
295    }
296}
297
298// The following `From` implementation breaks abstraction layers and MUST
299// NOT be made available outside of tests or "test-utils".
300#[cfg(any(feature = "test-utils", test))]
301impl From<AddProposalIn> for AddProposal {
302    fn from(value: AddProposalIn) -> Self {
303        Self {
304            key_package: value.key_package.into(),
305        }
306    }
307}
308
309#[cfg(any(feature = "test-utils", test))]
310impl From<AddProposalIn> for Box<AddProposal> {
311    fn from(value: AddProposalIn) -> Self {
312        Box::new(AddProposal {
313            key_package: value.key_package.into(),
314        })
315    }
316}
317
318impl From<AddProposal> for Box<AddProposalIn> {
319    fn from(value: AddProposal) -> Self {
320        Box::new(AddProposalIn {
321            key_package: value.key_package.into(),
322        })
323    }
324}
325
326// The following `From` implementation( breaks abstraction layers and MUST
327// NOT be made available outside of tests or "test-utils".
328#[cfg(any(feature = "test-utils", test))]
329impl From<UpdateProposalIn> for UpdateProposal {
330    fn from(value: UpdateProposalIn) -> Self {
331        Self {
332            leaf_node: value.leaf_node.into(),
333        }
334    }
335}
336
337impl From<UpdateProposal> for UpdateProposalIn {
338    fn from(value: UpdateProposal) -> Self {
339        Self {
340            leaf_node: value.leaf_node.into(),
341        }
342    }
343}
344
345// The following `From` implementation breaks abstraction layers and MUST
346// NOT be made available outside of tests or "test-utils".
347#[cfg(any(feature = "test-utils", test))]
348impl From<GroupContextExtensionProposalIn> for GroupContextExtensionProposal {
349    fn from(value: GroupContextExtensionProposalIn) -> Self {
350        Self::new(value.extensions_tbv.try_into().unwrap())
351    }
352}
353
354#[cfg(any(feature = "test-utils", test))]
355impl From<GroupContextExtensionProposalIn> for Box<GroupContextExtensionProposal> {
356    fn from(value: GroupContextExtensionProposalIn) -> Self {
357        Box::new(GroupContextExtensionProposal::new(
358            value.extensions_tbv.try_into().unwrap(),
359        ))
360    }
361}
362
363impl From<GroupContextExtensionProposal> for GroupContextExtensionProposalIn {
364    fn from(value: crate::messages::proposals::GroupContextExtensionProposal) -> Self {
365        Self {
366            extensions_tbv: value.extensions().clone().into(),
367        }
368    }
369}
370
371impl From<GroupContextExtensionProposal> for Box<GroupContextExtensionProposalIn> {
372    fn from(value: GroupContextExtensionProposal) -> Self {
373        Box::new(GroupContextExtensionProposalIn {
374            extensions_tbv: value.into_extensions().into(),
375        })
376    }
377}
378
379#[cfg(any(feature = "test-utils", test))]
380impl From<UpdateProposalIn> for Box<UpdateProposal> {
381    fn from(value: UpdateProposalIn) -> Self {
382        Box::new(UpdateProposal {
383            leaf_node: value.leaf_node.into(),
384        })
385    }
386}
387
388impl From<UpdateProposal> for Box<UpdateProposalIn> {
389    fn from(value: UpdateProposal) -> Self {
390        Box::new(UpdateProposalIn {
391            leaf_node: value.leaf_node.into(),
392        })
393    }
394}
395
396#[cfg(any(feature = "test-utils", test))]
397impl From<ProposalIn> for crate::messages::proposals::Proposal {
398    fn from(proposal: ProposalIn) -> Self {
399        match proposal {
400            ProposalIn::Add(add) => Self::Add((*add).into()),
401            ProposalIn::Update(update) => Self::Update((*update).into()),
402            ProposalIn::Remove(remove) => Self::Remove(remove),
403            ProposalIn::PreSharedKey(psk) => Self::PreSharedKey(psk),
404            ProposalIn::ReInit(reinit) => Self::ReInit(reinit),
405            ProposalIn::ExternalInit(external_init) => Self::ExternalInit(external_init),
406            ProposalIn::GroupContextExtensions(group_context_extension) => {
407                Self::GroupContextExtensions((*group_context_extension).into())
408            }
409            #[cfg(feature = "extensions-draft")]
410            ProposalIn::AppDataUpdate(app_data_update) => Self::AppDataUpdate(app_data_update),
411            ProposalIn::SelfRemove => Self::SelfRemove,
412            #[cfg(feature = "extensions-draft")]
413            ProposalIn::AppEphemeral(app_ephemeral) => Self::AppEphemeral(app_ephemeral),
414            ProposalIn::Custom(other) => Self::Custom(other),
415        }
416    }
417}
418
419impl From<crate::messages::proposals::Proposal> for ProposalIn {
420    fn from(proposal: crate::messages::proposals::Proposal) -> Self {
421        match proposal {
422            Proposal::Add(add) => Self::Add((*add).into()),
423            Proposal::Update(update) => Self::Update((*update).into()),
424            Proposal::Remove(remove) => Self::Remove(remove),
425            Proposal::PreSharedKey(psk) => Self::PreSharedKey(psk),
426            Proposal::ReInit(reinit) => Self::ReInit(reinit),
427            Proposal::ExternalInit(external_init) => Self::ExternalInit(external_init),
428            Proposal::GroupContextExtensions(group_context_extension) => {
429                Self::GroupContextExtensions((*group_context_extension).into())
430            }
431            #[cfg(feature = "extensions-draft")]
432            Proposal::AppDataUpdate(app_data_update) => Self::AppDataUpdate(app_data_update),
433            Proposal::SelfRemove => Self::SelfRemove,
434            #[cfg(feature = "extensions-draft")]
435            Proposal::AppEphemeral(app_ephemeral) => Self::AppEphemeral(app_ephemeral),
436            Proposal::Custom(other) => Self::Custom(other),
437        }
438    }
439}
440
441#[cfg(any(feature = "test-utils", test))]
442impl From<ProposalOrRefIn> for crate::messages::proposals::ProposalOrRef {
443    fn from(proposal: ProposalOrRefIn) -> Self {
444        match proposal {
445            ProposalOrRefIn::Proposal(proposal) => Self::Proposal(Box::new((*proposal).into())),
446            ProposalOrRefIn::Reference(reference) => Self::Reference(reference),
447        }
448    }
449}
450
451impl From<crate::messages::proposals::ProposalOrRef> for ProposalOrRefIn {
452    fn from(proposal: crate::messages::proposals::ProposalOrRef) -> Self {
453        match proposal {
454            crate::messages::proposals::ProposalOrRef::Proposal(proposal) => {
455                Self::Proposal(Box::new((*proposal).into()))
456            }
457            crate::messages::proposals::ProposalOrRef::Reference(reference) => {
458                Self::Reference(reference)
459            }
460        }
461    }
462}
463
464/// GroupContext Extension Proposal.
465#[derive(
466    Debug,
467    PartialEq,
468    Clone,
469    Serialize,
470    Deserialize,
471    TlsSerialize,
472    TlsDeserialize,
473    TlsDeserializeBytes,
474    TlsSize,
475)]
476pub struct GroupContextExtensionProposalIn {
477    extensions_tbv: Extensions<AnyObject>,
478}
479
480impl GroupContextExtensionProposalIn {
481    pub(crate) fn validate(self) -> Result<GroupContextExtensionProposal, ValidationError> {
482        let group_context_extensions = self.extensions_tbv;
483        Ok(GroupContextExtensionProposal::new(
484            group_context_extensions
485                .try_into()
486                .map_err(InvalidExtensionError::from)?,
487        ))
488    }
489}