Repository: openmls/openmls (rust)

Issue:
Wrong sample code in processing section of book
`impl From<MlsMessageIn> for ProtocolMessage` is guarded by test flags but is used in the sample code in the book.

Pull request:
Improve processing
Fixes #1494.

This PR does the following:

 - Fixes and expands the section about message processing in the book
 - Introduces a `TryFrom` conversion from `MlsMessageIn` into `ProtocolMessage`
 - Renames `MlsMessage[In/Out]Body` to `MlsMessageBody[In/Out]` to harmonize it with the naming scheme used by other structs
 - Refactors the tests because the old way to convert `MlsMessageIn` into `ProtocolMessage` is no longer possible

Patch:
diff --git a/book/src/user_manual/processing.md b/book/src/user_manual/processing.md
index de3160ada..7821ee654 100644
--- a/book/src/user_manual/processing.md
+++ b/book/src/user_manual/processing.md
@@ -12,11 +12,30 @@ Incoming messages can be deserialized from byte slices into an `MlsMessageIn`:
 
 If the message is malformed, the function will fail with an error.
 
-## Processing messages
-
-In the next step, the message needs to be processed. If the message was
-encrypted, it will be decrypted automatically. This step performs all syntactic
-and semantic validation checks and verifies the message's signature:
+## Processing messages in groups
+
+In the next step, the message needs to be processed in the context of the
+corresponding group.
+
+`MlsMessageIn` can carry all MLS messages, but only `PrivateMessageIn` and
+`PublicMessageIn` are processed in the context of a group. In OpenMLS these two
+message types are combined into a `ProtocolMessage` `enum`. There are 3 ways to
+extract the messages from an `MlsMessageIn`:
+
+1. `MlsMessageIn.try_into_protocol_message()` returns a `Result<ProtocolMessage, ProtocolMessageError>`
+2. `ProtocolMessage::try_from(m: MlsMessageIn)` returns a `Result<ProtocolMessage, ProtocolMessageError>`
+3. `MlsMessageIn.extract()` returns an `MlsMessageBodyIn` `enum` that has two
+   variants for `PrivateMessageIn` and `PublicMessageIn`
+
+`MlsGroup.process_message()` accepts either a `ProtocolMessage`, a
+`PrivateMessageIn`, or a `PublicMessageIn` and processes the message.
+`ProtocolMessage.group_id()` exposes the group ID that can help the application
+find the right group. 
+
+If the message was encrypted (i.e. if it was a `PrivateMessageIn`), it will be
+decrypted automatically. The processing performs all syntactic and semantic
+validation checks and verifies the message's signature. The function finally
+returns a `ProcessedMessage` object if all checks are successful.
 
 ```rust,no_run,noplayground
 {{#include ../../../openmls/tests/book_code.rs:process_message}}
diff --git a/cli/src/user.rs b/cli/src/user.rs
index dfd864a1f..04b851c4f 100644
--- a/cli/src/user.rs
+++ b/cli/src/user.rs
@@ -473,12 +473,12 @@ impl User {
         for message in self.backend.recv_msgs(self)?.drain(..) {
             log::debug!("Reading message format {:#?} ...", message.wire_format());
             match message.extract() {
-                MlsMessageInBody::Welcome(welcome) => {
+                MlsMessageBodyIn::Welcome(welcome) => {
                     // Join the group. (Later we should ask the user to
                     // approve first ...)
                     self.join_group(welcome)?;
                 }
-                MlsMessageInBody::PrivateMessage(message) => {
+                MlsMessageBodyIn::PrivateMessage(message) => {
                     match process_protocol_message(message.into()) {
                         Ok(p) => {
                             if p.0 == PostUpdateActions::Remove {
@@ -500,7 +500,7 @@ impl User {
                         }
                     };
                 }
-                MlsMessageInBody::PublicMessage(message) => {
+                MlsMessageBodyIn::PublicMessage(message) => {
                     if process_protocol_message(message.into()).is_err() {
                         continue;
                     }
diff --git a/delivery-service/ds/src/main.rs b/delivery-service/ds/src/main.rs
index c5e17c39e..d5734f265 100644
--- a/delivery-service/ds/src/main.rs
+++ b/delivery-service/ds/src/main.rs
@@ -276,7 +276,7 @@ async fn msg_send(mut body: Payload, data: web::Data<DsData>) -> impl Responder
     let mut clients = unwrap_data!(data.clients.lock());
     let mut groups = unwrap_data!(data.groups.lock());
 
-    let protocol_msg: ProtocolMessage = group_msg.msg.clone().into();
+    let protocol_msg: ProtocolMessage = group_msg.msg.clone().try_into().unwrap();
 
     // Reject any handshake message that has an earlier epoch than the one we know
     // about.
diff --git a/interop_client/src/main.rs b/interop_client/src/main.rs
index 827458617..f61f8175e 100644
--- a/interop_client/src/main.rs
+++ b/interop_client/src/main.rs
@@ -15,7 +15,7 @@ use mls_interop_proto::mls_client;
 use openmls::{
     ciphersuite::HpkePrivateKey,
     credentials::{Credential, CredentialType, CredentialWithKey},
-    framing::{MlsMessageIn, MlsMessageInBody, MlsMessageOut, ProcessedMessageContent},
+    framing::{MlsMessageBodyIn, MlsMessageIn, MlsMessageOut, ProcessedMessageContent},
     group::{
         GroupEpoch, GroupId, MlsGroup, MlsGroupCreateConfig, MlsGroupJoinConfig, WireFormatPolicy,
         PURE_CIPHERTEXT_WIRE_FORMAT_POLICY, PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
@@ -489,8 +489,8 @@ impl MlsClient for MlsClientImpl {
                     MlsMessageIn::tls_deserialize(&mut request.group_info.as_slice()).unwrap();
 
                 match msg.extract() {
-                    MlsMessageInBody::GroupInfo(verifiable_group_info) => verifiable_group_info,
-                    other => panic!("Expected `MlsMessageInBody::GroupInfo`, got {other:?}."),
+                    MlsMessageBodyIn::GroupInfo(verifiable_group_info) => verifiable_group_info,
+                    other => panic!("Expected `MlsMessageBodyIn::GroupInfo`, got {other:?}."),
                 }
             };
             debug!("Got `VerifiableGroupInfo`.");
@@ -695,7 +695,10 @@ impl MlsClient for MlsClientImpl {
         debug!("Processing message.");
         let processed_message = interop_group
             .group
-            .process_message(&interop_group.crypto_provider, message)
+            .process_message(
+                &interop_group.crypto_provider,
+                message.try_into_protocol_message().unwrap(),
+            )
             .map_err(into_status)?;
         debug!("Processed.");
         trace!(?processed_message);
@@ -994,7 +997,10 @@ impl MlsClient for MlsClientImpl {
             }
             trace!("Processing proposal ...");
             let processed_message = group
-                .process_message(&interop_group.crypto_provider, message)
+                .process_message(
+                    &interop_group.crypto_provider,
+                    message.try_into_protocol_message().unwrap(),
+                )
                 .map_err(into_status)?;
             trace!("... done");
 
@@ -1173,7 +1179,10 @@ impl MlsClient for MlsClientImpl {
             }
             trace!("   processing proposal ...");
             let processed_message = group
-                .process_message(&interop_group.crypto_provider, message)
+                .process_message(
+                    &interop_group.crypto_provider,
+                    message.try_into_protocol_message().unwrap(),
+                )
                 .map_err(into_status)?;
             trace!("       done");
             match processed_message.into_content() {
@@ -1197,7 +1206,10 @@ impl MlsClient for MlsClientImpl {
 
         debug!("Processing message.");
         let processed_message = group
-            .process_message(&interop_group.crypto_provider, message)
+            .process_message(
+                &interop_group.crypto_provider,
+                message.try_into_protocol_message().unwrap(),
+            )
             .map_err(into_status)?;
         debug!("Processed.");
         trace!(?processed_message);
diff --git a/openmls/src/framing/codec.rs b/openmls/src/framing/codec.rs
index 9f3d67dba..d8fcde1b5 100644
--- a/openmls/src/framing/codec.rs
+++ b/openmls/src/framing/codec.rs
@@ -63,10 +63,10 @@ pub(super) fn deserialize_ciphertext_content<R: Read>(
 impl Deserialize for MlsMessageIn {
     fn tls_deserialize<R: Read>(bytes: &mut R) -> Result<Self, tls_codec::Error> {
         let version = ProtocolVersion::tls_deserialize(bytes)?;
-        let body = MlsMessageInBody::tls_deserialize(bytes)?;
+        let body = MlsMessageBodyIn::tls_deserialize(bytes)?;
 
         // KeyPackage version must match MlsMessage version.
-        if let MlsMessageInBody::KeyPackage(key_package) = &body {
+        if let MlsMessageBodyIn::KeyPackage(key_package) = &body {
             if !key_package.version_is_supported(version) {
                 return Err(tls_codec::Error::DecodingError(
                     "KeyPackage version does not match MlsMessage version.".into(),
diff --git a/openmls/src/framing/errors.rs b/openmls/src/framing/errors.rs
index 55f5683cc..0b3b43d9d 100644
--- a/openmls/src/framing/errors.rs
+++ b/openmls/src/framing/errors.rs
@@ -77,3 +77,11 @@ pub enum MlsMessageError {
     #[error("The message (or one of its parts) is too large to be encoded.")]
     UnableToEncode,
 }
+
+/// ProtocolMessage error
+#[derive(Error, Debug, Clone)]
+pub enum ProtocolMessageError {
+    /// Wrong wire format
+    #[error("Wrong wire format")]
+    WrongWireFormat,
+}
diff --git a/openmls/src/framing/message_in.rs b/openmls/src/framing/message_in.rs
index caced3c3a..a0be9bdef 100644
--- a/openmls/src/framing/message_in.rs
+++ b/openmls/src/framing/message_in.rs
@@ -21,7 +21,7 @@ use crate::{
 };
 
 /// Before use with the [`MlsGroup`] API, the message has to be unpacked via
-/// `extract` to yield its [`MlsMessageInBody`].
+/// `extract` to yield its [`MlsMessageBodyIn`].
 ///
 /// ```c
 /// // draft-ietf-mls-protocol-17
