Skip to main content

openmls/framing/
public_message_in.rs

1//! # PublicMessageIn
2//!
3//! A PublicMessageIn is a framing structure for MLS messages. It can contain
4//! Proposals, Commits and application messages.
5
6use crate::{error::LibraryError, group::errors::ValidationError, versions::ProtocolVersion};
7
8use super::{
9    mls_auth_content::FramedContentAuthData,
10    mls_auth_content_in::{AuthenticatedContentIn, VerifiableAuthenticatedContentIn},
11    mls_content::{framed_content_tbs_serialized_detached, AuthenticatedContentTbm},
12    mls_content_in::FramedContentIn,
13    *,
14};
15
16use openmls_traits::types::Ciphersuite;
17use std::io::{Read, Write};
18use tls_codec::{Deserialize as TlsDeserializeTrait, Serialize as TlsSerializeTrait};
19
20/// [`PublicMessageIn`] is a framing structure for MLS messages. It can contain
21/// Proposals, Commits and application messages.
22///
23/// 9. Message framing
24///
25/// ```c
26/// // draft-ietf-mls-protocol-17
27///
28/// struct {
29///     FramedContent content;
30///     FramedContentAuthData auth;
31///     optional<MAC> membership_tag;
32/// } PublicMessage;
33/// ```
34#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
35pub struct PublicMessageIn {
36    pub(crate) content: FramedContentIn,
37    pub(crate) auth: FramedContentAuthData,
38    pub(crate) membership_tag: Option<MembershipTag>,
39}
40
41#[cfg(any(test, feature = "test-utils"))]
42impl PublicMessageIn {
43    pub(crate) fn content(&self) -> &crate::framing::mls_content_in::FramedContentBodyIn {
44        &self.content.body
45    }
46}
47
48#[cfg(test)]
49impl PublicMessageIn {
50    pub fn set_confirmation_tag(&mut self, confirmation_tag: Option<ConfirmationTag>) {
51        self.auth.confirmation_tag = confirmation_tag;
52    }
53
54    pub fn unset_membership_tag(&mut self) {
55        self.membership_tag = None;
56    }
57
58    pub(crate) fn set_content(&mut self, content: FramedContentBodyIn) {
59        self.content.body = content;
60    }
61
62    pub fn set_epoch(&mut self, epoch: u64) {
63        self.content.epoch = epoch.into();
64    }
65
66    /// Set the sender.
67    pub(crate) fn set_sender(&mut self, sender: Sender) {
68        self.content.sender = sender;
69    }
70}
71
72impl From<AuthenticatedContentIn> for PublicMessageIn {
73    fn from(v: AuthenticatedContentIn) -> Self {
74        Self {
75            content: v.content,
76            auth: v.auth,
77            membership_tag: None,
78        }
79    }
80}
81
82#[cfg(feature = "extensions-draft")]
83impl PublicMessageIn {
84    /// Returns the [`AppEphemeralProposal`]s for `component_id` that this
85    /// commit message carries by value, in the order they appear in the
86    /// commit. Returns an empty vector if the message is not a commit.
87    ///
88    /// This reads the deserialized message directly and requires no group
89    /// state. A client that receives an external commit for a group it is not
90    /// yet a member of can use it to inspect attached application data before
91    /// deciding how to proceed with the join.
92    ///
93    /// The returned data is unauthenticated. Neither the signature nor the
94    /// sender of the message has been checked at this point, so callers must
95    /// treat the data as untrusted until the commit has been verified against
96    /// the group, for example by processing it and reading the proposals from
97    /// the resulting [`StagedCommit`].
98    ///
99    /// [`AppEphemeralProposal`]: crate::messages::proposals::AppEphemeralProposal
100    /// [`StagedCommit`]: crate::group::StagedCommit
101    pub fn unverified_app_ephemeral_proposals(
102        &self,
103        component_id: crate::component::ComponentId,
104    ) -> Vec<&crate::messages::proposals::AppEphemeralProposal> {
105        use crate::{
106            framing::mls_content_in::FramedContentBodyIn,
107            messages::proposals_in::{ProposalIn, ProposalOrRefIn},
108        };
109
110        let FramedContentBodyIn::Commit(commit) = &self.content.body else {
111            return vec![];
112        };
113        commit
114            .unverified_proposals()
115            .iter()
116            .filter_map(|proposal_or_ref| match proposal_or_ref {
117                ProposalOrRefIn::Proposal(proposal) => match proposal.as_ref() {
118                    ProposalIn::AppEphemeral(app_ephemeral)
119                        if app_ephemeral.component_id() == component_id =>
120                    {
121                        Some(app_ephemeral.as_ref())
122                    }
123                    _ => None,
124                },
125                ProposalOrRefIn::Reference(_) => None,
126            })
127            .collect()
128    }
129}
130
131impl PublicMessageIn {
132    /// Build an [`PublicMessageIn`].
133    pub(crate) fn new(
134        content: FramedContentIn,
135        auth: FramedContentAuthData,
136        membership_tag: Option<MembershipTag>,
137    ) -> Self {
138        Self {
139            content,
140            auth,
141            membership_tag,
142        }
143    }
144
145    /// Returns the [`ContentType`] of the message.
146    pub fn content_type(&self) -> ContentType {
147        self.content.body.content_type()
148    }
149
150    /// Get the sender of this message.
151    pub fn sender(&self) -> &Sender {
152        &self.content.sender
153    }
154
155    #[cfg(test)]
156    pub(crate) fn set_membership_tag(
157        &mut self,
158        provider: &impl openmls_traits::OpenMlsProvider,
159        ciphersuite: Ciphersuite,
160        membership_key: &MembershipKey,
161        serialized_context: &[u8],
162    ) -> Result<(), LibraryError> {
163        let tbs_payload = framed_content_tbs_serialized_detached(
164            ProtocolVersion::default(),
165            WireFormat::PublicMessage,
166            &self.content,
167            &self.content.sender,
168            serialized_context,
169        )
170        .map_err(LibraryError::missing_bound_check)?;
171        let tbm_payload = AuthenticatedContentTbm::new(&tbs_payload, &self.auth)?;
172        let membership_tag =
173            membership_key.tag_message(provider.crypto(), ciphersuite, tbm_payload)?;
174
175        self.membership_tag = Some(membership_tag);
176        Ok(())
177    }
178
179    /// Verify the membership tag of a [`PublicMessage`] sent from a group
180    /// member. Returns `Ok(())` if successful or [`ValidationError`] otherwise.
181    /// Note, that the context must have been set before calling this function.
182    // TODO #133: Include this in the validation
183    pub(crate) fn verify_membership(
184        &self,
185        crypto: &impl openmls_traits::crypto::OpenMlsCrypto,
186        ciphersuite: Ciphersuite,
187        membership_key: &MembershipKey,
188        serialized_context: &[u8],
189    ) -> Result<(), ValidationError> {
190        log::debug!("Verifying membership tag.");
191        log_crypto!(trace, "  Membership key: {:x?}", membership_key);
192        log_crypto!(trace, "  Serialized context: {:x?}", serialized_context);
193        let tbs_payload = framed_content_tbs_serialized_detached(
194            ProtocolVersion::default(),
195            WireFormat::PublicMessage,
196            &self.content,
197            &self.content.sender,
198            serialized_context,
199        )
200        .map_err(LibraryError::missing_bound_check)?;
201        let tbm_payload = AuthenticatedContentTbm::new(&tbs_payload, &self.auth)?;
202        let expected_membership_tag =
203            &membership_key.tag_message(crypto, ciphersuite, tbm_payload)?;
204
205        // Verify the membership tag
206        // https://validation.openmls.tech/#valn1302
207        if let Some(membership_tag) = &self.membership_tag {
208            // TODO #133: make this a constant-time comparison
209            if membership_tag != expected_membership_tag {
210                return Err(ValidationError::InvalidMembershipTag);
211            }
212        } else {
213            return Err(ValidationError::MissingMembershipTag);
214        }
215        Ok(())
216    }
217
218    /// Get the group epoch.
219    pub fn epoch(&self) -> GroupEpoch {
220        self.content.epoch
221    }
222
223    /// Get the [`GroupId`].
224    pub fn group_id(&self) -> &GroupId {
225        &self.content.group_id
226    }
227
228    /// Turn this [`PublicMessageIn`] into a [`VerifiableAuthenticatedContent`].
229    pub(crate) fn into_verifiable_content(
230        self,
231        serialized_context: impl Into<Option<Vec<u8>>>,
232    ) -> VerifiableAuthenticatedContentIn {
233        VerifiableAuthenticatedContentIn::new(
234            WireFormat::PublicMessage,
235            self.content,
236            serialized_context,
237            self.auth,
238        )
239    }
240
241    /// Get the [`MembershipTag`].
242    pub(crate) fn membership_tag(&self) -> Option<&MembershipTag> {
243        self.membership_tag.as_ref()
244    }
245
246    /// Get the [`ConfirmationTag`].
247    pub fn confirmation_tag(&self) -> Option<&ConfirmationTag> {
248        self.auth.confirmation_tag.as_ref()
249    }
250}
251
252#[cfg(test)]
253impl From<PublicMessageIn> for FramedContentTbsIn {
254    fn from(v: PublicMessageIn) -> Self {
255        FramedContentTbsIn {
256            version: ProtocolVersion::default(),
257            wire_format: WireFormat::PublicMessage,
258            content: v.content,
259            serialized_context: None,
260        }
261    }
262}
263
264impl<'a> TryFrom<&'a PublicMessageIn> for InterimTranscriptHashInput<'a> {
265    type Error = &'static str;
266
267    fn try_from(public_message: &'a PublicMessageIn) -> Result<Self, Self::Error> {
268        match public_message.auth.confirmation_tag.as_ref() {
269            Some(confirmation_tag) => Ok(InterimTranscriptHashInput { confirmation_tag }),
270            None => Err("PublicMessage needs to contain a confirmation tag."),
271        }
272    }
273}
274
275impl TlsDeserializeTrait for PublicMessageIn {
276    fn tls_deserialize<R: Read>(bytes: &mut R) -> Result<Self, Error> {
277        let content = FramedContentIn::tls_deserialize(bytes)?;
278        let auth = FramedContentAuthData::deserialize(bytes, content.body.content_type())?;
279        let membership_tag = if content.sender.is_member() {
280            Some(MembershipTag::tls_deserialize(bytes)?)
281        } else {
282            None
283        };
284
285        Ok(PublicMessageIn::new(content, auth, membership_tag))
286    }
287}
288
289impl DeserializeBytes for PublicMessageIn {
290    fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error>
291    where
292        Self: Sized,
293    {
294        let mut bytes_ref = bytes;
295        let message = PublicMessageIn::tls_deserialize(&mut bytes_ref)?;
296        Ok((message, bytes_ref))
297    }
298}
299
300impl Size for PublicMessageIn {
301    #[inline]
302    fn tls_serialized_len(&self) -> usize {
303        self.content.tls_serialized_len()
304            + self.auth.tls_serialized_len()
305            + if let Some(membership_tag) = &self.membership_tag {
306                membership_tag.tls_serialized_len()
307            } else {
308                0
309            }
310    }
311}
312
313impl TlsSerializeTrait for PublicMessageIn {
314    fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, tls_codec::Error> {
315        // Serialize the content, not the TBS.
316        let mut written = self.content.tls_serialize(writer)?;
317        written += self.auth.tls_serialize(writer)?;
318        written += if let Some(membership_tag) = &self.membership_tag {
319            membership_tag.tls_serialize(writer)?
320        } else {
321            0
322        };
323        Ok(written)
324    }
325}
326
327impl From<PublicMessage> for PublicMessageIn {
328    fn from(v: PublicMessage) -> Self {
329        PublicMessageIn {
330            content: v.content.into(),
331            auth: v.auth,
332            membership_tag: v.membership_tag,
333        }
334    }
335}
336
337// The following `From` implementation( breaks abstraction layers and MUST
338// NOT be made available outside of tests or "test-utils".
339#[cfg(any(feature = "test-utils", test))]
340impl From<PublicMessageIn> for PublicMessage {
341    fn from(v: PublicMessageIn) -> Self {
342        PublicMessage {
343            content: v.content.into(),
344            auth: v.auth,
345            membership_tag: v.membership_tag,
346        }
347    }
348}