// 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 mls_rs_codec::{MlsDecode, MlsEncode, MlsSize}; use mls_rs_core::{
crypto::SignatureSecretKey, error::IntoAnyError, extension::ExtensionList, group::Member,
identity::IdentityProvider,
};
/// The result of processing an [ExternalGroup](ExternalGroup) message using /// [process_incoming_message](ExternalGroup::process_incoming_message) #[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] pubenum ExternalReceivedMessage { /// State update as the result of a successful commit.
Commit(CommitMessageDescription), /// Received proposal and its unique identifier.
Proposal(ProposalMessageDescription), /// Encrypted message that can not be processed.
Ciphertext(ContentType), /// Validated GroupInfo object
GroupInfo(GroupInfo), /// Validated welcome message
Welcome, /// Validated key package
KeyPackage(KeyPackage),
}
/// A handle to an observed group that can track plaintext control messages /// and the resulting group state. #[derive(Clone)] pubstruct ExternalGroup<C> where
C: ExternalClientConfig,
{ pub(crate) config: C, pub(crate) cipher_suite_provider: <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider, pub(crate) state: GroupState, pub(crate) signing_data: Option<(SignatureSecretKey, SigningIdentity)>,
}
/// Process a message that was sent to the group. /// /// * Proposals will be stored in the group state and processed by the /// same rules as a standard group. /// /// * Commits will result in the same outcome as a standard group. /// However, the integrity of the resulting group state can only be partially /// verified, since the external group does have access to the group /// secrets required to do a complete check. /// /// * Application messages are always encrypted so they result in a no-op /// that returns [ExternalReceivedMessage::Ciphertext] /// /// # Warning /// /// Processing an encrypted commit or proposal message has the same result /// as processing an encrypted application message. Proper tracking of /// the group state requires that all proposal and commit messages are /// readable. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn process_incoming_message(
&mutself,
message: MlsMessage,
) -> Result<ExternalReceivedMessage, MlsError> {
MessageProcessor::process_incoming_message( self,
message, #[cfg(feature = "by_ref_proposal")] self.config.cache_proposals(),
)
.await
}
/// Replay a proposal message into the group skipping all validation steps. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn insert_proposal_from_message(
&mutself,
message: MlsMessage,
) -> Result<(), MlsError> { let ptxt = match message.payload {
MlsMessagePayload::Plain(p) => Ok(p),
_ => Err(MlsError::UnexpectedMessageType),
}?;
let auth_content: AuthenticatedContent = ptxt.into();
let proposal_ref =
ProposalRef::from_content(&self.cipher_suite_provider, &auth_content). style='color:red'>await?;
let sender = auth_content.content.sender;
let proposal = match auth_content.content.content {
Content::Proposal(p) => Ok(*p),
_ => Err(MlsError::UnexpectedMessageType),
}?;
/// Force insert a proposal directly into the internal state of the group /// with no validation. #[cfg(feature = "by_ref_proposal")] pubfn insert_proposal(&mutself, proposal: CachedProposal) { self.group_state_mut().proposals.insert(
proposal.proposal_ref,
proposal.proposal,
proposal.sender,
)
}
/// Create an external proposal to request that a group add a new member /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_add(
&mutself,
key_package: MlsMessage,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let key_package = key_package
.into_key_package()
.ok_or(MlsError::UnexpectedMessageType)?;
/// Create an external proposal to request that a group remove an existing member /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_remove(
&mutself,
index: u32,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let to_remove = LeafIndex(index);
// Verify that this leaf is actually in the tree self.group_state().public_tree.get_leaf_node(to_remove)?;
/// Create an external proposal to request that a group inserts an external /// pre shared key into its state. /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_external_psk(
&mutself,
psk: ExternalPskId,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let proposal = self.psk_proposal(JustPreSharedKeyID::External(psk))?; self.propose(proposal, authenticated_data).await
}
/// Create an external proposal to request that a group adds a pre shared key /// from a previous epoch to the current group state. /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(all(feature = "by_ref_proposal", feature = "psk"))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_resumption_psk(
&mutself,
psk_epoch: u64,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let key_id = ResumptionPsk {
psk_epoch,
usage: ResumptionPSKUsage::Application,
psk_group_id: PskGroupId(self.group_context().group_id().to_vec()),
};
let proposal = self.psk_proposal(JustPreSharedKeyID::Resumption(key_id))?; self.propose(proposal, authenticated_data).await
}
/// Create an external proposal to request that a group sets extensions stored in the group /// state. /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_group_context_extensions(
&mutself,
extensions: ExtensionList,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let proposal = Proposal::GroupContextExtensions(extensions); self.propose(proposal, authenticated_data).await
}
/// Create an external proposal to request that a group is reinitialized. /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_reinit(
&mutself,
group_id: Option<Vec<u8>>,
version: ProtocolVersion,
cipher_suite: CipherSuite,
extensions: ExtensionList,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { let group_id = group_id.map(Ok).unwrap_or_else(|| { self.cipher_suite_provider
.random_bytes_vec(self.cipher_suite_provider.kdf_extract_size())
.map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))
})?;
let proposal = Proposal::ReInit(ReInitProposal {
group_id,
version,
cipher_suite,
extensions,
});
/// Create a custom proposal message. /// /// # Warning /// /// In order for the proposal generated by this function to be successfully /// committed, the group needs to have `signing_identity` as an entry /// within an [ExternalSendersExt](crate::extension::built_in::ExternalSendersExt) /// as part of its group context extensions. #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn propose_custom(
&mutself,
proposal: CustomProposal,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> { self.propose(Proposal::Custom(proposal), authenticated_data)
.await
}
/// Delete all sent and received proposals cached for commit. #[cfg(feature = "by_ref_proposal")] pubfn clear_proposal_cache(&mutself) { self.state.proposals.clear()
}
/// Get the current group context summarizing various information about the group. #[inline(always)] pubfn group_context(&self) -> &GroupContext {
&self.group_state().context
}
/// Export the current ratchet tree used within the group. pubfn export_tree(&self) -> Result<Vec<u8>, MlsError> { self.group_state()
.public_tree
.nodes
.mls_encode_to_vec()
.map_err(Into::into)
}
/// Get the current roster of the group. #[inline(always)] pubfn roster(&self) -> Roster { self.group_state().public_tree.roster()
}
/// Find a member based on their identity. /// /// Identities are matched based on the /// [IdentityProvider](crate::IdentityProvider) /// that this group was configured with. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn get_member_with_identity(
&self,
identity_id: &SigningIdentity,
) -> Result<Member, MlsError> { let identity = self
.identity_provider()
.identity(identity_id, self.group_context().extensions())
.await
.map_err(|error| MlsError::IdentityProviderError(error.into_any_error()))?;
let tree = &self.group_state().public_tree;
#[cfg(feature = "tree_index")] let index = tree.get_leaf_node_with_identity(&identity);
#[cfg(not(feature = "tree_index"))] let index = tree
.get_leaf_node_with_identity(
&identity,
&self.identity_provider(), self.group_context().extensions(),
)
.await?;
let index = index.ok_or(MlsError::MemberNotFound)?; let node = self.group_state().public_tree.get_leaf_node(index)?;
Ok(member_from_leaf_node(node, index))
}
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] #[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))] #[cfg_attr(
all(not(target_arch = "wasm32"), mls_build_async),
maybe_async::must_be_async
)] impl<C> MessageProcessor for ExternalGroup<C> where
C: ExternalClientConfig + Clone,
{ type MlsRules = C::MlsRules; type IdentityProvider = C::IdentityProvider; type PreSharedKeyStorage = AlwaysFoundPskStorage; type OutputType = ExternalReceivedMessage; type CipherSuiteProvider = <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider;
let info = alice
.group
.group_info_message_allowing_ext_commit(true)
.await
.unwrap();
let config = TestExternalClientBuilder::new_for_test().build_config(); letmut server = ExternalGroup::join(config, None, info, None).await.unwrap();
for _ in0..2 { let commit = alice.group.commit(vec![]).await.unwrap().commit_message;
alice.process_pending_commit().await.unwrap();
server.process_incoming_message(commit).await.unwrap();
}
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn external_group_can_be_serialized_to_tls_encoding() { let server =
make_external_group(&test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await).await;
let snapshot = server.snapshot().mls_encode_to_vec().unwrap(); let snapshot_restored = ExternalSnapshot::mls_decode(&mut snapshot.as_slice()).unwrap();
let server_restored =
ExternalGroup::from_snapshot(server.config.clone(), snapshot_restored)
.await
.unwrap();
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn external_group_can_validate_info() { let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; letmut server = make_external_group(&alice).await;
let info = alice
.group
.group_info_message_allowing_ext_commit(false)
.await
.unwrap();
let update = server.process_incoming_message(info.clone()).await.unwrap(); let info = info.into_group_info().unwrap();
assert_matches!(update, ExternalReceivedMessage::GroupInfo(update_info) if update_info == info);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn external_group_can_validate_key_package() { let alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; letmut server = make_external_group(&alice).await;
let kp = test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await;
let update = server.process_incoming_message(kp.clone()).await.unwrap(); let kp = kp.into_key_package().unwrap();
assert_matches!(update, ExternalReceivedMessage::KeyPackage(update_kp) if update_kp == kp);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn external_group_can_validate_welcome() { letmut alice = test_group_with_one_commit(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; letmut server = make_external_group(&alice).await;
let [welcome] = alice
.group
.commit_builder()
.add_member(
test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "john").await,
)
.unwrap()
.build()
.await
.unwrap()
.welcome_messages
.try_into()
.unwrap();
let update = server.process_incoming_message(welcome).await.unwrap();
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.