use errors::NewGroupError;
use openmls_traits::{crypto::OpenMlsCrypto, storage::StorageProvider as StorageProviderTrait};
use super::{builder::MlsGroupBuilder, *};
use crate::{
credentials::CredentialWithKey,
extensions::Extensions,
group::{
commit_builder::external_commits::ExternalCommitBuilder,
errors::{ExportSecretError, ExternalCommitError, WelcomeError},
},
messages::{
group_info::{GroupInfo, VerifiableGroupInfo},
Welcome,
},
schedule::{
psk::{store::ResumptionPskStore, PreSharedKeyId},
EpochSecretsResult,
},
storage::OpenMlsProvider,
treesync::{
errors::{DerivePathError, PublicTreeError},
node::leaf_node::{Capabilities, LeafNodeParameters},
RatchetTreeIn,
},
};
use crate::key_packages::KeyPackage;
impl MlsGroup {
pub fn builder() -> MlsGroupBuilder {
MlsGroupBuilder::new()
}
pub fn new<Provider: OpenMlsProvider>(
provider: &Provider,
signer: &impl Signer,
mls_group_create_config: &MlsGroupCreateConfig,
credential_with_key: CredentialWithKey,
) -> Result<Self, NewGroupError<Provider::StorageError>> {
MlsGroupBuilder::new().build_internal(
provider,
signer,
credential_with_key,
Some(mls_group_create_config.clone()),
)
}
pub fn new_with_group_id<Provider: OpenMlsProvider>(
provider: &Provider,
signer: &impl Signer,
mls_group_create_config: &MlsGroupCreateConfig,
group_id: GroupId,
credential_with_key: CredentialWithKey,
) -> Result<Self, NewGroupError<Provider::StorageError>> {
MlsGroupBuilder::new()
.with_group_id(group_id)
.build_internal(
provider,
signer,
credential_with_key,
Some(mls_group_create_config.clone()),
)
}
#[allow(clippy::too_many_arguments)]
#[deprecated(
since = "0.7.1",
note = "Use the `MlsGroup::external_commit_builder` instead."
)]
pub fn join_by_external_commit<Provider: OpenMlsProvider>(
provider: &Provider,
signer: &impl Signer,
ratchet_tree: Option<RatchetTreeIn>,
verifiable_group_info: VerifiableGroupInfo,
mls_group_config: &MlsGroupJoinConfig,
capabilities: Option<Capabilities>,
extensions: Option<Extensions<LeafNode>>,
aad: &[u8],
credential_with_key: CredentialWithKey,
) -> Result<(Self, MlsMessageOut, Option<GroupInfo>), ExternalCommitError<Provider::StorageError>>
{
let leaf_node_parameters = LeafNodeParameters::builder()
.with_capabilities(capabilities.unwrap_or_default())
.with_extensions(extensions.unwrap_or_default())
.build();
let mut external_commit_builder = ExternalCommitBuilder::new()
.with_aad(aad.to_vec())
.with_config(mls_group_config.clone());
if let Some(ratchet_tree) = ratchet_tree {
external_commit_builder = external_commit_builder.with_ratchet_tree(ratchet_tree)
}
let (mls_group, commit_message_bundle) = external_commit_builder
.build_group(provider, verifiable_group_info, credential_with_key)?
.leaf_node_parameters(leaf_node_parameters)
.load_psks(provider.storage())
.map_err(|e| {
log::error!("Error loading PSKs for external commit: {e:?}");
LibraryError::custom("Error loading PSKs for external commit")
})?
.build(provider.rand(), provider.crypto(), signer, |_| true)?
.finalize(provider)?;
let (commit, _, group_info) = commit_message_bundle.into_contents();
Ok((mls_group, commit, group_info))
}
}
impl ProcessedWelcome {
pub fn new_from_welcome<Provider: OpenMlsProvider>(
provider: &Provider,
mls_group_config: &MlsGroupJoinConfig,
welcome: Welcome,
) -> Result<Self, WelcomeError<Provider::StorageError>> {
let ciphersuite = welcome.ciphersuite();
provider
.crypto()
.supports(ciphersuite)
.map_err(|_| WelcomeError::UnsupportedCiphersuite(ciphersuite))?;
let (resumption_psk_store, key_material) =
keys_for_welcome(mls_group_config, &welcome, provider)?;
let Some(egs) =
welcome.find_encrypted_group_secret(key_material.key_package_ref(provider.crypto())?)
else {
return Err(WelcomeError::JoinerSecretNotFound);
};
if let Some(key_package_bundle) = key_material.key_package_bundle() {
if welcome.ciphersuite() != key_package_bundle.key_package().ciphersuite() {
let e = WelcomeError::CiphersuiteMismatch;
log::debug!("new_from_welcome {e:?}");
return Err(e);
}
}
let group_secrets = GroupSecrets::try_from_ciphertext(
key_material.init_private_key(),
egs.encrypted_group_secrets(),
welcome.encrypted_group_info(),
ciphersuite,
provider.crypto(),
)?;
PreSharedKeyId::validate_in_welcome(&group_secrets.psks, ciphersuite)?;
let psk_secret = {
let psks = load_psks(
provider.storage(),
&resumption_psk_store,
&group_secrets.psks,
)?;
PskSecret::new(provider.crypto(), ciphersuite, psks)?
};
let mut key_schedule = KeySchedule::init(
ciphersuite,
provider.crypto(),
&group_secrets.joiner_secret,
psk_secret,
)?;
let (welcome_key, welcome_nonce) = key_schedule
.welcome(provider.crypto(), ciphersuite)
.map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?
.derive_welcome_key_nonce(provider.crypto(), ciphersuite)
.map_err(LibraryError::unexpected_crypto_error)?;
let verifiable_group_info = VerifiableGroupInfo::try_from_ciphertext(
&welcome_key,
&welcome_nonce,
welcome.encrypted_group_info(),
&[],
provider.crypto(),
)?;
let serialized_group_context = verifiable_group_info
.group_context()
.tls_serialize_detached()
.map_err(LibraryError::missing_bound_check)?;
key_schedule.add_context(provider.crypto(), &serialized_group_context)?;
let epoch_secrets = key_schedule.epoch_secrets(provider.crypto(), ciphersuite)?;
if let Some(key_package_bundle) = key_material.key_package_bundle() {
if let Some(required_capabilities) =
verifiable_group_info.extensions().required_capabilities()
{
key_package_bundle
.key_package()
.leaf_node()
.capabilities()
.supports_required_capabilities(required_capabilities)?;
}
if verifiable_group_info.ciphersuite() != key_package_bundle.key_package().ciphersuite()
{
let e = WelcomeError::CiphersuiteMismatch;
log::debug!("new_from_welcome {e:?}");
return Err(e);
}
}
Ok(Self {
mls_group_config: mls_group_config.clone(),
ciphersuite,
group_secrets,
epoch_secrets,
verifiable_group_info,
resumption_psk_store,
key_material,
})
}
pub fn unverified_group_info(&self) -> &VerifiableGroupInfo {
&self.verifiable_group_info
}
pub fn psks(&self) -> &[PreSharedKeyId] {
&self.group_secrets.psks
}
pub fn into_staged_welcome<Provider: OpenMlsProvider>(
self,
provider: &Provider,
ratchet_tree: Option<RatchetTreeIn>,
) -> Result<StagedWelcome, WelcomeError<Provider::StorageError>> {
self.into_staged_welcome_inner(
provider,
ratchet_tree,
LeafNodeLifetimePolicy::Verify,
false,
)
}
pub(crate) fn into_staged_welcome_inner<Provider: OpenMlsProvider>(
mut self,
provider: &Provider,
ratchet_tree: Option<RatchetTreeIn>,
validate_lifetimes: LeafNodeLifetimePolicy,
replace_old_group: bool,
) -> Result<StagedWelcome, WelcomeError<Provider::StorageError>> {
if !replace_old_group
&& MlsGroup::load(provider.storage(), self.verifiable_group_info.group_id())
.map_err(WelcomeError::StorageError)?
.is_some()
{
return Err(WelcomeError::GroupAlreadyExists);
}
let ratchet_tree = match self.verifiable_group_info.extensions().ratchet_tree() {
Some(extension) => extension.ratchet_tree().clone(),
None => match ratchet_tree {
Some(ratchet_tree) => ratchet_tree,
None => return Err(WelcomeError::MissingRatchetTree),
},
};
let (public_group, _group_info_extensions) = PublicGroup::from_ratchet_tree(
provider.crypto(),
ratchet_tree,
self.verifiable_group_info.clone(),
ProposalStore::new(),
validate_lifetimes,
)?;
let own_leaf_index = match &self.key_material {
WelcomeKeyMaterial::KeyPackage(key_package_bundle) => {
let added_leaf_supports_all_group_context_extensions = public_group
.group_context()
.extensions()
.iter()
.all(|extension| {
key_package_bundle
.key_package
.leaf_node()
.supports_extension(&extension.extension_type())
});
if !added_leaf_supports_all_group_context_extensions {
return Err(WelcomeError::UnsupportedExtensions);
}
public_group
.members()
.find_map(|m| {
if m.signature_key
== key_package_bundle
.key_package()
.leaf_node()
.signature_key()
.as_slice()
{
Some(m.index)
} else {
None
}
})
.ok_or(WelcomeError::PublicTreeError(
PublicTreeError::MalformedTree,
))?
}
#[cfg(feature = "virtual-clients-draft")]
WelcomeKeyMaterial::VirtualClient(material) => {
find_and_validate_vc_own_leaf(provider, &public_group, material)?
}
};
struct KeyScheduleResult {
group_epoch_secrets: GroupEpochSecrets,
message_secrets: MessageSecrets,
#[cfg(feature = "extensions-draft")]
application_exporter: ApplicationExportSecret,
}
let KeyScheduleResult {
group_epoch_secrets,
message_secrets,
#[cfg(feature = "extensions-draft")]
application_exporter: application_export_secret,
} = {
let serialized_group_context = public_group
.group_context()
.tls_serialize_detached()
.map_err(LibraryError::missing_bound_check)?;
let EpochSecretsResult {
epoch_secrets,
#[cfg(feature = "extensions-draft")]
application_exporter,
} = self.epoch_secrets;
let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
serialized_group_context,
public_group.tree_size(),
own_leaf_index,
);
KeyScheduleResult {
group_epoch_secrets,
message_secrets,
#[cfg(feature = "extensions-draft")]
application_exporter,
}
};
let confirmation_tag = message_secrets
.confirmation_key()
.tag(
provider.crypto(),
self.ciphersuite,
public_group.group_context().confirmed_transcript_hash(),
)
.map_err(LibraryError::unexpected_crypto_error)?;
if &confirmation_tag != public_group.confirmation_tag() {
log::error!("Confirmation tag mismatch");
log_crypto!(trace, " Got: {:x?}", confirmation_tag);
log_crypto!(trace, " Expected: {:x?}", public_group.confirmation_tag());
if !crate::skip_validation::is_disabled::confirmation_tag() {
return Err(WelcomeError::ConfirmationTagMismatch);
}
}
let message_secrets_store = MessageSecretsStore::new_with_secret(
&PastEpochDeletionPolicy::MaxEpochs(0),
message_secrets,
);
let resumption_psk = group_epoch_secrets.resumption_psk();
self.resumption_psk_store
.add(public_group.group_context().epoch(), resumption_psk.clone());
let welcome_sender_index = self.verifiable_group_info.signer();
let path_keypairs = if let Some(path_secret) = self.group_secrets.path_secret {
let (path_keypairs, _commit_secret) = public_group
.derive_path_secrets(
provider.crypto(),
self.ciphersuite,
path_secret,
welcome_sender_index,
own_leaf_index,
)
.map_err(|e| match e {
DerivePathError::LibraryError(e) => e.into(),
DerivePathError::PublicKeyMismatch => {
WelcomeError::PublicTreeError(PublicTreeError::PublicKeyMismatch)
}
})?;
Some(path_keypairs)
} else {
None
};
let staged_welcome = StagedWelcome {
mls_group_config: self.mls_group_config,
public_group,
group_epoch_secrets,
own_leaf_index,
message_secrets_store,
#[cfg(feature = "extensions-draft")]
application_export_secret,
resumption_psk_store: self.resumption_psk_store,
verifiable_group_info: self.verifiable_group_info,
key_material: self.key_material,
path_keypairs,
};
Ok(staged_welcome)
}
pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
&self,
crypto: &CryptoProvider,
label: &str,
context: &[u8],
key_length: usize,
) -> Result<Vec<u8>, ExportSecretError> {
if key_length > u16::MAX as usize {
log::error!("Got a key that is larger than u16::MAX");
return Err(ExportSecretError::KeyLengthTooLong);
}
Ok(self
.epoch_secrets
.epoch_secrets
.exporter_secret()
.derive_exported_secret(self.ciphersuite, crypto, label, context, key_length)
.map_err(LibraryError::unexpected_crypto_error)?)
}
pub fn own_key_package(&self) -> Option<&KeyPackage> {
self.key_material
.key_package_bundle()
.map(|bundle| bundle.key_package())
}
}
impl StagedWelcome {
pub fn new_from_welcome<Provider: OpenMlsProvider>(
provider: &Provider,
mls_group_config: &MlsGroupJoinConfig,
welcome: Welcome,
ratchet_tree: Option<RatchetTreeIn>,
) -> Result<Self, WelcomeError<Provider::StorageError>> {
let processed_welcome =
ProcessedWelcome::new_from_welcome(provider, mls_group_config, welcome)?;
processed_welcome.into_staged_welcome(provider, ratchet_tree)
}
pub fn build_from_welcome<'a, Provider: OpenMlsProvider>(
provider: &'a Provider,
mls_group_config: &MlsGroupJoinConfig,
welcome: Welcome,
) -> Result<JoinBuilder<'a, Provider>, WelcomeError<Provider::StorageError>> {
let processed_welcome =
ProcessedWelcome::new_from_welcome(provider, mls_group_config, welcome)?;
Ok(JoinBuilder::new(provider, processed_welcome))
}
pub fn welcome_sender_index(&self) -> LeafNodeIndex {
self.verifiable_group_info.signer()
}
pub fn welcome_sender(&self) -> Result<&LeafNode, LibraryError> {
let sender_index = self.welcome_sender_index();
self.public_group
.leaf(sender_index)
.ok_or_else(|| LibraryError::custom("no leaf with given welcome sender index exists"))
}
pub fn own_leaf_index(&self) -> LeafNodeIndex {
self.own_leaf_index
}
pub fn own_leaf_node(&self) -> Option<&LeafNode> {
self.public_group.leaf(self.own_leaf_index())
}
pub fn group_context(&self) -> &GroupContext {
self.public_group.group_context()
}
pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
self.public_group.members()
}
#[cfg(feature = "extensions-draft")]
pub fn application_export_secret(&self) -> &ApplicationExportSecret {
&self.application_export_secret
}
pub fn into_group<Provider: OpenMlsProvider>(
self,
provider: &Provider,
) -> Result<MlsGroup, WelcomeError<Provider::StorageError>> {
let group_keypairs = if let Some(path_keypairs) = self.path_keypairs {
let mut keypairs = vec![self.key_material.encryption_key_pair()];
keypairs.extend_from_slice(&path_keypairs);
keypairs
} else {
vec![self.key_material.encryption_key_pair()]
};
#[cfg(feature = "extensions-draft")]
let application_export_tree = ApplicationExportTree::new(self.application_export_secret);
let past_epoch_deletion_policy = self.mls_group_config.past_epoch_deletion_policy().clone();
let mut mls_group = MlsGroup {
mls_group_config: self.mls_group_config,
own_leaf_nodes: vec![],
aad: vec![],
#[cfg(feature = "extensions-draft")]
safe_aad: crate::framing::SafeAad::empty(),
group_state: MlsGroupState::Operational,
public_group: self.public_group,
group_epoch_secrets: self.group_epoch_secrets,
own_leaf_index: self.own_leaf_index,
message_secrets_store: self.message_secrets_store,
resumption_psk_store: self.resumption_psk_store,
#[cfg(feature = "extensions-draft")]
application_export_tree: Some(application_export_tree),
};
mls_group
.store_epoch_keypairs(provider.storage(), group_keypairs.as_slice())
.map_err(WelcomeError::StorageError)?;
mls_group.resize_message_secrets_store(&past_epoch_deletion_policy);
mls_group
.store(provider.storage())
.map_err(WelcomeError::StorageError)?;
Ok(mls_group)
}
pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
&self,
crypto: &CryptoProvider,
label: &str,
context: &[u8],
key_length: usize,
) -> Result<Vec<u8>, ExportSecretError> {
if key_length > u16::MAX as usize {
log::error!("Got a key that is larger than u16::MAX");
return Err(ExportSecretError::KeyLengthTooLong);
}
Ok(self
.group_epoch_secrets
.exporter_secret()
.derive_exported_secret(
self.group_context().ciphersuite(),
crypto,
label,
context,
key_length,
)
.map_err(LibraryError::unexpected_crypto_error)?)
}
}
fn keys_for_welcome<Provider: OpenMlsProvider>(
mls_group_config: &MlsGroupJoinConfig,
welcome: &Welcome,
provider: &Provider,
) -> Result<
(ResumptionPskStore, WelcomeKeyMaterial),
WelcomeError<<Provider as OpenMlsProvider>::StorageError>,
> {
let resumption_psk_store = ResumptionPskStore::new(mls_group_config.number_of_resumption_psks);
for egs in welcome.secrets() {
let hash_ref = egs.new_member();
if let Some(key_package_bundle) = provider
.storage()
.key_package(&hash_ref)
.map_err(WelcomeError::StorageError)?
{
let key_package_bundle: KeyPackageBundle = key_package_bundle;
if !key_package_bundle.key_package().last_resort() {
provider
.storage()
.delete_key_package(
&key_package_bundle.key_package.hash_ref(provider.crypto())?,
)
.map_err(WelcomeError::StorageError)?;
} else {
log::debug!("Key package has last resort extension, not deleting");
}
return Ok((
resumption_psk_store,
WelcomeKeyMaterial::KeyPackage(Box::new(key_package_bundle)),
));
}
#[cfg(feature = "virtual-clients-draft")]
if let Some(material) = vc_welcome_material(provider, welcome.ciphersuite(), &hash_ref)? {
return Ok((
resumption_psk_store,
WelcomeKeyMaterial::VirtualClient(material),
));
}
}
Err(WelcomeError::NoMatchingKeyPackage)
}
#[cfg(feature = "virtual-clients-draft")]
fn vc_welcome_material<Provider: OpenMlsProvider>(
provider: &Provider,
ciphersuite: Ciphersuite,
hash_ref: &crate::ciphersuite::hash_ref::KeyPackageRef,
) -> Result<
Option<crate::components::vc_derivation_info::VcWelcomeMaterial>,
WelcomeError<<Provider as OpenMlsProvider>::StorageError>,
> {
use crate::components::vc_derivation_info::{
RetainedKeyPackageMaterial, VcWelcomeMaterial, VirtualClientsError,
};
let storage = provider.storage();
let Some(material) = storage
.retained_key_package_material::<_, RetainedKeyPackageMaterial>(hash_ref)
.map_err(WelcomeError::StorageError)?
else {
return Ok(None);
};
if material.key_package_ciphersuite != ciphersuite {
return Err(WelcomeError::CiphersuiteMismatch);
}
let crypto = provider.crypto();
storage
.delete_retained_key_package_material(hash_ref)
.map_err(|e| {
log::error!("vc: delete retained key package material in welcome failed: {e:?}");
VirtualClientsError::StorageError
})?;
let init_key_pair = material
.key_package_seed_secret
.derive_init_key_secret(crypto, ciphersuite)?
.generate_init_key_pair(crypto, ciphersuite)?;
let encryption_keypair = material
.key_package_seed_secret
.derive_encryption_key_secret(crypto, ciphersuite)?
.generate_encryption_key_pair(crypto, ciphersuite)?;
Ok(Some(VcWelcomeMaterial {
key_package_ref: hash_ref.clone(),
epoch_id: material.epoch_id,
leaf_index: material.leaf_index,
generation: material.generation,
key_package_index: material.key_package_index,
init_private_key: init_key_pair.private,
encryption_keypair,
}))
}
#[cfg(feature = "virtual-clients-draft")]
fn find_and_validate_vc_own_leaf<Provider: OpenMlsProvider>(
provider: &Provider,
public_group: &PublicGroup,
material: &crate::components::vc_derivation_info::VcWelcomeMaterial,
) -> Result<LeafNodeIndex, WelcomeError<<Provider as OpenMlsProvider>::StorageError>> {
use tls_codec::{DeserializeBytes as _, Serialize as _};
use crate::components::vc_derivation_info::{
DerivationInfo, DerivationInfoTbe, EmulationEpochState, VirtualClientOperationType,
VirtualClientsError, VC_COMPONENT_ID,
};
let crypto = provider.crypto();
let derived_encryption_key = material.encryption_keypair.public_key().as_slice().to_vec();
let own_index = public_group
.members()
.find(|m| m.encryption_key == derived_encryption_key)
.map(|m| m.index)
.ok_or(WelcomeError::PublicTreeError(
PublicTreeError::MalformedTree,
))?;
let own_leaf = public_group
.leaf(own_index)
.ok_or(WelcomeError::PublicTreeError(
PublicTreeError::MalformedTree,
))?;
let derivation_info_bytes = own_leaf
.extensions()
.app_data_dictionary()
.and_then(|dict| dict.dictionary().get(&VC_COMPONENT_ID))
.ok_or(VirtualClientsError::VcComponentNotListed)?;
let derivation_info = DerivationInfo::tls_deserialize_exact_bytes(derivation_info_bytes)
.map_err(|e| {
log::error!("vc: welcome leaf derivation info deserialize failed: {e:?}");
VirtualClientsError::DerivationInfoMalformed
})?;
if derivation_info.epoch_id() != &material.epoch_id {
log::error!("vc: welcome leaf epoch id does not match the retained material");
return Err(VirtualClientsError::DerivationInfoMalformed.into());
}
let state: EmulationEpochState = provider
.storage()
.vc_emulation_epoch_state(&material.epoch_id)
.map_err(|e| {
log::error!("vc: load emulation epoch state in welcome staging failed: {e:?}");
VirtualClientsError::StorageError
})?
.ok_or(VirtualClientsError::MissingEmulationEpochState)?;
let (_state_leaf_index, epoch_encryption_key, emulation_ciphersuite) = state.into_parts();
let leaf_encryption_key = own_leaf
.encryption_key()
.tls_serialize_detached()
.map_err(VirtualClientsError::from)?;
let tbe = derivation_info.decrypt(
crypto,
emulation_ciphersuite,
&epoch_encryption_key,
&leaf_encryption_key,
VirtualClientOperationType::KeyPackage,
)?;
let DerivationInfoTbe::KeyPackage {
leaf_index,
generation,
key_package_index,
} = tbe
else {
log::error!("vc: welcome leaf derivation info is not a key-package variant");
return Err(VirtualClientsError::DerivationInfoMalformed.into());
};
if leaf_index != material.leaf_index
|| generation != material.generation
|| key_package_index != material.key_package_index
{
log::error!("vc: welcome leaf derivation info does not match the retained material");
return Err(VirtualClientsError::DerivationInfoMalformed.into());
}
Ok(own_index)
}
#[cfg(feature = "virtual-clients-draft")]
impl MlsGroup {
pub fn vc_join_via_sibling_external_commit<Provider: OpenMlsProvider>(
provider: &Provider,
join_config: &MlsGroupJoinConfig,
verifiable_group_info: VerifiableGroupInfo,
ratchet_tree: Option<RatchetTreeIn>,
external_commit: impl Into<crate::framing::ProtocolMessage>,
epoch_id: crate::components::vc_derivation_info::EpochId,
) -> Result<MlsGroup, crate::group::errors::VcExternalCommitJoinError<Provider::StorageError>>
{
use crate::{
framing::Sender,
group::config::PastEpochDeletionPolicy,
group::errors::{ProcessMessageError, VcExternalCommitJoinError as Error},
group::public_group::PublicGroup,
prelude::mls_content::FramedContentBody,
schedule::{EpochSecrets, InitSecret},
};
let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
Some(extension) => extension.ratchet_tree().clone(),
None => ratchet_tree.ok_or(Error::MissingRatchetTree)?,
};
let (public_group, _group_info) = PublicGroup::from_ratchet_tree(
provider.crypto(),
ratchet_tree,
verifiable_group_info,
ProposalStore::new(),
LeafNodeLifetimePolicy::default(),
)?;
let ciphersuite = public_group.ciphersuite();
let serialized_group_context = public_group
.group_context()
.tls_serialize_detached()
.map_err(LibraryError::missing_bound_check)?;
let own_leaf_index = public_group.leftmost_free_index(std::iter::empty())?;
let init_secret = InitSecret::random(ciphersuite, provider.rand())
.map_err(LibraryError::unexpected_crypto_error)?;
let epoch_secrets =
EpochSecrets::with_init_secret(provider.crypto(), ciphersuite, init_secret)
.map_err(LibraryError::unexpected_crypto_error)?;
let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
serialized_group_context,
public_group.tree_size(),
LeafNodeIndex::new(0u32),
);
let message_secrets_store = MessageSecretsStore::new_with_secret(
&PastEpochDeletionPolicy::MaxEpochs(0),
message_secrets,
);
let mut group = MlsGroup {
mls_group_config: join_config.clone(),
own_leaf_nodes: vec![],
aad: vec![],
#[cfg(feature = "extensions-draft")]
safe_aad: crate::framing::SafeAad::empty(),
group_state: MlsGroupState::Operational,
public_group,
group_epoch_secrets,
own_leaf_index,
message_secrets_store,
resumption_psk_store: ResumptionPskStore::new(join_config.number_of_resumption_psks),
#[cfg(feature = "extensions-draft")]
application_export_tree: None,
};
let processing::UnprotectedMessage::Unverified(unverified) =
group.unprotect_message(provider, external_commit)?
else {
return Err(Error::NotAnExternalCommit);
};
let verified = unverified
.verify(group.ciphersuite(), provider.crypto(), group.version())
.map_err(ProcessMessageError::from)?;
if !matches!(verified.content.sender(), Sender::NewMemberCommit) {
return Err(Error::NotAnExternalCommit);
}
let content = verified.content;
let FramedContentBody::Commit(commit) = content.content() else {
return Err(Error::NotAnExternalCommit);
};
let material = group
.load_vc_commit_material(provider, commit)?
.ok_or(Error::MissingDerivationInfo)?;
if material.epoch_id != epoch_id {
return Err(Error::EpochIdMismatch);
}
let staged = group.stage_commit(&content, vec![], vec![], provider, Some(material))?;
group.merge_staged_commit(provider, staged)?;
group.resize_message_secrets_store(join_config.past_epoch_deletion_policy());
group
.store(provider.storage())
.map_err(Error::StorageError)?;
Ok(group)
}
pub fn vc_join_at_creation<Provider: OpenMlsProvider>(
provider: &Provider,
join_config: &MlsGroupJoinConfig,
verifiable_group_info: VerifiableGroupInfo,
ratchet_tree: Option<RatchetTreeIn>,
epoch_id: crate::components::vc_derivation_info::EpochId,
) -> Result<MlsGroup, crate::group::errors::VcGroupCreationJoinError<Provider::StorageError>>
{
use tls_codec::{DeserializeBytes as _, Serialize as _};
use crate::{
components::vc_derivation_info::{
load_vc_epoch_state_and_tree, DerivationInfo, DerivationInfoTbe,
VirtualClientOperationType, VirtualClientsError, VC_COMPONENT_ID,
},
group::errors::VcGroupCreationJoinError as Error,
group::public_group::PublicGroup,
schedule::EpochSecrets,
treesync::node::leaf_node::LeafNodeSource,
};
let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
Some(extension) => extension.ratchet_tree().clone(),
None => ratchet_tree.ok_or(Error::MissingRatchetTree)?,
};
let (public_group, group_info) = PublicGroup::from_ratchet_tree(
provider.crypto(),
ratchet_tree,
verifiable_group_info,
ProposalStore::new(),
LeafNodeLifetimePolicy::default(),
)?;
let ciphersuite = public_group.ciphersuite();
if public_group.members().count() != 1 {
return Err(Error::NotASingleLeafTree);
}
let creator_index = LeafNodeIndex::new(0);
let creator_leaf = public_group
.leaf(creator_index)
.ok_or(Error::NotASingleLeafTree)?;
let LeafNodeSource::KeyPackage(_) = creator_leaf.leaf_node_source() else {
return Err(Error::CreatorLeafNotKeyPackageSourced);
};
let derivation_info_bytes = creator_leaf
.extensions()
.app_data_dictionary()
.and_then(|dict| dict.dictionary().get(&VC_COMPONENT_ID))
.ok_or(Error::MissingDerivationInfo)?;
let derivation_info = DerivationInfo::tls_deserialize_exact_bytes(derivation_info_bytes)
.map_err(|_| VirtualClientsError::DerivationInfoMalformed)?;
if derivation_info.epoch_id() != &epoch_id {
return Err(Error::EpochIdMismatch);
}
let (state, mut operation_tree) = load_vc_epoch_state_and_tree(provider, &epoch_id)?;
let (_leaf_index, epoch_encryption_key, emulation_ciphersuite) = state.into_parts();
let leaf_encryption_key = creator_leaf
.encryption_key()
.tls_serialize_detached()
.map_err(VirtualClientsError::from)?;
let tbe = derivation_info.decrypt(
provider.crypto(),
emulation_ciphersuite,
&epoch_encryption_key,
&leaf_encryption_key,
VirtualClientOperationType::KeyPackage,
)?;
let DerivationInfoTbe::KeyPackage {
leaf_index,
generation,
key_package_index,
} = tbe
else {
return Err(LibraryError::custom("unexpected derivation info variant").into());
};
let operation_secret = operation_tree.derive_operation_secret(
provider.crypto(),
emulation_ciphersuite,
&epoch_id,
leaf_index,
VirtualClientOperationType::KeyPackage,
generation,
b"",
)?;
let key_package_seed = operation_secret.derive_key_package_seed_secret(
provider.crypto(),
ciphersuite,
key_package_index,
)?;
let leaf_keypair = key_package_seed
.derive_encryption_key_secret(provider.crypto(), ciphersuite)?
.generate_encryption_key_pair(provider.crypto(), ciphersuite)?;
if leaf_keypair.public_key() != creator_leaf.encryption_key() {
return Err(Error::LeafKeyMismatch);
}
let serialized_group_context = public_group
.group_context()
.tls_serialize_detached()
.map_err(LibraryError::missing_bound_check)?;
let epoch_secret =
key_package_seed.derive_group_creation_secret(provider.crypto(), ciphersuite)?;
let epoch_secrets =
EpochSecrets::from_epoch_secret(provider.crypto(), ciphersuite, epoch_secret)
.map_err(LibraryError::unexpected_crypto_error)?;
let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
serialized_group_context,
public_group.tree_size(),
creator_index,
);
let expected_confirmation_tag = message_secrets
.confirmation_key()
.tag(
provider.crypto(),
ciphersuite,
public_group.group_context().confirmed_transcript_hash(),
)
.map_err(LibraryError::unexpected_crypto_error)?;
if &expected_confirmation_tag != group_info.confirmation_tag() {
return Err(Error::ConfirmationTagMismatch);
}
provider
.storage()
.write_vc_operation_tree(&epoch_id, &operation_tree)
.map_err(Error::StorageError)?;
let message_secrets_store = MessageSecretsStore::new_with_secret(
join_config.past_epoch_deletion_policy(),
message_secrets,
);
let mut resumption_psk_store =
ResumptionPskStore::new(join_config.number_of_resumption_psks);
resumption_psk_store.add(
public_group.group_context().epoch(),
group_epoch_secrets.resumption_psk().clone(),
);
let mut bindings: crate::components::vc_derivation_info::VcEmulationBindings = provider
.storage()
.vc_emulation_bindings(public_group.group_id())
.map_err(Error::StorageError)?
.unwrap_or_default();
let max_entries = message_secrets_store.max_epochs.saturating_add(1);
bindings.insert(public_group.group_context().epoch(), epoch_id, max_entries);
provider
.storage()
.write_vc_emulation_bindings(public_group.group_id(), &bindings)
.map_err(Error::StorageError)?;
let mls_group = MlsGroup {
mls_group_config: join_config.clone(),
own_leaf_nodes: vec![],
aad: vec![],
#[cfg(feature = "extensions-draft")]
safe_aad: crate::framing::SafeAad::empty(),
group_state: MlsGroupState::Operational,
public_group,
group_epoch_secrets,
own_leaf_index: creator_index,
message_secrets_store,
resumption_psk_store,
#[cfg(feature = "extensions-draft")]
application_export_tree: None,
};
mls_group
.store(provider.storage())
.map_err(Error::StorageError)?;
mls_group
.store_epoch_keypairs(provider.storage(), &[leaf_keypair])
.map_err(Error::StorageError)?;
Ok(mls_group)
}
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum LeafNodeLifetimePolicy {
#[default]
Verify,
Skip,
}
pub struct JoinBuilder<'a, Provider: OpenMlsProvider> {
provider: &'a Provider,
processed_welcome: ProcessedWelcome,
ratchet_tree: Option<RatchetTreeIn>,
validate_lifetimes: LeafNodeLifetimePolicy,
replace_old_group: bool,
}
impl<'a, Provider: OpenMlsProvider> JoinBuilder<'a, Provider> {
pub fn new(provider: &'a Provider, processed_welcome: ProcessedWelcome) -> Self {
Self {
provider,
processed_welcome,
ratchet_tree: None,
replace_old_group: false,
validate_lifetimes: LeafNodeLifetimePolicy::Verify,
}
}
pub fn with_ratchet_tree(mut self, ratchet_tree: RatchetTreeIn) -> Self {
self.ratchet_tree = Some(ratchet_tree);
self
}
pub fn replace_old_group(mut self) -> Self {
self.replace_old_group = true;
self
}
pub fn skip_lifetime_validation(mut self) -> Self {
self.validate_lifetimes = LeafNodeLifetimePolicy::Skip;
self
}
pub fn processed_welcome(&self) -> &ProcessedWelcome {
&self.processed_welcome
}
pub fn build(self) -> Result<StagedWelcome, WelcomeError<Provider::StorageError>> {
self.processed_welcome.into_staged_welcome_inner(
self.provider,
self.ratchet_tree,
self.validate_lifetimes,
self.replace_old_group,
)
}
}