use thiserror::Error;
use tls_codec::Serialize as _;
#[cfg(doc)]
use super::CommitMessageBundle;
use crate::{
binary_tree::LeafNodeIndex,
credentials::CredentialWithKey,
error::LibraryError,
framing::{ContentType, DecryptedMessage, PublicMessageIn, Sender},
group::{
commit_builder::{CommitBuilder, ExternalCommitInfo, Initial},
past_secrets::MessageSecretsStore,
public_group::errors::CreationFromExternalError,
ExternalCommitBuilderFinalizeError, LeafNodeLifetimePolicy, MlsGroup, MlsGroupJoinConfig,
MlsGroupState, PendingCommitState, ProposalStore, PublicGroup, QueuedProposal,
ValidationError, PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
},
messages::{
group_info::VerifiableGroupInfo,
proposals::{
ExternalInitProposal, PreSharedKeyProposal, Proposal, ProposalOrRefType, ProposalType,
RemoveProposal,
},
},
schedule::{psk::store::ResumptionPskStore, EpochSecrets, InitSecret},
storage::OpenMlsProvider,
treesync::{LeafNodeParameters, RatchetTreeIn},
versions::ProtocolVersion,
};
#[derive(Debug, Error)]
pub enum ExternalCommitBuilderError<StorageError> {
#[error(transparent)]
LibraryError(#[from] LibraryError),
#[error("No ratchet tree available to build initial tree.")]
MissingRatchetTree,
#[error("No external_pub extension available to join group by external commit.")]
MissingExternalPub,
#[error("We don't support the ciphersuite of the group we are trying to join.")]
UnsupportedCiphersuite,
#[error(transparent)]
PublicGroupError(#[from] CreationFromExternalError<StorageError>),
#[error("An error occurred when writing group to storage.")]
StorageError(StorageError),
#[error("Error validating proposals: {0}")]
InvalidProposal(#[from] ValidationError),
}
#[derive(Default)]
pub struct ExternalCommitBuilder {
proposals: Vec<PublicMessageIn>,
ratchet_tree: Option<RatchetTreeIn>,
config: MlsGroupJoinConfig,
validate_lifetimes: LeafNodeLifetimePolicy,
aad: Vec<u8>,
}
impl MlsGroup {
pub fn external_commit_builder() -> ExternalCommitBuilder {
ExternalCommitBuilder::new()
}
}
impl ExternalCommitBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_proposals(mut self, proposals: Vec<PublicMessageIn>) -> Self {
self.proposals = proposals;
self
}
pub fn with_ratchet_tree(mut self, ratchet_tree: RatchetTreeIn) -> Self {
self.ratchet_tree = Some(ratchet_tree);
self
}
pub fn with_config(mut self, config: MlsGroupJoinConfig) -> Self {
self.config = config;
self
}
pub fn with_aad(mut self, aad: Vec<u8>) -> Self {
self.aad = aad;
self
}
pub fn skip_lifetime_validation(mut self) -> Self {
self.validate_lifetimes = LeafNodeLifetimePolicy::Skip;
self
}
pub fn build_group<Provider: OpenMlsProvider>(
self,
provider: &Provider,
verifiable_group_info: VerifiableGroupInfo,
credential_with_key: CredentialWithKey,
) -> Result<
CommitBuilder<'_, Initial, MlsGroup>,
ExternalCommitBuilderError<Provider::StorageError>,
> {
let ExternalCommitBuilder {
proposals,
ratchet_tree,
mut config,
aad,
validate_lifetimes,
} = self;
let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
Some(extension) => extension.ratchet_tree().clone(),
None => match ratchet_tree {
Some(ratchet_tree) => ratchet_tree,
None => return Err(ExternalCommitBuilderError::MissingRatchetTree),
},
};
let (public_group, group_info) = PublicGroup::from_ratchet_tree(
provider.crypto(),
ratchet_tree,
verifiable_group_info,
ProposalStore::new(),
validate_lifetimes,
)?;
let group_context = public_group.group_context();
let external_pub = group_info
.extensions()
.external_pub()
.ok_or(ExternalCommitBuilderError::MissingExternalPub)?
.external_pub();
let (init_secret, kem_output) = InitSecret::from_group_context(
provider.crypto(),
group_context,
external_pub.as_slice(),
)
.map_err(|_| ExternalCommitBuilderError::UnsupportedCiphersuite)?;
let ciphersuite = group_context.ciphersuite();
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(
group_context
.tls_serialize_detached()
.map_err(LibraryError::missing_bound_check)?,
public_group.tree_size(),
LeafNodeIndex::new(0u32),
);
let message_secrets_store =
MessageSecretsStore::new_with_secret(config.max_past_epochs, message_secrets);
let external_init_proposal =
Proposal::external_init(ExternalInitProposal::from(kem_output));
let serialized_context = group_context
.tls_serialize_detached()
.map_err(LibraryError::missing_bound_check)?;
let mut queued_proposals = Vec::new();
for message in proposals {
if message.content_type() != ContentType::Proposal {
continue; }
let decrypted_message = DecryptedMessage::from_inbound_public_message(
message,
None,
serialized_context.clone(),
provider.crypto(),
ciphersuite,
)?;
let unverified_message = public_group.parse_message(decrypted_message, None)?;
let (verified_message, _credential) = unverified_message.verify(
ciphersuite,
provider.crypto(),
ProtocolVersion::default(),
)?;
let queued_proposal = QueuedProposal::from_authenticated_content(
ciphersuite,
provider.crypto(),
verified_message,
ProposalOrRefType::Reference,
)?;
if queued_proposal.proposal().is_type(ProposalType::SelfRemove) {
queued_proposals.push(queued_proposal);
}
}
let inline_proposals = [external_init_proposal].into_iter();
let our_signature_key = credential_with_key.signature_key.as_slice();
let remove_proposal = public_group.members().find_map(|member| {
(member.signature_key == our_signature_key).then_some(Proposal::remove(
RemoveProposal {
removed: member.index,
},
))
});
let inline_proposals = inline_proposals
.chain(remove_proposal)
.map(|p| {
QueuedProposal::from_proposal_and_sender(
ciphersuite,
provider.crypto(),
p,
&Sender::NewMemberCommit,
)
})
.collect::<Result<Vec<_>, _>>()?;
queued_proposals.extend(inline_proposals);
let own_leaf_index = public_group.leftmost_free_index(queued_proposals.iter())?;
let original_wire_format_policy = config.wire_format_policy;
config.wire_format_policy = PURE_PLAINTEXT_WIRE_FORMAT_POLICY;
let mut mls_group = MlsGroup {
mls_group_config: config,
own_leaf_nodes: vec![],
aad: vec![],
group_state: MlsGroupState::Operational,
public_group,
group_epoch_secrets,
own_leaf_index,
message_secrets_store,
resumption_psk_store: ResumptionPskStore::new(32),
#[cfg(feature = "extensions-draft-08")]
application_export_tree: None,
};
let proposal_store = mls_group.proposal_store_mut();
for queued_proposal in queued_proposals {
proposal_store.add(queued_proposal);
}
let mut commit_builder = CommitBuilder::<'_, Initial, MlsGroup>::new(mls_group);
commit_builder.stage.force_self_update = true;
commit_builder.stage.external_commit_info = Some(ExternalCommitInfo {
wire_format_policy: original_wire_format_policy,
credential: credential_with_key.clone(),
aad,
});
let leaf_node_parameters = LeafNodeParameters::builder()
.with_credential_with_key(credential_with_key)
.build();
commit_builder.stage.leaf_node_parameters = leaf_node_parameters;
Ok(commit_builder)
}
}
impl<'a> CommitBuilder<'a, Initial, MlsGroup> {
pub fn add_psk_proposal(mut self, proposal: PreSharedKeyProposal) -> Self {
self.stage.own_proposals.push(Proposal::psk(proposal));
self
}
pub fn add_psk_proposals(
mut self,
proposals: impl IntoIterator<Item = PreSharedKeyProposal>,
) -> Self {
self.stage
.own_proposals
.extend(proposals.into_iter().map(Proposal::psk));
self
}
}
impl CommitBuilder<'_, super::Complete, MlsGroup> {
pub fn finalize<Provider: OpenMlsProvider>(
self,
provider: &Provider,
) -> Result<
(MlsGroup, super::CommitMessageBundle),
ExternalCommitBuilderFinalizeError<Provider::StorageError>,
> {
let Self {
mut group,
stage:
super::Complete {
result: create_commit_result,
original_wire_format_policy,
},
..
} = self;
let mls_message = group.content_to_mls_message(create_commit_result.commit, provider)?;
group.reset_aad();
if let Some(wire_format_policy) = original_wire_format_policy {
group.mls_group_config.wire_format_policy = wire_format_policy;
}
group
.store(provider.storage())
.map_err(ExternalCommitBuilderFinalizeError::StorageError)?;
group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
create_commit_result.staged_commit,
)));
group.merge_pending_commit(provider)?;
let bundle = super::CommitMessageBundle {
version: group.version(),
commit: mls_message,
welcome: create_commit_result.welcome_option,
group_info: create_commit_result.group_info,
};
Ok((group, bundle))
}
}