1use openmls::prelude::{ContentType, KeyPackageIn, MlsMessageIn, ProtocolMessage};
4use openmls_rust_crypto::RustCrypto;
5use tls_codec::Deserialize;
6use xmtp_common::RetryableError;
7use xmtp_id::{
8 associations::{
9 self, AssociationError, AssociationState, AssociationStateDiff, DeserializationError,
10 SignatureError, try_map_vec, unverified::UnverifiedIdentityUpdate, verify_updates,
11 },
12 key_package::{KeyPackageVerificationError, VerifiedKeyPackageV2},
13 scw_verifier::SmartContractSignatureVerifier,
14};
15use xmtp_mls_common::commit_log::decode_commit_log;
16use xmtp_proto::{
17 ConversionError,
18 types::{CanonicalEnvelope, Topic, TopicKind, canonical_envelope},
19 xmtp::{
20 backend::v1::{
21 ClientEnvelope, client_envelope::Payload, publish_error::Reason,
22 welcome_message::Version,
23 },
24 identity::associations::IdentityUpdate,
25 },
26};
27
28#[cfg(any(test, feature = "test-utils"))]
29pub mod test_utils;
30
31#[cfg(test)]
32mod tests;
33
34#[derive(Debug, thiserror::Error, xmtp_common::ErrorCode)]
35#[error_code(internal)]
36pub enum ValidationError {
37 #[error("envelope payload is absent")]
39 MissingPayload,
40 #[error("welcome version is absent")]
42 MissingWelcomeVersion,
43 #[error(transparent)]
45 Protobuf(#[from] prost::DecodeError),
46 #[error(transparent)]
48 Tls(#[from] tls_codec::Error),
49 #[error(transparent)]
51 Protocol(#[from] openmls::framing::errors::ProtocolMessageError),
52 #[error(transparent)]
54 #[error_code(inherit)]
55 Conversion(#[from] ConversionError),
56 #[error(transparent)]
58 Inbox(#[from] hex::FromHexError),
59 #[error(transparent)]
61 #[error_code(inherit)]
62 KeyPackage(#[from] KeyPackageVerificationError),
63 #[error(transparent)]
65 #[error_code(inherit)]
66 IdentityEncoding(#[from] DeserializationError),
67 #[error(transparent)]
69 #[error_code(inherit)]
70 Association(#[from] AssociationError),
71 #[error(transparent)]
73 #[error_code(inherit)]
74 Signature(#[from] SignatureError),
75}
76
77impl RetryableError for ValidationError {
78 fn is_retryable(&self) -> bool {
79 match self {
80 Self::Signature(error) | Self::Association(AssociationError::Signature(error)) => {
81 error.is_retryable()
82 }
83 _ => false,
84 }
85 }
86}
87
88impl ValidationError {
89 pub fn reason(&self) -> Reason {
95 match self {
96 Self::KeyPackage(_) => Reason::InvalidKeyPackage,
97 Self::Signature(_) | Self::Association(AssociationError::Signature(_)) => {
98 Reason::InvalidSignature
99 }
100 Self::IdentityEncoding(_) | Self::Association(_) => Reason::InvalidIdentityUpdate,
101 _ => Reason::MalformedPayload,
102 }
103 }
104}
105
106pub struct ParsedEnvelope {
112 pub envelope: ClientEnvelope,
114 pub topic: Topic,
116 pub is_commit_or_proposal: bool,
118 pub canonical: CanonicalEnvelope,
120}
121
122impl ParsedEnvelope {
123 pub fn push_fields(&self) -> (bool, Option<Vec<u8>>) {
126 match self.envelope.payload.as_ref() {
127 Some(Payload::GroupMessage(group)) => (
128 self.is_commit_or_proposal || group.should_push,
129 (group.sender_hmac.len() == 32).then(|| group.sender_hmac.clone()),
130 ),
131 Some(Payload::WelcomeMessage(_)) => (true, None),
132 _ => (false, None),
133 }
134 }
135}
136
137pub fn parse_group_message(data: &[u8]) -> Result<ProtocolMessage, ValidationError> {
142 Ok(MlsMessageIn::tls_deserialize(&mut &data[..])?.try_into_protocol_message()?)
143}
144
145pub fn is_commit_or_proposal(message: &ProtocolMessage) -> bool {
150 matches!(
151 message.content_type(),
152 ContentType::Commit | ContentType::Proposal
153 )
154}
155
156fn checked_topic(kind: TopicKind, identifier: impl AsRef<[u8]>) -> Result<Topic, ValidationError> {
158 let topic = kind.create(identifier);
159 Ok(Topic::parse(&topic)?)
160}
161
162pub fn parse_envelope(envelope: ClientEnvelope) -> Result<ParsedEnvelope, ValidationError> {
168 let payload = envelope
169 .payload
170 .as_ref()
171 .ok_or(ValidationError::MissingPayload)?;
172 let mut is_commit_or_proposal = false;
173 let topic = match payload {
174 Payload::GroupMessage(group) => {
175 let message = parse_group_message(&group.data)?;
176 is_commit_or_proposal = self::is_commit_or_proposal(&message);
177 checked_topic(TopicKind::GroupMessagesV1, message.group_id().as_slice())?
178 }
179 Payload::WelcomeMessage(welcome) => {
180 let key = match welcome
181 .version
182 .as_ref()
183 .ok_or(ValidationError::MissingWelcomeVersion)?
184 {
185 Version::V1(welcome) => &welcome.installation_key,
186 Version::WelcomePointer(pointer) => &pointer.installation_key,
187 };
188 checked_topic(TopicKind::WelcomeMessagesV1, key)?
189 }
190 Payload::KeyPackage(package) => {
191 let package = KeyPackageIn::tls_deserialize_exact(&package.key_package_tls_serialized)?;
192 checked_topic(
193 TopicKind::KeyPackagesV1,
194 package.unverified_credential().signature_key.as_slice(),
195 )?
196 }
197 Payload::IdentityUpdate(update) => {
198 checked_topic(TopicKind::IdentityUpdatesV1, hex::decode(&update.inbox_id)?)?
199 }
200 Payload::CommitLogEntry(entry) => {
201 let decoded = decode_commit_log(&entry.serialized_commit_log_entry)?;
202 checked_topic(TopicKind::CommitLogEntriesV1, &decoded.group_id)?
203 }
204 };
205 let canonical = canonical_envelope(&envelope);
206 Ok(ParsedEnvelope {
207 envelope,
208 topic,
209 is_commit_or_proposal,
210 canonical,
211 })
212}
213
214pub fn verify_key_package(
219 data: &[u8],
220) -> Result<VerifiedKeyPackageV2, KeyPackageVerificationError> {
221 VerifiedKeyPackageV2::from_bytes(&RustCrypto::default(), data)
222}
223
224pub struct AssociationValidation {
225 pub state: AssociationState,
227 pub diff: AssociationStateDiff,
229}
230
231pub async fn validate_identity_updates(
238 old_updates: Vec<IdentityUpdate>,
239 new_updates: Vec<IdentityUpdate>,
240 verifier: impl SmartContractSignatureVerifier,
241) -> Result<AssociationValidation, ValidationError> {
242 let old: Vec<UnverifiedIdentityUpdate> = try_map_vec(old_updates)?;
243 let new: Vec<UnverifiedIdentityUpdate> = try_map_vec(new_updates)?;
244 let old = verify_updates(old, &verifier).await?;
245 let new = verify_updates(new, &verifier).await?;
246 if old.is_empty() {
247 let state = associations::get_state(new)?;
248 let diff = state.as_diff();
249 return Ok(AssociationValidation { state, diff });
250 }
251 let old_state = associations::get_state(old)?;
252 let mut state = old_state.clone();
253 for update in new {
254 state = associations::apply_update(state, update)?;
255 }
256 let diff = old_state.diff(&state);
257 Ok(AssociationValidation { state, diff })
258}
259
260pub async fn validate_envelope(
266 parsed: &ParsedEnvelope,
267 history: &[IdentityUpdate],
268 verifier: impl SmartContractSignatureVerifier,
269) -> Result<Option<AssociationValidation>, ValidationError> {
270 match parsed
271 .envelope
272 .payload
273 .as_ref()
274 .ok_or(ValidationError::MissingPayload)?
275 {
276 Payload::KeyPackage(package) => {
277 verify_key_package(&package.key_package_tls_serialized)?;
278 Ok(None)
279 }
280 Payload::IdentityUpdate(update) => Ok(Some(
281 validate_identity_updates(history.to_vec(), vec![update.clone()], verifier).await?,
282 )),
283 _ => Ok(None),
284 }
285}