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,
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-08")]
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-08")]
64    AppDataUpdate(Box<AppDataUpdateProposal>),
65
66    // A SelfRemove proposal is an empty struct.
67    SelfRemove,
68    #[cfg(feature = "extensions-draft-08")]
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-08")]
85            ProposalIn::AppDataUpdate(_) => ProposalType::AppDataUpdate,
86            ProposalIn::SelfRemove => ProposalType::SelfRemove,
87            #[cfg(feature = "extensions-draft-08")]
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    ) -> Result<Proposal, ValidationError> {
108        Ok(match self {
109            ProposalIn::Add(add) => Proposal::Add(Box::new(add.validate(
110                crypto,
111                protocol_version,
112                ciphersuite,
113            )?)),
114            ProposalIn::Update(update) => {
115                let sender_context =
116                    sender_context.ok_or(ValidationError::CommitterIncludedOwnUpdate)?;
117                Proposal::Update(Box::new(update.validate(
118                    crypto,
119                    ciphersuite,
120                    sender_context,
121                )?))
122            }
123            ProposalIn::Remove(remove) => Proposal::Remove(remove),
124            ProposalIn::PreSharedKey(psk) => Proposal::PreSharedKey(psk),
125            ProposalIn::ReInit(reinit) => Proposal::ReInit(reinit),
126            ProposalIn::ExternalInit(external_init) => Proposal::ExternalInit(external_init),
127            ProposalIn::GroupContextExtensions(group_context_extension) => {
128                Proposal::group_context_extensions(group_context_extension.validate()?)
129            }
130            #[cfg(feature = "extensions-draft-08")]
131            ProposalIn::AppDataUpdate(app_data_update) => Proposal::AppDataUpdate(app_data_update),
132            ProposalIn::SelfRemove => Proposal::SelfRemove,
133            #[cfg(feature = "extensions-draft-08")]
134            ProposalIn::AppEphemeral(app_ephemeral) => Proposal::AppEphemeral(app_ephemeral),
135            ProposalIn::Custom(custom) => Proposal::Custom(custom),
136        })
137    }
138}
139
140/// Add Proposal.
141///
142/// An Add proposal requests that a client with a specified [`KeyPackage`] be added to the group.
143///
144/// ```c
145/// // draft-ietf-mls-protocol-17
146/// struct {
147///     KeyPackage key_package;
148/// } Add;
149/// ```
150#[derive(
151    Debug,
152    PartialEq,
153    Clone,
154    Serialize,
155    Deserialize,
156    TlsSerialize,
157    TlsDeserialize,
158    TlsDeserializeBytes,
159    TlsSize,
160)]
161pub struct AddProposalIn {
162    key_package: KeyPackageIn,
163}
164
165impl AddProposalIn {
166    pub(crate) fn unverified_credential(&self) -> CredentialWithKey {
167        self.key_package.unverified_credential()
168    }
169
170    /// Returns a [`AddProposal`] after successful validation.
171    pub(crate) fn validate(
172        self,
173        crypto: &impl OpenMlsCrypto,
174        protocol_version: ProtocolVersion,
175        ciphersuite: Ciphersuite,
176    ) -> Result<AddProposal, ValidationError> {
177        let key_package = self.key_package.validate(crypto, protocol_version)?;
178        // Verify that the ciphersuite is valid
179        if key_package.ciphersuite() != ciphersuite {
180            return Err(ValidationError::InvalidAddProposalCiphersuite);
181        }
182        Ok(AddProposal { key_package })
183    }
184}
185
186/// Update Proposal.
187///
188/// An Update proposal is a similar mechanism to [`AddProposalIn`] with the distinction that it
189/// replaces the sender's leaf node instead of adding a new leaf to the tree.
190///
191/// ```c
192/// // draft-ietf-mls-protocol-17
193/// struct {
194///     LeafNode leaf_node;
195/// } Update;
196/// ```
197#[derive(
198    Debug,
199    PartialEq,
200    Eq,
201    Clone,
202    Serialize,
203    Deserialize,
204    TlsDeserialize,
205    TlsDeserializeBytes,
206    TlsSerialize,
207    TlsSize,
208)]
209pub struct UpdateProposalIn {
210    leaf_node: LeafNodeIn,
211}
212
213impl UpdateProposalIn {
214    /// Returns a [`UpdateProposal`] after successful validation.
215    pub(crate) fn validate(
216        self,
217        crypto: &impl OpenMlsCrypto,
218        ciphersuite: Ciphersuite,
219        sender_context: SenderContext,
220    ) -> Result<UpdateProposal, ValidationError> {
221        let leaf_node = match self.leaf_node.into_verifiable_leaf_node() {
222            VerifiableLeafNode::Update(mut leaf_node) => {
223                let tree_position = match sender_context {
224                    SenderContext::Member((group_id, leaf_index)) => {
225                        TreePosition::new(group_id, leaf_index)
226                    }
227                    _ => return Err(ValidationError::InvalidSenderType),
228                };
229                leaf_node.add_tree_position(tree_position);
230                let pk = &leaf_node
231                    .signature_key()
232                    .clone()
233                    .into_signature_public_key_enriched(ciphersuite.signature_algorithm());
234
235                leaf_node
236                    .verify(crypto, pk)
237                    .map_err(|_| ValidationError::InvalidLeafNodeSignature)?
238            }
239            _ => return Err(ValidationError::InvalidLeafNodeSourceType),
240        };
241
242        Ok(UpdateProposal { leaf_node })
243    }
244}
245
246// Crate-only types
247
248/// Type of Proposal, either by value or by reference.
249#[derive(
250    Debug,
251    PartialEq,
252    Clone,
253    Serialize,
254    Deserialize,
255    TlsSerialize,
256    TlsDeserialize,
257    TlsDeserializeBytes,
258    TlsSize,
259)]
260#[repr(u8)]
261#[allow(missing_docs)]
262pub enum ProposalOrRefIn {
263    #[tls_codec(discriminant = 1)]
264    Proposal(Box<ProposalIn>),
265    Reference(Box<ProposalRef>),
266}
267
268impl ProposalOrRefIn {
269    /// Returns a [`ProposalOrRef`] after successful validation.
270    pub fn validate(
271        self,
272        crypto: &impl OpenMlsCrypto,
273        ciphersuite: Ciphersuite,
274        protocol_version: ProtocolVersion,
275    ) -> Result<ProposalOrRef, ValidationError> {
276        Ok(match self {
277            ProposalOrRefIn::Proposal(proposal_in) => ProposalOrRef::Proposal(Box::new(
278                proposal_in.validate(crypto, ciphersuite, None, protocol_version)?,
279            )),
280            ProposalOrRefIn::Reference(reference) => ProposalOrRef::Reference(reference),
281        })
282    }
283}
284
285// The following `From` implementation breaks abstraction layers and MUST
286// NOT be made available outside of tests or "test-utils".
287#[cfg(any(feature = "test-utils", test))]
288impl From<AddProposalIn> for AddProposal {
289    fn from(value: AddProposalIn) -> Self {
290        Self {
291            key_package: value.key_package.into(),
292        }
293    }
294}
295
296#[cfg(any(feature = "test-utils", test))]
297impl From<AddProposalIn> for Box<AddProposal> {
298    fn from(value: AddProposalIn) -> Self {
299        Box::new(AddProposal {
300            key_package: value.key_package.into(),
301        })
302    }
303}
304
305impl From<AddProposal> for Box<AddProposalIn> {
306    fn from(value: AddProposal) -> Self {
307        Box::new(AddProposalIn {
308            key_package: value.key_package.into(),
309        })
310    }
311}
312
313// The following `From` implementation( breaks abstraction layers and MUST
314// NOT be made available outside of tests or "test-utils".
315#[cfg(any(feature = "test-utils", test))]
316impl From<UpdateProposalIn> for UpdateProposal {
317    fn from(value: UpdateProposalIn) -> Self {
318        Self {
319            leaf_node: value.leaf_node.into(),
320        }
321    }
322}
323
324impl From<UpdateProposal> for UpdateProposalIn {
325    fn from(value: UpdateProposal) -> Self {
326        Self {
327            leaf_node: value.leaf_node.into(),
328        }
329    }
330}
331
332// The following `From` implementation breaks abstraction layers and MUST
333// NOT be made available outside of tests or "test-utils".
334#[cfg(any(feature = "test-utils", test))]
335impl From<GroupContextExtensionProposalIn> for GroupContextExtensionProposal {
336    fn from(value: GroupContextExtensionProposalIn) -> Self {
337        Self::new(value.extensions_tbv.try_into().unwrap())
338    }
339}
340
341#[cfg(any(feature = "test-utils", test))]
342impl From<GroupContextExtensionProposalIn> for Box<GroupContextExtensionProposal> {
343    fn from(value: GroupContextExtensionProposalIn) -> Self {
344        Box::new(GroupContextExtensionProposal::new(
345            value.extensions_tbv.try_into().unwrap(),
346        ))
347    }
348}
349
350impl From<GroupContextExtensionProposal> for GroupContextExtensionProposalIn {
351    fn from(value: crate::messages::proposals::GroupContextExtensionProposal) -> Self {
352        Self {
353            extensions_tbv: value.extensions().clone().into(),
354        }
355    }
356}
357
358impl From<GroupContextExtensionProposal> for Box<GroupContextExtensionProposalIn> {
359    fn from(value: GroupContextExtensionProposal) -> Self {
360        Box::new(GroupContextExtensionProposalIn {
361            extensions_tbv: value.into_extensions().into(),
362        })
363    }
364}
365
366#[cfg(any(feature = "test-utils", test))]
367impl From<UpdateProposalIn> for Box<UpdateProposal> {
368    fn from(value: UpdateProposalIn) -> Self {
369        Box::new(UpdateProposal {
370            leaf_node: value.leaf_node.into(),
371        })
372    }
373}
374
375impl From<UpdateProposal> for Box<UpdateProposalIn> {
376    fn from(value: UpdateProposal) -> Self {
377        Box::new(UpdateProposalIn {
378            leaf_node: value.leaf_node.into(),
379        })
380    }
381}
382
383#[cfg(any(feature = "test-utils", test))]
384impl From<ProposalIn> for crate::messages::proposals::Proposal {
385    fn from(proposal: ProposalIn) -> Self {
386        match proposal {
387            ProposalIn::Add(add) => Self::Add((*add).into()),
388            ProposalIn::Update(update) => Self::Update((*update).into()),
389            ProposalIn::Remove(remove) => Self::Remove(remove),
390            ProposalIn::PreSharedKey(psk) => Self::PreSharedKey(psk),
391            ProposalIn::ReInit(reinit) => Self::ReInit(reinit),
392            ProposalIn::ExternalInit(external_init) => Self::ExternalInit(external_init),
393            ProposalIn::GroupContextExtensions(group_context_extension) => {
394                Self::GroupContextExtensions((*group_context_extension).into())
395            }
396            #[cfg(feature = "extensions-draft-08")]
397            ProposalIn::AppDataUpdate(app_data_update) => Self::AppDataUpdate(app_data_update),
398            ProposalIn::SelfRemove => Self::SelfRemove,
399            #[cfg(feature = "extensions-draft-08")]
400            ProposalIn::AppEphemeral(app_ephemeral) => Self::AppEphemeral(app_ephemeral),
401            ProposalIn::Custom(other) => Self::Custom(other),
402        }
403    }
404}
405
406impl From<crate::messages::proposals::Proposal> for ProposalIn {
407    fn from(proposal: crate::messages::proposals::Proposal) -> Self {
408        match proposal {
409            Proposal::Add(add) => Self::Add((*add).into()),
410            Proposal::Update(update) => Self::Update((*update).into()),
411            Proposal::Remove(remove) => Self::Remove(remove),
412            Proposal::PreSharedKey(psk) => Self::PreSharedKey(psk),
413            Proposal::ReInit(reinit) => Self::ReInit(reinit),
414            Proposal::ExternalInit(external_init) => Self::ExternalInit(external_init),
415            Proposal::GroupContextExtensions(group_context_extension) => {
416                Self::GroupContextExtensions((*group_context_extension).into())
417            }
418            #[cfg(feature = "extensions-draft-08")]
419            Proposal::AppDataUpdate(app_data_update) => Self::AppDataUpdate(app_data_update),
420            Proposal::SelfRemove => Self::SelfRemove,
421            #[cfg(feature = "extensions-draft-08")]
422            Proposal::AppEphemeral(app_ephemeral) => Self::AppEphemeral(app_ephemeral),
423            Proposal::Custom(other) => Self::Custom(other),
424        }
425    }
426}
427
428#[cfg(any(feature = "test-utils", test))]
429impl From<ProposalOrRefIn> for crate::messages::proposals::ProposalOrRef {
430    fn from(proposal: ProposalOrRefIn) -> Self {
431        match proposal {
432            ProposalOrRefIn::Proposal(proposal) => Self::Proposal(Box::new((*proposal).into())),
433            ProposalOrRefIn::Reference(reference) => Self::Reference(reference),
434        }
435    }
436}
437
438impl From<crate::messages::proposals::ProposalOrRef> for ProposalOrRefIn {
439    fn from(proposal: crate::messages::proposals::ProposalOrRef) -> Self {
440        match proposal {
441            crate::messages::proposals::ProposalOrRef::Proposal(proposal) => {
442                Self::Proposal(Box::new((*proposal).into()))
443            }
444            crate::messages::proposals::ProposalOrRef::Reference(reference) => {
445                Self::Reference(reference)
446            }
447        }
448    }
449}
450
451/// GroupContext Extension Proposal.
452#[derive(
453    Debug,
454    PartialEq,
455    Clone,
456    Serialize,
457    Deserialize,
458    TlsSerialize,
459    TlsDeserialize,
460    TlsDeserializeBytes,
461    TlsSize,
462)]
463pub struct GroupContextExtensionProposalIn {
464    extensions_tbv: Extensions<AnyObject>,
465}
466
467impl GroupContextExtensionProposalIn {
468    pub(crate) fn validate(self) -> Result<GroupContextExtensionProposal, ValidationError> {
469        let group_context_extensions = self.extensions_tbv;
470        Ok(GroupContextExtensionProposal::new(
471            group_context_extensions
472                .try_into()
473                .map_err(InvalidExtensionError::from)?,
474        ))
475    }
476}