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