// 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; use alloc::vec::Vec; use core::fmt::{self, Debug}; use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize}; use mls_rs_core::{
crypto::{CipherSuiteProvider, SignatureSecretKey},
error::IntoAnyError,
};
#[cfg_attr(
all(feature = "ffi", not(test)),
safer_ffi_gen::ffi_type(clone, opaque)
)] #[derive(Clone, Debug)] #[non_exhaustive] /// Result of MLS commit operation using /// [`Group::commit`](crate::group::Group::commit) or /// [`CommitBuilder::build`](CommitBuilder::build). pubstruct CommitOutput { /// Commit message to send to other group members. pub commit_message: MlsMessage, /// Welcome messages to send to new group members. If the commit does not add members, /// this list is empty. Otherwise, if [`MlsRules::commit_options`] returns `single_welcome_message` /// set to true, then this list contains a single message sent to all members. Else, the list /// contains one message for each added member. Recipients of each message can be identified using /// [`MlsMessage::key_package_reference`] of their key packages and /// [`MlsMessage::welcome_key_package_references`]. pub welcome_messages: Vec<MlsMessage>, /// Ratchet tree that can be sent out of band if /// `ratchet_tree_extension` is not used according to /// [`MlsRules::commit_options`]. pub ratchet_tree: Option<ExportedTree<'static>>, /// A group info that can be provided to new members in order to enable external commit /// functionality. This value is set if [`MlsRules::commit_options`] returns /// `allow_external_commit` set to true. pub external_commit_group_info: Option<MlsMessage>, /// Proposals that were received in the prior epoch but not included in the following commit. #[cfg(feature = "by_ref_proposal")] pub unused_proposals: Vec<crate::mls_rules::ProposalInfo<Proposal>>,
}
#[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen)] impl CommitOutput { /// Commit message to send to other group members. #[cfg(feature = "ffi")] pubfn commit_message(&self) -> &MlsMessage {
&self.commit_message
}
/// Welcome message to send to new group members. #[cfg(feature = "ffi")] pubfn welcome_messages(&self) -> &[MlsMessage] {
&self.welcome_messages
}
/// Ratchet tree that can be sent out of band if /// `ratchet_tree_extension` is not used according to /// [`MlsRules::commit_options`]. #[cfg(feature = "ffi")] pubfn ratchet_tree(&self) -> Option<&ExportedTree<'static>> { self.ratchet_tree.as_ref()
}
/// A group info that can be provided to new members in order to enable external commit /// functionality. This value is set if [`MlsRules::commit_options`] returns /// `allow_external_commit` set to true. #[cfg(feature = "ffi")] pubfn external_commit_group_info(&self) -> Option<&MlsMessage> { self.external_commit_group_info.as_ref()
}
/// Proposals that were received in the prior epoch but not included in the following commit. #[cfg(all(feature = "ffi", feature = "by_ref_proposal"))] pubfn unused_proposals(&self) -> &[crate::mls_rules::ProposalInfo<Proposal>] {
&self.unused_proposals
}
}
/// Build a commit with multiple proposals by-value. /// /// Proposals within a commit can be by-value or by-reference. /// Proposals received during the current epoch will be added to the resulting /// commit by-reference automatically so long as they pass the rules defined /// in the current /// [proposal rules](crate::client_builder::ClientBuilder::mls_rules). pubstruct CommitBuilder<'a, C> where
C: ClientConfig + Clone,
{
group: &'a mut Group<C>, pub(super) proposals: Vec<Proposal>,
authenticated_data: Vec<u8>,
group_info_extensions: ExtensionList,
new_signer: Option<SignatureSecretKey>,
new_signing_identity: Option<SigningIdentity>,
}
impl<'a, C> CommitBuilder<'a, C> where
C: ClientConfig + Clone,
{ /// Insert an [`AddProposal`](crate::group::proposal::AddProposal) into /// the current commit that is being built. pubfn add_member(mutself, key_package: MlsMessage) -> Result<CommitBuilder<'a, C>, MlsError> { let proposal = self.group.add_proposal(key_package)?; self.proposals.push(proposal);
Ok(self)
}
/// Set group info extensions that will be inserted into the resulting /// [welcome messages](CommitOutput::welcome_messages) for new members. /// /// Group info extensions that are transmitted as part of a welcome message /// are encrypted along with other private values. /// /// These extensions can be retrieved as part of /// [`NewMemberInfo`](crate::group::NewMemberInfo) that is returned /// by joining the group via /// [`Client::join_group`](crate::Client::join_group). pubfn set_group_info_ext(self, extensions: ExtensionList) -> Self { Self {
group_info_extensions: extensions,
..self
}
}
/// Insert a [`RemoveProposal`](crate::group::proposal::RemoveProposal) into /// the current commit that is being built. pubfn remove_member(mutself, index: u32) -> Result<Self, MlsError> { let proposal = self.group.remove_proposal(index)?; self.proposals.push(proposal);
Ok(self)
}
/// Insert a /// [`GroupContextExtensions`](crate::group::proposal::Proposal::GroupContextExtensions) /// into the current commit that is being built. pubfn set_group_context_ext(mutself, extensions: ExtensionList) -> Result<Self, MlsError> { let proposal = self.group.group_context_extensions_proposal(extensions); self.proposals.push(proposal);
Ok(self)
}
/// Insert a /// [`PreSharedKeyProposal`](crate::group::proposal::PreSharedKeyProposal) with /// an external PSK into the current commit that is being built. #[cfg(feature = "psk")] pubfn add_external_psk(mutself, psk_id: ExternalPskId) -> Result<Self, MlsError> { let key_id = JustPreSharedKeyID::External(psk_id); let proposal = self.group.psk_proposal(key_id)?; self.proposals.push(proposal);
Ok(self)
}
/// Insert a /// [`PreSharedKeyProposal`](crate::group::proposal::PreSharedKeyProposal) with /// a resumption PSK into the current commit that is being built. #[cfg(feature = "psk")] pubfn add_resumption_psk(mutself, psk_epoch: u64) -> Result<Self, MlsError> { let psk_id = ResumptionPsk {
psk_epoch,
usage: ResumptionPSKUsage::Application,
psk_group_id: PskGroupId(self.group.group_id().to_vec()),
};
let key_id = JustPreSharedKeyID::Resumption(psk_id); let proposal = self.group.psk_proposal(key_id)?; self.proposals.push(proposal);
Ok(self)
}
/// Insert a [`ReInitProposal`](crate::group::proposal::ReInitProposal) into /// the current commit that is being built. pubfn reinit( mutself,
group_id: Option<Vec<u8>>,
version: ProtocolVersion,
cipher_suite: CipherSuite,
extensions: ExtensionList,
) -> Result<Self, MlsError> { let proposal = self
.group
.reinit_proposal(group_id, version, cipher_suite, extensions)?;
self.proposals.push(proposal);
Ok(self)
}
/// Insert a [`CustomProposal`](crate::group::proposal::CustomProposal) into /// the current commit that is being built. #[cfg(feature = "custom_proposal")] pubfn custom_proposal(mutself, proposal: CustomProposal) -> Self { self.proposals.push(Proposal::Custom(proposal)); self
}
/// Insert a proposal that was previously constructed such as when a /// proposal is returned from /// [`StateUpdate::unused_proposals`](super::StateUpdate::unused_proposals). pubfn raw_proposal(mutself, proposal: Proposal) -> Self { self.proposals.push(proposal); self
}
/// Insert proposals that were previously constructed such as when a /// proposal is returned from /// [`StateUpdate::unused_proposals`](super::StateUpdate::unused_proposals). pubfn raw_proposals(mutself, mut proposals: Vec<Proposal>) -> Self { self.proposals.append(&mut proposals); self
}
/// Add additional authenticated data to the commit. /// /// # Warning /// /// The data provided here is always sent unencrypted. pubfn authenticated_data(self, authenticated_data: Vec<u8>) -> Self { Self {
authenticated_data,
..self
}
}
/// Change the committer's signing identity as part of making this commit. /// This will only succeed if the [`IdentityProvider`](crate::IdentityProvider) /// in use by the group considers the credential inside this signing_identity /// [valid](crate::IdentityProvider::validate_member) /// and results in the same /// [identity](crate::IdentityProvider::identity) /// being used. pubfn set_new_signing_identity( self,
signer: SignatureSecretKey,
signing_identity: SigningIdentity,
) -> Self { Self {
new_signer: Some(signer),
new_signing_identity: Some(signing_identity),
..self
}
}
/// Finalize the commit to send. /// /// # Errors /// /// This function will return an error if any of the proposals provided /// are not contextually valid according to the rules defined by the /// MLS RFC, or if they do not pass the custom rules defined by the current /// [proposal rules](crate::client_builder::ClientBuilder::mls_rules). #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn build(self) -> Result<CommitOutput, MlsError> { self.group
.commit_internal( self.proposals,
None, self.authenticated_data, self.group_info_extensions, self.new_signer, self.new_signing_identity,
)
.await
}
}
impl<C> Group<C> where
C: ClientConfig + Clone,
{ /// Perform a commit of received proposals. /// /// This function is the equivalent of [`Group::commit_builder`] immediately /// followed by [`CommitBuilder::build`]. Any received proposals since the /// last commit will be included in the resulting message by-reference. /// /// Data provided in the `authenticated_data` field will be placed into /// the resulting commit message unencrypted. /// /// # Pending Commits /// /// When a commit is created, it is not applied immediately in order to /// allow for the resolution of conflicts when multiple members of a group /// attempt to make commits at the same time. For example, a central relay /// can be used to decide which commit should be accepted by the group by /// determining a consistent view of commit packet order for all clients. /// /// Pending commits are stored internally as part of the group's state /// so they do not need to be tracked outside of this library. Any commit /// message that is processed before calling [Group::apply_pending_commit] /// will clear the currently pending commit. /// /// # Empty Commits /// /// Sending a commit that contains no proposals is a valid operation /// within the MLS protocol. It is useful for providing stronger forward /// secrecy and post-compromise security, especially for long running /// groups when group membership does not change often. /// /// # Path Updates /// /// Path updates provide forward secrecy and post-compromise security /// within the MLS protocol. /// The `path_required` option returned by [`MlsRules::commit_options`](`crate::MlsRules::commit_options`) /// controls the ability of a group to send a commit without a path update. /// An update path will automatically be sent if there are no proposals /// in the commit, or if any proposal other than /// [`Add`](crate::group::proposal::Proposal::Add), /// [`Psk`](crate::group::proposal::Proposal::Psk), /// or [`ReInit`](crate::group::proposal::Proposal::ReInit) are part of the commit. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pubasyncfn commit(&mutself, authenticated_data: Vec<u8>) -> Result<CommitOutput, MlsError> { self.commit_internal(
vec![],
None,
authenticated_data,
Default::default(),
None,
None,
)
.await
}
/// Create a new commit builder that can include proposals /// by-value. pubfn commit_builder(&mutself) -> CommitBuilder<C> {
CommitBuilder {
group: self,
proposals: Default::default(),
authenticated_data: Default::default(),
group_info_extensions: Default::default(),
new_signer: Default::default(),
new_signing_identity: Default::default(),
}
}
// Construct an initial Commit object with the proposals field populated from Proposals // received during the current epoch, and an empty path field. Add passed in proposals // by value let sender = if is_external {
Sender::NewMemberCommit
} else {
Sender::Member(*self.private_tree.self_index)
};
let new_signer_ref = new_signer.as_ref().unwrap_or(&self.signer); let old_signer = &self.signer;
#[cfg(feature = "std")] let time = Some(crate::time::MlsTime::now());
#[cfg(not(feature = "std"))] let time = None;
#[cfg(feature = "by_ref_proposal")] let proposals = self.state.proposals.prepare_commit(sender, proposals);
#[cfg(not(feature = "by_ref_proposal"))] let proposals = prepare_commit(sender, proposals);
// Decide whether to populate the path field: If the path field is required based on the // proposals that are in the commit (see above), then it MUST be populated. Otherwise, the // sender MAY omit the path field at its discretion. let commit_options = mls_rules
.commit_options(
&provisional_state.public_tree.roster(),
&provisional_group_context.extensions,
&provisional_state.applied_proposals,
)
.map_err(|e| MlsError::MlsRulesError(e.into_any_error()))?;
let perform_path_update = commit_options.path_required
|| path_update_required(&provisional_state.applied_proposals);
let (update_path, path_secrets, commit_secret) = if perform_path_update { // If populating the path field: Create an UpdatePath using the new tree. Any new // member (from an add proposal) MUST be excluded from the resolution during the // computation of the UpdatePath. The GroupContext for this operation uses the // group_id, epoch, tree_hash, and confirmed_transcript_hash values in the initial // GroupContext object. The leaf_key_package for this UpdatePath must have a // parent_hash extension. let encap_gen = TreeKem::new(
&mut provisional_state.public_tree,
&mut provisional_private_tree,
)
.encap(
&mut provisional_group_context,
&provisional_state.indexes_of_added_kpkgs,
new_signer_ref, self.config.leaf_properties(),
new_signing_identity,
&self.cipher_suite_provider, #[cfg(test)]
&self.commit_modifiers,
)
.await?;
(
Some(encap_gen.update_path),
Some(encap_gen.path_secrets),
encap_gen.commit_secret,
)
} else { // Update the tree hash, since it was not updated by encap.
provisional_state
.public_tree
.update_hashes(
&[provisional_private_tree.self_index],
&self.cipher_suite_provider,
)
.await?;
// Use the signature, the commit_secret and the psk_secret to advance the key schedule and // compute the confirmation_tag value in the MlsPlaintext. let confirmed_transcript_hash = ConfirmedTranscriptHash::create( self.cipher_suite_provider(),
&self.state.interim_transcript_hash,
&auth_content,
)
.await?;
// Generate external commit group info if required by commit_options let external_commit_group_info = match commit_options.allow_external_commit { true => { letmut extensions = ExtensionList::new();
let info = self
.make_group_info(
&provisional_group_context,
extensions,
&confirmation_tag,
new_signer_ref,
)
.await?;
let msg =
MlsMessage::new(self.protocol_version(), MlsMessagePayload::GroupInfo(info));
Some(msg)
} false => None,
};
// Build the group info that will be placed into the welcome messages. // Add the ratchet tree extension if necessary iflet Some(ratchet_tree_ext) = ratchet_tree_ext {
welcome_group_info_extensions.set_from(ratchet_tree_ext)?;
}
// Encrypt the GroupInfo using the key and nonce derived from the joiner_secret for // the new epoch let welcome_secret = WelcomeSecret::from_joiner_secret(
&self.cipher_suite_provider,
&key_schedule_result.joiner_secret,
&psk_secret,
)
.await?;
let encrypted_group_info = welcome_secret
.encrypt(&welcome_group_info.mls_encode_to_vec()?)
.await?;
// Encrypt path secrets and joiner secret to new members let path_secrets = path_secrets.as_ref();
// Construct a GroupInfo reflecting the new state // Group ID, epoch, tree, and confirmed transcript hash from the new state #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] asyncfn make_group_info(
&self,
group_context: &GroupContext,
extensions: ExtensionList,
confirmation_tag: &ConfirmationTag,
signer: &SignatureSecretKey,
) -> Result<GroupInfo, MlsError> { letmut group_info = GroupInfo {
group_context: group_context.clone(),
extensions,
confirmation_tag: confirmation_tag.clone(), // The confirmation_tag from the MlsPlaintext object
signer: LeafIndex(self.current_member_index()),
signature: vec![],
};
group_info.grease(self.cipher_suite_provider())?;
// Sign the GroupInfo using the member's private signing key
group_info
.sign(&self.cipher_suite_provider, signer, &())
.await?;
// Check that the credential was updated by in the committer's state.
groups[0].process_pending_commit().await.unwrap(); let new_member = groups[0].group.roster().member_with_index(0).unwrap();
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] asyncfn member_identity_is_validated_against_new_extensions() { let alice = client_with_test_extension(b"alice").await; letmut alice = alice.create_group(ExtensionList::new()).await.unwrap();
let bob = client_with_test_extension(b"bob").await; let bob_kp = bob.generate_key_package_message().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.