xmtp_mls/groups/welcomes/
validated_membership.rs1use crate::context::XmtpSharedContext;
2use crate::groups::{GroupError, GroupMembership};
3use crate::identity::parse_credential;
4use crate::identity_updates::{
5 IdentityDependencyError, IdentityRequirement, InstallationDiffError, require_association_state,
6 resolve_identity_requirements,
7};
8use openmls::prelude::{BasicCredential, StagedWelcome};
9use std::collections::{HashMap, HashSet};
10use xmtp_common::snippet::Snippet;
11use xmtp_db::DbQuery;
12use xmtp_mls_validation::commit::extract_group_membership;
13
14#[allow(async_fn_in_trait)]
16pub trait ValidateGroupMembership {
17 async fn check_initial_membership(&self, welcome: &WelcomeMembership)
19 -> Result<(), GroupError>;
20
21 fn check_verified_membership(
23 &self,
24 _welcome: &WelcomeMembership,
25 _db: &impl DbQuery,
26 ) -> Result<(), GroupError> {
27 Ok(())
28 }
29}
30
31#[derive(Debug, PartialEq)]
33pub struct WelcomeMembership {
34 membership: GroupMembership,
36 members: Vec<(String, Vec<u8>)>,
38}
39
40impl WelcomeMembership {
41 pub(crate) fn from_staged(welcome: &StagedWelcome) -> Result<Self, GroupError> {
43 let extensions = welcome.public_group().group_context().extensions();
44 let membership =
45 extract_group_membership(extensions).map_err(|_| GroupError::InvalidWelcomeMetadata)?;
46 let members = welcome
47 .public_group()
48 .members()
49 .map(|member| {
50 let credential = BasicCredential::try_from(member.credential.clone())?;
51 Ok((
52 parse_credential(credential.identity())?,
53 member.signature_key,
54 ))
55 })
56 .collect::<Result<_, GroupError>>()?;
57 Ok(Self {
58 membership,
59 members,
60 })
61 }
62
63 fn requirements(&self) -> impl Iterator<Item = IdentityRequirement> + '_ {
64 self.membership
65 .members
66 .iter()
67 .map(|(inbox_id, sequence_id)| IdentityRequirement {
68 inbox_id: inbox_id.clone(),
69 sequence_id: *sequence_id,
70 })
71 }
72
73 pub(crate) fn validate_sequences(&self, welcome_sequence: u64) -> Result<(), GroupError> {
76 for requirement in self.requirements() {
77 if requirement.sequence_id == 0 || requirement.sequence_id >= welcome_sequence {
78 return Err(
79 InstallationDiffError::from(IdentityDependencyError::InvalidSequence(
80 requirement.sequence_id,
81 ))
82 .into(),
83 );
84 }
85 }
86 Ok(())
87 }
88}
89
90pub struct InitialMembershipValidator<C> {
92 context: C,
93}
94
95impl<C> InitialMembershipValidator<C> {
96 pub fn new(context: C) -> InitialMembershipValidator<C> {
97 Self { context }
98 }
99}
100
101impl<C> ValidateGroupMembership for InitialMembershipValidator<C>
102where
103 C: XmtpSharedContext,
104{
105 async fn check_initial_membership(
107 &self,
108 welcome: &WelcomeMembership,
109 ) -> Result<(), GroupError> {
110 for (_, result) in
111 resolve_identity_requirements(&self.context, welcome.requirements()).await
112 {
113 result.map_err(InstallationDiffError::from)?;
114 }
115 self.check_verified_membership(welcome, &self.context.db())
116 }
117
118 fn check_verified_membership(
120 &self,
121 welcome: &WelcomeMembership,
122 db: &impl DbQuery,
123 ) -> Result<(), GroupError> {
124 let membership = &welcome.membership;
125 let mut expected_members = HashMap::<String, HashSet<Vec<u8>>>::new();
126 for requirement in welcome.requirements() {
127 let association_state =
128 require_association_state(db, &requirement).map_err(InstallationDiffError::from)?;
129 expected_members.insert(
130 association_state.inbox_id().to_string(),
131 HashSet::from_iter(association_state.installation_ids()),
132 );
133 }
134
135 for (claimed_inbox_id, signature_key) in &welcome.members {
136 let Some(installation_ids) = expected_members.get_mut(claimed_inbox_id) else {
137 tracing::error!(
138 claimed_inbox_id = claimed_inbox_id,
139 "Inbox ID not found in expected members",
140 );
141 return Err(GroupError::InvalidGroupMembership);
142 };
143 if !installation_ids.contains(signature_key) {
144 tracing::error!(
145 claimed_inbox_id = claimed_inbox_id,
146 "Installation ID not found in expected members for inbox ID",
147 );
148 return Err(GroupError::InvalidGroupMembership);
149 }
150 installation_ids.remove(signature_key);
151 }
152 for installation_set in expected_members.values() {
153 for remaining_installation_id in installation_set {
154 if !membership
155 .failed_installations
156 .contains(remaining_installation_id)
157 {
158 tracing::error!(
159 installation_id = %remaining_installation_id.as_slice().snippet(),
160 "Installation ID in expected members not found in ratchet tree",
161 );
162 return Err(GroupError::InvalidGroupMembership);
163 }
164 }
165 }
166 tracing::info!("Group membership validated");
169
170 Ok(())
171 }
172}
173
174#[cfg(any(test, feature = "test-utils"))]
175pub mod test {
176 use super::*;
177
178 #[derive(Default)]
179 pub struct NoopValidator;
180
181 impl ValidateGroupMembership for NoopValidator {
182 async fn check_initial_membership(
183 &self,
184 _welcome: &WelcomeMembership,
185 ) -> Result<(), GroupError> {
186 Ok(())
187 }
188 }
189}