From 0165612695f0a0a58fe85e552a447598d0e42761 Mon Sep 17 00:00:00 2001 From: seemenkina Date: Wed, 17 Jun 2026 18:10:14 +0300 Subject: [PATCH 1/6] update code after mls service split up --- Cargo.lock | 45 +--- core/conversations/Cargo.toml | 2 +- .../src/conversation/group_v2.rs | 192 +++++++++++++++--- core/conversations/src/inbox_v2.rs | 3 +- core/conversations/src/inbox_v2/identity.rs | 1 + 5 files changed, 166 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4743ec..a83eaf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1814,13 +1814,11 @@ dependencies = [ [[package]] name = "de-mls" version = "3.0.0" -source = "git+https://github.com/vacp2p/de-mls?branch=develop#d838e832994fd1d14f624783741bc60b31510fa0" +source = "git+https://github.com/vacp2p/de-mls?branch=refactor%2Fmls-contract-engine-split#a87b5eba396a36037174e59b28ef561fada179ca" dependencies = [ "hashgraph-like-consensus", "indexmap 2.14.0", "openmls", - "openmls_basic_credential", - "openmls_rust_crypto 0.5.1", "openmls_traits 0.5.0", "prost", "prost-build", @@ -3895,20 +3893,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "openmls_basic_credential" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "983e8be1457dd6f316f409292cec334af3b57b49a19deadc925c83c3c35e15b6" -dependencies = [ - "ed25519-dalek", - "openmls_traits 0.5.0", - "p256", - "rand 0.8.6", - "serde", - "tls_codec", -] - [[package]] name = "openmls_libcrux_crypto" version = "0.2.4" @@ -4004,31 +3988,6 @@ dependencies = [ "tls_codec", ] -[[package]] -name = "openmls_rust_crypto" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fafcc8a3552b10fbb3ab757cccaf1a34081e826ca819f49aa7e6645b1d95c00f" -dependencies = [ - "aes-gcm", - "chacha20poly1305", - "ed25519-dalek", - "hkdf", - "hmac", - "hpke-rs", - "hpke-rs-crypto", - "hpke-rs-rust-crypto", - "openmls_memory_storage 0.5.0", - "openmls_traits 0.5.0", - "p256", - "rand 0.8.6", - "rand_chacha 0.3.1", - "serde", - "sha2 0.10.9", - "thiserror", - "tls_codec", -] - [[package]] name = "openmls_test" version = "0.2.1" @@ -4037,7 +3996,7 @@ checksum = "46c5984361586c8ef56108664ffec909fa78126be8eef1983723f0aed80a9266" dependencies = [ "ansi_term", "openmls_libcrux_crypto 0.2.4", - "openmls_rust_crypto 0.4.4", + "openmls_rust_crypto", "openmls_traits 0.4.1", "proc-macro2", "quote", diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml index 52caa29..f2e75e1 100644 --- a/core/conversations/Cargo.toml +++ b/core/conversations/Cargo.toml @@ -18,7 +18,7 @@ storage = { workspace = true } alloy = "2.0" base64 = "0.22" chat-proto = { git = "https://github.com/logos-messaging/chat_proto", rev = "37ec98a151f6d50aab2905802ac0a896477e62ea" } -de-mls = { git = "https://github.com/vacp2p/de-mls", branch = "develop" } +de-mls = { git = "https://github.com/vacp2p/de-mls", branch = "refactor/mls-contract-engine-split" } double-ratchets = { path = "../double-ratchets" } hashgraph-like-consensus = "0.5.1" hex = "0.4.3" diff --git a/core/conversations/src/conversation/group_v2.rs b/core/conversations/src/conversation/group_v2.rs index c8a42cb..b67fcb9 100644 --- a/core/conversations/src/conversation/group_v2.rs +++ b/core/conversations/src/conversation/group_v2.rs @@ -9,24 +9,32 @@ use blake2::{Blake2b, Digest, digest::consts::U6}; use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload}; use de_mls::core::{ ConsensusPlugin, ConsensusServiceFor, ConversationEvent, ConversationPluginsFactory, - ScoringConfig, StewardListConfig, + DeterministicStewardList, PeerScoringService, ScoringConfig, StewardListConfig, + default_score_deltas, }; use de_mls::defaults::{ - DefaultConsensusPlugin, DefaultConversationPluginsFactory, MemoryDeMlsStorage, + DefaultConsensusPlugin, DefaultPeerScoring, DefaultStewardList, InMemoryPeerScoreStorage, }; use de_mls::member_id::MemberId; -use de_mls::mls_crypto::MlsCredentials; +use de_mls::mls_crypto::{KeyPackageBytes, MlsError, OpenMlsService}; use de_mls::protos::de_mls::messages::v1::{ AppMessage as AppMessageProto, MemberWelcome, app_message, }; use de_mls::session::{Conversation, ConversationConfig, ConversationDeps}; use hashgraph_like_consensus::signing::EthereumConsensusSigner; +use openmls::key_packages::KeyPackage; +use openmls::prelude::tls_codec::Serialize as _; +use openmls_traits::signatures::Signer; use prost::Message; use shared_traits::{IdentId, IdentIdRef}; +use std::cell::RefCell; use std::sync::Arc; use std::time::Duration; use tracing::{info, instrument, warn}; +use crate::inbox_v2::{CIPHER_SUITE, MlsEphemeralPqProvider, MlsIdentityProvider}; +use crypto::{Ed25519Signature, Ed25519SigningKey, Ed25519VerifyingKey}; + use crate::IdentityProvider; use crate::conversation::{ConversationIdRef, ExternalServices, ServiceContext}; use crate::{ @@ -38,25 +46,59 @@ use crate::{ /// with the openmls (GroupV1) keypackage registered under the bare account id. const DEMLS_KEYPACKAGE_NAMESPACE: &str = "demls"; -/// This is a Test Wrapper of Demls MemberId Trait -/// Libchat has its own trait that will need to be intergrated at somepoint. -pub struct LocalDemlsMember { - name: String, +/// Owned, `Clone` identity de-mls's `Sig` can hold. A new type only because the +/// account identity (`S::IP`) is neither owned nor `Clone` here, and +/// `crypto::Identity` implements `IdentityProvider` only under `cfg(test)`. +/// Wrapped in [`MlsIdentityProvider`] to reuse its credential + `Signer`. +#[derive(Clone)] +struct DemlsMember { + id: IdentId, + signing: Ed25519SigningKey, + verifying: Ed25519VerifyingKey, +} + +impl DemlsMember { + fn new(name: impl Into) -> Self { + let signing = Ed25519SigningKey::generate(); + Self { + verifying: signing.verifying_key(), + signing, + id: IdentId::new(name.into()), + } + } } -impl LocalDemlsMember { - pub fn new(name: impl Into) -> Self { - Self { name: name.into() } +impl IdentityProvider for DemlsMember { + fn id(&self) -> IdentIdRef<'_> { + &self.id + } + + fn display_name(&self) -> String { + self.id.as_str().to_string() + } + + fn sign(&self, payload: &[u8]) -> Ed25519Signature { + self.signing.sign(payload) + } + + fn public_key(&self) -> &Ed25519VerifyingKey { + &self.verifying } } -impl MemberId for LocalDemlsMember { +/// The de-mls signer: libchat's `MlsIdentityProvider` over a [`DemlsMember`]. +/// Already a `Signer` + credential source; we also give it de-mls's `MemberId` +/// so the protocol-side identity bytes match the MLS credential's serialized +/// content (`id().as_str().as_bytes()`). +type DemlsSigner = MlsIdentityProvider; + +impl MemberId for DemlsSigner { fn member_id_bytes(&self) -> &[u8] { - self.name.as_bytes() + self.id().as_str().as_bytes() } fn member_id_display(&self) -> &str { - &self.name + self.id().as_str() } } @@ -97,9 +139,101 @@ impl IdentityProvider for NamespacedIdentity<'_> { } } +/// The de-mls MLS service over libchat's PQ provider. +type DemlsMls = OpenMlsService; + +/// Reference de-mls plug-in factory over libchat's existing PQ provider. Holds +/// a clone of the signer (to mint key packages) and stashes the provider that +/// minted our key package so the matching `welcome_mls` reuses its private keys +/// — replacing the old key-registry namespacing workaround for private keys. +struct DemlsFactory { + signer: DemlsSigner, + pending_provider: RefCell>, +} + +impl DemlsFactory { + fn new(signer: DemlsSigner) -> Self { + Self { + signer, + pending_provider: RefCell::new(None), + } + } + + /// Mint a single-use key package into a fresh provider, stashing that + /// provider so the matching `welcome_mls` can open the welcome with the key + /// package's private keys. + fn generate_key_package(&self) -> Result { + let provider = MlsEphemeralPqProvider::new().map_err(ChatError::generic)?; + let bundle = KeyPackage::builder() + .build( + CIPHER_SUITE, + &provider, + &self.signer, + self.signer.get_credential(), + ) + .map_err(ChatError::generic)?; + let bytes = bundle + .key_package() + .tls_serialize_detached() + .map_err(ChatError::generic)?; + *self.pending_provider.borrow_mut() = Some(provider); + Ok(KeyPackageBytes::new( + bytes, + self.signer.member_id_bytes().to_vec(), + )) + } +} + +impl ConversationPluginsFactory for DemlsFactory { + type Mls = DemlsMls; + type Scoring = DefaultPeerScoring; + type StewardList = DefaultStewardList; + + fn create_mls( + &self, + conversation_id: String, + key_package: &[u8], + signer: &impl Signer, + ) -> Result { + OpenMlsService::new_as_creator( + conversation_id, + MlsEphemeralPqProvider::new()?, + key_package, + signer, + ) + } + + fn welcome_mls(&self, welcome_bytes: &[u8]) -> Result, MlsError> { + // Each conversation has its own factory and stash, and welcomes are + // routed only to the joiner that minted the key package. A missing + // provider is therefore a logic error here — not a "not for us" case — + // so surface it instead of silently yielding `None`. + let provider = self.pending_provider.borrow_mut().take().ok_or_else(|| { + MlsError::Welcome("no pending key-package provider for this conversation".into()) + })?; + OpenMlsService::new_from_welcome(welcome_bytes, provider) + } + + fn make_scoring(&self, config: &ScoringConfig) -> Self::Scoring { + PeerScoringService::new( + InMemoryPeerScoreStorage::new(), + default_score_deltas(), + config.clone(), + ) + } + + fn make_steward_list( + &self, + conversation_id: &[u8], + config: StewardListConfig, + ) -> Self::StewardList { + DeterministicStewardList::empty(conversation_id.to_vec(), config) + } +} + struct DemlsSetup { - member: LocalDemlsMember, - factory: DefaultConversationPluginsFactory, + signer: DemlsSigner, + factory: DemlsFactory, consensus_storage: ::ConsensusStorage, consensus_signer: EthereumConsensusSigner, app_id: Vec, // random bytes; echo-dedup key @@ -108,12 +242,8 @@ struct DemlsSetup { impl DemlsSetup { fn new(identity_name: String) -> Result { - let member = LocalDemlsMember::new(identity_name); - let credentials = Arc::new(MlsCredentials::from_member_id(&member)?); - let factory = DefaultConversationPluginsFactory::new( - Arc::new(MemoryDeMlsStorage::new()), - credentials, - ); + let signer = MlsIdentityProvider::new(DemlsMember::new(identity_name)); + let factory = DemlsFactory::new(signer.clone()); // TODO(config): TEST-ONLY millisecond timers. de-mls deadlines are real // wall-clock, so the default 60s timers never fire under fast virtual // time. Production needs a real config injected from the caller, not @@ -128,7 +258,7 @@ impl DemlsSetup { ..ConversationConfig::default() }; Ok(DemlsSetup { - member, + signer, factory, consensus_storage: DefaultConsensusPlugin::new_storage(), consensus_signer: EthereumConsensusSigner::new(PrivateKeySigner::random()), @@ -138,9 +268,7 @@ impl DemlsSetup { } /// Call exactly once per Conversation construction. - fn deps( - &self, - ) -> ConversationDeps<'_, DefaultConsensusPlugin, DefaultConversationPluginsFactory> { + fn deps(&self) -> ConversationDeps<'_, DefaultConsensusPlugin, DemlsFactory, DemlsSigner> { ConversationDeps { plugins: &self.factory, consensus: ConsensusServiceFor::::new_with_components( @@ -149,7 +277,8 @@ impl DemlsSetup { self.consensus_signer.clone(), 10, ), - identity: &self.member, + signer: self.signer.clone(), + identity: &self.signer, app_id: Arc::from(self.app_id.as_slice()), config: self.config.clone(), scoring_config: ScoringConfig::default(), @@ -161,7 +290,7 @@ impl DemlsSetup { pub struct GroupV2Convo { convo_id: String, setup: DemlsSetup, - conversation: Option>, + conversation: Option>, /// Member-ids we proposed via add_member. WelcomeReady now fires on /// every member; we forward a welcome only to joiners WE invited. pending_invites: Vec>, @@ -186,7 +315,8 @@ impl GroupV2Convo { ) -> Result { let setup = DemlsSetup::new(service_ctx.mls_identity.display_name())?; let convo_id = rand_string(5); - let conversation = Conversation::create(&convo_id, setup.deps())?; + let key_package = setup.factory.generate_key_package()?; + let conversation = Conversation::create(&convo_id, key_package.as_bytes(), setup.deps())?; let convo = GroupV2Convo { convo_id, setup, @@ -209,10 +339,8 @@ impl GroupV2Convo { let setup = DemlsSetup::new(name.clone())?; let kp = setup.factory.generate_key_package()?; - // TEMPORARY: Demls creates its own Provider which causes keys to be fragmented in different storage providers. - // The key registry does not support a method to namespace keys with the same identity. When the key is pulled down it cannot - // guarentee it was the one created with demls owned provider, resulting in failure. - // This workaround prefixes the ID used to store the keys, such that they do not conflict. + // Namespace the key package so it doesn't collide with the GroupV1 + // key package the registry keys under the bare account id. let namespaced = NamespacedIdentity::new(&*service_ctx.mls_identity, DEMLS_KEYPACKAGE_NAMESPACE); service_ctx diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index 23589ff..91de64f 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -30,7 +30,8 @@ use crate::{ use crate::{IdentId, IdentIdRef, IdentityProvider}; // Downgraded from MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519 until demls accepts an external provider -const CIPHER_SUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519; +pub(crate) const CIPHER_SUITE: Ciphersuite = + Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519; // Define unique Identifiers derivations used in InboxV2 fn delivery_address_for(ident_id: IdentIdRef) -> String { diff --git a/core/conversations/src/inbox_v2/identity.rs b/core/conversations/src/inbox_v2/identity.rs index 779f90b..4016da5 100644 --- a/core/conversations/src/inbox_v2/identity.rs +++ b/core/conversations/src/inbox_v2/identity.rs @@ -16,6 +16,7 @@ use crate::IdentityProvider; /// This type stops OpenMLS internal from leaking outside of the crate. /// Developers provider a simple IdentitityProvider, and Signer and Credential generation /// is provided +#[derive(Clone)] pub struct MlsIdentityProvider(T); impl MlsIdentityProvider { From d3ff02ea4ec5450dfb187fbff536ebe983590729 Mon Sep 17 00:00:00 2001 From: seemenkina Date: Fri, 19 Jun 2026 13:25:27 +0300 Subject: [PATCH 2/6] update de-mls setup --- Cargo.lock | 100 +---- .../src/conversation/group_v2.rs | 355 +++++++----------- core/conversations/src/errors.rs | 2 +- 3 files changed, 142 insertions(+), 315 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a83eaf2..2145237 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -774,7 +774,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.59.0", "x11rb", ] @@ -1814,7 +1814,7 @@ dependencies = [ [[package]] name = "de-mls" version = "3.0.0" -source = "git+https://github.com/vacp2p/de-mls?branch=refactor%2Fmls-contract-engine-split#a87b5eba396a36037174e59b28ef561fada179ca" +source = "git+https://github.com/vacp2p/de-mls?branch=refactor%2Fmls-contract-engine-split#4269e2faccd0855840492313c201b76660457827" dependencies = [ "hashgraph-like-consensus", "indexmap 2.14.0", @@ -4493,7 +4493,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6427,7 +6427,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -6436,16 +6436,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -6463,31 +6454,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -6496,96 +6470,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "1.0.1" diff --git a/core/conversations/src/conversation/group_v2.rs b/core/conversations/src/conversation/group_v2.rs index b67fcb9..b5e0a4f 100644 --- a/core/conversations/src/conversation/group_v2.rs +++ b/core/conversations/src/conversation/group_v2.rs @@ -7,33 +7,28 @@ use crate::{Content, WakeupService}; use alloy::signers::local::PrivateKeySigner; use blake2::{Blake2b, Digest, digest::consts::U6}; use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload}; -use de_mls::core::{ - ConsensusPlugin, ConsensusServiceFor, ConversationEvent, ConversationPluginsFactory, - DeterministicStewardList, PeerScoringService, ScoringConfig, StewardListConfig, - default_score_deltas, -}; use de_mls::defaults::{ DefaultConsensusPlugin, DefaultPeerScoring, DefaultStewardList, InMemoryPeerScoreStorage, }; -use de_mls::member_id::MemberId; -use de_mls::mls_crypto::{KeyPackageBytes, MlsError, OpenMlsService}; use de_mls::protos::de_mls::messages::v1::{ AppMessage as AppMessageProto, MemberWelcome, app_message, }; -use de_mls::session::{Conversation, ConversationConfig, ConversationDeps}; +use de_mls::{ + ConsensusPlugin, ConsensusServiceFor, Conversation, ConversationConfig, ConversationEvent, + DeterministicStewardList, PeerScoringService, ScoringConfig, StewardListConfig, + default_score_deltas, +}; use hashgraph_like_consensus::signing::EthereumConsensusSigner; use openmls::key_packages::KeyPackage; use openmls::prelude::tls_codec::Serialize as _; -use openmls_traits::signatures::Signer; +use openmls::prelude::{Capabilities, ExtensionType}; use prost::Message; use shared_traits::{IdentId, IdentIdRef}; -use std::cell::RefCell; use std::sync::Arc; use std::time::Duration; use tracing::{info, instrument, warn}; -use crate::inbox_v2::{CIPHER_SUITE, MlsEphemeralPqProvider, MlsIdentityProvider}; -use crypto::{Ed25519Signature, Ed25519SigningKey, Ed25519VerifyingKey}; +use crate::inbox_v2::CIPHER_SUITE; use crate::IdentityProvider; use crate::conversation::{ConversationIdRef, ExternalServices, ServiceContext}; @@ -46,62 +41,6 @@ use crate::{ /// with the openmls (GroupV1) keypackage registered under the bare account id. const DEMLS_KEYPACKAGE_NAMESPACE: &str = "demls"; -/// Owned, `Clone` identity de-mls's `Sig` can hold. A new type only because the -/// account identity (`S::IP`) is neither owned nor `Clone` here, and -/// `crypto::Identity` implements `IdentityProvider` only under `cfg(test)`. -/// Wrapped in [`MlsIdentityProvider`] to reuse its credential + `Signer`. -#[derive(Clone)] -struct DemlsMember { - id: IdentId, - signing: Ed25519SigningKey, - verifying: Ed25519VerifyingKey, -} - -impl DemlsMember { - fn new(name: impl Into) -> Self { - let signing = Ed25519SigningKey::generate(); - Self { - verifying: signing.verifying_key(), - signing, - id: IdentId::new(name.into()), - } - } -} - -impl IdentityProvider for DemlsMember { - fn id(&self) -> IdentIdRef<'_> { - &self.id - } - - fn display_name(&self) -> String { - self.id.as_str().to_string() - } - - fn sign(&self, payload: &[u8]) -> Ed25519Signature { - self.signing.sign(payload) - } - - fn public_key(&self) -> &Ed25519VerifyingKey { - &self.verifying - } -} - -/// The de-mls signer: libchat's `MlsIdentityProvider` over a [`DemlsMember`]. -/// Already a `Signer` + credential source; we also give it de-mls's `MemberId` -/// so the protocol-side identity bytes match the MLS credential's serialized -/// content (`id().as_str().as_bytes()`). -type DemlsSigner = MlsIdentityProvider; - -impl MemberId for DemlsSigner { - fn member_id_bytes(&self) -> &[u8] { - self.id().as_str().as_bytes() - } - - fn member_id_display(&self) -> &str { - self.id().as_str() - } -} - /// Borrows an existing `IdentityProvider` but reports a namespaced `id()`, /// so the same identity can register multiple keypackage "flavors" /// (e.g. openmls vs. de-mls) without colliding in the registry. @@ -116,8 +55,8 @@ impl<'a> NamespacedIdentity<'a> { Self { inner, id } } - fn prefix(id: &IdentId, namesapce: &str) -> String { - format!("{namesapce}|{id}") + fn prefix(id: &IdentId, namespace: &str) -> String { + format!("{namespace}|{id}") } } @@ -139,160 +78,88 @@ impl IdentityProvider for NamespacedIdentity<'_> { } } -/// The de-mls MLS service over libchat's PQ provider. -type DemlsMls = OpenMlsService; - -/// Reference de-mls plug-in factory over libchat's existing PQ provider. Holds -/// a clone of the signer (to mint key packages) and stashes the provider that -/// minted our key package so the matching `welcome_mls` reuses its private keys -/// — replacing the old key-registry namespacing workaround for private keys. -struct DemlsFactory { - signer: DemlsSigner, - pending_provider: RefCell>, +/// Local member id bytes — the account identity the protocol matches on, +/// shared with the MLS credential and the consensus member. +fn member_id(service_ctx: &ServiceContext) -> Vec { + service_ctx.mls_identity.id().as_str().as_bytes().to_vec() } -impl DemlsFactory { - fn new(signer: DemlsSigner) -> Self { - Self { - signer, - pending_provider: RefCell::new(None), - } - } - - /// Mint a single-use key package into a fresh provider, stashing that - /// provider so the matching `welcome_mls` can open the welcome with the key - /// package's private keys. - fn generate_key_package(&self) -> Result { - let provider = MlsEphemeralPqProvider::new().map_err(ChatError::generic)?; - let bundle = KeyPackage::builder() - .build( - CIPHER_SUITE, - &provider, - &self.signer, - self.signer.get_credential(), - ) - .map_err(ChatError::generic)?; - let bytes = bundle - .key_package() - .tls_serialize_detached() - .map_err(ChatError::generic)?; - *self.pending_provider.borrow_mut() = Some(provider); - Ok(KeyPackageBytes::new( - bytes, - self.signer.member_id_bytes().to_vec(), - )) - } +/// `app_id` for outbound packets / echo-dedup — random per conversation. +fn rand_app_id() -> Arc<[u8]> { + Arc::from(rand_string(5).as_bytes()) } -impl ConversationPluginsFactory for DemlsFactory { - type Mls = DemlsMls; - type Scoring = DefaultPeerScoring; - type StewardList = DefaultStewardList; - - fn create_mls( - &self, - conversation_id: String, - key_package: &[u8], - signer: &impl Signer, - ) -> Result { - OpenMlsService::new_as_creator( - conversation_id, - MlsEphemeralPqProvider::new()?, - key_package, - signer, - ) - } - - fn welcome_mls(&self, welcome_bytes: &[u8]) -> Result, MlsError> { - // Each conversation has its own factory and stash, and welcomes are - // routed only to the joiner that minted the key package. A missing - // provider is therefore a logic error here — not a "not for us" case — - // so surface it instead of silently yielding `None`. - let provider = self.pending_provider.borrow_mut().take().ok_or_else(|| { - MlsError::Welcome("no pending key-package provider for this conversation".into()) - })?; - OpenMlsService::new_from_welcome(welcome_bytes, provider) - } - - fn make_scoring(&self, config: &ScoringConfig) -> Self::Scoring { - PeerScoringService::new( - InMemoryPeerScoreStorage::new(), - default_score_deltas(), - config.clone(), - ) - } +/// Peer-scoring plug-in: the library default over in-memory storage. +fn make_scoring() -> DefaultPeerScoring { + PeerScoringService::new( + InMemoryPeerScoreStorage::new(), + default_score_deltas(), + ScoringConfig::default(), + ) +} - fn make_steward_list( - &self, - conversation_id: &[u8], - config: StewardListConfig, - ) -> Self::StewardList { - DeterministicStewardList::empty(conversation_id.to_vec(), config) - } +/// Steward-list plug-in: the library default, seedless — the library stamps the +/// conversation-id sort salt when it builds the conversation. +fn make_steward() -> DefaultStewardList { + DeterministicStewardList::empty(StewardListConfig::default()) } -struct DemlsSetup { - signer: DemlsSigner, - factory: DemlsFactory, - consensus_storage: ::ConsensusStorage, - consensus_signer: EthereumConsensusSigner, - app_id: Vec, // random bytes; echo-dedup key - config: ConversationConfig, // the ms-scale test timers, as before +/// Consensus service: the library default over a fresh in-memory store and a +/// random Ethereum consensus signer. +fn make_consensus() -> ConsensusServiceFor { + ConsensusServiceFor::::new_with_components( + DefaultConsensusPlugin::new_storage(), + DefaultConsensusPlugin::new_event_bus(), + EthereumConsensusSigner::new(PrivateKeySigner::random()), + 10, + ) } -impl DemlsSetup { - fn new(identity_name: String) -> Result { - let signer = MlsIdentityProvider::new(DemlsMember::new(identity_name)); - let factory = DemlsFactory::new(signer.clone()); - // TODO(config): TEST-ONLY millisecond timers. de-mls deadlines are real - // wall-clock, so the default 60s timers never fire under fast virtual - // time. Production needs a real config injected from the caller, not - // these hardcoded values. - let config = ConversationConfig { - commit_inactivity_duration: Duration::from_millis(50), - freeze_duration: Duration::from_millis(20), - voting_delay: Duration::from_millis(30), - election_voting_delay: Duration::from_millis(30), - consensus_timeout: Duration::from_millis(150), - proposal_expiration: Duration::from_millis(2000), - ..ConversationConfig::default() - }; - Ok(DemlsSetup { - signer, - factory, - consensus_storage: DefaultConsensusPlugin::new_storage(), - consensus_signer: EthereumConsensusSigner::new(PrivateKeySigner::random()), - app_id: rand_string(5).as_bytes().to_vec(), - config, - }) +/// TEST-ONLY millisecond timers. de-mls deadlines are real wall-clock, so the +/// default 60s timers never fire under fast virtual time. Production needs a +/// real config injected from the caller, not these hardcoded values. +fn demls_config() -> ConversationConfig { + ConversationConfig { + commit_inactivity_duration: Duration::from_millis(50), + freeze_duration: Duration::from_millis(20), + voting_delay: Duration::from_millis(30), + election_voting_delay: Duration::from_millis(30), + consensus_timeout: Duration::from_millis(150), + proposal_expiration: Duration::from_millis(2000), + ..ConversationConfig::default() } +} - /// Call exactly once per Conversation construction. - fn deps(&self) -> ConversationDeps<'_, DefaultConsensusPlugin, DemlsFactory, DemlsSigner> { - ConversationDeps { - plugins: &self.factory, - consensus: ConsensusServiceFor::::new_with_components( - self.consensus_storage.clone(), - DefaultConsensusPlugin::new_event_bus(), - self.consensus_signer.clone(), - 10, - ), - signer: self.signer.clone(), - identity: &self.signer, - app_id: Arc::from(self.app_id.as_slice()), - config: self.config.clone(), - scoring_config: ScoringConfig::default(), - steward_list_config: StewardListConfig::default(), - } - } +/// Joiner: mint a single-use key package into the user's shared MLS provider +/// (storing its private keys there so the matching welcome opens), and return +/// the serialized public key package. +fn mint_key_package( + service_ctx: &ServiceContext, +) -> Result, ChatError> { + let capabilities = Capabilities::builder() + .ciphersuites(vec![CIPHER_SUITE]) + .extensions(vec![ExtensionType::ApplicationId]) + .build(); + let bundle = KeyPackage::builder() + .leaf_node_capabilities(capabilities) + .build( + CIPHER_SUITE, + &service_ctx.mls_provider, + &service_ctx.mls_identity, + service_ctx.mls_identity.get_credential(), + ) + .map_err(ChatError::generic)?; + bundle + .key_package() + .tls_serialize_detached() + .map_err(ChatError::generic) } pub struct GroupV2Convo { convo_id: String, - setup: DemlsSetup, - conversation: Option>, - /// Member-ids we proposed via add_member. WelcomeReady now fires on - /// every member; we forward a welcome only to joiners WE invited. + conversation: + Option>, + /// Member-ids we proposed via add_member. We forward a welcome only to joiners WE invited. pending_invites: Vec>, } @@ -313,13 +180,23 @@ impl GroupV2Convo { pub fn new( service_ctx: &mut ServiceContext, ) -> Result { - let setup = DemlsSetup::new(service_ctx.mls_identity.display_name())?; let convo_id = rand_string(5); - let key_package = setup.factory.generate_key_package()?; - let conversation = Conversation::create(&convo_id, key_package.as_bytes(), setup.deps())?; + let member = member_id(service_ctx); + let conversation = Conversation::create( + &convo_id, + &service_ctx.mls_provider, + service_ctx.mls_identity.get_credential(), + CIPHER_SUITE, + &service_ctx.mls_identity, + make_scoring(), + make_steward(), + make_consensus(), + rand_app_id(), + demls_config(), + &member, + )?; let convo = GroupV2Convo { convo_id, - setup, conversation: Some(conversation), pending_invites: vec![], }; @@ -335,9 +212,7 @@ impl GroupV2Convo { pub fn new_pending( service_ctx: &mut ServiceContext, ) -> Result { - let name = service_ctx.mls_identity.display_name(); - let setup = DemlsSetup::new(name.clone())?; - let kp = setup.factory.generate_key_package()?; + let kp_bytes = mint_key_package(service_ctx)?; // Namespace the key package so it doesn't collide with the GroupV1 // key package the registry keys under the bare account id. @@ -345,12 +220,11 @@ impl GroupV2Convo { NamespacedIdentity::new(&*service_ctx.mls_identity, DEMLS_KEYPACKAGE_NAMESPACE); service_ctx .registry - .register(&namespaced, kp.as_bytes().to_vec()) + .register(&namespaced, kp_bytes) .map_err(ChatError::generic)?; Ok(GroupV2Convo { convo_id: String::new(), - setup, conversation: None, pending_invites: vec![], }) @@ -366,8 +240,22 @@ impl GroupV2Convo { service_ctx: &mut ServiceContext, welcome: &MemberWelcome, ) -> Result<(), ChatError> { - let conv = Conversation::from_welcome(self.setup.deps(), welcome)? - .ok_or_else(|| ChatError::generic("welcome not addressed to this member"))?; + let member = member_id(service_ctx); + let Some(conv) = Conversation::join( + &service_ctx.mls_provider, + &welcome.welcome_bytes, + &welcome.conversation_sync_bytes, + make_scoring(), + make_steward(), + make_consensus(), + rand_app_id(), + demls_config(), + &member, + &service_ctx.mls_identity, + )? + else { + return Err(ChatError::generic("welcome not addressed to this member")); + }; self.convo_id = conv.id().to_string(); self.conversation = Some(conv); self.init(service_ctx)?; // subscribe @@ -414,7 +302,11 @@ where .conversation .as_mut() .ok_or_else(|| ChatError::generic("conversation not found"))?; - conv.send_message(content.to_vec())?; + conv.send_message( + &service_ctx.mls_provider, + content.to_vec(), + &service_ctx.mls_identity, + )?; self.after_op(service_ctx)?; Ok(()) } @@ -441,8 +333,13 @@ where .conversation .as_mut() .ok_or_else(|| ChatError::generic("no conversation"))?; - conv.process_inbound(&frame.sender_app_id, &inner)?; - conv.poll(); + conv.process_inbound( + &service_ctx.mls_provider, + &frame.sender_app_id, + &inner, + &service_ctx.mls_identity, + )?; + conv.poll(&service_ctx.mls_provider, &service_ctx.mls_identity); let events = self.after_op(service_ctx)?; // route + publish + re-arm, returns events match self.events_to_content(&events) { @@ -460,7 +357,7 @@ where let Some(conv) = self.conversation.as_mut() else { return Ok(()); // pending joiner: no deadlines exist yet }; - let outcome = conv.poll(); + let outcome = conv.poll(&ctx.mls_provider, &ctx.mls_identity); if outcome.leave_requested { // Commit ejected us (or join expired). Real handling - drops // this convo from its map; @@ -485,8 +382,8 @@ where members: &[IdentIdRef], ) -> Result<(), ChatError> { // Record who WE invited before touching the conversation: after_op - // forwards a welcome only to joiners in pending_invites (member-id - // bytes == account name bytes for LocalDemlsMember). + // forwards a welcome only to joiners in pending_invites (the de-mls + // member-id is the invitee's account id bytes). let mut kps = Vec::with_capacity(members.len()); for member in members { let device_id = NamespacedIdentity::prefix(member, DEMLS_KEYPACKAGE_NAMESPACE); @@ -505,7 +402,11 @@ where .as_mut() .ok_or_else(|| ChatError::generic("no conversation"))?; for kp_bytes in &kps { - conv.add_member(kp_bytes)?; + conv.add_member( + &service_ctx.mls_provider, + kp_bytes, + &service_ctx.mls_identity, + )?; } self.after_op(service_ctx)?; Ok(()) diff --git a/core/conversations/src/errors.rs b/core/conversations/src/errors.rs index 879e923..f49a8d6 100644 --- a/core/conversations/src/errors.rs +++ b/core/conversations/src/errors.rs @@ -1,4 +1,4 @@ -use de_mls::{mls_crypto::MlsError, session::ConversationError}; +use de_mls::{ConversationError, mls_crypto::MlsError}; use openmls::{framing::errors::MlsMessageError, prelude::tls_codec}; pub use thiserror::Error; From d773a2a50a1ec542b6de3ea57dea342258e63cc8 Mon Sep 17 00:00:00 2001 From: seemenkina Date: Fri, 19 Jun 2026 13:50:46 +0300 Subject: [PATCH 3/6] update de-mls dep --- Cargo.lock | 2 +- core/conversations/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2145237..ccfad79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1814,7 +1814,7 @@ dependencies = [ [[package]] name = "de-mls" version = "3.0.0" -source = "git+https://github.com/vacp2p/de-mls?branch=refactor%2Fmls-contract-engine-split#4269e2faccd0855840492313c201b76660457827" +source = "git+https://github.com/vacp2p/de-mls?branch=develop#0358d5b85105ab8b62016a1b2db1de26d299e851" dependencies = [ "hashgraph-like-consensus", "indexmap 2.14.0", diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml index f2e75e1..52caa29 100644 --- a/core/conversations/Cargo.toml +++ b/core/conversations/Cargo.toml @@ -18,7 +18,7 @@ storage = { workspace = true } alloy = "2.0" base64 = "0.22" chat-proto = { git = "https://github.com/logos-messaging/chat_proto", rev = "37ec98a151f6d50aab2905802ac0a896477e62ea" } -de-mls = { git = "https://github.com/vacp2p/de-mls", branch = "refactor/mls-contract-engine-split" } +de-mls = { git = "https://github.com/vacp2p/de-mls", branch = "develop" } double-ratchets = { path = "../double-ratchets" } hashgraph-like-consensus = "0.5.1" hex = "0.4.3" From 8e6a0b69433fa2c8ee8c1016d24fefd3d718479d Mon Sep 17 00:00:00 2001 From: seemenkina Date: Fri, 19 Jun 2026 18:10:57 +0300 Subject: [PATCH 4/6] update develop ref commit --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ccfad79..b9b09b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1814,7 +1814,7 @@ dependencies = [ [[package]] name = "de-mls" version = "3.0.0" -source = "git+https://github.com/vacp2p/de-mls?branch=develop#0358d5b85105ab8b62016a1b2db1de26d299e851" +source = "git+https://github.com/vacp2p/de-mls?branch=develop#c8cd6c903726684dc4660a773bfa712c2c8be5a3" dependencies = [ "hashgraph-like-consensus", "indexmap 2.14.0", From 85f49f521cb82957f11458ecff60d37da51670e6 Mon Sep 17 00:00:00 2001 From: Ekaterina Broslavskaya Date: Mon, 22 Jun 2026 09:14:47 +0300 Subject: [PATCH 5/6] Update core/conversations/src/conversation/group_v2.rs Co-authored-by: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> --- core/conversations/src/conversation/group_v2.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/conversations/src/conversation/group_v2.rs b/core/conversations/src/conversation/group_v2.rs index b5e0a4f..c3f5fad 100644 --- a/core/conversations/src/conversation/group_v2.rs +++ b/core/conversations/src/conversation/group_v2.rs @@ -383,7 +383,7 @@ where ) -> Result<(), ChatError> { // Record who WE invited before touching the conversation: after_op // forwards a welcome only to joiners in pending_invites (the de-mls - // member-id is the invitee's account id bytes). + // member-id is the invitee's id bytes). let mut kps = Vec::with_capacity(members.len()); for member in members { let device_id = NamespacedIdentity::prefix(member, DEMLS_KEYPACKAGE_NAMESPACE); From 1cc7dd076a01ecb8077c0cf1311d76a4a64dc444 Mon Sep 17 00:00:00 2001 From: seemenkina Date: Mon, 22 Jun 2026 10:10:49 +0300 Subject: [PATCH 6/6] remove clone --- core/conversations/src/inbox_v2/identity.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/conversations/src/inbox_v2/identity.rs b/core/conversations/src/inbox_v2/identity.rs index 4016da5..779f90b 100644 --- a/core/conversations/src/inbox_v2/identity.rs +++ b/core/conversations/src/inbox_v2/identity.rs @@ -16,7 +16,6 @@ use crate::IdentityProvider; /// This type stops OpenMLS internal from leaking outside of the crate. /// Developers provider a simple IdentitityProvider, and Signer and Credential generation /// is provided -#[derive(Clone)] pub struct MlsIdentityProvider(T); impl MlsIdentityProvider {