Skip to main content

openmls/messages/
proposals.rs

1//! # Proposals
2//!
3//! This module defines all the different types of Proposals.
4
5use std::io::{Read, Write};
6
7use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10use tls_codec::{
11    Deserialize as TlsDeserializeTrait, DeserializeBytes, Error, Serialize as TlsSerializeTrait,
12    Size, TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize, VLBytes,
13};
14
15use crate::{
16    binary_tree::array_representation::LeafNodeIndex,
17    ciphersuite::hash_ref::{make_proposal_ref, KeyPackageRef, ProposalRef},
18    error::LibraryError,
19    extensions::Extensions,
20    framing::{
21        mls_auth_content::AuthenticatedContent, mls_content::FramedContentBody, ContentType,
22    },
23    group::{GroupContext, GroupId},
24    key_packages::*,
25    schedule::psk::*,
26    treesync::LeafNode,
27    versions::ProtocolVersion,
28};
29
30#[cfg(feature = "extensions-draft")]
31use crate::component::ComponentId;
32
33/// ## MLS Proposal Types
34///
35///
36/// ```c
37/// // RFC 9420
38/// // See IANA registry for registered values
39/// uint16 ProposalType;
40/// ```
41///
42/// | Value           | Name                     | R | Ext | Path | Ref      |
43/// |-----------------|--------------------------|---|-----|------|----------|
44/// | 0x0000          | RESERVED                 | - | -   | -    | RFC 9420 |
45/// | 0x0001          | add                      | Y | Y   | N    | RFC 9420 |
46/// | 0x0002          | update                   | Y | N   | Y    | RFC 9420 |
47/// | 0x0003          | remove                   | Y | Y   | Y    | RFC 9420 |
48/// | 0x0004          | psk                      | Y | Y   | N    | RFC 9420 |
49/// | 0x0005          | reinit                   | Y | Y   | N    | RFC 9420 |
50/// | 0x0006          | external_init            | Y | N   | Y    | RFC 9420 |
51/// | 0x0007          | group_context_extensions | Y | Y   | Y    | RFC 9420 |
52/// | 0x0A0A          | GREASE                   | Y | -   | -    | RFC 9420 |
53/// | 0x1A1A          | GREASE                   | Y | -   | -    | RFC 9420 |
54/// | 0x2A2A          | GREASE                   | Y | -   | -    | RFC 9420 |
55/// | 0x3A3A          | GREASE                   | Y | -   | -    | RFC 9420 |
56/// | 0x4A4A          | GREASE                   | Y | -   | -    | RFC 9420 |
57/// | 0x5A5A          | GREASE                   | Y | -   | -    | RFC 9420 |
58/// | 0x6A6A          | GREASE                   | Y | -   | -    | RFC 9420 |
59/// | 0x7A7A          | GREASE                   | Y | -   | -    | RFC 9420 |
60/// | 0x8A8A          | GREASE                   | Y | -   | -    | RFC 9420 |
61/// | 0x9A9A          | GREASE                   | Y | -   | -    | RFC 9420 |
62/// | 0xAAAA          | GREASE                   | Y | -   | -    | RFC 9420 |
63/// | 0xBABA          | GREASE                   | Y | -   | -    | RFC 9420 |
64/// | 0xCACA          | GREASE                   | Y | -   | -    | RFC 9420 |
65/// | 0xDADA          | GREASE                   | Y | -   | -    | RFC 9420 |
66/// | 0xEAEA          | GREASE                   | Y | -   | -    | RFC 9420 |
67/// | 0xF000 - 0xFFFF | Reserved for Private Use | - | -   | -    | RFC 9420 |
68///
69/// # Extensions
70///
71/// | Value  | Name          | Recommended | Path Required | Reference | Notes                        |
72/// |:=======|:==============|:============|:==============|:==========|:=============================|
73/// | 0x0009 | app_ephemeral | Y           | N             | RFC XXXX  | draft-ietf-mls-extensions-08 |
74/// | 0x000a | self_remove   | Y           | Y             | RFC XXXX  | draft-ietf-mls-extensions-07 |
75#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash)]
76#[cfg_attr(
77    feature = "0-8-1-storage-format",
78    derive(serde::Serialize, serde::Deserialize)
79)]
80#[cfg_attr(
81    not(feature = "0-8-1-storage-format"),
82    derive(
83        openmls_serialization_helpers::Serialize,
84        openmls_serialization_helpers::Deserialize,
85    )
86)]
87#[allow(missing_docs)]
88#[repr(u16)]
89pub enum ProposalType {
90    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 0)]
91    Add,
92    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 1)]
93    Update,
94    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 2)]
95    Remove,
96    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 3)]
97    PreSharedKey,
98    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 4)]
99    Reinit,
100    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 5)]
101    ExternalInit,
102    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 6)]
103    GroupContextExtensions,
104    // AppAck = 7,
105    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 8)]
106    SelfRemove,
107    #[cfg(feature = "extensions-draft")]
108    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 11)]
109    AppEphemeral,
110    #[cfg(feature = "extensions-draft")]
111    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 12)]
112    AppDataUpdate,
113    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 10)]
114    Grease(u16),
115    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 9)]
116    Custom(u16),
117}
118
119impl ProposalType {
120    /// Returns true for all proposal types that are considered "default" by the
121    /// spec.
122    pub(crate) fn is_default(self) -> bool {
123        match self {
124            ProposalType::Add
125            | ProposalType::Update
126            | ProposalType::Remove
127            | ProposalType::PreSharedKey
128            | ProposalType::Reinit
129            | ProposalType::ExternalInit
130            | ProposalType::GroupContextExtensions => true,
131            ProposalType::SelfRemove | ProposalType::Grease(_) | ProposalType::Custom(_) => false,
132            #[cfg(feature = "extensions-draft")]
133            ProposalType::AppEphemeral | ProposalType::AppDataUpdate => false,
134        }
135    }
136
137    /// Returns true if this is a GREASE proposal type.
138    ///
139    /// GREASE values are used to ensure implementations properly handle unknown
140    /// proposal types. See [RFC 9420 Section 13.5](https://www.rfc-editor.org/rfc/rfc9420.html#section-13.5).
141    pub fn is_grease(&self) -> bool {
142        matches!(self, ProposalType::Grease(_))
143    }
144}
145
146impl Size for ProposalType {
147    fn tls_serialized_len(&self) -> usize {
148        2
149    }
150}
151
152impl TlsDeserializeTrait for ProposalType {
153    fn tls_deserialize<R: Read>(bytes: &mut R) -> Result<Self, Error>
154    where
155        Self: Sized,
156    {
157        let mut proposal_type = [0u8; 2];
158        bytes.read_exact(&mut proposal_type)?;
159
160        Ok(ProposalType::from(u16::from_be_bytes(proposal_type)))
161    }
162}
163
164impl TlsSerializeTrait for ProposalType {
165    fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
166        writer.write_all(&u16::from(*self).to_be_bytes())?;
167
168        Ok(2)
169    }
170}
171
172impl DeserializeBytes for ProposalType {
173    fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error>
174    where
175        Self: Sized,
176    {
177        let mut bytes_ref = bytes;
178        let proposal_type = ProposalType::tls_deserialize(&mut bytes_ref)?;
179        Ok((proposal_type, bytes_ref))
180    }
181}
182
183impl ProposalType {
184    /// Returns `true` if the proposal type requires a path and `false`
185    pub fn is_path_required(&self) -> bool {
186        matches!(
187            self,
188            Self::Update
189                | Self::Remove
190                | Self::ExternalInit
191                | Self::GroupContextExtensions
192                | Self::SelfRemove
193        )
194    }
195}
196
197impl From<u16> for ProposalType {
198    fn from(value: u16) -> Self {
199        match value {
200            1 => ProposalType::Add,
201            2 => ProposalType::Update,
202            3 => ProposalType::Remove,
203            4 => ProposalType::PreSharedKey,
204            5 => ProposalType::Reinit,
205            6 => ProposalType::ExternalInit,
206            7 => ProposalType::GroupContextExtensions,
207            #[cfg(feature = "extensions-draft")]
208            8 => ProposalType::AppDataUpdate,
209            #[cfg(feature = "extensions-draft")]
210            0x0009 => ProposalType::AppEphemeral,
211            0x000a => ProposalType::SelfRemove,
212            other if crate::grease::is_grease_value(other) => ProposalType::Grease(other),
213            other => ProposalType::Custom(other),
214        }
215    }
216}
217
218impl From<ProposalType> for u16 {
219    fn from(value: ProposalType) -> Self {
220        match value {
221            ProposalType::Add => 1,
222            ProposalType::Update => 2,
223            ProposalType::Remove => 3,
224            ProposalType::PreSharedKey => 4,
225            ProposalType::Reinit => 5,
226            ProposalType::ExternalInit => 6,
227            ProposalType::GroupContextExtensions => 7,
228            #[cfg(feature = "extensions-draft")]
229            ProposalType::AppDataUpdate => 8,
230            #[cfg(feature = "extensions-draft")]
231            ProposalType::AppEphemeral => 0x0009,
232            ProposalType::SelfRemove => 0x000a,
233            ProposalType::Grease(id) => id,
234            ProposalType::Custom(id) => id,
235        }
236    }
237}
238
239/// Proposal.
240///
241/// This `enum` contains the different proposals in its variants.
242///
243/// ```c
244/// // draft-ietf-mls-protocol-17
245/// struct {
246///     ProposalType msg_type;
247///     select (Proposal.msg_type) {
248///         case add:                      Add;
249///         case update:                   Update;
250///         case remove:                   Remove;
251///         case psk:                      PreSharedKey;
252///         case reinit:                   ReInit;
253///         case external_init:            ExternalInit;
254///         case group_context_extensions: GroupContextExtensions;
255///     };
256/// } Proposal;
257/// ```
258#[derive(Debug, PartialEq, Clone)]
259#[cfg_attr(
260    feature = "0-8-1-storage-format",
261    derive(serde::Serialize, serde::Deserialize)
262)]
263#[cfg_attr(
264    not(feature = "0-8-1-storage-format"),
265    derive(
266        openmls_serialization_helpers::Serialize,
267        openmls_serialization_helpers::Deserialize,
268    )
269)]
270#[allow(missing_docs)]
271pub enum Proposal {
272    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 0)]
273    Add(Box<AddProposal>),
274    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 1)]
275    Update(Box<UpdateProposal>),
276    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 2)]
277    Remove(Box<RemoveProposal>),
278    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 3)]
279    PreSharedKey(Box<PreSharedKeyProposal>),
280    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 4)]
281    ReInit(Box<ReInitProposal>),
282    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 5)]
283    ExternalInit(Box<ExternalInitProposal>),
284    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 6)]
285    GroupContextExtensions(Box<GroupContextExtensionProposal>),
286    // AppAck = 7,
287    // # Extensions
288    #[cfg(feature = "extensions-draft")]
289    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 10)]
290    AppDataUpdate(Box<AppDataUpdateProposal>),
291    // A SelfRemove proposal is an empty struct.
292    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 8)]
293    SelfRemove,
294    #[cfg(feature = "extensions-draft")]
295    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 11)]
296    AppEphemeral(Box<AppEphemeralProposal>),
297    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 9)]
298    Custom(Box<CustomProposal>),
299}
300
301impl Proposal {
302    /// Build a remove proposal.
303    pub(crate) fn remove(r: RemoveProposal) -> Self {
304        Self::Remove(Box::new(r))
305    }
306
307    /// Build an add proposal.
308    pub(crate) fn add(a: AddProposal) -> Self {
309        Self::Add(Box::new(a))
310    }
311
312    /// Build a custom proposal.
313    pub(crate) fn custom(c: CustomProposal) -> Self {
314        Self::Custom(Box::new(c))
315    }
316
317    /// Build a psk proposal.
318    pub(crate) fn psk(p: PreSharedKeyProposal) -> Self {
319        Self::PreSharedKey(Box::new(p))
320    }
321
322    /// Build an update proposal.
323    pub(crate) fn update(p: UpdateProposal) -> Self {
324        Self::Update(Box::new(p))
325    }
326
327    /// Build a GroupContextExtensionProposal proposal.
328    pub(crate) fn group_context_extensions(p: GroupContextExtensionProposal) -> Self {
329        Self::GroupContextExtensions(Box::new(p))
330    }
331
332    /// Build an ExternalInit proposal.
333    pub(crate) fn external_init(p: ExternalInitProposal) -> Self {
334        Self::ExternalInit(Box::new(p))
335    }
336
337    /// Build a ReInit proposal.
338    pub fn re_init(p: ReInitProposal) -> Self {
339        Self::ReInit(Box::new(p))
340    }
341
342    /// Returns the proposal type.
343    pub fn proposal_type(&self) -> ProposalType {
344        match self {
345            Proposal::Add(_) => ProposalType::Add,
346            Proposal::Update(_) => ProposalType::Update,
347            Proposal::Remove(_) => ProposalType::Remove,
348            Proposal::PreSharedKey(_) => ProposalType::PreSharedKey,
349            Proposal::ReInit(_) => ProposalType::Reinit,
350            Proposal::ExternalInit(_) => ProposalType::ExternalInit,
351            Proposal::GroupContextExtensions(_) => ProposalType::GroupContextExtensions,
352            #[cfg(feature = "extensions-draft")]
353            Proposal::AppDataUpdate(_) => ProposalType::AppDataUpdate,
354            Proposal::SelfRemove => ProposalType::SelfRemove,
355            #[cfg(feature = "extensions-draft")]
356            Proposal::AppEphemeral(_) => ProposalType::AppEphemeral,
357            Proposal::Custom(custom) => ProposalType::Custom(custom.proposal_type.to_owned()),
358        }
359    }
360
361    pub(crate) fn is_type(&self, proposal_type: ProposalType) -> bool {
362        self.proposal_type() == proposal_type
363    }
364
365    /// Indicates whether a Commit containing this [Proposal] requires a path.
366    pub fn is_path_required(&self) -> bool {
367        self.proposal_type().is_path_required()
368    }
369
370    pub(crate) fn has_lower_priority_than(&self, new_proposal: &Proposal) -> bool {
371        match (self, new_proposal) {
372            // Updates have the lowest priority.
373            (Proposal::Update(_), _) => true,
374            // Removes have a higher priority than Updates.
375            (Proposal::Remove(_), Proposal::Update(_)) => false,
376            // Later Removes trump earlier Removes
377            (Proposal::Remove(_), Proposal::Remove(_)) => true,
378            // SelfRemoves have the highest priority.
379            (_, Proposal::SelfRemove) => true,
380            (Proposal::SelfRemove, Proposal::Update(_) | Proposal::Remove(_)) => false,
381            _ => {
382                debug_assert!(false);
383                false
384            }
385        }
386    }
387
388    // Get this proposal as a `RemoveProposal`.
389    pub(crate) fn as_remove(&self) -> Option<&RemoveProposal> {
390        if let Self::Remove(v) = self {
391            Some(v)
392        } else {
393            None
394        }
395    }
396
397    /// Returns `true` if the proposal is [`Remove`].
398    ///
399    /// [`Remove`]: Proposal::Remove
400    #[must_use]
401    pub fn is_remove(&self) -> bool {
402        matches!(self, Self::Remove(..))
403    }
404}
405
406/// Add Proposal.
407///
408/// An Add proposal requests that a client with a specified [`KeyPackage`] be
409/// added to the group.
410///
411/// ```c
412/// // draft-ietf-mls-protocol-17
413/// struct {
414///     KeyPackage key_package;
415/// } Add;
416/// ```
417#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
418pub struct AddProposal {
419    pub(crate) key_package: KeyPackage,
420}
421
422impl AddProposal {
423    /// Returns a reference to the key package in the proposal.
424    pub fn key_package(&self) -> &KeyPackage {
425        &self.key_package
426    }
427}
428
429impl From<KeyPackage> for AddProposal {
430    fn from(key_package: KeyPackage) -> AddProposal {
431        AddProposal { key_package }
432    }
433}
434
435/// Update Proposal.
436///
437/// An Update proposal is a similar mechanism to [`AddProposal`] with the
438/// distinction that it replaces the sender's [`LeafNode`] in the tree instead
439/// of adding a new leaf to the tree.
440///
441/// ```c
442/// // draft-ietf-mls-protocol-17
443/// struct {
444///     LeafNode leaf_node;
445/// } Update;
446/// ```
447#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
448pub struct UpdateProposal {
449    pub(crate) leaf_node: LeafNode,
450}
451
452impl UpdateProposal {
453    /// Returns a reference to the leaf node in the proposal.
454    pub fn leaf_node(&self) -> &LeafNode {
455        &self.leaf_node
456    }
457}
458
459/// Remove Proposal.
460///
461/// A Remove proposal requests that the member with the leaf index removed be
462/// removed from the group.
463///
464/// ```c
465/// // draft-ietf-mls-protocol-17
466/// struct {
467///     uint32 removed;
468/// } Remove;
469/// ```
470#[derive(
471    Debug,
472    PartialEq,
473    Eq,
474    Clone,
475    Serialize,
476    Deserialize,
477    TlsDeserialize,
478    TlsDeserializeBytes,
479    TlsSerialize,
480    TlsSize,
481)]
482pub struct RemoveProposal {
483    pub(crate) removed: LeafNodeIndex,
484}
485
486impl RemoveProposal {
487    /// Returns the leaf index of the removed leaf in this proposal.
488    pub fn removed(&self) -> LeafNodeIndex {
489        self.removed
490    }
491}
492
493/// PreSharedKey Proposal.
494///
495/// A PreSharedKey proposal can be used to request that a pre-shared key be
496/// injected into the key schedule in the process of advancing the epoch.
497///
498/// ```c
499/// // draft-ietf-mls-protocol-17
500/// struct {
501///     PreSharedKeyID psk;
502/// } PreSharedKey;
503/// ```
504#[derive(
505    Debug,
506    PartialEq,
507    Eq,
508    Clone,
509    Serialize,
510    Deserialize,
511    TlsDeserialize,
512    TlsDeserializeBytes,
513    TlsSerialize,
514    TlsSize,
515)]
516pub struct PreSharedKeyProposal {
517    psk: PreSharedKeyId,
518}
519
520impl PreSharedKeyProposal {
521    /// Create a new PSK proposal
522    pub fn new(psk: PreSharedKeyId) -> Self {
523        Self { psk }
524    }
525
526    /// Returns a reference to the [`PreSharedKeyId`] in the proposal.
527    pub fn psk(&self) -> &PreSharedKeyId {
528        &self.psk
529    }
530
531    /// Returns the [`PreSharedKeyId`] and consume this proposal.
532    pub(crate) fn into_psk_id(self) -> PreSharedKeyId {
533        self.psk
534    }
535}
536
537/// ReInit Proposal.
538///
539/// A ReInit proposal represents a request to reinitialize the group with
540/// different parameters, for example, to increase the version number or to
541/// change the ciphersuite. The reinitialization is done by creating a
542/// completely new group and shutting down the old one.
543///
544/// ```c
545/// // draft-ietf-mls-protocol-17
546/// struct {
547///     opaque group_id<V>;
548///     ProtocolVersion version;
549///     CipherSuite cipher_suite;
550///     Extension extensions<V>;
551/// } ReInit;
552/// ```
553#[derive(
554    Debug,
555    PartialEq,
556    Eq,
557    Clone,
558    Serialize,
559    Deserialize,
560    TlsDeserialize,
561    TlsDeserializeBytes,
562    TlsSerialize,
563    TlsSize,
564)]
565pub struct ReInitProposal {
566    pub(crate) group_id: GroupId,
567    pub(crate) version: ProtocolVersion,
568    pub(crate) ciphersuite: Ciphersuite,
569    pub(crate) extensions: Extensions<GroupContext>,
570}
571
572impl ReInitProposal {
573    /// Create a new ReInit proposal describing the parameters of the successor
574    /// group the current group should be reinitialized into.
575    pub fn new(
576        group_id: GroupId,
577        version: ProtocolVersion,
578        ciphersuite: Ciphersuite,
579        extensions: Extensions<GroupContext>,
580    ) -> Self {
581        Self {
582            group_id,
583            version,
584            ciphersuite,
585            extensions,
586        }
587    }
588
589    /// Returns the [`GroupId`] of the successor group.
590    pub fn group_id(&self) -> &GroupId {
591        &self.group_id
592    }
593
594    /// Returns the [`ProtocolVersion`] of the successor group.
595    pub fn version(&self) -> ProtocolVersion {
596        self.version
597    }
598
599    /// Returns the [`Ciphersuite`] of the successor group.
600    pub fn ciphersuite(&self) -> Ciphersuite {
601        self.ciphersuite
602    }
603
604    /// Returns the [`Extensions`] of the successor group.
605    pub fn extensions(&self) -> &Extensions<GroupContext> {
606        &self.extensions
607    }
608}
609
610/// ExternalInit Proposal.
611///
612/// An ExternalInit proposal is used by new members that want to join a group by
613/// using an external commit. This proposal can only be used in that context.
614///
615/// ```c
616/// // draft-ietf-mls-protocol-17
617/// struct {
618///   opaque kem_output<V>;
619/// } ExternalInit;
620/// ```
621#[derive(
622    Debug,
623    PartialEq,
624    Eq,
625    Clone,
626    Serialize,
627    Deserialize,
628    TlsDeserialize,
629    TlsDeserializeBytes,
630    TlsSerialize,
631    TlsSize,
632)]
633pub struct ExternalInitProposal {
634    kem_output: VLBytes,
635}
636
637impl ExternalInitProposal {
638    /// Returns the `kem_output` contained in the proposal.
639    pub(crate) fn kem_output(&self) -> &[u8] {
640        self.kem_output.as_slice()
641    }
642}
643
644impl From<Vec<u8>> for ExternalInitProposal {
645    fn from(kem_output: Vec<u8>) -> Self {
646        ExternalInitProposal {
647            kem_output: kem_output.into(),
648        }
649    }
650}
651
652#[cfg(feature = "extensions-draft")]
653/// AppAck object.
654///
655/// This is not yet supported.
656#[derive(
657    Debug,
658    PartialEq,
659    Clone,
660    Serialize,
661    Deserialize,
662    TlsDeserialize,
663    TlsDeserializeBytes,
664    TlsSerialize,
665    TlsSize,
666)]
667pub struct AppAck {
668    received_ranges: Vec<MessageRange>,
669}
670
671#[cfg(feature = "extensions-draft")]
672/// AppEphemeral proposal.
673#[derive(
674    Debug,
675    PartialEq,
676    Clone,
677    Serialize,
678    Deserialize,
679    TlsDeserialize,
680    TlsDeserializeBytes,
681    TlsSerialize,
682    TlsSize,
683)]
684pub struct AppEphemeralProposal {
685    /// The unique [`ComponentId`] associated with the proposal.
686    component_id: ComponentId,
687    /// Application data.
688    data: VLBytes,
689}
690
691#[cfg(feature = "extensions-draft")]
692impl AppEphemeralProposal {
693    /// Create a new [`AppEphemeralProposal`].
694    pub fn new(component_id: ComponentId, data: Vec<u8>) -> Self {
695        Self {
696            component_id,
697            data: data.into(),
698        }
699    }
700    /// Returns the `component_id` contained in the proposal.
701    pub fn component_id(&self) -> ComponentId {
702        self.component_id
703    }
704
705    /// Returns the `data` contained in the proposal.
706    pub fn data(&self) -> &[u8] {
707        self.data.as_slice()
708    }
709}
710
711/// GroupContextExtensions Proposal.
712///
713/// A GroupContextExtensions proposal is used to update the list of extensions
714/// in the GroupContext for the group.
715///
716/// ```c
717/// // draft-ietf-mls-protocol-17
718/// struct {
719///   Extension extensions<V>;
720/// } GroupContextExtensions;
721/// ```
722#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
723pub struct GroupContextExtensionProposal {
724    extensions: Extensions<GroupContext>,
725}
726
727impl Size for GroupContextExtensionProposal {
728    fn tls_serialized_len(&self) -> usize {
729        self.extensions.tls_serialized_len()
730    }
731}
732
733impl TlsSerializeTrait for GroupContextExtensionProposal {
734    fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
735        self.extensions.tls_serialize(writer)
736    }
737}
738
739impl GroupContextExtensionProposal {
740    /// Create a new [`GroupContextExtensionProposal`].
741    pub(crate) fn new(extensions: Extensions<GroupContext>) -> Self {
742        Self { extensions }
743    }
744
745    /// Get the extensions of the proposal
746    pub fn extensions(&self) -> &Extensions<GroupContext> {
747        &self.extensions
748    }
749
750    /// Consumes the proposal and returns the contained extensions.
751    pub fn into_extensions(self) -> Extensions<GroupContext> {
752        self.extensions
753    }
754}
755
756// Crate-only types
757
758/// 11.2 Commit
759///
760/// enum {
761///   reserved(0),
762///   proposal(1)
763///   reference(2),
764///   (255)
765/// } ProposalOrRefType;
766///
767/// struct {
768///   ProposalOrRefType type;
769///   select (ProposalOrRef.type) {
770///     case proposal:  Proposal proposal;
771///     case reference: opaque hash<0..255>;
772///   }
773/// } ProposalOrRef;
774///
775/// Type of Proposal, either by value or by reference
776/// We only implement the values (1, 2), other values are not valid
777/// and will yield `ProposalOrRefTypeError::UnknownValue` when decoded.
778#[derive(
779    PartialEq,
780    Clone,
781    Copy,
782    Debug,
783    TlsSerialize,
784    TlsDeserialize,
785    TlsDeserializeBytes,
786    TlsSize,
787    Serialize,
788    Deserialize,
789)]
790#[repr(u8)]
791pub enum ProposalOrRefType {
792    /// Proposal by value.
793    Proposal = 1,
794    /// Proposal by reference
795    Reference = 2,
796}
797
798/// Type of Proposal, either by value or by reference.
799#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
800#[repr(u8)]
801#[allow(missing_docs)]
802pub enum ProposalOrRef {
803    #[tls_codec(discriminant = 1)]
804    Proposal(Box<Proposal>),
805    Reference(Box<ProposalRef>),
806}
807
808impl ProposalOrRef {
809    /// Create a proposal by value.
810    pub(crate) fn proposal(p: Proposal) -> Self {
811        Self::Proposal(Box::new(p))
812    }
813
814    /// Create a proposal by reference.
815    pub(crate) fn reference(p: ProposalRef) -> Self {
816        Self::Reference(Box::new(p))
817    }
818
819    pub(crate) fn as_proposal(&self) -> Option<&Proposal> {
820        if let Self::Proposal(v) = self {
821            Some(v)
822        } else {
823            None
824        }
825    }
826
827    pub(crate) fn as_reference(&self) -> Option<&ProposalRef> {
828        if let Self::Reference(v) = self {
829            Some(v)
830        } else {
831            None
832        }
833    }
834}
835
836impl From<Proposal> for ProposalOrRef {
837    fn from(value: Proposal) -> Self {
838        Self::proposal(value)
839    }
840}
841
842impl From<ProposalRef> for ProposalOrRef {
843    fn from(value: ProposalRef) -> Self {
844        Self::reference(value)
845    }
846}
847
848#[derive(Error, Debug)]
849pub(crate) enum ProposalRefError {
850    #[error("Expected `Proposal`, got `{wrong:?}`.")]
851    AuthenticatedContentHasWrongType { wrong: ContentType },
852    #[error(transparent)]
853    Other(#[from] LibraryError),
854}
855
856impl ProposalRef {
857    pub(crate) fn from_authenticated_content_by_ref(
858        crypto: &impl OpenMlsCrypto,
859        ciphersuite: Ciphersuite,
860        authenticated_content: &AuthenticatedContent,
861    ) -> Result<Self, ProposalRefError> {
862        if !matches!(
863            authenticated_content.content(),
864            FramedContentBody::Proposal(_)
865        ) {
866            return Err(ProposalRefError::AuthenticatedContentHasWrongType {
867                wrong: authenticated_content.content().content_type(),
868            });
869        };
870
871        let encoded = authenticated_content
872            .tls_serialize_detached()
873            .map_err(|error| ProposalRefError::Other(LibraryError::missing_bound_check(error)))?;
874
875        make_proposal_ref(&encoded, ciphersuite, crypto)
876            .map_err(|error| ProposalRefError::Other(LibraryError::unexpected_crypto_error(error)))
877    }
878
879    /// Note: A [`ProposalRef`] should be calculated by using TLS-serialized
880    /// [`AuthenticatedContent`]       as value input and not the
881    /// TLS-serialized proposal. However, to spare us a major refactoring,
882    ///       we calculate it from the raw value in some places that do not
883    /// interact with the outside world.
884    pub(crate) fn from_raw_proposal(
885        ciphersuite: Ciphersuite,
886        crypto: &impl OpenMlsCrypto,
887        proposal: &Proposal,
888    ) -> Result<Self, LibraryError> {
889        // This is used for hash domain separation.
890        let mut data = b"Internal OpenMLS ProposalRef Label".to_vec();
891
892        let mut encoded = proposal
893            .tls_serialize_detached()
894            .map_err(LibraryError::missing_bound_check)?;
895
896        data.append(&mut encoded);
897
898        make_proposal_ref(&data, ciphersuite, crypto).map_err(LibraryError::unexpected_crypto_error)
899    }
900}
901
902/// ```text
903/// struct {
904///     KeyPackageRef sender;
905///     uint32 first_generation;
906///     uint32 last_generation;
907/// } MessageRange;
908/// ```
909#[derive(
910    Debug,
911    PartialEq,
912    Clone,
913    Serialize,
914    Deserialize,
915    TlsDeserialize,
916    TlsDeserializeBytes,
917    TlsSerialize,
918    TlsSize,
919)]
920pub(crate) struct MessageRange {
921    sender: KeyPackageRef,
922    first_generation: u32,
923    last_generation: u32,
924}
925
926#[cfg(feature = "extensions-draft")]
927mod app_data_update;
928#[cfg(feature = "extensions-draft")]
929pub use app_data_update::*;
930
931/// A custom proposal with semantics to be implemented by the application.
932#[derive(
933    Debug,
934    PartialEq,
935    Clone,
936    Serialize,
937    Deserialize,
938    TlsSize,
939    TlsSerialize,
940    TlsDeserialize,
941    TlsDeserializeBytes,
942)]
943pub struct CustomProposal {
944    proposal_type: u16,
945    payload: Vec<u8>,
946}
947
948impl CustomProposal {
949    /// Generate a new custom proposal.
950    pub fn new(proposal_type: u16, payload: Vec<u8>) -> Self {
951        Self {
952            proposal_type,
953            payload,
954        }
955    }
956
957    /// Returns the proposal type of this [`CustomProposal`].
958    pub fn proposal_type(&self) -> u16 {
959        self.proposal_type
960    }
961
962    /// Returns the payload of this [`CustomProposal`].
963    pub fn payload(&self) -> &[u8] {
964        &self.payload
965    }
966}
967
968#[cfg(test)]
969mod tests {
970    use tls_codec::{Deserialize, Serialize};
971
972    use super::ProposalType;
973
974    #[test]
975    fn that_unknown_proposal_types_are_de_serialized_correctly() {
976        // Use non-GREASE unknown values for testing (GREASE values have pattern 0x_A_A)
977        let proposal_types = [0x0000u16, 0x0B0B, 0x7C7C, 0xF000, 0xFFFF];
978
979        for proposal_type in proposal_types.into_iter() {
980            // Construct an unknown proposal type.
981            let test = proposal_type.to_be_bytes().to_vec();
982
983            // Test deserialization.
984            let got = ProposalType::tls_deserialize_exact(&test).unwrap();
985
986            match got {
987                ProposalType::Custom(got_proposal_type) => {
988                    assert_eq!(proposal_type, got_proposal_type);
989                }
990                other => panic!("Expected `ProposalType::Unknown`, got `{other:?}`."),
991            }
992
993            // Test serialization.
994            let got_serialized = got.tls_serialize_detached().unwrap();
995            assert_eq!(test, got_serialized);
996        }
997    }
998}