core_crypto/mls/conversation/
own_commit.rsuse super::{Error, Result};
use crate::{
RecursiveError,
mls::credential::{
crl::{extract_crl_uris_from_group, get_new_crl_distribution_points},
ext::CredentialExt,
},
prelude::{MlsConversation, MlsConversationDecryptMessage},
};
use mls_crypto_provider::MlsCryptoProvider;
use openmls::prelude::{
ConfirmationTag, ContentType, CredentialWithKey, FramedContentBodyIn, MlsMessageIn, MlsMessageInBody, Sender,
};
impl MlsConversation {
pub(crate) fn extract_confirmation_tag_from_own_commit<'a>(
&self,
own_commit: &'a MlsMessageIn,
) -> Result<&'a ConfirmationTag> {
if let MlsMessageInBody::PublicMessage(msg) = own_commit.body_as_ref() {
let is_commit = matches!(msg.content_type(), ContentType::Commit);
let own_index = self.group.own_leaf_index();
let is_self_sent = matches!(msg.sender(), Sender::Member(i) if i == &own_index);
let is_own_commit = is_commit && is_self_sent;
assert!(
is_own_commit,
"extract_confirmation_tag_from_own_commit() must always be called with an own commit."
);
assert!(
matches!(msg.body(), FramedContentBodyIn::Commit(_)),
"extract_confirmation_tag_from_own_commit() must always be called with an own commit."
);
msg.auth
.confirmation_tag
.as_ref()
.ok_or(Error::MlsMessageInvalidState("Message confirmation tag not present"))
} else {
panic!(
"extract_confirmation_tag_from_own_commit() must always be called \
with an MlsMessageIn containing an MlsMessageInBody::PublicMessage"
);
}
}
pub(crate) async fn handle_own_commit(
&mut self,
backend: &MlsCryptoProvider,
ct: &ConfirmationTag,
) -> Result<MlsConversationDecryptMessage> {
if self.group.pending_commit().is_some() {
if self.eq_pending_commit(ct) {
self.merge_pending_commit(backend).await
} else {
Err(Error::ClearingPendingCommitError)
}
} else {
Err(Error::SelfCommitIgnored)
}
}
pub(crate) fn eq_pending_commit(&self, commit_ct: &ConfirmationTag) -> bool {
if let Some(pending_commit) = self.group.pending_commit() {
return pending_commit.get_confirmation_tag() == commit_ct;
}
false
}
pub(crate) async fn merge_pending_commit(
&mut self,
backend: &MlsCryptoProvider,
) -> Result<MlsConversationDecryptMessage> {
self.commit_accepted(backend).await?;
let own_leaf = self
.group
.own_leaf()
.ok_or(Error::MlsGroupInvalidState("own_leaf is None"))?;
let own_leaf_credential_with_key = CredentialWithKey {
credential: own_leaf.credential().clone(),
signature_key: own_leaf.signature_key().clone(),
};
let identity = own_leaf_credential_with_key
.extract_identity(self.ciphersuite(), None)
.map_err(RecursiveError::mls_credential("extracting identity"))?;
let crl_new_distribution_points = get_new_crl_distribution_points(
backend,
extract_crl_uris_from_group(&self.group)
.map_err(RecursiveError::mls_credential("extracting crl uris from group"))?,
)
.await
.map_err(RecursiveError::mls_credential("getting new crl distribution points"))?;
Ok(MlsConversationDecryptMessage {
app_msg: None,
proposals: vec![],
is_active: self.group.is_active(),
delay: self.compute_next_commit_delay(),
sender_client_id: None,
has_epoch_changed: true,
identity,
buffered_messages: None,
crl_new_distribution_points,
})
}
}
#[cfg(test)]
mod tests {
use crate::test_utils::*;
use openmls::prelude::{ProcessMessageError, ValidationError};
use super::super::error::Error;
use crate::prelude::MlsError;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
pub async fn should_succeed_when_incoming_commit_same_as_pending(case: TestCase) {
if !case.is_pure_ciphertext() && case.is_x509() {
run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
Box::pin(async move {
let x509_test_chain = alice_central
.x509_test_chain
.as_ref()
.as_ref()
.expect("No x509 test chain");
let id = conversation_id();
alice_central
.context
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
assert!(alice_central.pending_commit(&id).await.is_none());
let intermediate_ca = x509_test_chain.find_local_intermediate_ca();
let (new_handle, new_display_name) = ("new_alice_wire", "New Alice Smith");
let cb = alice_central
.save_new_credential(
&case,
new_handle,
new_display_name,
x509_test_chain.find_certificate_for_actor("alice").unwrap(),
intermediate_ca,
)
.await;
let commit = alice_central.create_unmerged_e2ei_rotate_commit(&id, &cb).await;
assert!(alice_central.pending_commit(&id).await.is_some());
let epoch = alice_central.context.conversation_epoch(&id).await.unwrap();
let decrypt_self = alice_central
.context
.decrypt_message(&id, &commit.commit.to_bytes().unwrap())
.await;
assert!(decrypt_self.is_ok());
let decrypt_self = decrypt_self.unwrap();
let epoch_after_decrypt = alice_central.context.conversation_epoch(&id).await.unwrap();
assert_eq!(epoch + 1, epoch_after_decrypt);
assert!(decrypt_self.proposals.is_empty());
alice_central.verify_sender_identity(&case, &decrypt_self).await;
alice_central
.verify_local_credential_rotated(&id, new_handle, new_display_name)
.await;
})
})
.await
}
}
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
pub async fn should_succeed_when_incoming_commit_mismatches_pending_commit(case: TestCase) {
if !case.is_pure_ciphertext() {
run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
Box::pin(async move {
let id = conversation_id();
alice_central
.context
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
assert!(alice_central.pending_commit(&id).await.is_none());
let unmerged_commit = alice_central.create_unmerged_commit(&id).await.commit;
assert!(alice_central.pending_commit(&id).await.is_some());
alice_central.context.clear_pending_commit(&id).await.unwrap();
assert!(alice_central.pending_commit(&id).await.is_none());
let unmerged_commit2 = alice_central.create_unmerged_commit(&id).await.commit;
assert_ne!(unmerged_commit, unmerged_commit2);
let decrypt = alice_central
.context
.decrypt_message(&id, &unmerged_commit.to_bytes().unwrap())
.await;
assert!(matches!(decrypt.unwrap_err(), Error::ClearingPendingCommitError));
})
})
.await
}
}
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
pub async fn should_ignore_self_incoming_commit_when_no_pending_commit(case: TestCase) {
if !case.is_pure_ciphertext() {
run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
Box::pin(async move {
let id = conversation_id();
alice_central
.context
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
assert!(alice_central.pending_commit(&id).await.is_none());
let commit = alice_central.create_unmerged_commit(&id).await.commit;
assert!(alice_central.pending_commit(&id).await.is_some());
alice_central.context.clear_pending_commit(&id).await.unwrap();
assert!(alice_central.pending_commit(&id).await.is_none());
let decrypt_self = alice_central
.context
.decrypt_message(&id, &commit.to_bytes().unwrap())
.await;
assert!(matches!(decrypt_self.unwrap_err(), Error::SelfCommitIgnored));
})
})
.await
}
}
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
pub async fn should_fail_when_tampering_with_incoming_own_commit_same_as_pending(case: TestCase) {
use crate::MlsErrorKind;
if case.is_pure_ciphertext() {
return;
}
run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
Box::pin(async move {
let conversation_id = conversation_id();
alice_central
.context
.new_conversation(&conversation_id, case.credential_type, case.cfg.clone())
.await
.unwrap();
assert!(alice_central.pending_commit(&conversation_id).await.is_none());
let add_bob_message = alice_central.create_unmerged_commit(&conversation_id).await.commit;
assert!(alice_central.pending_commit(&conversation_id).await.is_some());
let commit_serialized = &mut add_bob_message.to_bytes().unwrap();
commit_serialized[355] = commit_serialized[355].wrapping_add(1);
let decryption_result = alice_central
.context
.decrypt_message(&conversation_id, commit_serialized)
.await;
let error = decryption_result.unwrap_err();
assert!(matches!(
error,
Error::Mls(MlsError {
source: MlsErrorKind::MlsMessageError(ProcessMessageError::ValidationError(
ValidationError::InvalidMembershipTag
)),
..
})
));
assert!(alice_central.pending_commit(&conversation_id).await.is_some());
assert!(
alice_central
.context
.decrypt_message(&conversation_id, &add_bob_message.to_bytes().unwrap())
.await
.is_ok()
);
assert!(alice_central.pending_commit(&conversation_id).await.is_none());
})
})
.await
}
}