Skip to content

Commit

Permalink
Add detached commit functionality (#180)
Browse files Browse the repository at this point in the history
* Add detached commit functionality

* Apply suggestions from code review

Co-authored-by: Stephane Raux <[email protected]>

---------

Co-authored-by: Marta Mularczyk <[email protected]>
Co-authored-by: Stephane Raux <[email protected]>
  • Loading branch information
3 people authored Aug 15, 2024
1 parent 8c28e50 commit 6f905df
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 25 deletions.
107 changes: 88 additions & 19 deletions mls-rs/src/group/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,32 @@ pub(crate) struct Commit {

#[derive(Clone, PartialEq, Debug, MlsEncode, MlsDecode, MlsSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub(super) struct CommitGeneration {
pub(crate) struct CommitGeneration {
pub content: AuthenticatedContent,
pub pending_private_tree: TreeKemPrivate,
pub pending_commit_secret: PathSecret,
pub commit_message_hash: MessageHash,
}

#[cfg_attr(
all(feature = "ffi", not(test)),
safer_ffi_gen::ffi_type(clone, opaque)
)]
#[derive(Clone)]
pub struct CommitSecrets(pub(crate) CommitGeneration);

impl CommitSecrets {
/// Deserialize the commit secrets from bytes
pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
Ok(CommitGeneration::mls_decode(&mut &*bytes).map(Self)?)
}

/// Serialize the commit secrets to bytes
pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
Ok(self.0.mls_encode_to_vec()?)
}
}

#[cfg_attr(
all(feature = "ffi", not(test)),
safer_ffi_gen::ffi_type(clone, opaque)
Expand Down Expand Up @@ -316,7 +335,8 @@ where
/// [proposal rules](crate::client_builder::ClientBuilder::mls_rules).
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn build(self) -> Result<CommitOutput, MlsError> {
self.group
let (output, pending_commit) = self
.group
.commit_internal(
self.proposals,
None,
Expand All @@ -325,7 +345,32 @@ where
self.new_signer,
self.new_signing_identity,
)
.await
.await?;

self.group.pending_commit = Some(pending_commit);

Ok(output)
}

/// The same function as `GroupBuilder::build` except the secrets generated
/// for the commit are outputted instead of being cached internally.
///
/// A detached commit can be applied using `Group::apply_detached_commit`.
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn build_detached(self) -> Result<(CommitOutput, CommitSecrets), MlsError> {
let (output, pending_commit) = self
.group
.commit_internal(
self.proposals,
None,
self.authenticated_data,
self.group_info_extensions,
self.new_signer,
self.new_signing_identity,
)
.await?;

Ok((output, CommitSecrets(pending_commit)))
}
}

Expand Down Expand Up @@ -375,15 +420,25 @@ where
/// or [`ReInit`](crate::group::proposal::Proposal::ReInit) are part of the commit.
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn commit(&mut self, authenticated_data: Vec<u8>) -> Result<CommitOutput, MlsError> {
self.commit_internal(
vec![],
None,
authenticated_data,
Default::default(),
None,
None,
)
.await
self.commit_builder()
.authenticated_data(authenticated_data)
.build()
.await
}

/// The same function as `Group::commit` except the secrets generated
/// for the commit are outputted instead of being cached internally.
///
/// A detached commit can be applied using `Group::apply_detached_commit`.
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn commit_detached(
&mut self,
authenticated_data: Vec<u8>,
) -> Result<(CommitOutput, CommitSecrets), MlsError> {
self.commit_builder()
.authenticated_data(authenticated_data)
.build_detached()
.await
}

/// Create a new commit builder that can include proposals
Expand All @@ -401,7 +456,6 @@ where

