remove outdated factories and inline providers from de-mls

This commit is contained in:
Ekaterina Broslavskaya 2026-06-23 18:08:25 +03:00 committed by GitHub
parent aec902d796
commit e1921b944d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 155 additions and 169 deletions

47
Cargo.lock generated
View File

@ -1813,14 +1813,12 @@ dependencies = [
[[package]] [[package]]
name = "de-mls" name = "de-mls"
version = "3.0.0" version = "4.0.0"
source = "git+https://github.com/vacp2p/de-mls?branch=develop#2dfcd8c71668856e8d7027968c2c64d87ec7fea7" source = "git+https://github.com/vacp2p/de-mls?tag=v4.0.0#425203056d4586115beef6559b49560095a15256"
dependencies = [ dependencies = [
"hashgraph-like-consensus", "hashgraph-like-consensus",
"indexmap 2.14.0", "indexmap 2.14.0",
"openmls", "openmls",
"openmls_basic_credential",
"openmls_rust_crypto 0.5.1",
"openmls_traits 0.5.0", "openmls_traits 0.5.0",
"prost", "prost",
"prost-build", "prost-build",
@ -3898,20 +3896,6 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "openmls_libcrux_crypto" name = "openmls_libcrux_crypto"
version = "0.2.4" version = "0.2.4"
@ -4007,31 +3991,6 @@ dependencies = [
"tls_codec", "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]] [[package]]
name = "openmls_test" name = "openmls_test"
version = "0.2.1" version = "0.2.1"
@ -4040,7 +3999,7 @@ checksum = "46c5984361586c8ef56108664ffec909fa78126be8eef1983723f0aed80a9266"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"openmls_libcrux_crypto 0.2.4", "openmls_libcrux_crypto 0.2.4",
"openmls_rust_crypto 0.4.4", "openmls_rust_crypto",
"openmls_traits 0.4.1", "openmls_traits 0.4.1",
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -18,7 +18,7 @@ storage = { workspace = true }
alloy = "2.0" alloy = "2.0"
base64 = "0.22" base64 = "0.22"
chat-proto = { git = "https://github.com/logos-messaging/chat_proto", rev = "37ec98a151f6d50aab2905802ac0a896477e62ea" } 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", tag = "v4.0.0" }
double-ratchets = { path = "../double-ratchets" } double-ratchets = { path = "../double-ratchets" }
hashgraph-like-consensus = "0.5.1" hashgraph-like-consensus = "0.5.1"
hex = "0.4.3" hex = "0.4.3"

View File

@ -7,26 +7,29 @@ use crate::{Content, WakeupService};
use alloy::signers::local::PrivateKeySigner; use alloy::signers::local::PrivateKeySigner;
use blake2::{Blake2b, Digest, digest::consts::U6}; use blake2::{Blake2b, Digest, digest::consts::U6};
use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload}; use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload};
use de_mls::core::{
ConsensusPlugin, ConsensusServiceFor, ConversationEvent, ConversationPluginsFactory,
ScoringConfig, StewardListConfig,
};
use de_mls::defaults::{ 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::protos::de_mls::messages::v1::{ use de_mls::protos::de_mls::messages::v1::{
AppMessage as AppMessageProto, MemberWelcome, app_message, 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 hashgraph_like_consensus::signing::EthereumConsensusSigner;
use openmls::key_packages::KeyPackage;
use openmls::prelude::tls_codec::Serialize as _;
use openmls::prelude::{Capabilities, ExtensionType};
use prost::Message; use prost::Message;
use shared_traits::{IdentId, IdentIdRef}; use shared_traits::{IdentId, IdentIdRef};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tracing::{info, instrument, warn}; use tracing::{info, instrument, warn};
use crate::inbox_v2::CIPHER_SUITE;
use crate::IdentityProvider; use crate::IdentityProvider;
use crate::conversation::{ConversationIdRef, ExternalServices, ServiceContext}; use crate::conversation::{ConversationIdRef, ExternalServices, ServiceContext};
use crate::{ use crate::{
@ -38,28 +41,6 @@ use crate::{
/// with the openmls (GroupV1) keypackage registered under the bare account id. /// with the openmls (GroupV1) keypackage registered under the bare account id.
const DEMLS_KEYPACKAGE_NAMESPACE: &str = "demls"; 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,
}
impl LocalDemlsMember {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
}
impl MemberId for LocalDemlsMember {
fn member_id_bytes(&self) -> &[u8] {
self.name.as_bytes()
}
fn member_id_display(&self) -> &str {
&self.name
}
}
/// Borrows an existing `IdentityProvider` but reports a namespaced `id()`, /// Borrows an existing `IdentityProvider` but reports a namespaced `id()`,
/// so the same identity can register multiple keypackage "flavors" /// so the same identity can register multiple keypackage "flavors"
/// (e.g. openmls vs. de-mls) without colliding in the registry. /// (e.g. openmls vs. de-mls) without colliding in the registry.
@ -74,8 +55,8 @@ impl<'a> NamespacedIdentity<'a> {
Self { inner, id } Self { inner, id }
} }
fn prefix(id: &IdentId, namesapce: &str) -> String { fn prefix(id: &IdentId, namespace: &str) -> String {
format!("{namesapce}|{id}") format!("{namespace}|{id}")
} }
} }
@ -97,73 +78,88 @@ impl IdentityProvider for NamespacedIdentity<'_> {
} }
} }
struct DemlsSetup { /// Local member id bytes — the account identity the protocol matches on,
member: LocalDemlsMember, /// shared with the MLS credential and the consensus member.
factory: DefaultConversationPluginsFactory, fn member_id<S: ExternalServices>(service_ctx: &ServiceContext<S>) -> Vec<u8> {
consensus_storage: <DefaultConsensusPlugin as ConsensusPlugin>::ConsensusStorage, service_ctx.mls_identity.id().as_str().as_bytes().to_vec()
consensus_signer: EthereumConsensusSigner,
app_id: Vec<u8>, // random bytes; echo-dedup key
config: ConversationConfig, // the ms-scale test timers, as before
} }
impl DemlsSetup { /// `app_id` for outbound packets / echo-dedup — random per conversation.
fn new(identity_name: String) -> Result<Self, ChatError> { fn rand_app_id() -> Arc<[u8]> {
let member = LocalDemlsMember::new(identity_name); Arc::from(rand_string(5).as_bytes())
let credentials = Arc::new(MlsCredentials::from_member_id(&member)?); }
let factory = DefaultConversationPluginsFactory::new(
Arc::new(MemoryDeMlsStorage::new()),
credentials,
);
// 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 {
member,
factory,
consensus_storage: DefaultConsensusPlugin::new_storage(),
consensus_signer: EthereumConsensusSigner::new(PrivateKeySigner::random()),
app_id: rand_string(5).as_bytes().to_vec(),
config,
})
}
/// Call exactly once per Conversation construction. /// Peer-scoring plug-in: the library default over in-memory storage.
fn deps( fn make_scoring() -> DefaultPeerScoring {
&self, PeerScoringService::new(
) -> ConversationDeps<'_, DefaultConsensusPlugin, DefaultConversationPluginsFactory> { InMemoryPeerScoreStorage::new(),
ConversationDeps { default_score_deltas(),
plugins: &self.factory, ScoringConfig::default(),
consensus: ConsensusServiceFor::<DefaultConsensusPlugin>::new_with_components( )
self.consensus_storage.clone(), }
DefaultConsensusPlugin::new_event_bus(),
self.consensus_signer.clone(), /// Steward-list plug-in: the library default, seedless — the library stamps the
10, /// conversation-id sort salt when it builds the conversation.
), fn make_steward() -> DefaultStewardList {
identity: &self.member, DeterministicStewardList::empty(StewardListConfig::default())
app_id: Arc::from(self.app_id.as_slice()), }
config: self.config.clone(),
scoring_config: ScoringConfig::default(), /// Consensus service: the library default over a fresh in-memory store and a
steward_list_config: StewardListConfig::default(), /// random Ethereum consensus signer.
} fn make_consensus() -> ConsensusServiceFor<DefaultConsensusPlugin> {
ConsensusServiceFor::<DefaultConsensusPlugin>::new_with_components(
DefaultConsensusPlugin::new_storage(),
DefaultConsensusPlugin::new_event_bus(),
EthereumConsensusSigner::new(PrivateKeySigner::random()),
10,
)
}
/// 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()
} }
} }
/// 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<S: ExternalServices>(
service_ctx: &ServiceContext<S>,
) -> Result<Vec<u8>, 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 { pub struct GroupV2Convo {
convo_id: String, convo_id: String,
setup: DemlsSetup, conversation:
conversation: Option<Conversation<DefaultConsensusPlugin, DefaultConversationPluginsFactory>>, Option<Conversation<DefaultConsensusPlugin, DefaultPeerScoring, DefaultStewardList>>,
/// Member-ids we proposed via add_member. WelcomeReady now fires on /// Member-ids we proposed via add_member. We forward a welcome only to joiners WE invited.
/// every member; we forward a welcome only to joiners WE invited.
pending_invites: Vec<Vec<u8>>, pending_invites: Vec<Vec<u8>>,
} }
@ -184,12 +180,23 @@ impl GroupV2Convo {
pub fn new<S: ExternalServices>( pub fn new<S: ExternalServices>(
service_ctx: &mut ServiceContext<S>, service_ctx: &mut ServiceContext<S>,
) -> Result<Self, ChatError> { ) -> Result<Self, ChatError> {
let setup = DemlsSetup::new(service_ctx.mls_identity.id().as_str().to_string())?;
let convo_id = rand_string(5); let convo_id = rand_string(5);
let conversation = Conversation::create(&convo_id, 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 { let convo = GroupV2Convo {
convo_id, convo_id,
setup,
conversation: Some(conversation), conversation: Some(conversation),
pending_invites: vec![], pending_invites: vec![],
}; };
@ -205,24 +212,19 @@ impl GroupV2Convo {
pub fn new_pending<S: ExternalServices>( pub fn new_pending<S: ExternalServices>(
service_ctx: &mut ServiceContext<S>, service_ctx: &mut ServiceContext<S>,
) -> Result<Self, ChatError> { ) -> Result<Self, ChatError> {
let name = service_ctx.mls_identity.id().as_str().to_string(); let kp_bytes = mint_key_package(service_ctx)?;
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. // Namespace the key package so it doesn't collide with the GroupV1
// The key registry does not support a method to namespace keys with the same identity. When the key is pulled down it cannot // key package the registry keys under the bare account id.
// 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.
let namespaced = let namespaced =
NamespacedIdentity::new(&*service_ctx.mls_identity, DEMLS_KEYPACKAGE_NAMESPACE); NamespacedIdentity::new(&*service_ctx.mls_identity, DEMLS_KEYPACKAGE_NAMESPACE);
service_ctx service_ctx
.registry .registry
.register(&namespaced, kp.as_bytes().to_vec()) .register(&namespaced, kp_bytes)
.map_err(ChatError::generic)?; .map_err(ChatError::generic)?;
Ok(GroupV2Convo { Ok(GroupV2Convo {
convo_id: String::new(), convo_id: String::new(),
setup,
conversation: None, conversation: None,
pending_invites: vec![], pending_invites: vec![],
}) })
@ -238,8 +240,22 @@ impl GroupV2Convo {
service_ctx: &mut ServiceContext<S>, service_ctx: &mut ServiceContext<S>,
welcome: &MemberWelcome, welcome: &MemberWelcome,
) -> Result<(), ChatError> { ) -> Result<(), ChatError> {
let conv = Conversation::from_welcome(self.setup.deps(), welcome)? let member = member_id(service_ctx);
.ok_or_else(|| ChatError::generic("welcome not addressed to this member"))?; 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.convo_id = conv.id().to_string();
self.conversation = Some(conv); self.conversation = Some(conv);
self.init(service_ctx)?; // subscribe self.init(service_ctx)?; // subscribe
@ -292,7 +308,11 @@ where
.conversation .conversation
.as_mut() .as_mut()
.ok_or_else(|| ChatError::generic("conversation not found"))?; .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)?; self.after_op(service_ctx)?;
Ok(()) Ok(())
} }
@ -319,8 +339,13 @@ where
.conversation .conversation
.as_mut() .as_mut()
.ok_or_else(|| ChatError::generic("no conversation"))?; .ok_or_else(|| ChatError::generic("no conversation"))?;
conv.process_inbound(&frame.sender_app_id, &inner)?; conv.process_inbound(
conv.poll(); &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 let events = self.after_op(service_ctx)?; // route + publish + re-arm, returns events
match self.events_to_content(&events) { match self.events_to_content(&events) {
@ -338,7 +363,7 @@ where
let Some(conv) = self.conversation.as_mut() else { let Some(conv) = self.conversation.as_mut() else {
return Ok(()); // pending joiner: no deadlines exist yet 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 { if outcome.leave_requested {
// Commit ejected us (or join expired). Real handling - drops // Commit ejected us (or join expired). Real handling - drops
// this convo from its map; // this convo from its map;
@ -360,8 +385,8 @@ where
members: &[IdentIdRef], members: &[IdentIdRef],
) -> Result<(), ChatError> { ) -> Result<(), ChatError> {
// Record who WE invited before touching the conversation: after_op // Record who WE invited before touching the conversation: after_op
// forwards a welcome only to joiners in pending_invites (member-id // forwards a welcome only to joiners in pending_invites (the de-mls
// bytes == account name bytes for LocalDemlsMember). // member-id is the invitee's id bytes).
let mut kps = Vec::with_capacity(members.len()); let mut kps = Vec::with_capacity(members.len());
for member in members { for member in members {
let device_id = NamespacedIdentity::prefix(member, DEMLS_KEYPACKAGE_NAMESPACE); let device_id = NamespacedIdentity::prefix(member, DEMLS_KEYPACKAGE_NAMESPACE);
@ -380,7 +405,11 @@ where
.as_mut() .as_mut()
.ok_or_else(|| ChatError::generic("no conversation"))?; .ok_or_else(|| ChatError::generic("no conversation"))?;
for kp_bytes in &kps { 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)?; self.after_op(service_ctx)?;
Ok(()) Ok(())
@ -458,16 +487,13 @@ impl GroupV2Convo {
events.iter().find_map(|evt| match evt { events.iter().find_map(|evt| match evt {
ConversationEvent::AppMessage(AppMessageProto { ConversationEvent::AppMessage(AppMessageProto {
payload: Some(app_message::Payload::ConversationMessage(cm)), payload: Some(app_message::Payload::ConversationMessage(cm)),
}) => { }) => Some(ConvoOutcome {
let cred = cm.sender.as_bytes().to_vec(); convo_id: self.convo_id.clone(),
Some(ConvoOutcome { content: Some(Content {
convo_id: self.convo_id.clone(), bytes: cm.message.clone(),
content: Some(Content { encoded_credential: cm.sender.clone(),
bytes: cm.message.clone(), }),
encoded_credential: cred, }),
}),
})
}
_ => None, _ => None,
}) })
} }

View File

@ -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}; use openmls::{framing::errors::MlsMessageError, prelude::tls_codec};
pub use thiserror::Error; pub use thiserror::Error;

View File

@ -31,7 +31,8 @@ use crate::{
use crate::{IdentId, IdentIdRef, IdentityProvider}; use crate::{IdentId, IdentIdRef, IdentityProvider};
// Downgraded from MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519 until demls accepts an external provider // 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 // Define unique Identifiers derivations used in InboxV2
fn delivery_address_for(ident_id: IdentIdRef) -> String { fn delivery_address_for(ident_id: IdentIdRef) -> String {