Revision control
Copy as Markdown
Other Tools
// 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 crate::cipher_suite::CipherSuite;
use crate::client_builder::{recreate_config, BaseConfig, ClientBuilder, MakeConfig};
use crate::client_config::ClientConfig;
use crate::group::framing::MlsMessage;
#[cfg(feature = "by_ref_proposal")]
use crate::group::{
framing::{Content, MlsMessagePayload, PublicMessage, Sender, WireFormat},
message_signature::AuthenticatedContent,
proposal::{AddProposal, Proposal},
};
use crate::group::{snapshot::Snapshot, ExportedTree, Group, NewMemberInfo};
use crate::identity::SigningIdentity;
use crate::key_package::{KeyPackageGeneration, KeyPackageGenerator};
use crate::protocol_version::ProtocolVersion;
use crate::tree_kem::node::NodeIndex;
use alloc::vec::Vec;
use mls_rs_codec::MlsDecode;
use mls_rs_core::crypto::{CryptoProvider, SignatureSecretKey};
use mls_rs_core::error::{AnyError, IntoAnyError};
use mls_rs_core::extension::{ExtensionError, ExtensionList, ExtensionType};
use mls_rs_core::group::{GroupStateStorage, ProposalType};
use mls_rs_core::identity::CredentialType;
use mls_rs_core::key_package::KeyPackageStorage;
use crate::group::external_commit::ExternalCommitBuilder;
#[cfg(feature = "by_ref_proposal")]
use alloc::boxed::Box;
#[derive(Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::enum_to_error_code)]
#[non_exhaustive]
pub enum MlsError {
#[cfg_attr(feature = "std", error(transparent))]
IdentityProviderError(AnyError),
#[cfg_attr(feature = "std", error(transparent))]
CryptoProviderError(AnyError),
#[cfg_attr(feature = "std", error(transparent))]
KeyPackageRepoError(AnyError),
#[cfg_attr(feature = "std", error(transparent))]
GroupStorageError(AnyError),
#[cfg_attr(feature = "std", error(transparent))]
PskStoreError(AnyError),
#[cfg_attr(feature = "std", error(transparent))]
MlsRulesError(AnyError),
#[cfg_attr(feature = "std", error(transparent))]
SerializationError(AnyError),
#[cfg_attr(feature = "std", error(transparent))]
ExtensionError(AnyError),
#[cfg_attr(feature = "std", error("Cipher suite does not match"))]
CipherSuiteMismatch,
#[cfg_attr(feature = "std", error("Invalid commit, missing required path"))]
CommitMissingPath,
#[cfg_attr(feature = "std", error("plaintext message for incorrect epoch"))]
InvalidEpoch,
#[cfg_attr(feature = "std", error("invalid signature found"))]
InvalidSignature,
#[cfg_attr(feature = "std", error("invalid confirmation tag"))]
InvalidConfirmationTag,
#[cfg_attr(feature = "std", error("invalid membership tag"))]
InvalidMembershipTag,
#[cfg_attr(feature = "std", error("corrupt private key, missing required values"))]
InvalidTreeKemPrivateKey,
#[cfg_attr(feature = "std", error("key package not found, unable to process"))]
WelcomeKeyPackageNotFound,
#[cfg_attr(feature = "std", error("leaf not found in tree for index {0}"))]
LeafNotFound(u32),
#[cfg_attr(feature = "std", error("message from self can't be processed"))]
CantProcessMessageFromSelf,
#[cfg_attr(
feature = "std",
error("pending proposals found, commit required before application messages can be sent")
)]
CommitRequired,
#[cfg_attr(
feature = "std",
error("ratchet tree not provided or discovered in GroupInfo")
)]
RatchetTreeNotFound,
#[cfg_attr(feature = "std", error("External sender cannot commit"))]
ExternalSenderCannotCommit,
#[cfg_attr(feature = "std", error("Unsupported protocol version {0:?}"))]
UnsupportedProtocolVersion(ProtocolVersion),
#[cfg_attr(feature = "std", error("Protocol version mismatch"))]
ProtocolVersionMismatch,
#[cfg_attr(feature = "std", error("Unsupported cipher suite {0:?}"))]
UnsupportedCipherSuite(CipherSuite),
#[cfg_attr(feature = "std", error("Signing key of external sender is unknown"))]
UnknownSigningIdentityForExternalSender,
#[cfg_attr(
feature = "std",
error("External proposals are disabled for this group")
)]
ExternalProposalsDisabled,
#[cfg_attr(
feature = "std",
error("Signing identity is not allowed to externally propose")
)]
InvalidExternalSigningIdentity,
#[cfg_attr(feature = "std", error("Missing ExternalPub extension"))]
MissingExternalPubExtension,
#[cfg_attr(feature = "std", error("Epoch not found"))]
EpochNotFound,
#[cfg_attr(feature = "std", error("Unencrypted application message"))]
UnencryptedApplicationMessage,
#[cfg_attr(
feature = "std",
error("NewMemberCommit sender type can only be used to send Commit content")
)]
ExpectedCommitForNewMemberCommit,
#[cfg_attr(
feature = "std",
error("NewMemberProposal sender type can only be used to send add proposals")
)]
ExpectedAddProposalForNewMemberProposal,
#[cfg_attr(
feature = "std",
error("External commit missing ExternalInit proposal")
)]
ExternalCommitMissingExternalInit,
#[cfg_attr(
feature = "std",
error(
"A ReIinit has been applied. The next action must be creating or receiving a welcome."
)
)]
GroupUsedAfterReInit,
#[cfg_attr(feature = "std", error("Pending ReIinit not found."))]
PendingReInitNotFound,
#[cfg_attr(
feature = "std",
error("The extensions in the welcome message and in the reinit do not match.")
)]
ReInitExtensionsMismatch,
#[cfg_attr(feature = "std", error("signer not found for given identity"))]
SignerNotFound,
#[cfg_attr(feature = "std", error("commit already pending"))]
ExistingPendingCommit,
#[cfg_attr(feature = "std", error("pending commit not found"))]
PendingCommitNotFound,
#[cfg_attr(feature = "std", error("unexpected message type for action"))]
UnexpectedMessageType,
#[cfg_attr(
feature = "std",
error("membership tag on MlsPlaintext for non-member sender")
)]
MembershipTagForNonMember,
#[cfg_attr(feature = "std", error("No member found for given identity id."))]
MemberNotFound,
#[cfg_attr(feature = "std", error("group not found"))]
GroupNotFound,
#[cfg_attr(feature = "std", error("unexpected PSK ID"))]
UnexpectedPskId,
#[cfg_attr(feature = "std", error("invalid sender for content type"))]
InvalidSender,
#[cfg_attr(feature = "std", error("GroupID mismatch"))]
GroupIdMismatch,
#[cfg_attr(feature = "std", error("storage retention can not be zero"))]
NonZeroRetentionRequired,
#[cfg_attr(feature = "std", error("Too many PSK IDs to compute PSK secret"))]
TooManyPskIds,
#[cfg_attr(feature = "std", error("Missing required Psk"))]
MissingRequiredPsk,
#[cfg_attr(feature = "std", error("Old group state not found"))]
OldGroupStateNotFound,
#[cfg_attr(feature = "std", error("leaf secret already consumed"))]
InvalidLeafConsumption,
#[cfg_attr(feature = "std", error("key not available, invalid generation {0}"))]
KeyMissing(u32),
#[cfg_attr(
feature = "std",
error("requested generation {0} is too far ahead of current generation")
)]
InvalidFutureGeneration(u32),
#[cfg_attr(feature = "std", error("leaf node has no children"))]
LeafNodeNoChildren,
#[cfg_attr(feature = "std", error("root node has no parent"))]
LeafNodeNoParent,
#[cfg_attr(feature = "std", error("index out of range"))]
InvalidTreeIndex,
#[cfg_attr(feature = "std", error("time overflow"))]
TimeOverflow,
#[cfg_attr(feature = "std", error("invalid leaf_node_source"))]
InvalidLeafNodeSource,
#[cfg_attr(feature = "std", error("key package has expired or is not valid yet"))]
InvalidLifetime,
#[cfg_attr(feature = "std", error("required extension not found"))]
RequiredExtensionNotFound(ExtensionType),
#[cfg_attr(feature = "std", error("required proposal not found"))]
RequiredProposalNotFound(ProposalType),
#[cfg_attr(feature = "std", error("required credential not found"))]
RequiredCredentialNotFound(CredentialType),
#[cfg_attr(feature = "std", error("capabilities must describe extensions used"))]
ExtensionNotInCapabilities(ExtensionType),
#[cfg_attr(feature = "std", error("expected non-blank node"))]
ExpectedNode,
#[cfg_attr(feature = "std", error("node index is out of bounds {0}"))]
InvalidNodeIndex(NodeIndex),
#[cfg_attr(feature = "std", error("unexpected empty node found"))]
UnexpectedEmptyNode,
#[cfg_attr(
feature = "std",
error("duplicate signature key, hpke key or identity found at index {0}")
)]
DuplicateLeafData(u32),
#[cfg_attr(
feature = "std",
error("In-use credential type not supported by new leaf at index")
)]
InUseCredentialTypeUnsupportedByNewLeaf,
#[cfg_attr(
feature = "std",
error("Not all members support the credential type used by new leaf")
)]
CredentialTypeOfNewLeafIsUnsupported,
#[cfg_attr(
feature = "std",
error("the length of the update path is different than the length of the direct path")
)]
WrongPathLen,
#[cfg_attr(
feature = "std",
error("same HPKE leaf key before and after applying the update path for leaf {0}")
)]
SameHpkeKey(u32),
#[cfg_attr(feature = "std", error("init key is not valid for cipher suite"))]
InvalidInitKey,
#[cfg_attr(
feature = "std",
error("init key can not be equal to leaf node public key")
)]
InitLeafKeyEquality,
#[cfg_attr(feature = "std", error("different identity in update for leaf {0}"))]
DifferentIdentityInUpdate(u32),
#[cfg_attr(feature = "std", error("update path pub key mismatch"))]
PubKeyMismatch,
#[cfg_attr(feature = "std", error("tree hash mismatch"))]
TreeHashMismatch,
#[cfg_attr(feature = "std", error("bad update: no suitable secret key"))]
UpdateErrorNoSecretKey,
#[cfg_attr(feature = "std", error("invalid lca, not found on direct path"))]
LcaNotFoundInDirectPath,
#[cfg_attr(feature = "std", error("update path parent hash mismatch"))]
ParentHashMismatch,
#[cfg_attr(feature = "std", error("unexpected pattern of unmerged leaves"))]
UnmergedLeavesMismatch,
#[cfg_attr(feature = "std", error("empty tree"))]
UnexpectedEmptyTree,
#[cfg_attr(feature = "std", error("trailing blanks"))]
UnexpectedTrailingBlanks,
// Proposal Rules errors
#[cfg_attr(
feature = "std",
error("Commiter must not include any update proposals generated by the commiter")
)]
InvalidCommitSelfUpdate,
#[cfg_attr(feature = "std", error("A PreSharedKey proposal must have a PSK of type External or type Resumption and usage Application"))]
InvalidTypeOrUsageInPreSharedKeyProposal,
#[cfg_attr(feature = "std", error("psk nonce length does not match cipher suite"))]
InvalidPskNonceLength,
#[cfg_attr(
feature = "std",
error("ReInit proposal protocol version is less than the version of the original group")
)]
InvalidProtocolVersionInReInit,
#[cfg_attr(feature = "std", error("More than one proposal applying to leaf: {0}"))]
MoreThanOneProposalForLeaf(u32),
#[cfg_attr(
feature = "std",
error("More than one GroupContextExtensions proposal")
)]
MoreThanOneGroupContextExtensionsProposal,
#[cfg_attr(feature = "std", error("Invalid proposal type for sender"))]
InvalidProposalTypeForSender,
#[cfg_attr(
feature = "std",
error("External commit must have exactly one ExternalInit proposal")
)]
ExternalCommitMustHaveExactlyOneExternalInit,
#[cfg_attr(feature = "std", error("External commit must have a new leaf"))]
ExternalCommitMustHaveNewLeaf,
#[cfg_attr(
feature = "std",
error("External commit contains removal of other identity")
)]
ExternalCommitRemovesOtherIdentity,
#[cfg_attr(
feature = "std",
error("External commit contains more than one Remove proposal")
)]
ExternalCommitWithMoreThanOneRemove,
#[cfg_attr(feature = "std", error("Duplicate PSK IDs"))]
DuplicatePskIds,
#[cfg_attr(
feature = "std",
error("Invalid proposal type {0:?} in external commit")
)]
InvalidProposalTypeInExternalCommit(ProposalType),
#[cfg_attr(feature = "std", error("Committer can not remove themselves"))]
CommitterSelfRemoval,
#[cfg_attr(
feature = "std",
error("Only members can commit proposals by reference")
)]
OnlyMembersCanCommitProposalsByRef,
#[cfg_attr(feature = "std", error("Other proposal with ReInit"))]
OtherProposalWithReInit,
#[cfg_attr(feature = "std", error("Unsupported group extension {0:?}"))]
UnsupportedGroupExtension(ExtensionType),
#[cfg_attr(feature = "std", error("Unsupported custom proposal type {0:?}"))]
UnsupportedCustomProposal(ProposalType),
#[cfg_attr(feature = "std", error("by-ref proposal not found"))]
ProposalNotFound,
#[cfg_attr(
feature = "std",
error("Removing non-existing member (or removing a member twice)")
)]
RemovingNonExistingMember,
#[cfg_attr(feature = "std", error("Updated identity not a valid successor"))]
InvalidSuccessor,
#[cfg_attr(
feature = "std",
error("Updating non-existing member (or updating a member twice)")
)]
UpdatingNonExistingMember,
#[cfg_attr(feature = "std", error("Failed generating next path secret"))]
FailedGeneratingPathSecret,
#[cfg_attr(feature = "std", error("Invalid group info"))]
InvalidGroupInfo,
#[cfg_attr(feature = "std", error("Invalid welcome message"))]
InvalidWelcomeMessage,
}
impl IntoAnyError for MlsError {
#[cfg(feature = "std")]
fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
Ok(self.into())
}
}
impl From<mls_rs_codec::Error> for MlsError {
#[inline]
fn from(e: mls_rs_codec::Error) -> Self {
MlsError::SerializationError(e.into_any_error())
}
}
impl From<ExtensionError> for MlsError {
#[inline]
fn from(e: ExtensionError) -> Self {
MlsError::ExtensionError(e.into_any_error())
}
}
/// 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)]
pub struct 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.
pub fn builder() -> ClientBuilder<BaseConfig> {
ClientBuilder::new()
}
}
#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
impl<C> Client<C>
where
C: ClientConfig + Clone,
{
pub(crate) fn new(
config: C,
signer: Option<SignatureSecretKey>,
signing_identity: Option<(SigningIdentity, CipherSuite)>,
version: ProtocolVersion,
) -> Self {
Client {
config,
signer,
signing_identity,
version,
}
}
#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
pub fn to_builder(&self) -> ClientBuilder<MakeConfig<C>> {
ClientBuilder::from_config(recreate_config(
self.config.clone(),
self.signer.clone(),
self.signing_identity.clone(),
self.version,
))
}
/// 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)]
pub async fn generate_key_package_message(&self) -> Result<MlsMessage, MlsError> {
Ok(self.generate_key_package().await?.key_package_message())
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn generate_key_package(&self) -> Result<KeyPackageGeneration, MlsError> {
let (signing_identity, cipher_suite) = self.signing_identity()?;
let cipher_suite_provider = self
.config
.crypto_provider()
.cipher_suite_provider(cipher_suite)
.ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
let key_package_generator = KeyPackageGenerator {
protocol_version: self.version,
cipher_suite_provider: &cipher_suite_provider,
signing_key: self.signer()?,
signing_identity,
identity_provider: &self.config.identity_provider(),
};
let key_pkg_gen = key_package_generator
.generate(
self.config.lifetime(),
self.config.capabilities(),
self.config.key_package_extensions(),
self.config.leaf_node_extensions(),
)
.await?;
let (id, key_package_data) = key_pkg_gen.to_storage()?;
self.config
.key_package_repo()
.insert(id, key_package_data)
.await
.map_err(|e| MlsError::KeyPackageRepoError(e.into_any_error()))?;
Ok(key_pkg_gen)
}
/// 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)]
pub async fn 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()?;
Group::new(
self.config.clone(),
Some(group_id),
cipher_suite,
self.version,
signing_identity.clone(),
group_context_extensions,
self.signer()?.clone(),
)
.await
}
/// 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)]
pub async fn create_group(
&self,
group_context_extensions: ExtensionList,
) -> Result<Group<C>, MlsError> {
let (signing_identity, cipher_suite) = self.signing_identity()?;
Group::new(
self.config.clone(),
None,
cipher_suite,
self.version,
signing_identity.clone(),
group_context_extensions,
self.signer()?.clone(),
)
.await
}
/// 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)]
pub async fn 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)]
pub async fn 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
}
pub fn external_commit_builder(&self) -> Result<ExternalCommitBuilder<C>, MlsError> {
Ok(ExternalCommitBuilder::new(
self.signer()?.clone(),
self.signing_identity()?.0.clone(),
self.config.clone(),
))
}
/// 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)]
pub async fn 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)?;
Group::from_snapshot(self.config.clone(), snapshot).await
}
/// 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)]
pub async fn 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))?;
crate::group::validate_group_info_joiner(
protocol_version,
group_info,
tree_data,
&self.config.identity_provider(),
&cipher_suite_provider,
)
.await?;
let key_package = self.generate_key_package().await?.key_package;
(key_package.cipher_suite == cipher_suite)
.then_some(())
.ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
let message = AuthenticatedContent::new_signed(
&cipher_suite_provider,
&group_info.group_context,
Sender::NewMemberProposal,
Content::Proposal(Box::new(Proposal::Add(Box::new(AddProposal {
key_package,
})))),
self.signer()?,
WireFormat::PublicMessage,
authenticated_data,
)
.await?;
let plaintext = PublicMessage {
content: message.content,
auth: message.auth,
membership_tag: None,
};
Ok(MlsMessage {
version: protocol_version,
payload: MlsMessagePayload::Plain(plaintext),
})
}
fn signer(&self) -> Result<&SignatureSecretKey, MlsError> {
self.signer.as_ref().ok_or(MlsError::SignerNotFound)
}
#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
pub fn signing_identity(&self) -> Result<(&SigningIdentity, CipherSuite), MlsError> {
self.signing_identity
.as_ref()
.map(|(id, cs)| (id, *cs))
.ok_or(MlsError::SignerNotFound)
}
/// Returns key package extensions used by this client
pub fn 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)]
pub fn 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)]
pub fn 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)]
pub fn group_state_storage(&self) -> <C as ClientConfig>::GroupStateStorage {
self.config.group_state_storage()
}
}
#[cfg(test)]
pub(crate) mod test_utils {
use super::*;
use crate::identity::test_utils::get_test_signing_identity;
pub use crate::client_builder::test_utils::{TestClientBuilder, TestClientConfig};
pub const TEST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::MLS_10;
pub const TEST_CIPHER_SUITE: CipherSuite = CipherSuite::P256_AES128;
pub const TEST_CUSTOM_PROPOSAL_TYPE: ProposalType = ProposalType::new(65001);
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn test_client_with_key_pkg(
protocol_version: ProtocolVersion,
cipher_suite: CipherSuite,
identity: &str,
) -> (Client<TestClientConfig>, MlsMessage) {
test_client_with_key_pkg_custom(protocol_version, cipher_suite, identity, |_| {}).await
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn test_client_with_key_pkg_custom<F>(
protocol_version: ProtocolVersion,
cipher_suite: CipherSuite,
identity: &str,
mut config: F,
) -> (Client<TestClientConfig>, MlsMessage)
where
F: FnMut(&mut TestClientConfig),
{
let (identity, secret_key) =
get_test_signing_identity(cipher_suite, identity.as_bytes()).await;
let mut client = TestClientBuilder::new_for_test()
.used_protocol_version(protocol_version)
.signing_identity(identity.clone(), secret_key, cipher_suite)
.build();
config(&mut client.config);
let key_package = client.generate_key_package_message().await.unwrap();
(client, key_package)
}
}
#[cfg(test)]
mod tests {
use super::test_utils::*;
use super::*;
use crate::{
crypto::test_utils::TestCryptoProvider,
identity::test_utils::{get_test_basic_credential, get_test_signing_identity},
tree_kem::leaf_node::LeafNodeSource,
};
use assert_matches::assert_matches;
use crate::{
group::{
message_processor::ProposalMessageDescription,
proposal::Proposal,
test_utils::{test_group, test_group_custom_config},
ReceivedMessage,
},
psk::{ExternalPskId, PreSharedKey},
};
use alloc::vec;
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn 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();
assert_eq!(key_package.version, protocol_version);
let key_package = key_package.into_key_package().unwrap();
assert_eq!(key_package.cipher_suite, cipher_suite);
assert_eq!(
&key_package.leaf_node.signing_identity.credential,
&get_test_basic_credential(b"foo".to_vec())
);
assert_eq!(key_package.leaf_node.signing_identity, identity);
let capabilities = key_package.leaf_node.ungreased_capabilities();
assert_eq!(capabilities, client.config.capabilities());
let client_lifetime = client.config.lifetime();
assert_matches!(key_package.leaf_node.leaf_node_source, LeafNodeSource::KeyPackage(lifetime) if (lifetime.not_after - lifetime.not_before) == (client_lifetime.not_after - client_lifetime.not_before));
}
}
#[cfg(feature = "by_ref_proposal")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn new_member_add_proposal_adds_to_group() {
let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let (bob_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await;
let bob = TestClientBuilder::new_for_test()
.signing_identity(bob_identity.clone(), secret_key, TEST_CIPHER_SUITE)
.build();
let proposal = bob
.external_add_proposal(
&alice_group.group.group_info_message(true).await.unwrap(),
None,
vec![],
)
.await
.unwrap();
let message = alice_group
.group
.process_incoming_message(proposal)
.await
.unwrap();
assert_matches!(
message,
ReceivedMessage::Proposal(ProposalMessageDescription {
proposal: Proposal::Add(p), ..}
) if p.key_package.leaf_node.signing_identity == bob_identity
);
alice_group.group.commit(vec![]).await.unwrap();
alice_group.group.apply_pending_commit().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)]
async fn 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());
let mut alice_group =
test_group_custom_config(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, |c| {
c.psk(psk_id.clone(), psk.clone())
})
.await;
let (mut bob_group, _) = alice_group
.join_with_custom_config("bob", false, |c| {
c.0.psk_store.insert(psk_id.clone(), psk.clone());
})
.await
.unwrap();
let group_info_msg = alice_group
.group
.group_info_message_allowing_ext_commit(true)
.await
.unwrap();
let new_client_id = if do_remove { "bob" } else { "charlie" };
let (new_client_identity, secret_key) =
get_test_signing_identity(TEST_CIPHER_SUITE, new_client_id.as_bytes()).await;
let new_client = TestClientBuilder::new_for_test()
.psk(psk_id.clone(), psk)
.signing_identity(new_client_identity.clone(), secret_key, TEST_CIPHER_SUITE)
.build();
let mut builder = new_client.external_commit_builder().unwrap();
if do_remove {
builder = builder.with_removal(1);
}
if with_psk {
builder = builder.with_external_psk(psk_id);
}
let (new_group, external_commit) = builder.build(group_info_msg).await?;
let num_members = if do_remove { 2 } else { 3 };
assert_eq!(new_group.roster().members_iter().count(), num_members);
let _ = alice_group
.group
.process_incoming_message(external_commit.clone())
.await
.unwrap();
let bob_current_epoch = bob_group.group.current_epoch();
let message = bob_group
.group
.process_incoming_message(external_commit)
.await
.unwrap();
assert!(alice_group.group.roster().members_iter().count() == num_members);
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);
#[cfg(not(feature = "state_update"))]
assert_matches!(message, ReceivedMessage::Commit(_));
}
// 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))]
async fn 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();
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn creating_an_external_commit_requires_a_group_info_message() {
let (alice_identity, secret_key) =
get_test_signing_identity(TEST_CIPHER_SUITE, b"alice").await;
let alice = TestClientBuilder::new_for_test()
.signing_identity(alice_identity.clone(), secret_key, TEST_CIPHER_SUITE)
.build();
let msg = alice.generate_key_package_message().await.unwrap();
let res = alice.commit_external(msg).await.map(|_| ());
assert_matches!(res, Err(MlsError::UnexpectedMessageType));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn external_commit_with_invalid_group_info_fails() {
let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let mut bob_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
bob_group.group.commit(vec![]).await.unwrap();
bob_group.group.apply_pending_commit().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));
}
}