/// Returns commit and optional [`MlsMessage`] containing a welcome message
/// for newly added members.
#[allow(clippy::too_many_arguments)]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub(super) async fn commit_internal(
&mut self,
Expand All @@ -411,7 +465,7 @@ where
mut welcome_group_info_extensions: ExtensionList,
new_signer: Option<SignatureSecretKey>,
new_signing_identity: Option<SigningIdentity>,
) -> Result<CommitOutput, MlsError> {
) -> Result<(CommitOutput, CommitGeneration), MlsError> {
if self.pending_commit.is_some() {
return Err(MlsError::ExistingPendingCommit);
}
Expand Down Expand Up @@ -733,23 +787,23 @@ where
.await?,
};

self.pending_commit = Some(pending_commit);

let ratchet_tree = (!commit_options.ratchet_tree_extension)
.then(|| ExportedTree::new(provisional_state.public_tree.nodes));

if let Some(signer) = new_signer {
self.signer = signer;
}

Ok(CommitOutput {
let output = CommitOutput {
commit_message,
welcome_messages,
ratchet_tree,
external_commit_group_info,
#[cfg(feature = "by_ref_proposal")]
unused_proposals: provisional_state.unused_proposals,
})
};

Ok((output, pending_commit))
}

// Construct a GroupInfo reflecting the new state
Expand Down Expand Up @@ -836,7 +890,10 @@ mod tests {

use crate::{
crypto::test_utils::{test_cipher_suite_provider, TestCryptoProvider},
group::{mls_rules::DefaultMlsRules, test_utils::test_group_custom},
group::{
mls_rules::DefaultMlsRules,
test_utils::{test_group, test_group_custom},
},
mls_rules::CommitOptions,
Client,
};
Expand Down Expand Up @@ -1567,4 +1624,16 @@ mod tests {
.signing_identity(identity, secret_key, TEST_CIPHER_SUITE)
.build()
}

#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn detached_commit() {
let mut group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE)
.await
.group;

let (_commit, secrets) = group.commit_builder().build_detached().await.unwrap();
assert!(group.pending_commit.is_none());
group.apply_detached_commit(secrets).await.unwrap();
assert_eq!(group.context().epoch, 1);
}
}
3 changes: 2 additions & 1 deletion mls-rs/src/group/external_commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ impl<C: ClientConfig> ExternalCommitBuilder<C> {
}));
}

let commit_output = group
let (commit_output, pending_commit) = group
.commit_internal(
proposals,
Some(&leaf_node),
Expand All @@ -257,6 +257,7 @@ impl<C: ClientConfig> ExternalCommitBuilder<C> {
)
.await?;

group.pending_commit = Some(pending_commit);
group.apply_pending_commit().await?;

Ok((group, commit_output.commit_message))
Expand Down
23 changes: 18 additions & 5 deletions mls-rs/src/group/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ where
key_schedule: KeySchedule,
#[cfg(feature = "by_ref_proposal")]
pending_updates:
crate::map::SmallMap<HpkePublicKey, (HpkeSecretKey, Option<SignatureSecretKey>)>, // Hash of leaf node hpke public key to secret key
crate::map::SmallMap<HpkePublicKey, (HpkeSecretKey, Option<SignatureSecretKey>)>,
pending_commit: Option<CommitGeneration>,
#[cfg(feature = "psk")]
previous_psk: Option<PskSecretInput>,
Expand Down Expand Up @@ -1258,12 +1258,25 @@ where
/// [`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
let content = self
.pending_commit
.clone()
.ok_or(MlsError::PendingCommitNotFound)?;
.as_ref()
.ok_or(MlsError::PendingCommitNotFound)?
.content
.clone();

self.process_commit(content, None).await
}

self.process_commit(pending_commit.content, None).await
/// Apply a detached commit that was created by [`Group::commit_detached`] or
/// [`CommitBuilder::build_detached`].
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn apply_detached_commit(
&mut self,
commit_secrets: CommitSecrets,
) -> Result<CommitMessageDescription, MlsError> {
self.pending_commit = Some(commit_secrets.0);
self.apply_pending_commit().await
}

/// Returns true if a commit has been created but not yet applied
Expand Down

0 comments on commit 6f905df

Please sign in to comment.