1use 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#[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 #[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 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 pub fn is_grease(&self) -> bool {
142 matches!(self, ProposalType::Grease(v) if crate::grease::is_grease_value(*v))
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 let value = u16::from(*self);
167 if ProposalType::from(value) != *self {
170 return Err(Error::EncodingError(format!(
171 "{self:?} is encoded as {value:#06x}, which decodes as a different proposal type"
172 )));
173 }
174 writer.write_all(&value.to_be_bytes())?;
175
176 Ok(2)
177 }
178}
179
180impl DeserializeBytes for ProposalType {
181 fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error>
182 where
183 Self: Sized,
184 {
185 let mut bytes_ref = bytes;
186 let proposal_type = ProposalType::tls_deserialize(&mut bytes_ref)?;
187 Ok((proposal_type, bytes_ref))
188 }
189}
190
191impl ProposalType {
192 pub fn is_path_required(&self) -> bool {
194 matches!(
195 self,
196 Self::Update
197 | Self::Remove
198 | Self::ExternalInit
199 | Self::GroupContextExtensions
200 | Self::SelfRemove
201 )
202 }
203}
204
205impl From<u16> for ProposalType {
206 fn from(value: u16) -> Self {
207 match value {
208 1 => ProposalType::Add,
209 2 => ProposalType::Update,
210 3 => ProposalType::Remove,
211 4 => ProposalType::PreSharedKey,
212 5 => ProposalType::Reinit,
213 6 => ProposalType::ExternalInit,
214 7 => ProposalType::GroupContextExtensions,
215 #[cfg(feature = "extensions-draft")]
216 8 => ProposalType::AppDataUpdate,
217 #[cfg(feature = "extensions-draft")]
218 0x0009 => ProposalType::AppEphemeral,
219 0x000a => ProposalType::SelfRemove,
220 other if crate::grease::is_grease_value(other) => ProposalType::Grease(other),
221 other => ProposalType::Custom(other),
222 }
223 }
224}
225
226impl From<ProposalType> for u16 {
227 fn from(value: ProposalType) -> Self {
228 match value {
229 ProposalType::Add => 1,
230 ProposalType::Update => 2,
231 ProposalType::Remove => 3,
232 ProposalType::PreSharedKey => 4,
233 ProposalType::Reinit => 5,
234 ProposalType::ExternalInit => 6,
235 ProposalType::GroupContextExtensions => 7,
236 #[cfg(feature = "extensions-draft")]
237 ProposalType::AppDataUpdate => 8,
238 #[cfg(feature = "extensions-draft")]
239 ProposalType::AppEphemeral => 0x0009,
240 ProposalType::SelfRemove => 0x000a,
241 ProposalType::Grease(id) => id,
242 ProposalType::Custom(id) => id,
243 }
244 }
245}
246
247#[derive(Debug, PartialEq, Clone)]
267#[cfg_attr(
268 feature = "0-8-1-storage-format",
269 derive(serde::Serialize, serde::Deserialize)
270)]
271#[cfg_attr(
272 not(feature = "0-8-1-storage-format"),
273 derive(
274 openmls_serialization_helpers::Serialize,
275 openmls_serialization_helpers::Deserialize,
276 )
277)]
278#[allow(missing_docs)]
279pub enum Proposal {
280 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 0)]
281 Add(Box<AddProposal>),
282 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 1)]
283 Update(Box<UpdateProposal>),
284 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 2)]
285 Remove(Box<RemoveProposal>),
286 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 3)]
287 PreSharedKey(Box<PreSharedKeyProposal>),
288 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 4)]
289 ReInit(Box<ReInitProposal>),
290 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 5)]
291 ExternalInit(Box<ExternalInitProposal>),
292 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 6)]
293 GroupContextExtensions(Box<GroupContextExtensionProposal>),
294 #[cfg(feature = "extensions-draft")]
297 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 10)]
298 AppDataUpdate(Box<AppDataUpdateProposal>),
299 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 8)]
301 SelfRemove,
302 #[cfg(feature = "extensions-draft")]
303 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 11)]
304 AppEphemeral(Box<AppEphemeralProposal>),
305 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 9)]
306 Custom(Box<CustomProposal>),
307}
308
309impl Proposal {
310 pub(crate) fn remove(r: RemoveProposal) -> Self {
312 Self::Remove(Box::new(r))
313 }
314
315 pub(crate) fn add(a: AddProposal) -> Self {
317 Self::Add(Box::new(a))
318 }
319
320 pub(crate) fn custom(c: CustomProposal) -> Self {
322 Self::Custom(Box::new(c))
323 }
324
325 pub(crate) fn psk(p: PreSharedKeyProposal) -> Self {
327 Self::PreSharedKey(Box::new(p))
328 }
329
330 pub(crate) fn update(p: UpdateProposal) -> Self {
332 Self::Update(Box::new(p))
333 }
334
335 pub(crate) fn group_context_extensions(p: GroupContextExtensionProposal) -> Self {
337 Self::GroupContextExtensions(Box::new(p))
338 }
339
340 pub(crate) fn external_init(p: ExternalInitProposal) -> Self {
342 Self::ExternalInit(Box::new(p))
343 }
344
345 pub fn re_init(p: ReInitProposal) -> Self {
347 Self::ReInit(Box::new(p))
348 }
349
350 pub fn proposal_type(&self) -> ProposalType {
352 match self {
353 Proposal::Add(_) => ProposalType::Add,
354 Proposal::Update(_) => ProposalType::Update,
355 Proposal::Remove(_) => ProposalType::Remove,
356 Proposal::PreSharedKey(_) => ProposalType::PreSharedKey,
357 Proposal::ReInit(_) => ProposalType::Reinit,
358 Proposal::ExternalInit(_) => ProposalType::ExternalInit,
359 Proposal::GroupContextExtensions(_) => ProposalType::GroupContextExtensions,
360 #[cfg(feature = "extensions-draft")]
361 Proposal::AppDataUpdate(_) => ProposalType::AppDataUpdate,
362 Proposal::SelfRemove => ProposalType::SelfRemove,
363 #[cfg(feature = "extensions-draft")]
364 Proposal::AppEphemeral(_) => ProposalType::AppEphemeral,
365 Proposal::Custom(custom) => ProposalType::Custom(custom.proposal_type.to_owned()),
366 }
367 }
368
369 pub(crate) fn is_type(&self, proposal_type: ProposalType) -> bool {
370 self.proposal_type() == proposal_type
371 }
372
373 pub fn is_path_required(&self) -> bool {
375 self.proposal_type().is_path_required()
376 }
377
378 pub(crate) fn has_lower_priority_than(&self, new_proposal: &Proposal) -> bool {
379 match (self, new_proposal) {
380 (Proposal::Update(_), _) => true,
382 (Proposal::Remove(_), Proposal::Update(_)) => false,
384 (Proposal::Remove(_), Proposal::Remove(_)) => true,
386 (_, Proposal::SelfRemove) => true,
388 (Proposal::SelfRemove, Proposal::Update(_) | Proposal::Remove(_)) => false,
389 _ => {
390 debug_assert!(false);
391 false
392 }
393 }
394 }
395
396 pub(crate) fn as_remove(&self) -> Option<&RemoveProposal> {
398 if let Self::Remove(v) = self {
399 Some(v)
400 } else {
401 None
402 }
403 }
404
405 #[must_use]
409 pub fn is_remove(&self) -> bool {
410 matches!(self, Self::Remove(..))
411 }
412}
413
414#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
426pub struct AddProposal {
427 pub(crate) key_package: KeyPackage,
428}
429
430impl AddProposal {
431 pub fn key_package(&self) -> &KeyPackage {
433 &self.key_package
434 }
435}
436
437impl From<KeyPackage> for AddProposal {
438 fn from(key_package: KeyPackage) -> AddProposal {
439 AddProposal { key_package }
440 }
441}
442
443#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
456pub struct UpdateProposal {
457 pub(crate) leaf_node: LeafNode,
458}
459
460impl UpdateProposal {
461 pub fn leaf_node(&self) -> &LeafNode {
463 &self.leaf_node
464 }
465}
466
467#[derive(
479 Debug,
480 PartialEq,
481 Eq,
482 Clone,
483 Serialize,
484 Deserialize,
485 TlsDeserialize,
486 TlsDeserializeBytes,
487 TlsSerialize,
488 TlsSize,
489)]
490pub struct RemoveProposal {
491 pub(crate) removed: LeafNodeIndex,
492}
493
494impl RemoveProposal {
495 pub fn removed(&self) -> LeafNodeIndex {
497 self.removed
498 }
499}
500
501#[derive(
513 Debug,
514 PartialEq,
515 Eq,
516 Clone,
517 Serialize,
518 Deserialize,
519 TlsDeserialize,
520 TlsDeserializeBytes,
521 TlsSerialize,
522 TlsSize,
523)]
524pub struct PreSharedKeyProposal {
525 psk: PreSharedKeyId,
526}
527
528impl PreSharedKeyProposal {
529 pub fn new(psk: PreSharedKeyId) -> Self {
531 Self { psk }
532 }
533
534 pub fn psk(&self) -> &PreSharedKeyId {
536 &self.psk
537 }
538
539 pub(crate) fn into_psk_id(self) -> PreSharedKeyId {
541 self.psk
542 }
543}
544
545#[derive(
562 Debug,
563 PartialEq,
564 Eq,
565 Clone,
566 Serialize,
567 Deserialize,
568 TlsDeserialize,
569 TlsDeserializeBytes,
570 TlsSerialize,
571 TlsSize,
572)]
573pub struct ReInitProposal {
574 pub(crate) group_id: GroupId,
575 pub(crate) version: ProtocolVersion,
576 pub(crate) ciphersuite: Ciphersuite,
577 pub(crate) extensions: Extensions<GroupContext>,
578}
579
580impl ReInitProposal {
581 pub fn new(
584 group_id: GroupId,
585 version: ProtocolVersion,
586 ciphersuite: Ciphersuite,
587 extensions: Extensions<GroupContext>,
588 ) -> Self {
589 Self {
590 group_id,
591 version,
592 ciphersuite,
593 extensions,
594 }
595 }
596
597 pub fn group_id(&self) -> &GroupId {
599 &self.group_id
600 }
601
602 pub fn version(&self) -> ProtocolVersion {
604 self.version
605 }
606
607 pub fn ciphersuite(&self) -> Ciphersuite {
609 self.ciphersuite
610 }
611
612 pub fn extensions(&self) -> &Extensions<GroupContext> {
614 &self.extensions
615 }
616}
617
618#[derive(
630 Debug,
631 PartialEq,
632 Eq,
633 Clone,
634 Serialize,
635 Deserialize,
636 TlsDeserialize,
637 TlsDeserializeBytes,
638 TlsSerialize,
639 TlsSize,
640)]
641pub struct ExternalInitProposal {
642 kem_output: VLBytes,
643}
644
645impl ExternalInitProposal {
646 pub(crate) fn kem_output(&self) -> &[u8] {
648 self.kem_output.as_slice()
649 }
650}
651
652impl From<Vec<u8>> for ExternalInitProposal {
653 fn from(kem_output: Vec<u8>) -> Self {
654 ExternalInitProposal {
655 kem_output: kem_output.into(),
656 }
657 }
658}
659
660#[cfg(feature = "extensions-draft")]
661#[derive(
665 Debug,
666 PartialEq,
667 Clone,
668 Serialize,
669 Deserialize,
670 TlsDeserialize,
671 TlsDeserializeBytes,
672 TlsSerialize,
673 TlsSize,
674)]
675pub struct AppAck {
676 received_ranges: Vec<MessageRange>,
677}
678
679#[cfg(feature = "extensions-draft")]
680#[derive(
682 Debug,
683 PartialEq,
684 Clone,
685 Serialize,
686 Deserialize,
687 TlsDeserialize,
688 TlsDeserializeBytes,
689 TlsSerialize,
690 TlsSize,
691)]
692pub struct AppEphemeralProposal {
693 component_id: ComponentId,
695 data: VLBytes,
697}
698
699#[cfg(feature = "extensions-draft")]
700impl AppEphemeralProposal {
701 pub fn new(component_id: ComponentId, data: Vec<u8>) -> Self {
703 Self {
704 component_id,
705 data: data.into(),
706 }
707 }
708 pub fn component_id(&self) -> ComponentId {
710 self.component_id
711 }
712
713 pub fn data(&self) -> &[u8] {
715 self.data.as_slice()
716 }
717}
718
719#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
731pub struct GroupContextExtensionProposal {
732 extensions: Extensions<GroupContext>,
733}
734
735impl Size for GroupContextExtensionProposal {
736 fn tls_serialized_len(&self) -> usize {
737 self.extensions.tls_serialized_len()
738 }
739}
740
741impl TlsSerializeTrait for GroupContextExtensionProposal {
742 fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
743 self.extensions.tls_serialize(writer)
744 }
745}
746
747impl GroupContextExtensionProposal {
748 pub(crate) fn new(extensions: Extensions<GroupContext>) -> Self {
750 Self { extensions }
751 }
752
753 pub fn extensions(&self) -> &Extensions<GroupContext> {
755 &self.extensions
756 }
757
758 pub fn into_extensions(self) -> Extensions<GroupContext> {
760 self.extensions
761 }
762}
763
764#[derive(
787 PartialEq,
788 Clone,
789 Copy,
790 Debug,
791 TlsSerialize,
792 TlsDeserialize,
793 TlsDeserializeBytes,
794 TlsSize,
795 Serialize,
796 Deserialize,
797)]
798#[repr(u8)]
799pub enum ProposalOrRefType {
800 Proposal = 1,
802 Reference = 2,
804}
805
806#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
808#[repr(u8)]
809#[allow(missing_docs)]
810pub enum ProposalOrRef {
811 #[tls_codec(discriminant = 1)]
812 Proposal(Box<Proposal>),
813 Reference(Box<ProposalRef>),
814}
815
816impl ProposalOrRef {
817 pub(crate) fn proposal(p: Proposal) -> Self {
819 Self::Proposal(Box::new(p))
820 }
821
822 pub(crate) fn reference(p: ProposalRef) -> Self {
824 Self::Reference(Box::new(p))
825 }
826
827 pub(crate) fn as_proposal(&self) -> Option<&Proposal> {
828 if let Self::Proposal(v) = self {
829 Some(v)
830 } else {
831 None
832 }
833 }
834
835 pub(crate) fn as_reference(&self) -> Option<&ProposalRef> {
836 if let Self::Reference(v) = self {
837 Some(v)
838 } else {
839 None
840 }
841 }
842}
843
844impl From<Proposal> for ProposalOrRef {
845 fn from(value: Proposal) -> Self {
846 Self::proposal(value)
847 }
848}
849
850impl From<ProposalRef> for ProposalOrRef {
851 fn from(value: ProposalRef) -> Self {
852 Self::reference(value)
853 }
854}
855
856#[derive(Error, Debug)]
857pub(crate) enum ProposalRefError {
858 #[error("Expected `Proposal`, got `{wrong:?}`.")]
859 AuthenticatedContentHasWrongType { wrong: ContentType },
860 #[error(transparent)]
861 Other(#[from] LibraryError),
862}
863
864impl ProposalRef {
865 pub(crate) fn from_authenticated_content_by_ref(
866 crypto: &impl OpenMlsCrypto,
867 ciphersuite: Ciphersuite,
868 authenticated_content: &AuthenticatedContent,
869 ) -> Result<Self, ProposalRefError> {
870 if !matches!(
871 authenticated_content.content(),
872 FramedContentBody::Proposal(_)
873 ) {
874 return Err(ProposalRefError::AuthenticatedContentHasWrongType {
875 wrong: authenticated_content.content().content_type(),
876 });
877 };
878
879 let encoded = authenticated_content
880 .tls_serialize_detached()
881 .map_err(|error| ProposalRefError::Other(LibraryError::missing_bound_check(error)))?;
882
883 make_proposal_ref(&encoded, ciphersuite, crypto)
884 .map_err(|error| ProposalRefError::Other(LibraryError::unexpected_crypto_error(error)))
885 }
886
887 pub(crate) fn from_raw_proposal(
893 ciphersuite: Ciphersuite,
894 crypto: &impl OpenMlsCrypto,
895 proposal: &Proposal,
896 ) -> Result<Self, LibraryError> {
897 let mut data = b"Internal OpenMLS ProposalRef Label".to_vec();
899
900 let mut encoded = proposal
901 .tls_serialize_detached()
902 .map_err(LibraryError::missing_bound_check)?;
903
904 data.append(&mut encoded);
905
906 make_proposal_ref(&data, ciphersuite, crypto).map_err(LibraryError::unexpected_crypto_error)
907 }
908}
909
910#[derive(
918 Debug,
919 PartialEq,
920 Clone,
921 Serialize,
922 Deserialize,
923 TlsDeserialize,
924 TlsDeserializeBytes,
925 TlsSerialize,
926 TlsSize,
927)]
928pub(crate) struct MessageRange {
929 sender: KeyPackageRef,
930 first_generation: u32,
931 last_generation: u32,
932}
933
934#[cfg(feature = "extensions-draft")]
935mod app_data_update;
936#[cfg(feature = "extensions-draft")]
937pub use app_data_update::*;
938
939#[derive(
941 Debug,
942 PartialEq,
943 Clone,
944 Serialize,
945 Deserialize,
946 TlsSize,
947 TlsSerialize,
948 TlsDeserialize,
949 TlsDeserializeBytes,
950)]
951pub struct CustomProposal {
952 proposal_type: u16,
953 payload: Vec<u8>,
954}
955
956impl CustomProposal {
957 pub fn new(proposal_type: u16, payload: Vec<u8>) -> Self {
964 Self {
965 proposal_type,
966 payload,
967 }
968 }
969
970 pub fn proposal_type(&self) -> u16 {
972 self.proposal_type
973 }
974
975 pub fn payload(&self) -> &[u8] {
977 &self.payload
978 }
979}
980
981#[cfg(test)]
982mod tests {
983 use tls_codec::{Deserialize, Serialize};
984
985 use super::ProposalType;
986
987 #[test]
988 fn that_unknown_proposal_types_are_de_serialized_correctly() {
989 let proposal_types = [0x0000u16, 0x0B0B, 0x7C7C, 0xF000, 0xFFFF];
991
992 for proposal_type in proposal_types.into_iter() {
993 let test = proposal_type.to_be_bytes().to_vec();
995
996 let got = ProposalType::tls_deserialize_exact(&test).unwrap();
998
999 match got {
1000 ProposalType::Custom(got_proposal_type) => {
1001 assert_eq!(proposal_type, got_proposal_type);
1002 }
1003 other => panic!("Expected `ProposalType::Unknown`, got `{other:?}`."),
1004 }
1005
1006 let got_serialized = got.tls_serialize_detached().unwrap();
1008 assert_eq!(test, got_serialized);
1009 }
1010 }
1011
1012 #[test]
1013 fn non_canonical_proposal_types_are_not_serialized() {
1014 for proposal_type in [
1015 ProposalType::Custom(1),
1016 ProposalType::Custom(6),
1017 ProposalType::Custom(0x0A0A),
1018 ProposalType::Grease(1),
1019 ProposalType::Grease(0x1234),
1020 ] {
1021 assert!(
1022 proposal_type.tls_serialize_detached().is_err(),
1023 "{proposal_type:?} must not be serialized"
1024 );
1025 }
1026
1027 let proposal = super::Proposal::custom(super::CustomProposal::new(3, vec![9, 8, 7]));
1028 assert!(proposal.tls_serialize_detached().is_err());
1029 }
1030
1031 #[test]
1032 fn is_grease_checks_the_value() {
1033 assert!(ProposalType::Grease(0x0A0A).is_grease());
1034 assert!(!ProposalType::Grease(0x1234).is_grease());
1035 assert!(!ProposalType::Custom(0x0A0A).is_grease());
1036 }
1037}