1use crate::{
6 ciphersuite::{hash_ref::ProposalRef, signable::Verifiable},
7 credentials::CredentialWithKey,
8 extensions::{AnyObject, Extensions},
9 framing::SenderContext,
10 group::{errors::ValidationError, LeafNodeLifetimePolicy},
11 key_packages::*,
12 prelude::InvalidExtensionError,
13 treesync::node::leaf_node::{LeafNodeIn, TreePosition, VerifiableLeafNode},
14 versions::ProtocolVersion,
15};
16
17use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
18use serde::{Deserialize, Serialize};
19use tls_codec::{TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize};
20
21use super::{
22 proposals::{
23 AddProposal, ExternalInitProposal, GroupContextExtensionProposal, PreSharedKeyProposal,
24 Proposal, ProposalOrRef, ProposalType, ReInitProposal, RemoveProposal, UpdateProposal,
25 },
26 CustomProposal,
27};
28
29#[cfg(feature = "extensions-draft")]
30use super::proposals::{AppDataUpdateProposal, AppEphemeralProposal};
31
32#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
52#[allow(missing_docs)]
53#[repr(u16)]
54pub enum ProposalIn {
55 Add(Box<AddProposalIn>),
56 Update(Box<UpdateProposalIn>),
57 Remove(Box<RemoveProposal>),
58 PreSharedKey(Box<PreSharedKeyProposal>),
59 ReInit(Box<ReInitProposal>),
60 ExternalInit(Box<ExternalInitProposal>),
61 GroupContextExtensions(Box<GroupContextExtensionProposalIn>),
62 #[cfg(feature = "extensions-draft")]
64 AppDataUpdate(Box<AppDataUpdateProposal>),
65
66 SelfRemove,
68 #[cfg(feature = "extensions-draft")]
69 AppEphemeral(Box<AppEphemeralProposal>),
70 Custom(Box<CustomProposal>),
71}
72
73impl ProposalIn {
74 pub fn proposal_type(&self) -> ProposalType {
76 match self {
77 ProposalIn::Add(_) => ProposalType::Add,
78 ProposalIn::Update(_) => ProposalType::Update,
79 ProposalIn::Remove(_) => ProposalType::Remove,
80 ProposalIn::PreSharedKey(_) => ProposalType::PreSharedKey,
81 ProposalIn::ReInit(_) => ProposalType::Reinit,
82 ProposalIn::ExternalInit(_) => ProposalType::ExternalInit,
83 ProposalIn::GroupContextExtensions(_) => ProposalType::GroupContextExtensions,
84 #[cfg(feature = "extensions-draft")]
85 ProposalIn::AppDataUpdate(_) => ProposalType::AppDataUpdate,
86 ProposalIn::SelfRemove => ProposalType::SelfRemove,
87 #[cfg(feature = "extensions-draft")]
88 ProposalIn::AppEphemeral(_) => ProposalType::AppEphemeral,
89 ProposalIn::Custom(custom_proposal) => {
90 ProposalType::Custom(custom_proposal.proposal_type())
91 }
92 }
93 }
94
95 pub fn is_path_required(&self) -> bool {
97 self.proposal_type().is_path_required()
98 }
99
100 pub(crate) fn validate(
102 self,
103 crypto: &impl OpenMlsCrypto,
104 ciphersuite: Ciphersuite,
105 sender_context: Option<SenderContext>,
106 protocol_version: ProtocolVersion,
107 lifetime_policy: LeafNodeLifetimePolicy,
108 ) -> Result<Proposal, ValidationError> {
109 Ok(match self {
110 ProposalIn::Add(add) => Proposal::Add(Box::new(add.validate(
111 crypto,
112 protocol_version,
113 ciphersuite,
114 lifetime_policy,
115 )?)),
116 ProposalIn::Update(update) => {
117 let sender_context =
118 sender_context.ok_or(ValidationError::CommitterIncludedOwnUpdate)?;
119 Proposal::Update(Box::new(update.validate(
120 crypto,
121 ciphersuite,
122 sender_context,
123 )?))
124 }
125 ProposalIn::Remove(remove) => Proposal::Remove(remove),
126 ProposalIn::PreSharedKey(psk) => Proposal::PreSharedKey(psk),
127 ProposalIn::ReInit(reinit) => Proposal::ReInit(reinit),
128 ProposalIn::ExternalInit(external_init) => Proposal::ExternalInit(external_init),
129 ProposalIn::GroupContextExtensions(group_context_extension) => {
130 Proposal::group_context_extensions(group_context_extension.validate()?)
131 }
132 #[cfg(feature = "extensions-draft")]
133 ProposalIn::AppDataUpdate(app_data_update) => Proposal::AppDataUpdate(app_data_update),
134 ProposalIn::SelfRemove => Proposal::SelfRemove,
135 #[cfg(feature = "extensions-draft")]
136 ProposalIn::AppEphemeral(app_ephemeral) => Proposal::AppEphemeral(app_ephemeral),
137 ProposalIn::Custom(custom) => Proposal::Custom(custom),
138 })
139 }
140}
141
142#[derive(
153 Debug,
154 PartialEq,
155 Clone,
156 Serialize,
157 Deserialize,
158 TlsSerialize,
159 TlsDeserialize,
160 TlsDeserializeBytes,
161 TlsSize,
162)]
163pub struct AddProposalIn {
164 key_package: KeyPackageIn,
165}
166
167impl AddProposalIn {
168 pub(crate) fn unverified_credential(&self) -> CredentialWithKey {
169 self.key_package.unverified_credential()
170 }
171
172 pub(crate) fn validate(
174 self,
175 crypto: &impl OpenMlsCrypto,
176 protocol_version: ProtocolVersion,
177 ciphersuite: Ciphersuite,
178 lifetime_policy: LeafNodeLifetimePolicy,
179 ) -> Result<AddProposal, ValidationError> {
180 if self.key_package.unverified_ciphersuite() != ciphersuite {
181 return Err(ValidationError::InvalidAddProposalCiphersuite);
182 }
183 let key_package = self.key_package.validate_with_lifetime_policy(
184 crypto,
185 protocol_version,
186 lifetime_policy,
187 )?;
188 Ok(AddProposal { key_package })
189 }
190}
191
192#[derive(
204 Debug,
205 PartialEq,
206 Eq,
207 Clone,
208 Serialize,
209 Deserialize,
210 TlsDeserialize,
211 TlsDeserializeBytes,
212 TlsSerialize,
213 TlsSize,
214)]
215pub struct UpdateProposalIn {
216 leaf_node: LeafNodeIn,
217}
218
219impl UpdateProposalIn {
220 pub(crate) fn validate(
222 self,
223 crypto: &impl OpenMlsCrypto,
224 ciphersuite: Ciphersuite,
225 sender_context: SenderContext,
226 ) -> Result<UpdateProposal, ValidationError> {
227 let leaf_node = match self.leaf_node.into_verifiable_leaf_node() {
228 VerifiableLeafNode::Update(mut leaf_node) => {
229 let tree_position = match sender_context {
230 SenderContext::Member((group_id, leaf_index)) => {
231 TreePosition::new(group_id, leaf_index)
232 }
233 _ => return Err(ValidationError::InvalidSenderType),
234 };
235 leaf_node.add_tree_position(tree_position);
236 let pk = &leaf_node
237 .signature_key()
238 .clone()
239 .into_signature_public_key_enriched(ciphersuite.signature_algorithm());
240
241 leaf_node
242 .verify(crypto, pk)
243 .map_err(|_| ValidationError::InvalidLeafNodeSignature)?
244 }
245 _ => return Err(ValidationError::InvalidLeafNodeSourceType),
246 };
247
248 Ok(UpdateProposal { leaf_node })
249 }
250}
251
252#[derive(
256 Debug,
257 PartialEq,
258 Clone,
259 Serialize,
260 Deserialize,
261 TlsSerialize,
262 TlsDeserialize,
263 TlsDeserializeBytes,
264 TlsSize,
265)]
266#[repr(u8)]
267#[allow(missing_docs)]
268pub enum ProposalOrRefIn {
269 #[tls_codec(discriminant = 1)]
270 Proposal(Box<ProposalIn>),
271 Reference(Box<ProposalRef>),
272}
273
274impl ProposalOrRefIn {
275 pub(crate) fn validate(
277 self,
278 crypto: &impl OpenMlsCrypto,
279 ciphersuite: Ciphersuite,
280 protocol_version: ProtocolVersion,
281 lifetime_policy: LeafNodeLifetimePolicy,
282 ) -> Result<ProposalOrRef, ValidationError> {
283 Ok(match self {
284 ProposalOrRefIn::Proposal(proposal_in) => {
285 ProposalOrRef::Proposal(Box::new(proposal_in.validate(
286 crypto,
287 ciphersuite,
288 None,
289 protocol_version,
290 lifetime_policy,
291 )?))
292 }
293 ProposalOrRefIn::Reference(reference) => ProposalOrRef::Reference(reference),
294 })
295 }
296}
297
298#[cfg(any(feature = "test-utils", test))]
301impl From<AddProposalIn> for AddProposal {
302 fn from(value: AddProposalIn) -> Self {
303 Self {
304 key_package: value.key_package.into(),
305 }
306 }
307}
308
309#[cfg(any(feature = "test-utils", test))]
310impl From<AddProposalIn> for Box<AddProposal> {
311 fn from(value: AddProposalIn) -> Self {
312 Box::new(AddProposal {
313 key_package: value.key_package.into(),
314 })
315 }
316}
317
318impl From<AddProposal> for Box<AddProposalIn> {
319 fn from(value: AddProposal) -> Self {
320 Box::new(AddProposalIn {
321 key_package: value.key_package.into(),
322 })
323 }
324}
325
326#[cfg(any(feature = "test-utils", test))]
329impl From<UpdateProposalIn> for UpdateProposal {
330 fn from(value: UpdateProposalIn) -> Self {
331 Self {
332 leaf_node: value.leaf_node.into(),
333 }
334 }
335}
336
337impl From<UpdateProposal> for UpdateProposalIn {
338 fn from(value: UpdateProposal) -> Self {
339 Self {
340 leaf_node: value.leaf_node.into(),
341 }
342 }
343}
344
345#[cfg(any(feature = "test-utils", test))]
348impl From<GroupContextExtensionProposalIn> for GroupContextExtensionProposal {
349 fn from(value: GroupContextExtensionProposalIn) -> Self {
350 Self::new(value.extensions_tbv.try_into().unwrap())
351 }
352}
353
354#[cfg(any(feature = "test-utils", test))]
355impl From<GroupContextExtensionProposalIn> for Box<GroupContextExtensionProposal> {
356 fn from(value: GroupContextExtensionProposalIn) -> Self {
357 Box::new(GroupContextExtensionProposal::new(
358 value.extensions_tbv.try_into().unwrap(),
359 ))
360 }
361}
362
363impl From<GroupContextExtensionProposal> for GroupContextExtensionProposalIn {
364 fn from(value: crate::messages::proposals::GroupContextExtensionProposal) -> Self {
365 Self {
366 extensions_tbv: value.extensions().clone().into(),
367 }
368 }
369}
370
371impl From<GroupContextExtensionProposal> for Box<GroupContextExtensionProposalIn> {
372 fn from(value: GroupContextExtensionProposal) -> Self {
373 Box::new(GroupContextExtensionProposalIn {
374 extensions_tbv: value.into_extensions().into(),
375 })
376 }
377}
378
379#[cfg(any(feature = "test-utils", test))]
380impl From<UpdateProposalIn> for Box<UpdateProposal> {
381 fn from(value: UpdateProposalIn) -> Self {
382 Box::new(UpdateProposal {
383 leaf_node: value.leaf_node.into(),
384 })
385 }
386}
387
388impl From<UpdateProposal> for Box<UpdateProposalIn> {
389 fn from(value: UpdateProposal) -> Self {
390 Box::new(UpdateProposalIn {
391 leaf_node: value.leaf_node.into(),
392 })
393 }
394}
395
396#[cfg(any(feature = "test-utils", test))]
397impl From<ProposalIn> for crate::messages::proposals::Proposal {
398 fn from(proposal: ProposalIn) -> Self {
399 match proposal {
400 ProposalIn::Add(add) => Self::Add((*add).into()),
401 ProposalIn::Update(update) => Self::Update((*update).into()),
402 ProposalIn::Remove(remove) => Self::Remove(remove),
403 ProposalIn::PreSharedKey(psk) => Self::PreSharedKey(psk),
404 ProposalIn::ReInit(reinit) => Self::ReInit(reinit),
405 ProposalIn::ExternalInit(external_init) => Self::ExternalInit(external_init),
406 ProposalIn::GroupContextExtensions(group_context_extension) => {
407 Self::GroupContextExtensions((*group_context_extension).into())
408 }
409 #[cfg(feature = "extensions-draft")]
410 ProposalIn::AppDataUpdate(app_data_update) => Self::AppDataUpdate(app_data_update),
411 ProposalIn::SelfRemove => Self::SelfRemove,
412 #[cfg(feature = "extensions-draft")]
413 ProposalIn::AppEphemeral(app_ephemeral) => Self::AppEphemeral(app_ephemeral),
414 ProposalIn::Custom(other) => Self::Custom(other),
415 }
416 }
417}
418
419impl From<crate::messages::proposals::Proposal> for ProposalIn {
420 fn from(proposal: crate::messages::proposals::Proposal) -> Self {
421 match proposal {
422 Proposal::Add(add) => Self::Add((*add).into()),
423 Proposal::Update(update) => Self::Update((*update).into()),
424 Proposal::Remove(remove) => Self::Remove(remove),
425 Proposal::PreSharedKey(psk) => Self::PreSharedKey(psk),
426 Proposal::ReInit(reinit) => Self::ReInit(reinit),
427 Proposal::ExternalInit(external_init) => Self::ExternalInit(external_init),
428 Proposal::GroupContextExtensions(group_context_extension) => {
429 Self::GroupContextExtensions((*group_context_extension).into())
430 }
431 #[cfg(feature = "extensions-draft")]
432 Proposal::AppDataUpdate(app_data_update) => Self::AppDataUpdate(app_data_update),
433 Proposal::SelfRemove => Self::SelfRemove,
434 #[cfg(feature = "extensions-draft")]
435 Proposal::AppEphemeral(app_ephemeral) => Self::AppEphemeral(app_ephemeral),
436 Proposal::Custom(other) => Self::Custom(other),
437 }
438 }
439}
440
441#[cfg(any(feature = "test-utils", test))]
442impl From<ProposalOrRefIn> for crate::messages::proposals::ProposalOrRef {
443 fn from(proposal: ProposalOrRefIn) -> Self {
444 match proposal {
445 ProposalOrRefIn::Proposal(proposal) => Self::Proposal(Box::new((*proposal).into())),
446 ProposalOrRefIn::Reference(reference) => Self::Reference(reference),
447 }
448 }
449}
450
451impl From<crate::messages::proposals::ProposalOrRef> for ProposalOrRefIn {
452 fn from(proposal: crate::messages::proposals::ProposalOrRef) -> Self {
453 match proposal {
454 crate::messages::proposals::ProposalOrRef::Proposal(proposal) => {
455 Self::Proposal(Box::new((*proposal).into()))
456 }
457 crate::messages::proposals::ProposalOrRef::Reference(reference) => {
458 Self::Reference(reference)
459 }
460 }
461 }
462}
463
464#[derive(
466 Debug,
467 PartialEq,
468 Clone,
469 Serialize,
470 Deserialize,
471 TlsSerialize,
472 TlsDeserialize,
473 TlsDeserializeBytes,
474 TlsSize,
475)]
476pub struct GroupContextExtensionProposalIn {
477 extensions_tbv: Extensions<AnyObject>,
478}
479
480impl GroupContextExtensionProposalIn {
481 pub(crate) fn validate(self) -> Result<GroupContextExtensionProposal, ValidationError> {
482 let group_context_extensions = self.extensions_tbv;
483 Ok(GroupContextExtensionProposal::new(
484 group_context_extensions
485 .try_into()
486 .map_err(InvalidExtensionError::from)?,
487 ))
488 }
489}