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