From c13dc5888845d8b59acd737f09bb64887078a841 Mon Sep 17 00:00:00 2001 From: Marta Mularczyk Date: Mon, 6 Oct 2025 14:12:55 +0200 Subject: [PATCH 1/2] chore: Add insert method to PSK storage trait --- mls-rs-core/src/psk.rs | 3 +++ mls-rs-provider-sqlite/src/psk.rs | 9 +++++++++ mls-rs/src/group/proposal_cache.rs | 4 ++++ mls-rs/src/psk.rs | 4 ++++ mls-rs/src/storage_provider/in_memory/psk_storage.rs | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/mls-rs-core/src/psk.rs b/mls-rs-core/src/psk.rs index 348fa89e4..7daa5f73e 100644 --- a/mls-rs-core/src/psk.rs +++ b/mls-rs-core/src/psk.rs @@ -132,4 +132,7 @@ pub trait PreSharedKeyStorage: Send + Sync { async fn contains(&self, id: &ExternalPskId) -> Result { self.get(id).await.map(|key| key.is_some()) } + + async fn insert(&mut self, psk_id: ExternalPskId, psk: PreSharedKey) + -> Result<(), Self::Error>; } diff --git a/mls-rs-provider-sqlite/src/psk.rs b/mls-rs-provider-sqlite/src/psk.rs index 47ab948c5..12e86c6ce 100644 --- a/mls-rs-provider-sqlite/src/psk.rs +++ b/mls-rs-provider-sqlite/src/psk.rs @@ -71,6 +71,15 @@ impl PreSharedKeyStorage for SqLitePreSharedKeyStorage { self.get(id) .map_err(|e| SqLiteDataStorageError::DataConversionError(e.into())) } + + async fn insert( + &mut self, + psk_id: ExternalPskId, + psk: PreSharedKey, + ) -> Result<(), Self::Error> { + SqLitePreSharedKeyStorage::insert(self, psk_id.as_ref(), &psk) + .map_err(|e| SqLiteDataStorageError::DataConversionError(e.into())) + } } #[cfg(test)] diff --git a/mls-rs/src/group/proposal_cache.rs b/mls-rs/src/group/proposal_cache.rs index f95f2e402..7946ef479 100644 --- a/mls-rs/src/group/proposal_cache.rs +++ b/mls-rs/src/group/proposal_cache.rs @@ -3822,6 +3822,10 @@ mod tests { async fn get(&self, _: &ExternalPskId) -> Result, Self::Error> { Ok(None) } + + async fn insert(&mut self, _: ExternalPskId, _: PreSharedKey) -> Result<(), Self::Error> { + Ok(()) + } } #[cfg(feature = "psk")] diff --git a/mls-rs/src/psk.rs b/mls-rs/src/psk.rs index 79c003529..d08bc0853 100644 --- a/mls-rs/src/psk.rs +++ b/mls-rs/src/psk.rs @@ -144,6 +144,10 @@ impl PreSharedKeyStorage for AlwaysFoundPskStorage { async fn get(&self, _: &ExternalPskId) -> Result, Self::Error> { Ok(Some(vec![].into())) } + + async fn insert(&mut self, _: ExternalPskId, _: PreSharedKey) -> Result<(), Self::Error> { + Ok(()) + } } #[cfg(feature = "psk")] diff --git a/mls-rs/src/storage_provider/in_memory/psk_storage.rs b/mls-rs/src/storage_provider/in_memory/psk_storage.rs index 4ebad2994..29e77dc67 100644 --- a/mls-rs/src/storage_provider/in_memory/psk_storage.rs +++ b/mls-rs/src/storage_provider/in_memory/psk_storage.rs @@ -73,4 +73,9 @@ impl PreSharedKeyStorage for InMemoryPreSharedKeyStorage { async fn get(&self, id: &ExternalPskId) -> Result, Self::Error> { Ok(self.get(id)) } + + async fn insert(&mut self, id: ExternalPskId, psk: PreSharedKey) -> Result<(), Self::Error> { + self.insert(id, psk); + Ok(()) + } } From fa5c3e529ee1370fb275233f169e3a60f2a96b78 Mon Sep 17 00:00:00 2001 From: Marta Mularczyk Date: Fri, 10 Oct 2025 20:52:29 +0200 Subject: [PATCH 2/2] feat: Add a more complete basic example --- mls-rs/Cargo.toml | 3 + mls-rs/examples/complete_basic_usage.rs | 155 ++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 mls-rs/examples/complete_basic_usage.rs diff --git a/mls-rs/Cargo.toml b/mls-rs/Cargo.toml index be9d948c4..7c0602ad4 100644 --- a/mls-rs/Cargo.toml +++ b/mls-rs/Cargo.toml @@ -141,6 +141,9 @@ required-features = ["std"] name = "basic_server_usage" required-features = ["external_client"] +[[example]] +name = "complete_basic_usage" + [[bench]] name = "group_add" harness = false diff --git a/mls-rs/examples/complete_basic_usage.rs b/mls-rs/examples/complete_basic_usage.rs new file mode 100644 index 000000000..1f838b2ce --- /dev/null +++ b/mls-rs/examples/complete_basic_usage.rs @@ -0,0 +1,155 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! 🔐 Cryptographic Security Showcase +//! +//! This example demonstrates advanced MLS cryptographic properties: +//! - Forward Secrecy & Post-Compromise Security +//! - Key Rotation and Epoch Evolution +//! - Compromise Recovery Scenarios + +use assert_matches::assert_matches; +use mls_rs::{ + client_builder::{MlsConfig, PaddingMode}, + crypto::SignatureSecretKey, + error::MlsError, + group::ReceivedMessage, + identity::{ + basic::{BasicCredential, BasicIdentityProvider}, + SigningIdentity, + }, + mls_rules::{CommitOptions, DefaultMlsRules, EncryptionOptions}, + psk::{ExternalPskId, PreSharedKey}, + CipherSuite, CipherSuiteProvider, Client, CryptoProvider, PreSharedKeyStorage, +}; + +const CIPHERSUITE: CipherSuite = CipherSuite::CURVE25519_AES128; + +fn create_identity(name: &str) -> (SigningIdentity, SignatureSecretKey) { + let crypto_provider = mls_rs_crypto_openssl::OpensslCryptoProvider::default(); + let cs_provider = crypto_provider.cipher_suite_provider(CIPHERSUITE).unwrap(); + let (secret, public) = cs_provider.signature_key_generate().unwrap(); + let credential = BasicCredential::new(name.as_bytes().to_vec()); + let signing_identity = SigningIdentity::new(credential.into_credential(), public); + (signing_identity, secret) +} + +fn create_client(name: &str) -> Client { + let (signing_identity, secret_key) = create_identity(name); + + Client::builder() + .crypto_provider(mls_rs_crypto_openssl::OpensslCryptoProvider::default()) + .identity_provider(BasicIdentityProvider) + .mls_rules( + DefaultMlsRules::new() + .with_encryption_options(EncryptionOptions::new(true, PaddingMode::StepFunction)) + .with_commit_options(CommitOptions::new().with_path_required(true)), + ) + .signing_identity(signing_identity, secret_key, CIPHERSUITE) + .build() +} + +fn main() -> Result<(), MlsError> { + // 🔐 Create three MLS clients with unique identities + let ann = create_client("📡"); + let carl = create_client("🚗"); + let lara = create_client("💻"); + + let lara_kp = + lara.generate_key_package_message(Default::default(), Default::default(), None)?; + + let ann_kp = ann.generate_key_package_message(Default::default(), Default::default(), None)?; + + // 🏗️ Carl 🚗 creates initial group (Epoch 0) + let mut carl_group = carl.create_group(Default::default(), Default::default(), None)?; + + // ➕ Group evolution: Carl 🚗 adds Lara 💻 and Ann 📡 (Epoch 0 → 1) + let commit_output = carl_group + .commit_builder() + .add_member(lara_kp)? + .add_member(ann_kp)? + .build()?; + + carl_group.apply_pending_commit()?; + let welcome = &commit_output.welcome_messages[0]; + + // 🤝 New members join using Welcome messages + let (mut lara_group, _info) = lara.join_group(None, welcome, None)?; + let (ann_group, _info) = ann.join_group(None, welcome, None)?; + + // 🔑 Demonstrate shared cryptographic state across all group members + // All clients derive identical secrets from the same group key material + let carl_secret = carl_group.export_secret(b"HKDF label", b"HKDF info", 32)?; + let lara_secret = lara_group.export_secret(b"HKDF label", b"HKDF info", 32)?; + let ann_secret = ann_group.export_secret(b"HKDF label", b"HKDF info", 32)?; + assert_eq!(carl_secret, lara_secret); + assert_eq!(carl_secret, ann_secret); + + // 💬 End-to-end encrypted messaging with authentication + let msg = + carl_group.encrypt_application_message(b"Hello, world!", b"authenticated data".into())?; + + // 🔓 Lara decrypts and verifies the message + let lara_msg = lara_group.process_incoming_message(msg)?; + + match lara_msg { + ReceivedMessage::ApplicationMessage(msg) => { + assert_eq!(msg.data(), b"Hello, world!"); + assert_eq!(msg.authenticated_data, b"authenticated data"); + assert_eq!(msg.sender_index, 0); // Carl is sender (index 0) + } + _ => { + panic!("Expected application message"); + } + }; + + // ➖ Group evolution: Carl removes Ann + let commit_output = carl_group.commit_builder().remove_member(2)?.build()?; + carl_group.apply_pending_commit()?; + lara_group.process_incoming_message(commit_output.commit_message)?; + assert_eq!(lara_group.roster().members().len(), 2); // Now only Carl + Lara + + // 🔄 Key rotation: Carl proposes key update for forward secrecy + let proposal = carl_group.propose_update(vec![])?; + lara_group.process_incoming_message(proposal)?; + + // Lara commits the key update proposal + let commit_output = lara_group.commit(vec![])?; + lara_group.apply_pending_commit()?; + carl_group.process_incoming_message(commit_output.commit_message)?; + + // 🔐 Pre-Shared Key (PSK) injection for enhanced security + let psk_id = ExternalPskId::new(b"PSK ID".to_vec()); + let psk = PreSharedKey::new(b"shared secret value".to_vec()); + + // 💾 Both clients store the PSK in their secret stores + lara.secret_store() + .insert(psk_id.clone(), psk.clone()) + .unwrap(); + + carl.secret_store() + .insert(psk_id.clone(), psk.clone()) + .unwrap(); + + // 🔄 Lara commits the PSK to the group + let commit_output = lara_group + .commit_builder() + .add_external_psk(psk_id)? + .build()?; + + // 📨 Carl processes the PSK commit message + carl_group.process_incoming_message(commit_output.commit_message)?; + + // 🧟‍♀️ Zoe joins via external commit (no invitation needed!) + let zoe = create_client("🧟‍♀️"); + let group_info = carl_group.group_info_message_allowing_ext_commit(true)?; + let (mut zoe_group, external_commit) = zoe.commit_external(group_info)?; + carl_group.process_incoming_message(external_commit)?; + + let msg = zoe_group.encrypt_application_message(b"hello", vec![])?; + let carl_msg = carl_group.process_incoming_message(msg)?; + assert_matches!(carl_msg, ReceivedMessage::ApplicationMessage(text) if text.data() == b"hello"); + + Ok(()) +}