@@ -38,12 +38,12 @@ use crate::{
 #[cfg_attr(feature = "test-utils", derive(TlsSerialize))]
 pub struct MlsMessageIn {
     pub(crate) version: ProtocolVersion,
-    pub(crate) body: MlsMessageInBody,
+    pub(crate) body: MlsMessageBodyIn,
 }
 
 /// MLSMessage (Body)
 ///
-/// Note: Because [`MlsMessageInBody`] already discriminates between
+/// Note: Because [`MlsMessageBodyIn`] already discriminates between
 /// `public_message`, `private_message`, etc., we don't use the `wire_format`
 /// field. This prevents inconsistent assignments where `wire_format`
 /// contradicts the variant given in `body`.
@@ -71,7 +71,7 @@ pub struct MlsMessageIn {
 #[derive(Debug, PartialEq, Clone, TlsDeserialize, TlsDeserializeBytes, TlsSize)]
 #[cfg_attr(feature = "test-utils", derive(TlsSerialize))]
 #[repr(u16)]
-pub enum MlsMessageInBody {
+pub enum MlsMessageBodyIn {
     /// Plaintext message
     #[tls_codec(discriminant = 1)]
     PublicMessage(PublicMessageIn),
@@ -97,24 +97,29 @@ impl MlsMessageIn {
     /// Returns the wire format.
     pub fn wire_format(&self) -> WireFormat {
         match self.body {
-            MlsMessageInBody::PrivateMessage(_) => WireFormat::PrivateMessage,
-            MlsMessageInBody::PublicMessage(_) => WireFormat::PublicMessage,
-            MlsMessageInBody::Welcome(_) => WireFormat::Welcome,
-            MlsMessageInBody::GroupInfo(_) => WireFormat::GroupInfo,
-            MlsMessageInBody::KeyPackage(_) => WireFormat::KeyPackage,
+            MlsMessageBodyIn::PrivateMessage(_) => WireFormat::PrivateMessage,
+            MlsMessageBodyIn::PublicMessage(_) => WireFormat::PublicMessage,
+            MlsMessageBodyIn::Welcome(_) => WireFormat::Welcome,
+            MlsMessageBodyIn::GroupInfo(_) => WireFormat::GroupInfo,
+            MlsMessageBodyIn::KeyPackage(_) => WireFormat::KeyPackage,
         }
     }
 
     /// Extract the content of an [`MlsMessageIn`] after deserialization for use
     /// with the [`MlsGroup`] API.
-    pub fn extract(self) -> MlsMessageInBody {
+    pub fn extract(self) -> MlsMessageBodyIn {
         self.body
     }
 
+    /// Try to convert the message into a [`ProtocolMessage`].
+    pub fn try_into_protocol_message(self) -> Result<ProtocolMessage, ProtocolMessageError> {
+        self.try_into()
+    }
+
     #[cfg(any(test, feature = "test-utils"))]
     pub fn into_keypackage(self) -> Option<crate::key_packages::KeyPackage> {
         match self.body {
-            MlsMessageInBody::KeyPackage(key_package) => {
+            MlsMessageBodyIn::KeyPackage(key_package) => {
                 debug_assert!(key_package.version_is_supported(self.version));
                 Some(key_package.into())
             }
@@ -125,7 +130,7 @@ impl MlsMessageIn {
     #[cfg(test)]
     pub(crate) fn into_plaintext(self) -> Option<PublicMessage> {
         match self.body {
-            MlsMessageInBody::PublicMessage(m) => Some(m.into()),
+            MlsMessageBodyIn::PublicMessage(m) => Some(m.into()),
             _ => None,
         }
     }
@@ -133,7 +138,7 @@ impl MlsMessageIn {
     #[cfg(test)]
     pub(crate) fn into_ciphertext(self) -> Option<PrivateMessageIn> {
         match self.body {
-            MlsMessageInBody::PrivateMessage(m) => Some(m),
+            MlsMessageBodyIn::PrivateMessage(m) => Some(m),
             _ => None,
         }
     }
@@ -144,7 +149,7 @@ impl MlsMessageIn {
     #[cfg(any(feature = "test-utils", test))]
     pub fn into_welcome(self) -> Option<Welcome> {
         match self.body {
-            MlsMessageInBody::Welcome(w) => Some(w),
+            MlsMessageBodyIn::Welcome(w) => Some(w),
             _ => None,
         }
     }
@@ -152,8 +157,8 @@ impl MlsMessageIn {
     #[cfg(any(feature = "test-utils", test))]
     pub fn into_protocol_message(self) -> Option<ProtocolMessage> {
         match self.body {
-            MlsMessageInBody::PublicMessage(m) => Some(m.into()),
-            MlsMessageInBody::PrivateMessage(m) => Some(m.into()),
+            MlsMessageBodyIn::PublicMessage(m) => Some(m.into()),
+            MlsMessageBodyIn::PrivateMessage(m) => Some(m.into()),
             _ => None,
         }
     }
@@ -161,7 +166,7 @@ impl MlsMessageIn {
     #[cfg(any(feature = "test-utils", test))]
     pub fn into_verifiable_group_info(self) -> Option<VerifiableGroupInfo> {
         match self.body {
-            MlsMessageInBody::GroupInfo(group_info) => Some(group_info),
+            MlsMessageBodyIn::GroupInfo(group_info) => Some(group_info),
             _ => None,
         }
     }
@@ -243,13 +248,14 @@ impl From<PublicMessageIn> for ProtocolMessage {
     }
 }
 
-#[cfg(any(feature = "test-utils", test))]
-impl From<MlsMessageIn> for ProtocolMessage {
-    fn from(msg: MlsMessageIn) -> Self {
+impl TryFrom<MlsMessageIn> for ProtocolMessage {
+    type Error = ProtocolMessageError;
+
+    fn try_from(msg: MlsMessageIn) -> Result<Self, Self::Error> {
         match msg.body {
-            MlsMessageInBody::PublicMessage(m) => ProtocolMessage::PublicMessage(m),
-            MlsMessageInBody::PrivateMessage(m) => ProtocolMessage::PrivateMessage(m),
-            _ => panic!("Wrong message type"),
+            MlsMessageBodyIn::PublicMessage(m) => Ok(ProtocolMessage::PublicMessage(m)),
+            MlsMessageBodyIn::PrivateMessage(m) => Ok(ProtocolMessage::PrivateMessage(m)),
+            _ => Err(ProtocolMessageError::WrongWireFormat),
         }
     }
 }
diff --git a/openmls/src/framing/message_out.rs b/openmls/src/framing/message_out.rs
index 918096ba3..a6af5cd03 100644
--- a/openmls/src/framing/message_out.rs
+++ b/openmls/src/framing/message_out.rs
@@ -21,12 +21,12 @@ use crate::messages::group_info::VerifiableGroupInfo;
 #[derive(Debug, Clone, PartialEq, TlsSerialize, TlsSize)]
 pub struct MlsMessageOut {
     pub(crate) version: ProtocolVersion,
-    pub(crate) body: MlsMessageOutBody,
+    pub(crate) body: MlsMessageBodyOut,
 }
 
 /// MLSMessage (Body)
 ///
-/// Note: Because [MlsMessageOutBody] already discriminates between
+/// Note: Because [MlsMessageBodyOut] already discriminates between
 /// `public_message`, `private_message`, etc., we don't use the
 /// `wire_format` field. This prevents inconsistent assignments
 /// where `wire_format` contradicts the variant given in `body`.
@@ -53,7 +53,7 @@ pub struct MlsMessageOut {
 /// ```
 #[derive(Debug, PartialEq, Clone, TlsSerialize, TlsSize)]
 #[repr(u16)]
-pub(crate) enum MlsMessageOutBody {
+pub(crate) enum MlsMessageBodyOut {
     /// Plaintext message
     #[tls_codec(discriminant = 1)]
     PublicMessage(PublicMessage),
@@ -82,7 +82,7 @@ impl From<PublicMessage> for MlsMessageOut {
             // TODO #34: The version should be set explicitly here instead of
             // the default.
             version: ProtocolVersion::default(),
-            body: MlsMessageOutBody::PublicMessage(public_message),
+            body: MlsMessageBodyOut::PublicMessage(public_message),
         }
     }
 }
@@ -93,7 +93,7 @@ impl From<PrivateMessage> for MlsMessageOut {
             // TODO #34: The version should be set explicitly here instead of
             // the default.
             version: ProtocolVersion::default(),
-            body: MlsMessageOutBody::PrivateMessage(private_message),
+            body: MlsMessageBodyOut::PrivateMessage(private_message),
         }
     }
 }
@@ -102,7 +102,7 @@ impl From<GroupInfo> for MlsMessageOut {
     fn from(group_info: GroupInfo) -> Self {
         Self {
             version: group_info.group_context().protocol_version(),
-            body: MlsMessageOutBody::GroupInfo(group_info),
+            body: MlsMessageBodyOut::GroupInfo(group_info),
         }
     }
 }
@@ -111,7 +111,7 @@ impl From<KeyPackage> for MlsMessageOut {
     fn from(key_package: KeyPackage) -> Self {
         Self {
             version: key_package.protocol_version(),
-            body: MlsMessageOutBody::KeyPackage(key_package),
+            body: MlsMessageBodyOut::KeyPackage(key_package),
         }
     }
 }
@@ -125,7 +125,7 @@ impl MlsMessageOut {
     ) -> Self {
         Self {
             version,
-            body: MlsMessageOutBody::PrivateMessage(private_message),
+            body: MlsMessageBodyOut::PrivateMessage(private_message),
         }
     }
 
@@ -134,7 +134,7 @@ impl MlsMessageOut {
     pub fn from_welcome(welcome: Welcome, version: ProtocolVersion) -> Self {
         MlsMessageOut {
             version,
-            body: MlsMessageOutBody::Welcome(welcome),
+            body: MlsMessageBodyOut::Welcome(welcome),
         }
     }
 
@@ -153,7 +153,7 @@ impl MlsMessageOut {
     #[cfg(any(feature = "test-utils", test))]
     pub fn into_welcome(self) -> Option<Welcome> {
         match self.body {
-            MlsMessageOutBody::Welcome(w) => Some(w),
+            MlsMessageBodyOut::Welcome(w) => Some(w),
             _ => None,
         }
     }
@@ -163,8 +163,8 @@ impl MlsMessageOut {
         let mls_message_in: MlsMessageIn = self.into();
 
         match mls_message_in.extract() {
-            MlsMessageInBody::PublicMessage(pm) => Some(pm.into()),
-            MlsMessageInBody::PrivateMessage(pm) => Some(pm.into()),
+            MlsMessageBodyIn::PublicMessage(pm) => Some(pm.into()),
+            MlsMessageBodyIn::PrivateMessage(pm) => Some(pm.into()),
             _ => None,
         }
     }
@@ -172,7 +172,7 @@ impl MlsMessageOut {
     #[cfg(any(feature = "test-utils", test))]
     pub fn into_verifiable_group_info(self) -> Option<VerifiableGroupInfo> {
         match self.body {
-            MlsMessageOutBody::GroupInfo(group_info) => {
+            MlsMessageBodyOut::GroupInfo(group_info) => {
                 Some(group_info.into_verifiable_group_info())
             }
             _ => None,
@@ -188,11 +188,11 @@ impl From<MlsMessageIn> for MlsMessageOut {
     fn from(mls_message: MlsMessageIn) -> Self {
         let version = mls_message.version;
         let body = match mls_message.body {
-            MlsMessageInBody::Welcome(w) => MlsMessageOutBody::Welcome(w),
-            MlsMessageInBody::GroupInfo(gi) => MlsMessageOutBody::GroupInfo(gi.into()),
-            MlsMessageInBody::KeyPackage(kp) => MlsMessageOutBody::KeyPackage(kp.into()),
-            MlsMessageInBody::PublicMessage(pm) => MlsMessageOutBody::PublicMessage(pm.into()),
-            MlsMessageInBody::PrivateMessage(pm) => MlsMessageOutBody::PrivateMessage(pm.into()),
+            MlsMessageBodyIn::Welcome(w) => MlsMessageBodyOut::Welcome(w),
+            MlsMessageBodyIn::GroupInfo(gi) => MlsMessageBodyOut::GroupInfo(gi.into()),
+            MlsMessageBodyIn::KeyPackage(kp) => MlsMessageBodyOut::KeyPackage(kp.into()),
+            MlsMessageBodyIn::PublicMessage(pm) => MlsMessageBodyOut::PublicMessage(pm.into()),
+            MlsMessageBodyIn::PrivateMessage(pm) => MlsMessageBodyOut::PrivateMessage(pm.into()),
         };
         Self { version, body }
     }
@@ -203,13 +203,13 @@ impl From<MlsMessageOut> for MlsMessageIn {
     fn from(mls_message_out: MlsMessageOut) -> Self {
         let version = mls_message_out.version;
         let body = match mls_message_out.body {
-            MlsMessageOutBody::PublicMessage(pm) => MlsMessageInBody::PublicMessage(pm.into()),
-            MlsMessageOutBody::PrivateMessage(pm) => MlsMessageInBody::PrivateMessage(pm.into()),
-            MlsMessageOutBody::Welcome(w) => MlsMessageInBody::Welcome(w),
-            MlsMessageOutBody::GroupInfo(gi) => {
-                MlsMessageInBody::GroupInfo(gi.into_verifiable_group_info())
+            MlsMessageBodyOut::PublicMessage(pm) => MlsMessageBodyIn::PublicMessage(pm.into()),
+            MlsMessageBodyOut::PrivateMessage(pm) => MlsMessageBodyIn::PrivateMessage(pm.into()),
+            MlsMessageBodyOut::Welcome(w) => MlsMessageBodyIn::Welcome(w),
+            MlsMessageBodyOut::GroupInfo(gi) => {
+                MlsMessageBodyIn::GroupInfo(gi.into_verifiable_group_info())
             }
-            MlsMessageOutBody::KeyPackage(kp) => MlsMessageInBody::KeyPackage(kp.into()),
+            MlsMessageBodyOut::KeyPackage(kp) => MlsMessageBodyIn::KeyPackage(kp.into()),
         };
         Self { version, body }
     }
diff --git a/openmls/src/group/core_group/kat_passive_client.rs b/openmls/src/group/core_group/kat_passive_client.rs
index b0a9d495b..38fbdec70 100644
--- a/openmls/src/group/core_group/kat_passive_client.rs
+++ b/openmls/src/group/core_group/kat_passive_client.rs
@@ -5,7 +5,7 @@ use serde::{self, Deserialize, Serialize};
 use tls_codec::{Deserialize as TlsDeserialize, Serialize as TlsSerialize};
 
 use crate::{
-    framing::{MlsMessageIn, MlsMessageInBody, MlsMessageOut, ProcessedMessageContent},
+    framing::{MlsMessageBodyIn, MlsMessageIn, MlsMessageOut, ProcessedMessageContent},
     group::{config::CryptoConfig, *},
     key_packages::*,
     schedule::psk::PreSharedKeyId,
@@ -242,7 +242,7 @@ impl PassiveClient {
             let mls_message_key_package = MlsMessageIn::tls_deserialize_exact(key_package).unwrap();
 
             match mls_message_key_package.body {
-                MlsMessageInBody::KeyPackage(key_package) => key_package.into(),
+                MlsMessageBodyIn::KeyPackage(key_package) => key_package.into(),
                 _ => panic!(),
             }
         };
diff --git a/openmls/src/group/core_group/kat_welcome.rs b/openmls/src/group/core_group/kat_welcome.rs
index 7cc0dc61e..e306fd7ff 100644
--- a/openmls/src/group/core_group/kat_welcome.rs
+++ b/openmls/src/group/core_group/kat_welcome.rs
@@ -27,7 +27,7 @@ use tls_codec::{Deserialize as TlsDeserialize, Serialize as TlsSerialize};
 use crate::{
     binary_tree::{array_representation::TreeSize, LeafNodeIndex},
     ciphersuite::signable::Verifiable,
-    framing::{MlsMessageIn, MlsMessageInBody},
+    framing::{MlsMessageBodyIn, MlsMessageIn},
     group::*,
     key_packages::*,
     messages::*,
@@ -129,7 +129,7 @@ pub fn run_test_vector(test_vector: WelcomeTestVector) -> Result<(), &'static st
             MlsMessageIn::tls_deserialize_exact(test_vector.key_package).unwrap();
 
         match mls_message_key_package.body {
-            MlsMessageInBody::KeyPackage(key_package) => key_package.into(),
+            MlsMessageBodyIn::KeyPackage(key_package) => key_package.into(),
             _ => return Err("Expected MLSMessage.wire_format == mls_key_package."),
         }
     };
@@ -140,7 +140,7 @@ pub fn run_test_vector(test_vector: WelcomeTestVector) -> Result<(), &'static st
         let mls_message_welcome = MlsMessageIn::tls_deserialize_exact(test_vector.welcome).unwrap();
 
         match mls_message_welcome.body {
-            MlsMessageInBody::Welcome(welcome) => welcome,
+            MlsMessageBodyIn::Welcome(welcome) => welcome,
             _ => return Err("Expected MLSMessage.wire_format == mls_welcome."),
         }
     };
diff --git a/openmls/src/lib.rs b/openmls/src/lib.rs
index 3d4452fc0..43b781c59 100644
--- a/openmls/src/lib.rs
+++ b/openmls/src/lib.rs
@@ -116,7 +116,7 @@
 //!
 //! // ... and inspect the message.
 //! let welcome = match mls_message_in.extract() {
-//!    MlsMessageInBody::Welcome(welcome) => welcome,
+//!    MlsMessageBodyIn::Welcome(welcome) => welcome,
 //!    // We know it's a welcome message, so we ignore all other cases.
 //!    _ => unreachable!("Unexpected message type."),
 //! };
diff --git a/openmls/src/prelude.rs b/openmls/src/prelude.rs
index 6c5579797..19521b6b9 100644
--- a/openmls/src/prelude.rs
+++ b/openmls/src/prelude.rs
@@ -23,7 +23,7 @@ pub use crate::extensions::{errors::*, *};
 
 // Framing
 pub use crate::framing::{
-    message_in::{MlsMessageIn, MlsMessageInBody, ProtocolMessage},
+    message_in::{MlsMessageBodyIn, MlsMessageIn, ProtocolMessage},
     message_out::MlsMessageOut,
     sender::Sender,
     validation::{ApplicationMessage, ProcessedMessage, ProcessedMessageContent},


Test patch:
diff --git a/delivery-service/ds/src/test.rs b/delivery-service/ds/src/test.rs
index 5d0ae7616..a79d9efb5 100644
--- a/delivery-service/ds/src/test.rs
+++ b/delivery-service/ds/src/test.rs
@@ -368,8 +368,8 @@ async fn test_group() {
         })
         .expect("Didn't get an MLS application message from the server.");
     let protocol_message: ProtocolMessage = match messages.remove(mls_message).extract() {
-        MlsMessageInBody::PrivateMessage(m) => m.into(),
-        MlsMessageInBody::PublicMessage(m) => m.into(),
+        MlsMessageBodyIn::PrivateMessage(m) => m.into(),
+        MlsMessageBodyIn::PublicMessage(m) => m.into(),
         _ => panic!("This is not an MLS message."),
     };
     assert!(messages.is_empty());
diff --git a/openmls/src/framing/test_framing.rs b/openmls/src/framing/test_framing.rs
index 5cfa572e8..2edf48693 100644
--- a/openmls/src/framing/test_framing.rs
+++ b/openmls/src/framing/test_framing.rs
@@ -740,7 +740,7 @@ fn key_package_version(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider
 
     let message = MlsMessageOut {
         version: ProtocolVersion::Mls10,
-        body: MlsMessageOutBody::KeyPackage(key_package),
+        body: MlsMessageBodyOut::KeyPackage(key_package),
     };
 
     let encoded = message
diff --git a/openmls/src/group/mls_group/test_mls_group.rs b/openmls/src/group/mls_group/test_mls_group.rs
index 55658d3ce..c7361db78 100644
--- a/openmls/src/group/mls_group/test_mls_group.rs
+++ b/openmls/src/group/mls_group/test_mls_group.rs
@@ -288,7 +288,7 @@ fn test_invalid_plaintext(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvi
     // Right now the membership tag is verified first, wihich yields `VerificationError::InvalidMembershipTag`
     // error instead of a `CredentialError:InvalidSignature`.
     let mut msg_invalid_signature = mls_message.clone();
-    if let MlsMessageOutBody::PublicMessage(ref mut pt) = msg_invalid_signature.body {
+    if let MlsMessageBodyOut::PublicMessage(ref mut pt) = msg_invalid_signature.body {
         pt.invalidate_signature()
     };
 
@@ -296,7 +296,7 @@ fn test_invalid_plaintext(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvi
     let mut msg_invalid_sender = mls_message;
     let random_sender = Sender::build_member(LeafNodeIndex::new(987543210));
     match &mut msg_invalid_sender.body {
-        MlsMessageOutBody::PublicMessage(pt) => {
+        MlsMessageBodyOut::PublicMessage(pt) => {
             pt.set_sender(random_sender);
             pt.set_membership_tag(
                 provider.crypto(),
@@ -474,7 +474,7 @@ fn test_verify_staged_commit_credentials(
 
     // further process the deserialized message
     let processed_message = bob_group
-        .process_message(provider, msg_in)
+        .process_message(provider, msg_in.try_into_protocol_message().unwrap())
         .expect("bob failed processing alice's message");
 
     // the processed message must be a staged commit message
@@ -649,7 +649,7 @@ fn test_commit_with_update_path_leaf_node(
 
     // further process the deserialized message
     let processed_message = bob_group
-        .process_message(provider, msg_in)
+        .process_message(provider, msg_in.try_into_protocol_message().unwrap())
         .expect("bob failed processing alice's message");
 
     // the processed message must be a staged commit message
@@ -990,7 +990,12 @@ fn remove_prosposal_by_ref(ciphersuite: Ciphersuite, provider: &impl OpenMlsProv
         .commit_to_pending_proposals(provider, &alice_signer)
         .unwrap();
     let msg = bob_group
-        .process_message(provider, MlsMessageIn::from(commit))
+        .process_message(
+            provider,
+            MlsMessageIn::from(commit)
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .unwrap();
     match msg.into_content() {
         ProcessedMessageContent::StagedCommitMessage(commit) => {
diff --git a/openmls/src/group/tests/external_add_proposal.rs b/openmls/src/group/tests/external_add_proposal.rs
index 6dbc55ae3..de4db5764 100644
--- a/openmls/src/group/tests/external_add_proposal.rs
+++ b/openmls/src/group/tests/external_add_proposal.rs
@@ -143,7 +143,7 @@ fn external_add_proposal_should_succeed(ciphersuite: Ciphersuite, provider: &imp
                 && matches!(msg.content(), FramedContentBody::Proposal(p) if p.proposal_type() == ProposalType::Add)
         };
         assert!(
-            matches!(proposal.body, MlsMessageOutBody::PublicMessage(ref msg) if verify_proposal(msg))
+            matches!(proposal.body, MlsMessageBodyOut::PublicMessage(ref msg) if verify_proposal(msg))
         );
 
         let msg = alice_group
@@ -285,7 +285,7 @@ fn new_member_proposal_sender_should_be_reserved_for_join_proposals(
     )
     .unwrap();
 
-    if let MlsMessageOutBody::PublicMessage(plaintext) = &join_proposal.body {
+    if let MlsMessageBodyOut::PublicMessage(plaintext) = &join_proposal.body {
         // Make sure it's an add proposal...
         assert!(matches!(
             plaintext.content(),
@@ -309,7 +309,7 @@ fn new_member_proposal_sender_should_be_reserved_for_join_proposals(
         .propose_remove_member(provider, &alice_signer, LeafNodeIndex::new(1))
         .map(|(out, _)| MlsMessageIn::from(out))
         .unwrap();
-    if let MlsMessageInBody::PublicMessage(mut plaintext) = remove_proposal.body {
+    if let MlsMessageBodyIn::PublicMessage(mut plaintext) = remove_proposal.body {
         plaintext.set_sender(Sender::NewMemberProposal);
         assert!(matches!(
             bob_group.process_message(provider, plaintext).unwrap_err(),
@@ -325,7 +325,7 @@ fn new_member_proposal_sender_should_be_reserved_for_join_proposals(
         .propose_self_update(provider, &alice_signer, None)
         .map(|(out, _)| MlsMessageIn::from(out))
         .unwrap();
-    if let MlsMessageInBody::PublicMessage(mut plaintext) = update_proposal.body {
+    if let MlsMessageBodyIn::PublicMessage(mut plaintext) = update_proposal.body {
         plaintext.set_sender(Sender::NewMemberProposal);
         assert!(matches!(
             bob_group.process_message(provider, plaintext).unwrap_err(),
diff --git a/openmls/src/group/tests/external_remove_proposal.rs b/openmls/src/group/tests/external_remove_proposal.rs
index e29d3d707..63cb76c9b 100644
--- a/openmls/src/group/tests/external_remove_proposal.rs
+++ b/openmls/src/group/tests/external_remove_proposal.rs
@@ -141,7 +141,12 @@ fn external_remove_proposal_should_remove_member(
 
     // Alice validates the message
     let processed_message = alice_group
-        .process_message(provider, bob_external_remove_proposal)
+        .process_message(
+            provider,
+            bob_external_remove_proposal
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .unwrap();
     // commit the proposal
     let ProcessedMessageContent::ProposalMessage(remove_proposal) =
@@ -167,7 +172,12 @@ fn external_remove_proposal_should_remove_member(
     .unwrap()
     .into();
     let processed_message = alice_group
-        .process_message(provider, invalid_bob_external_remove_proposal)
+        .process_message(
+            provider,
+            invalid_bob_external_remove_proposal
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .unwrap();
     // commit the proposal
     let ProcessedMessageContent::ProposalMessage(remove_proposal) =
@@ -235,7 +245,12 @@ fn external_remove_proposal_should_fail_when_invalid_external_senders_index(
 
     // Alice tries to validate the message and should fail as sender is invalid
     let error = alice_group
-        .process_message(provider, bob_external_remove_proposal)
+        .process_message(
+            provider,
+            bob_external_remove_proposal
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .unwrap_err();
     assert_eq!(
         error,
@@ -293,7 +308,12 @@ fn external_remove_proposal_should_fail_when_invalid_signature(
 
     // Alice tries to validate the message and should fail as sender is invalid
     let error = alice_group
-        .process_message(provider, bob_external_remove_proposal)
+        .process_message(
+            provider,
+            bob_external_remove_proposal
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .unwrap_err();
     assert_eq!(error, ProcessMessageError::InvalidSignature);
 }
@@ -335,7 +355,12 @@ fn external_remove_proposal_should_fail_when_no_external_senders(
 
     // Alice tries to validate the message and should fail as sender is invalid
     let error = alice_group
-        .process_message(provider, bob_external_remove_proposal)
+        .process_message(
+            provider,
+            bob_external_remove_proposal
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .unwrap_err();
     assert_eq!(
         error,
diff --git a/openmls/src/group/tests/test_commit_validation.rs b/openmls/src/group/tests/test_commit_validation.rs
index 2b734e660..c8788e33b 100644
--- a/openmls/src/group/tests/test_commit_validation.rs
+++ b/openmls/src/group/tests/test_commit_validation.rs
@@ -489,7 +489,12 @@ fn test_valsem202(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
 
     // Positive case
     bob_group
-        .process_message(provider, original_update_plaintext)
+        .process_message(
+            provider,
+            original_update_plaintext
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .expect("Unexpected error.");
 }
 
@@ -562,7 +567,12 @@ fn test_valsem203(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
 
     // Positive case
     bob_group
-        .process_message(provider, original_update_plaintext)
+        .process_message(
+            provider,
+            original_update_plaintext
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .expect("Unexpected error.");
 }
 
@@ -681,7 +691,12 @@ fn test_valsem204(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
 
     // Positive case
     bob_group
-        .process_message(provider, original_update_plaintext)
+        .process_message(
+            provider,
+            original_update_plaintext
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .expect("Unexpected error.");
 }
 
@@ -773,7 +788,9 @@ fn test_partial_proposal_commit(ciphersuite: Ciphersuite, provider: &impl OpenMl
         .propose_remove_member(provider, &alice_credential.signer, charlie_index)
         .map(|(out, _)| MlsMessageIn::from(out))
         .unwrap();
-    let proposal_1 = bob_group.process_message(provider, proposal_1).unwrap();
+    let proposal_1 = bob_group
+        .process_message(provider, proposal_1.try_into_protocol_message().unwrap())
+        .unwrap();
     match proposal_1.into_content() {
         ProcessedMessageContent::ProposalMessage(p) => bob_group.store_pending_proposal(*p),
         _ => unreachable!(),
@@ -784,7 +801,9 @@ fn test_partial_proposal_commit(ciphersuite: Ciphersuite, provider: &impl OpenMl
         .propose_self_update(provider, &alice_credential.signer, None)
         .map(|(out, _)| MlsMessageIn::from(out))
         .unwrap();
-    let proposal_2 = bob_group.process_message(provider, proposal_2).unwrap();
+    let proposal_2 = bob_group
+        .process_message(provider, proposal_2.try_into_protocol_message().unwrap())
+        .unwrap();
     match proposal_2.into_content() {
         ProcessedMessageContent::ProposalMessage(p) => bob_group.store_pending_proposal(*p),
         _ => unreachable!(),
diff --git a/openmls/src/group/tests/test_external_commit_validation.rs b/openmls/src/group/tests/test_external_commit_validation.rs
index 8071501eb..5acd81ee5 100644
--- a/openmls/src/group/tests/test_external_commit_validation.rs
+++ b/openmls/src/group/tests/test_external_commit_validation.rs
@@ -740,7 +740,10 @@ fn test_pure_ciphertest(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvide
 
     // Would fail if handshake message processing did not distinguish external messages
     assert!(alice_group
-        .process_message(provider, mls_message_in)
+        .process_message(
+            provider,
+            mls_message_in.try_into_protocol_message().unwrap()
+        )
         .is_ok());
 }
 
diff --git a/openmls/src/group/tests/test_proposal_validation.rs b/openmls/src/group/tests/test_proposal_validation.rs
index 12a4bf698..f3bfbf527 100644
--- a/openmls/src/group/tests/test_proposal_validation.rs
+++ b/openmls/src/group/tests/test_proposal_validation.rs
@@ -26,7 +26,7 @@ use crate::{
         proposals::{AddProposal, Proposal, ProposalOrRef, RemoveProposal, UpdateProposal},
         Commit, Welcome,
     },
-    prelude::MlsMessageInBody,
+    prelude::MlsMessageBodyIn,
     schedule::PreSharedKeyId,
     treesync::{errors::ApplyUpdatePathError, node::leaf_node::Capabilities},
     versions::ProtocolVersion,
@@ -394,7 +394,12 @@ fn test_valsem101a(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
 
     // Positive case
     bob_group
-        .process_message(provider, original_update_plaintext)
+        .process_message(
+            provider,
+            original_update_plaintext
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .expect("Unexpected error.");
 }
 
@@ -546,7 +551,12 @@ fn test_valsem102(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
 
     // Positive case
     bob_group
-        .process_message(provider, original_update_plaintext)
+        .process_message(
+            provider,
+            original_update_plaintext
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .expect("Unexpected error.");
 }
 
@@ -961,7 +971,12 @@ fn test_valsem103_valsem104(ciphersuite: Ciphersuite, provider: &impl OpenMlsPro
 
     // Positive case
     bob_group
-        .process_message(provider, original_update_plaintext)
+        .process_message(
+            provider,
+            original_update_plaintext
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .expect("Unexpected error.");
 }
 
@@ -1307,7 +1322,12 @@ fn test_valsem105(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
 
             // Positive case
             bob_group
-                .process_message(provider, original_update_plaintext)
+                .process_message(
+                    provider,
+                    original_update_plaintext
+                        .try_into_protocol_message()
+                        .unwrap(),
+                )
                 .unwrap();
         }
 
@@ -1409,7 +1429,7 @@ fn test_valsem107(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
             let mls_message_in = MlsMessageIn::from(ref_propose);
 
             let authenticated_content = match mls_message_in.body {
-                MlsMessageInBody::PublicMessage(ref public) => AuthenticatedContent::new(
+                MlsMessageBodyIn::PublicMessage(ref public) => AuthenticatedContent::new(
                     mls_message_in.wire_format(),
                     FramedContent::from(public.content.clone()),
                     public.auth.clone(),
@@ -1582,7 +1602,12 @@ fn test_valsem108(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
 
     // Positive case
     bob_group
-        .process_message(provider, original_update_plaintext)
+        .process_message(
+            provider,
+            original_update_plaintext
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .expect("Unexpected error.");
 }
 
@@ -1644,7 +1669,10 @@ fn test_valsem110(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
 
     // Have Alice process this proposal.
     if let ProcessedMessageContent::ProposalMessage(proposal) = alice_group
-        .process_message(provider, update_proposal)
+        .process_message(
+            provider,
+            update_proposal.try_into_protocol_message().unwrap(),
+        )
         .expect("error processing proposal")
         .into_content()
     {
@@ -1889,7 +1917,12 @@ fn test_valsem111(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
 
     // Positive case
     bob_group
-        .process_message(provider, original_update_plaintext)
+        .process_message(
+            provider,
+            original_update_plaintext
+                .try_into_protocol_message()
+                .unwrap(),
+        )
         .expect("Unexpected error.");
 }
 
diff --git a/openmls/src/group/tests/test_wire_format_policy.rs b/openmls/src/group/tests/test_wire_format_policy.rs
index c1acfa16b..4d1e3614d 100644
--- a/openmls/src/group/tests/test_wire_format_policy.rs
+++ b/openmls/src/group/tests/test_wire_format_policy.rs
@@ -105,7 +105,7 @@ fn test_wire_policy_positive(ciphersuite: Ciphersuite, provider: &impl OpenMlsPr
             &alice_credential_with_key_and_signer.signer,
         );
         alice_group
-            .process_message(provider, message)
+            .process_message(provider, message.try_into_protocol_message().unwrap())
             .expect("An unexpected error occurred.");
     }
 }
@@ -134,7 +134,7 @@ fn test_wire_policy_negative(ciphersuite: Ciphersuite, provider: &impl OpenMlsPr
             &alice_credential_with_key_and_signer.signer,
         );
         let err = alice_group
-            .process_message(provider, message)
+            .process_message(provider, message.try_into_protocol_message().unwrap())
             .expect_err("An unexpected error occurred.");
         assert_eq!(err, ProcessMessageError::IncompatibleWireFormat);
     }
diff --git a/openmls/tests/book_code.rs b/openmls/tests/book_code.rs
index 9bf48cc13..364a80dfd 100644
--- a/openmls/tests/book_code.rs
+++ b/openmls/tests/book_code.rs
@@ -346,7 +346,9 @@ fn book_operations(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
     // ANCHOR_END: mls_message_in_from_bytes
 
     // ANCHOR: process_message
-    let protocol_message: ProtocolMessage = mls_message.into();
+    let protocol_message: ProtocolMessage = mls_message
+        .try_into_protocol_message()
+        .expect("Expected a PublicMessage or a PrivateMessage");
     let processed_message = bob_group
         .process_message(provider, protocol_message)
         .expect("Could not process message.");
diff --git a/openmls/tests/test_external_commit.rs b/openmls/tests/test_external_commit.rs
index 8a0967156..fcf52e56e 100644
--- a/openmls/tests/test_external_commit.rs
+++ b/openmls/tests/test_external_commit.rs
@@ -161,7 +161,7 @@ fn test_group_info(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
 
     // let alice process bob's new client
     let msg = alice_group
-        .process_message(provider, msg)
+        .process_message(provider, msg.try_into_protocol_message().unwrap())
         .unwrap()
         .into_content();
     match msg {
@@ -177,7 +177,9 @@ fn test_group_info(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {
         .unwrap()
         .into();
 
-    let msg = alice_group.process_message(provider, message).unwrap();
+    let msg = alice_group
+        .process_message(provider, message.try_into_protocol_message().unwrap())
+        .unwrap();
     let decrypted = match msg.into_content() {
         ProcessedMessageContent::ApplicationMessage(msg) => msg.into_bytes(),
         _ => panic!("Not an ApplicationMessage"),