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    #[cfg(test)]
338    /// Build a ReInit proposal.
339    pub(crate) fn re_init(p: ReInitProposal) -> Self {
340        Self::ReInit(Box::new(p))
341    }
342
343    /// Returns the proposal type.
344    pub fn proposal_type(&self) -> ProposalType {
345        match self {
346            Proposal::Add(_) => ProposalType::Add,
347            Proposal::Update(_) => ProposalType::Update,
348            Proposal::Remove(_) => ProposalType::Remove,
349            Proposal::PreSharedKey(_) => ProposalType::PreSharedKey,
350            Proposal::ReInit(_) => ProposalType::Reinit,
351            Proposal::ExternalInit(_) => ProposalType::ExternalInit,
352            Proposal::GroupContextExtensions(_) => ProposalType::GroupContextExtensions,
353            #[cfg(feature = "extensions-draft")]
354            Proposal::AppDataUpdate(_) => ProposalType::AppDataUpdate,
355            Proposal::SelfRemove => ProposalType::SelfRemove,
356            #[cfg(feature = "extensions-draft")]
357            Proposal::AppEphemeral(_) => ProposalType::AppEphemeral,
358            Proposal::Custom(custom) => ProposalType::Custom(custom.proposal_type.to_owned()),
359        }
360    }
361
362    pub(crate) fn is_type(&self, proposal_type: ProposalType) -> bool {
363        self.proposal_type() == proposal_type
364    }
365
366    /// Indicates whether a Commit containing this [Proposal] requires a path.
367    pub fn is_path_required(&self) -> bool {
368        self.proposal_type().is_path_required()
369    }
370
371    pub(crate) fn has_lower_priority_than(&self, new_proposal: &Proposal) -> bool {
372        match (self, new_proposal) {
373            // Updates have the lowest priority.
374            (Proposal::Update(_), _) => true,
375            // Removes have a higher priority than Updates.
376            (Proposal::Remove(_), Proposal::Update(_)) => false,
377            // Later Removes trump earlier Removes
378            (Proposal::Remove(_), Proposal::Remove(_)) => true,
379            // SelfRemoves have the highest priority.
380            (_, Proposal::SelfRemove) => true,
381            (Proposal::SelfRemove, Proposal::Update(_) | Proposal::Remove(_)) => false,
382            _ => {
383                debug_assert!(false);
384                false
385            }
386        }
387    }
388
389    // Get this proposal as a `RemoveProposal`.
390    pub(crate) fn as_remove(&self) -> Option<&RemoveProposal> {
391        if let Self::Remove(v) = self {
392            Some(v)
393        } else {
394            None
395        }
396    }
397
398    /// Returns `true` if the proposal is [`Remove`].
399    ///
400    /// [`Remove`]: Proposal::Remove
401    #[must_use]
402    pub fn is_remove(&self) -> bool {
403        matches!(self, Self::Remove(..))
404    }
405}
406
407/// Add Proposal.
408///
409/// An Add proposal requests that a client with a specified [`KeyPackage`] be
410/// added to the group.
411///
412/// ```c
413/// // draft-ietf-mls-protocol-17
414/// struct {
415///     KeyPackage key_package;
416/// } Add;
417/// ```
418#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
419pub struct AddProposal {
420    pub(crate) key_package: KeyPackage,
421}
422
423impl AddProposal {
424    /// Returns a reference to the key package in the proposal.
425    pub fn key_package(&self) -> &KeyPackage {
426        &self.key_package
427    }
428}
429
430impl From<KeyPackage> for AddProposal {
431    fn from(key_package: KeyPackage) -> AddProposal {
432        AddProposal { key_package }
433    }
434}
435
436/// Update Proposal.
437///
438/// An Update proposal is a similar mechanism to [`AddProposal`] with the
439/// distinction that it replaces the sender's [`LeafNode`] in the tree instead
440/// of adding a new leaf to the tree.
441///
442/// ```c
443/// // draft-ietf-mls-protocol-17
444/// struct {
445///     LeafNode leaf_node;
446/// } Update;
447/// ```
448#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
449pub struct UpdateProposal {
450    pub(crate) leaf_node: LeafNode,
451}
452
453impl UpdateProposal {
454    /// Returns a reference to the leaf node in the proposal.
455    pub fn leaf_node(&self) -> &LeafNode {
456        &self.leaf_node
457    }
458}
459
460/// Remove Proposal.
461///
462/// A Remove proposal requests that the member with the leaf index removed be
463/// removed from the group.
464///
465/// ```c
466/// // draft-ietf-mls-protocol-17
467/// struct {
468///     uint32 removed;
469/// } Remove;
470/// ```
471#[derive(
472    Debug,
473    PartialEq,
474    Eq,
475    Clone,
476    Serialize,
477    Deserialize,
478    TlsDeserialize,
479    TlsDeserializeBytes,
480    TlsSerialize,
481    TlsSize,
482)]
483pub struct RemoveProposal {
484    pub(crate) removed: LeafNodeIndex,
485}
486
487impl RemoveProposal {
488    /// Returns the leaf index of the removed leaf in this proposal.
489    pub fn removed(&self) -> LeafNodeIndex {
490        self.removed
491    }
492}
493
494/// PreSharedKey Proposal.
495///
496/// A PreSharedKey proposal can be used to request that a pre-shared key be
497/// injected into the key schedule in the process of advancing the epoch.
498///
499/// ```c
500/// // draft-ietf-mls-protocol-17
501/// struct {
502///     PreSharedKeyID psk;
503/// } PreSharedKey;
504/// ```
505#[derive(
506    Debug,
507    PartialEq,
508    Eq,
509    Clone,
510    Serialize,
511    Deserialize,
512    TlsDeserialize,
513    TlsDeserializeBytes,
514    TlsSerialize,
515    TlsSize,
516)]
517pub struct PreSharedKeyProposal {
518    psk: PreSharedKeyId,
519}
520
521impl PreSharedKeyProposal {
522    /// Create a new PSK proposal
523    pub fn new(psk: PreSharedKeyId) -> Self {
524        Self { psk }
525    }
526
527    /// Returns a reference to the [`PreSharedKeyId`] in the proposal.
528    pub fn psk(&self) -> &PreSharedKeyId {
529        &self.psk
530    }
531
532    /// Returns the [`PreSharedKeyId`] and consume this proposal.
533    pub(crate) fn into_psk_id(self) -> PreSharedKeyId {
534        self.psk
535    }
536}
537
538/// ReInit Proposal.
539///
540/// A ReInit proposal represents a request to reinitialize the group with
541/// different parameters, for example, to increase the version number or to
542/// change the ciphersuite. The reinitialization is done by creating a
543/// completely new group and shutting down the old one.
544///
545/// ```c
546/// // draft-ietf-mls-protocol-17
547/// struct {
548///     opaque group_id<V>;
549///     ProtocolVersion version;
550///     CipherSuite cipher_suite;
551///     Extension extensions<V>;
552/// } ReInit;
553/// ```
554#[derive(
555    Debug,
556    PartialEq,
557    Eq,
558    Clone,
559    Serialize,
560    Deserialize,
561    TlsDeserialize,
562    TlsDeserializeBytes,
563    TlsSerialize,
564    TlsSize,
565)]
566pub struct ReInitProposal {
567    pub(crate) group_id: GroupId,
568    pub(crate) version: ProtocolVersion,
569    pub(crate) ciphersuite: Ciphersuite,
570    pub(crate) extensions: Extensions<GroupContext>,
571}
572
573/// ExternalInit Proposal.
574///
575/// An ExternalInit proposal is used by new members that want to join a group by
576/// using an external commit. This proposal can only be used in that context.
577///
578/// ```c
579/// // draft-ietf-mls-protocol-17
580/// struct {
581///   opaque kem_output<V>;
582/// } ExternalInit;
583/// ```
584#[derive(
585    Debug,
586    PartialEq,
587    Eq,
588    Clone,
589    Serialize,
590    Deserialize,
591    TlsDeserialize,
592    TlsDeserializeBytes,
593    TlsSerialize,
594    TlsSize,
595)]
596pub struct ExternalInitProposal {
597    kem_output: VLBytes,
598}
599
600impl ExternalInitProposal {
601    /// Returns the `kem_output` contained in the proposal.
602    pub(crate) fn kem_output(&self) -> &[u8] {
603        self.kem_output.as_slice()
604    }
605}
606
607impl From<Vec<u8>> for ExternalInitProposal {
608    fn from(kem_output: Vec<u8>) -> Self {
609        ExternalInitProposal {
610            kem_output: kem_output.into(),
611        }
612    }
613}
614
615#[cfg(feature = "extensions-draft")]
616/// AppAck object.
617///
618/// This is not yet supported.
619#[derive(
620    Debug,
621    PartialEq,
622    Clone,
623    Serialize,
624    Deserialize,
625    TlsDeserialize,
626    TlsDeserializeBytes,
627    TlsSerialize,
628    TlsSize,
629)]
630pub struct AppAck {
631    received_ranges: Vec<MessageRange>,
632}
633
634#[cfg(feature = "extensions-draft")]
635/// AppEphemeral proposal.
636#[derive(
637    Debug,
638    PartialEq,
639    Clone,
640    Serialize,
641    Deserialize,
642    TlsDeserialize,
643    TlsDeserializeBytes,
644    TlsSerialize,
645    TlsSize,
646)]
647pub struct AppEphemeralProposal {
648    /// The unique [`ComponentId`] associated with the proposal.
649    component_id: ComponentId,
650    /// Application data.
651    data: VLBytes,
652}
653
654#[cfg(feature = "extensions-draft")]
655impl AppEphemeralProposal {
656    /// Create a new [`AppEphemeralProposal`].
657    pub fn new(component_id: ComponentId, data: Vec<u8>) -> Self {
658        Self {
659            component_id,
660            data: data.into(),
661        }
662    }
663    /// Returns the `component_id` contained in the proposal.
664    pub fn component_id(&self) -> ComponentId {
665        self.component_id
666    }
667
668    /// Returns the `data` contained in the proposal.
669    pub fn data(&self) -> &[u8] {
670        self.data.as_slice()
671    }
672}
673
674/// GroupContextExtensions Proposal.
675///
676/// A GroupContextExtensions proposal is used to update the list of extensions
677/// in the GroupContext for the group.
678///
679/// ```c
680/// // draft-ietf-mls-protocol-17
681/// struct {
682///   Extension extensions<V>;
683/// } GroupContextExtensions;
684/// ```
685#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
686pub struct GroupContextExtensionProposal {
687    extensions: Extensions<GroupContext>,
688}
689
690impl Size for GroupContextExtensionProposal {
691    fn tls_serialized_len(&self) -> usize {
692        self.extensions.tls_serialized_len()
693    }
694}
695
696impl TlsSerializeTrait for GroupContextExtensionProposal {
697    fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
698        self.extensions.tls_serialize(writer)
699    }
700}
701
702impl GroupContextExtensionProposal {
703    /// Create a new [`GroupContextExtensionProposal`].
704    pub(crate) fn new(extensions: Extensions<GroupContext>) -> Self {
705        Self { extensions }
706    }
707
708    /// Get the extensions of the proposal
709    pub fn extensions(&self) -> &Extensions<GroupContext> {
710        &self.extensions
711    }
712
713    /// Consumes the proposal and returns the contained extensions.
714    pub fn into_extensions(self) -> Extensions<GroupContext> {
715        self.extensions
716    }
717}
718
719// Crate-only types
720
721/// 11.2 Commit
722///
723/// enum {
724///   reserved(0),
725///   proposal(1)
726///   reference(2),
727///   (255)
728/// } ProposalOrRefType;
729///
730/// struct {
731///   ProposalOrRefType type;
732///   select (ProposalOrRef.type) {
733///     case proposal:  Proposal proposal;
734///     case reference: opaque hash<0..255>;
735///   }
736/// } ProposalOrRef;
737///
738/// Type of Proposal, either by value or by reference
739/// We only implement the values (1, 2), other values are not valid
740/// and will yield `ProposalOrRefTypeError::UnknownValue` when decoded.
741#[derive(
742    PartialEq,
743    Clone,
744    Copy,
745    Debug,
746    TlsSerialize,
747    TlsDeserialize,
748    TlsDeserializeBytes,
749    TlsSize,
750    Serialize,
751    Deserialize,
752)]
753#[repr(u8)]
754pub enum ProposalOrRefType {
755    /// Proposal by value.
756    Proposal = 1,
757    /// Proposal by reference
758    Reference = 2,
759}
760
761/// Type of Proposal, either by value or by reference.
762#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
763#[repr(u8)]
764#[allow(missing_docs)]
765pub enum ProposalOrRef {
766    #[tls_codec(discriminant = 1)]
767    Proposal(Box<Proposal>),
768    Reference(Box<ProposalRef>),
769}
770
771impl ProposalOrRef {
772    /// Create a proposal by value.
773    pub(crate) fn proposal(p: Proposal) -> Self {
774        Self::Proposal(Box::new(p))
775    }
776
777    /// Create a proposal by reference.
778    pub(crate) fn reference(p: ProposalRef) -> Self {
779        Self::Reference(Box::new(p))
780    }
781
782    pub(crate) fn as_proposal(&self) -> Option<&Proposal> {
783        if let Self::Proposal(v) = self {
784            Some(v)
785        } else {
786            None
787        }
788    }
789
790    pub(crate) fn as_reference(&self) -> Option<&ProposalRef> {
791        if let Self::Reference(v) = self {
792            Some(v)
793        } else {
794            None
795        }
796    }
797}
798
799impl From<Proposal> for ProposalOrRef {
800    fn from(value: Proposal) -> Self {
801        Self::proposal(value)
802    }
803}
804
805impl From<ProposalRef> for ProposalOrRef {
806    fn from(value: ProposalRef) -> Self {
807        Self::reference(value)
808    }
809}
810
811#[derive(Error, Debug)]
812pub(crate) enum ProposalRefError {
813    #[error("Expected `Proposal`, got `{wrong:?}`.")]
814    AuthenticatedContentHasWrongType { wrong: ContentType },
815    #[error(transparent)]
816    Other(#[from] LibraryError),
817}
818
819impl ProposalRef {
820    pub(crate) fn from_authenticated_content_by_ref(
821        crypto: &impl OpenMlsCrypto,
822        ciphersuite: Ciphersuite,
823        authenticated_content: &AuthenticatedContent,
824    ) -> Result<Self, ProposalRefError> {
825        if !matches!(
826            authenticated_content.content(),
827            FramedContentBody::Proposal(_)
828        ) {
829            return Err(ProposalRefError::AuthenticatedContentHasWrongType {
830                wrong: authenticated_content.content().content_type(),
831            });
832        };
833
834        let encoded = authenticated_content
835            .tls_serialize_detached()
836            .map_err(|error| ProposalRefError::Other(LibraryError::missing_bound_check(error)))?;
837
838        make_proposal_ref(&encoded, ciphersuite, crypto)
839            .map_err(|error| ProposalRefError::Other(LibraryError::unexpected_crypto_error(error)))
840    }
841
842    /// Note: A [`ProposalRef`] should be calculated by using TLS-serialized
843    /// [`AuthenticatedContent`]       as value input and not the
844    /// TLS-serialized proposal. However, to spare us a major refactoring,
845    ///       we calculate it from the raw value in some places that do not
846    /// interact with the outside world.
847    pub(crate) fn from_raw_proposal(
848        ciphersuite: Ciphersuite,
849        crypto: &impl OpenMlsCrypto,
850        proposal: &Proposal,
851    ) -> Result<Self, LibraryError> {
852        // This is used for hash domain separation.
853        let mut data = b"Internal OpenMLS ProposalRef Label".to_vec();
854
855        let mut encoded = proposal
856            .tls_serialize_detached()
857            .map_err(LibraryError::missing_bound_check)?;
858
859        data.append(&mut encoded);
860
861        make_proposal_ref(&data, ciphersuite, crypto).map_err(LibraryError::unexpected_crypto_error)
862    }
863}
864
865/// ```text
866/// struct {
867///     KeyPackageRef sender;
868///     uint32 first_generation;
869///     uint32 last_generation;
870/// } MessageRange;
871/// ```
872#[derive(
873    Debug,
874    PartialEq,
875    Clone,
876    Serialize,
877    Deserialize,
878    TlsDeserialize,
879    TlsDeserializeBytes,
880    TlsSerialize,
881    TlsSize,
882)]
883pub(crate) struct MessageRange {
884    sender: KeyPackageRef,
885    first_generation: u32,
886    last_generation: u32,
887}
888
889#[cfg(feature = "extensions-draft")]
890mod app_data_update;
891#[cfg(feature = "extensions-draft")]
892pub use app_data_update::*;
893
894/// A custom proposal with semantics to be implemented by the application.
895#[derive(
896    Debug,
897    PartialEq,
898    Clone,
899    Serialize,
900    Deserialize,
901    TlsSize,
902    TlsSerialize,
903    TlsDeserialize,
904    TlsDeserializeBytes,
905)]
906pub struct CustomProposal {
907    proposal_type: u16,
908    payload: Vec<u8>,
909}
910
911impl CustomProposal {
912    /// Generate a new custom proposal.
913    pub fn new(proposal_type: u16, payload: Vec<u8>) -> Self {
914        Self {
915            proposal_type,
916            payload,
917        }
918    }
919
920    /// Returns the proposal type of this [`CustomProposal`].
921    pub fn proposal_type(&self) -> u16 {
922        self.proposal_type
923    }
924
925    /// Returns the payload of this [`CustomProposal`].
926    pub fn payload(&self) -> &[u8] {
927        &self.payload
928    }
929}
930
931#[cfg(test)]
932mod tests {
933    use tls_codec::{Deserialize, Serialize};
934
935    use super::ProposalType;
936
937    #[test]
938    fn that_unknown_proposal_types_are_de_serialized_correctly() {
939        // Use non-GREASE unknown values for testing (GREASE values have pattern 0x_A_A)
940        let proposal_types = [0x0000u16, 0x0B0B, 0x7C7C, 0xF000, 0xFFFF];
941
942        for proposal_type in proposal_types.into_iter() {
943            // Construct an unknown proposal type.
944            let test = proposal_type.to_be_bytes().to_vec();
945
946            // Test deserialization.
947            let got = ProposalType::tls_deserialize_exact(&test).unwrap();
948
949            match got {
950                ProposalType::Custom(got_proposal_type) => {
951                    assert_eq!(proposal_type, got_proposal_type);
952                }
953                other => panic!("Expected `ProposalType::Unknown`, got `{other:?}`."),
954            }
955
956            // Test serialization.
957            let got_serialized = got.tls_serialize_detached().unwrap();
958            assert_eq!(test, got_serialized);
959        }
960    }
961}