Quelle mod.rs
Sprache: unbekannt
|
|
Untersuchungsergebnis.rs Download desUnknown {[0] [0] [0]}zum Wurzelverzeichnis wechseln
// 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::error::IntoAnyError;
use mls_rs_core::secret::Secret;
use mls_rs_core::time::MlsTime;
use crate::cipher_suite::CipherSuite;
use crate::client::MlsError;
use crate::client_config::ClientConfig;
use crate::crypto::{HpkeCiphertext, SignatureSecretKey};
use crate::extension::RatchetTreeExt;
use crate::identity::SigningIdentity;
use crate::key_package::{KeyPackage, KeyPackageRef};
use crate::protocol_version::ProtocolVersion;
use crate::psk::secret::PskSecret;
use crate::psk::PreSharedKeyID;
use crate::signer::Signable;
use crate::tree_kem::hpke_encryption::HpkeEncryptable;
use crate::tree_kem::kem::TreeKem;
use crate::tree_kem::node::LeafIndex;
use crate::tree_kem::path_secret::PathSecret;
pub use crate::tree_kem::Capabilities;
use crate::tree_kem::{
leaf_node::LeafNode,
leaf_node_validator::{LeafNodeValidator, ValidationContext},
};
use crate::tree_kem::{math as tree_math, ValidatedUpdatePath};
use crate::tree_kem::{TreeKemPrivate, TreeKemPublic};
use crate::{CipherSuiteProvider, CryptoProvider};
#[cfg(feature = "by_ref_proposal")]
use crate::crypto::{HpkePublicKey, HpkeSecretKey};
use crate::extension::ExternalPubExt;
#[cfg(feature = "private_message")]
use self::mls_rules::{EncryptionOptions, MlsRules};
#[cfg(feature = "psk")]
pub use self::resumption::ReinitClient;
#[cfg(feature = "psk")]
use crate::psk::{
resolver::PskResolver, secret::PskSecretInput, ExternalPskId, JustPreSharedKeyID, Ps kGroupId,
ResumptionPSKUsage, ResumptionPsk,
};
#[cfg(all(feature = "std", feature = "by_ref_proposal"))]
use std::collections::HashMap;
#[cfg(feature = "private_message")]
use ciphertext_processor::*;
use confirmation_tag::*;
use framing::*;
use key_schedule::*;
use membership_tag::*;
use message_signature::*;
use message_verifier::*;
use proposal::*;
#[cfg(feature = "by_ref_proposal")]
use proposal_cache::*;
use state::*;
use transcript_hash::*;
#[cfg(test)]
pub(crate) use self::commit::test_utils::CommitModifiers;
#[cfg(all(test, feature = "private_message"))]
pub use self::framing::PrivateMessage;
#[cfg(feature = "psk")]
use self::proposal_filter::ProposalInfo;
#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
use secret_tree::*;
#[cfg(feature = "prior_epoch")]
use self::epoch::PriorEpoch;
use self::epoch::EpochSecrets;
pub use self::message_processor::{
ApplicationMessageDescription, CommitMessageDescription, ProposalMessageDescription,
ProposalSender, ReceivedMessage, StateUpdate,
};
use self::message_processor::{EventOrContent, MessageProcessor, ProvisionalState};
#[cfg(feature = "by_ref_proposal")]
use self::proposal_ref::ProposalRef;
use self::state_repo::GroupStateRepository;
pub use group_info::GroupInfo;
pub use self::framing::{ContentType, Sender};
pub use commit::*;
pub use context::GroupContext;
pub use roster::*;
pub(crate) use transcript_hash::ConfirmedTranscriptHash;
pub(crate) use util::*;
#[cfg(all(feature = "by_ref_proposal", feature = "external_client"))]
pub use self::message_processor::CachedProposal;
#[cfg(feature = "private_message")]
mod ciphertext_processor;
mod commit;
pub(crate) mod confirmation_tag;
mod context;
pub(crate) mod epoch;
pub(crate) mod framing;
mod group_info;
pub(crate) mod key_schedule;
mod membership_tag;
pub(crate) mod message_processor;
pub(crate) mod message_signature;
pub(crate) mod message_verifier;
pub mod mls_rules;
#[cfg(feature = "private_message")]
pub(crate) mod padding;
/// Proposals to evolve a MLS [`Group`]
pub mod proposal;
mod proposal_cache;
pub(crate) mod proposal_filter;
#[cfg(feature = "by_ref_proposal")]
pub(crate) mod proposal_ref;
#[cfg(feature = "psk")]
mod resumption;
mod roster;
pub(crate) mod snapshot;
pub(crate) mod state;
#[cfg(feature = "prior_epoch")]
pub(crate) mod state_repo;
#[cfg(not(feature = "prior_epoch"))]
pub(crate) mod state_repo_light;
#[cfg(not(feature = "prior_epoch"))]
pub(crate) use state_repo_light as state_repo;
pub(crate) mod transcript_hash;
mod util;
/// External commit building.
pub mod external_commit;
#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
pub(crate) mod secret_tree;
#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
pub use secret_tree::MessageKeyData as MessageKey;
#[cfg(all(test, feature = "rfc_compliant"))]
mod interop_test_vectors;
mod exported_tree;
pub use exported_tree::ExportedTree;
#[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
struct GroupSecrets {
joiner_secret: JoinerSecret,
path_secret: Option<PathSecret>,
psks: Vec<PreSharedKeyID>,
}
impl HpkeEncryptable for GroupSecrets {
const ENCRYPT_LABEL: &'static str = "Welcome";
fn from_bytes(bytes: Vec<u8>) -> Result<Self, MlsError> {
Self::mls_decode(&mut bytes.as_slice()).map_err(Into::into)
}
fn get_bytes(&self) -> Result<Vec<u8>, MlsError> {
self.mls_encode_to_vec().map_err(Into::into)
}
}
#[derive(Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub(crate) struct EncryptedGroupSecrets {
pub new_member: KeyPackageRef,
pub encrypted_group_secrets: HpkeCiphertext,
}
#[derive(Clone, Eq, PartialEq, MlsSize, MlsEncode, MlsDecode)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub(crate) struct Welcome {
pub cipher_suite: CipherSuite,
pub secrets: Vec<EncryptedGroupSecrets>,
#[mls_codec(with = "mls_rs_codec::byte_vec")]
pub encrypted_group_info: Vec<u8>,
}
impl Debug for Welcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Welcome")
.field("cipher_suite", &self.cipher_suite)
.field("secrets", &self.secrets)
.field(
"encrypted_group_info",
&mls_rs_core::debug::pretty_bytes(&self.encrypted_group_info),
)
.finish()
}
}
#[derive(Clone, Debug)]
// #[cfg_attr(
// all(feature = "ffi", not(test)),
// safer_ffi_gen::ffi_type(clone, opaque)
// )]
#[non_exhaustive]
/// Information provided to new members upon joining a group.
pub struct NewMemberInfo {
/// Group info extensions found within the Welcome message used to join
/// the group.
pub group_info_extensions: ExtensionList,
}
// #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
impl NewMemberInfo {
pub(crate) fn new(group_info_extensions: ExtensionList) -> Self {
let mut new_member_info = Self {
group_info_extensions,
};
new_member_info.ungrease();
new_member_info
}
/// Group info extensions found within the Welcome message used to join
/// the group.
#[cfg(feature = "ffi")]
pub fn group_info_extensions(&self) -> &ExtensionList {
&self.group_info_extensions
}
}
/// An MLS end-to-end encrypted group.
///
/// # Group Evolution
///
/// MLS Groups are evolved via a propose-then-commit system. Each group state
/// produced by a commit is called an epoch and can produce and consume
/// application, proposal, and commit messages. A [commit](Group::commit) is used
/// to advance to the next epoch by applying existing proposals sent in
/// the current epoch by-reference along with an optional set of proposals
/// that are included by-value using a [`CommitBuilder`].
// #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))]
#[derive(Clone)]
pub struct Group<C>
where
C: ClientConfig,
{
config: C,
cipher_suite_provider: <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider,
state_repo: GroupStateRepository<C::GroupStateStorage, C::KeyPackageRepository>,
pub(crate) state: GroupState,
epoch_secrets: EpochSecrets,
private_tree: TreeKemPrivate,
key_schedule: KeySchedule,
#[cfg(all(feature = "std", feature = "by_ref_proposal"))]
pending_updates: HashMap<HpkePublicKey, (HpkeSecretKey, Option<SignatureSecretKey>)>, // Hash of leaf node hpke public key to secret key
#[cfg(all(not(feature = "std"), feature = "by_ref_proposal"))]
pending_updates: Vec<(HpkePublicKey, (HpkeSecretKey, Option<SignatureSecretKey>))>,
pending_commit: Option<CommitGeneration>,
#[cfg(feature = "psk")]
previous_psk: Option<PskSecretInput>,
#[cfg(test)]
pub(crate) commit_modifiers: CommitModifiers,
pub(crate) signer: SignatureSecretKey,
}
// #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
impl<C> Group<C>
where
C: ClientConfig + Clone,
{
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub(crate) async fn new(
config: C,
group_id: Option<Vec<u8>>,
cipher_suite: CipherSuite,
protocol_version: ProtocolVersion,
signing_identity: SigningIdentity,
group_context_extensions: ExtensionList,
signer: SignatureSecretKey,
) -> Result<Self, MlsError> {
let cipher_suite_provider = cipher_suite_provider(config.crypto_provider(), cipher_suite)?;
let (leaf_node, leaf_node_secret) = LeafNode::generate(
&cipher_suite_provider,
config.leaf_properties(),
signing_identity,
&signer,
config.lifetime(),
)
.await?;
let identity_provider = config.identity_provider();
let leaf_node_validator = LeafNodeValidator::new(
&cipher_suite_provider,
&identity_provider,
Some(&group_context_extensions),
);
leaf_node_validator
.check_if_valid(&leaf_node, ValidationContext::Add(None))
.await?;
let (mut public_tree, private_tree) = TreeKemPublic::derive(
leaf_node,
leaf_node_secret,
&config.identity_provider(),
&group_context_extensions,
)
.await?;
let tree_hash = public_tree.tree_hash(&cipher_suite_provider).await?;
let group_id = group_id.map(Ok).unwrap_or_else(|| {
cipher_suite_provider
.random_bytes_vec(cipher_suite_provider.kdf_extract_size())
.map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))
})?;
let context = GroupContext::new_group(
protocol_version,
cipher_suite,
group_id,
tree_hash,
group_context_extensions,
);
let state_repo = GroupStateRepository::new(
#[cfg(feature = "prior_epoch")]
context.group_id.clone(),
config.group_state_storage(),
config.key_package_repo(),
None,
)?;
let key_schedule_result = KeySchedule::from_random_epoch_secret(
&cipher_suite_provider,
#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
public_tree.total_leaf_count(),
)
.await?;
let confirmation_tag = ConfirmationTag::create(
&key_schedule_result.confirmation_key,
&vec![].into(),
&cipher_suite_provider,
)
.await?;
let interim_hash = InterimTranscriptHash::create(
&cipher_suite_provider,
&vec![].into(),
&confirmation_tag,
)
.await?;
Ok(Self {
config,
state: GroupState::new(context, public_tree, interim_hash, confirmation_tag),
private_tree,
key_schedule: key_schedule_result.key_schedule,
#[cfg(feature = "by_ref_proposal")]
pending_updates: Default::default(),
pending_commit: None,
#[cfg(test)]
commit_modifiers: Default::default(),
epoch_secrets: key_schedule_result.epoch_secrets,
state_repo,
cipher_suite_provider,
#[cfg(feature = "psk")]
previous_psk: None,
signer,
})
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub(crate) async fn join(
welcome: &MlsMessage,
tree_data: Option<ExportedTree<'_>>,
config: C,
signer: SignatureSecretKey,
) -> Result<(Self, NewMemberInfo), MlsError> {
Self::from_welcome_message(
welcome,
tree_data,
config,
signer,
#[cfg(feature = "psk")]
None,
)
.await
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn from_welcome_message(
welcome: &MlsMessage,
tree_data: Option<ExportedTree<'_>>,
config: C,
signer: SignatureSecretKey,
#[cfg(feature = "psk")] additional_psk: Option<PskSecretInput>,
) -> Result<(Self, NewMemberInfo), MlsError> {
let protocol_version = welcome.version;
if !config.version_supported(protocol_version) {
return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
}
let MlsMessagePayload::Welcome(welcome) = &welcome.payload else {
return Err(MlsError::UnexpectedMessageType);
};
let cipher_suite_provider =
cipher_suite_provider(config.crypto_provider(), welcome.cipher_suite)?;
let (encrypted_group_secrets, key_package_generation) =
find_key_package_generation(&config.key_package_repo(), &welcome.secrets).await?;
let key_package_version = key_package_generation.key_package.version;
if key_package_version != protocol_version {
return Err(MlsError::ProtocolVersionMismatch);
}
// Decrypt the encrypted_group_secrets using HPKE with the algorithms indicated by the
// cipher suite and the HPKE private key corresponding to the GroupSecrets. If a
// PreSharedKeyID is part of the GroupSecrets and the client is not in possession of
// the corresponding PSK, return an error
let group_secrets = GroupSecrets::decrypt(
&cipher_suite_provider,
&key_package_generation.init_secret_key,
&key_package_generation.key_package.hpke_init_key,
&welcome.encrypted_group_info,
&encrypted_group_secrets.encrypted_group_secrets,
)
.await?;
#[cfg(feature = "psk")]
let psk_secret = if let Some(psk) = additional_psk {
let psk_id = group_secrets
.psks
.first()
.ok_or(MlsError::UnexpectedPskId)?;
match &psk_id.key_id {
JustPreSharedKeyID::Resumption(r) if r.usage != ResumptionPSKUsage::Application => {
Ok(())
}
_ => Err(MlsError::UnexpectedPskId),
}?;
let mut psk = psk;
psk.id.psk_nonce = psk_id.psk_nonce.clone();
PskSecret::calculate(&[psk], &cipher_suite_provider).await?
} else {
PskResolver::<
<C as ClientConfig>::GroupStateStorage,
<C as ClientConfig>::KeyPackageRepository,
<C as ClientConfig>::PskStore,
> {
group_context: None,
current_epoch: None,
prior_epochs: None,
psk_store: &config.secret_store(),
}
.resolve_to_secret(&group_secrets.psks, &cipher_suite_provider)
.await?
};
#[cfg(not(feature = "psk"))]
let psk_secret = PskSecret::new(&cipher_suite_provider);
// From the joiner_secret in the decrypted GroupSecrets object and the PSKs specified in
// the GroupSecrets, derive the welcome_secret and using that the welcome_key and
// welcome_nonce.
let welcome_secret = WelcomeSecret::from_joiner_secret(
&cipher_suite_provider,
&group_secrets.joiner_secret,
&psk_secret,
)
.await?;
// Use the key and nonce to decrypt the encrypted_group_info field.
let decrypted_group_info = welcome_secret
.decrypt(&welcome.encrypted_group_info)
.await?;
let group_info = GroupInfo::mls_decode(&mut &**decrypted_group_info)?;
let public_tree = validate_group_info_joiner(
protocol_version,
&group_info,
tree_data,
&config.identity_provider(),
&cipher_suite_provider,
)
.await?;
// Identify a leaf in the tree array (any even-numbered node) whose leaf_node is identical
// to the leaf_node field of the KeyPackage. If no such field exists, return an error. Let
// index represent the index of this node among the leaves in the tree, namely the index of
// the node in the tree array divided by two.
let self_index = public_tree
.find_leaf_node(&key_package_generation.key_package.leaf_node)
.ok_or(MlsError::WelcomeKeyPackageNotFound)?;
let used_key_package_ref = key_package_generation.reference;
let mut private_tree =
TreeKemPrivate::new_self_leaf(self_index, key_package_generation.leaf_node_secret_key);
// If the path_secret value is set in the GroupSecrets object
if let Some(path_secret) = group_secrets.path_secret {
private_tree
.update_secrets(
&cipher_suite_provider,
group_info.signer,
path_secret,
&public_tree,
)
.await?;
}
// Use the joiner_secret from the GroupSecrets object to generate the epoch secret and
// other derived secrets for the current epoch.
let key_schedule_result = KeySchedule::from_joiner(
&cipher_suite_provider,
&group_secrets.joiner_secret,
&group_info.group_context,
#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
public_tree.total_leaf_count(),
&psk_secret,
)
.await?;
// Verify the confirmation tag in the GroupInfo using the derived confirmation key and the
// confirmed_transcript_hash from the GroupInfo.
if !group_info
.confirmation_tag
.matches(
&key_schedule_result.confirmation_key,
&group_info.group_context.confirmed_transcript_hash,
&cipher_suite_provider,
)
.await?
{
return Err(MlsError::InvalidConfirmationTag);
}
Self::join_with(
config,
group_info,
public_tree,
key_schedule_result.key_schedule,
key_schedule_result.epoch_secrets,
private_tree,
Some(used_key_package_ref),
signer,
)
.await
}
#[allow(clippy::too_many_arguments)]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn join_with(
config: C,
group_info: GroupInfo,
public_tree: TreeKemPublic,
key_schedule: KeySchedule,
epoch_secrets: EpochSecrets,
private_tree: TreeKemPrivate,
used_key_package_ref: Option<KeyPackageRef>,
signer: SignatureSecretKey,
) -> Result<(Self, NewMemberInfo), MlsError> {
let cs = group_info.group_context.cipher_suite;
let cs = config
.crypto_provider()
.cipher_suite_provider(cs)
.ok_or(MlsError::UnsupportedCipherSuite(cs))?;
// Use the confirmed transcript hash and confirmation tag to compute the interim transcript
// hash in the new state.
let interim_transcript_hash = InterimTranscriptHash::create(
&cs,
&group_info.group_context.confirmed_transcript_hash,
&group_info.confirmation_tag,
)
.await?;
let state_repo = GroupStateRepository::new(
#[cfg(feature = "prior_epoch")]
group_info.group_context.group_id.clone(),
config.group_state_storage(),
config.key_package_repo(),
used_key_package_ref,
)?;
let group = Group {
config,
state: GroupState::new(
group_info.group_context,
public_tree,
interim_transcript_hash,
group_info.confirmation_tag,
),
private_tree,
key_schedule,
#[cfg(feature = "by_ref_proposal")]
pending_updates: Default::default(),
pending_commit: None,
#[cfg(test)]
commit_modifiers: Default::default(),
epoch_secrets,
state_repo,
cipher_suite_provider: cs,
#[cfg(feature = "psk")]
previous_psk: None,
signer,
};
Ok((group, NewMemberInfo::new(group_info.extensions)))
}
#[inline(always)]
pub(crate) fn current_epoch_tree(&self) -> &TreeKemPublic {
&self.state.public_tree
}
/// The current epoch of the group. This value is incremented each
/// time a [`Group::commit`] message is processed.
#[inline(always)]
pub fn current_epoch(&self) -> u64 {
self.context().epoch
}
/// Index within the group's state for the local group instance.
///
/// This index corresponds to indexes in content descriptions within
/// [`ReceivedMessage`].
#[inline(always)]
pub fn current_member_index(&self) -> u32 {
self.private_tree.self_index.0
}
fn current_user_leaf_node(&self) -> Result<&LeafNode, MlsError> {
self.current_epoch_tree()
.get_leaf_node(self.private_tree.self_index)
}
/// Signing identity currently in use by the local group instance.
// #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
pub fn current_member_signing_identity(&self) -> Result<&SigningIdentity, MlsError> {
self.current_user_leaf_node().map(|ln| &ln.signing_identity)
}
/// Member at a specific index in the group state.
///
/// These indexes correspond to indexes in content descriptions within
/// [`ReceivedMessage`].
pub fn member_at_index(&self, index: u32) -> Option<Member> {
let leaf_index = LeafIndex(index);
self.current_epoch_tree()
.get_leaf_node(leaf_index)
.ok()
.map(|ln| member_from_leaf_node(ln, leaf_index))
}
#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn proposal_message(
&mut self,
proposal: Proposal,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> {
let sender = Sender::Member(*self.private_tree.self_index);
let auth_content = AuthenticatedContent::new_signed(
&self.cipher_suite_provider,
self.context(),
sender,
Content::Proposal(alloc::boxed::Box::new(proposal.clone())),
&self.signer,
#[cfg(feature = "private_message")]
self.encryption_options()?.control_wire_format(sender),
#[cfg(not(feature = "private_message"))]
WireFormat::PublicMessage,
authenticated_data,
)
.await?;
let proposal_ref =
ProposalRef::from_content(&self.cipher_suite_provider, &auth_content).await?;
self.state
.proposals
.insert(proposal_ref, proposal, auth_content.content.sender);
self.format_for_wire(auth_content).await
}
/// Unique identifier for this group.
pub fn group_id(&self) -> &[u8] {
&self.context().group_id
}
fn provisional_private_tree(
&self,
provisional_state: &ProvisionalState,
) -> Result<(TreeKemPrivate, Option<SignatureSecretKey>), MlsError> {
let mut provisional_private_tree = self.private_tree.clone();
let self_index = provisional_private_tree.self_index;
// Remove secret keys for blanked nodes
let path = provisional_state
.public_tree
.nodes
.direct_copath(self_index);
provisional_private_tree
.secret_keys
.resize(path.len() + 1, None);
for (i, n) in path.iter().enumerate() {
if provisional_state.public_tree.nodes.is_blank(n.path)? {
provisional_private_tree.secret_keys[i + 1] = None;
}
}
// Apply own update
let new_signer = None;
#[cfg(feature = "by_ref_proposal")]
let mut new_signer = new_signer;
#[cfg(feature = "by_ref_proposal")]
for p in &provisional_state.applied_proposals.updates {
if p.sender == Sender::Member(*self_index) {
let leaf_pk = &p.proposal.leaf_node.public_key;
// Update the leaf in the private tree if this is our update
#[cfg(feature = "std")]
let new_leaf_sk_and_signer = self.pending_updates.get(leaf_pk);
#[cfg(not(feature = "std"))]
let new_leaf_sk_and_signer = self
.pending_updates
.iter()
.find_map(|(pk, sk)| (pk == leaf_pk).then_some(sk));
let new_leaf_sk = new_leaf_sk_and_signer.map(|(sk, _)| sk.clone());
new_signer = new_leaf_sk_and_signer.and_then(|(_, sk)| sk.clone());
provisional_private_tree
.update_leaf(new_leaf_sk.ok_or(MlsError::UpdateErrorNoSecretKey)?);
break;
}
}
Ok((provisional_private_tree, new_signer))
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn encrypt_group_secrets(
&self,
key_package: &KeyPackage,
leaf_index: LeafIndex,
joiner_secret: &JoinerSecret,
path_secrets: Option<&Vec<Option<PathSecret>>>,
#[cfg(feature = "psk")] psks: Vec<PreSharedKeyID>,
encrypted_group_info: &[u8],
) -> Result<EncryptedGroupSecrets, MlsError> {
let path_secret = path_secrets
.map(|secrets| {
secrets
.get(
tree_math::leaf_lca_level(*self.private_tree.self_index, *leaf_index)
as usize
- 1,
)
.cloned()
.flatten()
.ok_or(MlsError::InvalidTreeKemPrivateKey)
})
.transpose()?;
#[cfg(not(feature = "psk"))]
let psks = Vec::new();
let group_secrets = GroupSecrets {
joiner_secret: joiner_secret.clone(),
path_secret,
psks,
};
let encrypted_group_secrets = group_secrets
.encrypt(
&self.cipher_suite_provider,
&key_package.hpke_init_key,
encrypted_group_info,
)
.await?;
Ok(EncryptedGroupSecrets {
new_member: key_package
.to_reference(&self.cipher_suite_provider)
.await?,
encrypted_group_secrets,
})
}
/// Create a proposal message that adds a new member to the group.
///
/// `authenticated_data` will be sent unencrypted along with the contents
/// of the proposal message.
#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn propose_add(
&mut self,
key_package: MlsMessage,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> {
let proposal = self.add_proposal(key_package)?;
self.proposal_message(proposal, authenticated_data).await
}
fn add_proposal(&self, key_package: MlsMessage) -> Result<Proposal, MlsError> {
Ok(Proposal::Add(alloc::boxed::Box::new(AddProposal {
key_package: key_package
.into_key_package()
.ok_or(MlsError::UnexpectedMessageType)?,
})))
}
/// Create a proposal message that updates your own public keys.
///
/// This proposal is useful for contributing additional forward secrecy
/// and post-compromise security to the group without having to perform
/// the necessary computation of a [`Group::commit`].
///
/// `authenticated_data` will be sent unencrypted along with the contents
/// of the proposal message.
#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn propose_update(
&mut self,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> {
let proposal = self.update_proposal(None, None).await?;
self.proposal_message(proposal, authenticated_data).await
}
/// Create a proposal message that updates your own public keys
/// as well as your credential.
///
/// This proposal is useful for contributing additional forward secrecy
/// and post-compromise security to the group without having to perform
/// the necessary computation of a [`Group::commit`].
///
/// Identity updates are allowed by the group by default assuming that the
/// new identity provided is considered
/// [valid](crate::IdentityProvider::validate_member)
/// by and matches the output of the
/// [identity](crate::IdentityProvider)
/// function of the current
/// [`IdentityProvider`](crate::IdentityProvider).
///
/// `authenticated_data` will be sent unencrypted along with the contents
/// of the proposal message.
#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn propose_update_with_identity(
&mut self,
signer: SignatureSecretKey,
signing_identity: SigningIdentity,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> {
let proposal = self
.update_proposal(Some(signer), Some(signing_identity))
.await?;
self.proposal_message(proposal, authenticated_data).await
}
#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn update_proposal(
&mut self,
signer: Option<SignatureSecretKey>,
signing_identity: Option<SigningIdentity>,
) -> Result<Proposal, MlsError> {
// Grab a copy of the current node and update it to have new key material
let mut new_leaf_node = self.current_user_leaf_node()?.clone();
let secret_key = new_leaf_node
.update(
&self.cipher_suite_provider,
self.group_id(),
self.current_member_index(),
self.config.leaf_properties(),
signing_identity,
signer.as_ref().unwrap_or(&self.signer),
)
.await?;
// Store the secret key in the pending updates storage for later
#[cfg(feature = "std")]
self.pending_updates
.insert(new_leaf_node.public_key.clone(), (secret_key, signer));
#[cfg(not(feature = "std"))]
self.pending_updates
.push((new_leaf_node.public_key.clone(), (secret_key, signer)));
Ok(Proposal::Update(UpdateProposal {
leaf_node: new_leaf_node,
}))
}
/// Create a proposal message that removes an existing member from the
/// group.
///
/// `authenticated_data` will be sent unencrypted along with the contents
/// of the proposal message.
#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn propose_remove(
&mut self,
index: u32,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> {
let proposal = self.remove_proposal(index)?;
self.proposal_message(proposal, authenticated_data).await
}
fn remove_proposal(&self, index: u32) -> Result<Proposal, MlsError> {
let leaf_index = LeafIndex(index);
// Verify that this leaf is actually in the tree
self.current_epoch_tree().get_leaf_node(leaf_index)?;
Ok(Proposal::Remove(RemoveProposal {
to_remove: leaf_index,
}))
}
/// Create a proposal message that adds an external pre shared key to the group.
///
/// Each group member will need to have the PSK associated with
/// [`ExternalPskId`](mls_rs_core::psk::ExternalPskId) installed within
/// the [`PreSharedKeyStorage`](mls_rs_core::psk::PreSharedKeyStorage)
/// in use by this group upon processing a [commit](Group::commit) that
/// contains this proposal.
///
/// `authenticated_data` will be sent unencrypted along with the contents
/// of the proposal message.
#[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn propose_external_psk(
&mut self,
psk: ExternalPskId,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> {
let proposal = self.psk_proposal(JustPreSharedKeyID::External(psk))?;
self.proposal_message(proposal, authenticated_data).await
}
#[cfg(feature = "psk")]
fn psk_proposal(&self, key_id: JustPreSharedKeyID) -> Result<Proposal, MlsError> {
Ok(Proposal::Psk(PreSharedKeyProposal {
psk: PreSharedKeyID::new(key_id, &self.cipher_suite_provider)?,
}))
}
/// Create a proposal message that adds a pre shared key from a previous
/// epoch to the current group state.
///
/// Each group member will need to have the secret state from `psk_epoch`.
/// In particular, the members who joined between `psk_epoch` and the
/// current epoch cannot process a commit containing this proposal.
///
/// `authenticated_data` will be sent unencrypted along with the contents
/// of the proposal message.
#[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn propose_resumption_psk(
&mut self,
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_id().to_vec()),
};
let proposal = self.psk_proposal(JustPreSharedKeyID::Resumption(key_id))?;
self.proposal_message(proposal, authenticated_data).await
}
/// Create a proposal message that requests for this group to be
/// reinitialized.
///
/// Once a [`ReInitProposal`](proposal::ReInitProposal)
/// has been sent, another group member can complete reinitialization of
/// the group by calling [`Group::get_reinit_client`].
///
/// `authenticated_data` will be sent unencrypted along with the contents
/// of the proposal message.
#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn propose_reinit(
&mut self,
group_id: Option<Vec<u8>>,
version: ProtocolVersion,
cipher_suite: CipherSuite,
extensions: ExtensionList,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> {
let proposal = self.reinit_proposal(group_id, version, cipher_suite, extensions)?;
self.proposal_message(proposal, authenticated_data).await
}
fn reinit_proposal(
&self,
group_id: Option<Vec<u8>>,
version: ProtocolVersion,
cipher_suite: CipherSuite,
extensions: ExtensionList,
) -> Result<Proposal, 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()))
})?;
Ok(Proposal::ReInit(ReInitProposal {
group_id,
version,
cipher_suite,
extensions,
}))
}
/// Create a proposal message that sets extensions stored in the group
/// state.
///
/// # Warning
///
/// This function does not create a diff that will be applied to the
/// current set of extension that are in use. In order for an existing
/// extension to not be overwritten by this proposal, it must be included
/// in the new set of extensions being proposed.
///
///
/// `authenticated_data` will be sent unencrypted along with the contents
/// of the proposal message.
#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn propose_group_context_extensions(
&mut self,
extensions: ExtensionList,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> {
let proposal = self.group_context_extensions_proposal(extensions);
self.proposal_message(proposal, authenticated_data).await
}
fn group_context_extensions_proposal(&self, extensions: ExtensionList) -> Proposal {
Proposal::GroupContextExtensions(extensions)
}
/// Create a custom proposal message.
///
/// `authenticated_data` will be sent unencrypted along with the contents
/// of the proposal message.
#[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn propose_custom(
&mut self,
proposal: CustomProposal,
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> {
self.proposal_message(Proposal::Custom(proposal), authenticated_data)
.await
}
/// Delete all sent and received proposals cached for commit.
#[cfg(feature = "by_ref_proposal")]
pub fn clear_proposal_cache(&mut self) {
self.state.proposals.clear()
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub(crate) async fn format_for_wire(
&mut self,
content: AuthenticatedContent,
) -> Result<MlsMessage, MlsError> {
#[cfg(feature = "private_message")]
let payload = if content.wire_format == WireFormat::PrivateMessage {
MlsMessagePayload::Cipher(self.create_ciphertext(content).await?)
} else {
MlsMessagePayload::Plain(self.create_plaintext(content).await?)
};
#[cfg(not(feature = "private_message"))]
let payload = MlsMessagePayload::Plain(self.create_plaintext(content).await?);
Ok(MlsMessage::new(self.protocol_version(), payload))
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn create_plaintext(
&self,
auth_content: AuthenticatedContent,
) -> Result<PublicMessage, MlsError> {
let membership_tag = if matches!(auth_content.content.sender, Sender::Member(_)) {
let tag = self
.key_schedule
.get_membership_tag(&auth_content, self.context(), &self.cipher_suite_provider)
.await?;
Some(tag)
} else {
None
};
Ok(PublicMessage {
content: auth_content.content,
auth: auth_content.auth,
membership_tag,
})
}
#[cfg(feature = "private_message")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn create_ciphertext(
&mut self,
auth_content: AuthenticatedContent,
) -> Result<PrivateMessage, MlsError> {
let padding_mode = self.encryption_options()?.padding_mode;
let mut encryptor = CiphertextProcessor::new(self, self.cipher_suite_provider.clone());
encryptor.seal(auth_content, padding_mode).await
}
/// Encrypt an application message using the current group state.
///
/// `authenticated_data` will be sent unencrypted along with the contents
/// of the proposal message.
#[cfg(feature = "private_message")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn encrypt_application_message(
&mut self,
message: &[u8],
authenticated_data: Vec<u8>,
) -> Result<MlsMessage, MlsError> {
// A group member that has observed one or more proposals within an epoch MUST send a Commit message
// before sending application data
#[cfg(feature = "by_ref_proposal")]
if !self.state.proposals.is_empty() {
return Err(MlsError::CommitRequired);
}
let auth_content = AuthenticatedContent::new_signed(
&self.cipher_suite_provider,
self.context(),
Sender::Member(*self.private_tree.self_index),
Content::Application(message.to_vec().into()),
&self.signer,
WireFormat::PrivateMessage,
authenticated_data,
)
.await?;
self.format_for_wire(auth_content).await
}
#[cfg(feature = "private_message")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn decrypt_incoming_ciphertext(
&mut self,
message: &PrivateMessage,
) -> Result<AuthenticatedContent, MlsError> {
let epoch_id = message.epoch;
let auth_content = if epoch_id == self.context().epoch {
let content = CiphertextProcessor::new(self, self.cipher_suite_provider.clone())
.open(message)
.await?;
verify_auth_content_signature(
&self.cipher_suite_provider,
SignaturePublicKeysContainer::RatchetTree(&self.state.public_tree),
self.context(),
&content,
#[cfg(feature = "by_ref_proposal")]
&[],
)
.await?;
Ok::<_, MlsError>(content)
} else {
#[cfg(feature = "prior_epoch")]
{
let epoch = self
.state_repo
.get_epoch_mut(epoch_id)
.await?
.ok_or(MlsError::EpochNotFound)?;
let content = CiphertextProcessor::new(epoch, self.cipher_suite_provider.clone())
.open(message)
.await?;
verify_auth_content_signature(
&self.cipher_suite_provider,
SignaturePublicKeysContainer::List(&epoch.signature_public_keys),
&epoch.context,
&content,
#[cfg(feature = "by_ref_proposal")]
&[],
)
.await?;
Ok(content)
}
#[cfg(not(feature = "prior_epoch"))]
Err(MlsError::EpochNotFound)
}?;
Ok(auth_content)
}
/// Apply a pending commit that was created by [`Group::commit`] or
/// [`CommitBuilder::build`].
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn apply_pending_commit(&mut self) -> Result<CommitMessageDescription, MlsError> {
let pending_commit = self
.pending_commit
.clone()
.ok_or(MlsError::PendingCommitNotFound)?;
self.process_commit(pending_commit.content, None).await
}
/// Returns true if a commit has been created but not yet applied
/// with [`Group::apply_pending_commit`] or cleared with [`Group::clear_pending_commit`]
pub fn has_pending_commit(&self) -> bool {
self.pending_commit.is_some()
}
/// Clear the currently pending commit.
///
/// This function will automatically be called in the event that a
/// commit message is processed using [`Group::process_incoming_message`]
/// before [`Group::apply_pending_commit`] is called.
pub fn clear_pending_commit(&mut self) {
self.pending_commit = None
}
/// Process an inbound message for this group.
///
/// # Warning
///
/// Changes to the group's state as a result of processing `message` will
/// not be persisted by the
/// [`GroupStateStorage`](crate::GroupStateStorage)
/// in use by this group until [`Group::write_to_storage`] is called.
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
#[inline(never)]
pub async fn process_incoming_message(
&mut self,
message: MlsMessage,
) -> Result<ReceivedMessage, MlsError> {
if let Some(pending) = &self.pending_commit {
let message_hash = CommitHash::compute(&self.cipher_suite_provider, &message).await?;
if message_hash == pending.commit_message_hash {
let message_description = self.apply_pending_commit().await?;
return Ok(ReceivedMessage::Commit(message_description));
}
}
MessageProcessor::process_incoming_message(
self,
message,
#[cfg(feature = "by_ref_proposal")]
true,
)
.await
}
/// Process an inbound message for this group, providing additional context
/// with a message timestamp.
///
/// Providing a timestamp is useful when the
/// [`IdentityProvider`](crate::IdentityProvider)
/// in use by the group can determine validity based on a timestamp.
/// For example, this allows for checking X.509 certificate expiration
/// at the time when `message` was received by a server rather than when
/// a specific client asynchronously received `message`
///
/// # Warning
///
/// Changes to the group's state as a result of processing `message` will
/// not be persisted by the
/// [`GroupStateStorage`](crate::GroupStateStorage)
/// in use by this group until [`Group::write_to_storage`] is called.
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn process_incoming_message_with_time(
&mut self,
message: MlsMessage,
time: MlsTime,
) -> Result<ReceivedMessage, MlsError> {
MessageProcessor::process_incoming_message_with_time(
self,
message,
#[cfg(feature = "by_ref_proposal")]
true,
Some(time),
)
.await
}
/// Find a group member by
/// [identity](crate::IdentityProvider::identity)
///
/// This function determines identity by calling the
/// [`IdentityProvider`](crate::IdentityProvider)
/// currently in use by the group.
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn member_with_identity(&self, identity: &[u8]) -> Result<Member, MlsError> {
let tree = &self.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.state.context.extensions,
)
.await?;
let index = index.ok_or(MlsError::MemberNotFound)?;
let node = self.state.public_tree.get_leaf_node(index)?;
Ok(member_from_leaf_node(node, index))
}
/// Create a group info message that can be used for external proposals and commits.
///
/// The returned `GroupInfo` is suitable for one external commit for the current epoch.
/// If `with_tree_in_extension` is set to true, the returned `GroupInfo` contains the
/// ratchet tree and therefore contains all information needed to join the group. Otherwise,
/// the ratchet tree must be obtained separately, e.g. via
/// (ExternalClient::export_tree)[crate::external_client::ExternalGroup::export_tree].
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn group_info_message_allowing_ext_commit(
&self,
with_tree_in_extension: bool,
) -> Result<MlsMessage, MlsError> {
let mut extensions = ExtensionList::new();
extensions.set_from({
self.key_schedule
.get_external_key_pair_ext(&self.cipher_suite_provider)
.await?
})?;
self.group_info_message_internal(extensions, with_tree_in_extension)
.await
}
/// Create a group info message that can be used for external proposals.
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn group_info_message(
&self,
with_tree_in_extension: bool,
) -> Result<MlsMessage, MlsError> {
self.group_info_message_internal(ExtensionList::new(), with_tree_in_extension)
.await
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn group_info_message_internal(
&self,
mut initial_extensions: ExtensionList,
with_tree_in_extension: bool,
) -> Result<MlsMessage, MlsError> {
if with_tree_in_extension {
initial_extensions.set_from(RatchetTreeExt {
tree_data: ExportedTree::new(self.state.public_tree.nodes.clone()),
})?;
}
let mut info = GroupInfo {
group_context: self.context().clone(),
extensions: initial_extensions,
confirmation_tag: self.state.confirmation_tag.clone(),
signer: self.private_tree.self_index,
signature: Vec::new(),
};
info.grease(self.cipher_suite_provider())?;
info.sign(&self.cipher_suite_provider, &self.signer, &())
.await?;
Ok(MlsMessage::new(
self.protocol_version(),
MlsMessagePayload::GroupInfo(info),
))
}
/// Get the current group context summarizing various information about the group.
#[inline(always)]
pub fn context(&self) -> &GroupContext {
&self.group_state().context
}
/// Get the
/// [epoch_authenticator](https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#name-key-schedule)
/// of the current epoch.
pub fn epoch_authenticator(&self) -> Result<Secret, MlsError> {
Ok(self.key_schedule.authentication_secret.clone().into())
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn export_secret(
&self,
label: &[u8],
context: &[u8],
len: usize,
) -> Result<Secret, MlsError> {
self.key_schedule
.export_secret(label, context, len, &self.cipher_suite_provider)
.await
.map(Into::into)
}
/// Export the current epoch's ratchet tree in serialized format.
///
/// This function is used to provide the current group tree to new members
/// when the `ratchet_tree_extension` is not used according to [`MlsRules::commit_options`].
pub fn export_tree(&self) -> ExportedTree<'_> {
ExportedTree::new_borrowed(&self.current_epoch_tree().nodes)
}
/// Current version of the MLS protocol in use by this group.
pub fn protocol_version(&self) -> ProtocolVersion {
self.context().protocol_version
}
/// Current cipher suite in use by this group.
pub fn cipher_suite(&self) -> CipherSuite {
self.context().cipher_suite
}
/// Current roster
pub fn roster(&self) -> Roster<'_> {
self.group_state().public_tree.roster()
}
/// Determines equality of two different groups internal states.
/// Useful for testing.
///
pub fn equal_group_state(a: &Group<C>, b: &Group<C>) -> bool {
a.state == b.state && a.key_schedule == b.key_schedule && a.epoch_secrets == b.epoch_secrets
}
#[cfg(feature = "psk")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn get_psk(
&self,
psks: &[ProposalInfo<PreSharedKeyProposal>],
) -> Result<(PskSecret, Vec<PreSharedKeyID>), MlsError> {
if let Some(psk) = self.previous_psk.clone() {
// TODO consider throwing error if psks not empty
let psk_id = vec![psk.id.clone()];
let psk = PskSecret::calculate(&[psk], self.cipher_suite_provider()).await?;
Ok((psk, psk_id))
} else {
let psks = psks
.iter()
.map(|psk| psk.proposal.psk.clone())
.collect::<Vec<_>>();
let psk = PskResolver {
group_context: Some(self.context()),
current_epoch: Some(&self.epoch_secrets),
prior_epochs: Some(&self.state_repo),
psk_store: &self.config.secret_store(),
}
.resolve_to_secret(&psks, self.cipher_suite_provider())
.await?;
Ok((psk, psks))
}
}
#[cfg(feature = "private_message")]
pub(crate) fn encryption_options(&self) -> Result<EncryptionOptions, MlsError> {
self.config
.mls_rules()
.encryption_options(&self.roster(), self.group_context().extensions())
.map_err(|e| MlsError::MlsRulesError(e.into_any_error()))
}
#[cfg(not(feature = "psk"))]
fn get_psk(&self) -> PskSecret {
PskSecret::new(self.cipher_suite_provider())
}
#[cfg(feature = "secret_tree_access")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
#[inline(never)]
pub async fn next_encryption_key(&mut self) -> Result<MessageKey, MlsError> {
self.epoch_secrets
.secret_tree
.next_message_key(
&self.cipher_suite_provider,
crate::tree_kem::node::NodeIndex::from(self.private_tree.self_index),
KeyType::Application,
)
.await
}
#[cfg(feature = "secret_tree_access")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn derive_decryption_key(
&mut self,
sender: u32,
generation: u32,
) -> Result<MessageKey, MlsError> {
self.epoch_secrets
.secret_tree
.message_key_generation(
&self.cipher_suite_provider,
crate::tree_kem::node::NodeIndex::from(sender),
KeyType::Application,
generation,
)
.await
}
}
#[cfg(feature = "private_message")]
impl<C> GroupStateProvider for Group<C>
where
C: ClientConfig + Clone,
{
fn group_context(&self) -> &GroupContext {
self.context()
}
fn self_index(&self) -> LeafIndex {
self.private_tree.self_index
}
fn epoch_secrets_mut(&mut self) -> &mut EpochSecrets {
&mut self.epoch_secrets
}
fn epoch_secrets(&self) -> &EpochSecrets {
&self.epoch_secrets
}
}
#[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 Group<C>
where
C: ClientConfig + Clone,
{
type MlsRules = C::MlsRules;
type IdentityProvider = C::IdentityProvider;
type PreSharedKeyStorage = C::PskStore;
type OutputType = ReceivedMessage;
type CipherSuiteProvider = <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider;
#[cfg(feature = "private_message")]
async fn process_ciphertext(
&mut self,
cipher_text: &PrivateMessage,
) -> Result<EventOrContent<Self::OutputType>, MlsError> {
self.decrypt_incoming_ciphertext(cipher_text)
.await
.map(EventOrContent::Content)
}
async fn verify_plaintext_authentication(
&self,
message: PublicMessage,
) -> Result<EventOrContent<Self::OutputType>, MlsError> {
let auth_content = verify_plaintext_authentication(
&self.cipher_suite_provider,
message,
Some(&self.key_schedule),
Some(self.private_tree.self_index),
&self.state,
)
.await?;
Ok(EventOrContent::Content(auth_content))
}
async fn apply_update_path(
&mut self,
sender: LeafIndex,
update_path: &ValidatedUpdatePath,
provisional_state: &mut ProvisionalState,
) -> Result<Option<(TreeKemPrivate, PathSecret)>, MlsError> {
// Update the private tree to create a provisional private tree
let (mut provisional_private_tree, new_signer) =
self.provisional_private_tree(provisional_state)?;
if let Some(signer) = new_signer {
self.signer = signer;
}
provisional_state
.public_tree
.apply_update_path(
sender,
update_path,
&provisional_state.group_context.extensions,
self.identity_provider(),
self.cipher_suite_provider(),
)
.await?;
if sender == self.private_tree.self_index {
let pending = self
.pending_commit
.as_ref()
.ok_or(MlsError::CantProcessMessageFromSelf)?;
Ok(Some((
pending.pending_private_tree.clone(),
pending.pending_commit_secret.clone(),
)))
} else {
// Update the tree hash to get context for decryption
provisional_state.group_context.tree_hash = provisional_state
.public_tree
.tree_hash(&self.cipher_suite_provider)
.await?;
let context_bytes = provisional_state.group_context.mls_encode_to_vec()?;
TreeKem::new(
&mut provisional_state.public_tree,
&mut provisional_private_tree,
)
.decap(
sender,
update_path,
&provisional_state.indexes_of_added_kpkgs,
&context_bytes,
&self.cipher_suite_provider,
)
.await
.map(|root_secret| Some((provisional_private_tree, root_secret)))
}
}
async fn update_key_schedule(
&mut self,
secrets: Option<(TreeKemPrivate, PathSecret)>,
interim_transcript_hash: InterimTranscriptHash,
confirmation_tag: &ConfirmationTag,
provisional_state: ProvisionalState,
) -> Result<(), MlsError> {
let commit_secret = if let Some(secrets) = secrets {
self.private_tree = secrets.0;
secrets.1
} else {
PathSecret::empty(&self.cipher_suite_provider)
};
// Use the commit_secret, the psk_secret, the provisional GroupContext, and the init secret
// from the previous epoch (or from the external init) to compute the epoch secret and
// derived secrets for the new epoch
let key_schedule = match provisional_state
.applied_proposals
.external_initializations
.first()
.cloned()
{
Some(ext_init) if self.pending_commit.is_none() => {
self.key_schedule
.derive_for_external(&ext_init.proposal.kem_output, &self.cipher_suite_provider)
.await?
}
_ => self.key_schedule.clone(),
};
#[cfg(feature = "psk")]
let (psk, _) = self
.get_psk(&provisional_state.applied_proposals.psks)
.await?;
#[cfg(not(feature = "psk"))]
let psk = self.get_psk();
let key_schedule_result = KeySchedule::from_key_schedule(
&key_schedule,
&commit_secret,
&provisional_state.group_context,
#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
provisional_state.public_tree.total_leaf_count(),
&psk,
&self.cipher_suite_provider,
)
.await?;
// Use the confirmation_key for the new epoch to compute the confirmation tag for
// this message, as described below, and verify that it is the same as the
// confirmation_tag field in the MlsPlaintext object.
let new_confirmation_tag = ConfirmationTag::create(
&key_schedule_result.confirmation_key,
&provisional_state.group_context.confirmed_transcript_hash,
&self.cipher_suite_provider,
)
.await?;
if &new_confirmation_tag != confirmation_tag {
return Err(MlsError::InvalidConfirmationTag);
}
#[cfg(feature = "prior_epoch")]
let signature_public_keys = self
.state
.public_tree
.leaves()
.map(|l| l.map(|n| n.signing_identity.signature_key.clone()))
.collect();
#[cfg(feature = "prior_epoch")]
let past_epoch = PriorEpoch {
context: self.context().clone(),
self_index: self.private_tree.self_index,
secrets: self.epoch_secrets.clone(),
signature_public_keys,
};
#[cfg(feature = "prior_epoch")]
self.state_repo.insert(past_epoch).await?;
self.epoch_secrets = key_schedule_result.epoch_secrets;
self.state.context = provisional_state.group_context;
self.state.interim_transcript_hash = interim_transcript_hash;
self.key_schedule = key_schedule_result.key_schedule;
self.state.public_tree = provisional_state.public_tree;
self.state.confirmation_tag = new_confirmation_tag;
// Clear the proposals list
#[cfg(feature = "by_ref_proposal")]
self.state.proposals.clear();
// Clear the pending updates list
#[cfg(feature = "by_ref_proposal")]
{
self.pending_updates = Default::default();
}
self.pending_commit = None;
Ok(())
}
fn mls_rules(&self) -> Self::MlsRules {
self.config.mls_rules()
}
fn identity_provider(&self) -> Self::IdentityProvider {
self.config.identity_provider()
}
fn psk_storage(&self) -> Self::PreSharedKeyStorage {
self.config.secret_store()
}
fn group_state(&self) -> &GroupState {
&self.state
}
fn group_state_mut(&mut self) -> &mut GroupState {
&mut self.state
}
fn can_continue_processing(&self, provisional_state: &ProvisionalState) -> bool {
!(provisional_state
.applied_proposals
.removals
.iter()
.any(|p| p.proposal.to_remove == self.private_tree.self_index)
&& self.pending_commit.is_none())
}
#[cfg(feature = "private_message")]
fn min_epoch_available(&self) -> Option<u64> {
None
}
fn cipher_suite_provider(&self) -> &Self::CipherSuiteProvider {
&self.cipher_suite_provider
}
}
#[cfg(test)]
pub(crate) mod test_utils;
#[cfg(test)]
mod tests {
use crate::{
client::test_utils::{
test_client_with_key_pkg, TestClientBuilder, TEST_CIPHER_SUITE,
TEST_CUSTOM_PROPOSAL_TYPE, TEST_PROTOCOL_VERSION,
},
client_builder::{test_utils::TestClientConfig, ClientBuilder, MlsConfig},
crypto::test_utils::TestCryptoProvider,
group::{
mls_rules::{CommitDirection, CommitSource},
proposal_filter::ProposalBundle,
},
identity::{
basic::BasicIdentityProvider,
test_utils::{get_test_signing_identity, BasicWithCustomProvider},
},
key_package::test_utils::test_key_package_message,
mls_rules::CommitOptions,
tree_kem::{
leaf_node::{test_utils::get_test_capabilities, LeafNodeSource},
UpdatePathNode,
},
};
#[cfg(any(feature = "private_message", feature = "custom_proposal"))]
use crate::group::mls_rules::DefaultMlsRules;
#[cfg(feature = "prior_epoch")]
use crate::group::padding::PaddingMode;
use crate::{extension::RequiredCapabilitiesExt, key_package::test_utils::test_key_package};
#[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))]
use super::test_utils::test_group_custom_config;
#[cfg(feature = "psk")]
use crate::{client::Client, psk::PreSharedKey};
#[cfg(any(feature = "by_ref_proposal", feature = "private_message"))]
use crate::group::test_utils::random_bytes;
#[cfg(feature = "by_ref_proposal")]
use crate::{
extension::test_utils::TestExtension, identity::test_utils::get_test_basic_credential,
time::MlsTime,
};
use super::{
test_utils::{
--> --------------------
--> maximum size reached
--> --------------------
[ 0.74Quellennavigators
]
|
2026-04-04
|