use openmls_traits::{types::Ciphersuite, OpenMlsProvider};
use tls_codec::Serialize as _;
use crate::{
component::{ComponentId, ComponentType},
components::vc_derivation_info::{
load_vc_epoch_state_and_tree, EpochId, VirtualClientOperationType, VC_COMPONENT_ID,
},
credentials::test_utils::new_credential,
extensions::{
AppDataDictionary, AppDataDictionaryExtension, Extension, ExtensionType, Extensions,
},
group::{MlsGroup, MlsGroupCreateConfig, PURE_PLAINTEXT_WIRE_FORMAT_POLICY},
key_packages::KeyPackage,
messages::PathSecret,
prelude::{Capabilities, LeafNode, LeafNodeParameters},
};
const EMULATION_CIPHERSUITE: Ciphersuite =
Ciphersuite::MLS_128_MLKEM768X25519_AES256GCM_SHA384_Ed25519;
const GROUP_CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
fn vc_capabilities() -> Capabilities {
Capabilities::builder()
.extensions(vec![ExtensionType::AppDataDictionary])
.build()
}
fn vc_leaf_extensions() -> Extensions<LeafNode> {
let supported_components: Vec<u16> = vec![VC_COMPONENT_ID];
let app_components_body = supported_components
.tls_serialize_detached()
.expect("serialize AppComponents body");
let mut dictionary = AppDataDictionary::new();
dictionary.insert(
ComponentId::from(ComponentType::AppComponents),
app_components_body,
);
let ext = Extension::AppDataDictionary(AppDataDictionaryExtension::new(dictionary));
Extensions::from_vec(vec![ext]).expect("build leaf-node Extensions")
}
fn vc_group_config(ciphersuite: Ciphersuite) -> MlsGroupCreateConfig {
MlsGroupCreateConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.ciphersuite(ciphersuite)
.use_ratchet_tree_extension(true)
.capabilities(vc_capabilities())
.with_leaf_node_extensions(vc_leaf_extensions())
.expect("attach leaf-node extensions")
.build()
}
fn registered_emulation_epoch<P: OpenMlsProvider>(provider: &P) -> EpochId {
let (credential, signer) = new_credential(
provider,
b"Emulator",
EMULATION_CIPHERSUITE.signature_algorithm(),
);
let mut emulator_group = MlsGroup::new(
provider,
&signer,
&vc_group_config(EMULATION_CIPHERSUITE),
credential,
)
.expect("create emulation group");
emulator_group
.register_vc_emulation_epoch(provider.crypto(), provider.storage())
.expect("register emulation epoch")
}
#[openmls_test::openmls_test]
fn register_vc_emulation_epoch_is_idempotent_per_epoch() {
let provider = &Provider::default();
let (credential, signer) = new_credential(
provider,
b"Emulator",
EMULATION_CIPHERSUITE.signature_algorithm(),
);
let mut emulator_group = MlsGroup::new(
provider,
&signer,
&vc_group_config(EMULATION_CIPHERSUITE),
credential,
)
.expect("create emulation group");
let epoch_id = emulator_group
.register_vc_emulation_epoch(provider.crypto(), provider.storage())
.expect("first registration");
let epoch_id_again = emulator_group
.register_vc_emulation_epoch(provider.crypto(), provider.storage())
.expect("repeated registration in the same epoch");
assert_eq!(
epoch_id, epoch_id_again,
"a repeated registration in the same epoch must return the recorded \
epoch id"
);
emulator_group
.self_update(provider, &signer, LeafNodeParameters::default())
.expect("self update");
emulator_group
.merge_pending_commit(provider)
.expect("merge self update");
let next_epoch_id = emulator_group
.register_vc_emulation_epoch(provider.crypto(), provider.storage())
.expect("registration in the next epoch");
assert_ne!(
epoch_id, next_epoch_id,
"a registration after the epoch advanced must derive a fresh \
emulation epoch"
);
}
#[openmls_test::openmls_test]
fn vc_commit_path_material_imports_into_group_ciphersuite() {
let provider = &Provider::default();
let bob_provider = &Provider::default();
let epoch_id = registered_emulation_epoch(provider);
let (alice_credential, alice_signer) = new_credential(
provider,
b"Alice (VC)",
GROUP_CIPHERSUITE.signature_algorithm(),
);
let mut main_group = MlsGroup::new(
provider,
&alice_signer,
&vc_group_config(GROUP_CIPHERSUITE),
alice_credential,
)
.expect("create main group");
let (bob_credential, bob_signer) = new_credential(
bob_provider,
b"Bob",
GROUP_CIPHERSUITE.signature_algorithm(),
);
let bob_key_package = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(GROUP_CIPHERSUITE, bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
main_group
.add_members(provider, &alice_signer, &[bob_key_package])
.expect("add bob");
main_group
.merge_pending_commit(provider)
.expect("merge add");
let (state, mut scratch_tree) =
load_vc_epoch_state_and_tree(provider, &epoch_id).expect("load vc epoch state");
let (emulation_leaf_index, _epoch_encryption_key, emulation_ciphersuite) = state.into_parts();
assert_eq!(emulation_ciphersuite, EMULATION_CIPHERSUITE);
let (generation, operation_secret) = scratch_tree
.next_operation_secret(
provider.crypto(),
EMULATION_CIPHERSUITE,
&epoch_id,
emulation_leaf_index,
VirtualClientOperationType::LeafNode,
main_group.group_id().as_slice(),
)
.expect("derive reference operation secret");
assert_eq!(generation, 0);
drop(scratch_tree);
let target_operation_secret = operation_secret
.derive_target_operation_secret(provider.crypto(), GROUP_CIPHERSUITE, main_group.group_id())
.expect("derive reference target operation secret");
let expected_leaf_keypair = target_operation_secret
.derive_encryption_key_secret(provider.crypto(), GROUP_CIPHERSUITE)
.expect("derive reference encryption key secret")
.generate_encryption_key_pair(provider.crypto(), GROUP_CIPHERSUITE)
.expect("generate reference leaf keypair");
let expected_parent_keypair = PathSecret::from(
target_operation_secret
.derive_path_generation_secret(provider.crypto(), GROUP_CIPHERSUITE)
.expect("derive reference path generation secret"),
)
.derive_key_pair(provider.crypto(), GROUP_CIPHERSUITE)
.expect("derive reference parent keypair");
main_group
.commit_builder()
.vc_emulation(provider.crypto(), provider.storage(), epoch_id)
.expect("vc_emulation")
.load_psks(provider.storage())
.expect("load psks")
.build(provider.rand(), provider.crypto(), &alice_signer, |_| true)
.expect("build vc commit")
.stage_commit(provider)
.expect("stage vc commit");
main_group
.merge_pending_commit(provider)
.expect("merge vc commit");
assert_eq!(
main_group
.own_leaf_node()
.expect("own leaf")
.encryption_key(),
expected_leaf_keypair.public_key(),
"the leaf encryption key secret must derive from the operation \
secret imported into the group's ciphersuite"
);
let ratchet_tree = main_group.export_ratchet_tree();
let root = ratchet_tree.parents().next().expect("one parent node");
assert_eq!(
root.encryption_key(),
expected_parent_keypair.public_key(),
"the path generation secret must derive from the operation secret \
imported into the group's ciphersuite"
);
}
#[openmls_test::openmls_test]
fn vc_group_creation_leaf_key_imports_into_group_ciphersuite() {
let provider = &Provider::default();
let epoch_id = registered_emulation_epoch(provider);
let (state, mut scratch_tree) =
load_vc_epoch_state_and_tree(provider, &epoch_id).expect("load vc epoch state");
let (emulation_leaf_index, _epoch_encryption_key, emulation_ciphersuite) = state.into_parts();
assert_eq!(emulation_ciphersuite, EMULATION_CIPHERSUITE);
let (generation, operation_secret) = scratch_tree
.next_operation_secret(
provider.crypto(),
EMULATION_CIPHERSUITE,
&epoch_id,
emulation_leaf_index,
VirtualClientOperationType::KeyPackage,
b"",
)
.expect("derive reference operation secret");
assert_eq!(generation, 0);
drop(scratch_tree);
let expected_leaf_keypair = operation_secret
.derive_key_package_seed_secret(provider.crypto(), GROUP_CIPHERSUITE, 0)
.expect("derive reference key package seed")
.derive_encryption_key_secret(provider.crypto(), GROUP_CIPHERSUITE)
.expect("derive reference encryption key secret")
.generate_encryption_key_pair(provider.crypto(), GROUP_CIPHERSUITE)
.expect("generate reference leaf keypair");
let (vc_credential, vc_signer) = new_credential(
provider,
b"Alice (VC)",
GROUP_CIPHERSUITE.signature_algorithm(),
);
let main_group = MlsGroup::builder()
.with_wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.ciphersuite(GROUP_CIPHERSUITE)
.use_ratchet_tree_extension(true)
.with_capabilities(vc_capabilities())
.with_leaf_node_extensions(vc_leaf_extensions())
.expect("attach leaf-node extensions")
.vc_emulation(epoch_id)
.build(provider, &vc_signer, vc_credential)
.expect("create vc group");
assert_eq!(
main_group
.own_leaf_node()
.expect("creator leaf")
.encryption_key(),
expected_leaf_keypair.public_key(),
"the creator leaf's encryption key secret must derive from the \
per-KeyPackage seed imported into the created group's ciphersuite"
);
}