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/// ## MLS Proposal Types
31///
32///
33/// ```c
34/// // draft-ietf-mls-protocol-20
35/// // See IANA registry for registered values
36/// uint16 ProposalType;
37/// ```
38///
39/// | Value           | Name                     | R | Ext | Path | Ref      |
40/// |-----------------|--------------------------|---|-----|------|----------|
41/// | 0x0000          | RESERVED                 | - | -   | -    | RFC XXXX |
42/// | 0x0001          | add                      | Y | Y   | N    | RFC XXXX |
43/// | 0x0002          | update                   | Y | N   | Y    | RFC XXXX |
44/// | 0x0003          | remove                   | Y | Y   | Y    | RFC XXXX |
45/// | 0x0004          | psk                      | Y | Y   | N    | RFC XXXX |
46/// | 0x0005          | reinit                   | Y | Y   | N    | RFC XXXX |
47/// | 0x0006          | external_init            | Y | N   | Y    | RFC XXXX |
48/// | 0x0007          | group_context_extensions | Y | Y   | Y    | RFC XXXX |
49/// | 0x0A0A          | GREASE                   | Y | -   | -    | RFC XXXX |
50/// | 0x1A1A          | GREASE                   | Y | -   | -    | RFC XXXX |
51/// | 0x2A2A          | GREASE                   | Y | -   | -    | RFC XXXX |
52/// | 0x3A3A          | GREASE                   | Y | -   | -    | RFC XXXX |
53/// | 0x4A4A          | GREASE                   | Y | -   | -    | RFC XXXX |
54/// | 0x5A5A          | GREASE                   | Y | -   | -    | RFC XXXX |
55/// | 0x6A6A          | GREASE                   | Y | -   | -    | RFC XXXX |
56/// | 0x7A7A          | GREASE                   | Y | -   | -    | RFC XXXX |
57/// | 0x8A8A          | GREASE                   | Y | -   | -    | RFC XXXX |
58/// | 0x9A9A          | GREASE                   | Y | -   | -    | RFC XXXX |
59/// | 0xAAAA          | GREASE                   | Y | -   | -    | RFC XXXX |
60/// | 0xBABA          | GREASE                   | Y | -   | -    | RFC XXXX |
61/// | 0xCACA          | GREASE                   | Y | -   | -    | RFC XXXX |
62/// | 0xDADA          | GREASE                   | Y | -   | -    | RFC XXXX |
63/// | 0xEAEA          | GREASE                   | Y | -   | -    | RFC XXXX |
64/// | 0xF000 - 0xFFFF | Reserved for Private Use | - | -   | -    | RFC XXXX |
65///
66/// # Extensions
67///
68/// | Value  | Name    | Recommended | Path Required | Reference | Notes                        |
69/// |:=======|:========|:============|:==============|:==========|:=============================|
70/// | 0x0008 | app_ack | Y           | Y             | RFC XXXX  | draft-ietf-mls-extensions-00 |
71#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Serialize, Deserialize, Hash)]
72#[allow(missing_docs)]
73pub enum ProposalType {
74    Add,
75    Update,
76    Remove,
77    PreSharedKey,
78    Reinit,
79    ExternalInit,
80    GroupContextExtensions,
81    AppAck,
82    SelfRemove,
83    Custom(u16),
84}
85
86impl ProposalType {
87    /// Returns true for all proposal types that are considered "default" by the
88    /// spec.
89    pub(crate) fn is_default(self) -> bool {
90        match self {
91            ProposalType::Add
92            | ProposalType::Update
93            | ProposalType::Remove
94            | ProposalType::PreSharedKey
95            | ProposalType::Reinit
96            | ProposalType::ExternalInit
97            | ProposalType::GroupContextExtensions => true,
98            ProposalType::SelfRemove | ProposalType::AppAck | ProposalType::Custom(_) => false,
99        }
100    }
101}
102
103impl Size for ProposalType {
104    fn tls_serialized_len(&self) -> usize {
105        2
106    }
107}
108
109impl TlsDeserializeTrait for ProposalType {
110    fn tls_deserialize<R: Read>(bytes: &mut R) -> Result<Self, Error>
111    where
112        Self: Sized,
113    {
114        let mut proposal_type = [0u8; 2];
115        bytes.read_exact(&mut proposal_type)?;
116
117        Ok(ProposalType::from(u16::from_be_bytes(proposal_type)))
118    }
119}
120
121impl TlsSerializeTrait for ProposalType {
122    fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
123        writer.write_all(&u16::from(*self).to_be_bytes())?;
124
125        Ok(2)
126    }
127}
128
129impl DeserializeBytes for ProposalType {
130    fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error>
131    where
132        Self: Sized,
133    {
134        let mut bytes_ref = bytes;
135        let proposal_type = ProposalType::tls_deserialize(&mut bytes_ref)?;
136        let remainder = &bytes[proposal_type.tls_serialized_len()..];
137        Ok((proposal_type, remainder))
138    }
139}
140
141impl ProposalType {
142    /// Returns `true` if the proposal type requires a path and `false`
143    pub fn is_path_required(&self) -> bool {
144        matches!(
145            self,
146            Self::Update
147                | Self::Remove
148                | Self::ExternalInit
149                | Self::GroupContextExtensions
150                | Self::SelfRemove
151        )
152    }
153}
154
155impl From<u16> for ProposalType {
156    fn from(value: u16) -> Self {
157        match value {
158            1 => ProposalType::Add,
159            2 => ProposalType::Update,
160            3 => ProposalType::Remove,
161            4 => ProposalType::PreSharedKey,
162            5 => ProposalType::Reinit,
163            6 => ProposalType::ExternalInit,
164            7 => ProposalType::GroupContextExtensions,
165            8 => ProposalType::AppAck,
166            0x000c => ProposalType::SelfRemove,
167            other => ProposalType::Custom(other),
168        }
169    }
170}
171
172impl From<ProposalType> for u16 {
173    fn from(value: ProposalType) -> Self {
174        match value {
175            ProposalType::Add => 1,
176            ProposalType::Update => 2,
177            ProposalType::Remove => 3,
178            ProposalType::PreSharedKey => 4,
179            ProposalType::Reinit => 5,
180            ProposalType::ExternalInit => 6,
181            ProposalType::GroupContextExtensions => 7,
182            ProposalType::AppAck => 8,
183            ProposalType::SelfRemove => 0x000c,
184            ProposalType::Custom(id) => id,
185        }
186    }
187}
188
189/// Proposal.
190///
191/// This `enum` contains the different proposals in its variants.
192///
193/// ```c
194/// // draft-ietf-mls-protocol-17
195/// struct {
196///     ProposalType msg_type;
197///     select (Proposal.msg_type) {
198///         case add:                      Add;
199///         case update:                   Update;
200///         case remove:                   Remove;
201///         case psk:                      PreSharedKey;
202///         case reinit:                   ReInit;
203///         case external_init:            ExternalInit;
204///         case group_context_extensions: GroupContextExtensions;
205///     };
206/// } Proposal;
207/// ```
208#[allow(clippy::large_enum_variant)]
209#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
210#[allow(missing_docs)]
211#[repr(u16)]
212pub enum Proposal {
213    Add(AddProposal),
214    Update(UpdateProposal),
215    Remove(RemoveProposal),
216    PreSharedKey(PreSharedKeyProposal),
217    ReInit(ReInitProposal),
218    ExternalInit(ExternalInitProposal),
219    GroupContextExtensions(GroupContextExtensionProposal),
220    // # Extensions
221    // TODO(#916): `AppAck` is not in draft-ietf-mls-protocol-17 but
222    //             was moved to `draft-ietf-mls-extensions-00`.
223    AppAck(AppAckProposal),
224    // A SelfRemove proposal is an empty struct.
225    SelfRemove,
226    Custom(CustomProposal),
227}
228
229impl Proposal {
230    /// Returns the proposal type.
231    pub fn proposal_type(&self) -> ProposalType {
232        match self {
233            Proposal::Add(_) => ProposalType::Add,
234            Proposal::Update(_) => ProposalType::Update,
235            Proposal::Remove(_) => ProposalType::Remove,
236            Proposal::PreSharedKey(_) => ProposalType::PreSharedKey,
237            Proposal::ReInit(_) => ProposalType::Reinit,
238            Proposal::ExternalInit(_) => ProposalType::ExternalInit,
239            Proposal::GroupContextExtensions(_) => ProposalType::GroupContextExtensions,
240            Proposal::AppAck(_) => ProposalType::AppAck,
241            Proposal::SelfRemove => ProposalType::SelfRemove,
242            Proposal::Custom(CustomProposal {
243                proposal_type,
244                payload: _,
245            }) => ProposalType::Custom(proposal_type.to_owned()),
246        }
247    }
248
249    pub(crate) fn is_type(&self, proposal_type: ProposalType) -> bool {
250        self.proposal_type() == proposal_type
251    }
252
253    /// Indicates whether a Commit containing this [Proposal] requires a path.
254    pub fn is_path_required(&self) -> bool {
255        self.proposal_type().is_path_required()
256    }
257
258    pub(crate) fn has_lower_priority_than(&self, new_proposal: &Proposal) -> bool {
259        match (self, new_proposal) {
260            // Updates have the lowest priority.
261            (Proposal::Update(_), _) => true,
262            // Removes have a higher priority than Updates.
263            (Proposal::Remove(_), Proposal::Update(_)) => false,
264            // Later Removes trump earlier Removes
265            (Proposal::Remove(_), Proposal::Remove(_)) => true,
266            // SelfRemoves have the highest priority.
267            (_, Proposal::SelfRemove) => true,
268            // All other combinations are invalid
269            _ => {
270                debug_assert!(false);
271                false
272            }
273        }
274    }
275}
276
277/// Add Proposal.
278///
279/// An Add proposal requests that a client with a specified [`KeyPackage`] be
280/// added to the group.
281///
282/// ```c
283/// // draft-ietf-mls-protocol-17
284/// struct {
285///     KeyPackage key_package;
286/// } Add;
287/// ```
288#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
289pub struct AddProposal {
290    pub(crate) key_package: KeyPackage,
291}
292
293impl AddProposal {
294    /// Returns a reference to the key package in the proposal.
295    pub fn key_package(&self) -> &KeyPackage {
296        &self.key_package
297    }
298}
299
300/// Update Proposal.
301///
302/// An Update proposal is a similar mechanism to [`AddProposal`] with the
303/// distinction that it replaces the sender's [`LeafNode`] in the tree instead
304/// of adding a new leaf to the tree.
305///
306/// ```c
307/// // draft-ietf-mls-protocol-17
308/// struct {
309///     LeafNode leaf_node;
310/// } Update;
311/// ```
312#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
313pub struct UpdateProposal {
314    pub(crate) leaf_node: LeafNode,
315}
316
317impl UpdateProposal {
318    /// Returns a reference to the leaf node in the proposal.
319    pub fn leaf_node(&self) -> &LeafNode {
320        &self.leaf_node
321    }
322}
323
324/// Remove Proposal.
325///
326/// A Remove proposal requests that the member with the leaf index removed be
327/// removed from the group.
328///
329/// ```c
330/// // draft-ietf-mls-protocol-17
331/// struct {
332///     uint32 removed;
333/// } Remove;
334/// ```
335#[derive(
336    Debug,
337    PartialEq,
338    Eq,
339    Clone,
340    Serialize,
341    Deserialize,
342    TlsDeserialize,
343    TlsDeserializeBytes,
344    TlsSerialize,
345    TlsSize,
346)]
347pub struct RemoveProposal {
348    pub(crate) removed: LeafNodeIndex,
349}
350
351impl RemoveProposal {
352    /// Returns the leaf index of the removed leaf in this proposal.
353    pub fn removed(&self) -> LeafNodeIndex {
354        self.removed
355    }
356}
357
358/// PreSharedKey Proposal.
359///
360/// A PreSharedKey proposal can be used to request that a pre-shared key be
361/// injected into the key schedule in the process of advancing the epoch.
362///
363/// ```c
364/// // draft-ietf-mls-protocol-17
365/// struct {
366///     PreSharedKeyID psk;
367/// } PreSharedKey;
368/// ```
369#[derive(
370    Debug,
371    PartialEq,
372    Eq,
373    Clone,
374    Serialize,
375    Deserialize,
376    TlsDeserialize,
377    TlsDeserializeBytes,
378    TlsSerialize,
379    TlsSize,
380)]
381pub struct PreSharedKeyProposal {
382    psk: PreSharedKeyId,
383}
384
385impl PreSharedKeyProposal {
386    /// Returns the [`PreSharedKeyId`] and consume this proposal.
387    pub(crate) fn into_psk_id(self) -> PreSharedKeyId {
388        self.psk
389    }
390}
391
392impl PreSharedKeyProposal {
393    /// Create a new PSK proposal
394    pub(crate) fn new(psk: PreSharedKeyId) -> Self {
395        Self { psk }
396    }
397}
398
399/// ReInit Proposal.
400///
401/// A ReInit proposal represents a request to reinitialize the group with
402/// different parameters, for example, to increase the version number or to
403/// change the ciphersuite. The reinitialization is done by creating a
404/// completely new group and shutting down the old one.
405///
406/// ```c
407/// // draft-ietf-mls-protocol-17
408/// struct {
409///     opaque group_id<V>;
410///     ProtocolVersion version;
411///     CipherSuite cipher_suite;
412///     Extension extensions<V>;
413/// } ReInit;
414/// ```
415#[derive(
416    Debug,
417    PartialEq,
418    Eq,
419    Clone,
420    Serialize,
421    Deserialize,
422    TlsDeserialize,
423    TlsDeserializeBytes,
424    TlsSerialize,
425    TlsSize,
426)]
427pub struct ReInitProposal {
428    pub(crate) group_id: GroupId,
429    pub(crate) version: ProtocolVersion,
430    pub(crate) ciphersuite: Ciphersuite,
431    pub(crate) extensions: Extensions,
432}
433
434/// ExternalInit Proposal.
435///
436/// An ExternalInit proposal is used by new members that want to join a group by
437/// using an external commit. This proposal can only be used in that context.
438///
439/// ```c
440/// // draft-ietf-mls-protocol-17
441/// struct {
442///   opaque kem_output<V>;
443/// } ExternalInit;
444/// ```
445#[derive(
446    Debug,
447    PartialEq,
448    Eq,
449    Clone,
450    Serialize,
451    Deserialize,
452    TlsDeserialize,
453    TlsDeserializeBytes,
454    TlsSerialize,
455    TlsSize,
456)]
457pub struct ExternalInitProposal {
458    kem_output: VLBytes,
459}
460
461impl ExternalInitProposal {
462    /// Returns the `kem_output` contained in the proposal.
463    pub(crate) fn kem_output(&self) -> &[u8] {
464        self.kem_output.as_slice()
465    }
466}
467
468impl From<Vec<u8>> for ExternalInitProposal {
469    fn from(kem_output: Vec<u8>) -> Self {
470        ExternalInitProposal {
471            kem_output: kem_output.into(),
472        }
473    }
474}
475
476/// AppAck Proposal.
477///
478/// This is not yet supported.
479#[derive(
480    Debug,
481    PartialEq,
482    Clone,
483    Serialize,
484    Deserialize,
485    TlsDeserialize,
486    TlsDeserializeBytes,
487    TlsSerialize,
488    TlsSize,
489)]
490pub struct AppAckProposal {
491    received_ranges: Vec<MessageRange>,
492}
493
494/// GroupContextExtensions Proposal.
495///
496/// A GroupContextExtensions proposal is used to update the list of extensions
497/// in the GroupContext for the group.
498///
499/// ```c
500/// // draft-ietf-mls-protocol-17
501/// struct {
502///   Extension extensions<V>;
503/// } GroupContextExtensions;
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 GroupContextExtensionProposal {
518    extensions: Extensions,
519}
520
521impl GroupContextExtensionProposal {
522    /// Create a new [`GroupContextExtensionProposal`].
523    pub(crate) fn new(extensions: Extensions) -> Self {
524        Self { extensions }
525    }
526
527    /// Get the extensions of the proposal
528    pub fn extensions(&self) -> &Extensions {
529        &self.extensions
530    }
531}
532
533// Crate-only types
534
535/// 11.2 Commit
536///
537/// enum {
538///   reserved(0),
539///   proposal(1)
540///   reference(2),
541///   (255)
542/// } ProposalOrRefType;
543///
544/// struct {
545///   ProposalOrRefType type;
546///   select (ProposalOrRef.type) {
547///     case proposal:  Proposal proposal;
548///     case reference: opaque hash<0..255>;
549///   }
550/// } ProposalOrRef;
551///
552/// Type of Proposal, either by value or by reference
553/// We only implement the values (1, 2), other values are not valid
554/// and will yield `ProposalOrRefTypeError::UnknownValue` when decoded.
555#[derive(
556    PartialEq,
557    Clone,
558    Copy,
559    Debug,
560    TlsSerialize,
561    TlsDeserialize,
562    TlsDeserializeBytes,
563    TlsSize,
564    Serialize,
565    Deserialize,
566)]
567#[repr(u8)]
568pub enum ProposalOrRefType {
569    /// Proposal by value.
570    Proposal = 1,
571    /// Proposal by reference
572    Reference = 2,
573}
574
575/// Type of Proposal, either by value or by reference.
576#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
577#[repr(u8)]
578#[allow(missing_docs)]
579#[allow(clippy::large_enum_variant)]
580pub(crate) enum ProposalOrRef {
581    #[tls_codec(discriminant = 1)]
582    Proposal(Proposal),
583    Reference(ProposalRef),
584}
585
586#[derive(Error, Debug)]
587pub(crate) enum ProposalRefError {
588    #[error("Expected `Proposal`, got `{wrong:?}`.")]
589    AuthenticatedContentHasWrongType { wrong: ContentType },
590    #[error(transparent)]
591    Other(#[from] LibraryError),
592}
593
594impl ProposalRef {
595    pub(crate) fn from_authenticated_content_by_ref(
596        crypto: &impl OpenMlsCrypto,
597        ciphersuite: Ciphersuite,
598        authenticated_content: &AuthenticatedContent,
599    ) -> Result<Self, ProposalRefError> {
600        if !matches!(
601            authenticated_content.content(),
602            FramedContentBody::Proposal(_)
603        ) {
604            return Err(ProposalRefError::AuthenticatedContentHasWrongType {
605                wrong: authenticated_content.content().content_type(),
606            });
607        };
608
609        let encoded = authenticated_content
610            .tls_serialize_detached()
611            .map_err(|error| ProposalRefError::Other(LibraryError::missing_bound_check(error)))?;
612
613        make_proposal_ref(&encoded, ciphersuite, crypto)
614            .map_err(|error| ProposalRefError::Other(LibraryError::unexpected_crypto_error(error)))
615    }
616
617    /// Note: A [`ProposalRef`] should be calculated by using TLS-serialized
618    /// [`AuthenticatedContent`]       as value input and not the
619    /// TLS-serialized proposal. However, to spare us a major refactoring,
620    ///       we calculate it from the raw value in some places that do not
621    /// interact with the outside world.
622    pub(crate) fn from_raw_proposal(
623        ciphersuite: Ciphersuite,
624        crypto: &impl OpenMlsCrypto,
625        proposal: &Proposal,
626    ) -> Result<Self, LibraryError> {
627        // This is used for hash domain separation.
628        let mut data = b"Internal OpenMLS ProposalRef Label".to_vec();
629
630        let mut encoded = proposal
631            .tls_serialize_detached()
632            .map_err(LibraryError::missing_bound_check)?;
633
634        data.append(&mut encoded);
635
636        make_proposal_ref(&data, ciphersuite, crypto).map_err(LibraryError::unexpected_crypto_error)
637    }
638}
639
640/// ```text
641/// struct {
642///     KeyPackageRef sender;
643///     uint32 first_generation;
644///     uint32 last_generation;
645/// } MessageRange;
646/// ```
647#[derive(
648    Debug,
649    PartialEq,
650    Clone,
651    Serialize,
652    Deserialize,
653    TlsDeserialize,
654    TlsDeserializeBytes,
655    TlsSerialize,
656    TlsSize,
657)]
658pub(crate) struct MessageRange {
659    sender: KeyPackageRef,
660    first_generation: u32,
661    last_generation: u32,
662}
663
664/// A custom proposal with semantics to be implemented by the application.
665#[derive(
666    Debug,
667    PartialEq,
668    Clone,
669    Serialize,
670    Deserialize,
671    TlsSize,
672    TlsSerialize,
673    TlsDeserialize,
674    TlsDeserializeBytes,
675)]
676pub struct CustomProposal {
677    proposal_type: u16,
678    payload: Vec<u8>,
679}
680
681impl CustomProposal {
682    /// Generate a new custom proposal.
683    pub fn new(proposal_type: u16, payload: Vec<u8>) -> Self {
684        Self {
685            proposal_type,
686            payload,
687        }
688    }
689
690    /// Returns the proposal type of this [`CustomProposal`].
691    pub fn proposal_type(&self) -> u16 {
692        self.proposal_type
693    }
694
695    /// Returns the payload of this [`CustomProposal`].
696    pub fn payload(&self) -> &[u8] {
697        &self.payload
698    }
699}
700
701#[cfg(test)]
702mod tests {
703    use tls_codec::{Deserialize, Serialize};
704
705    use super::ProposalType;
706
707    #[test]
708    fn that_unknown_proposal_types_are_de_serialized_correctly() {
709        let proposal_types = [0x0000u16, 0x0A0A, 0x7A7A, 0xF000, 0xFFFF];
710
711        for proposal_type in proposal_types.into_iter() {
712            // Construct an unknown proposal type.
713            let test = proposal_type.to_be_bytes().to_vec();
714
715            // Test deserialization.
716            let got = ProposalType::tls_deserialize_exact(&test).unwrap();
717
718            match got {
719                ProposalType::Custom(got_proposal_type) => {
720                    assert_eq!(proposal_type, got_proposal_type);
721                }
722                other => panic!("Expected `ProposalType::Unknown`, got `{:?}`.", other),
723            }
724
725            // Test serialization.
726            let got_serialized = got.tls_serialize_detached().unwrap();
727            assert_eq!(test, got_serialized);
728        }
729    }
730}