diff --git a/Cargo.toml b/Cargo.toml index 1fb9d3444a..453e0a40be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,28 +46,32 @@ git = "https://github.com/wireapp/proteus" branch = "2.x" [patch.crates-io.openmls] -package = "openmls" -git = "https://github.com/wireapp/openmls" +#package = "openmls" +#git = "https://github.com/wireapp/openmls" #tag = "v1.0.0-pre.core-crypto-1.0.0" -branch = "feat/rfc9420" +#branch = "feat/rfc9420" +path = "../openmls/openmls" [patch.crates-io.openmls_traits] -package = "openmls_traits" -git = "https://github.com/wireapp/openmls" +#package = "openmls_traits" +#git = "https://github.com/wireapp/openmls" #tag = "v1.0.0-pre.core-crypto-1.0.0" -branch = "feat/rfc9420" +#branch = "feat/rfc9420" +path = "../openmls/traits" [patch.crates-io.openmls_basic_credential] -package = "openmls_basic_credential" -git = "https://github.com/wireapp/openmls" +#package = "openmls_basic_credential" +#git = "https://github.com/wireapp/openmls" #tag = "v1.0.0-pre.core-crypto-1.0.0" -branch = "feat/rfc9420" +#branch = "feat/rfc9420" +path = "../openmls/basic_credential" [patch.crates-io.openmls_x509_credential] -package = "openmls_x509_credential" -git = "https://github.com/wireapp/openmls" +#package = "openmls_x509_credential" +#git = "https://github.com/wireapp/openmls" #tag = "v1.0.0-pre.core-crypto-1.0.0" -branch = "feat/rfc9420" +#branch = "feat/rfc9420" +path = "../openmls/x509_credential" [patch.crates-io.hpke] git = "https://github.com/wireapp/rust-hpke.git" diff --git a/crypto-ffi/src/generic.rs b/crypto-ffi/src/generic.rs index 74385675f3..eb811662c4 100644 --- a/crypto-ffi/src/generic.rs +++ b/crypto-ffi/src/generic.rs @@ -214,21 +214,39 @@ pub struct MemberAddedMessages { pub welcome: Vec, pub commit: Vec, pub group_info: GroupInfoBundle, + pub crl_new_distribution_points: Option>, } impl TryFrom for MemberAddedMessages { type Error = CoreCryptoError; fn try_from(msg: MlsConversationCreationMessage) -> Result { - let (welcome, commit, group_info) = msg.to_bytes_triple()?; + let (welcome, commit, group_info, crl_new_distribution_points) = msg.to_bytes_triple()?; Ok(Self { welcome, commit, group_info: group_info.into(), + crl_new_distribution_points, }) } } +#[derive(Debug, uniffi::Record)] +/// see [core_crypto::prelude::MlsConversationCreationMessage] +pub struct WelcomeBundle { + pub id: ConversationId, + pub crl_new_distribution_points: Option>, +} + +impl From for WelcomeBundle { + fn from(w: core_crypto::prelude::WelcomeBundle) -> Self { + Self { + id: w.id, + crl_new_distribution_points: w.crl_new_distribution_points, + } + } +} + #[derive(Debug, uniffi::Record)] pub struct CommitBundle { pub welcome: Option>, @@ -372,6 +390,7 @@ pub struct ConversationInitBundle { pub conversation_id: Vec, pub commit: Vec, pub group_info: GroupInfoBundle, + pub crl_new_distribution_points: Option>, } impl TryFrom for ConversationInitBundle { @@ -379,11 +398,12 @@ impl TryFrom for ConversationInitBundle { fn try_from(mut from: MlsConversationInitBundle) -> Result { let conversation_id = std::mem::take(&mut from.conversation_id); - let (commit, gi) = from.to_bytes_pair()?; + let (commit, gi, crl_new_distribution_points) = from.to_bytes_triple()?; Ok(Self { conversation_id, commit, group_info: gi.into(), + crl_new_distribution_points, }) } } @@ -919,13 +939,14 @@ impl CoreCrypto { &self, welcome_message: Vec, custom_configuration: CustomConfiguration, - ) -> CoreCryptoResult> { + ) -> CoreCryptoResult { Ok(self .central .lock() .await .process_raw_welcome_message(welcome_message, custom_configuration.into()) - .await?) + .await? + .into()) } /// See [core_crypto::mls::MlsCentral::add_members_to_conversation] diff --git a/crypto-ffi/src/wasm.rs b/crypto-ffi/src/wasm.rs index 91ffd9cb8d..7296462357 100644 --- a/crypto-ffi/src/wasm.rs +++ b/crypto-ffi/src/wasm.rs @@ -496,6 +496,7 @@ pub struct ConversationInitBundle { conversation_id: ConversationId, commit: Vec, group_info: GroupInfoBundle, + crl_new_distribution_points: Option>, } #[wasm_bindgen] @@ -514,6 +515,13 @@ impl ConversationInitBundle { pub fn group_info(&self) -> GroupInfoBundle { self.group_info.clone() } + + #[wasm_bindgen(getter)] + pub fn crl_new_distribution_points(&self) -> Option { + self.crl_new_distribution_points + .clone() + .map(|crl_dp| crl_dp.iter().cloned().map(JsValue::from).collect::()) + } } impl TryFrom for ConversationInitBundle { @@ -530,6 +538,7 @@ impl TryFrom for ConversationInitBundle { conversation_id, commit, group_info: pgs.into(), + crl_new_distribution_points: from.crl_new_distribution_points, }) } } @@ -653,6 +662,38 @@ impl DecryptedMessage { } } +#[wasm_bindgen] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +/// see [core_crypto::prelude::WelcomeBundle] +pub struct WelcomeBundle { + id: ConversationId, + crl_new_distribution_points: Option>, +} + +impl From for WelcomeBundle { + fn from(w: core_crypto::prelude::WelcomeBundle) -> Self { + Self { + id: w.id, + crl_new_distribution_points: w.crl_new_distribution_points, + } + } +} + +#[wasm_bindgen] +impl WelcomeBundle { + #[wasm_bindgen(getter)] + pub fn id(&self) -> Uint8Array { + Uint8Array::from(&*self.id) + } + + #[wasm_bindgen(getter)] + pub fn crl_new_distribution_points(&self) -> Option { + self.crl_new_distribution_points + .clone() + .map(|crl_dp| crl_dp.iter().cloned().map(JsValue::from).collect::()) + } +} + #[wasm_bindgen] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] /// to avoid recursion @@ -1470,13 +1511,14 @@ impl CoreCrypto { let this = self.inner.clone(); future_to_promise( async move { - let conversation_id = this + let bundle = this .write() .await .process_raw_welcome_message(welcome_message.into(), custom_configuration.into()) .await .map_err(CoreCryptoError::from)?; - WasmCryptoResult::Ok(Uint8Array::from(conversation_id.as_slice()).into()) + let bundle: WelcomeBundle = bundle.into(); + WasmCryptoResult::Ok(serde_wasm_bindgen::to_value(&bundle)?) } .err_into(), ) diff --git a/crypto/src/e2e_identity/rotate.rs b/crypto/src/e2e_identity/rotate.rs index 04920cfa45..49b4a95717 100644 --- a/crypto/src/e2e_identity/rotate.rs +++ b/crypto/src/e2e_identity/rotate.rs @@ -15,52 +15,6 @@ use crate::{ MlsError, }; -/// Result returned after rotating the Credential of the current client in all the local conversations -#[derive(Debug, Clone)] -pub struct MlsRotateBundle { - /// An Update commit for each conversation - pub commits: HashMap, - /// Fresh KeyPackages with the new Credential - pub new_key_packages: Vec, - /// All the now deprecated KeyPackages. Once deleted remotely, delete them locally with [MlsCentral::delete_keypackages] - pub key_package_refs_to_remove: Vec, -} - -impl MlsRotateBundle { - /// Lower through the FFI - #[allow(clippy::type_complexity)] - pub fn to_bytes(self) -> CryptoResult<(HashMap, Vec>, Vec>)> { - use openmls::prelude::TlsSerializeTrait as _; - - let commits_size = self.commits.len(); - let commits = self - .commits - .into_iter() - .try_fold(HashMap::with_capacity(commits_size), |mut acc, (id, c)| { - // because uniffi ONLY supports HashMap - let id = hex::encode(id); - let _ = acc.insert(id, c); - CryptoResult::Ok(acc) - })?; - - let kp_size = self.new_key_packages.len(); - let new_key_packages = - self.new_key_packages - .into_iter() - .try_fold(Vec::with_capacity(kp_size), |mut acc, kp| { - acc.push(kp.tls_serialize_detached().map_err(MlsError::from)?); - CryptoResult::Ok(acc) - })?; - let key_package_refs_to_remove = self - .key_package_refs_to_remove - .into_iter() - // TODO: add a method for taking ownership in HashReference - .map(|r| r.as_slice().to_vec()) - .collect::>(); - Ok((commits, new_key_packages, key_package_refs_to_remove)) - } -} - impl MlsCentral { /// Generates an E2EI enrollment instance for a "regular" client (with a Basic credential) /// willing to migrate to E2EI. As a consequence, this method does not support changing the @@ -159,6 +113,9 @@ impl MlsCentral { signature_scheme: cs.signature_algorithm(), }; + let ee = certificate_chain.get(0).ok_or(CryptoError::InvalidCertificateChain)?; + let crl_new_distribution_points = None; + let cert_bundle = CertificateBundle { certificate_chain, private_key, @@ -184,6 +141,7 @@ impl MlsCentral { commits, new_key_packages, key_package_refs_to_remove, + crl_new_distribution_points, }) } @@ -261,6 +219,66 @@ impl MlsConversation { } } +/// Result returned after rotating the Credential of the current client in all the local conversations +#[derive(Debug, Clone)] +pub struct MlsRotateBundle { + /// An Update commit for each conversation + pub commits: HashMap, + /// Fresh KeyPackages with the new Credential + pub new_key_packages: Vec, + /// All the now deprecated KeyPackages. Once deleted remotely, delete them locally with [MlsCentral::delete_keypackages] + pub key_package_refs_to_remove: Vec, + /// New CRL distribution points that appeared by the introduction of a new credential + pub crl_new_distribution_points: Option>, +} + +impl MlsRotateBundle { + /// Lower through the FFI + #[allow(clippy::type_complexity)] + pub fn to_bytes( + self, + ) -> CryptoResult<( + HashMap, + Vec>, + Vec>, + Option>, + )> { + use openmls::prelude::TlsSerializeTrait as _; + + let commits_size = self.commits.len(); + let commits = self + .commits + .into_iter() + .try_fold(HashMap::with_capacity(commits_size), |mut acc, (id, c)| { + // because uniffi ONLY supports HashMap + let id = hex::encode(id); + let _ = acc.insert(id, c); + CryptoResult::Ok(acc) + })?; + + let kp_size = self.new_key_packages.len(); + let new_key_packages = + self.new_key_packages + .into_iter() + .try_fold(Vec::with_capacity(kp_size), |mut acc, kp| { + acc.push(kp.tls_serialize_detached().map_err(MlsError::from)?); + CryptoResult::Ok(acc) + })?; + let key_package_refs_to_remove = self + .key_package_refs_to_remove + .into_iter() + // TODO: add a method for taking ownership in HashReference + .map(|r| r.as_slice().to_vec()) + .collect::>(); + Ok(( + commits, + new_key_packages, + key_package_refs_to_remove, + self.crl_new_distribution_points, + )) + } +} + #[cfg(test)] pub mod tests { use std::collections::HashSet; diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 076077c042..64053ca8e4 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -83,10 +83,12 @@ pub mod prelude { client::*, config::MlsCentralConfiguration, conversation::{ + commit::{MlsCommitBundle, MlsConversationCreationMessage}, config::{MlsConversationConfiguration, MlsCustomConfiguration, MlsWirePolicy}, decrypt::{MlsBufferedConversationDecryptMessage, MlsConversationDecryptMessage}, group_info::{GroupInfoPayload, MlsGroupInfoBundle, MlsGroupInfoEncryptionType, MlsRatchetTreeType}, - handshake::{MlsCommitBundle, MlsConversationCreationMessage, MlsProposalBundle}, + proposal::MlsProposalBundle, + welcome::WelcomeBundle, *, }, credential::{typ::MlsCredentialType, x509::CertificateBundle}, diff --git a/crypto/src/mls/conversation/handshake.rs b/crypto/src/mls/conversation/commit.rs similarity index 76% rename from crypto/src/mls/conversation/handshake.rs rename to crypto/src/mls/conversation/commit.rs index 9b45f79ca6..3eaa2bb397 100644 --- a/crypto/src/mls/conversation/handshake.rs +++ b/crypto/src/mls/conversation/commit.rs @@ -1,6 +1,3 @@ -//! Handshake refers here to either a commit or proposal message. Overall, it covers all the -//! operation modifying the group state -//! //! This table summarizes when a MLS group can create a commit or proposal: //! //! | can create handshake ? | 0 pend. Commit | 1 pend. Commit | @@ -8,189 +5,126 @@ //! | 0 pend. Proposal | ✅ | ❌ | //! | 1+ pend. Proposal | ✅ | ❌ | -use openmls::prelude::{KeyPackage, KeyPackageIn, LeafNode, LeafNodeIndex, MlsMessageOut}; +use openmls::prelude::{KeyPackageIn, LeafNode, LeafNodeIndex, MlsMessageOut}; use mls_crypto_provider::MlsCryptoProvider; -use crate::mls::credential::CredentialBundle; -use crate::prelude::{ - Client, ClientId, ConversationId, CryptoError, CryptoResult, MlsCentral, MlsError, MlsGroupInfoBundle, - MlsProposalRef, +use crate::{ + mls::credential::{crl::extract_dp, CredentialBundle}, + prelude::{Client, ClientId, ConversationId, CryptoError, CryptoResult, MlsCentral, MlsError, MlsGroupInfoBundle}, }; use super::MlsConversation; -/// Returned when a Proposal is created. Helps roll backing a local proposal -#[derive(Debug)] -pub struct MlsProposalBundle { - /// The proposal message - pub proposal: MlsMessageOut, - /// A unique identifier of the proposal to rollback it later if required - pub proposal_ref: MlsProposalRef, -} - -impl From<(MlsMessageOut, openmls::prelude::hash_ref::ProposalRef)> for MlsProposalBundle { - fn from((proposal, proposal_ref): (MlsMessageOut, openmls::prelude::hash_ref::ProposalRef)) -> Self { - Self { - proposal, - proposal_ref: proposal_ref.into(), - } - } -} - -impl MlsProposalBundle { - /// Serializes both wrapped objects into TLS and return them as a tuple of byte arrays. - /// 0 -> proposal - /// 1 -> proposal reference - pub fn to_bytes_pair(&self) -> CryptoResult<(Vec, Vec)> { - use openmls::prelude::TlsSerializeTrait as _; - let proposal = self.proposal.tls_serialize_detached().map_err(MlsError::from)?; - let proposal_ref = self.proposal_ref.to_bytes(); - - Ok((proposal, proposal_ref)) - } -} - -/// Creating proposals -impl MlsConversation { - /// see [openmls::group::MlsGroup::propose_add_member] - #[cfg_attr(test, crate::durable)] - pub async fn propose_add_member( - &mut self, - client: &Client, - backend: &MlsCryptoProvider, - key_package: KeyPackage, - ) -> CryptoResult { - let signer = &self - .find_current_credential_bundle(client)? - .ok_or(CryptoError::IdentityInitializationError)? - .signature_key; - - let proposal = self - .group - .propose_add_member(backend, signer, key_package.into()) - .map_err(MlsError::from) - .map(MlsProposalBundle::from)?; - self.persist_group_when_changed(backend, false).await?; - Ok(proposal) - } - - /// see [openmls::group::MlsGroup::propose_remove_member] - #[cfg_attr(test, crate::durable)] - pub async fn propose_remove_member( - &mut self, - client: &Client, - backend: &MlsCryptoProvider, - member: LeafNodeIndex, - ) -> CryptoResult { - let signer = &self - .find_current_credential_bundle(client)? - .ok_or(CryptoError::IdentityInitializationError)? - .signature_key; - let proposal = self - .group - .propose_remove_member(backend, signer, member) - .map_err(MlsError::from) - .map_err(CryptoError::from) - .map(MlsProposalBundle::from)?; - self.persist_group_when_changed(backend, false).await?; - Ok(proposal) - } - - /// see [openmls::group::MlsGroup::propose_self_update] - #[cfg_attr(test, crate::durable)] - pub async fn propose_self_update( +impl MlsCentral { + /// Adds new members to the group/conversation + /// + /// # Arguments + /// * `id` - group/conversation id + /// * `members` - members to be added to the group + /// + /// # Return type + /// An optional struct containing a welcome and a message will be returned on successful call. + /// The value will be `None` only if the group can't be found locally (no error will be returned + /// in this case). + /// + /// # Errors + /// If the authorisation callback is set, an error can be caused when the authorization fails. + /// Other errors are KeyStore and OpenMls errors: + #[cfg_attr(test, crate::idempotent)] + pub async fn add_members_to_conversation( &mut self, - client: &Client, - backend: &MlsCryptoProvider, - ) -> CryptoResult { - self.propose_explicit_self_update(client, backend, None).await + id: &ConversationId, + key_packages: Vec, + ) -> CryptoResult { + if let Some(callbacks) = self.callbacks.as_ref() { + let client_id = self.mls_client()?.id().clone(); + if !callbacks.authorize(id.clone(), client_id).await { + return Err(CryptoError::Unauthorized); + } + } + self.get_conversation(id) + .await? + .write() + .await + .add_members(self.mls_client()?, key_packages, &self.mls_backend) + .await } - /// see [openmls::group::MlsGroup::propose_self_update] - #[cfg_attr(test, crate::durable)] - pub async fn propose_explicit_self_update( + /// Removes clients from the group/conversation. + /// + /// # Arguments + /// * `id` - group/conversation id + /// * `clients` - list of client ids to be removed from the group + /// + /// # Return type + /// An struct containing a welcome(optional, will be present only if there's pending add + /// proposals in the store), a message with the commit to fan out to other clients and + /// the group info will be returned on successful call. + /// + /// # Errors + /// If the authorisation callback is set, an error can be caused when the authorization fails. Other errors are KeyStore and OpenMls errors. + #[cfg_attr(test, crate::idempotent)] + pub async fn remove_members_from_conversation( &mut self, - client: &Client, - backend: &MlsCryptoProvider, - leaf_node: Option, - ) -> CryptoResult { - let msg_signer = &self - .find_current_credential_bundle(client)? - .ok_or(CryptoError::IdentityInitializationError)? - .signature_key; - - let proposal = if let Some(leaf_node) = leaf_node { - let leaf_node_signer = &self - .find_most_recent_credential_bundle(client)? - .ok_or(CryptoError::IdentityInitializationError)? - .signature_key; - - self.group - .propose_explicit_self_update(backend, msg_signer, leaf_node, leaf_node_signer) - .await - } else { - self.group.propose_self_update(backend, msg_signer).await + id: &ConversationId, + clients: &[ClientId], + ) -> CryptoResult { + if let Some(callbacks) = self.callbacks.as_ref() { + let client_id = self.mls_client()?.id().clone(); + if !callbacks.authorize(id.clone(), client_id).await { + return Err(CryptoError::Unauthorized); + } } - .map_err(MlsError::from) - .map(MlsProposalBundle::from)?; - - self.persist_group_when_changed(backend, false).await?; - Ok(proposal) + self.get_conversation(id) + .await? + .write() + .await + .remove_members(self.mls_client()?, clients, &self.mls_backend) + .await } -} - -/// Returned when initializing a conversation through a commit. -/// Different from conversation created from a [`Welcome`] message or an external commit. -#[derive(Debug)] -pub struct MlsConversationCreationMessage { - /// A welcome message for new members to join the group - pub welcome: MlsMessageOut, - /// Commit message adding members to the group - pub commit: MlsMessageOut, - /// [`GroupInfo`] (aka GroupInfo) if the commit is merged - pub group_info: MlsGroupInfoBundle, -} -impl MlsConversationCreationMessage { - /// Serializes both wrapped objects into TLS and return them as a tuple of byte arrays. - /// 0 -> welcome - /// 1 -> commit - /// 2 -> group_info - pub fn to_bytes_triple(self) -> CryptoResult<(Vec, Vec, MlsGroupInfoBundle)> { - use openmls::prelude::TlsSerializeTrait as _; - let welcome = self.welcome.tls_serialize_detached().map_err(MlsError::from)?; - let msg = self.commit.tls_serialize_detached().map_err(MlsError::from)?; - Ok((welcome, msg, self.group_info)) + /// Self updates the KeyPackage and automatically commits. Pending proposals will be commited + /// + /// # Arguments + /// * `conversation_id` - the group/conversation id + /// + /// # Return type + /// An struct containing a welcome(optional, will be present only if there's pending add + /// proposals in the store), a message with the commit to fan out to other clients and + /// the group info will be returned on successful call. + /// + /// # Errors + /// If the conversation can't be found, an error will be returned. Other errors are originating + /// from OpenMls and the KeyStore + #[cfg_attr(test, crate::idempotent)] + pub async fn update_keying_material(&mut self, id: &ConversationId) -> CryptoResult { + self.get_conversation(id) + .await? + .write() + .await + .update_keying_material(self.mls_client()?, &self.mls_backend, None, None) + .await } -} - -/// Returned when a commit is created -#[derive(Debug, Clone)] -pub struct MlsCommitBundle { - /// A welcome message if there are pending Add proposals - pub welcome: Option, - /// The commit message - pub commit: MlsMessageOut, - /// [`GroupInfo`] (aka GroupInfo) if the commit is merged - pub group_info: MlsGroupInfoBundle, -} -impl MlsCommitBundle { - /// Serializes both wrapped objects into TLS and return them as a tuple of byte arrays. - /// 0 -> welcome - /// 1 -> message - /// 2 -> public group state - #[allow(clippy::type_complexity)] - pub fn to_bytes_triple(self) -> CryptoResult<(Option>, Vec, MlsGroupInfoBundle)> { - use openmls::prelude::TlsSerializeTrait as _; - let welcome = self - .welcome - .as_ref() - .map(|w| w.tls_serialize_detached().map_err(MlsError::from)) - .transpose()?; - let commit = self.commit.tls_serialize_detached().map_err(MlsError::from)?; - Ok((welcome, commit, self.group_info)) + /// Commits all pending proposals of the group + /// + /// # Arguments + /// * `backend` - the KeyStore to persist group changes + /// + /// # Return type + /// A tuple containing the commit message and a possible welcome (in the case `Add` proposals were pending within the internal MLS Group) + /// + /// # Errors + /// Errors can be originating from the KeyStore and OpenMls + #[cfg_attr(test, crate::idempotent)] + pub async fn commit_pending_proposals(&mut self, id: &ConversationId) -> CryptoResult> { + self.get_conversation(id) + .await? + .write() + .await + .commit_pending_proposals(self.mls_client()?, &self.mls_backend) + .await } } @@ -210,6 +144,23 @@ impl MlsConversation { .ok_or(CryptoError::IdentityInitializationError)? .signature_key; + // No need to also check pending proposals since they should already have been scanned while decrypting the proposal message + let crl_new_distribution_points = key_packages + .iter() + .filter_map(|kp| match kp.credential().mls_credential() { + openmls::prelude::MlsCredentialType::X509(cert) => Some(cert), + _ => None, + }) + .try_fold(vec![], |mut acc, c| { + acc.extend(extract_dp(c)?); + CryptoResult::Ok(acc) + })?; + let crl_new_distribution_points = if crl_new_distribution_points.is_empty() { + None + } else { + Some(crl_new_distribution_points) + }; + let (commit, welcome, gi) = self .group .add_members(backend, signer, key_packages) @@ -226,6 +177,7 @@ impl MlsConversation { welcome, commit, group_info, + crl_new_distribution_points, }) } @@ -340,115 +292,59 @@ impl MlsConversation { } } -impl MlsCentral { - /// Adds new members to the group/conversation - /// - /// # Arguments - /// * `id` - group/conversation id - /// * `members` - members to be added to the group - /// - /// # Return type - /// An optional struct containing a welcome and a message will be returned on successful call. - /// The value will be `None` only if the group can't be found locally (no error will be returned - /// in this case). - /// - /// # Errors - /// If the authorisation callback is set, an error can be caused when the authorization fails. - /// Other errors are KeyStore and OpenMls errors: - #[cfg_attr(test, crate::idempotent)] - pub async fn add_members_to_conversation( - &mut self, - id: &ConversationId, - key_packages: Vec, - ) -> CryptoResult { - if let Some(callbacks) = self.callbacks.as_ref() { - let client_id = self.mls_client()?.id().clone(); - if !callbacks.authorize(id.clone(), client_id).await { - return Err(CryptoError::Unauthorized); - } - } - self.get_conversation(id) - .await? - .write() - .await - .add_members(self.mls_client()?, key_packages, &self.mls_backend) - .await - } +/// Returned when initializing a conversation through a commit. +/// Different from conversation created from a [`Welcome`] message or an external commit. +#[derive(Debug)] +pub struct MlsConversationCreationMessage { + /// A welcome message for new members to join the group + pub welcome: MlsMessageOut, + /// Commit message adding members to the group + pub commit: MlsMessageOut, + /// [`GroupInfo`] (aka GroupInfo) if the commit is merged + pub group_info: MlsGroupInfoBundle, + /// New CRL distribution points that appeared by the introduction of a new credential + pub crl_new_distribution_points: Option>, +} - /// Removes clients from the group/conversation. - /// - /// # Arguments - /// * `id` - group/conversation id - /// * `clients` - list of client ids to be removed from the group - /// - /// # Return type - /// An struct containing a welcome(optional, will be present only if there's pending add - /// proposals in the store), a message with the commit to fan out to other clients and - /// the group info will be returned on successful call. - /// - /// # Errors - /// If the authorisation callback is set, an error can be caused when the authorization fails. Other errors are KeyStore and OpenMls errors. - #[cfg_attr(test, crate::idempotent)] - pub async fn remove_members_from_conversation( - &mut self, - id: &ConversationId, - clients: &[ClientId], - ) -> CryptoResult { - if let Some(callbacks) = self.callbacks.as_ref() { - let client_id = self.mls_client()?.id().clone(); - if !callbacks.authorize(id.clone(), client_id).await { - return Err(CryptoError::Unauthorized); - } - } - self.get_conversation(id) - .await? - .write() - .await - .remove_members(self.mls_client()?, clients, &self.mls_backend) - .await +impl MlsConversationCreationMessage { + /// Serializes both wrapped objects into TLS and return them as a tuple of byte arrays. + /// 0 -> welcome + /// 1 -> commit + /// 2 -> group_info + pub fn to_bytes_triple(self) -> CryptoResult<(Vec, Vec, MlsGroupInfoBundle, Option>)> { + use openmls::prelude::TlsSerializeTrait as _; + let welcome = self.welcome.tls_serialize_detached().map_err(MlsError::from)?; + let msg = self.commit.tls_serialize_detached().map_err(MlsError::from)?; + Ok((welcome, msg, self.group_info, self.crl_new_distribution_points)) } +} - /// Self updates the KeyPackage and automatically commits. Pending proposals will be commited - /// - /// # Arguments - /// * `conversation_id` - the group/conversation id - /// - /// # Return type - /// An struct containing a welcome(optional, will be present only if there's pending add - /// proposals in the store), a message with the commit to fan out to other clients and - /// the group info will be returned on successful call. - /// - /// # Errors - /// If the conversation can't be found, an error will be returned. Other errors are originating - /// from OpenMls and the KeyStore - #[cfg_attr(test, crate::idempotent)] - pub async fn update_keying_material(&mut self, id: &ConversationId) -> CryptoResult { - self.get_conversation(id) - .await? - .write() - .await - .update_keying_material(self.mls_client()?, &self.mls_backend, None, None) - .await - } +/// Returned when a commit is created +#[derive(Debug, Clone)] +pub struct MlsCommitBundle { + /// A welcome message if there are pending Add proposals + pub welcome: Option, + /// The commit message + pub commit: MlsMessageOut, + /// [`GroupInfo`] (aka GroupInfo) if the commit is merged + pub group_info: MlsGroupInfoBundle, +} - /// Commits all pending proposals of the group - /// - /// # Arguments - /// * `backend` - the KeyStore to persist group changes - /// - /// # Return type - /// A tuple containing the commit message and a possible welcome (in the case `Add` proposals were pending within the internal MLS Group) - /// - /// # Errors - /// Errors can be originating from the KeyStore and OpenMls - #[cfg_attr(test, crate::idempotent)] - pub async fn commit_pending_proposals(&mut self, id: &ConversationId) -> CryptoResult> { - self.get_conversation(id) - .await? - .write() - .await - .commit_pending_proposals(self.mls_client()?, &self.mls_backend) - .await +impl MlsCommitBundle { + /// Serializes both wrapped objects into TLS and return them as a tuple of byte arrays. + /// 0 -> welcome + /// 1 -> message + /// 2 -> public group state + #[allow(clippy::type_complexity)] + pub fn to_bytes_triple(self) -> CryptoResult<(Option>, Vec, MlsGroupInfoBundle)> { + use openmls::prelude::TlsSerializeTrait as _; + let welcome = self + .welcome + .as_ref() + .map(|w| w.tls_serialize_detached().map_err(MlsError::from)) + .transpose()?; + let commit = self.commit.tls_serialize_detached().map_err(MlsError::from)?; + Ok((welcome, commit, self.group_info)) } } @@ -580,62 +476,6 @@ pub mod tests { } } - pub mod propose_add_members { - use super::*; - - #[apply(all_cred_cipher)] - #[wasm_bindgen_test] - pub async fn can_propose_adding_members_to_conversation(case: TestCase) { - run_test_with_client_ids( - case.clone(), - ["alice", "bob", "charlie"], - move |[mut alice_central, mut bob_central, mut charlie_central]| { - Box::pin(async move { - let id = conversation_id(); - alice_central - .new_conversation(&id, case.credential_type, case.cfg.clone()) - .await - .unwrap(); - alice_central.invite_all(&case, &id, [&mut bob_central]).await.unwrap(); - let charlie_kp = charlie_central.get_one_key_package(&case).await; - - assert!(alice_central.pending_proposals(&id).await.is_empty()); - let proposal = alice_central.new_add_proposal(&id, charlie_kp).await.unwrap().proposal; - assert_eq!(alice_central.pending_proposals(&id).await.len(), 1); - bob_central - .decrypt_message(&id, proposal.to_bytes().unwrap()) - .await - .unwrap(); - let MlsCommitBundle { commit, welcome, .. } = - bob_central.commit_pending_proposals(&id).await.unwrap().unwrap(); - bob_central.commit_accepted(&id).await.unwrap(); - assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3); - - // if 'new_proposal' wasn't durable this would fail because proposal would - // not be referenced in commit - alice_central - .decrypt_message(&id, commit.to_bytes().unwrap()) - .await - .unwrap(); - assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3); - - charlie_central - .try_join_from_welcome( - &id, - welcome.unwrap().into(), - case.custom_cfg(), - vec![&mut alice_central, &mut bob_central], - ) - .await - .unwrap(); - assert_eq!(charlie_central.get_conversation_unchecked(&id).await.members().len(), 3); - }) - }, - ) - .await - } - } - pub mod remove_members { use super::*; @@ -768,56 +608,6 @@ pub mod tests { } } - pub mod propose_remove_members { - use super::*; - - #[apply(all_cred_cipher)] - #[wasm_bindgen_test] - pub async fn can_propose_removing_members_from_conversation(case: TestCase) { - run_test_with_client_ids( - case.clone(), - ["alice", "bob", "charlie"], - move |[mut alice_central, mut bob_central, mut charlie_central]| { - Box::pin(async move { - let id = conversation_id(); - alice_central - .new_conversation(&id, case.credential_type, case.cfg.clone()) - .await - .unwrap(); - alice_central - .invite_all(&case, &id, [&mut bob_central, &mut charlie_central]) - .await - .unwrap(); - - assert!(alice_central.pending_proposals(&id).await.is_empty()); - let proposal = alice_central - .new_remove_proposal(&id, charlie_central.get_client_id()) - .await - .unwrap() - .proposal; - assert_eq!(alice_central.pending_proposals(&id).await.len(), 1); - bob_central - .decrypt_message(&id, proposal.to_bytes().unwrap()) - .await - .unwrap(); - let commit = bob_central.commit_pending_proposals(&id).await.unwrap().unwrap().commit; - bob_central.commit_accepted(&id).await.unwrap(); - assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2); - - // if 'new_proposal' wasn't durable this would fail because proposal would - // not be referenced in commit - alice_central - .decrypt_message(&id, commit.to_bytes().unwrap()) - .await - .unwrap(); - assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2); - }) - }, - ) - .await - } - } - pub mod update_keying_material { use super::*; @@ -1087,85 +877,6 @@ pub mod tests { } } - pub mod propose_self_update { - use super::*; - - #[apply(all_cred_cipher)] - #[wasm_bindgen_test] - pub async fn can_propose_updating(case: TestCase) { - run_test_with_client_ids( - case.clone(), - ["alice", "bob"], - move |[mut alice_central, mut bob_central]| { - Box::pin(async move { - let id = conversation_id(); - alice_central - .new_conversation(&id, case.credential_type, case.cfg.clone()) - .await - .unwrap(); - alice_central.invite_all(&case, &id, [&mut bob_central]).await.unwrap(); - - let bob_keys = bob_central - .get_conversation_unchecked(&id) - .await - .signature_keys() - .collect::>(); - let alice_keys = alice_central - .get_conversation_unchecked(&id) - .await - .signature_keys() - .collect::>(); - assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key))); - let alice_key = alice_central - .encryption_key_of(&id, alice_central.get_client_id()) - .await; - - let proposal = alice_central.new_update_proposal(&id).await.unwrap().proposal; - bob_central - .decrypt_message(&id, proposal.to_bytes().unwrap()) - .await - .unwrap(); - let commit = bob_central.commit_pending_proposals(&id).await.unwrap().unwrap().commit; - - // before merging, commit is not applied - assert!(bob_central - .get_conversation_unchecked(&id) - .await - .encryption_keys() - .contains(&alice_key)); - bob_central.commit_accepted(&id).await.unwrap(); - assert!(!bob_central - .get_conversation_unchecked(&id) - .await - .encryption_keys() - .contains(&alice_key)); - - assert!(alice_central - .get_conversation_unchecked(&id) - .await - .encryption_keys() - .contains(&alice_key)); - // if 'new_proposal' wasn't durable this would fail because proposal would - // not be referenced in commit - alice_central - .decrypt_message(&id, commit.to_bytes().unwrap()) - .await - .unwrap(); - assert!(!alice_central - .get_conversation_unchecked(&id) - .await - .encryption_keys() - .contains(&alice_key)); - - // ensuring both can encrypt messages - assert!(alice_central.try_talk_to(&id, &mut bob_central).await.is_ok()); - }) - }, - ) - .await; - } - } - pub mod commit_pending_proposals { use super::*; @@ -1463,39 +1174,5 @@ pub mod tests { .await; } } - - #[apply(all_cred_cipher)] - #[wasm_bindgen_test] - pub async fn should_prevent_out_of_order_proposals(case: TestCase) { - run_test_with_client_ids( - case.clone(), - ["alice", "bob"], - move |[mut alice_central, mut bob_central]| { - Box::pin(async move { - let id = conversation_id(); - alice_central - .new_conversation(&id, case.credential_type, case.cfg.clone()) - .await - .unwrap(); - alice_central.invite_all(&case, &id, [&mut bob_central]).await.unwrap(); - - let proposal = alice_central.new_update_proposal(&id).await.unwrap().proposal; - - bob_central - .decrypt_message(&id, &proposal.to_bytes().unwrap()) - .await - .unwrap(); - bob_central.commit_pending_proposals(&id).await.unwrap(); - // epoch++ - bob_central.commit_accepted(&id).await.unwrap(); - - // fails when we try to decrypt a proposal for past epoch - let past_proposal = bob_central.decrypt_message(&id, &proposal.to_bytes().unwrap()).await; - assert!(matches!(past_proposal.unwrap_err(), CryptoError::WrongEpoch)); - }) - }, - ) - .await; - } } } diff --git a/crypto/src/mls/conversation/commit_delay.rs b/crypto/src/mls/conversation/commit_delay.rs index 6fbd888b9f..1cddbb1478 100644 --- a/crypto/src/mls/conversation/commit_delay.rs +++ b/crypto/src/mls/conversation/commit_delay.rs @@ -87,7 +87,7 @@ impl MlsConversation { #[cfg(test)] pub mod tests { use super::*; - use crate::{mls::conversation::handshake::MlsConversationCreationMessage, test_utils::*}; + use crate::{prelude::MlsConversationCreationMessage, test_utils::*}; use tls_codec::Serialize as _; use wasm_bindgen_test::*; diff --git a/crypto/src/mls/conversation/decrypt.rs b/crypto/src/mls/conversation/decrypt.rs index 2ea49fde06..2f6df8fad7 100644 --- a/crypto/src/mls/conversation/decrypt.rs +++ b/crypto/src/mls/conversation/decrypt.rs @@ -10,25 +10,32 @@ use openmls::{ framing::errors::{MessageDecryptionError, SecretTreeError}, + group::StagedCommit, prelude::{ - MlsCredentialType, MlsMessageIn, MlsMessageInBody, ProcessMessageError, ProcessedMessage, - ProcessedMessageContent, Proposal, ProtocolMessage, ValidationError, + CredentialType, MlsMessageIn, MlsMessageInBody, ProcessMessageError, ProcessedMessage, ProcessedMessageContent, + Proposal, ProtocolMessage, ValidationError, }, }; use openmls_traits::OpenMlsCryptoProvider; use tls_codec::Deserialize; +use wire_e2e_identity::prelude::x509::revocation::PkiEnvironment; -use core_crypto_keystore::entities::{E2eiCrl, MlsPendingMessage}; +use core_crypto_keystore::entities::MlsPendingMessage; use mls_crypto_provider::MlsCryptoProvider; -use wire_e2e_identity::prelude::x509::{extract_crl_uris, revocation::PkiEnvironment}; use crate::{ + e2e_identity::conversation_state::compute_state, group_store::GroupStoreValue, mls::{ - client::Client, conversation::renew::Renew, credential::ext::CredentialExt, ClientId, ConversationId, - MlsCentral, MlsConversation, + client::Client, + conversation::renew::Renew, + credential::crl::{ + extract_crl_uris_from_proposals, extract_crl_uris_from_update_path, get_new_crl_distribution_points, + }, + credential::ext::CredentialExt, + ClientId, ConversationId, MlsCentral, MlsConversation, }, - prelude::{MlsProposalBundle, WireIdentity}, + prelude::{E2eiConversationState, MlsProposalBundle, WireIdentity}, CoreCryptoCallbacks, CryptoError, CryptoResult, MlsError, }; @@ -99,64 +106,6 @@ impl From for MlsBufferedConversationDecryptMessa } } -fn extract_crl_uris_from_proposals(proposals: &[Proposal]) -> CryptoResult> { - use x509_cert::der::Decode as _; - - let mut crl_dps = std::collections::HashSet::new(); - - for proposal in proposals { - let mut maybe_certs = match proposal { - Proposal::Add(add_proposal) => { - if let MlsCredentialType::X509(certs) = - add_proposal.key_package().leaf_node().credential().mls_credential() - { - Some(&certs.certificates) - } else { - None - } - } - Proposal::Update(update_proposal) => { - if let MlsCredentialType::X509(certs) = update_proposal.leaf_node().credential().mls_credential() { - Some(&certs.certificates) - } else { - None - } - } - _ => None, - }; - - if let Some(certs) = maybe_certs.take() { - for cert in certs { - let cert = x509_cert::Certificate::from_der(cert.as_slice())?; - if let Some(crl_uris) = extract_crl_uris(&cert).map_err(|e| CryptoError::E2eiError(e.into()))? { - crl_dps.extend(crl_uris); - } - } - } - } - - Ok(crl_dps.into_iter().collect()) -} - -async fn get_new_crl_distribution_points( - backend: &MlsCryptoProvider, - crl_dps: Vec, -) -> CryptoResult>> { - if !crl_dps.is_empty() { - let stored_crls = backend.key_store().find_all::(Default::default()).await?; - let stored_crl_dps: Vec<&str> = stored_crls.iter().map(|crl| crl.distribution_point.as_str()).collect(); - - Ok(Some( - crl_dps - .into_iter() - .filter(|dp| stored_crl_dps.contains(&dp.as_str())) - .collect(), - )) - } else { - Ok(None) - } -} - /// Abstraction over a MLS group capable of decrypting a MLS message impl MlsConversation { /// see [MlsCentral::decrypt_message] @@ -198,11 +147,8 @@ impl MlsConversation { crl_new_distribution_points: None, }, ProcessedMessageContent::ProposalMessage(proposal) => { - let crl_new_distribution_points = get_new_crl_distribution_points( - backend, - extract_crl_uris_from_proposals(&[proposal.proposal().clone()])?, - ) - .await?; + let crl_dps = extract_crl_uris_from_proposals(&[proposal.proposal().clone()])?; + let crl_new_distribution_points = get_new_crl_distribution_points(backend, crl_dps).await?; self.group.store_pending_proposal(*proposal); @@ -219,9 +165,11 @@ impl MlsConversation { } } ProcessedMessageContent::StagedCommitMessage(staged_commit) => { - self.validate_external_commit(&staged_commit, sender_client_id, parent_conv, pki_env, callbacks) + self.validate_external_commit(&staged_commit, sender_client_id, parent_conv, callbacks) .await?; + self.validate_commit(&staged_commit, pki_env)?; + #[allow(clippy::needless_collect)] // false positive let pending_proposals = self.self_pending_proposals().cloned().collect::>(); @@ -240,11 +188,14 @@ impl MlsConversation { ) .collect(); - // TODO: Fetch UpdatePath from `staged_commit` as it's much like an Update proposal and can introduce new credentials // - This requires a change in OpenMLS to get access to it - let crl_new_distribution_points = - get_new_crl_distribution_points(backend, extract_crl_uris_from_proposals(proposal_refs.as_ref())?) - .await?; + let crl_dps_from_proposals = extract_crl_uris_from_proposals(proposal_refs.as_ref())?; + let crl_dps_from_update_path = extract_crl_uris_from_update_path(&staged_commit)?; + let crl_new_distribution_points = get_new_crl_distribution_points( + backend, + [crl_dps_from_proposals, crl_dps_from_update_path].concat(), + ) + .await?; // getting the pending has to be done before `merge_staged_commit` otherwise it's wiped out let pending_commit = self.group.pending_commit().cloned(); @@ -293,11 +244,8 @@ impl MlsConversation { ProcessedMessageContent::ExternalJoinProposalMessage(proposal) => { self.validate_external_proposal(&proposal, parent_conv, callbacks) .await?; - let crl_new_distribution_points = get_new_crl_distribution_points( - backend, - extract_crl_uris_from_proposals(&[proposal.proposal().clone()])?, - ) - .await?; + let crl_dps = extract_crl_uris_from_proposals(&[proposal.proposal().clone()])?; + let crl_new_distribution_points = get_new_crl_distribution_points(backend, crl_dps).await?; self.group.store_pending_proposal(*proposal); MlsConversationDecryptMessage { @@ -370,6 +318,29 @@ impl MlsConversation { } Ok(processed_msg) } + + fn validate_commit(&self, commit: &StagedCommit, pki_env: Option<&PkiEnvironment>) -> CryptoResult<()> { + if let Some(pki_env) = pki_env { + let credentials: Vec<_> = commit + .add_proposals() + .filter_map(|add_proposal| { + let credential = add_proposal.add_proposal().key_package().leaf_node().credential(); + + matches!(credential.credential_type(), CredentialType::X509).then(|| credential.clone()) + }) + .collect(); + let state = compute_state( + credentials.iter(), + Some(pki_env), + crate::prelude::MlsCredentialType::X509, + ); + if state != E2eiConversationState::Verified { + // FIXME: Uncomment when PKI env can be seeded - the computation is still done to assess performance and impact of the validations + // return Err(CryptoError::InvalidCertificateChain); + } + } + Ok(()) + } } impl MlsCentral { @@ -435,7 +406,7 @@ pub mod tests { use crate::mls::conversation::config::MAX_PAST_EPOCHS; use crate::{ - prelude::{handshake::MlsCommitBundle, MlsWirePolicy}, + prelude::{MlsCommitBundle, MlsWirePolicy}, test_utils::{ValidationCallbacks, *}, CryptoError, }; @@ -689,7 +660,8 @@ pub mod tests { let id = charlie_central .process_welcome_message(welcome.unwrap().into(), case.custom_cfg()) .await - .unwrap(); + .unwrap() + .id; assert!(charlie_central.try_talk_to(&id, &mut alice_central).await.is_ok()); }) }, diff --git a/crypto/src/mls/conversation/mod.rs b/crypto/src/mls/conversation/mod.rs index 80a33ce38a..2e561e4fde 100644 --- a/crypto/src/mls/conversation/mod.rs +++ b/crypto/src/mls/conversation/mod.rs @@ -44,6 +44,7 @@ use crate::{ }; mod buffer_messages; +pub mod commit; mod commit_delay; pub mod config; #[cfg(test)] @@ -55,10 +56,10 @@ mod durability; pub mod encrypt; pub mod export; pub(crate) mod group_info; -pub mod handshake; mod leaf_node_validation; pub mod merge; mod orphan_welcome; +pub mod proposal; mod renew; mod self_commit; pub(crate) mod welcome; diff --git a/crypto/src/mls/conversation/proposal.rs b/crypto/src/mls/conversation/proposal.rs new file mode 100644 index 0000000000..a794eed5f1 --- /dev/null +++ b/crypto/src/mls/conversation/proposal.rs @@ -0,0 +1,384 @@ +//! This table summarizes when a MLS group can create a commit or proposal: +//! +//! | can create handshake ? | 0 pend. Commit | 1 pend. Commit | +//! |------------------------|----------------|----------------| +//! | 0 pend. Proposal | ✅ | ❌ | +//! | 1+ pend. Proposal | ✅ | ❌ | + +use openmls::{binary_tree::LeafNodeIndex, framing::MlsMessageOut, key_packages::KeyPackageIn, prelude::LeafNode}; + +use mls_crypto_provider::MlsCryptoProvider; + +use crate::{ + mls::credential::crl::extract_dp, + prelude::{Client, MlsConversation, MlsProposalRef}, + CryptoError, CryptoResult, MlsError, +}; + +/// Creating proposals +impl MlsConversation { + /// see [openmls::group::MlsGroup::propose_add_member] + #[cfg_attr(test, crate::durable)] + pub async fn propose_add_member( + &mut self, + client: &Client, + backend: &MlsCryptoProvider, + key_package: KeyPackageIn, + ) -> CryptoResult { + let signer = &self + .find_current_credential_bundle(client)? + .ok_or(CryptoError::IdentityInitializationError)? + .signature_key; + + let crl_new_distribution_points = match key_package.credential().mls_credential() { + openmls::prelude::MlsCredentialType::X509(cert) => Some(extract_dp(cert)?), + _ => None, + }; + + let (proposal, proposal_ref) = self + .group + .propose_add_member(backend, signer, key_package) + .map_err(MlsError::from)?; + let proposal = MlsProposalBundle { + proposal, + proposal_ref: proposal_ref.into(), + crl_new_distribution_points, + }; + self.persist_group_when_changed(backend, false).await?; + Ok(proposal) + } + + /// see [openmls::group::MlsGroup::propose_remove_member] + #[cfg_attr(test, crate::durable)] + pub async fn propose_remove_member( + &mut self, + client: &Client, + backend: &MlsCryptoProvider, + member: LeafNodeIndex, + ) -> CryptoResult { + let signer = &self + .find_current_credential_bundle(client)? + .ok_or(CryptoError::IdentityInitializationError)? + .signature_key; + let proposal = self + .group + .propose_remove_member(backend, signer, member) + .map_err(MlsError::from) + .map_err(CryptoError::from) + .map(MlsProposalBundle::from)?; + self.persist_group_when_changed(backend, false).await?; + Ok(proposal) + } + + /// see [openmls::group::MlsGroup::propose_self_update] + #[cfg_attr(test, crate::durable)] + pub async fn propose_self_update( + &mut self, + client: &Client, + backend: &MlsCryptoProvider, + ) -> CryptoResult { + self.propose_explicit_self_update(client, backend, None).await + } + + /// see [openmls::group::MlsGroup::propose_self_update] + #[cfg_attr(test, crate::durable)] + pub async fn propose_explicit_self_update( + &mut self, + client: &Client, + backend: &MlsCryptoProvider, + leaf_node: Option, + ) -> CryptoResult { + let msg_signer = &self + .find_current_credential_bundle(client)? + .ok_or(CryptoError::IdentityInitializationError)? + .signature_key; + + let proposal = if let Some(leaf_node) = leaf_node { + let leaf_node_signer = &self + .find_most_recent_credential_bundle(client)? + .ok_or(CryptoError::IdentityInitializationError)? + .signature_key; + + self.group + .propose_explicit_self_update(backend, msg_signer, leaf_node, leaf_node_signer) + .await + } else { + self.group.propose_self_update(backend, msg_signer).await + } + .map_err(MlsError::from) + .map(MlsProposalBundle::from)?; + + self.persist_group_when_changed(backend, false).await?; + Ok(proposal) + } +} + +/// Returned when a Proposal is created. Helps roll backing a local proposal +#[derive(Debug)] +pub struct MlsProposalBundle { + /// The proposal message + pub proposal: MlsMessageOut, + /// A unique identifier of the proposal to rollback it later if required + pub proposal_ref: MlsProposalRef, + /// New CRL distribution points that appeared by the introduction of a new credential + pub crl_new_distribution_points: Option>, +} + +impl From<(MlsMessageOut, openmls::prelude::hash_ref::ProposalRef)> for MlsProposalBundle { + fn from((proposal, proposal_ref): (MlsMessageOut, openmls::prelude::hash_ref::ProposalRef)) -> Self { + Self { + proposal, + proposal_ref: proposal_ref.into(), + crl_new_distribution_points: None, + } + } +} + +impl MlsProposalBundle { + /// Serializes both wrapped objects into TLS and return them as a tuple of byte arrays. + /// 0 -> proposal + /// 1 -> proposal reference + pub fn to_bytes_pair(&self) -> CryptoResult<(Vec, Vec)> { + use openmls::prelude::TlsSerializeTrait as _; + let proposal = self.proposal.tls_serialize_detached().map_err(MlsError::from)?; + let proposal_ref = self.proposal_ref.to_bytes(); + + Ok((proposal, proposal_ref)) + } +} + +#[cfg(test)] +pub mod tests { + use itertools::Itertools; + use openmls::prelude::SignaturePublicKey; + use wasm_bindgen_test::*; + + use crate::{prelude::MlsCommitBundle, test_utils::*}; + + use super::*; + + wasm_bindgen_test_configure!(run_in_browser); + + pub mod propose_add_members { + use super::*; + + #[apply(all_cred_cipher)] + #[wasm_bindgen_test] + pub async fn can_propose_adding_members_to_conversation(case: TestCase) { + run_test_with_client_ids( + case.clone(), + ["alice", "bob", "charlie"], + move |[mut alice_central, mut bob_central, mut charlie_central]| { + Box::pin(async move { + let id = conversation_id(); + alice_central + .new_conversation(&id, case.credential_type, case.cfg.clone()) + .await + .unwrap(); + alice_central.invite_all(&case, &id, [&mut bob_central]).await.unwrap(); + let charlie_kp = charlie_central.get_one_key_package(&case).await; + + assert!(alice_central.pending_proposals(&id).await.is_empty()); + let proposal = alice_central.new_add_proposal(&id, charlie_kp).await.unwrap().proposal; + assert_eq!(alice_central.pending_proposals(&id).await.len(), 1); + bob_central + .decrypt_message(&id, proposal.to_bytes().unwrap()) + .await + .unwrap(); + let MlsCommitBundle { commit, welcome, .. } = + bob_central.commit_pending_proposals(&id).await.unwrap().unwrap(); + bob_central.commit_accepted(&id).await.unwrap(); + assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3); + + // if 'new_proposal' wasn't durable this would fail because proposal would + // not be referenced in commit + alice_central + .decrypt_message(&id, commit.to_bytes().unwrap()) + .await + .unwrap(); + assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3); + + charlie_central + .try_join_from_welcome( + &id, + welcome.unwrap().into(), + case.custom_cfg(), + vec![&mut alice_central, &mut bob_central], + ) + .await + .unwrap(); + assert_eq!(charlie_central.get_conversation_unchecked(&id).await.members().len(), 3); + }) + }, + ) + .await + } + } + + pub mod propose_remove_members { + use super::*; + + #[apply(all_cred_cipher)] + #[wasm_bindgen_test] + pub async fn can_propose_removing_members_from_conversation(case: TestCase) { + run_test_with_client_ids( + case.clone(), + ["alice", "bob", "charlie"], + move |[mut alice_central, mut bob_central, mut charlie_central]| { + Box::pin(async move { + let id = conversation_id(); + alice_central + .new_conversation(&id, case.credential_type, case.cfg.clone()) + .await + .unwrap(); + alice_central + .invite_all(&case, &id, [&mut bob_central, &mut charlie_central]) + .await + .unwrap(); + + assert!(alice_central.pending_proposals(&id).await.is_empty()); + let proposal = alice_central + .new_remove_proposal(&id, charlie_central.get_client_id()) + .await + .unwrap() + .proposal; + assert_eq!(alice_central.pending_proposals(&id).await.len(), 1); + bob_central + .decrypt_message(&id, proposal.to_bytes().unwrap()) + .await + .unwrap(); + let commit = bob_central.commit_pending_proposals(&id).await.unwrap().unwrap().commit; + bob_central.commit_accepted(&id).await.unwrap(); + assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2); + + // if 'new_proposal' wasn't durable this would fail because proposal would + // not be referenced in commit + alice_central + .decrypt_message(&id, commit.to_bytes().unwrap()) + .await + .unwrap(); + assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2); + }) + }, + ) + .await + } + } + + pub mod propose_self_update { + use super::*; + + #[apply(all_cred_cipher)] + #[wasm_bindgen_test] + pub async fn can_propose_updating(case: TestCase) { + run_test_with_client_ids( + case.clone(), + ["alice", "bob"], + move |[mut alice_central, mut bob_central]| { + Box::pin(async move { + let id = conversation_id(); + alice_central + .new_conversation(&id, case.credential_type, case.cfg.clone()) + .await + .unwrap(); + alice_central.invite_all(&case, &id, [&mut bob_central]).await.unwrap(); + + let bob_keys = bob_central + .get_conversation_unchecked(&id) + .await + .signature_keys() + .collect::>(); + let alice_keys = alice_central + .get_conversation_unchecked(&id) + .await + .signature_keys() + .collect::>(); + assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key))); + let alice_key = alice_central + .encryption_key_of(&id, alice_central.get_client_id()) + .await; + + let proposal = alice_central.new_update_proposal(&id).await.unwrap().proposal; + bob_central + .decrypt_message(&id, proposal.to_bytes().unwrap()) + .await + .unwrap(); + let commit = bob_central.commit_pending_proposals(&id).await.unwrap().unwrap().commit; + + // before merging, commit is not applied + assert!(bob_central + .get_conversation_unchecked(&id) + .await + .encryption_keys() + .contains(&alice_key)); + bob_central.commit_accepted(&id).await.unwrap(); + assert!(!bob_central + .get_conversation_unchecked(&id) + .await + .encryption_keys() + .contains(&alice_key)); + + assert!(alice_central + .get_conversation_unchecked(&id) + .await + .encryption_keys() + .contains(&alice_key)); + // if 'new_proposal' wasn't durable this would fail because proposal would + // not be referenced in commit + alice_central + .decrypt_message(&id, commit.to_bytes().unwrap()) + .await + .unwrap(); + assert!(!alice_central + .get_conversation_unchecked(&id) + .await + .encryption_keys() + .contains(&alice_key)); + + // ensuring both can encrypt messages + assert!(alice_central.try_talk_to(&id, &mut bob_central).await.is_ok()); + }) + }, + ) + .await; + } + } + + pub mod delivery_semantics { + use super::*; + + #[apply(all_cred_cipher)] + #[wasm_bindgen_test] + pub async fn should_prevent_out_of_order_proposals(case: TestCase) { + run_test_with_client_ids( + case.clone(), + ["alice", "bob"], + move |[mut alice_central, mut bob_central]| { + Box::pin(async move { + let id = conversation_id(); + alice_central + .new_conversation(&id, case.credential_type, case.cfg.clone()) + .await + .unwrap(); + alice_central.invite_all(&case, &id, [&mut bob_central]).await.unwrap(); + + let proposal = alice_central.new_update_proposal(&id).await.unwrap().proposal; + + bob_central + .decrypt_message(&id, &proposal.to_bytes().unwrap()) + .await + .unwrap(); + bob_central.commit_pending_proposals(&id).await.unwrap(); + // epoch++ + bob_central.commit_accepted(&id).await.unwrap(); + + // fails when we try to decrypt a proposal for past epoch + let past_proposal = bob_central.decrypt_message(&id, &proposal.to_bytes().unwrap()).await; + assert!(matches!(past_proposal.unwrap_err(), CryptoError::WrongEpoch)); + }) + }, + ) + .await; + } + } +} diff --git a/crypto/src/mls/conversation/renew.rs b/crypto/src/mls/conversation/renew.rs index 3a0d24f4af..8e0c75c907 100644 --- a/crypto/src/mls/conversation/renew.rs +++ b/crypto/src/mls/conversation/renew.rs @@ -4,7 +4,7 @@ use openmls_traits::OpenMlsCryptoProvider; use mls_crypto_provider::MlsCryptoProvider; -use crate::prelude::{handshake::MlsProposalBundle, Client, CryptoError, CryptoResult, MlsConversation}; +use crate::prelude::{Client, CryptoError, CryptoResult, MlsConversation, MlsProposalBundle}; /// Marker struct holding methods responsible for restoring (renewing) proposals (or pending commit) /// in case another commit has been accepted by the backend instead of ours @@ -107,7 +107,7 @@ impl MlsConversation { let proposals = proposals.filter(|p| !is_external(p)); for proposal in proposals { let msg = match proposal.proposal { - Proposal::Add(add) => self.propose_add_member(client, backend, add.key_package).await?, + Proposal::Add(add) => self.propose_add_member(client, backend, add.key_package.into()).await?, Proposal::Remove(remove) => self.propose_remove_member(client, backend, remove.removed()).await?, Proposal::Update(update) => self.renew_update(client, backend, Some(update.leaf_node())).await?, _ => return Err(CryptoError::ImplementationError), diff --git a/crypto/src/mls/conversation/welcome.rs b/crypto/src/mls/conversation/welcome.rs index 17b5da356f..d68185b25e 100644 --- a/crypto/src/mls/conversation/welcome.rs +++ b/crypto/src/mls/conversation/welcome.rs @@ -1,3 +1,4 @@ +use crate::mls::credential::crl::extract_dp; use crate::{ group_store::GroupStore, prelude::{ @@ -11,6 +12,15 @@ use openmls::prelude::{MlsGroup, MlsMessageIn, MlsMessageInBody, Welcome}; use openmls_traits::OpenMlsCryptoProvider; use tls_codec::Deserialize; +/// Contains everything client needs to know after decrypting an (encrypted) Welcome message +#[derive(Debug)] +pub struct WelcomeBundle { + /// MLS Group Id + pub id: ConversationId, + /// New CRL distribution points that appeared by the introduction of a new credential + pub crl_new_distribution_points: Option>, +} + impl MlsCentral { /// Create a conversation from a TLS serialized MLS Welcome message. The `MlsConversationConfiguration` used in this function will be the default implementation. /// @@ -28,7 +38,7 @@ impl MlsCentral { &mut self, welcome: Vec, custom_cfg: MlsCustomConfiguration, - ) -> CryptoResult { + ) -> CryptoResult { let mut cursor = std::io::Cursor::new(welcome); let welcome = MlsMessageIn::tls_deserialize(&mut cursor).map_err(MlsError::from)?; self.process_welcome_message(welcome, custom_cfg).await @@ -52,7 +62,7 @@ impl MlsCentral { &mut self, welcome: MlsMessageIn, custom_cfg: MlsCustomConfiguration, - ) -> CryptoResult { + ) -> CryptoResult { let welcome = match welcome.extract() { MlsMessageInBody::Welcome(welcome) => welcome, _ => return Err(CryptoError::ConsumerError), @@ -67,9 +77,32 @@ impl MlsCentral { MlsConversation::from_welcome_message(welcome, configuration, &mut self.mls_backend, &mut self.mls_groups) .await?; - let conversation_id = conversation.id.clone(); - self.mls_groups.insert(conversation_id.clone(), conversation); - Ok(conversation_id) + // We wait for the group to be created then we iterate through all members + // We cannot use the Welcome since we don't have other added members KeyPackage + let crl_new_distribution_points = conversation + .group + .members_credentials() + .filter_map(|c| match c.mls_credential() { + openmls::prelude::MlsCredentialType::X509(cert) => Some(cert), + _ => None, + }) + .try_fold(vec![], |mut acc, c| { + acc.extend(extract_dp(c)?); + CryptoResult::Ok(acc) + })?; + let crl_new_distribution_points = if crl_new_distribution_points.is_empty() { + None + } else { + Some(crl_new_distribution_points) + }; + + let id = conversation.id.clone(); + self.mls_groups.insert(id.clone(), conversation); + + Ok(WelcomeBundle { + id, + crl_new_distribution_points, + }) } } @@ -118,7 +151,7 @@ impl MlsConversation { pub mod tests { use wasm_bindgen_test::*; - use crate::{mls::conversation::handshake::MlsConversationCreationMessage, test_utils::*}; + use crate::{prelude::MlsConversationCreationMessage, test_utils::*}; use super::*; diff --git a/crypto/src/mls/credential/crl.rs b/crypto/src/mls/credential/crl.rs new file mode 100644 index 0000000000..da62d6d702 --- /dev/null +++ b/crypto/src/mls/credential/crl.rs @@ -0,0 +1,69 @@ +use crate::{CryptoError, CryptoResult}; +use core_crypto_keystore::entities::E2eiCrl; +use mls_crypto_provider::MlsCryptoProvider; +use openmls::prelude::{Certificate, MlsCredentialType, Proposal, StagedCommit}; +use openmls_traits::OpenMlsCryptoProvider; +use wire_e2e_identity::prelude::x509::extract_crl_uris; + +pub(crate) fn extract_crl_uris_from_proposals(proposals: &[Proposal]) -> CryptoResult> { + proposals + .iter() + .filter_map(|p| match p { + Proposal::Add(add) => Some(add.key_package().leaf_node()), + Proposal::Update(update) => Some(update.leaf_node()), + _ => None, + }) + .map(|ln| ln.credential().mls_credential()) + .filter_map(|c| match c { + MlsCredentialType::X509(cert) => Some(cert), + _ => None, + }) + .try_fold(vec![], |mut acc, c| { + acc.extend(extract_dp(c)?); + CryptoResult::Ok(acc) + }) +} + +pub(crate) fn extract_crl_uris_from_update_path(commit: &StagedCommit) -> CryptoResult> { + if let Some(update_path) = commit.get_update_path_leaf_node() { + if let MlsCredentialType::X509(cert) = update_path.credential().mls_credential() { + return extract_dp(cert); + } + } + Ok(vec![]) +} + +pub(crate) fn extract_dp(cert: &Certificate) -> CryptoResult> { + Ok(cert + .certificates + .iter() + .try_fold(std::collections::HashSet::new(), |mut acc, cert| { + use x509_cert::der::Decode as _; + let cert = x509_cert::Certificate::from_der(cert.as_slice())?; + if let Some(crl_uris) = extract_crl_uris(&cert).map_err(|e| CryptoError::E2eiError(e.into()))? { + acc.extend(crl_uris); + } + CryptoResult::Ok(acc) + })? + .into_iter() + .collect()) +} + +pub(crate) async fn get_new_crl_distribution_points( + backend: &MlsCryptoProvider, + crl_dps: Vec, +) -> CryptoResult>> { + if !crl_dps.is_empty() { + let stored_crls = backend.key_store().find_all::(Default::default()).await?; + let stored_crl_dps: Vec<&str> = stored_crls.iter().map(|crl| crl.distribution_point.as_str()).collect(); + + Ok(Some( + crl_dps + .into_iter() + .filter(|dp| stored_crl_dps.contains(&dp.as_str())) + .collect(), + )) + } else { + Ok(None) + } +} diff --git a/crypto/src/mls/credential/mod.rs b/crypto/src/mls/credential/mod.rs index 23fd8214c3..0bd7deaa96 100644 --- a/crypto/src/mls/credential/mod.rs +++ b/crypto/src/mls/credential/mod.rs @@ -3,6 +3,7 @@ use openmls_traits::OpenMlsCryptoProvider; use std::cmp::Ordering; use std::hash::{Hash, Hasher}; +pub(crate) mod crl; pub(crate) mod ext; pub(crate) mod typ; pub(crate) mod x509; diff --git a/crypto/src/mls/external_commit.rs b/crypto/src/mls/external_commit.rs index 0da6cf078e..6d81c48ad4 100644 --- a/crypto/src/mls/external_commit.rs +++ b/crypto/src/mls/external_commit.rs @@ -14,26 +14,22 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see http://www.gnu.org/licenses/. -use openmls::prelude::{ - group_info::VerifiableGroupInfo, CredentialType, MlsGroup, MlsMessageOut, Proposal, Sender, StagedCommit, -}; +use openmls::prelude::{group_info::VerifiableGroupInfo, MlsGroup, MlsMessageOut, Proposal, Sender, StagedCommit}; use openmls_traits::OpenMlsCryptoProvider; - -use core_crypto_keystore::entities::MlsPendingMessage; -use core_crypto_keystore::{entities::PersistedMlsPendingGroup, CryptoKeystoreMls}; use tls_codec::Serialize; -use wire_e2e_identity::prelude::x509::revocation::PkiEnvironment; -use crate::{ - e2e_identity::conversation_state::compute_state, - prelude::{decrypt::MlsBufferedConversationDecryptMessage, E2eiConversationState}, +use core_crypto_keystore::{ + entities::{MlsPendingMessage, PersistedMlsPendingGroup}, + CryptoKeystoreMls, }; + +use crate::mls::credential::crl::extract_dp; use crate::{ group_store::GroupStoreValue, prelude::{ - id::ClientId, ConversationId, CoreCryptoCallbacks, CryptoError, CryptoResult, MlsCentral, MlsCiphersuite, - MlsConversation, MlsConversationConfiguration, MlsCredentialType, MlsCustomConfiguration, MlsError, - MlsGroupInfoBundle, + decrypt::MlsBufferedConversationDecryptMessage, id::ClientId, ConversationId, CoreCryptoCallbacks, CryptoError, + CryptoResult, MlsCentral, MlsCiphersuite, MlsConversation, MlsConversationConfiguration, MlsCredentialType, + MlsCustomConfiguration, MlsError, MlsGroupInfoBundle, }, }; @@ -46,6 +42,8 @@ pub struct MlsConversationInitBundle { pub commit: MlsMessageOut, /// [`GroupInfo`] (aka GroupInfo) which becomes valid when the external commit is accepted by the Delivery Service pub group_info: MlsGroupInfoBundle, + /// New CRL distribution points that appeared by the introduction of a new credential + pub crl_new_distribution_points: Option>, } impl MlsConversationInitBundle { @@ -53,9 +51,9 @@ impl MlsConversationInitBundle { /// 0 -> external commit /// 1 -> public group state #[allow(clippy::type_complexity)] - pub fn to_bytes_pair(self) -> CryptoResult<(Vec, MlsGroupInfoBundle)> { + pub fn to_bytes_triple(self) -> CryptoResult<(Vec, MlsGroupInfoBundle, Option>)> { let commit = self.commit.tls_serialize_detached().map_err(MlsError::from)?; - Ok((commit, self.group_info)) + Ok((commit, self.group_info, self.crl_new_distribution_points)) } } @@ -120,6 +118,15 @@ impl MlsCentral { .await .map_err(MlsError::from)?; + // We should always have ratchet tree extension turned on hence GroupInfo should always be present + let group_info = group_info.ok_or(CryptoError::ImplementationError)?; + let group_info = MlsGroupInfoBundle::try_new_full_plaintext(group_info)?; + + let crl_new_distribution_points = match cb.credential.mls_credential() { + openmls::prelude::MlsCredentialType::X509(cert) => Some(extract_dp(cert)?), + _ => None, + }; + self.mls_backend .key_store() .mls_pending_groups_save( @@ -130,13 +137,11 @@ impl MlsCentral { ) .await?; - // We should always have ratchet tree extension turned on hence GroupInfo should always be present - let group_info = group_info.ok_or(CryptoError::ImplementationError)?; - Ok(MlsConversationInitBundle { conversation_id: group.group_id().to_vec(), commit, - group_info: MlsGroupInfoBundle::try_new_full_plaintext(group_info)?, + group_info, + crl_new_distribution_points, }) } @@ -223,7 +228,6 @@ impl MlsConversation { commit: &StagedCommit, sender: ClientId, parent_conversation: Option<&GroupStoreValue>, - pki_env: Option<&PkiEnvironment>, callbacks: Option<&dyn CoreCryptoCallbacks>, ) -> CryptoResult<()> { // i.e. has this commit been created by [MlsCentral::join_by_external_commit] ? @@ -269,35 +273,20 @@ impl MlsConversation { } } - if let Some(pki_env) = pki_env { - let credentials: Vec<_> = commit - .add_proposals() - .filter_map(|add_proposal| { - let credential = add_proposal.add_proposal().key_package().leaf_node().credential(); - - matches!(credential.credential_type(), CredentialType::X509).then(|| credential.clone()) - }) - .collect(); - let state = compute_state(credentials.iter(), Some(pki_env), MlsCredentialType::X509); - if state != E2eiConversationState::Verified { - // FIXME: Uncomment when PKI env can be seeded - the computation is still done to assess performance and impact of the validations - // return Err(CryptoError::InvalidCertificateChain); - } - } - Ok(()) } } #[cfg(test)] pub mod tests { - use crate::{prelude::MlsConversationInitBundle, test_utils::*, CryptoError}; use openmls::prelude::*; use wasm_bindgen_test::*; - use crate::prelude::MlsConversationConfiguration; use core_crypto_keystore::{CryptoKeystoreError, CryptoKeystoreMls, MissingKeyErrorKind}; + use crate::prelude::MlsConversationConfiguration; + use crate::{prelude::MlsConversationInitBundle, test_utils::*, CryptoError}; + wasm_bindgen_test_configure!(run_in_browser); #[apply(all_cred_cipher)] diff --git a/crypto/src/mls/external_proposal.rs b/crypto/src/mls/external_proposal.rs index cbfacc8977..3a2182748b 100644 --- a/crypto/src/mls/external_proposal.rs +++ b/crypto/src/mls/external_proposal.rs @@ -145,7 +145,7 @@ impl MlsCentral { pub mod tests { use wasm_bindgen_test::*; - use crate::{prelude::handshake::MlsCommitBundle, test_utils::*}; + use crate::{prelude::MlsCommitBundle, test_utils::*}; wasm_bindgen_test_configure!(run_in_browser); diff --git a/crypto/src/mls/proposal.rs b/crypto/src/mls/proposal.rs index f85c29bbf7..3710540f47 100644 --- a/crypto/src/mls/proposal.rs +++ b/crypto/src/mls/proposal.rs @@ -20,7 +20,7 @@ use mls_crypto_provider::MlsCryptoProvider; use crate::{ mls::{ClientId, ConversationId, MlsCentral, MlsConversation}, - prelude::{handshake::MlsProposalBundle, Client, CryptoError, CryptoResult}, + prelude::{Client, CryptoError, CryptoResult, MlsProposalBundle}, }; /// Abstraction over a [openmls::prelude::hash_ref::ProposalRef] to deal with conversions @@ -71,7 +71,11 @@ impl MlsProposal { mut conversation: impl std::ops::DerefMut, ) -> CryptoResult { let proposal = match self { - MlsProposal::Add(key_package) => (*conversation).propose_add_member(client, backend, key_package).await, + MlsProposal::Add(key_package) => { + (*conversation) + .propose_add_member(client, backend, key_package.into()) + .await + } MlsProposal::Update => (*conversation).propose_self_update(client, backend).await, MlsProposal::Remove(client_id) => { let index = conversation @@ -139,7 +143,7 @@ impl MlsCentral { pub mod tests { use wasm_bindgen_test::*; - use crate::{prelude::handshake::MlsCommitBundle, prelude::*, test_utils::*}; + use crate::{prelude::MlsCommitBundle, prelude::*, test_utils::*}; wasm_bindgen_test_configure!(run_in_browser); @@ -168,7 +172,8 @@ pub mod tests { let new_id = bob_central .process_welcome_message(welcome.unwrap().into(), case.custom_cfg()) .await - .unwrap(); + .unwrap() + .id; assert_eq!(id, new_id); assert!(bob_central.try_talk_to(&id, &mut alice_central).await.is_ok()); }) diff --git a/interop/src/clients/corecrypto/native.rs b/interop/src/clients/corecrypto/native.rs index 32e346a394..a02309f8af 100644 --- a/interop/src/clients/corecrypto/native.rs +++ b/interop/src/clients/corecrypto/native.rs @@ -140,7 +140,8 @@ impl EmulatedMlsClient for CoreCryptoNativeClient { Ok(self .cc .process_raw_welcome_message(welcome.into(), MlsCustomConfiguration::default()) - .await?) + .await? + .id) } async fn encrypt_message(&mut self, conversation_id: &[u8], message: &[u8]) -> Result> {