// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // Copyright by contributors to this project. // SPDX-License-Identifier: (Apache-2.0 OR MIT) use mls_rs::{ client_builder::MlsConfig, error::MlsError, external_client::{ builder::MlsConfig as ExternalMlsConfig, ExternalClient, ExternalReceivedMessage, ExternalSnapshot, }, group::{CachedProposal, ReceivedMessage}, identity::{ basic::{BasicCredential, BasicIdentityProvider}, SigningIdentity, }, CipherSuite, CipherSuiteProvider, Client, CryptoProvider, ExtensionList, MlsMessage, }; use mls_rs_core::crypto::SignatureSecretKey; const CIPHERSUITE: CipherSuite = CipherSuite::CURVE25519_AES128; fn cipher_suite_provider() -> impl CipherSuiteProvider { crypto_provider() .cipher_suite_provider(CIPHERSUITE) .unwrap() } fn crypto_provider() -> impl CryptoProvider + Clone { mls_rs_crypto_openssl::OpensslCryptoProvider::default() } #[derive(Default)] struct BasicServer { group_state: Vec, cached_proposals: Vec>, message_queue: Vec>, } impl BasicServer { // Client uploads group data after creating the group fn create_group(group_info: &[u8]) -> Result { let server = make_server(); let group_info = MlsMessage::from_bytes(group_info)?; let group = server.observe_group(group_info, None)?; Ok(Self { group_state: group.snapshot().to_bytes()?, ..Default::default() }) } // Client uploads a proposal. This doesn't change the server's group state, so clients can // upload prposals without synchronization (`cached_proposals` and `message_queue` collect // all proposals in any order). fn upload_proposal(&mut self, proposal: Vec) -> Result<(), MlsError> { let server = make_server(); let group_state = ExternalSnapshot::from_bytes(&self.group_state)?; let mut group = server.load_group(group_state)?; let proposal_msg = MlsMessage::from_bytes(&proposal)?; let res = group.process_incoming_message(proposal_msg)?; let ExternalReceivedMessage::Proposal(proposal_desc) = res else { panic!("expected proposal message!") }; self.cached_proposals .push(proposal_desc.cached_proposal().to_bytes()?); self.message_queue.push(proposal); Ok(()) } // Client uploads a commit. This changes the server's group state, so in a real application, // it must be synchronized. That is, only one `upload_commit` operation can succeed. fn upload_commit(&mut self, commit: Vec) -> Result<(), MlsError> { let server = make_server(); let group_state = ExternalSnapshot::from_bytes(&self.group_state)?; let mut group = server.load_group(group_state)?; for p in &self.cached_proposals { group.insert_proposal(CachedProposal::from_bytes(p)?); } let commit_msg = MlsMessage::from_bytes(&commit)?; let res = group.process_incoming_message(commit_msg)?; let ExternalReceivedMessage::Commit(_commit_desc) = res else { panic!("expected commit message!") }; self.cached_proposals = Vec::new(); self.group_state = group.snapshot().to_bytes()?; self.message_queue.push(commit); Ok(()) } pub fn download_messages(&self, i: usize) -> &[Vec] { &self.message_queue[i..] } } fn make_server() -> ExternalClient { ExternalClient::builder() .identity_provider(BasicIdentityProvider) .crypto_provider(crypto_provider()) .build() } fn make_client(name: &str) -> Result, MlsError> { let (secret, signing_identity) = make_identity(name); Ok(Client::builder() .identity_provider(BasicIdentityProvider) .crypto_provider(crypto_provider()) .signing_identity(signing_identity, secret, CIPHERSUITE) .build()) } fn make_identity(name: &str) -> (SignatureSecretKey, SigningIdentity) { let cipher_suite = cipher_suite_provider(); let (secret, public) = cipher_suite.signature_key_generate().unwrap(); // Create a basic credential for the session. // NOTE: BasicCredential is for demonstration purposes and not recommended for production. // X.509 credentials are recommended. let basic_identity = BasicCredential::new(name.as_bytes().to_vec()); let identity = SigningIdentity::new(basic_identity.into_credential(), public); (secret, identity) } fn main() -> Result<(), MlsError> { // Create clients for Alice and Bob let alice = make_client("alice")?; let bob = make_client("bob")?; // Alice creates a group with bob let mut alice_group = alice.create_group(ExtensionList::default())?; let bob_key_package = bob.generate_key_package_message()?; let welcome = &alice_group .commit_builder() .add_member(bob_key_package)? .build()? .welcome_messages[0]; let (mut bob_group, _) = bob.join_group(None, welcome)?; alice_group.apply_pending_commit()?; // Server starts observing Alice's group let group_info = alice_group.group_info_message(true)?.to_bytes()?; let mut server = BasicServer::create_group(&group_info)?; // Bob uploads a proposal let proposal = bob_group .propose_group_context_extensions(ExtensionList::new(), Vec::new())? .to_bytes()?; server.upload_proposal(proposal)?; // Alice downloads all messages and commits for m in server.download_messages(0) { alice_group.process_incoming_message(MlsMessage::from_bytes(m)?)?; } let commit = alice_group .commit(b"changing extensions".to_vec())? .commit_message .to_bytes()?; server.upload_commit(commit)?; // Alice waits for an ACK from the server and applies the commit alice_group.apply_pending_commit()?; // Bob downloads the commit let message = server.download_messages(1).first().unwrap(); let res = bob_group.process_incoming_message(MlsMessage::from_bytes(message)?)?; let ReceivedMessage::Commit(commit_desc) = res else { panic!("expected commit message") }; assert_eq!(&commit_desc.authenticated_data, b"changing extensions"); Ok(()) }