// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // Copyright by contributors to this project. // SPDX-License-Identifier: (Apache-2.0 OR MIT)
/// MLS client used to create key packages and manage groups. /// /// [`Client::builder`] can be used to instantiate it. /// /// Clients are able to support multiple protocol versions, ciphersuites /// and underlying identities used to join groups and generate key packages. /// Applications may decide to create one or many clients depending on their /// specific needs. #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))] #[derive(Clone, Debug)] pubstruct Client<C> { pub(crate) config: C, pub(crate) signing_identity: Option<(SigningIdentity, CipherSuite)>, pub(crate) signer: Option<SignatureSecretKey>, pub(crate) version: ProtocolVersion,
}
impl Client<()> { /// Returns a [`ClientBuilder`] /// used to configure client preferences and providers. pubfn builder() -> ClientBuilder<BaseConfig> {
ClientBuilder::new()
}
}
/// Creates a new key package message that can be used to to add this /// client to a [Group](crate::group::Group). Each call to this function /// will produce a unique value that is signed by `signing_identity`. /// /// The secret keys for the resulting key package message will be stored in /// the [KeyPackageStorage](crate::KeyPackageStorage) /// that was used to configure the client and will /// automatically be erased when this key package is used to /// [join a group](Client::join_group). /// /// # Warning /// /// A key package message may only be used once. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn generate_key_package_message(&self) -> Result<MlsMessage, MlsError> {
Ok(self.generate_key_package().await?.key_package_message())
}
/// Create a group with a specific group_id. /// /// This function behaves the same way as /// [create_group](Client::create_group) except that it /// specifies a specific unique group identifier to be used. /// /// # Warning /// /// It is recommended to use [create_group](Client::create_group) /// instead of this function because it guarantees that group_id values /// are globally unique. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn create_group_with_id(
&self,
group_id: Vec<u8>,
group_context_extensions: ExtensionList,
) -> Result<Group<C>, MlsError> { let (signing_identity, cipher_suite) = self.signing_identity()?;
/// Create a MLS group. /// /// The `cipher_suite` provided must be supported by the /// [CipherSuiteProvider](crate::CipherSuiteProvider) /// that was used to build the client. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn create_group(
&self,
group_context_extensions: ExtensionList,
) -> Result<Group<C>, MlsError> { let (signing_identity, cipher_suite) = self.signing_identity()?;
/// Join a MLS group via a welcome message created by a /// [Commit](crate::group::CommitOutput). /// /// `tree_data` is required to be provided out of band if the client that /// created `welcome_message` did not use the `ratchet_tree_extension` /// according to [`MlsRules::commit_options`](`crate::MlsRules::commit_options`). /// at the time the welcome message was created. `tree_data` can /// be exported from a group using the /// [export tree function](crate::group::Group::export_tree). #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn join_group(
&self,
tree_data: Option<ExportedTree<'_>>,
welcome_message: &MlsMessage,
) -> Result<(Group<C>, NewMemberInfo), MlsError> {
Group::join(
welcome_message,
tree_data, self.config.clone(), self.signer()?.clone(),
)
.await
}
/// 0-RTT add to an existing [group](crate::group::Group) /// /// External commits allow for immediate entry into a /// [group](crate::group::Group), even if all of the group members /// are currently offline and unable to process messages. Sending an /// external commit is only allowed for groups that have provided /// a public `group_info_message` containing an /// [ExternalPubExt](crate::extension::ExternalPubExt), which can be /// generated by an existing group member using the /// [group_info_message](crate::group::Group::group_info_message) /// function. /// /// `tree_data` may be provided following the same rules as [Client::join_group] /// /// If PSKs are provided in `external_psks`, the /// [PreSharedKeyStorage](crate::PreSharedKeyStorage) /// used to configure the client will be searched to resolve their values. /// /// `to_remove` may be used to remove an existing member provided that the /// identity of the existing group member at that [index](crate::group::Member::index) /// is a [valid successor](crate::IdentityProvider::valid_successor) /// of `signing_identity` as defined by the /// [IdentityProvider](crate::IdentityProvider) that this client /// was configured with. /// /// # Warning /// /// Only one external commit can be performed against a given group info. /// There may also be security trade-offs to this approach. /// // TODO: Add a comment about forward secrecy and a pointer to the future // book chapter on this topic #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn commit_external(
&self,
group_info_msg: MlsMessage,
) -> Result<(Group<C>, MlsMessage), MlsError> {
ExternalCommitBuilder::new( self.signer()?.clone(), self.signing_identity()?.0.clone(), self.config.clone(),
)
.build(group_info_msg)
.await
}
/// Load an existing group state into this client using the /// [GroupStateStorage](crate::GroupStateStorage) that /// this client was configured to use. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] #[inline(never)] pubasyncfn load_group(&self, group_id: &[u8]) -> Result<Group<C>, MlsError> { let snapshot = self
.config
.group_state_storage()
.state(group_id)
.await
.map_err(|e| MlsError::GroupStorageError(e.into_any_error()))?
.ok_or(MlsError::GroupNotFound)?;
let snapshot = Snapshot::mls_decode(&mut &*snapshot)?;
/// Request to join an existing [group](crate::group::Group). /// /// An existing group member will need to perform a /// [commit](crate::Group::commit) to complete the add and the resulting /// welcome message can be used by [join_group](Client::join_group). #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn external_add_proposal(
&self,
group_info: &MlsMessage,
tree_data: Option<crate::group::ExportedTree<'_>>,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let protocol_version = group_info.version;
if !self.config.version_supported(protocol_version) && protocol_version == self.version { return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
}
let group_info = group_info
.as_group_info()
.ok_or(MlsError::UnexpectedMessageType)?;
let cipher_suite = group_info.group_context.cipher_suite;
let cipher_suite_provider = self
.config
.crypto_provider()
.cipher_suite_provider(cipher_suite)
.ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
/// Returns key package extensions used by this client pubfn key_package_extensions(&self) -> ExtensionList { self.config.key_package_extensions()
}
/// The [KeyPackageStorage] that this client was configured to use. #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] pubfn key_package_store(&self) -> <C as ClientConfig>::KeyPackageRepository { self.config.key_package_repo()
}
/// The [PreSharedKeyStorage](crate::PreSharedKeyStorage) that /// this client was configured to use. #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] pubfn secret_store(&self) -> <C as ClientConfig>::PskStore { self.config.secret_store()
}
/// The [GroupStateStorage] that this client was configured to use. #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] pubfn group_state_storage(&self) -> <C as ClientConfig>::GroupStateStorage { self.config.group_state_storage()
}
}
#[cfg(test)] pub(crate) mod test_utils { usesuper::*; usecrate::identity::test_utils::get_test_signing_identity;
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn test_keygen() { // This is meant to test the inputs to the internal key package generator // See KeyPackageGenerator tests for key generation specific tests for (protocol_version, cipher_suite) in ProtocolVersion::all().flat_map(|p| {
TestCryptoProvider::all_supported_cipher_suites()
.into_iter()
.map(move |cs| (p, cs))
}) { let (identity, secret_key) = get_test_signing_identity(cipher_suite, b"foo").await;
let client = TestClientBuilder::new_for_test()
.signing_identity(identity.clone(), secret_key, cipher_suite)
.build();
// TODO: Tests around extensions let key_package = client.generate_key_package_message().await.unwrap();
// Check that the new member is in the group
assert!(alice_group
.group
.roster()
.members_iter()
.any(|member| member.signing_identity == bob_identity))
}
#[cfg(feature = "psk")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] asyncfn join_via_external_commit(do_remove: bool, with_psk: bool) -> Result<(), MlsError> { // An external commit cannot be the first commit in a group as it requires // interim_transcript_hash to be computed from the confirmed_transcript_hash and // confirmation_tag, which is not the case for the initial interim_transcript_hash.
let psk = PreSharedKey::from(b"psk".to_vec()); let psk_id = ExternalPskId::new(b"psk id".to_vec());
if !do_remove {
assert!(bob_group.group.roster().members_iter().count() == num_members);
} else { // Bob was removed so his epoch must stay the same
assert_eq!(bob_group.group.current_epoch(), bob_current_epoch);
#[cfg(feature = "state_update")]
assert_matches!(message, ReceivedMessage::Commit(desc) if !desc.state_update.active);
// Comparing epoch authenticators is sufficient to check that members are in sync.
assert_eq!(
alice_group.group.epoch_authenticator().unwrap(),
new_group.epoch_authenticator().unwrap()
);
Ok(())
}
#[cfg(feature = "psk")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn test_external_commit() { // New member can join
join_via_external_commit(false, false).await.unwrap(); // New member can remove an old copy of themselves
join_via_external_commit(true, false).await.unwrap(); // New member can inject a PSK
join_via_external_commit(false, true).await.unwrap(); // All works together
join_via_external_commit(true, true).await.unwrap();
}
let group_info_msg = bob_group
.group
.group_info_message_allowing_ext_commit(true)
.await
.unwrap();
let (carol_identity, secret_key) =
get_test_signing_identity(TEST_CIPHER_SUITE, b"carol").await;
let carol = TestClientBuilder::new_for_test()
.signing_identity(carol_identity, secret_key, TEST_CIPHER_SUITE)
.build();
let (_, external_commit) = carol
.external_commit_builder()
.unwrap()
.build(group_info_msg)
.await
.unwrap();
// If Carol tries to join Alice's group using the group info from Bob's group, that fails. let res = alice_group
.group
.process_incoming_message(external_commit)
.await;
assert_matches!(res, Err(_));
}
#[test] fn builder_can_be_obtained_from_client_to_edit_properties_for_new_client() { let alice = TestClientBuilder::new_for_test()
.extension_type(33.into())
.build(); let bob = alice.to_builder().extension_type(34.into()).build();
assert_eq!(bob.config.supported_extensions(), [33, 34].map(Into::into));
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.16 Sekunden
(vorverarbeitet am 2026-06-18)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.