| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| // Copyright by contributors to this project. |
| // SPDX-License-Identifier: (Apache-2.0 OR MIT) |
| |
| use alloc::vec::Vec; |
| use core::{ |
| fmt::{self, Debug}, |
| ops::Deref, |
| }; |
| |
| use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize}; |
| use mls_rs_core::{crypto::CipherSuiteProvider, error::IntoAnyError}; |
| |
| use crate::{ |
| client::MlsError, |
| group::{framing::FramedContent, MessageSignature}, |
| WireFormat, |
| }; |
| |
| use super::{AuthenticatedContent, ConfirmationTag}; |
| |
| #[derive(Clone, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)] |
| #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| pub struct ConfirmedTranscriptHash( |
| #[mls_codec(with = "mls_rs_codec::byte_vec")] |
| #[cfg_attr(feature = "serde", serde(with = "mls_rs_core::vec_serde"))] |
| Vec<u8>, |
| ); |
| |
| impl Debug for ConfirmedTranscriptHash { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| mls_rs_core::debug::pretty_bytes(&self.0) |
| .named("ConfirmedTranscriptHash") |
| .fmt(f) |
| } |
| } |
| |
| impl Deref for ConfirmedTranscriptHash { |
| type Target = Vec<u8>; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.0 |
| } |
| } |
| |
| impl From<Vec<u8>> for ConfirmedTranscriptHash { |
| fn from(value: Vec<u8>) -> Self { |
| Self(value) |
| } |
| } |
| |
| impl ConfirmedTranscriptHash { |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub(crate) async fn create<P: CipherSuiteProvider>( |
| cipher_suite_provider: &P, |
| interim_transcript_hash: &InterimTranscriptHash, |
| content: &AuthenticatedContent, |
| ) -> Result<Self, MlsError> { |
| #[derive(Debug, MlsSize, MlsEncode)] |
| struct ConfirmedTranscriptHashInput<'a> { |
| wire_format: WireFormat, |
| content: &'a FramedContent, |
| signature: &'a MessageSignature, |
| } |
| |
| let input = ConfirmedTranscriptHashInput { |
| wire_format: content.wire_format, |
| content: &content.content, |
| signature: &content.auth.signature, |
| }; |
| |
| let hash_input = [ |
| interim_transcript_hash.deref(), |
| input.mls_encode_to_vec()?.deref(), |
| ] |
| .concat(); |
| |
| cipher_suite_provider |
| .hash(&hash_input) |
| .await |
| .map(Into::into) |
| .map_err(|e| MlsError::CryptoProviderError(e.into_any_error())) |
| } |
| } |
| |
| #[derive(Clone, PartialEq, MlsSize, MlsEncode, MlsDecode)] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| pub(crate) struct InterimTranscriptHash( |
| #[mls_codec(with = "mls_rs_codec::byte_vec")] |
| #[cfg_attr(feature = "serde", serde(with = "mls_rs_core::vec_serde"))] |
| Vec<u8>, |
| ); |
| |
| impl Debug for InterimTranscriptHash { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| mls_rs_core::debug::pretty_bytes(&self.0) |
| .named("InterimTranscriptHash") |
| .fmt(f) |
| } |
| } |
| |
| impl Deref for InterimTranscriptHash { |
| type Target = Vec<u8>; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.0 |
| } |
| } |
| |
| impl From<Vec<u8>> for InterimTranscriptHash { |
| fn from(value: Vec<u8>) -> Self { |
| Self(value) |
| } |
| } |
| |
| impl InterimTranscriptHash { |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn create<P: CipherSuiteProvider>( |
| cipher_suite_provider: &P, |
| confirmed: &ConfirmedTranscriptHash, |
| confirmation_tag: &ConfirmationTag, |
| ) -> Result<Self, MlsError> { |
| #[derive(Debug, MlsSize, MlsEncode)] |
| struct InterimTranscriptHashInput<'a> { |
| confirmation_tag: &'a ConfirmationTag, |
| } |
| |
| let input = InterimTranscriptHashInput { confirmation_tag }.mls_encode_to_vec()?; |
| |
| cipher_suite_provider |
| .hash(&[confirmed.0.deref(), &input].concat()) |
| .await |
| .map(Into::into) |
| .map_err(|e| MlsError::CryptoProviderError(e.into_any_error())) |
| } |
| } |
| |
| // Test vectors come from the MLS interop repository and contain a proposal by reference. |
| #[cfg(feature = "by_ref_proposal")] |
| #[cfg(test)] |
| mod tests { |
| use alloc::vec::Vec; |
| |
| use mls_rs_codec::MlsDecode; |
| |
| use crate::{ |
| crypto::test_utils::try_test_cipher_suite_provider, |
| group::{framing::ContentType, message_signature::AuthenticatedContent, transcript_hashes}, |
| }; |
| |
| #[cfg(not(mls_build_async))] |
| use alloc::{boxed::Box, vec}; |
| |
| #[cfg(not(mls_build_async))] |
| use crate::{ |
| crypto::test_utils::test_cipher_suite_provider, |
| group::{ |
| confirmation_tag::ConfirmationTag, |
| framing::Content, |
| proposal::{Proposal, ProposalOrRef, RemoveProposal}, |
| test_utils::get_test_group_context, |
| Commit, LeafIndex, Sender, |
| }, |
| mls_rs_codec::MlsEncode, |
| CipherSuite, CipherSuiteProvider, WireFormat, |
| }; |
| |
| #[cfg(not(mls_build_async))] |
| use super::{ConfirmedTranscriptHash, InterimTranscriptHash}; |
| |
| #[derive(serde::Serialize, serde::Deserialize, Debug, Default, Clone)] |
| struct TestCase { |
| pub cipher_suite: u16, |
| |
| #[serde(with = "hex::serde")] |
| pub confirmation_key: Vec<u8>, |
| #[serde(with = "hex::serde")] |
| pub authenticated_content: Vec<u8>, |
| #[serde(with = "hex::serde")] |
| pub interim_transcript_hash_before: Vec<u8>, |
| |
| #[serde(with = "hex::serde")] |
| pub confirmed_transcript_hash_after: Vec<u8>, |
| #[serde(with = "hex::serde")] |
| pub interim_transcript_hash_after: Vec<u8>, |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn transcript_hash() { |
| let test_cases: Vec<TestCase> = |
| load_test_case_json!(interop_transcript_hashes, generate_test_vector()); |
| |
| for test_case in test_cases.into_iter() { |
| let Some(cs) = try_test_cipher_suite_provider(test_case.cipher_suite) else { |
| continue; |
| }; |
| |
| let auth_content = |
| AuthenticatedContent::mls_decode(&mut &*test_case.authenticated_content).unwrap(); |
| |
| assert!(auth_content.content.content_type() == ContentType::Commit); |
| |
| let conf_key = &test_case.confirmation_key; |
| let conf_hash_after = test_case.confirmed_transcript_hash_after.into(); |
| let conf_tag = auth_content.auth.confirmation_tag.clone().unwrap(); |
| |
| let matches = conf_tag |
| .matches(conf_key, &conf_hash_after, &cs) |
| .await |
| .unwrap(); |
| |
| assert!(matches); |
| |
| let (expected_interim, expected_conf) = transcript_hashes( |
| &cs, |
| &test_case.interim_transcript_hash_before.into(), |
| &auth_content, |
| ) |
| .await |
| .unwrap(); |
| |
| assert_eq!(*expected_interim, test_case.interim_transcript_hash_after); |
| assert_eq!(expected_conf, conf_hash_after); |
| } |
| } |
| |
| #[cfg(not(mls_build_async))] |
| #[cfg_attr(coverage_nightly, coverage(off))] |
| fn generate_test_vector() -> Vec<TestCase> { |
| CipherSuite::all().fold(vec![], |mut test_cases, cs| { |
| let cs = test_cipher_suite_provider(cs); |
| |
| let context = get_test_group_context(0x3456, cs.cipher_suite()); |
| |
| let proposal = Proposal::Remove(RemoveProposal { |
| to_remove: LeafIndex(1), |
| }); |
| |
| let proposal = ProposalOrRef::Proposal(Box::new(proposal)); |
| |
| let commit = Commit { |
| proposals: vec![proposal], |
| path: None, |
| }; |
| |
| let signer = cs.signature_key_generate().unwrap().0; |
| |
| let mut auth_content = AuthenticatedContent::new_signed( |
| &cs, |
| &context, |
| Sender::Member(0), |
| Content::Commit(alloc::boxed::Box::new(commit)), |
| &signer, |
| WireFormat::PublicMessage, |
| vec![], |
| ) |
| .unwrap(); |
| |
| let interim_hash_before = cs.random_bytes_vec(cs.kdf_extract_size()).unwrap().into(); |
| |
| let conf_hash_after = |
| ConfirmedTranscriptHash::create(&cs, &interim_hash_before, &auth_content).unwrap(); |
| |
| let conf_key = cs.random_bytes_vec(cs.kdf_extract_size()).unwrap(); |
| let conf_tag = ConfirmationTag::create(&conf_key, &conf_hash_after, &cs).unwrap(); |
| |
| let interim_hash_after = |
| InterimTranscriptHash::create(&cs, &conf_hash_after, &conf_tag).unwrap(); |
| |
| auth_content.auth.confirmation_tag = Some(conf_tag); |
| |
| let test_case = TestCase { |
| cipher_suite: cs.cipher_suite().into(), |
| |
| confirmation_key: conf_key, |
| authenticated_content: auth_content.mls_encode_to_vec().unwrap(), |
| interim_transcript_hash_before: interim_hash_before.0, |
| |
| confirmed_transcript_hash_after: conf_hash_after.0, |
| interim_transcript_hash_after: interim_hash_after.0, |
| }; |
| |
| test_cases.push(test_case); |
| test_cases |
| }) |
| } |
| |
| #[cfg(mls_build_async)] |
| fn generate_test_vector() -> Vec<TestCase> { |
| panic!("Tests cannot be generated in async mode"); |
| } |
| } |