Skip to content

Commit 0cf20a9

Browse files
committed
Merge remote-tracking branch 'upstream/franziskus/readd-api' into 10-08-temporary-readd-api
2 parents 7da990a + 3107180 commit 0cf20a9

4 files changed

Lines changed: 327 additions & 2 deletions

File tree

openmls/src/group/mls_group/commit_builder.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,36 @@ pub struct CommitMessageBundle {
824824
group_info: Option<GroupInfo>,
825825
}
826826

827+
/// The result of a commit with an add proposal. This includes
828+
/// - The Commit as an [`MlsMessageOut`]
829+
/// - The [`Welcome`] as an [`MlsMessageOut`]
830+
/// - Optionally a [`GroupInfo`] as an [`MlsMessageOut`]
831+
pub struct WelcomeCommitMessages {
832+
/// The Commit as an [`MlsMessageOut`].
833+
pub commit: MlsMessageOut,
834+
835+
/// The [`Welcome`] as an [`MlsMessageOut`].
836+
pub welcome: MlsMessageOut,
837+
838+
/// Optionally a [`GroupInfo`] as an [`MlsMessageOut`].
839+
pub group_info: Option<MlsMessageOut>,
840+
}
841+
842+
impl TryFrom<CommitMessageBundle> for WelcomeCommitMessages {
843+
type Error = LibraryError;
844+
845+
fn try_from(value: CommitMessageBundle) -> Result<Self, Self::Error> {
846+
let (commit, welcome_opt, group_info) = value.into_messages();
847+
Ok(Self {
848+
commit,
849+
welcome: welcome_opt.ok_or(LibraryError::custom(
850+
"WelcomeCommitMessages must only be used with commits that produce a welcome.",
851+
))?,
852+
group_info,
853+
})
854+
}
855+
}
856+
827857
#[cfg(test)]
828858
impl CommitMessageBundle {
829859
pub fn new(

openmls/src/group/mls_group/errors.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,42 @@ pub enum AddMembersError<StorageError> {
187187
StorageError(StorageError),
188188
}
189189

190+
/// Add members error
191+
#[derive(Error, Debug, PartialEq, Clone)]
192+
pub enum ReAddMembersError<StorageError> {
193+
/// Unable to map the key packages to the given leaf indices.
194+
#[error("Unable to map the key packages to the given leaf indices.")]
195+
InvalidInput,
196+
197+
/// See [`EmptyInputError`] for more details.
198+
#[error(transparent)]
199+
EmptyInput(#[from] EmptyInputError),
200+
201+
/// See [`MlsGroupStateError`] for more details.
202+
#[error(transparent)]
203+
GroupStateError(#[from] MlsGroupStateError),
204+
205+
/// See [`LibraryError`] for more details.
206+
#[error(transparent)]
207+
LibraryError(#[from] LibraryError),
208+
209+
/// The member that should be removed can not be found.
210+
#[error("The member that should be removed can not be found.")]
211+
UnknownMember,
212+
213+
/// Error writing to storage
214+
#[error("Error writing to storage: {0}")]
215+
StorageError(StorageError),
216+
217+
/// See [`CommitBuilderStageError`] for more details.
218+
#[error(transparent)]
219+
CommitBuilderStageError(#[from] CommitBuilderStageError<StorageError>),
220+
221+
/// See [`CreateCommitError`] for more details.
222+
#[error(transparent)]
223+
CreateCommitError(#[from] CreateCommitError),
224+
}
225+
190226
/// Propose add members error
191227
#[derive(Error, Debug, PartialEq, Clone)]
192228
pub enum ProposeAddMemberError<StorageError> {

openmls/src/group/mls_group/membership.rs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ use super::{
1111
*,
1212
};
1313
use crate::{
14-
binary_tree::array_representation::LeafNodeIndex, key_packages::KeyPackage,
15-
messages::group_info::GroupInfo, storage::OpenMlsProvider, treesync::LeafNode,
14+
binary_tree::array_representation::LeafNodeIndex,
15+
group::{ReAddMembersError, WelcomeCommitMessages},
16+
key_packages::KeyPackage,
17+
messages::group_info::GroupInfo,
18+
storage::OpenMlsProvider,
19+
treesync::LeafNode,
1620
};
1721

1822
type UpdateResult<Provider> = Result<
@@ -84,6 +88,94 @@ impl MlsGroup {
8488
self.add_members_internal(provider, signer, key_packages, true)
8589
}
8690

91+
/// Re-add members.
92+
///
93+
/// If there are issues with a set of members and they are suspected to be
94+
/// forked, this functions allows to remove and re-add them.
95+
/// This is a convenience function that first removes, and then re-adds the
96+
/// list of provided users.
97+
///
98+
/// Note that this functions _does not_ enforce that the removed `members`
99+
/// and new members in the `key_packages` correspond.
100+
///
101+
/// For the best performance it is important to sort the key packages in the
102+
/// order they appear in the tree (left to right). If `sort` is set to
103+
/// `true`, this function sorts the indices internally, based on the
104+
/// credential. Note that this only works if the credential doesn't change
105+
/// and the credential allows uniquely identifying a leaf node.
106+
pub fn readd_members<Provider: OpenMlsProvider>(
107+
&mut self,
108+
provider: &Provider,
109+
signer: &impl Signer,
110+
members: &[LeafNodeIndex],
111+
key_packages: &[KeyPackage],
112+
sort: bool,
113+
) -> Result<WelcomeCommitMessages, ReAddMembersError<Provider::StorageError>> {
114+
self.is_operational()?;
115+
116+
if members.is_empty() {
117+
return Err(EmptyInputError::RemoveMembers.into());
118+
}
119+
120+
if key_packages.is_empty() {
121+
return Err(EmptyInputError::AddMembers.into());
122+
}
123+
124+
let key_packages = if sort {
125+
// Sort the key packages to get the order from left to right.
126+
// This reduces churn by putting everyone back into their place.
127+
// This is not the most efficient, but we don't expect these lists
128+
// to be too long.
129+
let member_credentials_opt = members
130+
.iter()
131+
.map(|&leaf_index| self.member(leaf_index).map(|c| (leaf_index, c)));
132+
let member_credentials = member_credentials_opt.clone().flatten();
133+
134+
if member_credentials_opt.len() != member_credentials.clone().count() {
135+
// We couldn't find an index. The remove is not valid.
136+
return Err(ReAddMembersError::InvalidInput);
137+
}
138+
139+
// Get leaf indices for all key packages.
140+
let kp_indices = key_packages.iter().cloned().map(|kp| {
141+
self.member_leaf_index(kp.leaf_node().credential())
142+
.map(|i| (i, kp))
143+
});
144+
let mut indices_flat: Vec<_> = kp_indices.clone().flatten().collect();
145+
146+
if kp_indices.len() != indices_flat.len() {
147+
// We couldn't find a credential. The re-add is not valid.
148+
return Err(ReAddMembersError::InvalidInput);
149+
}
150+
151+
// Sort the key packages by index.
152+
indices_flat.sort_by(|(ai, _), (bi, _)| ai.cmp(bi));
153+
indices_flat
154+
.into_iter()
155+
.map(|(_, kp)| kp)
156+
.collect::<Vec<_>>()
157+
} else {
158+
key_packages.to_vec()
159+
};
160+
161+
let bundle = self
162+
.commit_builder()
163+
.propose_removals(members.iter().cloned())
164+
.propose_adds(key_packages.into_iter())
165+
.load_psks(provider.storage())?
166+
.build(provider.rand(), provider.crypto(), signer, |_| true)?
167+
.stage_commit(provider)?;
168+
169+
provider
170+
.storage()
171+
.write_group_state(self.group_id(), &self.group_state)
172+
.map_err(ReAddMembersError::StorageError)?;
173+
174+
self.reset_aad();
175+
176+
Ok(bundle.try_into()?)
177+
}
178+
87179
/// Adds members to the group.
88180
///
89181
/// New members are added by providing a `KeyPackage` for each member.
@@ -303,6 +395,14 @@ impl MlsGroup {
303395
self.public_group().members()
304396
}
305397

398+
/// Returns the [`LeafNodeIndex`] of a member corresponding to the given
399+
/// credential. Returns `None` if the member can not be found in this group.
400+
pub fn member_leaf_index(&self, credential: &Credential) -> Option<LeafNodeIndex> {
401+
self.members()
402+
.find(|m| &m.credential == credential)
403+
.map(|m| m.index)
404+
}
405+
306406
/// Returns the [`Credential`] of a member corresponding to the given
307407
/// leaf index. Returns `None` if the member can not be found in this group.
308408
pub fn member(&self, leaf_index: LeafNodeIndex) -> Option<&Credential> {

openmls/tests/readd.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use openmls::{prelude::*, test_utils::single_group_test_framework::*};
2+
use openmls_test::openmls_test;
3+
4+
#[openmls_test]
5+
fn readd() {
6+
let alice_party = CorePartyState::<Provider>::new("alice");
7+
let bob_party = CorePartyState::<Provider>::new("bob");
8+
let charlie_party = CorePartyState::<Provider>::new("charlie");
9+
let yuk_party = CorePartyState::<Provider>::new("yuk");
10+
11+
let alice_pre_group = alice_party.generate_pre_group(ciphersuite);
12+
let bob_pre_group = bob_party.generate_pre_group(ciphersuite);
13+
let charlie_pre_group = charlie_party.generate_pre_group(ciphersuite);
14+
let yuk_pre_group = yuk_party.generate_pre_group(ciphersuite);
15+
16+
let group_id = GroupId::from_slice(b"Test Group");
17+
18+
// Define the MlsGroup configuration
19+
let group_config = MlsGroupCreateConfig::builder()
20+
.ciphersuite(ciphersuite)
21+
.use_ratchet_tree_extension(true)
22+
.build();
23+
24+
let mut group_state =
25+
GroupState::new_from_party(group_id.clone(), alice_pre_group, group_config.clone())
26+
.unwrap();
27+
28+
// Generate KeyPackages
29+
let bob_key_package = bob_pre_group.key_package_bundle.key_package().clone();
30+
let charlie_key_package = charlie_pre_group.key_package_bundle.key_package().clone();
31+
let yuk_key_package = yuk_pre_group.key_package_bundle.key_package().clone();
32+
33+
let [alice] = group_state.members_mut(&["alice"]);
34+
35+
// === Alice adds Bob ===
36+
let (_commit, welcome, _group_info) = alice
37+
.group
38+
.add_members(
39+
&alice_party.provider,
40+
&alice.party.signer,
41+
&[bob_key_package, charlie_key_package, yuk_key_package],
42+
)
43+
.expect("Could not add folks");
44+
45+
let welcome: MlsMessageIn = welcome.into();
46+
let welcome = welcome.into_welcome().unwrap();
47+
48+
// Bob
49+
let staged_join = StagedWelcome::new_from_welcome(
50+
&bob_party.provider,
51+
group_config.join_config(),
52+
welcome.clone(),
53+
None,
54+
)
55+
.expect("Error constructing staged join");
56+
let mut bob_group = staged_join
57+
.into_group(&bob_party.provider)
58+
.expect("Error joining group from StagedWelcome");
59+
60+
// Charlie
61+
let staged_join = StagedWelcome::new_from_welcome(
62+
&charlie_party.provider,
63+
group_config.join_config(),
64+
welcome.clone(),
65+
None,
66+
)
67+
.expect("Error constructing staged join");
68+
69+
let _charlie_group = staged_join
70+
.into_group(&charlie_party.provider)
71+
.expect("Error joining group from StagedWelcome");
72+
73+
// Yuk
74+
let staged_join = StagedWelcome::new_from_welcome(
75+
&yuk_party.provider,
76+
group_config.join_config(),
77+
welcome.clone(),
78+
None,
79+
)
80+
.expect("Error constructing staged join");
81+
82+
let _yuk_group = staged_join
83+
.into_group(&yuk_party.provider)
84+
.expect("Error joining group from StagedWelcome");
85+
86+
// Bob re-adds alice and yuk
87+
let alice_key_package = KeyPackage::builder()
88+
.build(
89+
ciphersuite,
90+
&alice_party.provider,
91+
&alice.party.signer,
92+
alice.party.credential_with_key.clone(),
93+
)
94+
.unwrap();
95+
let yuk_key_package = KeyPackage::builder()
96+
.build(
97+
ciphersuite,
98+
&yuk_party.provider,
99+
&yuk_pre_group.signer,
100+
yuk_pre_group.credential_with_key.clone(),
101+
)
102+
.unwrap();
103+
104+
let commit_messages = bob_group
105+
.readd_members(
106+
&bob_party.provider,
107+
&bob_pre_group.signer,
108+
&[
109+
LeafNodeIndex::new(0), /*Alice */
110+
LeafNodeIndex::new(3), /*Yuk */
111+
],
112+
&[
113+
// This is the wrong order, but we ask the function to order.
114+
yuk_key_package.key_package().clone(),
115+
alice_key_package.key_package().clone(),
116+
],
117+
// If we make this false (no sorting), the check for indices below will fail.
118+
true,
119+
)
120+
.unwrap();
121+
bob_group.merge_pending_commit(&bob_party.provider).unwrap();
122+
123+
let welcome: MlsMessageIn = commit_messages.welcome.into();
124+
let welcome = welcome.into_welcome().unwrap();
125+
126+
// New Yuk
127+
let staged_join = StagedWelcome::new_from_welcome(
128+
&yuk_party.provider,
129+
group_config.join_config(),
130+
welcome.clone(),
131+
None,
132+
)
133+
.expect("Error constructing staged join");
134+
135+
let yuk_group = staged_join
136+
.into_group(&yuk_party.provider)
137+
.expect("Error joining group from StagedWelcome");
138+
139+
// New Alice
140+
let staged_join = StagedWelcome::new_from_welcome(
141+
&alice_party.provider,
142+
group_config.join_config(),
143+
welcome.clone(),
144+
None,
145+
)
146+
.expect("Error constructing staged join");
147+
148+
let alice_group = staged_join
149+
.into_group(&alice_party.provider)
150+
.expect("Error joining group from StagedWelcome");
151+
152+
// Yuk and Alice are back in.
153+
assert_eq!(alice_group.confirmation_tag(), bob_group.confirmation_tag());
154+
assert_eq!(yuk_group.confirmation_tag(), bob_group.confirmation_tag());
155+
156+
// They are in the same place
157+
assert_eq!(alice_group.own_leaf_index().u32(), 0);
158+
assert_eq!(yuk_group.own_leaf_index().u32(), 3);
159+
}

0 commit comments

Comments
 (0)