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