diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml index 3ab21bb1f..a07aaeb19 100644 --- a/.github/workflows/interop.yml +++ b/.github/workflows/interop.yml @@ -84,6 +84,8 @@ jobs: make run-go || echo "Build despite errors." cd test-runner # TODO(#1366) + go get -u google.golang.org/grpc + go mod tidy -e patch main.go main.go.patch go build diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c68edd5b..f939c6bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1506](https://github.com/openmls/openmls/pull/1506): Add `StagedWelcome` and `StagedCoreWelcome` to make joining a group staged in order to inspect the `Welcome` message. This was followed up with PR [#1533](https://github.com/openmls/openmls/pull/1533) to adjust the API. - [#1516](https://github.com/openmls/openmls/pull/1516): Add `MlsGroup::clear_pending_proposals` to the public API; this allows users to clear a group's internal `ProposalStore` +- [#1565](https://github.com/openmls/openmls/pull/1565): Add new `StorageProvider` trait to the `openmls_traits` crate. ### Changed @@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1548](https://github.com/openmls/openmls/pull/1548): CryptoConfig is now replaced by just Ciphersuite. - [#1542](https://github.com/openmls/openmls/pull/1542): Add support for custom proposals. ProposalType::Unknown is now called ProposalType::Other. Proposal::Unknown is now called Proposal::Other. - [#1559](https://github.com/openmls/openmls/pull/1559): Remove the `PartialEq` type constraint on the error type of both the `OpenMlsRand` and `OpenMlsKeyStore` traits. Additionally, remove the `Clone` type constraint on the error type of the `OpenMlsRand` trait. +- [#1565](https://github.com/openmls/openmls/pull/1565): Removed `OpenMlsKeyStore` and replace it with a new `StorageProvider` trait in the `openmls_traits` crate. ### Fixed diff --git a/openmls/benches/benchmark.rs b/openmls/benches/benchmark.rs index 0d11c40bd..1516f846f 100644 --- a/openmls/benches/benchmark.rs +++ b/openmls/benches/benchmark.rs @@ -6,11 +6,10 @@ extern crate rand; use criterion::Criterion; use openmls::prelude::*; use openmls_basic_credential::SignatureKeyPair; +use openmls_rust_crypto::OpenMlsRustCrypto; use openmls_traits::{crypto::OpenMlsCrypto, OpenMlsProvider}; -pub type OpenMlsRustCrypto = openmls_rust_crypto::OpenMlsRustCrypto; - -fn criterion_kp_bundle(c: &mut Criterion, provider: &impl OpenMlsProvider) { +fn criterion_key_package(c: &mut Criterion, provider: &impl OpenMlsProvider) { for &ciphersuite in provider.crypto().supported_ciphersuites().iter() { c.bench_function( &format!("KeyPackage create bundle with ciphersuite: {ciphersuite:?}"), @@ -38,14 +37,249 @@ fn criterion_kp_bundle(c: &mut Criterion, provider: &impl OpenMlsProvider) { } } +fn create_welcome(c: &mut Criterion, provider: &impl OpenMlsProvider) { + for &ciphersuite in provider.crypto().supported_ciphersuites().iter() { + c.bench_function( + &format!("Create a welcome message with ciphersuite: {ciphersuite:?}"), + move |b| { + b.iter_with_setup( + || { + let alice_credential = BasicCredential::new("Alice".into()); + let alice_signer = + SignatureKeyPair::new(ciphersuite.signature_algorithm()).unwrap(); + let alice_credential_with_key = CredentialWithKey { + credential: alice_credential.into(), + signature_key: alice_signer.to_public_vec().into(), + }; + + let bob_credential = BasicCredential::new("Bob".into()); + let bob_signer = + SignatureKeyPair::new(ciphersuite.signature_algorithm()).unwrap(); + let bob_credential_with_key = CredentialWithKey { + credential: bob_credential.into(), + signature_key: bob_signer.to_public_vec().into(), + }; + let bob_key_package = KeyPackage::builder() + .build( + ciphersuite, + provider, + &bob_signer, + bob_credential_with_key.clone(), + ) + .expect("An unexpected error occurred."); + + let mls_group_create_config = MlsGroupCreateConfig::builder() + .wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY) + .ciphersuite(ciphersuite) + .build(); + + // === Alice creates a group === + let alice_group = MlsGroup::new( + provider, + &alice_signer, + &mls_group_create_config, + alice_credential_with_key.clone(), + ) + .expect("An unexpected error occurred."); + + (alice_signer, alice_group, bob_key_package) + }, + |(alice_signer, mut alice_group, bob_key_package)| { + let _welcome = match alice_group.add_members( + provider, + &alice_signer, + &[bob_key_package.key_package().clone()], + ) { + Ok((_, welcome, _)) => welcome, + Err(e) => panic!("Could not add member to group: {e:?}"), + }; + }, + ); + }, + ); + } +} + +fn join_group(c: &mut Criterion, provider: &impl OpenMlsProvider) { + for &ciphersuite in provider.crypto().supported_ciphersuites().iter() { + c.bench_function( + &format!("Join a group with ciphersuite: {ciphersuite:?}"), + move |b| { + b.iter_with_setup( + || { + let alice_credential = BasicCredential::new("Alice".into()); + let alice_signer = + SignatureKeyPair::new(ciphersuite.signature_algorithm()).unwrap(); + let alice_credential_with_key = CredentialWithKey { + credential: alice_credential.into(), + signature_key: alice_signer.to_public_vec().into(), + }; + + let bob_credential = BasicCredential::new("Bob".into()); + let bob_signer = + SignatureKeyPair::new(ciphersuite.signature_algorithm()).unwrap(); + let bob_credential_with_key = CredentialWithKey { + credential: bob_credential.into(), + signature_key: bob_signer.to_public_vec().into(), + }; + let bob_key_package = KeyPackage::builder() + .build( + ciphersuite, + provider, + &bob_signer, + bob_credential_with_key.clone(), + ) + .expect("An unexpected error occurred."); + + let mls_group_create_config = MlsGroupCreateConfig::builder() + .wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY) + .ciphersuite(ciphersuite) + .build(); + + // === Alice creates a group === + let mut alice_group = MlsGroup::new( + provider, + &alice_signer, + &mls_group_create_config, + alice_credential_with_key.clone(), + ) + .expect("An unexpected error occurred."); + + let welcome = match alice_group.add_members( + provider, + &alice_signer, + &[bob_key_package.key_package().clone()], + ) { + Ok((_, welcome, _)) => welcome, + Err(e) => panic!("Could not add member to group: {e:?}"), + }; + + alice_group + .merge_pending_commit(provider) + .expect("error merging pending commit"); + + (alice_group, mls_group_create_config, welcome) + }, + |(alice_group, mls_group_create_config, welcome)| { + let welcome: MlsMessageIn = welcome.into(); + let welcome = welcome + .into_welcome() + .expect("expected the message to be a welcome message"); + let _bob_group = StagedWelcome::new_from_welcome( + provider, + mls_group_create_config.join_config(), + welcome, + Some(alice_group.export_ratchet_tree().into()), + ) + .unwrap() + .into_group(provider); + }, + ); + }, + ); + } +} + +fn create_commit(c: &mut Criterion, provider: &impl OpenMlsProvider) { + for &ciphersuite in provider.crypto().supported_ciphersuites().iter() { + c.bench_function( + &format!("Create a commit with ciphersuite: {ciphersuite:?}"), + move |b| { + b.iter_with_setup( + || { + let alice_credential = BasicCredential::new("Alice".into()); + let alice_signer = + SignatureKeyPair::new(ciphersuite.signature_algorithm()).unwrap(); + let alice_credential_with_key = CredentialWithKey { + credential: alice_credential.into(), + signature_key: alice_signer.to_public_vec().into(), + }; + + let bob_credential = BasicCredential::new("Bob".into()); + let bob_signer = + SignatureKeyPair::new(ciphersuite.signature_algorithm()).unwrap(); + let bob_credential_with_key = CredentialWithKey { + credential: bob_credential.into(), + signature_key: bob_signer.to_public_vec().into(), + }; + let bob_key_package = KeyPackage::builder() + .build( + ciphersuite, + provider, + &bob_signer, + bob_credential_with_key.clone(), + ) + .expect("An unexpected error occurred."); + + let mls_group_create_config = MlsGroupCreateConfig::builder() + .wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY) + .ciphersuite(ciphersuite) + .build(); + + // === Alice creates a group === + let mut alice_group = MlsGroup::new( + provider, + &alice_signer, + &mls_group_create_config, + alice_credential_with_key.clone(), + ) + .expect("An unexpected error occurred."); + + let welcome = match alice_group.add_members( + provider, + &alice_signer, + &[bob_key_package.key_package().clone()], + ) { + Ok((_, welcome, _)) => welcome, + Err(e) => panic!("Could not add member to group: {e:?}"), + }; + + alice_group + .merge_pending_commit(provider) + .expect("error merging pending commit"); + + let welcome: MlsMessageIn = welcome.into(); + let welcome = welcome + .into_welcome() + .expect("expected the message to be a welcome message"); + let bob_group = StagedWelcome::new_from_welcome( + provider, + mls_group_create_config.join_config(), + welcome, + Some(alice_group.export_ratchet_tree().into()), + ) + .unwrap() + .into_group(provider) + .unwrap(); + + (bob_group, bob_signer) + }, + |(mut bob_group, bob_signer)| { + let (queued_message, welcome_option, _group_info) = + bob_group.self_update(provider, &bob_signer).unwrap(); + + bob_group + .merge_pending_commit(provider) + .expect("error merging pending commit"); + }, + ); + }, + ); + } +} + fn kp_bundle_rust_crypto(c: &mut Criterion) { let provider = &OpenMlsRustCrypto::default(); println!("provider: RustCrypto"); - criterion_kp_bundle(c, provider); + criterion_key_package(c, provider); } fn criterion_benchmark(c: &mut Criterion) { kp_bundle_rust_crypto(c); + criterion_key_package(c, &openmls_libcrux_crypto::Provider::default()); + create_welcome(c, &openmls_libcrux_crypto::Provider::default()); + join_group(c, &openmls_libcrux_crypto::Provider::default()); + create_commit(c, &openmls_libcrux_crypto::Provider::default()); } criterion_group!(benches, criterion_benchmark); diff --git a/openmls/src/group/core_group/mod.rs b/openmls/src/group/core_group/mod.rs index 11f9f3dea..28d4c5d3a 100644 --- a/openmls/src/group/core_group/mod.rs +++ b/openmls/src/group/core_group/mod.rs @@ -6,7 +6,7 @@ //! error, will still return a `Result` since they may throw a `LibraryError`. // Private -mod new_from_welcome; +pub(super) mod new_from_welcome; // Crate pub(crate) mod create_commit_params; diff --git a/openmls/src/group/core_group/new_from_welcome.rs b/openmls/src/group/core_group/new_from_welcome.rs index 0bb4a7c12..eb8e46c66 100644 --- a/openmls/src/group/core_group/new_from_welcome.rs +++ b/openmls/src/group/core_group/new_from_welcome.rs @@ -19,207 +19,26 @@ impl StagedCoreWelcome { ratchet_tree: Option, key_package_bundle: KeyPackageBundle, provider: &Provider, - mut resumption_psk_store: ResumptionPskStore, + resumption_psk_store: ResumptionPskStore, ) -> Result> { log::debug!("CoreGroup::new_from_welcome_internal"); - let ciphersuite = welcome.ciphersuite(); - - // Find key_package in welcome secrets - let egs = if let Some(egs) = CoreGroup::find_key_package_from_welcome_secrets( - key_package_bundle - .key_package() - .hash_ref(provider.crypto())?, - welcome.secrets(), - ) { - egs - } else { - return Err(WelcomeError::JoinerSecretNotFound); - }; - if ciphersuite != key_package_bundle.key_package().ciphersuite() { - let e = WelcomeError::CiphersuiteMismatch; - debug!("new_from_welcome {:?}", e); - return Err(e); - } - - let group_secrets = GroupSecrets::try_from_ciphertext( - key_package_bundle.init_private_key(), - egs.encrypted_group_secrets(), - welcome.encrypted_group_info(), - ciphersuite, - provider.crypto(), - )?; - - // Prepare the PskSecret - let psk_secret = { - let psks = load_psks( - provider.storage(), - &resumption_psk_store, - &group_secrets.psks, - )?; - - PskSecret::new(provider.crypto(), ciphersuite, psks)? - }; - - // Create key schedule - let mut key_schedule = KeySchedule::init( - ciphersuite, - provider.crypto(), - &group_secrets.joiner_secret, - psk_secret, - )?; - - // Derive welcome key & nonce from the key schedule - let (welcome_key, welcome_nonce) = key_schedule - .welcome(provider.crypto(), ciphersuite) - .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))? - .derive_welcome_key_nonce(provider.crypto(), ciphersuite) - .map_err(LibraryError::unexpected_crypto_error)?; - - let verifiable_group_info = VerifiableGroupInfo::try_from_ciphertext( - &welcome_key, - &welcome_nonce, - welcome.encrypted_group_info(), - &[], - provider.crypto(), - )?; - - // Make sure that we can support the required capabilities in the group info. - if let Some(required_capabilities) = - verifiable_group_info.extensions().required_capabilities() - { - // Also check that our key package actually supports the extensions. - // Per spec the sender must have checked this. But you never know. - key_package_bundle - .key_package() - .leaf_node() - .capabilities() - .supports_required_capabilities(required_capabilities)?; - } - - // Build the ratchet tree - - // Set nodes either from the extension or from the `nodes_option`. - // If we got a ratchet tree extension in the welcome, we enable it for - // this group. Note that this is not strictly necessary. But there's - // currently no other mechanism to enable the extension. - let (ratchet_tree, enable_ratchet_tree_extension) = - match verifiable_group_info.extensions().ratchet_tree() { - Some(extension) => (extension.ratchet_tree().clone(), true), - None => match ratchet_tree { - Some(ratchet_tree) => (ratchet_tree, false), - None => return Err(WelcomeError::MissingRatchetTree), - }, - }; - - // Since there is currently only the external pub extension, there is no - // group info extension of interest here. - let (public_group, _group_info_extensions) = PublicGroup::from_external( + let (ciphersuite, group_secrets, key_schedule, verifiable_group_info) = process_welcome( + welcome, + &key_package_bundle, provider, - ratchet_tree, - verifiable_group_info.clone(), - ProposalStore::new(), + &resumption_psk_store, )?; - // Find our own leaf in the tree. - let own_leaf_index = public_group - .members() - .find_map(|m| { - if m.signature_key - == key_package_bundle - .key_package() - .leaf_node() - .signature_key() - .as_slice() - { - Some(m.index) - } else { - None - } - }) - .ok_or(WelcomeError::PublicTreeError( - PublicTreeError::MalformedTree, - ))?; - - let (group_epoch_secrets, message_secrets) = { - let serialized_group_context = public_group - .group_context() - .tls_serialize_detached() - .map_err(LibraryError::missing_bound_check)?; - - // TODO #751: Implement PSK - key_schedule - .add_context(provider.crypto(), &serialized_group_context) - .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?; - - let epoch_secrets = key_schedule - .epoch_secrets(provider.crypto(), ciphersuite) - .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?; - - epoch_secrets.split_secrets( - serialized_group_context, - public_group.tree_size(), - own_leaf_index, - ) - }; - - let confirmation_tag = message_secrets - .confirmation_key() - .tag( - provider.crypto(), - ciphersuite, - public_group.group_context().confirmed_transcript_hash(), - ) - .map_err(LibraryError::unexpected_crypto_error)?; - - // Verify confirmation tag - if &confirmation_tag != public_group.confirmation_tag() { - log::error!("Confirmation tag mismatch"); - log_crypto!(trace, " Got: {:x?}", confirmation_tag); - log_crypto!(trace, " Expected: {:x?}", public_group.confirmation_tag()); - debug_assert!(false, "Confirmation tag mismatch"); - return Err(WelcomeError::ConfirmationTagMismatch); - } - - let message_secrets_store = MessageSecretsStore::new_with_secret(0, message_secrets); - - // Extract and store the resumption PSK for the current epoch. - let resumption_psk = group_epoch_secrets.resumption_psk(); - resumption_psk_store.add(public_group.group_context().epoch(), resumption_psk.clone()); - - let welcome_sender_index = verifiable_group_info.signer(); - let path_keypairs = if let Some(path_secret) = group_secrets.path_secret { - let (path_keypairs, _commit_secret) = public_group - .derive_path_secrets( - provider.crypto(), - ciphersuite, - path_secret, - welcome_sender_index, - own_leaf_index, - ) - .map_err(|e| match e { - DerivePathError::LibraryError(e) => e.into(), - DerivePathError::PublicKeyMismatch => { - WelcomeError::PublicTreeError(PublicTreeError::PublicKeyMismatch) - } - })?; - Some(path_keypairs) - } else { - None - }; - - let group = StagedCoreWelcome { - public_group, - group_epoch_secrets, - own_leaf_index, - use_ratchet_tree_extension: enable_ratchet_tree_extension, - message_secrets_store, - resumption_psk_store, + build_staged_welcome( verifiable_group_info, + ratchet_tree, + provider, key_package_bundle, - path_keypairs, - }; - - Ok(group) + key_schedule, + ciphersuite, + resumption_psk_store, + group_secrets, + ) } /// Returns the [`LeafNodeIndex`] of the group member that authored the [`Welcome`] message. @@ -272,6 +91,221 @@ impl StagedCoreWelcome { } } +#[allow(clippy::too_many_arguments)] +pub(in crate::group) fn build_staged_welcome( + verifiable_group_info: VerifiableGroupInfo, + ratchet_tree: Option, + provider: &Provider, + key_package_bundle: KeyPackageBundle, + mut key_schedule: KeySchedule, + ciphersuite: Ciphersuite, + mut resumption_psk_store: ResumptionPskStore, + group_secrets: GroupSecrets, +) -> Result> { + // Build the ratchet tree and group + + // Set nodes either from the extension or from the `nodes_option`. + // If we got a ratchet tree extension in the welcome, we enable it for + // this group. Note that this is not strictly necessary. But there's + // currently no other mechanism to enable the extension. + let (ratchet_tree, enable_ratchet_tree_extension) = + match verifiable_group_info.extensions().ratchet_tree() { + Some(extension) => (extension.ratchet_tree().clone(), true), + None => match ratchet_tree { + Some(ratchet_tree) => (ratchet_tree, false), + None => return Err(WelcomeError::MissingRatchetTree), + }, + }; + + // Since there is currently only the external pub extension, there is no + // group info extension of interest here. + let (public_group, _group_info_extensions) = PublicGroup::from_external( + provider, + ratchet_tree, + verifiable_group_info.clone(), + ProposalStore::new(), + )?; + + // Find our own leaf in the tree. + let own_leaf_index = public_group + .members() + .find_map(|m| { + if m.signature_key + == key_package_bundle + .key_package() + .leaf_node() + .signature_key() + .as_slice() + { + Some(m.index) + } else { + None + } + }) + .ok_or(WelcomeError::PublicTreeError( + PublicTreeError::MalformedTree, + ))?; + + let (group_epoch_secrets, message_secrets) = { + let serialized_group_context = public_group + .group_context() + .tls_serialize_detached() + .map_err(LibraryError::missing_bound_check)?; + + // TODO #751: Implement PSK + key_schedule + .add_context(provider.crypto(), &serialized_group_context) + .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?; + + let epoch_secrets = key_schedule + .epoch_secrets(provider.crypto(), ciphersuite) + .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?; + + epoch_secrets.split_secrets( + serialized_group_context, + public_group.tree_size(), + own_leaf_index, + ) + }; + + let confirmation_tag = message_secrets + .confirmation_key() + .tag( + provider.crypto(), + ciphersuite, + public_group.group_context().confirmed_transcript_hash(), + ) + .map_err(LibraryError::unexpected_crypto_error)?; + + // Verify confirmation tag + if &confirmation_tag != public_group.confirmation_tag() { + log::error!("Confirmation tag mismatch"); + log_crypto!(trace, " Got: {:x?}", confirmation_tag); + log_crypto!(trace, " Expected: {:x?}", public_group.confirmation_tag()); + debug_assert!(false, "Confirmation tag mismatch"); + return Err(WelcomeError::ConfirmationTagMismatch); + } + + let message_secrets_store = MessageSecretsStore::new_with_secret(0, message_secrets); + + // Extract and store the resumption PSK for the current epoch. + let resumption_psk = group_epoch_secrets.resumption_psk(); + resumption_psk_store.add(public_group.group_context().epoch(), resumption_psk.clone()); + + let welcome_sender_index = verifiable_group_info.signer(); + let path_keypairs = if let Some(path_secret) = group_secrets.path_secret { + let (path_keypairs, _commit_secret) = public_group + .derive_path_secrets( + provider.crypto(), + ciphersuite, + path_secret, + welcome_sender_index, + own_leaf_index, + ) + .map_err(|e| match e { + DerivePathError::LibraryError(e) => e.into(), + DerivePathError::PublicKeyMismatch => { + WelcomeError::PublicTreeError(PublicTreeError::PublicKeyMismatch) + } + })?; + Some(path_keypairs) + } else { + None + }; + + let group = StagedCoreWelcome { + public_group, + group_epoch_secrets, + own_leaf_index, + use_ratchet_tree_extension: enable_ratchet_tree_extension, + message_secrets_store, + resumption_psk_store, + verifiable_group_info, + key_package_bundle, + path_keypairs, + }; + + Ok(group) +} + +/// Process a Welcome message up to the point where the ratchet tree is is required. +pub(in crate::group) fn process_welcome( + welcome: Welcome, + key_package_bundle: &KeyPackageBundle, + provider: &Provider, + resumption_psk_store: &ResumptionPskStore, +) -> Result< + (Ciphersuite, GroupSecrets, KeySchedule, VerifiableGroupInfo), + WelcomeError, +> { + let ciphersuite = welcome.ciphersuite(); + let egs = if let Some(egs) = CoreGroup::find_key_package_from_welcome_secrets( + key_package_bundle + .key_package() + .hash_ref(provider.crypto())?, + welcome.secrets(), + ) { + egs + } else { + return Err(WelcomeError::JoinerSecretNotFound); + }; + if ciphersuite != key_package_bundle.key_package().ciphersuite() { + let e = WelcomeError::CiphersuiteMismatch; + debug!("new_from_welcome {:?}", e); + return Err(e); + } + let group_secrets = GroupSecrets::try_from_ciphertext( + key_package_bundle.init_private_key(), + egs.encrypted_group_secrets(), + welcome.encrypted_group_info(), + ciphersuite, + provider.crypto(), + )?; + let psk_secret = { + let psks = load_psks( + provider.storage(), + resumption_psk_store, + &group_secrets.psks, + )?; + + PskSecret::new(provider.crypto(), ciphersuite, psks)? + }; + let key_schedule = KeySchedule::init( + ciphersuite, + provider.crypto(), + &group_secrets.joiner_secret, + psk_secret, + )?; + let (welcome_key, welcome_nonce) = key_schedule + .welcome(provider.crypto(), ciphersuite) + .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))? + .derive_welcome_key_nonce(provider.crypto(), ciphersuite) + .map_err(LibraryError::unexpected_crypto_error)?; + let verifiable_group_info = VerifiableGroupInfo::try_from_ciphertext( + &welcome_key, + &welcome_nonce, + welcome.encrypted_group_info(), + &[], + provider.crypto(), + )?; + if let Some(required_capabilities) = verifiable_group_info.extensions().required_capabilities() + { + // Also check that our key package actually supports the extensions. + // Per spec the sender must have checked this. But you never know. + key_package_bundle + .key_package() + .leaf_node() + .capabilities() + .supports_required_capabilities(required_capabilities)?; + } + Ok(( + ciphersuite, + group_secrets, + key_schedule, + verifiable_group_info, + )) +} + impl CoreGroup { // Helper functions diff --git a/openmls/src/group/mls_group/creation.rs b/openmls/src/group/mls_group/creation.rs index ff0522bb8..7aafcbc83 100644 --- a/openmls/src/group/mls_group/creation.rs +++ b/openmls/src/group/mls_group/creation.rs @@ -11,7 +11,7 @@ use crate::{ group_info::{GroupInfo, VerifiableGroupInfo}, Welcome, }, - schedule::psk::store::ResumptionPskStore, + schedule::psk::{store::ResumptionPskStore, PreSharedKeyId}, storage::OpenMlsProvider, treesync::RatchetTreeIn, }; @@ -147,6 +147,81 @@ fn transpose_err_opt(v: Result, E>) -> Option> { } } +impl ProcessedWelcome { + /// Creates a new processed [`Welcome`] message that can be used to parse + /// it before creating a [`StagedWelcome`]. + /// + /// This does not require a ratchet tree yet. + /// + /// [`Welcome`]: crate::messages::Welcome + pub fn new_from_welcome( + provider: &Provider, + mls_group_config: &MlsGroupJoinConfig, + welcome: Welcome, + ) -> Result> { + let (resumption_psk_store, key_package_bundle) = + keys_for_welcome(mls_group_config, &welcome, provider)?; + + let (ciphersuite, group_secrets, key_schedule, verifiable_group_info) = + crate::group::core_group::new_from_welcome::process_welcome( + welcome, + &key_package_bundle, + provider, + &resumption_psk_store, + )?; + + Ok(Self { + mls_group_config: mls_group_config.clone(), + ciphersuite, + group_secrets, + key_schedule, + verifiable_group_info, + resumption_psk_store, + key_package_bundle, + }) + } + + /// Get a reference to the GroupInfo in this Welcome message. + /// + /// **NOTE:** The group info contains **unverified** values. Use with caution. + pub fn unverified_group_info(&self) -> &VerifiableGroupInfo { + &self.verifiable_group_info + } + + /// Get a reference to the PSKs in this Welcome message. + /// + /// **NOTE:** The group info contains **unverified** values. Use with caution. + pub fn psks(&self) -> &[PreSharedKeyId] { + &self.group_secrets.psks + } + + /// Consume the `ProcessedWelcome` and combine it witht he ratchet tree into + /// a `StagedWelcome`. + pub fn into_staged_welcome( + self, + provider: &Provider, + ratchet_tree: Option, + ) -> Result> { + let group = crate::group::core_group::new_from_welcome::build_staged_welcome( + self.verifiable_group_info, + ratchet_tree, + provider, + self.key_package_bundle, + self.key_schedule, + self.ciphersuite, + self.resumption_psk_store, + self.group_secrets, + )?; + + let staged_welcome = StagedWelcome { + mls_group_config: self.mls_group_config, + group, + }; + + Ok(staged_welcome) + } +} + impl StagedWelcome { /// Creates a new staged welcome from a [`Welcome`] message. Returns an error /// ([`WelcomeError::NoMatchingKeyPackage`]) if no [`KeyPackage`] @@ -161,33 +236,8 @@ impl StagedWelcome { welcome: Welcome, ratchet_tree: Option, ) -> Result> { - let resumption_psk_store = - ResumptionPskStore::new(mls_group_config.number_of_resumption_psks); - let key_package_bundle: KeyPackageBundle = welcome - .secrets() - .iter() - .find_map(|egs| { - let hash_ref = egs.new_member(); - - transpose_err_opt( - provider - .storage() - .key_package(&hash_ref) - .map_err(WelcomeError::StorageError), - ) - }) - .ok_or(WelcomeError::NoMatchingKeyPackage)??; - - // Delete the [`KeyPackage`] and the corresponding private key from the - // key store, but only if it doesn't have a last resort extension. - if !key_package_bundle.key_package().last_resort() { - provider - .storage() - .delete_key_package(&key_package_bundle.key_package.hash_ref(provider.crypto())?) - .map_err(WelcomeError::StorageError)?; - } else { - log::debug!("Key package has last resort extension, not deleting"); - } + let (resumption_psk_store, key_package_bundle) = + keys_for_welcome(mls_group_config, &welcome, provider)?; let group = StagedCoreWelcome::new_from_welcome( welcome, @@ -248,3 +298,37 @@ impl StagedWelcome { Ok(mls_group) } } + +fn keys_for_welcome( + mls_group_config: &MlsGroupJoinConfig, + welcome: &Welcome, + provider: &Provider, +) -> Result< + (ResumptionPskStore, KeyPackageBundle), + WelcomeError<::StorageError>, +> { + let resumption_psk_store = ResumptionPskStore::new(mls_group_config.number_of_resumption_psks); + let key_package_bundle: KeyPackageBundle = welcome + .secrets() + .iter() + .find_map(|egs| { + let hash_ref = egs.new_member(); + + transpose_err_opt( + provider + .storage() + .key_package(&hash_ref) + .map_err(WelcomeError::StorageError), + ) + }) + .ok_or(WelcomeError::NoMatchingKeyPackage)??; + if !key_package_bundle.key_package().last_resort() { + provider + .storage() + .delete_key_package(&key_package_bundle.key_package.hash_ref(provider.crypto())?) + .map_err(WelcomeError::StorageError)?; + } else { + log::debug!("Key package has last resort extension, not deleting"); + } + Ok((resumption_psk_store, key_package_bundle)) +} diff --git a/openmls/src/group/mls_group/mod.rs b/openmls/src/group/mls_group/mod.rs index 501dc9c0d..b32b229bf 100644 --- a/openmls/src/group/mls_group/mod.rs +++ b/openmls/src/group/mls_group/mod.rs @@ -11,7 +11,7 @@ use crate::{ framing::{mls_auth_content::AuthenticatedContent, *}, group::*, key_packages::{KeyPackage, KeyPackageBundle}, - messages::proposals::*, + messages::{proposals::*, GroupSecrets}, schedule::ResumptionPskSecret, storage::{OpenMlsProvider, StorageProvider}, treesync::{node::leaf_node::LeafNode, RatchetTree}, @@ -494,3 +494,23 @@ pub struct StagedWelcome { // information. group: StagedCoreWelcome, } + +/// A parsed, but not fully processed `Welcome` message. +/// +/// This may be used in order to retrieve information from the `Welcome` about +/// the ratchet tree. +/// +/// Use `into_staged_welcome` to get the [`StagedWelcome`] on this. +pub struct ProcessedWelcome { + // The group configuration. See [`MlsGroupJoinConfig`] for more information. + mls_group_config: MlsGroupJoinConfig, + + // The following is the state after parsing the Welcome message, before actually + // building the group. + ciphersuite: Ciphersuite, + group_secrets: GroupSecrets, + key_schedule: crate::schedule::KeySchedule, + verifiable_group_info: crate::messages::group_info::VerifiableGroupInfo, + resumption_psk_store: crate::schedule::psk::store::ResumptionPskStore, + key_package_bundle: KeyPackageBundle, +} diff --git a/openmls/src/group/mls_group/proposal.rs b/openmls/src/group/mls_group/proposal.rs index be6df8e93..46cd8ee64 100644 --- a/openmls/src/group/mls_group/proposal.rs +++ b/openmls/src/group/mls_group/proposal.rs @@ -387,10 +387,13 @@ impl MlsGroup { Ok((mls_message, proposal_ref)) } - /// Updates group context extensions + /// Updates Group Context Extensions + /// + /// Commits to the Group Context Extension inline proposal using the [`Extensions`] /// /// Returns an error when the group does not support all the required capabilities - /// in the new `extensions`. + /// in the new `extensions` or if there is a pending commit. + //// FIXME: #1217 #[allow(clippy::type_complexity)] pub fn update_group_context_extensions( &mut self, @@ -403,11 +406,12 @@ impl MlsGroup { > { self.is_operational()?; - // Create group context extension proposals + // Create inline group context extension proposals let inline_proposals = vec![Proposal::GroupContextExtensions( - GroupContextExtensionProposal { extensions }, + GroupContextExtensionProposal::new(extensions), )]; + // Create Commit over all proposals let params = CreateCommitParams::builder() .framing_parameters(self.framing_parameters()) .proposal_store(&self.proposal_store) @@ -416,6 +420,9 @@ impl MlsGroup { let create_commit_result = self.group.create_commit(params, provider, signer)?; let mls_messages = self.content_to_mls_message(create_commit_result.commit, provider)?; + + // Set the current group state to [`MlsGroupState::PendingCommit`], + // storing the current [`StagedCommit`] from the commit results self.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member( create_commit_result.staged_commit, ))); diff --git a/openmls/src/group/mls_group/test_mls_group.rs b/openmls/src/group/mls_group/test_mls_group.rs index 25fc1958f..2c0edcac9 100644 --- a/openmls/src/group/mls_group/test_mls_group.rs +++ b/openmls/src/group/mls_group/test_mls_group.rs @@ -1553,6 +1553,137 @@ fn update_group_context_with_unknown_extension() { + let alice_provider = Provider::default(); + let (alice_credential_with_key, _alice_kpb, alice_signer, _alice_pk) = + setup_client("Alice", ciphersuite, &alice_provider); + + // === Define the unknown group context extension and initial data === + const UNKNOWN_EXTENSION_TYPE: u16 = 0xff11; + let unknown_extension_data = vec![1, 2]; + let unknown_gc_extension = Extension::Unknown( + UNKNOWN_EXTENSION_TYPE, + UnknownExtension(unknown_extension_data), + ); + let required_extension_types = &[ExtensionType::Unknown(UNKNOWN_EXTENSION_TYPE)]; + let required_capabilities = Extension::RequiredCapabilities( + RequiredCapabilitiesExtension::new(required_extension_types, &[], &[]), + ); + let capabilities = Capabilities::new(None, None, Some(required_extension_types), None, None); + let test_gc_extensions = Extensions::from_vec(vec![ + unknown_gc_extension.clone(), + required_capabilities.clone(), + ]) + .expect("error creating test group context extensions"); + let mls_group_create_config = MlsGroupCreateConfig::builder() + .with_group_context_extensions(test_gc_extensions.clone()) + .expect("error adding unknown extension to config") + .capabilities(capabilities.clone()) + .ciphersuite(ciphersuite) + .build(); + + // === Alice creates a group === + let mut alice_group = MlsGroup::new( + &alice_provider, + &alice_signer, + &mls_group_create_config, + alice_credential_with_key, + ) + .expect("error creating group"); + + // === Verify the initial group context extension data is correct === + let group_context_extensions = alice_group.group().context().extensions(); + let mut extracted_data = None; + for extension in group_context_extensions.iter() { + if let Extension::Unknown(UNKNOWN_EXTENSION_TYPE, UnknownExtension(data)) = extension { + extracted_data = Some(data.clone()); + } + } + assert_eq!( + extracted_data.unwrap(), + vec![1, 2], + "The data of Extension::Unknown(0xff11) does not match the expected data" + ); + + // === Propose the new group context extension using update_group_context_extensions === + let updated_unknown_extension_data = vec![3, 4]; + let updated_unknown_gc_extension = Extension::Unknown( + UNKNOWN_EXTENSION_TYPE, + UnknownExtension(updated_unknown_extension_data.clone()), + ); + + let mut updated_extensions = test_gc_extensions.clone(); + updated_extensions.add_or_replace(updated_unknown_gc_extension); + + let update_result = alice_group.update_group_context_extensions( + &alice_provider, + updated_extensions, + &alice_signer, + ); + assert!( + update_result.is_ok(), + "Failed to update group context extensions: {:?}", + update_result.err() + ); + + // === Test clearing staged commit before merge, verify context shows expected data === + alice_group + .clear_pending_commit(provider.storage()) + .unwrap(); + let group_context_extensions = alice_group.group().context().extensions(); + let mut extracted_data = None; + for extension in group_context_extensions.iter() { + if let Extension::Unknown(UNKNOWN_EXTENSION_TYPE, UnknownExtension(data)) = extension { + extracted_data = Some(data.clone()); + } + } + assert_eq!( + extracted_data.unwrap(), + vec![1, 2], + "The data of Extension::Unknown(0xff11) does not match the expected data" + ); + + // === Propose the new group context extension using update_group_context_extensions === + let updated_unknown_extension_data = vec![4, 5]; // Sample data for the extension + let updated_unknown_gc_extension = Extension::Unknown( + UNKNOWN_EXTENSION_TYPE, + UnknownExtension(updated_unknown_extension_data.clone()), + ); + + let mut updated_extensions = test_gc_extensions.clone(); + updated_extensions.add_or_replace(updated_unknown_gc_extension); + let update_result = alice_group.update_group_context_extensions( + &alice_provider, + updated_extensions, + &alice_signer, + ); + assert!( + update_result.is_ok(), + "Failed to update group context extensions: {:?}", + update_result.err() + ); + + // === Merge Pending Commit === + alice_group.merge_pending_commit(&alice_provider).unwrap(); + + // === Verify the group context extension was updated === + let group_context_extensions = alice_group.group().context().extensions(); + let mut extracted_data_updated = None; + for extension in group_context_extensions.iter() { + if let Extension::Unknown(UNKNOWN_EXTENSION_TYPE, UnknownExtension(data)) = extension { + extracted_data_updated = Some(data.clone()); + } + } + assert_eq!( + extracted_data_updated.unwrap(), + vec![4, 5], + "The data of Extension::Unknown(0xff11) does not match the expected data" + ); +} + // Test that unknown group context and leaf node extensions can be used in groups #[openmls_test] fn unknown_extensions() { diff --git a/openmls/src/key_packages/mod.rs b/openmls/src/key_packages/mod.rs index cf5369500..20d401e54 100644 --- a/openmls/src/key_packages/mod.rs +++ b/openmls/src/key_packages/mod.rs @@ -537,11 +537,6 @@ impl KeyPackageBuilder { .write_key_package(&full_kp.key_package.hash_ref(provider.crypto())?, &full_kp) .map_err(|_| KeyPackageNewError::StorageError)?; - // Store the encryption key pair in the key store. - encryption_keypair - .write(provider.storage()) - .map_err(|_| KeyPackageNewError::StorageError)?; - Ok(full_kp) } } diff --git a/openmls/src/messages/group_info.rs b/openmls/src/messages/group_info.rs index 39871c9fd..f5d9a7d75 100644 --- a/openmls/src/messages/group_info.rs +++ b/openmls/src/messages/group_info.rs @@ -103,7 +103,7 @@ impl VerifiableGroupInfo { /// Get (unverified) extensions of the verifiable group info. /// /// Note: This method should only be used when necessary to verify the group info signature. - pub(crate) fn extensions(&self) -> &Extensions { + pub fn extensions(&self) -> &Extensions { &self.payload.extensions } @@ -111,7 +111,7 @@ impl VerifiableGroupInfo { /// /// Note: This method should only be used when necessary to verify the group /// info signature. - pub(crate) fn group_id(&self) -> &GroupId { + pub fn group_id(&self) -> &GroupId { self.payload.group_context.group_id() } } diff --git a/openmls/src/messages/tests/test_welcome.rs b/openmls/src/messages/tests/test_welcome.rs index f817bbb07..888f94fd8 100644 --- a/openmls/src/messages/tests/test_welcome.rs +++ b/openmls/src/messages/tests/test_welcome.rs @@ -9,7 +9,8 @@ use crate::{ }, extensions::Extensions, group::{ - errors::WelcomeError, GroupContext, GroupId, MlsGroup, MlsGroupCreateConfig, StagedWelcome, + errors::WelcomeError, GroupContext, GroupId, MlsGroup, MlsGroupCreateConfig, + ProcessedWelcome, StagedWelcome, }, messages::{ group_info::{GroupInfoTBS, VerifiableGroupInfo}, @@ -31,8 +32,6 @@ fn test_welcome_context_mismatch( ciphersuite: Ciphersuite, provider: &impl crate::storage::OpenMlsProvider, ) { - let _ = pretty_env_logger::try_init(); - // We need a ciphersuite that is different from the current one to create // the mismatch let mismatched_ciphersuite = match ciphersuite { @@ -148,9 +147,10 @@ fn test_welcome_context_mismatch( welcome.encrypted_group_info = encrypted_verifiable_group_info.into(); // Create backup of encryption keypair, s.t. we can process the welcome a second time after failing. - let encryption_keypair = - EncryptionKeyPair::read(provider, bob_kpb.key_package().leaf_node().encryption_key()) - .unwrap(); + let encryption_keypair = EncryptionKeyPair::from(( + bob_kpb.key_package().leaf_node().encryption_key().clone(), + bob_kpb.private_encryption_key.clone(), + )); // Bob tries to join the group let err = StagedWelcome::new_from_welcome( @@ -296,6 +296,81 @@ fn test_welcome_message(ciphersuite: Ciphersuite, provider: &impl crate::storage ); } +/// Test the parsed welcome flow where the Welcome is first processed to give +/// the caller the GroupInfo. +/// This allows transporting information in the Welcome for retrieving the ratchet +/// tree. +#[openmls_test::openmls_test] +fn test_welcome_processing() { + let group_id = GroupId::random(provider.rand()); + let mls_group_create_config = MlsGroupCreateConfig::builder() + .ciphersuite(ciphersuite) + .build(); + + let (alice_credential_with_key, _alice_kpb, alice_signer, _alice_signature_key) = + crate::group::test_core_group::setup_client("Alice", ciphersuite, provider); + let (_bob_credential, bob_kpb, _bob_signer, _bob_signature_key) = + crate::group::test_core_group::setup_client("Bob", ciphersuite, provider); + + let bob_kp = bob_kpb.key_package(); + + // === Alice creates a group and adds Bob === + let mut alice_group = MlsGroup::new_with_group_id( + provider, + &alice_signer, + &mls_group_create_config, + group_id, + alice_credential_with_key, + ) + .expect("An unexpected error occurred."); + + let (_queued_message, welcome, _group_info) = alice_group + .add_members(provider, &alice_signer, &[bob_kp.clone()]) + .expect("Could not add member to group."); + + alice_group + .merge_pending_commit(provider) + .expect("error merging pending commit"); + + let welcome = welcome.into_welcome().expect("Unexpected message type."); + + provider + .storage() + .write_key_package(&bob_kp.hash_ref(provider.crypto()).unwrap(), &bob_kpb) + .unwrap(); + + // Process the welcome + let processed_welcome = ProcessedWelcome::new_from_welcome( + provider, + mls_group_create_config.join_config(), + welcome, + ) + .unwrap(); + + // Check values in processed welcome + let unverified_group_info = processed_welcome.unverified_group_info(); + let group_id = unverified_group_info.group_id(); + assert_eq!(group_id, alice_group.group_id()); + let alice_group_info = alice_group + .export_group_info(provider, &alice_signer, false) + .unwrap() + .into_verifiable_group_info() + .unwrap(); + assert_eq!( + unverified_group_info.extensions(), + alice_group_info.extensions() + ); + // Use the group id or extensions to get the ratchet tree. + + // Stage the welcome + let staged_welcome = processed_welcome + .into_staged_welcome(provider, Some(alice_group.export_ratchet_tree().into())) + .unwrap(); + let _group = staged_welcome + .into_group(provider) + .expect("Error creating group from a valid staged join."); +} + #[test] fn invalid_welcomes() { // An almost good welcome message. diff --git a/openmls/tests/test_mls_group.rs b/openmls/tests/test_mls_group.rs index f86cc90db..3f83faff7 100644 --- a/openmls/tests/test_mls_group.rs +++ b/openmls/tests/test_mls_group.rs @@ -1229,6 +1229,10 @@ fn group_context_extensions_proposal( // No required capabilities, so no specifically required extensions. assert!(alice_group.extensions().required_capabilities().is_none()); + // The old group context + let group_context_before = alice_group.export_group_context().clone(); + assert_eq!(group_context_before.extensions(), &Extensions::empty()); + let new_extensions = Extensions::single(Extension::RequiredCapabilities( RequiredCapabilitiesExtension::new(&[ExtensionType::RequiredCapabilities], &[], &[]), )); @@ -1247,6 +1251,14 @@ fn group_context_extensions_proposal( .commit_to_pending_proposals(provider, &alice_signer) .expect("failed to commit to pending proposals"); + // The staged commit has the new group context extensions. + let group_context_staged = alice_group + .pending_commit() + .unwrap() + .group_context() + .clone(); + assert_eq!(group_context_staged.extensions(), &new_extensions); + alice_group .merge_pending_commit(provider) .expect("error merging pending commit"); diff --git a/openmls_test/src/lib.rs b/openmls_test/src/lib.rs index 8908ec750..8ae2cd39a 100644 --- a/openmls_test/src/lib.rs +++ b/openmls_test/src/lib.rs @@ -32,6 +32,7 @@ pub fn openmls_test(_attr: TokenStream, item: TokenStream) -> TokenStream { use openmls_traits::{types::Ciphersuite, crypto::OpenMlsCrypto}; type Provider = OpenMlsRustCrypto; + let _ = pretty_env_logger::try_init(); let ciphersuite = Ciphersuite::try_from(#val).unwrap(); let provider = OpenMlsRustCrypto::default(); @@ -67,6 +68,7 @@ pub fn openmls_test(_attr: TokenStream, item: TokenStream) -> TokenStream { use openmls_traits::{types::Ciphersuite, prelude::*}; type Provider = OpenMlsLibcrux; + let _ = pretty_env_logger::try_init(); let ciphersuite = Ciphersuite::try_from(#val).unwrap(); let provider = OpenMlsLibcrux::default(); diff --git a/traits/src/storage.rs b/traits/src/storage.rs index f0b138fe6..e5aad83e7 100644 --- a/traits/src/storage.rs +++ b/traits/src/storage.rs @@ -21,6 +21,10 @@ pub const V_TEST: u16 = u16::MAX; /// Many getters for lists return a `Result, E>`. In this case, if there was no error but /// the value doesn't exist, an empty vector should be returned. /// +/// Any value that uses the group id as key is required by the group. +/// Returning `None` or an error for any of them will cause a failure when +/// loading a group. +/// /// More details can be taken from the comments on the respective method. pub trait StorageProvider { /// An opaque error returned by all methods on this trait. @@ -349,6 +353,9 @@ pub trait StorageProvider { ) -> Result, Self::Error>; /// Returns the ResumptionPskStore for the group with the given id. + /// + /// Returning `None` here is considered an error because the store is needed + /// by OpenMLS when loading a group. fn resumption_psk_store< GroupId: traits::GroupId, ResumptionPskStore: traits::ResumptionPskStore,