1 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 // Copyright by contributors to this project.
3 // SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5 use super::{
6 commit_sender,
7 confirmation_tag::ConfirmationTag,
8 framing::{
9 ApplicationData, Content, ContentType, MlsMessage, MlsMessagePayload, PublicMessage, Sender,
10 },
11 message_signature::AuthenticatedContent,
12 mls_rules::{CommitDirection, MlsRules},
13 proposal_filter::ProposalBundle,
14 state::GroupState,
15 transcript_hash::InterimTranscriptHash,
16 transcript_hashes, validate_group_info_member, GroupContext, GroupInfo, Welcome,
17 };
18 use crate::{
19 client::MlsError,
20 key_package::validate_key_package_properties,
21 time::MlsTime,
22 tree_kem::{
23 leaf_node_validator::{LeafNodeValidator, ValidationContext},
24 node::LeafIndex,
25 path_secret::PathSecret,
26 validate_update_path, TreeKemPrivate, TreeKemPublic, ValidatedUpdatePath,
27 },
28 CipherSuiteProvider, KeyPackage,
29 };
30 use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
31
32 #[cfg(mls_build_async)]
33 use alloc::boxed::Box;
34 use alloc::vec::Vec;
35 use core::fmt::{self, Debug};
36 use mls_rs_core::{
37 identity::IdentityProvider, protocol_version::ProtocolVersion, psk::PreSharedKeyStorage,
38 };
39
40 #[cfg(feature = "by_ref_proposal")]
41 use super::proposal_ref::ProposalRef;
42
43 #[cfg(not(feature = "by_ref_proposal"))]
44 use crate::group::proposal_cache::resolve_for_commit;
45
46 #[cfg(feature = "by_ref_proposal")]
47 use super::proposal::Proposal;
48
49 #[cfg(feature = "custom_proposal")]
50 use super::proposal_filter::ProposalInfo;
51
52 #[cfg(feature = "state_update")]
53 use mls_rs_core::{
54 crypto::CipherSuite,
55 group::{MemberUpdate, RosterUpdate},
56 };
57
58 #[cfg(all(feature = "state_update", feature = "psk"))]
59 use mls_rs_core::psk::ExternalPskId;
60
61 #[cfg(feature = "state_update")]
62 use crate::tree_kem::UpdatePath;
63
64 #[cfg(feature = "state_update")]
65 use super::{member_from_key_package, member_from_leaf_node};
66
67 #[cfg(all(feature = "state_update", feature = "custom_proposal"))]
68 use super::proposal::CustomProposal;
69
70 #[cfg(feature = "private_message")]
71 use crate::group::framing::PrivateMessage;
72
73 #[derive(Debug)]
74 pub(crate) struct ProvisionalState {
75 pub(crate) public_tree: TreeKemPublic,
76 pub(crate) applied_proposals: ProposalBundle,
77 pub(crate) group_context: GroupContext,
78 pub(crate) external_init_index: Option<LeafIndex>,
79 pub(crate) indexes_of_added_kpkgs: Vec<LeafIndex>,
80 #[cfg(feature = "by_ref_proposal")]
81 pub(crate) unused_proposals: Vec<crate::mls_rules::ProposalInfo<Proposal>>,
82 }
83
84 //By default, the path field of a Commit MUST be populated. The path field MAY be omitted if
85 //(a) it covers at least one proposal and (b) none of the proposals covered by the Commit are
86 //of "path required" types. A proposal type requires a path if it cannot change the group
87 //membership in a way that requires the forward secrecy and post-compromise security guarantees
88 //that an UpdatePath provides. The only proposal types defined in this document that do not
89 //require a path are:
90
91 // add
92 // psk
93 // reinit
path_update_required(proposals: &ProposalBundle) -> bool94 pub(crate) fn path_update_required(proposals: &ProposalBundle) -> bool {
95 let res = proposals.external_init_proposals().first().is_some();
96
97 #[cfg(feature = "by_ref_proposal")]
98 let res = res || !proposals.update_proposals().is_empty();
99
100 res || proposals.length() == 0
101 || proposals.group_context_extensions_proposal().is_some()
102 || !proposals.remove_proposals().is_empty()
103 }
104
105 /// Representation of changes made by a [commit](crate::Group::commit).
106 #[cfg(feature = "state_update")]
107 #[derive(Clone, Debug, PartialEq)]
108 pub struct StateUpdate {
109 pub(crate) roster_update: RosterUpdate,
110 #[cfg(feature = "psk")]
111 pub(crate) added_psks: Vec<ExternalPskId>,
112 pub(crate) pending_reinit: Option<CipherSuite>,
113 pub(crate) active: bool,
114 pub(crate) epoch: u64,
115 #[cfg(feature = "custom_proposal")]
116 pub(crate) custom_proposals: Vec<ProposalInfo<CustomProposal>>,
117 #[cfg(feature = "by_ref_proposal")]
118 pub(crate) unused_proposals: Vec<crate::mls_rules::ProposalInfo<Proposal>>,
119 }
120
121 #[cfg(not(feature = "state_update"))]
122 #[non_exhaustive]
123 #[derive(Clone, Debug, PartialEq)]
124 pub struct StateUpdate {}
125
126 #[cfg(feature = "state_update")]
127 impl StateUpdate {
128 /// Changes to the roster as a result of proposals.
roster_update(&self) -> &RosterUpdate129 pub fn roster_update(&self) -> &RosterUpdate {
130 &self.roster_update
131 }
132
133 #[cfg(feature = "psk")]
134 /// Pre-shared keys that have been added to the group.
added_psks(&self) -> &[ExternalPskId]135 pub fn added_psks(&self) -> &[ExternalPskId] {
136 &self.added_psks
137 }
138
139 /// Flag to indicate if the group is now pending reinitialization due to
140 /// receiving a [`ReInit`](crate::group::proposal::Proposal::ReInit)
141 /// proposal.
is_pending_reinit(&self) -> bool142 pub fn is_pending_reinit(&self) -> bool {
143 self.pending_reinit.is_some()
144 }
145
146 /// Flag to indicate the group is still active. This will be false if the
147 /// member processing the commit has been removed from the group.
is_active(&self) -> bool148 pub fn is_active(&self) -> bool {
149 self.active
150 }
151
152 /// The new epoch of the group state.
new_epoch(&self) -> u64153 pub fn new_epoch(&self) -> u64 {
154 self.epoch
155 }
156
157 /// Custom proposals that were committed to.
158 #[cfg(feature = "custom_proposal")]
custom_proposals(&self) -> &[ProposalInfo<CustomProposal>]159 pub fn custom_proposals(&self) -> &[ProposalInfo<CustomProposal>] {
160 &self.custom_proposals
161 }
162
163 /// Proposals that were received in the prior epoch but not committed to.
164 #[cfg(feature = "by_ref_proposal")]
unused_proposals(&self) -> &[crate::mls_rules::ProposalInfo<Proposal>]165 pub fn unused_proposals(&self) -> &[crate::mls_rules::ProposalInfo<Proposal>] {
166 &self.unused_proposals
167 }
168
pending_reinit_ciphersuite(&self) -> Option<CipherSuite>169 pub fn pending_reinit_ciphersuite(&self) -> Option<CipherSuite> {
170 self.pending_reinit
171 }
172 }
173
174 #[cfg_attr(
175 all(feature = "ffi", not(test)),
176 safer_ffi_gen::ffi_type(clone, opaque)
177 )]
178 #[derive(Debug, Clone)]
179 #[allow(clippy::large_enum_variant)]
180 /// An event generated as a result of processing a message for a group with
181 /// [`Group::process_incoming_message`](crate::group::Group::process_incoming_message).
182 pub enum ReceivedMessage {
183 /// An application message was decrypted.
184 ApplicationMessage(ApplicationMessageDescription),
185 /// A new commit was processed creating a new group state.
186 Commit(CommitMessageDescription),
187 /// A proposal was received.
188 Proposal(ProposalMessageDescription),
189 /// Validated GroupInfo object
190 GroupInfo(GroupInfo),
191 /// Validated welcome message
192 Welcome,
193 /// Validated key package
194 KeyPackage(KeyPackage),
195 }
196
197 impl TryFrom<ApplicationMessageDescription> for ReceivedMessage {
198 type Error = MlsError;
199
try_from(value: ApplicationMessageDescription) -> Result<Self, Self::Error>200 fn try_from(value: ApplicationMessageDescription) -> Result<Self, Self::Error> {
201 Ok(ReceivedMessage::ApplicationMessage(value))
202 }
203 }
204
205 impl From<CommitMessageDescription> for ReceivedMessage {
from(value: CommitMessageDescription) -> Self206 fn from(value: CommitMessageDescription) -> Self {
207 ReceivedMessage::Commit(value)
208 }
209 }
210
211 impl From<ProposalMessageDescription> for ReceivedMessage {
from(value: ProposalMessageDescription) -> Self212 fn from(value: ProposalMessageDescription) -> Self {
213 ReceivedMessage::Proposal(value)
214 }
215 }
216
217 impl From<GroupInfo> for ReceivedMessage {
from(value: GroupInfo) -> Self218 fn from(value: GroupInfo) -> Self {
219 ReceivedMessage::GroupInfo(value)
220 }
221 }
222
223 impl From<Welcome> for ReceivedMessage {
from(_: Welcome) -> Self224 fn from(_: Welcome) -> Self {
225 ReceivedMessage::Welcome
226 }
227 }
228
229 impl From<KeyPackage> for ReceivedMessage {
from(value: KeyPackage) -> Self230 fn from(value: KeyPackage) -> Self {
231 ReceivedMessage::KeyPackage(value)
232 }
233 }
234
235 #[cfg_attr(
236 all(feature = "ffi", not(test)),
237 safer_ffi_gen::ffi_type(clone, opaque)
238 )]
239 #[derive(Clone, PartialEq, Eq)]
240 /// Description of a MLS application message.
241 pub struct ApplicationMessageDescription {
242 /// Index of this user in the group state.
243 pub sender_index: u32,
244 /// Received application data.
245 data: ApplicationData,
246 /// Plaintext authenticated data in the received MLS packet.
247 pub authenticated_data: Vec<u8>,
248 }
249
250 impl Debug for ApplicationMessageDescription {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 f.debug_struct("ApplicationMessageDescription")
253 .field("sender_index", &self.sender_index)
254 .field("data", &self.data)
255 .field(
256 "authenticated_data",
257 &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
258 )
259 .finish()
260 }
261 }
262
263 #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
264 impl ApplicationMessageDescription {
data(&self) -> &[u8]265 pub fn data(&self) -> &[u8] {
266 self.data.as_bytes()
267 }
268 }
269
270 #[cfg_attr(
271 all(feature = "ffi", not(test)),
272 safer_ffi_gen::ffi_type(clone, opaque)
273 )]
274 #[derive(Clone, PartialEq)]
275 #[non_exhaustive]
276 /// Description of a processed MLS commit message.
277 pub struct CommitMessageDescription {
278 /// True if this is the result of an external commit.
279 pub is_external: bool,
280 /// The index in the group state of the member who performed this commit.
281 pub committer: u32,
282 /// A full description of group state changes as a result of this commit.
283 pub state_update: StateUpdate,
284 /// Plaintext authenticated data in the received MLS packet.
285 pub authenticated_data: Vec<u8>,
286 }
287
288 impl Debug for CommitMessageDescription {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 f.debug_struct("CommitMessageDescription")
291 .field("is_external", &self.is_external)
292 .field("committer", &self.committer)
293 .field("state_update", &self.state_update)
294 .field(
295 "authenticated_data",
296 &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
297 )
298 .finish()
299 }
300 }
301
302 #[derive(Debug, Clone, Copy, PartialEq, Eq, MlsEncode, MlsDecode, MlsSize)]
303 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
304 #[repr(u8)]
305 /// Proposal sender type.
306 pub enum ProposalSender {
307 /// A current member of the group by index in the group state.
308 Member(u32) = 1u8,
309 /// An external entity by index within an
310 /// [`ExternalSendersExt`](crate::extension::built_in::ExternalSendersExt).
311 External(u32) = 2u8,
312 /// A new member proposing their addition to the group.
313 NewMember = 3u8,
314 }
315
316 impl TryFrom<Sender> for ProposalSender {
317 type Error = MlsError;
318
try_from(value: Sender) -> Result<Self, Self::Error>319 fn try_from(value: Sender) -> Result<Self, Self::Error> {
320 match value {
321 Sender::Member(index) => Ok(Self::Member(index)),
322 #[cfg(feature = "by_ref_proposal")]
323 Sender::External(index) => Ok(Self::External(index)),
324 #[cfg(feature = "by_ref_proposal")]
325 Sender::NewMemberProposal => Ok(Self::NewMember),
326 Sender::NewMemberCommit => Err(MlsError::InvalidSender),
327 }
328 }
329 }
330
331 #[cfg(feature = "by_ref_proposal")]
332 #[cfg_attr(
333 all(feature = "ffi", not(test)),
334 safer_ffi_gen::ffi_type(clone, opaque)
335 )]
336 #[derive(Clone, MlsEncode, MlsDecode, MlsSize, PartialEq)]
337 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
338 #[non_exhaustive]
339 /// Description of a processed MLS proposal message.
340 pub struct ProposalMessageDescription {
341 /// Sender of the proposal.
342 pub sender: ProposalSender,
343 /// Proposal content.
344 pub proposal: Proposal,
345 /// Plaintext authenticated data in the received MLS packet.
346 pub authenticated_data: Vec<u8>,
347 /// Proposal reference.
348 pub proposal_ref: ProposalRef,
349 }
350
351 #[cfg(feature = "by_ref_proposal")]
352 impl Debug for ProposalMessageDescription {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result353 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354 f.debug_struct("ProposalMessageDescription")
355 .field("sender", &self.sender)
356 .field("proposal", &self.proposal)
357 .field(
358 "authenticated_data",
359 &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
360 )
361 .field("proposal_ref", &self.proposal_ref)
362 .finish()
363 }
364 }
365
366 #[cfg(feature = "by_ref_proposal")]
367 #[derive(MlsSize, MlsEncode, MlsDecode)]
368 pub struct CachedProposal {
369 pub(crate) proposal: Proposal,
370 pub(crate) proposal_ref: ProposalRef,
371 pub(crate) sender: Sender,
372 }
373
374 #[cfg(feature = "by_ref_proposal")]
375 impl CachedProposal {
376 /// Deserialize the proposal
from_bytes(bytes: &[u8]) -> Result<Self, MlsError>377 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
378 Ok(Self::mls_decode(&mut &*bytes)?)
379 }
380
381 /// Serialize the proposal
to_bytes(&self) -> Result<Vec<u8>, MlsError>382 pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
383 Ok(self.mls_encode_to_vec()?)
384 }
385 }
386
387 #[cfg(feature = "by_ref_proposal")]
388 impl ProposalMessageDescription {
cached_proposal(self) -> CachedProposal389 pub fn cached_proposal(self) -> CachedProposal {
390 let sender = match self.sender {
391 ProposalSender::Member(i) => Sender::Member(i),
392 ProposalSender::External(i) => Sender::External(i),
393 ProposalSender::NewMember => Sender::NewMemberProposal,
394 };
395
396 CachedProposal {
397 proposal: self.proposal,
398 proposal_ref: self.proposal_ref,
399 sender,
400 }
401 }
402
proposal_ref(&self) -> Vec<u8>403 pub fn proposal_ref(&self) -> Vec<u8> {
404 self.proposal_ref.to_vec()
405 }
406
407 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
new<C: CipherSuiteProvider>( cs: &C, content: &AuthenticatedContent, proposal: Proposal, ) -> Result<Self, MlsError>408 pub(crate) async fn new<C: CipherSuiteProvider>(
409 cs: &C,
410 content: &AuthenticatedContent,
411 proposal: Proposal,
412 ) -> Result<Self, MlsError> {
413 Ok(ProposalMessageDescription {
414 authenticated_data: content.content.authenticated_data.clone(),
415 proposal,
416 sender: content.content.sender.try_into()?,
417 proposal_ref: ProposalRef::from_content(cs, content).await?,
418 })
419 }
420 }
421
422 #[cfg(not(feature = "by_ref_proposal"))]
423 #[cfg_attr(
424 all(feature = "ffi", not(test)),
425 safer_ffi_gen::ffi_type(clone, opaque)
426 )]
427 #[derive(Debug, Clone)]
428 /// Description of a processed MLS proposal message.
429 pub struct ProposalMessageDescription {}
430
431 #[allow(clippy::large_enum_variant)]
432 pub(crate) enum EventOrContent<E> {
433 #[cfg_attr(
434 not(all(feature = "private_message", feature = "external_client")),
435 allow(dead_code)
436 )]
437 Event(E),
438 Content(AuthenticatedContent),
439 }
440
441 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
442 #[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
443 #[cfg_attr(
444 all(not(target_arch = "wasm32"), mls_build_async),
445 maybe_async::must_be_async
446 )]
447 pub(crate) trait MessageProcessor: Send + Sync {
448 type OutputType: TryFrom<ApplicationMessageDescription, Error = MlsError>
449 + From<CommitMessageDescription>
450 + From<ProposalMessageDescription>
451 + From<GroupInfo>
452 + From<Welcome>
453 + From<KeyPackage>
454 + Send;
455
456 type MlsRules: MlsRules;
457 type IdentityProvider: IdentityProvider;
458 type CipherSuiteProvider: CipherSuiteProvider;
459 type PreSharedKeyStorage: PreSharedKeyStorage;
460
process_incoming_message( &mut self, message: MlsMessage, #[cfg(feature = "by_ref_proposal")] cache_proposal: bool, ) -> Result<Self::OutputType, MlsError>461 async fn process_incoming_message(
462 &mut self,
463 message: MlsMessage,
464 #[cfg(feature = "by_ref_proposal")] cache_proposal: bool,
465 ) -> Result<Self::OutputType, MlsError> {
466 self.process_incoming_message_with_time(
467 message,
468 #[cfg(feature = "by_ref_proposal")]
469 cache_proposal,
470 None,
471 )
472 .await
473 }
474
process_incoming_message_with_time( &mut self, message: MlsMessage, #[cfg(feature = "by_ref_proposal")] cache_proposal: bool, time_sent: Option<MlsTime>, ) -> Result<Self::OutputType, MlsError>475 async fn process_incoming_message_with_time(
476 &mut self,
477 message: MlsMessage,
478 #[cfg(feature = "by_ref_proposal")] cache_proposal: bool,
479 time_sent: Option<MlsTime>,
480 ) -> Result<Self::OutputType, MlsError> {
481 let event_or_content = self.get_event_from_incoming_message(message).await?;
482
483 self.process_event_or_content(
484 event_or_content,
485 #[cfg(feature = "by_ref_proposal")]
486 cache_proposal,
487 time_sent,
488 )
489 .await
490 }
491
get_event_from_incoming_message( &mut self, message: MlsMessage, ) -> Result<EventOrContent<Self::OutputType>, MlsError>492 async fn get_event_from_incoming_message(
493 &mut self,
494 message: MlsMessage,
495 ) -> Result<EventOrContent<Self::OutputType>, MlsError> {
496 self.check_metadata(&message)?;
497
498 match message.payload {
499 MlsMessagePayload::Plain(plaintext) => {
500 self.verify_plaintext_authentication(plaintext).await
501 }
502 #[cfg(feature = "private_message")]
503 MlsMessagePayload::Cipher(cipher_text) => self.process_ciphertext(&cipher_text).await,
504 MlsMessagePayload::GroupInfo(group_info) => {
505 validate_group_info_member(
506 self.group_state(),
507 message.version,
508 &group_info,
509 self.cipher_suite_provider(),
510 )
511 .await?;
512
513 Ok(EventOrContent::Event(group_info.into()))
514 }
515 MlsMessagePayload::Welcome(welcome) => {
516 self.validate_welcome(&welcome, message.version)?;
517
518 Ok(EventOrContent::Event(welcome.into()))
519 }
520 MlsMessagePayload::KeyPackage(key_package) => {
521 self.validate_key_package(&key_package, message.version)
522 .await?;
523
524 Ok(EventOrContent::Event(key_package.into()))
525 }
526 }
527 }
528
process_event_or_content( &mut self, event_or_content: EventOrContent<Self::OutputType>, #[cfg(feature = "by_ref_proposal")] cache_proposal: bool, time_sent: Option<MlsTime>, ) -> Result<Self::OutputType, MlsError>529 async fn process_event_or_content(
530 &mut self,
531 event_or_content: EventOrContent<Self::OutputType>,
532 #[cfg(feature = "by_ref_proposal")] cache_proposal: bool,
533 time_sent: Option<MlsTime>,
534 ) -> Result<Self::OutputType, MlsError> {
535 let msg = match event_or_content {
536 EventOrContent::Event(event) => event,
537 EventOrContent::Content(content) => {
538 self.process_auth_content(
539 content,
540 #[cfg(feature = "by_ref_proposal")]
541 cache_proposal,
542 time_sent,
543 )
544 .await?
545 }
546 };
547
548 Ok(msg)
549 }
550
process_auth_content( &mut self, auth_content: AuthenticatedContent, #[cfg(feature = "by_ref_proposal")] cache_proposal: bool, time_sent: Option<MlsTime>, ) -> Result<Self::OutputType, MlsError>551 async fn process_auth_content(
552 &mut self,
553 auth_content: AuthenticatedContent,
554 #[cfg(feature = "by_ref_proposal")] cache_proposal: bool,
555 time_sent: Option<MlsTime>,
556 ) -> Result<Self::OutputType, MlsError> {
557 let event = match auth_content.content.content {
558 #[cfg(feature = "private_message")]
559 Content::Application(data) => {
560 let authenticated_data = auth_content.content.authenticated_data;
561 let sender = auth_content.content.sender;
562
563 self.process_application_message(data, sender, authenticated_data)
564 .and_then(Self::OutputType::try_from)
565 }
566 Content::Commit(_) => self
567 .process_commit(auth_content, time_sent)
568 .await
569 .map(Self::OutputType::from),
570 #[cfg(feature = "by_ref_proposal")]
571 Content::Proposal(ref proposal) => self
572 .process_proposal(&auth_content, proposal, cache_proposal)
573 .await
574 .map(Self::OutputType::from),
575 }?;
576
577 Ok(event)
578 }
579
580 #[cfg(feature = "private_message")]
process_application_message( &self, data: ApplicationData, sender: Sender, authenticated_data: Vec<u8>, ) -> Result<ApplicationMessageDescription, MlsError>581 fn process_application_message(
582 &self,
583 data: ApplicationData,
584 sender: Sender,
585 authenticated_data: Vec<u8>,
586 ) -> Result<ApplicationMessageDescription, MlsError> {
587 let Sender::Member(sender_index) = sender else {
588 return Err(MlsError::InvalidSender);
589 };
590
591 Ok(ApplicationMessageDescription {
592 authenticated_data,
593 sender_index,
594 data,
595 })
596 }
597
598 #[cfg(feature = "by_ref_proposal")]
599 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
process_proposal( &mut self, auth_content: &AuthenticatedContent, proposal: &Proposal, cache_proposal: bool, ) -> Result<ProposalMessageDescription, MlsError>600 async fn process_proposal(
601 &mut self,
602 auth_content: &AuthenticatedContent,
603 proposal: &Proposal,
604 cache_proposal: bool,
605 ) -> Result<ProposalMessageDescription, MlsError> {
606 let proposal = ProposalMessageDescription::new(
607 self.cipher_suite_provider(),
608 auth_content,
609 proposal.clone(),
610 )
611 .await?;
612
613 let group_state = self.group_state_mut();
614
615 if cache_proposal {
616 group_state.proposals.insert(
617 proposal.proposal_ref.clone(),
618 proposal.proposal.clone(),
619 auth_content.content.sender,
620 );
621 }
622
623 Ok(proposal)
624 }
625
626 #[cfg(feature = "state_update")]
make_state_update( &self, provisional: &ProvisionalState, path: Option<&UpdatePath>, sender: LeafIndex, ) -> Result<StateUpdate, MlsError>627 async fn make_state_update(
628 &self,
629 provisional: &ProvisionalState,
630 path: Option<&UpdatePath>,
631 sender: LeafIndex,
632 ) -> Result<StateUpdate, MlsError> {
633 let added = provisional
634 .applied_proposals
635 .additions
636 .iter()
637 .zip(provisional.indexes_of_added_kpkgs.iter())
638 .map(|(p, index)| member_from_key_package(&p.proposal.key_package, *index))
639 .collect::<Vec<_>>();
640
641 let mut added = added;
642
643 let old_tree = &self.group_state().public_tree;
644
645 let removed = provisional
646 .applied_proposals
647 .removals
648 .iter()
649 .map(|p| {
650 let index = p.proposal.to_remove;
651 let node = old_tree.nodes.borrow_as_leaf(index)?;
652 Ok(member_from_leaf_node(node, index))
653 })
654 .collect::<Result<_, MlsError>>()?;
655
656 #[cfg(feature = "by_ref_proposal")]
657 let mut updated = provisional
658 .applied_proposals
659 .update_senders
660 .iter()
661 .map(|index| {
662 let prior = old_tree
663 .get_leaf_node(*index)
664 .map(|n| member_from_leaf_node(n, *index))?;
665
666 let new = provisional
667 .public_tree
668 .get_leaf_node(*index)
669 .map(|n| member_from_leaf_node(n, *index))?;
670
671 Ok::<_, MlsError>(MemberUpdate::new(prior, new))
672 })
673 .collect::<Result<Vec<_>, _>>()?;
674
675 #[cfg(not(feature = "by_ref_proposal"))]
676 let mut updated = Vec::new();
677
678 if let Some(path) = path {
679 if !provisional
680 .applied_proposals
681 .external_initializations
682 .is_empty()
683 {
684 added.push(member_from_leaf_node(&path.leaf_node, sender))
685 } else {
686 let prior = old_tree
687 .get_leaf_node(sender)
688 .map(|n| member_from_leaf_node(n, sender))?;
689
690 let new = member_from_leaf_node(&path.leaf_node, sender);
691
692 updated.push(MemberUpdate::new(prior, new))
693 }
694 }
695
696 #[cfg(feature = "psk")]
697 let psks = provisional
698 .applied_proposals
699 .psks
700 .iter()
701 .filter_map(|psk| psk.proposal.external_psk_id().cloned())
702 .collect::<Vec<_>>();
703
704 let roster_update = RosterUpdate::new(added, removed, updated);
705
706 let update = StateUpdate {
707 roster_update,
708 #[cfg(feature = "psk")]
709 added_psks: psks,
710 pending_reinit: provisional
711 .applied_proposals
712 .reinitializations
713 .first()
714 .map(|ri| ri.proposal.new_cipher_suite()),
715 active: true,
716 epoch: provisional.group_context.epoch,
717 #[cfg(feature = "custom_proposal")]
718 custom_proposals: provisional.applied_proposals.custom_proposals.clone(),
719 #[cfg(feature = "by_ref_proposal")]
720 unused_proposals: provisional.unused_proposals.clone(),
721 };
722
723 Ok(update)
724 }
725
process_commit( &mut self, auth_content: AuthenticatedContent, time_sent: Option<MlsTime>, ) -> Result<CommitMessageDescription, MlsError>726 async fn process_commit(
727 &mut self,
728 auth_content: AuthenticatedContent,
729 time_sent: Option<MlsTime>,
730 ) -> Result<CommitMessageDescription, MlsError> {
731 if self.group_state().pending_reinit.is_some() {
732 return Err(MlsError::GroupUsedAfterReInit);
733 }
734
735 // Update the new GroupContext's confirmed and interim transcript hashes using the new Commit.
736 let (interim_transcript_hash, confirmed_transcript_hash) = transcript_hashes(
737 self.cipher_suite_provider(),
738 &self.group_state().interim_transcript_hash,
739 &auth_content,
740 )
741 .await?;
742
743 #[cfg(any(feature = "private_message", feature = "by_ref_proposal"))]
744 let commit = match auth_content.content.content {
745 Content::Commit(commit) => Ok(commit),
746 _ => Err(MlsError::UnexpectedMessageType),
747 }?;
748
749 #[cfg(not(any(feature = "private_message", feature = "by_ref_proposal")))]
750 let Content::Commit(commit) = auth_content.content.content;
751
752 let group_state = self.group_state();
753 let id_provider = self.identity_provider();
754
755 #[cfg(feature = "by_ref_proposal")]
756 let proposals = group_state
757 .proposals
758 .resolve_for_commit(auth_content.content.sender, commit.proposals)?;
759
760 #[cfg(not(feature = "by_ref_proposal"))]
761 let proposals = resolve_for_commit(auth_content.content.sender, commit.proposals)?;
762
763 let mut provisional_state = group_state
764 .apply_resolved(
765 auth_content.content.sender,
766 proposals,
767 commit.path.as_ref().map(|path| &path.leaf_node),
768 &id_provider,
769 self.cipher_suite_provider(),
770 &self.psk_storage(),
771 &self.mls_rules(),
772 time_sent,
773 CommitDirection::Receive,
774 )
775 .await?;
776
777 let sender = commit_sender(&auth_content.content.sender, &provisional_state)?;
778
779 #[cfg(feature = "state_update")]
780 let mut state_update = self
781 .make_state_update(&provisional_state, commit.path.as_ref(), sender)
782 .await?;
783
784 #[cfg(not(feature = "state_update"))]
785 let state_update = StateUpdate {};
786
787 //Verify that the path value is populated if the proposals vector contains any Update
788 // or Remove proposals, or if it's empty. Otherwise, the path value MAY be omitted.
789 if path_update_required(&provisional_state.applied_proposals) && commit.path.is_none() {
790 return Err(MlsError::CommitMissingPath);
791 }
792
793 if !self.can_continue_processing(&provisional_state) {
794 #[cfg(feature = "state_update")]
795 {
796 state_update.active = false;
797 }
798
799 return Ok(CommitMessageDescription {
800 is_external: matches!(auth_content.content.sender, Sender::NewMemberCommit),
801 authenticated_data: auth_content.content.authenticated_data,
802 committer: *sender,
803 state_update,
804 });
805 }
806
807 let update_path = match commit.path {
808 Some(update_path) => Some(
809 validate_update_path(
810 &self.identity_provider(),
811 self.cipher_suite_provider(),
812 update_path,
813 &provisional_state,
814 sender,
815 time_sent,
816 )
817 .await?,
818 ),
819 None => None,
820 };
821
822 let new_secrets = match update_path {
823 Some(update_path) => {
824 self.apply_update_path(sender, &update_path, &mut provisional_state)
825 .await
826 }
827 None => Ok(None),
828 }?;
829
830 // Update the transcript hash to get the new context.
831 provisional_state.group_context.confirmed_transcript_hash = confirmed_transcript_hash;
832
833 // Update the parent hashes in the new context
834 provisional_state
835 .public_tree
836 .update_hashes(&[sender], self.cipher_suite_provider())
837 .await?;
838
839 // Update the tree hash in the new context
840 provisional_state.group_context.tree_hash = provisional_state
841 .public_tree
842 .tree_hash(self.cipher_suite_provider())
843 .await?;
844
845 if let Some(reinit) = provisional_state.applied_proposals.reinitializations.pop() {
846 self.group_state_mut().pending_reinit = Some(reinit.proposal);
847
848 #[cfg(feature = "state_update")]
849 {
850 state_update.active = false;
851 }
852 }
853
854 if let Some(confirmation_tag) = &auth_content.auth.confirmation_tag {
855 // Update the key schedule to calculate new private keys
856 self.update_key_schedule(
857 new_secrets,
858 interim_transcript_hash,
859 confirmation_tag,
860 provisional_state,
861 )
862 .await?;
863
864 Ok(CommitMessageDescription {
865 is_external: matches!(auth_content.content.sender, Sender::NewMemberCommit),
866 authenticated_data: auth_content.content.authenticated_data,
867 committer: *sender,
868 state_update,
869 })
870 } else {
871 Err(MlsError::InvalidConfirmationTag)
872 }
873 }
874
group_state(&self) -> &GroupState875 fn group_state(&self) -> &GroupState;
group_state_mut(&mut self) -> &mut GroupState876 fn group_state_mut(&mut self) -> &mut GroupState;
mls_rules(&self) -> Self::MlsRules877 fn mls_rules(&self) -> Self::MlsRules;
identity_provider(&self) -> Self::IdentityProvider878 fn identity_provider(&self) -> Self::IdentityProvider;
cipher_suite_provider(&self) -> &Self::CipherSuiteProvider879 fn cipher_suite_provider(&self) -> &Self::CipherSuiteProvider;
psk_storage(&self) -> Self::PreSharedKeyStorage880 fn psk_storage(&self) -> Self::PreSharedKeyStorage;
can_continue_processing(&self, provisional_state: &ProvisionalState) -> bool881 fn can_continue_processing(&self, provisional_state: &ProvisionalState) -> bool;
882
883 #[cfg(feature = "private_message")]
min_epoch_available(&self) -> Option<u64>884 fn min_epoch_available(&self) -> Option<u64>;
885
check_metadata(&self, message: &MlsMessage) -> Result<(), MlsError>886 fn check_metadata(&self, message: &MlsMessage) -> Result<(), MlsError> {
887 let context = &self.group_state().context;
888
889 if message.version != context.protocol_version {
890 return Err(MlsError::ProtocolVersionMismatch);
891 }
892
893 if let Some((group_id, epoch, content_type)) = match &message.payload {
894 MlsMessagePayload::Plain(plaintext) => Some((
895 &plaintext.content.group_id,
896 plaintext.content.epoch,
897 plaintext.content.content_type(),
898 )),
899 #[cfg(feature = "private_message")]
900 MlsMessagePayload::Cipher(ciphertext) => Some((
901 &ciphertext.group_id,
902 ciphertext.epoch,
903 ciphertext.content_type,
904 )),
905 _ => None,
906 } {
907 if group_id != &context.group_id {
908 return Err(MlsError::GroupIdMismatch);
909 }
910
911 match content_type {
912 ContentType::Commit => {
913 if context.epoch != epoch {
914 Err(MlsError::InvalidEpoch)
915 } else {
916 Ok(())
917 }
918 }
919 #[cfg(feature = "by_ref_proposal")]
920 ContentType::Proposal => {
921 if context.epoch != epoch {
922 Err(MlsError::InvalidEpoch)
923 } else {
924 Ok(())
925 }
926 }
927 #[cfg(feature = "private_message")]
928 ContentType::Application => {
929 if let Some(min) = self.min_epoch_available() {
930 if epoch < min {
931 Err(MlsError::InvalidEpoch)
932 } else {
933 Ok(())
934 }
935 } else {
936 Ok(())
937 }
938 }
939 }?;
940
941 // Proposal and commit messages must be sent in the current epoch
942 let check_epoch = content_type == ContentType::Commit;
943
944 #[cfg(feature = "by_ref_proposal")]
945 let check_epoch = check_epoch || content_type == ContentType::Proposal;
946
947 if check_epoch && epoch != context.epoch {
948 return Err(MlsError::InvalidEpoch);
949 }
950
951 // Unencrypted application messages are not allowed
952 #[cfg(feature = "private_message")]
953 if !matches!(&message.payload, MlsMessagePayload::Cipher(_))
954 && content_type == ContentType::Application
955 {
956 return Err(MlsError::UnencryptedApplicationMessage);
957 }
958 }
959
960 Ok(())
961 }
962
validate_welcome( &self, welcome: &Welcome, version: ProtocolVersion, ) -> Result<(), MlsError>963 fn validate_welcome(
964 &self,
965 welcome: &Welcome,
966 version: ProtocolVersion,
967 ) -> Result<(), MlsError> {
968 let state = self.group_state();
969
970 (welcome.cipher_suite == state.context.cipher_suite
971 && version == state.context.protocol_version)
972 .then_some(())
973 .ok_or(MlsError::InvalidWelcomeMessage)
974 }
975
validate_key_package( &self, key_package: &KeyPackage, version: ProtocolVersion, ) -> Result<(), MlsError>976 async fn validate_key_package(
977 &self,
978 key_package: &KeyPackage,
979 version: ProtocolVersion,
980 ) -> Result<(), MlsError> {
981 let cs = self.cipher_suite_provider();
982 let id = self.identity_provider();
983
984 validate_key_package(key_package, version, cs, &id).await
985 }
986
987 #[cfg(feature = "private_message")]
process_ciphertext( &mut self, cipher_text: &PrivateMessage, ) -> Result<EventOrContent<Self::OutputType>, MlsError>988 async fn process_ciphertext(
989 &mut self,
990 cipher_text: &PrivateMessage,
991 ) -> Result<EventOrContent<Self::OutputType>, MlsError>;
992
verify_plaintext_authentication( &self, message: PublicMessage, ) -> Result<EventOrContent<Self::OutputType>, MlsError>993 async fn verify_plaintext_authentication(
994 &self,
995 message: PublicMessage,
996 ) -> Result<EventOrContent<Self::OutputType>, MlsError>;
997
apply_update_path( &mut self, sender: LeafIndex, update_path: &ValidatedUpdatePath, provisional_state: &mut ProvisionalState, ) -> Result<Option<(TreeKemPrivate, PathSecret)>, MlsError>998 async fn apply_update_path(
999 &mut self,
1000 sender: LeafIndex,
1001 update_path: &ValidatedUpdatePath,
1002 provisional_state: &mut ProvisionalState,
1003 ) -> Result<Option<(TreeKemPrivate, PathSecret)>, MlsError> {
1004 provisional_state
1005 .public_tree
1006 .apply_update_path(
1007 sender,
1008 update_path,
1009 &provisional_state.group_context.extensions,
1010 self.identity_provider(),
1011 self.cipher_suite_provider(),
1012 )
1013 .await
1014 .map(|_| None)
1015 }
1016
update_key_schedule( &mut self, secrets: Option<(TreeKemPrivate, PathSecret)>, interim_transcript_hash: InterimTranscriptHash, confirmation_tag: &ConfirmationTag, provisional_public_state: ProvisionalState, ) -> Result<(), MlsError>1017 async fn update_key_schedule(
1018 &mut self,
1019 secrets: Option<(TreeKemPrivate, PathSecret)>,
1020 interim_transcript_hash: InterimTranscriptHash,
1021 confirmation_tag: &ConfirmationTag,
1022 provisional_public_state: ProvisionalState,
1023 ) -> Result<(), MlsError>;
1024 }
1025
1026 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
validate_key_package<C: CipherSuiteProvider, I: IdentityProvider>( key_package: &KeyPackage, version: ProtocolVersion, cs: &C, id: &I, ) -> Result<(), MlsError>1027 pub(crate) async fn validate_key_package<C: CipherSuiteProvider, I: IdentityProvider>(
1028 key_package: &KeyPackage,
1029 version: ProtocolVersion,
1030 cs: &C,
1031 id: &I,
1032 ) -> Result<(), MlsError> {
1033 let validator = LeafNodeValidator::new(cs, id, None);
1034
1035 #[cfg(feature = "std")]
1036 let context = Some(MlsTime::now());
1037
1038 #[cfg(not(feature = "std"))]
1039 let context = None;
1040
1041 let context = ValidationContext::Add(context);
1042
1043 validator
1044 .check_if_valid(&key_package.leaf_node, context)
1045 .await?;
1046
1047 validate_key_package_properties(key_package, version, cs).await?;
1048
1049 Ok(())
1050 }
1051