From 1efb377de7166240420f9af7f23b6d34a2540c0b Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:04:21 -0700 Subject: [PATCH] Add senderId to ConvoOutcome::Content --- .../src/conversation/group_v1.rs | 5 +++ .../src/conversation/group_v2.rs | 3 +- .../src/conversation/mls_utils.rs | 27 ++++++++++++++++ core/conversations/src/lib.rs | 2 +- core/conversations/src/outcomes.rs | 4 +-- core/shared-traits/src/lib.rs | 31 +++++++++++++++++++ 6 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 core/conversations/src/conversation/mls_utils.rs diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index 6d6b64a..af64e98 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -13,6 +13,7 @@ use std::collections::VecDeque; use tracing::debug; use crate::conversation::ConversationIdRef; +use crate::conversation::mls_utils::signer_for_sender; use crate::inbox_v2::MlsProvider; use crate::service_context::{ExternalServices, ServiceContext}; @@ -263,6 +264,9 @@ impl Convo for GroupV1Convo { .process_message(&cx.mls_provider, protocol_message) .map_err(ChatError::generic)?; + // Sender Id is not validated, the AuthService/Client is responsible for validating that the credential + // is valid for the sender + let sender_id = signer_for_sender(&self.mls_group, &processed)?; let cred_bytes = processed.credential().serialized_content().to_vec(); let content = match processed.into_content() { @@ -271,6 +275,7 @@ impl Convo for GroupV1Convo { cx.causal.on_receive(&self.convo_id, &reliable); Some(Content { bytes: reliable.content.to_vec(), + sender_id, encoded_credential: cred_bytes, }) } diff --git a/core/conversations/src/conversation/group_v2.rs b/core/conversations/src/conversation/group_v2.rs index c547e5a..7929bdf 100644 --- a/core/conversations/src/conversation/group_v2.rs +++ b/core/conversations/src/conversation/group_v2.rs @@ -471,7 +471,8 @@ impl GroupV2Convo { payload: Some(app_message::Payload::ConversationMessage(cm)), }) => Some(Content { bytes: cm.message.clone(), - encoded_credential: cm.sender.clone(), + sender_id: cm.sender.as_slice().into(), + encoded_credential: cm.sender_credential.clone(), }), _ => None, }); diff --git a/core/conversations/src/conversation/mls_utils.rs b/core/conversations/src/conversation/mls_utils.rs new file mode 100644 index 0000000..ca6ce41 --- /dev/null +++ b/core/conversations/src/conversation/mls_utils.rs @@ -0,0 +1,27 @@ +use openmls::{ + framing::{ProcessedMessage, Sender}, + group::MlsGroup, +}; + +use crate::{ChatError, SignerId}; + +pub fn signer_for_sender( + mls_group: &MlsGroup, + processed: &ProcessedMessage, +) -> Result { + // The signature key openmls just verified this message under. + let sender_sig_key: Vec = match processed.sender() { + Sender::Member(leaf_index) => { + mls_group + .member_at(*leaf_index) + .ok_or_else(|| ChatError::generic("sender leaf not in tree"))? + .signature_key + } + // Application/private messages always come from a Member; anything else + // here is a protocol violation. + other => { + return Err(ChatError::generic(format!("unexpected sender: {other:?}"))); + } + }; + Ok(sender_sig_key.into()) +} diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index 6aa9d50..fa0504b 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -28,10 +28,10 @@ pub use outcomes::{ Content, ConversationClass, ConvoOutcome, InboxOutcome, NewConversation, PayloadOutcome, }; pub use service_context::ExternalServices; -pub use shared_traits::{IdentId, IdentIdRef, IdentityProvider}; pub use service_traits::{ AuthResult, AuthVerifyService, DeliveryService, RegistrationService, WakeupService, }; +pub use shared_traits::{IdentId, IdentIdRef, IdentityProvider, SignerId}; pub use storage::{ChatStore, ConversationKind}; pub use types::{AddressedEnvelope, ConvoMetadata}; pub use utils::{hex_trunc, trunc}; diff --git a/core/conversations/src/outcomes.rs b/core/conversations/src/outcomes.rs index 18877e7..33b4b4f 100644 --- a/core/conversations/src/outcomes.rs +++ b/core/conversations/src/outcomes.rs @@ -8,13 +8,13 @@ use storage::ConversationKind; +use crate::SignerId; use crate::conversation::ConversationId; #[derive(Debug, Clone)] pub struct Content { pub bytes: Vec, - /// Hex-encoded [`DelegateCredential`] of the sender, if present in the message. - /// Empty when the sender did not attach a credential. + pub sender_id: SignerId, pub encoded_credential: Vec, } diff --git a/core/shared-traits/src/lib.rs b/core/shared-traits/src/lib.rs index 06316cb..60b5596 100644 --- a/core/shared-traits/src/lib.rs +++ b/core/shared-traits/src/lib.rs @@ -27,6 +27,37 @@ impl AsRef for IdentId { } } +#[derive(Debug, Clone)] +pub struct SignerId(Vec); + +impl SignerId { + pub fn from_ed25519(key: &Ed25519VerifyingKey) -> Self { + Self(key.as_ref().to_vec()) + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_slice() + } +} + +impl From> for SignerId { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl From<&[u8]> for SignerId { + fn from(value: &[u8]) -> Self { + Self(value.to_vec()) + } +} + +impl AsRef<[u8]> for SignerId { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + /// Represents an external Identity /// Implement this to provide an Authentication model for users/installations pub trait IdentityProvider {