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