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