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