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        let remainder = &bytes[proposal_type.tls_serialized_len()..];
180        Ok((proposal_type, remainder))
181    }
182}
183
184impl ProposalType {
185    /// Returns `true` if the proposal type requires a path and `false`
186    pub fn is_path_required(&self) -> bool {
187        matches!(
188            self,
189            Self::Update
190                | Self::Remove
191                | Self::ExternalInit
192                | Self::GroupContextExtensions
193                | Self::SelfRemove
194        )
195    }
196}
197
198impl From<u16> for ProposalType {
199    fn from(value: u16) -> Self {
200        match value {
201            1 => ProposalType::Add,
202            2 => ProposalType::Update,
203            3 => ProposalType::Remove,
204            4 => ProposalType::PreSharedKey,
205            5 => ProposalType::Reinit,
206            6 => ProposalType::ExternalInit,
207            7 => ProposalType::GroupContextExtensions,
208            #[cfg(feature = "extensions-draft")]
209            8 => ProposalType::AppDataUpdate,
210            #[cfg(feature = "extensions-draft")]
211            0x0009 => ProposalType::AppEphemeral,
212            0x000a => ProposalType::SelfRemove,
213            other if crate::grease::is_grease_value(other) => ProposalType::Grease(other),
214            other => ProposalType::Custom(other),
215        }
216    }
217}
218
219impl From<ProposalType> for u16 {
220    fn from(value: ProposalType) -> Self {
221        match value {
222            ProposalType::Add => 1,
223            ProposalType::Update => 2,
224            ProposalType::Remove => 3,
225            ProposalType::PreSharedKey => 4,
226            ProposalType::Reinit => 5,
227            ProposalType::ExternalInit => 6,
228            ProposalType::GroupContextExtensions => 7,
229            #[cfg(feature = "extensions-draft")]
230            ProposalType::AppDataUpdate => 8,
231            #[cfg(feature = "extensions-draft")]
232            ProposalType::AppEphemeral => 0x0009,
233            ProposalType::SelfRemove => 0x000a,
234            ProposalType::Grease(id) => id,
235            ProposalType::Custom(id) => id,
236        }
237    }
238}
239
240/// Proposal.
241///
242/// This `enum` contains the different proposals in its variants.
243///
244/// ```c
245/// // draft-ietf-mls-protocol-17
246/// struct {
247///     ProposalType msg_type;
248///     select (Proposal.msg_type) {
249///         case add:                      Add;
250///         case update:                   Update;
251///         case remove:                   Remove;
252///         case psk:                      PreSharedKey;
253///         case reinit:                   ReInit;
254///         case external_init:            ExternalInit;
255///         case group_context_extensions: GroupContextExtensions;
256///     };
257/// } Proposal;
258/// ```
259#[derive(Debug, PartialEq, Clone)]
260#[cfg_attr(
261    feature = "0-8-1-storage-format",
262    derive(serde::Serialize, serde::Deserialize)
263)]
264#[cfg_attr(
265    not(feature = "0-8-1-storage-format"),
266    derive(
267        openmls_serialization_helpers::Serialize,
268        openmls_serialization_helpers::Deserialize,
269    )
270)]
271#[allow(missing_docs)]
272pub enum Proposal {
273    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 0)]
274    Add(Box<AddProposal>),
275    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 1)]
276    Update(Box<UpdateProposal>),
277    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 2)]
278    Remove(Box<RemoveProposal>),
279    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 3)]
280    PreSharedKey(Box<PreSharedKeyProposal>),
281    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 4)]
282    ReInit(Box<ReInitProposal>),
283    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 5)]
284    ExternalInit(Box<ExternalInitProposal>),
285    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 6)]
286    GroupContextExtensions(Box<GroupContextExtensionProposal>),
287    // AppAck = 7,
288    // # Extensions
289    #[cfg(feature = "extensions-draft")]
290    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 10)]
291    AppDataUpdate(Box<AppDataUpdateProposal>),
292    // A SelfRemove proposal is an empty struct.
293    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 8)]
294    SelfRemove,
295    #[cfg(feature = "extensions-draft")]
296    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 11)]
297    AppEphemeral(Box<AppEphemeralProposal>),
298    #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 9)]
299    Custom(Box<CustomProposal>),
300}
301
302impl Proposal {
303    /// Build a remove proposal.
304    pub(crate) fn remove(r: RemoveProposal) -> Self {
305        Self::Remove(Box::new(r))
306    }
307
308    /// Build an add proposal.
309    pub(crate) fn add(a: AddProposal) -> Self {
310        Self::Add(Box::new(a))
311    }
312
313    /// Build a custom proposal.
314    pub(crate) fn custom(c: CustomProposal) -> Self {
315        Self::Custom(Box::new(c))
316    }
317
318    /// Build a psk proposal.
319    pub(crate) fn psk(p: PreSharedKeyProposal) -> Self {
320        Self::PreSharedKey(Box::new(p))
321    }
322
323    /// Build an update proposal.
324    pub(crate) fn update(p: UpdateProposal) -> Self {
325        Self::Update(Box::new(p))
326    }
327
328    /// Build a GroupContextExtensionProposal proposal.
329    pub(crate) fn group_context_extensions(p: GroupContextExtensionProposal) -> Self {
330        Self::GroupContextExtensions(Box::new(p))
331    }
332
333    /// Build an ExternalInit proposal.
334    pub(crate) fn external_init(p: ExternalInitProposal) -> Self {
335        Self::ExternalInit(Box::new(p))
336    }
337
338    #[cfg(test)]
339    /// Build a ReInit proposal.
340    pub(crate) fn re_init(p: ReInitProposal) -> Self {
341        Self::ReInit(Box::new(p))
342    }
343
344    /// Returns the proposal type.
345    pub fn proposal_type(&self) -> ProposalType {
346        match self {
347            Proposal::Add(_) => ProposalType::Add,
348            Proposal::Update(_) => ProposalType::Update,
349            Proposal::Remove(_) => ProposalType::Remove,
350            Proposal::PreSharedKey(_) => ProposalType::PreSharedKey,
351            Proposal::ReInit(_) => ProposalType::Reinit,
352            Proposal::ExternalInit(_) => ProposalType::ExternalInit,
353            Proposal::GroupContextExtensions(_) => ProposalType::GroupContextExtensions,
354            #[cfg(feature = "extensions-draft")]
355            Proposal::AppDataUpdate(_) => ProposalType::AppDataUpdate,
356            Proposal::SelfRemove => ProposalType::SelfRemove,
357            #[cfg(feature = "extensions-draft")]
358            Proposal::AppEphemeral(_) => ProposalType::AppEphemeral,
359            Proposal::Custom(custom) => ProposalType::Custom(custom.proposal_type.to_owned()),
360        }
361    }
362
363    pub(crate) fn is_type(&self, proposal_type: ProposalType) -> bool {
364        self.proposal_type() == proposal_type
365    }
366
367    /// Indicates whether a Commit containing this [Proposal] requires a path.
368    pub fn is_path_required(&self) -> bool {
369        self.proposal_type().is_path_required()
370    }
371
372    pub(crate) fn has_lower_priority_than(&self, new_proposal: &Proposal) -> bool {
373        match (self, new_proposal) {
374            // Updates have the lowest priority.
375            (Proposal::Update(_), _) => true,
376            // Removes have a higher priority than Updates.
377            (Proposal::Remove(_), Proposal::Update(_)) => false,
378            // Later Removes trump earlier Removes
379            (Proposal::Remove(_), Proposal::Remove(_)) => true,
380            // SelfRemoves have the highest priority.
381            (_, Proposal::SelfRemove) => true,
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    /// Returns the [`PreSharedKeyId`] and consume this proposal.
523    pub(crate) fn into_psk_id(self) -> PreSharedKeyId {
524        self.psk
525    }
526}
527
528impl PreSharedKeyProposal {
529    /// Create a new PSK proposal
530    pub fn new(psk: PreSharedKeyId) -> Self {
531        Self { psk }
532    }
533}
534
535/// ReInit Proposal.
536///
537/// A ReInit proposal represents a request to reinitialize the group with
538/// different parameters, for example, to increase the version number or to
539/// change the ciphersuite. The reinitialization is done by creating a
540/// completely new group and shutting down the old one.
541///
542/// ```c
543/// // draft-ietf-mls-protocol-17
544/// struct {
545///     opaque group_id<V>;
546///     ProtocolVersion version;
547///     CipherSuite cipher_suite;
548///     Extension extensions<V>;
549/// } ReInit;
550/// ```
551#[derive(
552    Debug,
553    PartialEq,
554    Eq,
555    Clone,
556    Serialize,
557    Deserialize,
558    TlsDeserialize,
559    TlsDeserializeBytes,
560    TlsSerialize,
561    TlsSize,
562)]
563pub struct ReInitProposal {
564    pub(crate) group_id: GroupId,
565    pub(crate) version: ProtocolVersion,
566    pub(crate) ciphersuite: Ciphersuite,
567    pub(crate) extensions: Extensions<GroupContext>,
568}
569
570/// ExternalInit Proposal.
571///
572/// An ExternalInit proposal is used by new members that want to join a group by
573/// using an external commit. This proposal can only be used in that context.
574///
575/// ```c
576/// // draft-ietf-mls-protocol-17
577/// struct {
578///   opaque kem_output<V>;
579/// } ExternalInit;
580/// ```
581#[derive(
582    Debug,
583    PartialEq,
584    Eq,
585    Clone,
586    Serialize,
587    Deserialize,
588    TlsDeserialize,
589    TlsDeserializeBytes,
590    TlsSerialize,
591    TlsSize,
592)]
593pub struct ExternalInitProposal {
594    kem_output: VLBytes,
595}
596
597impl ExternalInitProposal {
598    /// Returns the `kem_output` contained in the proposal.
599    pub(crate) fn kem_output(&self) -> &[u8] {
600        self.kem_output.as_slice()
601    }
602}
603
604impl From<Vec<u8>> for ExternalInitProposal {
605    fn from(kem_output: Vec<u8>) -> Self {
606        ExternalInitProposal {
607            kem_output: kem_output.into(),
608        }
609    }
610}
611
612#[cfg(feature = "extensions-draft")]
613/// AppAck object.
614///
615/// This is not yet supported.
616#[derive(
617    Debug,
618    PartialEq,
619    Clone,
620    Serialize,
621    Deserialize,
622    TlsDeserialize,
623    TlsDeserializeBytes,
624    TlsSerialize,
625    TlsSize,
626)]
627pub struct AppAck {
628    received_ranges: Vec<MessageRange>,
629}
630
631#[cfg(feature = "extensions-draft")]
632/// AppEphemeral proposal.
633#[derive(
634    Debug,
635    PartialEq,
636    Clone,
637    Serialize,
638    Deserialize,
639    TlsDeserialize,
640    TlsDeserializeBytes,
641    TlsSerialize,
642    TlsSize,
643)]
644pub struct AppEphemeralProposal {
645    /// The unique [`ComponentId`] associated with the proposal.
646    component_id: ComponentId,
647    /// Application data.
648    data: VLBytes,
649}
650
651#[cfg(feature = "extensions-draft")]
652impl AppEphemeralProposal {
653    /// Create a new [`AppEphemeralProposal`].
654    pub fn new(component_id: ComponentId, data: Vec<u8>) -> Self {
655        Self {
656            component_id,
657            data: data.into(),
658        }
659    }
660    /// Returns the `component_id` contained in the proposal.
661    pub fn component_id(&self) -> ComponentId {
662        self.component_id
663    }
664
665    /// Returns the `data` contained in the proposal.
666    pub fn data(&self) -> &[u8] {
667        self.data.as_slice()
668    }
669}
670
671/// GroupContextExtensions Proposal.
672///
673/// A GroupContextExtensions proposal is used to update the list of extensions
674/// in the GroupContext for the group.
675///
676/// ```c
677/// // draft-ietf-mls-protocol-17
678/// struct {
679///   Extension extensions<V>;
680/// } GroupContextExtensions;
681/// ```
682#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
683pub struct GroupContextExtensionProposal {
684    extensions: Extensions<GroupContext>,
685}
686
687impl Size for GroupContextExtensionProposal {
688    fn tls_serialized_len(&self) -> usize {
689        self.extensions.tls_serialized_len()
690    }
691}
692
693impl TlsSerializeTrait for GroupContextExtensionProposal {
694    fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
695        self.extensions.tls_serialize(writer)
696    }
697}
698
699impl GroupContextExtensionProposal {
700    /// Create a new [`GroupContextExtensionProposal`].
701    pub(crate) fn new(extensions: Extensions<GroupContext>) -> Self {
702        Self { extensions }
703    }
704
705    /// Get the extensions of the proposal
706    pub fn extensions(&self) -> &Extensions<GroupContext> {
707        &self.extensions
708    }
709
710    /// Consumes the proposal and returns the contained extensions.
711    pub fn into_extensions(self) -> Extensions<GroupContext> {
712        self.extensions
713    }
714}
715
716// Crate-only types
717
718/// 11.2 Commit
719///
720/// enum {
721///   reserved(0),
722///   proposal(1)
723///   reference(2),
724///   (255)
725/// } ProposalOrRefType;
726///
727/// struct {
728///   ProposalOrRefType type;
729///   select (ProposalOrRef.type) {
730///     case proposal:  Proposal proposal;
731///     case reference: opaque hash<0..255>;
732///   }
733/// } ProposalOrRef;
734///
735/// Type of Proposal, either by value or by reference
736/// We only implement the values (1, 2), other values are not valid
737/// and will yield `ProposalOrRefTypeError::UnknownValue` when decoded.
738#[derive(
739    PartialEq,
740    Clone,
741    Copy,
742    Debug,
743    TlsSerialize,
744    TlsDeserialize,
745    TlsDeserializeBytes,
746    TlsSize,
747    Serialize,
748    Deserialize,
749)]
750#[repr(u8)]
751pub enum ProposalOrRefType {
752    /// Proposal by value.
753    Proposal = 1,
754    /// Proposal by reference
755    Reference = 2,
756}
757
758/// Type of Proposal, either by value or by reference.
759#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
760#[repr(u8)]
761#[allow(missing_docs)]
762pub enum ProposalOrRef {
763    #[tls_codec(discriminant = 1)]
764    Proposal(Box<Proposal>),
765    Reference(Box<ProposalRef>),
766}
767
768impl ProposalOrRef {
769    /// Create a proposal by value.
770    pub(crate) fn proposal(p: Proposal) -> Self {
771        Self::Proposal(Box::new(p))
772    }
773
774    /// Create a proposal by reference.
775    pub(crate) fn reference(p: ProposalRef) -> Self {
776        Self::Reference(Box::new(p))
777    }
778
779    pub(crate) fn as_proposal(&self) -> Option<&Proposal> {
780        if let Self::Proposal(v) = self {
781            Some(v)
782        } else {
783            None
784        }
785    }
786
787    pub(crate) fn as_reference(&self) -> Option<&ProposalRef> {
788        if let Self::Reference(v) = self {
789            Some(v)
790        } else {
791            None
792        }
793    }
794}
795
796impl From<Proposal> for ProposalOrRef {
797    fn from(value: Proposal) -> Self {
798        Self::proposal(value)
799    }
800}
801
802impl From<ProposalRef> for ProposalOrRef {
803    fn from(value: ProposalRef) -> Self {
804        Self::reference(value)
805    }
806}
807
808#[derive(Error, Debug)]
809pub(crate) enum ProposalRefError {
810    #[error("Expected `Proposal`, got `{wrong:?}`.")]
811    AuthenticatedContentHasWrongType { wrong: ContentType },
812    #[error(transparent)]
813    Other(#[from] LibraryError),
814}
815
816impl ProposalRef {
817    pub(crate) fn from_authenticated_content_by_ref(
818        crypto: &impl OpenMlsCrypto,
819        ciphersuite: Ciphersuite,
820        authenticated_content: &AuthenticatedContent,
821    ) -> Result<Self, ProposalRefError> {
822        if !matches!(
823            authenticated_content.content(),
824            FramedContentBody::Proposal(_)
825        ) {
826            return Err(ProposalRefError::AuthenticatedContentHasWrongType {
827                wrong: authenticated_content.content().content_type(),
828            });
829        };
830
831        let encoded = authenticated_content
832            .tls_serialize_detached()
833            .map_err(|error| ProposalRefError::Other(LibraryError::missing_bound_check(error)))?;
834
835        make_proposal_ref(&encoded, ciphersuite, crypto)
836            .map_err(|error| ProposalRefError::Other(LibraryError::unexpected_crypto_error(error)))
837    }
838
839    /// Note: A [`ProposalRef`] should be calculated by using TLS-serialized
840    /// [`AuthenticatedContent`]       as value input and not the
841    /// TLS-serialized proposal. However, to spare us a major refactoring,
842    ///       we calculate it from the raw value in some places that do not
843    /// interact with the outside world.
844    pub(crate) fn from_raw_proposal(
845        ciphersuite: Ciphersuite,
846        crypto: &impl OpenMlsCrypto,
847        proposal: &Proposal,
848    ) -> Result<Self, LibraryError> {
849        // This is used for hash domain separation.
850        let mut data = b"Internal OpenMLS ProposalRef Label".to_vec();
851
852        let mut encoded = proposal
853            .tls_serialize_detached()
854            .map_err(LibraryError::missing_bound_check)?;
855
856        data.append(&mut encoded);
857
858        make_proposal_ref(&data, ciphersuite, crypto).map_err(LibraryError::unexpected_crypto_error)
859    }
860}
861
862/// ```text
863/// struct {
864///     KeyPackageRef sender;
865///     uint32 first_generation;
866///     uint32 last_generation;
867/// } MessageRange;
868/// ```
869#[derive(
870    Debug,
871    PartialEq,
872    Clone,
873    Serialize,
874    Deserialize,
875    TlsDeserialize,
876    TlsDeserializeBytes,
877    TlsSerialize,
878    TlsSize,
879)]
880pub(crate) struct MessageRange {
881    sender: KeyPackageRef,
882    first_generation: u32,
883    last_generation: u32,
884}
885
886#[cfg(feature = "extensions-draft")]
887mod app_data_update;
888#[cfg(feature = "extensions-draft")]
889pub use app_data_update::*;
890
891/// A custom proposal with semantics to be implemented by the application.
892#[derive(
893    Debug,
894    PartialEq,
895    Clone,
896    Serialize,
897    Deserialize,
898    TlsSize,
899    TlsSerialize,
900    TlsDeserialize,
901    TlsDeserializeBytes,
902)]
903pub struct CustomProposal {
904    proposal_type: u16,
905    payload: Vec<u8>,
906}
907
908impl CustomProposal {
909    /// Generate a new custom proposal.
910    pub fn new(proposal_type: u16, payload: Vec<u8>) -> Self {
911        Self {
912            proposal_type,
913            payload,
914        }
915    }
916
917    /// Returns the proposal type of this [`CustomProposal`].
918    pub fn proposal_type(&self) -> u16 {
919        self.proposal_type
920    }
921
922    /// Returns the payload of this [`CustomProposal`].
923    pub fn payload(&self) -> &[u8] {
924        &self.payload
925    }
926}
927
928#[cfg(test)]
929mod tests {
930    use tls_codec::{Deserialize, Serialize};
931
932    use super::ProposalType;
933
934    #[test]
935    fn that_unknown_proposal_types_are_de_serialized_correctly() {
936        // Use non-GREASE unknown values for testing (GREASE values have pattern 0x_A_A)
937        let proposal_types = [0x0000u16, 0x0B0B, 0x7C7C, 0xF000, 0xFFFF];
938
939        for proposal_type in proposal_types.into_iter() {
940            // Construct an unknown proposal type.
941            let test = proposal_type.to_be_bytes().to_vec();
942
943            // Test deserialization.
944            let got = ProposalType::tls_deserialize_exact(&test).unwrap();
945
946            match got {
947                ProposalType::Custom(got_proposal_type) => {
948                    assert_eq!(proposal_type, got_proposal_type);
949                }
950                other => panic!("Expected `ProposalType::Unknown`, got `{other:?}`."),
951            }
952
953            // Test serialization.
954            let got_serialized = got.tls_serialize_detached().unwrap();
955            assert_eq!(test, got_serialized);
956        }
957    }
958}