// 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(0 x3456, 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" );
}
}
Messung V0.5 in Prozent C=82 H=97 G=89
¤ Dauer der Verarbeitung: 0.1 Sekunden
(vorverarbeitet am 2026-06-23)
¤
*© Formatika GbR, Deutschland