#![cfg(feature = "virtual-clients-draft")]
use openmls::{
components::vc_derivation_info::{EpochId, VcEmulationBindings, VC_COMPONENT_ID},
credentials::NewSignerBundle,
extensions::{
AppDataDictionary, AppDataDictionaryExtension, Extension, ExtensionType, Extensions,
},
framing::errors::{MessageDecryptionError, SecretTreeError},
group::{
ConfirmMessageError, GroupEpoch, MlsGroup, MlsGroupCreateConfig, MlsGroupJoinConfig,
Propose, StagedWelcome, MIXED_CIPHERTEXT_WIRE_FORMAT_POLICY,
PURE_CIPHERTEXT_WIRE_FORMAT_POLICY, PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
},
key_packages::KeyPackage,
prelude::{
test_utils::new_credential, Capabilities, LeafNode, LeafNodeParameters,
ProcessMessageError, ProcessedMessageContent, ProposalOrRefType, ValidationError,
},
};
use openmls_basic_credential::SignatureKeyPair;
use openmls_rust_crypto::OpenMlsRustCrypto;
use openmls_test::openmls_test;
use openmls_traits::storage::StorageProvider as _;
use openmls_traits::OpenMlsProvider;
use tls_codec::Serialize as _;
mod mls_group;
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(1, app_components_body);
let ext = Extension::AppDataDictionary(AppDataDictionaryExtension::new(dictionary));
Extensions::from_vec(vec![ext]).expect("build leaf-node Extensions")
}
fn setup_alice_bob_group<P: OpenMlsProvider>(
ciphersuite: openmls_traits::types::Ciphersuite,
alice_provider: &P,
bob_provider: &P,
) -> (MlsGroup, SignatureKeyPair, MlsGroup, SignatureKeyPair) {
setup_alice_bob_group_with_policy(
ciphersuite,
alice_provider,
bob_provider,
PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
)
}
fn setup_alice_bob_group_with_policy<P: OpenMlsProvider>(
ciphersuite: openmls_traits::types::Ciphersuite,
alice_provider: &P,
bob_provider: &P,
wire_format_policy: openmls::group::WireFormatPolicy,
) -> (MlsGroup, SignatureKeyPair, MlsGroup, SignatureKeyPair) {
let (alice_credential, alice_signer) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let (bob_credential, bob_signer) =
new_credential(bob_provider, b"Bob", ciphersuite.signature_algorithm());
let group_config = MlsGroupCreateConfig::builder()
.wire_format_policy(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 on alice config")
.build();
let mut alice_group = MlsGroup::new(
alice_provider,
&alice_signer,
&group_config,
alice_credential,
)
.expect("alice create group");
let bob_key_package = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build(ciphersuite, bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_commit, welcome, _gi) = alice_group
.add_members(alice_provider, &alice_signer, &[bob_key_package])
.expect("alice add bob");
alice_group
.merge_pending_commit(alice_provider)
.expect("alice merge add");
let join_config = MlsGroupJoinConfig::builder()
.wire_format_policy(wire_format_policy)
.use_ratchet_tree_extension(true)
.build();
let bob_group = StagedWelcome::new_from_welcome(
bob_provider,
&join_config,
welcome.into_welcome().unwrap(),
Some(alice_group.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(bob_provider))
.expect("bob join via welcome");
(alice_group, alice_signer, bob_group, bob_signer)
}
fn make_emulator_group<P: OpenMlsProvider>(
ciphersuite: openmls_traits::types::Ciphersuite,
provider: &P,
label: &[u8],
) -> (MlsGroup, SignatureKeyPair) {
let (credential, signer) = new_credential(provider, label, ciphersuite.signature_algorithm());
let group_config = 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 on emulator config")
.build();
let group =
MlsGroup::new(provider, &signer, &group_config, credential).expect("create emulator group");
(group, signer)
}
fn send_vc_commit<P: OpenMlsProvider>(
sender_group: &mut MlsGroup,
emulator_group: &mut MlsGroup,
sender_provider: &P,
sender_signer: &SignatureKeyPair,
) -> (openmls::prelude::MlsMessageOut, EpochId) {
let epoch_id = emulator_group
.register_vc_emulation_epoch(sender_provider.crypto(), sender_provider.storage())
.expect("register vc epoch (sender)");
let commit = send_vc_commit_with_epoch(
sender_group,
sender_provider,
sender_signer,
epoch_id.clone(),
);
(commit, epoch_id)
}
fn send_vc_commit_with_epoch<P: OpenMlsProvider>(
sender_group: &mut MlsGroup,
sender_provider: &P,
sender_signer: &SignatureKeyPair,
epoch_id: EpochId,
) -> openmls::prelude::MlsMessageOut {
let bundle = sender_group
.commit_builder()
.vc_emulation(
sender_provider.crypto(),
sender_provider.storage(),
epoch_id,
)
.unwrap()
.load_psks(sender_provider.storage())
.unwrap()
.build(
sender_provider.rand(),
sender_provider.crypto(),
sender_signer,
|_| true,
)
.unwrap()
.stage_commit(sender_provider)
.unwrap();
sender_group
.merge_pending_commit(sender_provider)
.expect("sender merge");
bundle.into_commit()
}
fn shared_vc_identity<P: OpenMlsProvider>(
ciphersuite: openmls_traits::types::Ciphersuite,
provider_a: &P,
provider_b: &P,
) -> (SignatureKeyPair, openmls::credentials::CredentialWithKey) {
use openmls::credentials::{BasicCredential, CredentialWithKey};
let vc_signer = SignatureKeyPair::new(ciphersuite.signature_algorithm()).expect("vc signer");
vc_signer
.store(provider_a.storage())
.expect("store vc signer on alice_a");
vc_signer
.store(provider_b.storage())
.expect("store vc signer on alice_b");
let vc_credential = CredentialWithKey {
credential: BasicCredential::new(b"Alice (VC)".to_vec()).into(),
signature_key: vc_signer.public().into(),
};
(vc_signer, vc_credential)
}
fn new_vc_main_group<P: OpenMlsProvider>(
ciphersuite: openmls_traits::types::Ciphersuite,
provider: &P,
signer: &SignatureKeyPair,
credential: openmls::credentials::CredentialWithKey,
) -> MlsGroup {
let group_config = 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();
MlsGroup::new(provider, signer, &group_config, credential).expect("create vc main group")
}
fn new_vc_main_group_with_policy<P: OpenMlsProvider>(
ciphersuite: openmls_traits::types::Ciphersuite,
provider: &P,
signer: &SignatureKeyPair,
credential: openmls::credentials::CredentialWithKey,
wire_format_policy: openmls::group::WireFormatPolicy,
) -> MlsGroup {
let group_config = MlsGroupCreateConfig::builder()
.wire_format_policy(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();
MlsGroup::new(provider, signer, &group_config, credential).expect("create vc main group")
}
fn vc_join_config() -> MlsGroupJoinConfig {
MlsGroupJoinConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.build()
}
struct SiblingEmulators {
emulator_a: MlsGroup,
emulator_a_signer: SignatureKeyPair,
emulator_b: MlsGroup,
alice_b_main: MlsGroup,
epoch_id: EpochId,
}
fn join_sibling_emulator<P: OpenMlsProvider>(
emulator_ciphersuite: openmls_traits::types::Ciphersuite,
alice_a_provider: &P,
alice_b_provider: &P,
vc_signer: &SignatureKeyPair,
vc_credential: openmls::credentials::CredentialWithKey,
alice_a_main: &MlsGroup,
main_join_config: MlsGroupJoinConfig,
) -> (SiblingEmulators, openmls::prelude::MlsMessageOut) {
use openmls::prelude::{LeafNodeParameters, MlsMessageIn};
use tls_codec::Deserialize as _;
let (mut emulator_a, emulator_a_signer) =
make_emulator_group(emulator_ciphersuite, alice_a_provider, b"AliceEmulatorA");
let (emulator_b_credential, emulator_b_signer) = new_credential(
alice_b_provider,
b"AliceEmulatorB",
emulator_ciphersuite.signature_algorithm(),
);
let emulator_b_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build(
emulator_ciphersuite,
alice_b_provider,
&emulator_b_signer,
emulator_b_credential,
)
.expect("emulator_b KP build")
.key_package()
.to_owned();
let (_e_commit, e_welcome, _e_gi) = emulator_a
.add_members(alice_a_provider, &emulator_a_signer, &[emulator_b_kp])
.expect("emulator_a add alice_b");
emulator_a
.merge_pending_commit(alice_a_provider)
.expect("emulator_a merge add");
let join_config = MlsGroupJoinConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.build();
let mut emulator_b = StagedWelcome::new_from_welcome(
alice_b_provider,
&join_config,
e_welcome.into_welcome().expect("emulator welcome"),
Some(emulator_a.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(alice_b_provider))
.expect("alice_b join emulator group");
let epoch_id = emulator_a
.register_vc_emulation_epoch(alice_a_provider.crypto(), alice_a_provider.storage())
.expect("alice_a register vc epoch");
let epoch_id_b = emulator_b
.register_vc_emulation_epoch(alice_b_provider.crypto(), alice_b_provider.storage())
.expect("alice_b register vc epoch");
assert_eq!(
epoch_id, epoch_id_b,
"siblings must derive the same EpochId"
);
let verifiable_group_info = {
let group_info_msg = alice_a_main
.export_group_info(alice_a_provider.crypto(), vc_signer, true)
.expect("export group info");
let serialized = group_info_msg
.tls_serialize_detached()
.expect("serialize group info");
MlsMessageIn::tls_deserialize(&mut serialized.as_slice())
.expect("deserialize group info message")
.into_verifiable_group_info()
.expect("into verifiable group info")
};
let (alice_b_main, bundle) = MlsGroup::external_commit_builder()
.with_config(main_join_config)
.build_group(alice_b_provider, verifiable_group_info, vc_credential)
.expect("build_group")
.leaf_node_parameters(
LeafNodeParameters::builder()
.with_capabilities(vc_capabilities())
.with_extensions(vc_leaf_extensions())
.build(),
)
.vc_emulation(
alice_b_provider.crypto(),
alice_b_provider.storage(),
epoch_id.clone(),
)
.expect("vc emulation")
.load_psks(alice_b_provider.storage())
.expect("load psks")
.build(
alice_b_provider.rand(),
alice_b_provider.crypto(),
vc_signer,
|_| true,
)
.expect("build external commit")
.finalize(alice_b_provider)
.expect("finalize external commit");
(
SiblingEmulators {
emulator_a,
emulator_a_signer,
emulator_b,
alice_b_main,
epoch_id,
},
bundle.into_commit(),
)
}
fn send_and_process_app_message<P: OpenMlsProvider>(
sender: &mut MlsGroup,
sender_provider: &P,
sender_signer: &SignatureKeyPair,
receiver: &mut MlsGroup,
receiver_provider: &P,
plaintext: &[u8],
) -> openmls::prelude::ProcessedMessage {
let app_msg = sender
.create_message(sender_provider, sender_signer, plaintext)
.expect("sender creates application message");
receiver
.process_message(receiver_provider, app_msg.into_protocol_message().unwrap())
.expect("receiver processes application message")
}
#[openmls_test]
fn vc_operation_tree_persists_across_own_commits() {
let provider = Provider::default();
let (alice_credential, alice_signer) =
new_credential(&provider, b"Alice", ciphersuite.signature_algorithm());
let group_config = MlsGroupCreateConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.ciphersuite(ciphersuite)
.capabilities(vc_capabilities())
.with_leaf_node_extensions(vc_leaf_extensions())
.expect("attach leaf-node extensions")
.build();
let mut alice = MlsGroup::new(&provider, &alice_signer, &group_config, alice_credential)
.expect("create group");
let (mut emulator, _emulator_signer) =
make_emulator_group(ciphersuite, &provider, b"AliceEmulator");
let epoch_id = emulator
.register_vc_emulation_epoch(provider.crypto(), provider.storage())
.expect("register vc epoch");
let _msg1 = send_vc_commit_with_epoch(&mut alice, &provider, &alice_signer, epoch_id.clone());
let epoch_after_first = alice.epoch();
let _msg2 = send_vc_commit_with_epoch(&mut alice, &provider, &alice_signer, epoch_id);
assert_eq!(
alice.epoch().as_u64(),
epoch_after_first.as_u64() + 1,
"second VC commit on the same emulation epoch must succeed"
);
}
#[openmls_test]
fn non_emulator_processes_vc_commit_without_registering_state() {
let alice_provider = Provider::default();
let bob_provider = Provider::default();
let (mut alice, alice_signer, mut bob, _bob_signer) =
setup_alice_bob_group(ciphersuite, &alice_provider, &bob_provider);
let (mut emulator, _emulator_signer) =
make_emulator_group(ciphersuite, &alice_provider, b"AliceEmulator");
let (commit_msg, _epoch_id) =
send_vc_commit(&mut alice, &mut emulator, &alice_provider, &alice_signer);
let processed = bob
.process_message(&bob_provider, commit_msg.into_protocol_message().unwrap())
.expect("non-emulator must process VC commit via the normal HPKE path");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
_ => panic!("expected staged commit"),
};
bob.merge_staged_commit(&bob_provider, staged)
.expect("bob merge");
assert_eq!(alice.epoch(), bob.epoch());
}
#[openmls_test]
fn sibling_resync_external_commit_fails_when_receiver_lacks_operation_tree() {
use openmls::credentials::{BasicCredential, CredentialWithKey};
use openmls::prelude::{LeafNodeParameters, MlsMessageIn};
use tls_codec::Deserialize as _;
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let vc_signer = SignatureKeyPair::new(ciphersuite.signature_algorithm()).expect("vc signer");
vc_signer
.store(alice_a_provider.storage())
.expect("store vc signer on alice_a");
vc_signer
.store(alice_b_provider.storage())
.expect("store vc signer on alice_b");
let vc_credential = CredentialWithKey {
credential: BasicCredential::new(b"Alice (VC)".to_vec()).into(),
signature_key: vc_signer.public().into(),
};
let (mut emulator_a, alice_emulator_a_signer) =
make_emulator_group(ciphersuite, &alice_a_provider, b"AliceEmulatorA");
let (alice_emulator_b_credential, alice_emulator_b_signer) = new_credential(
&alice_b_provider,
b"AliceEmulatorB",
ciphersuite.signature_algorithm(),
);
let alice_emulator_b_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build(
ciphersuite,
&alice_b_provider,
&alice_emulator_b_signer,
alice_emulator_b_credential,
)
.expect("alice_b emulator KP build")
.key_package()
.to_owned();
let (_, e_welcome, _) = emulator_a
.add_members(
&alice_a_provider,
&alice_emulator_a_signer,
&[alice_emulator_b_kp],
)
.expect("emulator_a add alice_b");
emulator_a
.merge_pending_commit(&alice_a_provider)
.expect("emulator_a merge add");
let join_config = MlsGroupJoinConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.build();
let mut emulator_b = StagedWelcome::new_from_welcome(
&alice_b_provider,
&join_config,
e_welcome.into_welcome().expect("welcome"),
Some(emulator_a.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&alice_b_provider))
.expect("alice_b join emulator group");
let group_config = 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 on higher-level config")
.build();
let mut alice_a_main = MlsGroup::new(
&alice_a_provider,
&vc_signer,
&group_config,
vc_credential.clone(),
)
.expect("alice_a create higher-level group");
let epoch_id_b = emulator_b
.register_vc_emulation_epoch(alice_b_provider.crypto(), alice_b_provider.storage())
.expect("alice_b register vc epoch");
let verifiable_group_info = {
let group_info_msg = alice_a_main
.export_group_info(alice_a_provider.crypto(), &vc_signer, true)
.expect("export group info");
let serialized = group_info_msg
.tls_serialize_detached()
.expect("serialize group info");
MlsMessageIn::tls_deserialize(&mut serialized.as_slice())
.expect("deserialize group info message")
.into_verifiable_group_info()
.expect("into verifiable group info")
};
let (_alice_b_main, bundle) = MlsGroup::external_commit_builder()
.with_config(
MlsGroupJoinConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.build(),
)
.build_group(&alice_b_provider, verifiable_group_info, vc_credential)
.expect("build_group")
.leaf_node_parameters(
LeafNodeParameters::builder()
.with_capabilities(vc_capabilities())
.with_extensions(vc_leaf_extensions())
.build(),
)
.vc_emulation(
alice_b_provider.crypto(),
alice_b_provider.storage(),
epoch_id_b,
)
.expect("vc emulation")
.load_psks(alice_b_provider.storage())
.expect("load psks")
.build(
alice_b_provider.rand(),
alice_b_provider.crypto(),
&vc_signer,
|_| true,
)
.expect("build external commit")
.finalize(&alice_b_provider)
.expect("finalize external commit");
let commit_msg = bundle.into_commit();
let err = alice_a_main
.process_message(
&alice_a_provider,
commit_msg.into_protocol_message().unwrap(),
)
.expect_err("must fail without VC state");
let msg = format!("{err:?}");
assert!(
msg.contains("MissingEmulationEpochState")
|| msg.contains("MissingOperationTree")
|| msg.contains("VirtualClients"),
"expected a virtual-clients error, got {msg}"
);
}
#[openmls_test]
fn vc_two_alice_clients_in_group_with_bob_and_charly() {
use openmls::credentials::{BasicCredential, CredentialWithKey};
use openmls::prelude::{LeafNodeParameters, MlsMessageIn};
use tls_codec::Deserialize as _;
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let bob_provider = Provider::default();
let charly_provider = Provider::default();
let vc_signer = SignatureKeyPair::new(ciphersuite.signature_algorithm()).expect("vc signer");
vc_signer
.store(alice_a_provider.storage())
.expect("store vc signer on alice_a");
vc_signer
.store(alice_b_provider.storage())
.expect("store vc signer on alice_b");
let vc_credential = CredentialWithKey {
credential: BasicCredential::new(b"Alice (VC)".to_vec()).into(),
signature_key: vc_signer.public().into(),
};
let (bob_credential, bob_signer) =
new_credential(&bob_provider, b"Bob", ciphersuite.signature_algorithm());
let (charly_credential, charly_signer) = new_credential(
&charly_provider,
b"Charly",
ciphersuite.signature_algorithm(),
);
let group_config = 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 on alice main group config")
.build();
let mut alice_a_main = MlsGroup::new(
&alice_a_provider,
&vc_signer,
&group_config,
vc_credential.clone(),
)
.expect("alice create main group");
let bob_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, &bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let charly_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(
ciphersuite,
&charly_provider,
&charly_signer,
charly_credential,
)
.expect("charly KP build")
.key_package()
.to_owned();
let (_commit, welcome, _gi) = alice_a_main
.add_members(&alice_a_provider, &vc_signer, &[bob_kp, charly_kp])
.expect("alice add bob+charly");
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("alice merge add");
let join_config = MlsGroupJoinConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.build();
let welcome_msg = welcome.into_welcome().expect("welcome present");
let ratchet_tree = alice_a_main.export_ratchet_tree();
let mut bob_main = StagedWelcome::new_from_welcome(
&bob_provider,
&join_config,
welcome_msg.clone(),
Some(ratchet_tree.clone().into()),
)
.and_then(|s| s.into_group(&bob_provider))
.expect("bob join");
let mut charly_main = StagedWelcome::new_from_welcome(
&charly_provider,
&join_config,
welcome_msg,
Some(ratchet_tree.into()),
)
.and_then(|s| s.into_group(&charly_provider))
.expect("charly join");
let (mut emulator_a, alice_emulator_a_signer) =
make_emulator_group(ciphersuite, &alice_a_provider, b"AliceEmulatorA");
let (alice_emulator_b_credential, alice_emulator_b_signer) = new_credential(
&alice_b_provider,
b"AliceEmulatorB",
ciphersuite.signature_algorithm(),
);
let alice_emulator_b_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build(
ciphersuite,
&alice_b_provider,
&alice_emulator_b_signer,
alice_emulator_b_credential,
)
.expect("alice_b emulator KP build")
.key_package()
.to_owned();
let (_e_commit, e_welcome, _e_gi) = emulator_a
.add_members(
&alice_a_provider,
&alice_emulator_a_signer,
&[alice_emulator_b_kp],
)
.expect("emulator_a add alice_b");
emulator_a
.merge_pending_commit(&alice_a_provider)
.expect("emulator_a merge add");
let mut emulator_b = StagedWelcome::new_from_welcome(
&alice_b_provider,
&join_config,
e_welcome.into_welcome().expect("emulator welcome"),
Some(emulator_a.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&alice_b_provider))
.expect("alice_b join emulator group");
let epoch_id_a = emulator_a
.register_vc_emulation_epoch(alice_a_provider.crypto(), alice_a_provider.storage())
.expect("alice_a register vc epoch");
let epoch_id_b = emulator_b
.register_vc_emulation_epoch(alice_b_provider.crypto(), alice_b_provider.storage())
.expect("alice_b register vc epoch");
assert_eq!(
epoch_id_a, epoch_id_b,
"deterministic derivation must yield the same EpochId on both Alice clients"
);
let verifiable_group_info = {
let group_info_msg = alice_a_main
.export_group_info(alice_a_provider.crypto(), &vc_signer, true)
.expect("export group info");
let serialized = group_info_msg
.tls_serialize_detached()
.expect("serialize group info");
MlsMessageIn::tls_deserialize(&mut serialized.as_slice())
.expect("deserialize group info message")
.into_verifiable_group_info()
.expect("into verifiable group info")
};
let (mut alice_b_main, bundle) = MlsGroup::external_commit_builder()
.with_config(
MlsGroupJoinConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.build(),
)
.build_group(&alice_b_provider, verifiable_group_info, vc_credential)
.expect("build_group")
.leaf_node_parameters(
LeafNodeParameters::builder()
.with_capabilities(vc_capabilities())
.with_extensions(vc_leaf_extensions())
.build(),
)
.vc_emulation(
alice_b_provider.crypto(),
alice_b_provider.storage(),
epoch_id_b.clone(),
)
.expect("vc emulation")
.load_psks(alice_b_provider.storage())
.expect("load psks")
.build(
alice_b_provider.rand(),
alice_b_provider.crypto(),
&vc_signer,
|_| true,
)
.expect("build external commit")
.finalize(&alice_b_provider)
.expect("finalize external commit");
let resync_commit = bundle.into_commit();
for (group, provider) in [
(&mut alice_a_main, &alice_a_provider),
(&mut bob_main, &bob_provider),
(&mut charly_main, &charly_provider),
] {
let processed = group
.process_message(
provider,
resync_commit.clone().into_protocol_message().unwrap(),
)
.expect("process resync commit");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
_ => panic!("expected staged commit"),
};
group
.merge_staged_commit(provider, staged)
.expect("merge resync");
}
fn assert_all_agree(groups: &[&MlsGroup], label: &str) {
let mut iter = groups.iter();
let reference = iter
.next()
.expect("at least one party")
.epoch_authenticator();
for group in iter {
assert_eq!(
group.epoch_authenticator(),
reference,
"epoch authenticator divergence at {label}"
);
}
}
let baseline_epoch = alice_a_main.epoch();
assert_all_agree(
&[&alice_a_main, &alice_b_main, &bob_main, &charly_main],
"post resync",
);
fn deliver_commit<P: OpenMlsProvider>(
receiver: &mut MlsGroup,
provider: &P,
commit_msg: &openmls::prelude::MlsMessageOut,
) {
let processed = receiver
.process_message(
provider,
commit_msg.clone().into_protocol_message().unwrap(),
)
.expect("process commit");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
_ => panic!("expected staged commit"),
};
receiver
.merge_staged_commit(provider, staged)
.expect("merge staged commit");
}
fn build_regular_commit<P: OpenMlsProvider>(
sender_group: &mut MlsGroup,
provider: &P,
signer: &SignatureKeyPair,
) -> openmls::prelude::MlsMessageOut {
let bundle = sender_group
.commit_builder()
.force_self_update(true)
.load_psks(provider.storage())
.expect("load psks")
.build(provider.rand(), provider.crypto(), signer, |_| true)
.expect("build commit")
.stage_commit(provider)
.expect("stage commit");
sender_group
.merge_pending_commit(provider)
.expect("sender merge");
bundle.into_commit()
}
let bob_commit = build_regular_commit(&mut bob_main, &bob_provider, &bob_signer);
deliver_commit(&mut alice_a_main, &alice_a_provider, &bob_commit);
deliver_commit(&mut alice_b_main, &alice_b_provider, &bob_commit);
deliver_commit(&mut charly_main, &charly_provider, &bob_commit);
assert_all_agree(
&[&alice_a_main, &alice_b_main, &bob_main, &charly_main],
"post Bob commit",
);
let charly_commit = build_regular_commit(&mut charly_main, &charly_provider, &charly_signer);
deliver_commit(&mut alice_a_main, &alice_a_provider, &charly_commit);
deliver_commit(&mut alice_b_main, &alice_b_provider, &charly_commit);
deliver_commit(&mut bob_main, &bob_provider, &charly_commit);
assert_all_agree(
&[&alice_a_main, &alice_b_main, &bob_main, &charly_main],
"post Charly commit",
);
let alice_a_vc_commit = send_vc_commit_with_epoch(
&mut alice_a_main,
&alice_a_provider,
&vc_signer,
epoch_id_a.clone(),
);
deliver_commit(&mut alice_b_main, &alice_b_provider, &alice_a_vc_commit);
deliver_commit(&mut bob_main, &bob_provider, &alice_a_vc_commit);
deliver_commit(&mut charly_main, &charly_provider, &alice_a_vc_commit);
assert_all_agree(
&[&alice_a_main, &alice_b_main, &bob_main, &charly_main],
"post alice_a VC commit",
);
let alice_b_vc_commit = send_vc_commit_with_epoch(
&mut alice_b_main,
&alice_b_provider,
&vc_signer,
epoch_id_b.clone(),
);
deliver_commit(&mut alice_a_main, &alice_a_provider, &alice_b_vc_commit);
deliver_commit(&mut bob_main, &bob_provider, &alice_b_vc_commit);
deliver_commit(&mut charly_main, &charly_provider, &alice_b_vc_commit);
assert_all_agree(
&[&alice_a_main, &alice_b_main, &bob_main, &charly_main],
"post alice_b VC commit",
);
let alice_a_second_vc_commit = send_vc_commit_with_epoch(
&mut alice_a_main,
&alice_a_provider,
&vc_signer,
epoch_id_a.clone(),
);
deliver_commit(
&mut alice_b_main,
&alice_b_provider,
&alice_a_second_vc_commit,
);
deliver_commit(&mut bob_main, &bob_provider, &alice_a_second_vc_commit);
deliver_commit(
&mut charly_main,
&charly_provider,
&alice_a_second_vc_commit,
);
assert_all_agree(
&[&alice_a_main, &alice_b_main, &bob_main, &charly_main],
"post alice_a second VC commit",
);
assert_eq!(
alice_a_main.epoch().as_u64(),
baseline_epoch.as_u64() + 5,
"expected five-epoch advance across the five commits"
);
}
#[openmls_test]
fn vc_sibling_emulator_resyncs_into_higher_level_group_via_external_commit() {
use openmls::credentials::{BasicCredential, CredentialWithKey};
use openmls::prelude::{LeafNodeParameters, MlsMessageIn};
use tls_codec::Deserialize as _;
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let bob_provider = Provider::default();
let vc_signer = SignatureKeyPair::new(ciphersuite.signature_algorithm()).expect("vc signer");
vc_signer
.store(alice_a_provider.storage())
.expect("store vc signer on alice_a");
vc_signer
.store(alice_b_provider.storage())
.expect("store vc signer on alice_b");
let vc_credential = CredentialWithKey {
credential: BasicCredential::new(b"Alice (VC)".to_vec()).into(),
signature_key: vc_signer.public().into(),
};
let (mut emulator_a, alice_emulator_a_signer) =
make_emulator_group(ciphersuite, &alice_a_provider, b"AliceEmulatorA");
let (alice_emulator_b_credential, alice_emulator_b_signer) = new_credential(
&alice_b_provider,
b"AliceEmulatorB",
ciphersuite.signature_algorithm(),
);
let alice_emulator_b_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build(
ciphersuite,
&alice_b_provider,
&alice_emulator_b_signer,
alice_emulator_b_credential,
)
.expect("alice_b emulator KP build")
.key_package()
.to_owned();
let (_, e_welcome, _) = emulator_a
.add_members(
&alice_a_provider,
&alice_emulator_a_signer,
&[alice_emulator_b_kp],
)
.expect("emulator_a add alice_b");
emulator_a
.merge_pending_commit(&alice_a_provider)
.expect("emulator_a merge add");
let join_config = MlsGroupJoinConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.build();
let mut emulator_b = StagedWelcome::new_from_welcome(
&alice_b_provider,
&join_config,
e_welcome.into_welcome().expect("welcome"),
Some(emulator_a.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&alice_b_provider))
.expect("alice_b join emulator group");
let group_config = 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 on higher-level config")
.build();
let mut alice_a_main = MlsGroup::new(
&alice_a_provider,
&vc_signer,
&group_config,
vc_credential.clone(),
)
.expect("alice_a create higher-level group");
let (bob_credential, bob_signer) =
new_credential(&bob_provider, b"Bob", ciphersuite.signature_algorithm());
let bob_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, &bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_, welcome, _) = alice_a_main
.add_members(&alice_a_provider, &vc_signer, &[bob_kp])
.expect("alice_a add bob");
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("alice_a merge add");
let mut bob_main = StagedWelcome::new_from_welcome(
&bob_provider,
&join_config,
welcome.into_welcome().expect("welcome"),
Some(alice_a_main.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&bob_provider))
.expect("bob join higher-level group");
let epoch_id_a = emulator_a
.register_vc_emulation_epoch(alice_a_provider.crypto(), alice_a_provider.storage())
.expect("alice_a register vc epoch");
let epoch_id_b = emulator_b
.register_vc_emulation_epoch(alice_b_provider.crypto(), alice_b_provider.storage())
.expect("alice_b register vc epoch");
assert_eq!(
epoch_id_a, epoch_id_b,
"deterministic derivation must yield the same EpochId on both Alice clients"
);
let verifiable_group_info = {
let group_info_msg = alice_a_main
.export_group_info(alice_a_provider.crypto(), &vc_signer, true)
.expect("export group info");
let serialized = group_info_msg
.tls_serialize_detached()
.expect("serialize group info");
MlsMessageIn::tls_deserialize(&mut serialized.as_slice())
.expect("deserialize group info message")
.into_verifiable_group_info()
.expect("into verifiable group info")
};
let (mut alice_b_main, bundle) = MlsGroup::external_commit_builder()
.with_config(
MlsGroupJoinConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.build(),
)
.build_group(&alice_b_provider, verifiable_group_info, vc_credential)
.expect("build_group")
.leaf_node_parameters(
LeafNodeParameters::builder()
.with_capabilities(vc_capabilities())
.with_extensions(vc_leaf_extensions())
.build(),
)
.vc_emulation(
alice_b_provider.crypto(),
alice_b_provider.storage(),
epoch_id_b.clone(),
)
.expect("vc emulation")
.load_psks(alice_b_provider.storage())
.expect("load psks")
.build(
alice_b_provider.rand(),
alice_b_provider.crypto(),
&vc_signer,
|_| true,
)
.expect("build external commit")
.finalize(&alice_b_provider)
.expect("finalize external commit");
let commit_msg = bundle.into_commit();
let new_leaf_index = alice_b_main.own_leaf_index();
let new_epoch = alice_b_main.epoch();
{
let processed = alice_a_main
.process_message(
&alice_a_provider,
commit_msg.clone().into_protocol_message().unwrap(),
)
.expect("alice_a process resync external commit");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
_ => panic!("expected staged commit"),
};
assert!(
!staged.self_removed(),
"sibling-resync external commit must not mark alice_a as self-removed"
);
alice_a_main
.merge_staged_commit(&alice_a_provider, staged)
.expect("alice_a merge resync");
}
assert!(alice_a_main.is_active(), "alice_a must stay active");
assert_eq!(alice_a_main.epoch(), new_epoch);
assert_eq!(
alice_a_main.own_leaf_index(),
new_leaf_index,
"alice_a's own_leaf_index must point to the joiner's new leaf"
);
assert_eq!(
alice_a_main.epoch_authenticator(),
alice_b_main.epoch_authenticator(),
"alice_a and alice_b must agree on the epoch authenticator"
);
{
let processed = bob_main
.process_message(&bob_provider, commit_msg.into_protocol_message().unwrap())
.expect("bob process resync external commit");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
_ => panic!("expected staged commit"),
};
bob_main
.merge_staged_commit(&bob_provider, staged)
.expect("bob merge resync");
}
assert_eq!(
bob_main.epoch_authenticator(),
alice_a_main.epoch_authenticator()
);
let mut reloaded_alice_a = MlsGroup::load(alice_a_provider.storage(), alice_a_main.group_id())
.expect("reload alice_a after resync")
.expect("alice_a group present after resync");
assert_eq!(
reloaded_alice_a.own_leaf_index(),
new_leaf_index,
"resync must persist alice_a's new own_leaf_index"
);
let alice_a_app = reloaded_alice_a
.create_message(&alice_a_provider, &vc_signer, b"alice_a after resync")
.expect("alice_a create app message after resync");
for (group, provider) in [
(&mut alice_b_main, &alice_b_provider),
(&mut bob_main, &bob_provider),
] {
let processed = group
.process_message(
provider,
alice_a_app.clone().into_protocol_message().unwrap(),
)
.expect("process alice_a app message after resync");
let ProcessedMessageContent::ApplicationMessage(message) = processed.into_content() else {
panic!("expected application message");
};
assert_eq!(message.into_bytes(), b"alice_a after resync");
}
let alice_b_followup = send_vc_commit_with_epoch(
&mut alice_b_main,
&alice_b_provider,
&vc_signer,
epoch_id_b.clone(),
);
for (group, provider) in [
(&mut alice_a_main, &alice_a_provider),
(&mut bob_main, &bob_provider),
] {
let processed = group
.process_message(
provider,
alice_b_followup.clone().into_protocol_message().unwrap(),
)
.expect("process alice_b followup");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
_ => panic!("expected staged commit"),
};
group
.merge_staged_commit(provider, staged)
.expect("merge alice_b followup");
}
assert_eq!(
alice_a_main.epoch_authenticator(),
alice_b_main.epoch_authenticator()
);
assert_eq!(
bob_main.epoch_authenticator(),
alice_a_main.epoch_authenticator()
);
let bob_followup = {
let bundle = bob_main
.commit_builder()
.force_self_update(true)
.load_psks(bob_provider.storage())
.expect("load psks")
.build(
bob_provider.rand(),
bob_provider.crypto(),
&bob_signer,
|_| true,
)
.expect("build")
.stage_commit(&bob_provider)
.expect("stage");
bob_main
.merge_pending_commit(&bob_provider)
.expect("bob merge own");
bundle.into_commit()
};
for (group, provider) in [
(&mut alice_a_main, &alice_a_provider),
(&mut alice_b_main, &alice_b_provider),
] {
let processed = group
.process_message(
provider,
bob_followup.clone().into_protocol_message().unwrap(),
)
.expect("process bob followup");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
_ => panic!("expected staged commit"),
};
group
.merge_staged_commit(provider, staged)
.expect("merge bob followup");
}
assert_eq!(
alice_a_main.epoch_authenticator(),
alice_b_main.epoch_authenticator()
);
assert_eq!(
bob_main.epoch_authenticator(),
alice_a_main.epoch_authenticator()
);
}
#[openmls_test]
fn vc_second_emulator_client_onboards_via_external_commit() {
use openmls::credentials::{BasicCredential, CredentialWithKey};
use openmls::prelude::{LeafNodeParameters, MlsMessageIn};
use tls_codec::Deserialize as _;
let alice_provider = Provider::default();
let bob_provider = Provider::default();
let charly_a_provider = Provider::default();
let charly_b_provider = Provider::default();
let (alice_credential, alice_signer) =
new_credential(&alice_provider, b"Alice", ciphersuite.signature_algorithm());
let mut alice_main = new_vc_main_group(
ciphersuite,
&alice_provider,
&alice_signer,
alice_credential,
);
let (bob_credential, bob_signer) =
new_credential(&bob_provider, b"Bob", ciphersuite.signature_algorithm());
let bob_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, &bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_, welcome, _) = alice_main
.add_members(&alice_provider, &alice_signer, &[bob_kp])
.expect("alice add bob");
alice_main
.merge_pending_commit(&alice_provider)
.expect("alice merge add bob");
let mut bob_main = StagedWelcome::new_from_welcome(
&bob_provider,
&vc_join_config(),
welcome.into_welcome().expect("welcome"),
Some(alice_main.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&bob_provider))
.expect("bob join higher-level group");
let vc_signer = SignatureKeyPair::new(ciphersuite.signature_algorithm()).expect("vc signer");
vc_signer
.store(charly_a_provider.storage())
.expect("store vc signer on charly_a");
vc_signer
.store(charly_b_provider.storage())
.expect("store vc signer on charly_b");
let vc_credential = CredentialWithKey {
credential: BasicCredential::new(b"Charly (VC)".to_vec()).into(),
signature_key: vc_signer.public().into(),
};
let (mut emulator_a, emulator_a_signer) =
make_emulator_group(ciphersuite, &charly_a_provider, b"CharlyEmulatorA");
let (emulator_b_credential, emulator_b_signer) = new_credential(
&charly_b_provider,
b"CharlyEmulatorB",
ciphersuite.signature_algorithm(),
);
let emulator_b_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build(
ciphersuite,
&charly_b_provider,
&emulator_b_signer,
emulator_b_credential,
)
.expect("charly_b emulator KP build")
.key_package()
.to_owned();
let (_, e_welcome, _) = emulator_a
.add_members(&charly_a_provider, &emulator_a_signer, &[emulator_b_kp])
.expect("emulator_a add charly_b");
emulator_a
.merge_pending_commit(&charly_a_provider)
.expect("emulator_a merge add");
let mut emulator_b = StagedWelcome::new_from_welcome(
&charly_b_provider,
&vc_join_config(),
e_welcome.into_welcome().expect("emulator welcome"),
Some(emulator_a.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&charly_b_provider))
.expect("charly_b join emulator group");
let epoch_id = emulator_a
.register_vc_emulation_epoch(charly_a_provider.crypto(), charly_a_provider.storage())
.expect("charly_a register vc epoch");
let epoch_id_b = emulator_b
.register_vc_emulation_epoch(charly_b_provider.crypto(), charly_b_provider.storage())
.expect("charly_b register vc epoch");
assert_eq!(
epoch_id, epoch_id_b,
"siblings must derive the same EpochId"
);
let export_prior_epoch_vgi = |label: &str| {
let gi = alice_main
.export_group_info(alice_provider.crypto(), &alice_signer, true)
.unwrap_or_else(|_| panic!("export group info ({label})"));
let serialized = gi.tls_serialize_detached().expect("serialize gi");
MlsMessageIn::tls_deserialize(&mut serialized.as_slice())
.expect("deserialize gi")
.into_verifiable_group_info()
.expect("into vgi")
};
let vgi_charly_a = export_prior_epoch_vgi("charly_a");
let vgi_charly_b = export_prior_epoch_vgi("charly_b");
let pre_join_message = alice_main
.create_message(&alice_provider, &alice_signer, b"before charly joins")
.expect("alice create pre-join app message");
let (charly_a_main, bundle) = MlsGroup::external_commit_builder()
.with_config(vc_join_config())
.build_group(&charly_a_provider, vgi_charly_a, vc_credential.clone())
.expect("build_group charly_a")
.leaf_node_parameters(
LeafNodeParameters::builder()
.with_capabilities(vc_capabilities())
.with_extensions(vc_leaf_extensions())
.build(),
)
.vc_emulation(
charly_a_provider.crypto(),
charly_a_provider.storage(),
epoch_id.clone(),
)
.expect("vc emulation charly_a")
.load_psks(charly_a_provider.storage())
.expect("load psks")
.build(
charly_a_provider.rand(),
charly_a_provider.crypto(),
&vc_signer,
|_| true,
)
.expect("build external commit charly_a")
.finalize(&charly_a_provider)
.expect("finalize charly_a join");
let charly_a_join = bundle.into_commit();
let charly_a_commit_for_b = charly_a_join
.clone()
.into_protocol_message()
.expect("charly_a commit as protocol message");
process_and_merge_commit(&mut alice_main, &alice_provider, charly_a_join.clone());
process_and_merge_commit(&mut bob_main, &bob_provider, charly_a_join);
let auth_after_join = charly_a_main.epoch_authenticator();
assert_eq!(alice_main.epoch_authenticator(), auth_after_join);
assert_eq!(bob_main.epoch_authenticator(), auth_after_join);
let bootstrap_join_config = MlsGroupJoinConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.max_past_epochs(1)
.build();
let mut charly_b_main = MlsGroup::vc_join_via_sibling_external_commit(
&charly_b_provider,
&bootstrap_join_config,
vgi_charly_b,
None,
charly_a_commit_for_b,
epoch_id_b.clone(),
)
.expect("charly_b bootstraps by processing charly_a's external commit");
let err = charly_b_main
.process_message(
&charly_b_provider,
pre_join_message.into_protocol_message().unwrap(),
)
.expect_err("bootstrapped sibling must not retain fake prior-epoch secrets");
let ProcessMessageError::ValidationError(ValidationError::UnableToDecrypt(
MessageDecryptionError::SecretTreeError(SecretTreeError::TooDistantInThePast),
)) = err
else {
panic!("expected no usable past epoch secret for pre-bootstrap message");
};
assert!(charly_a_main.is_active() && charly_b_main.is_active());
assert_eq!(
charly_b_main.own_leaf_index(),
charly_a_main.own_leaf_index(),
"the bootstrapped sibling must land on the shared VC leaf"
);
let auth = charly_b_main.epoch_authenticator();
assert_eq!(charly_a_main.epoch_authenticator(), auth);
assert_eq!(alice_main.epoch_authenticator(), auth);
assert_eq!(bob_main.epoch_authenticator(), auth);
let alice_app = alice_main
.create_message(&alice_provider, &alice_signer, b"hello charly_b")
.expect("alice create app message");
let processed = charly_b_main
.process_message(
&charly_b_provider,
alice_app.into_protocol_message().unwrap(),
)
.expect("charly_b processes alice's app message");
let ProcessedMessageContent::ApplicationMessage(message) = processed.into_content() else {
panic!("expected application message");
};
assert_eq!(message.into_bytes(), b"hello charly_b");
let charly_b_app = charly_b_main
.create_message(&charly_b_provider, &vc_signer, b"charly_b bootstrapped")
.expect("charly_b create app message");
for (group, provider) in [
(&mut alice_main, &alice_provider),
(&mut bob_main, &bob_provider),
] {
let processed = group
.process_message(
provider,
charly_b_app.clone().into_protocol_message().unwrap(),
)
.expect("process charly_b app message");
let ProcessedMessageContent::ApplicationMessage(message) = processed.into_content() else {
panic!("expected application message");
};
assert_eq!(message.into_bytes(), b"charly_b bootstrapped");
}
}
#[openmls_test]
fn vc_sibling_joins_higher_level_group_via_key_package_welcome() {
use openmls::components::vc_derivation_info::{
assemble_vc_key_package_upload, process_vc_key_package_upload,
};
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let bob_provider = Provider::default();
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let (mut emulator_a, emulator_a_signer) =
make_emulator_group(ciphersuite, &alice_a_provider, b"AliceEmulatorA");
let (emulator_b_credential, emulator_b_signer) = new_credential(
&alice_b_provider,
b"AliceEmulatorB",
ciphersuite.signature_algorithm(),
);
let emulator_b_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build(
ciphersuite,
&alice_b_provider,
&emulator_b_signer,
emulator_b_credential,
)
.expect("emulator_b KP build")
.key_package()
.to_owned();
let (_e_commit, e_welcome, _e_gi) = emulator_a
.add_members(&alice_a_provider, &emulator_a_signer, &[emulator_b_kp])
.expect("emulator_a add alice_b");
emulator_a
.merge_pending_commit(&alice_a_provider)
.expect("emulator_a merge add");
let mut emulator_b = StagedWelcome::new_from_welcome(
&alice_b_provider,
&vc_join_config(),
e_welcome.into_welcome().expect("emulator welcome"),
Some(emulator_a.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&alice_b_provider))
.expect("alice_b join emulator group");
let epoch_id_a = emulator_a
.register_vc_emulation_epoch(alice_a_provider.crypto(), alice_a_provider.storage())
.expect("alice_a register vc epoch");
let epoch_id_b = emulator_b
.register_vc_emulation_epoch(alice_b_provider.crypto(), alice_b_provider.storage())
.expect("alice_b register vc epoch");
assert_eq!(
epoch_id_a, epoch_id_b,
"siblings must derive the same EpochId"
);
let mut batch = KeyPackage::builder()
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build_vc_batch(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential.clone(),
epoch_id_a.clone(),
1,
)
.expect("alice_a build_vc_batch");
let generation = batch.generation;
let (vc_key_package_bundle, kp_info) = batch.key_packages.remove(0);
let upload = assemble_vc_key_package_upload(
alice_a_provider.storage(),
epoch_id_a.clone(),
generation,
vec![kp_info],
)
.expect("assemble upload");
process_vc_key_package_upload(&alice_b_provider, &upload).expect("alice_b process upload");
let (bob_credential, bob_signer) =
new_credential(&bob_provider, b"Bob", ciphersuite.signature_algorithm());
let bob_group_config = MlsGroupCreateConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.ciphersuite(ciphersuite)
.use_ratchet_tree_extension(true)
.build();
let mut bob_main = MlsGroup::new(
&bob_provider,
&bob_signer,
&bob_group_config,
bob_credential,
)
.expect("bob create higher-level group");
let (_commit, welcome, _gi) = bob_main
.add_members(
&bob_provider,
&bob_signer,
&[vc_key_package_bundle.key_package().clone()],
)
.expect("bob add virtual client");
bob_main
.merge_pending_commit(&bob_provider)
.expect("bob merge add");
let ratchet_tree = bob_main.export_ratchet_tree();
let processed = openmls::group::ProcessedWelcome::new_from_welcome(
&alice_b_provider,
&vc_join_config(),
welcome.into_welcome().expect("welcome present"),
)
.expect("alice_b process welcome");
let mut alice_b_main = processed
.into_staged_welcome(&alice_b_provider, Some(ratchet_tree.into()))
.expect("alice_b stage welcome")
.into_group(&alice_b_provider)
.expect("alice_b join higher-level group");
let vc_signature_key = vc_signer.public().to_vec();
let own_member = alice_b_main
.members()
.find(|m| m.index == alice_b_main.own_leaf_index())
.expect("own member present");
assert_eq!(
own_member.signature_key, vc_signature_key,
"alice_b's joined leaf must carry the virtual client's signature key"
);
assert_eq!(
bob_main.epoch_authenticator(),
alice_b_main.epoch_authenticator(),
"bob and the joined virtual client must agree on the epoch"
);
let to_vc = send_and_process_app_message(
&mut bob_main,
&bob_provider,
&bob_signer,
&mut alice_b_main,
&alice_b_provider,
b"hello virtual client",
);
match to_vc.into_content() {
ProcessedMessageContent::ApplicationMessage(msg) => {
assert_eq!(msg.into_bytes().as_slice(), b"hello virtual client");
}
_ => panic!("expected application message from bob"),
}
let from_vc = send_and_process_app_message(
&mut alice_b_main,
&alice_b_provider,
&vc_signer,
&mut bob_main,
&bob_provider,
b"hello bob",
);
match from_vc.into_content() {
ProcessedMessageContent::ApplicationMessage(msg) => {
assert_eq!(msg.into_bytes().as_slice(), b"hello bob");
}
_ => panic!("expected application message from virtual client"),
}
}
#[openmls_test]
fn vc_batch_key_packages_join_in_any_order() {
use openmls::components::vc_derivation_info::{
assemble_vc_key_package_upload, process_vc_key_package_upload,
};
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let (mut emulator_a, emulator_a_signer) =
make_emulator_group(ciphersuite, &alice_a_provider, b"AliceEmulatorA");
let (emulator_b_credential, emulator_b_signer) = new_credential(
&alice_b_provider,
b"AliceEmulatorB",
ciphersuite.signature_algorithm(),
);
let emulator_b_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build(
ciphersuite,
&alice_b_provider,
&emulator_b_signer,
emulator_b_credential,
)
.expect("emulator_b KP build")
.key_package()
.to_owned();
let (_e_commit, e_welcome, _e_gi) = emulator_a
.add_members(&alice_a_provider, &emulator_a_signer, &[emulator_b_kp])
.expect("emulator_a add alice_b");
emulator_a
.merge_pending_commit(&alice_a_provider)
.expect("emulator_a merge add");
let mut emulator_b = StagedWelcome::new_from_welcome(
&alice_b_provider,
&vc_join_config(),
e_welcome.into_welcome().expect("emulator welcome"),
Some(emulator_a.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&alice_b_provider))
.expect("alice_b join emulator group");
let epoch_id_a = emulator_a
.register_vc_emulation_epoch(alice_a_provider.crypto(), alice_a_provider.storage())
.expect("alice_a register vc epoch");
let epoch_id_b = emulator_b
.register_vc_emulation_epoch(alice_b_provider.crypto(), alice_b_provider.storage())
.expect("alice_b register vc epoch");
assert_eq!(
epoch_id_a, epoch_id_b,
"siblings must derive the same EpochId"
);
let count = 40;
let batch = KeyPackage::builder()
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build_vc_batch(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential.clone(),
epoch_id_a.clone(),
count,
)
.expect("alice_a build_vc_batch");
let generation = batch.generation;
assert_eq!(generation, 0, "the batch consumes a single generation");
assert_eq!(batch.key_packages.len(), count);
let kp_infos = batch
.key_packages
.iter()
.map(|(_bundle, info)| info)
.map(
|info| openmls::components::vc_derivation_info::KeyPackageInfo {
key_package_ref: info.key_package_ref.clone(),
cipher_suite: info.cipher_suite,
key_package_index: info.key_package_index,
},
)
.collect::<Vec<_>>();
let upload = assemble_vc_key_package_upload(
alice_a_provider.storage(),
epoch_id_a.clone(),
generation,
kp_infos,
)
.expect("assemble upload");
process_vc_key_package_upload(&alice_b_provider, &upload).expect("alice_b process upload");
let high_bundle = batch.key_packages[count - 1].0.key_package().clone();
let low_bundle = batch.key_packages[0].0.key_package().clone();
for (label, kp) in [
(b"BobHigh".as_slice(), high_bundle),
(b"BobLow".as_slice(), low_bundle),
] {
let bob_provider = Provider::default();
let (bob_credential, bob_signer) =
new_credential(&bob_provider, label, ciphersuite.signature_algorithm());
let bob_group_config = MlsGroupCreateConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.ciphersuite(ciphersuite)
.use_ratchet_tree_extension(true)
.build();
let mut bob_main = MlsGroup::new(
&bob_provider,
&bob_signer,
&bob_group_config,
bob_credential,
)
.expect("bob create higher-level group");
let (_commit, welcome, _gi) = bob_main
.add_members(&bob_provider, &bob_signer, &[kp])
.expect("bob add virtual client");
bob_main
.merge_pending_commit(&bob_provider)
.expect("bob merge add");
let ratchet_tree = bob_main.export_ratchet_tree();
let processed = openmls::group::ProcessedWelcome::new_from_welcome(
&alice_b_provider,
&vc_join_config(),
welcome.into_welcome().expect("welcome present"),
)
.expect("alice_b process welcome");
let alice_b_main = processed
.into_staged_welcome(&alice_b_provider, Some(ratchet_tree.into()))
.expect("alice_b stage welcome")
.into_group(&alice_b_provider)
.expect("alice_b join higher-level group");
assert_eq!(
bob_main.epoch_authenticator(),
alice_b_main.epoch_authenticator(),
"bob and the joined virtual client must agree on the epoch"
);
}
}
#[openmls_test::openmls_test]
fn processing_own_application_message() {
let alice_provider = &Provider::default();
let (alice_credential, alice_signer) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let mut alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.build(alice_provider, &alice_signer, alice_credential.clone())
.expect("An unexpected error occurred.");
let alice_message = b"Hello, this is Alice!";
let unconfirmed = alice_group
.create_unconfirmed_message(alice_provider, &alice_signer, alice_message)
.unwrap();
assert!(
unconfirmed.generation_id.is_none(),
"a group with no emulation binding must not produce a generation id"
);
let ciphertext = unconfirmed.message;
let processed_message = alice_group
.process_message(
alice_provider,
ciphertext.clone().into_protocol_message().unwrap(),
)
.unwrap();
let ProcessedMessageContent::ApplicationMessage(msg) = processed_message.into_content() else {
panic!("Expected an application message.");
};
assert!(alice_message.as_slice() == msg.into_bytes().as_slice());
let processed_message = alice_group
.process_message(alice_provider, ciphertext.into_protocol_message().unwrap())
.expect("Expected processing the same message again to succeed.");
assert!(matches!(
processed_message.into_content(),
ProcessedMessageContent::OwnPrivateMessage
));
let alice_message = b"Hello, this is Alice again!";
let unconfirmed = alice_group
.create_unconfirmed_message(alice_provider, &alice_signer, alice_message)
.unwrap();
let ciphertext = unconfirmed.message;
alice_group
.confirm_application_message(
alice_provider.storage(),
unconfirmed.epoch,
unconfirmed.generation,
)
.unwrap();
let processed_message = alice_group
.process_message(
alice_provider,
ciphertext.clone().into_protocol_message().unwrap(),
)
.expect("Expected processing a confirmed message to succeed.");
assert!(matches!(
processed_message.into_content(),
ProcessedMessageContent::OwnPrivateMessage
));
}
#[openmls_test::openmls_test]
fn confirm_targets_creation_epoch() {
let alice_provider = &Provider::default();
let bob_provider = &Provider::default();
let (alice_credential, alice_signer) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let (bob_credential, bob_signer) =
new_credential(bob_provider, b"Bob", ciphersuite.signature_algorithm());
let mut alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.max_past_epochs(1)
.build(alice_provider, &alice_signer, alice_credential)
.expect("alice create group");
let bob_key_package = KeyPackage::builder()
.build(ciphersuite, bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_commit, welcome, _gi) = alice_group
.add_members(alice_provider, &alice_signer, &[bob_key_package])
.expect("alice add bob");
alice_group
.merge_pending_commit(alice_provider)
.expect("alice merge add");
let mut bob_group = StagedWelcome::new_from_welcome(
bob_provider,
&MlsGroupJoinConfig::default(),
welcome.into_welcome().unwrap(),
Some(alice_group.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(bob_provider))
.expect("bob join");
let msg1 = alice_group
.create_unconfirmed_message(alice_provider, &alice_signer, b"epoch N message")
.expect("create msg1");
let bob_commit = bob_group
.self_update(bob_provider, &bob_signer, LeafNodeParameters::default())
.expect("bob self-update")
.into_commit();
bob_group
.merge_pending_commit(bob_provider)
.expect("bob merge self-update");
let processed = alice_group
.process_message(alice_provider, bob_commit.into_protocol_message().unwrap())
.expect("alice process bob commit");
let ProcessedMessageContent::StagedCommitMessage(staged) = processed.into_content() else {
panic!("expected a staged commit");
};
alice_group
.merge_staged_commit(alice_provider, *staged)
.expect("alice merge bob commit");
let msg2 = alice_group
.create_unconfirmed_message(alice_provider, &alice_signer, b"epoch N+1 message")
.expect("create msg2");
assert_ne!(
msg1.epoch, msg2.epoch,
"the commit must have advanced the epoch"
);
assert_eq!(
msg1.generation, msg2.generation,
"both are the first application send of their epoch"
);
alice_group
.confirm_application_message(alice_provider.storage(), msg1.epoch, msg1.generation)
.expect("confirm msg1");
let processed = alice_group
.process_message(
alice_provider,
msg2.message.into_protocol_message().unwrap(),
)
.expect("process msg2 echo");
let ProcessedMessageContent::ApplicationMessage(app) = processed.into_content() else {
panic!("expected msg2 to decrypt to an application message");
};
assert_eq!(app.into_bytes().as_slice(), b"epoch N+1 message");
let processed = alice_group
.process_message(
alice_provider,
msg1.message.into_protocol_message().unwrap(),
)
.expect("process msg1 echo");
assert!(matches!(
processed.into_content(),
ProcessedMessageContent::OwnPrivateMessage
));
}
#[openmls_test::openmls_test]
fn confirm_aged_out_epoch_is_noop() {
let alice_provider = &Provider::default();
let bob_provider = &Provider::default();
let (alice_credential, alice_signer) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let (bob_credential, bob_signer) =
new_credential(bob_provider, b"Bob", ciphersuite.signature_algorithm());
let mut alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.build(alice_provider, &alice_signer, alice_credential)
.expect("alice create group");
let bob_key_package = KeyPackage::builder()
.build(ciphersuite, bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_commit, welcome, _gi) = alice_group
.add_members(alice_provider, &alice_signer, &[bob_key_package])
.expect("alice add bob");
alice_group
.merge_pending_commit(alice_provider)
.expect("alice merge add");
let mut bob_group = StagedWelcome::new_from_welcome(
bob_provider,
&MlsGroupJoinConfig::default(),
welcome.into_welcome().unwrap(),
Some(alice_group.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(bob_provider))
.expect("bob join");
let msg = alice_group
.create_unconfirmed_message(alice_provider, &alice_signer, b"epoch N message")
.expect("create msg");
let bob_commit = bob_group
.self_update(bob_provider, &bob_signer, LeafNodeParameters::default())
.expect("bob self-update")
.into_commit();
bob_group
.merge_pending_commit(bob_provider)
.expect("bob merge self-update");
let processed = alice_group
.process_message(alice_provider, bob_commit.into_protocol_message().unwrap())
.expect("alice process bob commit");
let ProcessedMessageContent::StagedCommitMessage(staged) = processed.into_content() else {
panic!("expected a staged commit");
};
alice_group
.merge_staged_commit(alice_provider, *staged)
.expect("alice merge bob commit");
alice_group
.confirm_application_message(alice_provider.storage(), msg.epoch, msg.generation)
.expect("confirming an aged-out epoch must be a no-op success");
}
#[openmls_test::openmls_test]
fn confirm_after_processing_own_echo_is_noop() {
let alice_provider = &Provider::default();
let (alice_credential, alice_signer) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let mut alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.build(alice_provider, &alice_signer, alice_credential)
.expect("alice create group");
let unconfirmed = alice_group
.create_unconfirmed_message(alice_provider, &alice_signer, b"echo me")
.expect("create unconfirmed message");
let epoch = unconfirmed.epoch;
let generation = unconfirmed.generation;
let processed = alice_group
.process_message(
alice_provider,
unconfirmed.message.into_protocol_message().unwrap(),
)
.expect("process own echo");
let ProcessedMessageContent::ApplicationMessage(app) = processed.into_content() else {
panic!("expected the own echo to decrypt");
};
assert_eq!(app.into_bytes().as_slice(), b"echo me");
alice_group
.confirm_application_message(alice_provider.storage(), epoch, generation)
.expect("confirming an already-consumed generation must be a no-op success");
}
#[openmls_test::openmls_test]
fn confirm_future_epoch_errors() {
let alice_provider = &Provider::default();
let (alice_credential, alice_signer) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let mut alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.build(alice_provider, &alice_signer, alice_credential)
.expect("alice create group");
let future_epoch = GroupEpoch::from(alice_group.epoch().as_u64() + 1);
let err = alice_group
.confirm_application_message(alice_provider.storage(), future_epoch, 0)
.expect_err("confirming a future epoch must error");
assert!(matches!(err, ConfirmMessageError::FutureEpoch));
}
#[openmls_test::openmls_test]
fn confirm_handshake_message_deletes_retained_secret() {
let alice_provider = &Provider::default();
let bob_provider = &Provider::default();
let (alice_credential, alice_signer) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let (bob_credential, bob_signer) =
new_credential(bob_provider, b"Bob", ciphersuite.signature_algorithm());
let (charlie_credential, charlie_signer) = new_credential(
alice_provider,
b"Charlie",
ciphersuite.signature_algorithm(),
);
let (dave_credential, dave_signer) =
new_credential(alice_provider, b"Dave", ciphersuite.signature_algorithm());
let group_config = MlsGroupCreateConfig::builder()
.wire_format_policy(PURE_CIPHERTEXT_WIRE_FORMAT_POLICY)
.ciphersuite(ciphersuite)
.use_ratchet_tree_extension(true)
.build();
let mut alice_group = MlsGroup::new(
alice_provider,
&alice_signer,
&group_config,
alice_credential,
)
.expect("alice create group");
let bob_key_package = KeyPackage::builder()
.build(ciphersuite, bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_commit, _welcome, _gi) = alice_group
.add_members(alice_provider, &alice_signer, &[bob_key_package])
.expect("alice add bob");
alice_group
.merge_pending_commit(alice_provider)
.expect("alice merge add");
let charlie_key_package = KeyPackage::builder()
.build(
ciphersuite,
alice_provider,
&charlie_signer,
charlie_credential,
)
.expect("charlie KP build")
.key_package()
.to_owned();
let dave_key_package = KeyPackage::builder()
.build(ciphersuite, alice_provider, &dave_signer, dave_credential)
.expect("dave KP build")
.key_package()
.to_owned();
let epoch = alice_group.epoch();
let (proposal_a, _ref_a, _confirmation_a) = alice_group
.propose_unconfirmed(
alice_provider,
&alice_signer,
Propose::Add(charlie_key_package),
ProposalOrRefType::Reference,
)
.expect("propose add charlie");
let processed = alice_group
.process_message(alice_provider, proposal_a.into_protocol_message().unwrap())
.expect("process proposal A echo");
assert!(matches!(
processed.into_content(),
ProcessedMessageContent::ProposalMessage(_)
));
let (proposal_b, _ref_b, _confirmation_b) = alice_group
.propose_unconfirmed(
alice_provider,
&alice_signer,
Propose::Add(dave_key_package),
ProposalOrRefType::Reference,
)
.expect("propose add dave");
alice_group
.confirm_handshake_message(alice_provider.storage(), epoch, 1)
.expect("confirm proposal B");
let processed = alice_group
.process_message(alice_provider, proposal_b.into_protocol_message().unwrap())
.expect("process proposal B echo");
assert!(matches!(
processed.into_content(),
ProcessedMessageContent::OwnPrivateMessage
));
}
#[openmls_test::openmls_test]
fn unconfirmed_message_decrypts_after_next_message_is_confirmed() {
let alice_provider = &Provider::default();
let (alice_credential, alice_signer) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let mut alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.build(alice_provider, &alice_signer, alice_credential)
.expect("An unexpected error occurred.");
let first_message = b"first unconfirmed message";
let first = alice_group
.create_unconfirmed_message(alice_provider, &alice_signer, first_message)
.expect("Could not create first unconfirmed message.");
assert_eq!(first.generation, 0);
let second_message = b"second confirmed message";
let second = alice_group
.create_unconfirmed_message(alice_provider, &alice_signer, second_message)
.expect("Could not create second message.");
assert_eq!(second.generation, 1);
alice_group
.confirm_application_message(alice_provider.storage(), second.epoch, second.generation)
.expect("Could not confirm second message.");
let processed_message = alice_group
.process_message(
alice_provider,
first.message.into_protocol_message().unwrap(),
)
.expect("Expected first unconfirmed message to decrypt.");
let ProcessedMessageContent::ApplicationMessage(msg) = processed_message.into_content() else {
panic!("Expected an application message.");
};
assert_eq!(first_message.as_slice(), msg.into_bytes().as_slice());
}
#[openmls_test::openmls_test]
fn old_unconfirmed_own_message_survives_later_confirmations() {
let alice_provider = &Provider::default();
let (alice_credential, alice_signer) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let mut alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.build(alice_provider, &alice_signer, alice_credential)
.expect("An unexpected error occurred.");
let first_message = b"first unconfirmed message";
let first = alice_group
.create_unconfirmed_message(alice_provider, &alice_signer, first_message)
.expect("Could not create first unconfirmed message.");
let tolerance = alice_group
.configuration()
.sender_ratchet_configuration()
.out_of_order_tolerance();
for i in 0..tolerance + 2 {
let later = alice_group
.create_unconfirmed_message(
alice_provider,
&alice_signer,
format!("later confirmed message {i}").as_bytes(),
)
.expect("Could not create later unconfirmed message.");
alice_group
.confirm_application_message(alice_provider.storage(), later.epoch, later.generation)
.expect("Could not confirm later message.");
}
let processed_message = alice_group
.process_message(
alice_provider,
first.message.into_protocol_message().unwrap(),
)
.expect("Expected old unconfirmed own message to decrypt.");
let ProcessedMessageContent::ApplicationMessage(msg) = processed_message.into_content() else {
panic!("Expected an application message.");
};
assert_eq!(first_message.as_slice(), msg.into_bytes().as_slice());
}
#[openmls_test::openmls_test]
fn reuse_guard_recovers_emulator_leaf_index() {
let _ = ciphersuite;
let ciphersuite =
openmls_traits::types::Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
let alice_a_provider = OpenMlsRustCrypto::default();
let alice_b_provider = OpenMlsRustCrypto::default();
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let mut alice_a_main = new_vc_main_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential.clone(),
);
let (sib, resync_commit) = join_sibling_emulator(
ciphersuite,
&alice_a_provider,
&alice_b_provider,
&vc_signer,
vc_credential,
&alice_a_main,
vc_join_config(),
);
let SiblingEmulators {
emulator_a,
mut alice_b_main,
..
} = sib;
process_and_merge_commit(&mut alice_a_main, &alice_a_provider, resync_commit);
let expected_emulation_leaf = emulator_a.own_leaf_index();
let plaintext = b"reuse-guard recovery payload";
let processed_app = send_and_process_app_message(
&mut alice_a_main,
&alice_a_provider,
&vc_signer,
&mut alice_b_main,
&alice_b_provider,
plaintext,
);
assert_eq!(
processed_app.emulator_sender_leaf_index(),
Some(expected_emulation_leaf),
);
match processed_app.into_content() {
ProcessedMessageContent::ApplicationMessage(msg) => {
assert_eq!(msg.into_bytes().as_slice(), plaintext);
}
_ => panic!("expected application message"),
}
}
#[openmls_test::openmls_test]
fn emulator_sender_leaf_index_none_without_binding() {
let alice_provider = Provider::default();
let bob_provider = Provider::default();
let (mut alice, _alice_signer, mut bob, bob_signer) =
setup_alice_bob_group(ciphersuite, &alice_provider, &bob_provider);
let plaintext = b"non-emulator application message";
let bob_msg = bob
.create_message(&bob_provider, &bob_signer, plaintext)
.expect("bob creates application message");
let processed = alice
.process_message(&alice_provider, bob_msg.into_protocol_message().unwrap())
.expect("alice processes bob's application message");
assert_eq!(processed.emulator_sender_leaf_index(), None);
match processed.into_content() {
ProcessedMessageContent::ApplicationMessage(msg) => {
assert_eq!(msg.into_bytes().as_slice(), plaintext);
}
_ => panic!("expected application message"),
}
}
#[test]
fn bound_group_fails_closed_when_emulation_state_missing_on_send() {
let ciphersuite =
openmls_traits::types::Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
let provider = OpenMlsRustCrypto::default();
let (alice_credential, alice_signer) =
new_credential(&provider, b"Alice", ciphersuite.signature_algorithm());
let group_config = 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();
let mut alice_group = MlsGroup::new(&provider, &alice_signer, &group_config, alice_credential)
.expect("create alice group");
let (mut emulator_group, _emulator_signer) =
make_emulator_group(ciphersuite, &provider, b"AliceEmulator");
let (_commit_msg, epoch_id) = send_vc_commit(
&mut alice_group,
&mut emulator_group,
&provider,
&alice_signer,
);
let deleted = provider
.storage()
.delete_vc_emulation_state_if_unreferenced(&epoch_id)
.expect("delete emulation state");
assert!(
deleted,
"no retained material, so the epoch state is deleted"
);
let err = alice_group
.create_message(&provider, &alice_signer, b"must not send")
.expect_err("bound group without emulation state must fail closed");
assert!(
matches!(
err,
openmls::group::CreateMessageError::MessageEncryptionError(
openmls::framing::errors::MessageEncryptionError::VirtualClientsError(
openmls::components::vc_derivation_info::VirtualClientsError::MissingEmulationEpochState
)
)
),
"unexpected error: {err:?}"
);
}
#[test]
fn create_unconfirmed_message_returns_generation_id_when_bound() {
let ciphersuite =
openmls_traits::types::Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
let provider = OpenMlsRustCrypto::default();
let (alice_credential, alice_signer) =
new_credential(&provider, b"Alice", ciphersuite.signature_algorithm());
let mut alice_group =
new_vc_main_group(ciphersuite, &provider, &alice_signer, alice_credential);
let (mut emulator_group, _emulator_signer) =
make_emulator_group(ciphersuite, &provider, b"AliceEmulator");
let _ = send_vc_commit(
&mut alice_group,
&mut emulator_group,
&provider,
&alice_signer,
);
let first = alice_group
.create_unconfirmed_message(&provider, &alice_signer, b"first")
.expect("create first unconfirmed message");
let generation_id_first = first
.generation_id
.expect("a bound group must produce a generation id");
assert_eq!(
generation_id_first.as_slice().len(),
ciphersuite.hash_length()
);
alice_group
.confirm_application_message(provider.storage(), first.epoch, first.generation)
.expect("confirm first message");
let second = alice_group
.create_unconfirmed_message(&provider, &alice_signer, b"second")
.expect("create second unconfirmed message");
let generation_id_second = second
.generation_id
.expect("a bound group must produce a generation id");
assert_ne!(
generation_id_first, generation_id_second,
"distinct ratchet generations must yield distinct generation ids"
);
}
#[test]
fn vc_emulation_rejects_misconfigured_leaf_before_allocating() {
let ciphersuite =
openmls_traits::types::Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
let provider = OpenMlsRustCrypto::default();
let (alice_credential, alice_signer) =
new_credential(&provider, b"Alice", ciphersuite.signature_algorithm());
let group_config = MlsGroupCreateConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.ciphersuite(ciphersuite)
.use_ratchet_tree_extension(true)
.capabilities(vc_capabilities())
.build();
let mut alice_group = MlsGroup::new(&provider, &alice_signer, &group_config, alice_credential)
.expect("create alice group");
let (mut emulator_group, _emulator_signer) =
make_emulator_group(ciphersuite, &provider, b"AliceEmulator");
let epoch_id = emulator_group
.register_vc_emulation_epoch(provider.crypto(), provider.storage())
.expect("register vc epoch");
let err = alice_group
.commit_builder()
.vc_emulation(provider.crypto(), provider.storage(), epoch_id)
.expect_err("misconfigured leaf must be rejected at the builder step");
assert!(
matches!(
err,
openmls::group::CreateCommitError::VirtualClientsError(
openmls::components::vc_derivation_info::VirtualClientsError::VcComponentNotListed
)
),
"unexpected error: {err:?}"
);
}
#[test]
fn reuse_guard_recovery_across_mismatched_ciphersuites() {
let _ = pretty_env_logger::try_init();
let higher_level_ciphersuite =
openmls_traits::types::Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
let emulator_ciphersuite =
openmls_traits::types::Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519;
assert_ne!(higher_level_ciphersuite, emulator_ciphersuite);
let alice_a_provider = OpenMlsRustCrypto::default();
let alice_b_provider = OpenMlsRustCrypto::default();
let (vc_signer, vc_credential) = shared_vc_identity(
higher_level_ciphersuite,
&alice_a_provider,
&alice_b_provider,
);
let mut alice_a_main = new_vc_main_group(
higher_level_ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential.clone(),
);
let (sib, resync_commit) = join_sibling_emulator(
emulator_ciphersuite,
&alice_a_provider,
&alice_b_provider,
&vc_signer,
vc_credential,
&alice_a_main,
vc_join_config(),
);
let SiblingEmulators {
emulator_a,
mut alice_b_main,
..
} = sib;
process_and_merge_commit(&mut alice_a_main, &alice_a_provider, resync_commit);
let expected_emulation_leaf = emulator_a.own_leaf_index();
let plaintext = b"mismatched-ciphersuite reuse-guard recovery payload";
let processed_app = send_and_process_app_message(
&mut alice_a_main,
&alice_a_provider,
&vc_signer,
&mut alice_b_main,
&alice_b_provider,
plaintext,
);
assert_eq!(
processed_app.emulator_sender_leaf_index(),
Some(expected_emulation_leaf),
);
match processed_app.into_content() {
ProcessedMessageContent::ApplicationMessage(msg) => {
assert_eq!(msg.into_bytes().as_slice(), plaintext);
}
_ => panic!("expected application message"),
}
}
fn process_and_merge_commit<P: OpenMlsProvider>(
receiver: &mut MlsGroup,
provider: &P,
commit_msg: openmls::prelude::MlsMessageOut,
) {
let processed = receiver
.process_message(provider, commit_msg.into_protocol_message().unwrap())
.expect("process commit");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
_ => panic!("expected staged commit"),
};
receiver
.merge_staged_commit(provider, staged)
.expect("merge staged commit");
}
#[test]
fn vc_binding_is_kept_per_epoch_for_delayed_messages() {
let ciphersuite =
openmls_traits::types::Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
let alice_a_provider = OpenMlsRustCrypto::default();
let alice_b_provider = OpenMlsRustCrypto::default();
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let group_config = MlsGroupCreateConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.ciphersuite(ciphersuite)
.use_ratchet_tree_extension(true)
.max_past_epochs(2)
.capabilities(vc_capabilities())
.with_leaf_node_extensions(vc_leaf_extensions())
.expect("attach leaf-node extensions")
.build();
let mut alice_a_main = MlsGroup::new(
&alice_a_provider,
&vc_signer,
&group_config,
vc_credential.clone(),
)
.expect("alice_a create main group");
let main_group_id = alice_a_main.group_id().clone();
let (sib, resync_commit) = join_sibling_emulator(
ciphersuite,
&alice_a_provider,
&alice_b_provider,
&vc_signer,
vc_credential,
&alice_a_main,
MlsGroupJoinConfig::builder()
.wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.max_past_epochs(2)
.build(),
);
let SiblingEmulators {
mut emulator_a,
emulator_a_signer,
mut emulator_b,
mut alice_b_main,
epoch_id: epoch_id_one,
} = sib;
let expected_emulation_leaf = emulator_a.own_leaf_index();
process_and_merge_commit(&mut alice_a_main, &alice_a_provider, resync_commit);
let first_bound_epoch = alice_a_main.epoch();
let plaintext = b"delayed across an epoch change";
let delayed_msg = alice_a_main
.create_message(&alice_a_provider, &vc_signer, plaintext)
.expect("alice_a creates delayed application message");
let emulator_commit = {
let bundle = emulator_a
.commit_builder()
.force_self_update(true)
.load_psks(alice_a_provider.storage())
.expect("load psks")
.build(
alice_a_provider.rand(),
alice_a_provider.crypto(),
&emulator_a_signer,
|_| true,
)
.expect("build emulator commit")
.stage_commit(&alice_a_provider)
.expect("stage emulator commit");
emulator_a
.merge_pending_commit(&alice_a_provider)
.expect("emulator_a merge");
bundle.into_commit()
};
process_and_merge_commit(&mut emulator_b, &alice_b_provider, emulator_commit);
let epoch_id_two = emulator_a
.register_vc_emulation_epoch(alice_a_provider.crypto(), alice_a_provider.storage())
.expect("register second vc epoch (alice_a)");
let epoch_id_two_b = emulator_b
.register_vc_emulation_epoch(alice_b_provider.crypto(), alice_b_provider.storage())
.expect("register second vc epoch (alice_b)");
assert_eq!(epoch_id_two, epoch_id_two_b);
assert_ne!(epoch_id_one, epoch_id_two);
let commit_two = send_vc_commit_with_epoch(
&mut alice_a_main,
&alice_a_provider,
&vc_signer,
epoch_id_two.clone(),
);
let second_bound_epoch = alice_a_main.epoch();
process_and_merge_commit(&mut alice_b_main, &alice_b_provider, commit_two);
let bindings: VcEmulationBindings = alice_b_provider
.storage()
.vc_emulation_bindings(&main_group_id)
.expect("read emulation bindings")
.expect("emulation bindings present");
assert_eq!(bindings.get(first_bound_epoch), Some(&epoch_id_one));
assert_eq!(bindings.get(second_bound_epoch), Some(&epoch_id_two));
let processed_app = alice_b_main
.process_message(
&alice_b_provider,
delayed_msg.into_protocol_message().unwrap(),
)
.expect("alice_b processes delayed application message");
assert_eq!(
processed_app.emulator_sender_leaf_index(),
Some(expected_emulation_leaf),
);
match processed_app.into_content() {
ProcessedMessageContent::ApplicationMessage(msg) => {
assert_eq!(msg.into_bytes().as_slice(), plaintext);
}
_ => panic!("expected application message"),
}
}
#[test]
fn vc_binding_carries_forward_across_foreign_commits() {
let ciphersuite =
openmls_traits::types::Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
let alice_a_provider = OpenMlsRustCrypto::default();
let alice_b_provider = OpenMlsRustCrypto::default();
let bob_provider = OpenMlsRustCrypto::default();
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let mut alice_a_main = new_vc_main_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential.clone(),
);
let (bob_credential, bob_signer) =
new_credential(&bob_provider, b"Bob", ciphersuite.signature_algorithm());
let bob_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, &bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_commit, welcome, _gi) = alice_a_main
.add_members(&alice_a_provider, &vc_signer, &[bob_kp])
.expect("alice add bob");
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("alice merge add");
let mut bob_group = StagedWelcome::new_from_welcome(
&bob_provider,
&vc_join_config(),
welcome.into_welcome().expect("welcome present"),
Some(alice_a_main.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&bob_provider))
.expect("bob join");
let (sib, resync_commit) = join_sibling_emulator(
ciphersuite,
&alice_a_provider,
&alice_b_provider,
&vc_signer,
vc_credential,
&alice_a_main,
vc_join_config(),
);
let SiblingEmulators {
emulator_a,
mut alice_b_main,
..
} = sib;
let expected_emulation_leaf = emulator_a.own_leaf_index();
process_and_merge_commit(&mut alice_a_main, &alice_a_provider, resync_commit.clone());
process_and_merge_commit(&mut bob_group, &bob_provider, resync_commit);
let bob_commit = {
let bundle = bob_group
.commit_builder()
.force_self_update(true)
.load_psks(bob_provider.storage())
.expect("load psks")
.build(
bob_provider.rand(),
bob_provider.crypto(),
&bob_signer,
|_| true,
)
.expect("build bob commit")
.stage_commit(&bob_provider)
.expect("stage bob commit");
bob_group
.merge_pending_commit(&bob_provider)
.expect("bob merge");
bundle.into_commit()
};
process_and_merge_commit(&mut alice_a_main, &alice_a_provider, bob_commit.clone());
process_and_merge_commit(&mut alice_b_main, &alice_b_provider, bob_commit);
let plaintext = b"carried-forward binding";
let processed_app = send_and_process_app_message(
&mut alice_a_main,
&alice_a_provider,
&vc_signer,
&mut alice_b_main,
&alice_b_provider,
plaintext,
);
assert_eq!(
processed_app.emulator_sender_leaf_index(),
Some(expected_emulation_leaf),
);
match processed_app.into_content() {
ProcessedMessageContent::ApplicationMessage(msg) => {
assert_eq!(msg.into_bytes().as_slice(), plaintext);
}
_ => panic!("expected application message"),
}
}
#[openmls_test]
fn vc_sibling_applies_commit_without_update_path() {
use openmls::credentials::{BasicCredential, CredentialWithKey};
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let bob_provider = Provider::default();
let charly_provider = Provider::default();
let vc_signer = SignatureKeyPair::new(ciphersuite.signature_algorithm()).expect("vc signer");
vc_signer
.store(alice_a_provider.storage())
.expect("store vc signer on alice_a");
vc_signer
.store(alice_b_provider.storage())
.expect("store vc signer on alice_b");
let vc_credential = CredentialWithKey {
credential: BasicCredential::new(b"Alice (VC)".to_vec()).into(),
signature_key: vc_signer.public().into(),
};
let mut alice_a_main = new_vc_main_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential.clone(),
);
let (bob_credential, bob_signer) =
new_credential(&bob_provider, b"Bob", ciphersuite.signature_algorithm());
let bob_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, &bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_, welcome, _) = alice_a_main
.add_members(&alice_a_provider, &vc_signer, &[bob_kp])
.expect("alice_a add bob");
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("alice_a merge add bob");
let mut bob_main = StagedWelcome::new_from_welcome(
&bob_provider,
&vc_join_config(),
welcome.into_welcome().expect("welcome"),
Some(alice_a_main.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&bob_provider))
.expect("bob join");
let (siblings, resync_commit) = join_sibling_emulator(
ciphersuite,
&alice_a_provider,
&alice_b_provider,
&vc_signer,
vc_credential,
&alice_a_main,
vc_join_config(),
);
let mut alice_b_main = siblings.alice_b_main;
for (group, provider) in [
(&mut alice_a_main, &alice_a_provider),
(&mut bob_main, &bob_provider),
] {
let processed = group
.process_message(
provider,
resync_commit.clone().into_protocol_message().unwrap(),
)
.expect("process resync commit");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
_ => panic!("expected staged commit"),
};
group
.merge_staged_commit(provider, staged)
.expect("merge resync commit");
}
assert_eq!(
alice_a_main.own_leaf_index(),
alice_b_main.own_leaf_index(),
"both Alice clients must share the higher-level leaf"
);
let (charly_credential, charly_signer) = new_credential(
&charly_provider,
b"Charly",
ciphersuite.signature_algorithm(),
);
let charly_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(
ciphersuite,
&charly_provider,
&charly_signer,
charly_credential,
)
.expect("charly KP build")
.key_package()
.to_owned();
let (commit, charly_welcome, _) = alice_a_main
.add_members_without_update(&alice_a_provider, &vc_signer, &[charly_kp])
.expect("alice_a add charly without an UpdatePath");
let staged_pending = alice_a_main
.pending_commit()
.expect("alice_a has a pending commit");
assert!(
staged_pending.update_path_leaf_node().is_none(),
"the add-only commit must not carry a path"
);
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("alice_a merge add without an UpdatePath");
let processed = alice_b_main
.process_message(
&alice_b_provider,
commit.clone().into_protocol_message().unwrap(),
)
.expect("alice_b processes sibling commit without an UpdatePath");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
other => panic!("expected staged commit, got {other:?}"),
};
assert!(
!staged.self_removed(),
"a sibling's add-only commit must not remove alice_b"
);
alice_b_main
.merge_staged_commit(&alice_b_provider, staged)
.expect("alice_b merge sibling commit without an UpdatePath");
let processed = bob_main
.process_message(&bob_provider, commit.into_protocol_message().unwrap())
.expect("bob processes commit without an UpdatePath");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
_ => panic!("expected staged commit"),
};
bob_main
.merge_staged_commit(&bob_provider, staged)
.expect("bob merge commit without an UpdatePath");
let mut charly_main = StagedWelcome::new_from_welcome(
&charly_provider,
&vc_join_config(),
charly_welcome.into_welcome().expect("charly welcome"),
Some(alice_a_main.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&charly_provider))
.expect("charly join");
let authenticator = alice_a_main.epoch_authenticator();
assert_eq!(
alice_b_main.epoch_authenticator(),
authenticator,
"sibling must converge with the committer"
);
assert_eq!(bob_main.epoch_authenticator(), authenticator);
assert_eq!(charly_main.epoch_authenticator(), authenticator);
let processed = send_and_process_app_message(
&mut charly_main,
&charly_provider,
&charly_signer,
&mut alice_b_main,
&alice_b_provider,
b"hello from charly",
);
match processed.into_content() {
ProcessedMessageContent::ApplicationMessage(msg) => {
assert_eq!(msg.into_bytes().as_slice(), b"hello from charly");
}
_ => panic!("expected application message"),
}
}
#[openmls_test]
fn vc_own_commit_echo_surfaces_as_own_pending_commit() {
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let bob_provider = Provider::default();
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let mut alice_a_main = new_vc_main_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential.clone(),
);
let (bob_credential, bob_signer) =
new_credential(&bob_provider, b"Bob", ciphersuite.signature_algorithm());
let bob_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, &bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_, welcome, _) = alice_a_main
.add_members(&alice_a_provider, &vc_signer, &[bob_kp])
.expect("alice_a add bob");
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("alice_a merge add bob");
let mut bob_main = StagedWelcome::new_from_welcome(
&bob_provider,
&vc_join_config(),
welcome.into_welcome().expect("welcome"),
Some(alice_a_main.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&bob_provider))
.expect("bob join");
let (siblings, resync_commit) = join_sibling_emulator(
ciphersuite,
&alice_a_provider,
&alice_b_provider,
&vc_signer,
vc_credential,
&alice_a_main,
vc_join_config(),
);
let mut alice_b_main = siblings.alice_b_main;
let epoch_id = siblings.epoch_id;
for (group, provider) in [
(&mut alice_a_main, &alice_a_provider),
(&mut bob_main, &bob_provider),
] {
process_and_merge_commit(group, provider, resync_commit.clone());
}
assert_eq!(
alice_a_main.own_leaf_index(),
alice_b_main.own_leaf_index(),
"both Alice clients must share the higher-level leaf"
);
let bundle = alice_a_main
.commit_builder()
.vc_emulation(
alice_a_provider.crypto(),
alice_a_provider.storage(),
epoch_id.clone(),
)
.expect("alice_a bind commit to emulation epoch")
.load_psks(alice_a_provider.storage())
.expect("alice_a load psks")
.build(
alice_a_provider.rand(),
alice_a_provider.crypto(),
&vc_signer,
|_| true,
)
.expect("alice_a build vc commit")
.stage_commit(&alice_a_provider)
.expect("alice_a stage vc commit");
let commit = bundle.into_commit();
let processed = alice_a_main
.process_message(
&alice_a_provider,
commit.clone().into_protocol_message().unwrap(),
)
.expect("alice_a processes her own fanned-back vc commit");
assert!(matches!(
processed.into_content(),
ProcessedMessageContent::OwnPendingCommit
));
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("alice_a merge her own vc commit");
let processed = alice_b_main
.process_message(
&alice_b_provider,
commit.clone().into_protocol_message().unwrap(),
)
.expect("alice_b processes sibling vc commit");
let staged = match processed.into_content() {
ProcessedMessageContent::StagedCommitMessage(s) => *s,
other => panic!("expected staged commit, got {other:?}"),
};
alice_b_main
.merge_staged_commit(&alice_b_provider, staged)
.expect("alice_b merge sibling vc commit");
process_and_merge_commit(&mut bob_main, &bob_provider, commit);
let authenticator = alice_a_main.epoch_authenticator();
assert_eq!(
alice_b_main.epoch_authenticator(),
authenticator,
"sibling must converge with the committer"
);
assert_eq!(
bob_main.epoch_authenticator(),
authenticator,
"bob must converge with the committer"
);
let second_commit =
send_vc_commit_with_epoch(&mut alice_b_main, &alice_b_provider, &vc_signer, epoch_id);
process_and_merge_commit(&mut alice_a_main, &alice_a_provider, second_commit.clone());
process_and_merge_commit(&mut bob_main, &bob_provider, second_commit);
let authenticator = alice_b_main.epoch_authenticator();
assert_eq!(
alice_a_main.epoch_authenticator(),
authenticator,
"committer's sibling must converge after the second commit"
);
assert_eq!(
bob_main.epoch_authenticator(),
authenticator,
"bob must converge after the second commit"
);
}
fn setup_sibling_emulation_epoch<P: OpenMlsProvider>(
emulator_ciphersuite: openmls_traits::types::Ciphersuite,
alice_a_provider: &P,
alice_b_provider: &P,
) -> EpochId {
let (mut emulator_a, emulator_a_signer) =
make_emulator_group(emulator_ciphersuite, alice_a_provider, b"AliceEmulatorA");
let (emulator_b_credential, emulator_b_signer) = new_credential(
alice_b_provider,
b"AliceEmulatorB",
emulator_ciphersuite.signature_algorithm(),
);
let emulator_b_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.leaf_node_capabilities(vc_capabilities())
.leaf_node_extensions(vc_leaf_extensions())
.build(
emulator_ciphersuite,
alice_b_provider,
&emulator_b_signer,
emulator_b_credential,
)
.expect("emulator_b KP build")
.key_package()
.to_owned();
let (_e_commit, e_welcome, _e_gi) = emulator_a
.add_members(alice_a_provider, &emulator_a_signer, &[emulator_b_kp])
.expect("emulator_a add alice_b");
emulator_a
.merge_pending_commit(alice_a_provider)
.expect("emulator_a merge add");
let mut emulator_b = StagedWelcome::new_from_welcome(
alice_b_provider,
&vc_join_config(),
e_welcome.into_welcome().expect("emulator welcome"),
Some(emulator_a.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(alice_b_provider))
.expect("alice_b join emulator group");
let epoch_id = emulator_a
.register_vc_emulation_epoch(alice_a_provider.crypto(), alice_a_provider.storage())
.expect("alice_a register vc epoch");
let epoch_id_b = emulator_b
.register_vc_emulation_epoch(alice_b_provider.crypto(), alice_b_provider.storage())
.expect("alice_b register vc epoch");
assert_eq!(
epoch_id, epoch_id_b,
"siblings must derive the same EpochId"
);
epoch_id
}
fn export_verifiable_group_info<P: OpenMlsProvider>(
group: &MlsGroup,
provider: &P,
signer: &SignatureKeyPair,
with_ratchet_tree: bool,
) -> openmls::messages::group_info::VerifiableGroupInfo {
use openmls::prelude::MlsMessageIn;
use tls_codec::Deserialize as _;
let group_info_msg = group
.export_group_info(provider.crypto(), signer, with_ratchet_tree)
.expect("export group info");
let serialized = group_info_msg
.tls_serialize_detached()
.expect("serialize group info");
MlsMessageIn::tls_deserialize(&mut serialized.as_slice())
.expect("deserialize group info message")
.into_verifiable_group_info()
.expect("into verifiable group info")
}
fn create_vc_group<P: OpenMlsProvider>(
ciphersuite: openmls_traits::types::Ciphersuite,
provider: &P,
signer: &SignatureKeyPair,
credential: openmls::credentials::CredentialWithKey,
epoch_id: EpochId,
) -> MlsGroup {
MlsGroup::builder()
.with_wire_format_policy(PURE_PLAINTEXT_WIRE_FORMAT_POLICY)
.ciphersuite(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, signer, credential)
.expect("create vc group")
}
#[openmls_test]
fn vc_sibling_joins_group_created_by_virtual_client() {
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let bob_provider = Provider::default();
let epoch_id = setup_sibling_emulation_epoch(ciphersuite, &alice_a_provider, &alice_b_provider);
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let alice_a_main = create_vc_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential.clone(),
epoch_id.clone(),
);
let verifiable_group_info =
export_verifiable_group_info(&alice_a_main, &alice_a_provider, &vc_signer, true);
let mut alice_b_main = MlsGroup::vc_join_at_creation(
&alice_b_provider,
&vc_join_config(),
verifiable_group_info,
None,
epoch_id.clone(),
)
.expect("alice_b reconstructs the created group");
assert_eq!(alice_a_main.epoch(), alice_b_main.epoch());
assert_eq!(
alice_b_main.own_leaf_index(),
alice_a_main.own_leaf_index(),
"the sibling must land on the shared VC leaf"
);
assert_eq!(
alice_a_main.epoch_authenticator(),
alice_b_main.epoch_authenticator(),
"reconstruction must reproduce the creator's epoch secrets"
);
assert_eq!(
alice_a_main.export_ratchet_tree(),
alice_b_main.export_ratchet_tree(),
);
let (bob_credential, bob_signer) =
new_credential(&bob_provider, b"Bob", ciphersuite.signature_algorithm());
let bob_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, &bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_commit, welcome, _gi) = alice_b_main
.add_members(&alice_b_provider, &vc_signer, &[bob_kp])
.expect("alice_b adds bob");
alice_b_main
.merge_pending_commit(&alice_b_provider)
.expect("alice_b merge add");
let mut bob_main = StagedWelcome::new_from_welcome(
&bob_provider,
&vc_join_config(),
welcome.into_welcome().expect("welcome"),
Some(alice_b_main.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&bob_provider))
.expect("bob joins the created group");
let processed = send_and_process_app_message(
&mut alice_b_main,
&alice_b_provider,
&vc_signer,
&mut bob_main,
&bob_provider,
b"hello from the virtual client",
);
let ProcessedMessageContent::ApplicationMessage(message) = processed.into_content() else {
panic!("expected application message");
};
assert_eq!(message.into_bytes(), b"hello from the virtual client");
}
#[openmls_test]
fn vc_group_creation_join_with_separate_ratchet_tree() {
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let epoch_id = setup_sibling_emulation_epoch(ciphersuite, &alice_a_provider, &alice_b_provider);
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let alice_a_main = create_vc_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential,
epoch_id.clone(),
);
let verifiable_group_info =
export_verifiable_group_info(&alice_a_main, &alice_a_provider, &vc_signer, false);
let ratchet_tree = alice_a_main.export_ratchet_tree().into();
let alice_b_main = MlsGroup::vc_join_at_creation(
&alice_b_provider,
&vc_join_config(),
verifiable_group_info,
Some(ratchet_tree),
epoch_id,
)
.expect("alice_b reconstructs with a separately supplied ratchet tree");
assert_eq!(
alice_a_main.epoch_authenticator(),
alice_b_main.epoch_authenticator(),
);
}
#[openmls_test]
fn vc_group_creation_join_fails_without_emulation_state() {
use openmls::prelude::VcGroupCreationJoinError;
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let outsider_provider = Provider::default();
let epoch_id = setup_sibling_emulation_epoch(ciphersuite, &alice_a_provider, &alice_b_provider);
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let alice_a_main = create_vc_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential,
epoch_id.clone(),
);
let verifiable_group_info =
export_verifiable_group_info(&alice_a_main, &alice_a_provider, &vc_signer, true);
let err = MlsGroup::vc_join_at_creation(
&outsider_provider,
&vc_join_config(),
verifiable_group_info,
None,
epoch_id,
)
.expect_err("a non-sibling cannot reconstruct the created group");
assert!(matches!(
err,
VcGroupCreationJoinError::VirtualClientsError(_)
));
}
#[openmls_test]
fn vc_group_creation_join_fails_on_epoch_id_mismatch() {
use openmls::prelude::VcGroupCreationJoinError;
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let other_a_provider = Provider::default();
let other_b_provider = Provider::default();
let epoch_id = setup_sibling_emulation_epoch(ciphersuite, &alice_a_provider, &alice_b_provider);
let other_epoch_id =
setup_sibling_emulation_epoch(ciphersuite, &other_a_provider, &other_b_provider);
assert_ne!(epoch_id, other_epoch_id);
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let alice_a_main = create_vc_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential,
epoch_id,
);
let verifiable_group_info =
export_verifiable_group_info(&alice_a_main, &alice_a_provider, &vc_signer, true);
let err = MlsGroup::vc_join_at_creation(
&alice_b_provider,
&vc_join_config(),
verifiable_group_info,
None,
other_epoch_id,
)
.expect_err("mismatched emulation epoch must be rejected");
assert!(matches!(err, VcGroupCreationJoinError::EpochIdMismatch));
}
#[openmls_test]
fn vc_group_creation_join_fails_on_non_key_package_creator_leaf() {
use openmls::prelude::VcGroupCreationJoinError;
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let epoch_id = setup_sibling_emulation_epoch(ciphersuite, &alice_a_provider, &alice_b_provider);
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let mut alice_a_main = create_vc_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential,
epoch_id.clone(),
);
alice_a_main
.commit_builder()
.force_self_update(true)
.vc_emulation(
alice_a_provider.crypto(),
alice_a_provider.storage(),
epoch_id.clone(),
)
.expect("vc emulation commit builder")
.load_psks(alice_a_provider.storage())
.expect("load psks")
.build(
alice_a_provider.rand(),
alice_a_provider.crypto(),
&vc_signer,
|_| true,
)
.expect("build commit")
.stage_commit(&alice_a_provider)
.expect("stage commit");
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("merge self-commit");
let verifiable_group_info =
export_verifiable_group_info(&alice_a_main, &alice_a_provider, &vc_signer, true);
let err = MlsGroup::vc_join_at_creation(
&alice_b_provider,
&vc_join_config(),
verifiable_group_info,
None,
epoch_id,
)
.expect_err("a commit-sourced creator leaf must be rejected");
assert!(matches!(
err,
VcGroupCreationJoinError::CreatorLeafNotKeyPackageSourced
));
}
#[openmls_test]
fn vc_group_creation_join_fails_on_multi_leaf_tree() {
use openmls::prelude::VcGroupCreationJoinError;
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let bob_provider = Provider::default();
let epoch_id = setup_sibling_emulation_epoch(ciphersuite, &alice_a_provider, &alice_b_provider);
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let mut alice_a_main = create_vc_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential,
epoch_id.clone(),
);
let (bob_credential, bob_signer) =
new_credential(&bob_provider, b"Bob", ciphersuite.signature_algorithm());
let bob_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, &bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
alice_a_main
.add_members(&alice_a_provider, &vc_signer, &[bob_kp])
.expect("alice_a adds bob");
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("alice_a merge add");
let verifiable_group_info =
export_verifiable_group_info(&alice_a_main, &alice_a_provider, &vc_signer, true);
let err = MlsGroup::vc_join_at_creation(
&alice_b_provider,
&vc_join_config(),
verifiable_group_info,
None,
epoch_id,
)
.expect_err("a multi-leaf tree must be rejected");
assert!(matches!(err, VcGroupCreationJoinError::NotASingleLeafTree));
}
#[openmls_test]
fn vc_group_creation_join_fails_on_missing_derivation_info() {
use openmls::prelude::VcGroupCreationJoinError;
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let epoch_id = setup_sibling_emulation_epoch(ciphersuite, &alice_a_provider, &alice_b_provider);
let (founder_credential, founder_signer) = new_credential(
&alice_a_provider,
b"Founder",
ciphersuite.signature_algorithm(),
);
let alice_a_main = new_vc_main_group(
ciphersuite,
&alice_a_provider,
&founder_signer,
founder_credential,
);
let verifiable_group_info =
export_verifiable_group_info(&alice_a_main, &alice_a_provider, &founder_signer, true);
let err = MlsGroup::vc_join_at_creation(
&alice_b_provider,
&vc_join_config(),
verifiable_group_info,
None,
epoch_id,
)
.expect_err("a creator leaf without derivation info must be rejected");
assert!(matches!(
err,
VcGroupCreationJoinError::MissingDerivationInfo
));
}
#[openmls_test]
fn vc_group_creation_double_join_consumes_generation() {
use openmls::prelude::VcGroupCreationJoinError;
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let epoch_id = setup_sibling_emulation_epoch(ciphersuite, &alice_a_provider, &alice_b_provider);
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let alice_a_main = create_vc_group(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential,
epoch_id.clone(),
);
let first_group_info =
export_verifiable_group_info(&alice_a_main, &alice_a_provider, &vc_signer, true);
MlsGroup::vc_join_at_creation(
&alice_b_provider,
&vc_join_config(),
first_group_info,
None,
epoch_id.clone(),
)
.expect("first reconstruction succeeds");
let second_group_info =
export_verifiable_group_info(&alice_a_main, &alice_a_provider, &vc_signer, true);
let err = MlsGroup::vc_join_at_creation(
&alice_b_provider,
&vc_join_config(),
second_group_info,
None,
epoch_id,
)
.expect_err("second reconstruction must fail on the consumed generation");
assert!(matches!(
err,
VcGroupCreationJoinError::VirtualClientsError(_)
));
}
#[openmls_test::openmls_test]
fn propose_unconfirmed_confirm_flow() {
let alice_provider = &Provider::default();
let bob_provider = &Provider::default();
let (alice_credential, alice_signer) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let (bob_credential, bob_signer) =
new_credential(bob_provider, b"Bob", ciphersuite.signature_algorithm());
let (charlie_credential, charlie_signer) = new_credential(
alice_provider,
b"Charlie",
ciphersuite.signature_algorithm(),
);
let (dave_credential, dave_signer) =
new_credential(alice_provider, b"Dave", ciphersuite.signature_algorithm());
let group_config = MlsGroupCreateConfig::builder()
.wire_format_policy(PURE_CIPHERTEXT_WIRE_FORMAT_POLICY)
.ciphersuite(ciphersuite)
.use_ratchet_tree_extension(true)
.build();
let mut alice_group = MlsGroup::new(
alice_provider,
&alice_signer,
&group_config,
alice_credential,
)
.expect("alice create group");
let bob_key_package = KeyPackage::builder()
.build(ciphersuite, bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_commit, _welcome, _gi) = alice_group
.add_members(alice_provider, &alice_signer, &[bob_key_package])
.expect("alice add bob");
alice_group
.merge_pending_commit(alice_provider)
.expect("alice merge add");
let charlie_key_package = KeyPackage::builder()
.build(
ciphersuite,
alice_provider,
&charlie_signer,
charlie_credential,
)
.expect("charlie KP build")
.key_package()
.to_owned();
let dave_key_package = KeyPackage::builder()
.build(ciphersuite, alice_provider, &dave_signer, dave_credential)
.expect("dave KP build")
.key_package()
.to_owned();
let epoch = alice_group.epoch();
let (proposal_a, _ref_a, confirmation_a) = alice_group
.propose_unconfirmed(
alice_provider,
&alice_signer,
Propose::Add(charlie_key_package),
ProposalOrRefType::Reference,
)
.expect("propose_unconfirmed add charlie");
let confirmation_a = confirmation_a.expect("ciphertext-framed proposal carries confirmation");
assert_eq!(confirmation_a.epoch, epoch);
assert!(confirmation_a.generation_id.is_none());
alice_group
.confirm_handshake_message(
alice_provider.storage(),
confirmation_a.epoch,
confirmation_a.generation,
)
.expect("confirm proposal A");
let processed = alice_group
.process_message(alice_provider, proposal_a.into_protocol_message().unwrap())
.expect("process proposal A echo");
assert!(matches!(
processed.into_content(),
ProcessedMessageContent::OwnPrivateMessage
));
let (proposal_b, _ref_b, _confirmation_b) = alice_group
.propose_unconfirmed(
alice_provider,
&alice_signer,
Propose::Add(dave_key_package),
ProposalOrRefType::Reference,
)
.expect("propose_unconfirmed add dave");
let processed = alice_group
.process_message(alice_provider, proposal_b.into_protocol_message().unwrap())
.expect("process proposal B echo");
assert!(matches!(
processed.into_content(),
ProcessedMessageContent::ProposalMessage(_)
));
}
#[openmls_test::openmls_test]
fn vc_private_commit_end_to_end() {
let alice_a_provider = Provider::default();
let alice_b_provider = Provider::default();
let bob_provider = Provider::default();
let (vc_signer, vc_credential) =
shared_vc_identity(ciphersuite, &alice_a_provider, &alice_b_provider);
let (bob_credential, bob_signer) =
new_credential(&bob_provider, b"Bob", ciphersuite.signature_algorithm());
let (charlie_credential, charlie_signer) = new_credential(
&alice_a_provider,
b"Charlie",
ciphersuite.signature_algorithm(),
);
let mut alice_a_main = new_vc_main_group_with_policy(
ciphersuite,
&alice_a_provider,
&vc_signer,
vc_credential.clone(),
MIXED_CIPHERTEXT_WIRE_FORMAT_POLICY,
);
let bob_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, &bob_provider, &bob_signer, bob_credential)
.expect("bob KP build")
.key_package()
.to_owned();
let (_commit, welcome, _gi) = alice_a_main
.add_members(&alice_a_provider, &vc_signer, &[bob_kp])
.expect("alice add bob");
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("alice merge add");
let bob_join_config = MlsGroupJoinConfig::builder()
.wire_format_policy(MIXED_CIPHERTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.build();
let mut bob_main = StagedWelcome::new_from_welcome(
&bob_provider,
&bob_join_config,
welcome.into_welcome().expect("welcome present"),
Some(alice_a_main.export_ratchet_tree().into()),
)
.and_then(|s| s.into_group(&bob_provider))
.expect("bob join");
let main_join_config = MlsGroupJoinConfig::builder()
.wire_format_policy(MIXED_CIPHERTEXT_WIRE_FORMAT_POLICY)
.use_ratchet_tree_extension(true)
.build();
let (sib, resync_commit) = join_sibling_emulator(
ciphersuite,
&alice_a_provider,
&alice_b_provider,
&vc_signer,
vc_credential,
&alice_a_main,
main_join_config,
);
let SiblingEmulators {
mut alice_b_main,
epoch_id,
..
} = sib;
process_and_merge_commit(&mut alice_a_main, &alice_a_provider, resync_commit.clone());
process_and_merge_commit(&mut bob_main, &bob_provider, resync_commit);
assert_eq!(
alice_a_main.epoch_authenticator(),
alice_b_main.epoch_authenticator()
);
assert_eq!(
alice_a_main.epoch_authenticator(),
bob_main.epoch_authenticator()
);
let mut bundle = alice_a_main
.commit_builder()
.vc_emulation(
alice_a_provider.crypto(),
alice_a_provider.storage(),
epoch_id.clone(),
)
.expect("vc emulation")
.load_psks(alice_a_provider.storage())
.expect("load psks")
.build(
alice_a_provider.rand(),
alice_a_provider.crypto(),
&vc_signer,
|_| true,
)
.expect("build commit")
.stage_commit(&alice_a_provider)
.expect("stage commit");
let confirmation = bundle
.take_confirmation()
.expect("private VC commit carries confirmation");
assert!(
confirmation.generation_id.is_some(),
"a VC-bound private commit carries a generation id"
);
let vc_commit = bundle.into_commit();
let processed = alice_a_main
.process_message(
&alice_a_provider,
vc_commit.clone().into_protocol_message().unwrap(),
)
.expect("alice_a processes her own private VC commit echo");
assert!(matches!(
processed.into_content(),
ProcessedMessageContent::OwnPendingCommit
));
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("alice_a merge VC commit");
process_and_merge_commit(&mut alice_b_main, &alice_b_provider, vc_commit.clone());
process_and_merge_commit(&mut bob_main, &bob_provider, vc_commit);
assert_eq!(
alice_a_main.epoch_authenticator(),
alice_b_main.epoch_authenticator()
);
assert_eq!(
alice_a_main.epoch_authenticator(),
bob_main.epoch_authenticator()
);
let charlie_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(
ciphersuite,
&alice_a_provider,
&charlie_signer,
charlie_credential,
)
.expect("charlie KP build")
.key_package()
.to_owned();
let (proposal_msg, _pref, proposal_confirmation) = alice_a_main
.propose_unconfirmed(
&alice_a_provider,
&vc_signer,
Propose::Add(charlie_kp),
ProposalOrRefType::Reference,
)
.expect("alice_a proposes add charlie");
assert!(proposal_confirmation
.expect("private proposal carries confirmation")
.generation_id
.is_some());
let processed_proposal = alice_b_main
.process_message(
&alice_b_provider,
proposal_msg.into_protocol_message().unwrap(),
)
.expect("alice_b processes alice_a's private proposal");
assert!(processed_proposal.emulator_sender_leaf_index().is_some());
assert!(matches!(
processed_proposal.into_content(),
ProcessedMessageContent::ProposalMessage(_)
));
alice_a_main
.clear_pending_proposals(alice_a_provider.storage())
.expect("clear pending proposal");
let second_bundle = alice_a_main
.commit_builder()
.vc_emulation(
alice_a_provider.crypto(),
alice_a_provider.storage(),
epoch_id.clone(),
)
.expect("vc emulation")
.load_psks(alice_a_provider.storage())
.expect("load psks")
.build(
alice_a_provider.rand(),
alice_a_provider.crypto(),
&vc_signer,
|_| true,
)
.expect("build second commit")
.stage_commit(&alice_a_provider)
.expect("stage second commit");
let second_confirmation = second_bundle
.confirmation()
.expect("private VC commit carries confirmation")
.clone();
alice_a_main
.merge_pending_commit(&alice_a_provider)
.expect("merge second VC commit");
alice_a_main
.confirm_handshake_message(
alice_a_provider.storage(),
second_confirmation.epoch,
second_confirmation.generation,
)
.expect("confirm second VC commit");
}
#[openmls_test::openmls_test]
fn handshake_generation_ids_are_distinct() {
let provider = &Provider::default();
let (alice_credential, alice_signer) =
new_credential(provider, b"Alice", ciphersuite.signature_algorithm());
let (charlie_credential, charlie_signer) =
new_credential(provider, b"Charlie", ciphersuite.signature_algorithm());
let (dave_credential, dave_signer) =
new_credential(provider, b"Dave", ciphersuite.signature_algorithm());
let mut alice_group = new_vc_main_group_with_policy(
ciphersuite,
provider,
&alice_signer,
alice_credential,
MIXED_CIPHERTEXT_WIRE_FORMAT_POLICY,
);
let (mut emulator_group, _emulator_signer) =
make_emulator_group(ciphersuite, provider, b"AliceEmulator");
let _ = send_vc_commit(
&mut alice_group,
&mut emulator_group,
provider,
&alice_signer,
);
let charlie_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, provider, &charlie_signer, charlie_credential)
.expect("charlie KP build")
.key_package()
.to_owned();
let dave_kp = KeyPackage::builder()
.key_package_extensions(Extensions::empty())
.build(ciphersuite, provider, &dave_signer, dave_credential)
.expect("dave KP build")
.key_package()
.to_owned();
let (_msg_a, _ref_a, confirmation_a) = alice_group
.propose_unconfirmed(
provider,
&alice_signer,
Propose::Add(charlie_kp),
ProposalOrRefType::Reference,
)
.expect("first private proposal");
let (_msg_b, _ref_b, confirmation_b) = alice_group
.propose_unconfirmed(
provider,
&alice_signer,
Propose::Add(dave_kp),
ProposalOrRefType::Reference,
)
.expect("second private proposal");
let generation_id_a = confirmation_a
.expect("bound group proposal carries confirmation")
.generation_id
.expect("bound group proposal carries a generation id");
let generation_id_b = confirmation_b
.expect("bound group proposal carries confirmation")
.generation_id
.expect("bound group proposal carries a generation id");
assert_ne!(
generation_id_a, generation_id_b,
"consecutive private handshake sends must yield distinct generation ids"
);
}
#[openmls_test::openmls_test]
fn propose_unconfirmed_group_context_extensions_flow() {
let alice_provider = &Provider::default();
let bob_provider = &Provider::default();
let (mut alice_group, alice_signer, mut bob_group, _bob_signer) =
setup_alice_bob_group_with_policy(
ciphersuite,
alice_provider,
bob_provider,
PURE_CIPHERTEXT_WIRE_FORMAT_POLICY,
);
let epoch = alice_group.epoch();
let (proposal, _pref, confirmation) = alice_group
.propose_unconfirmed(
alice_provider,
&alice_signer,
Propose::GroupContextExtensions(Extensions::empty()),
ProposalOrRefType::Reference,
)
.expect("propose_unconfirmed group context extensions");
let confirmation = confirmation.expect("ciphertext-framed GCE proposal carries confirmation");
assert_eq!(confirmation.epoch, epoch);
assert!(confirmation.generation_id.is_none());
let processed = bob_group
.process_message(bob_provider, proposal.into_protocol_message().unwrap())
.expect("bob processes the GCE proposal");
assert!(matches!(
processed.into_content(),
ProcessedMessageContent::ProposalMessage(_)
));
alice_group
.confirm_handshake_message(
alice_provider.storage(),
confirmation.epoch,
confirmation.generation,
)
.expect("confirm GCE proposal");
}
#[openmls_test::openmls_test]
fn propose_self_update_with_new_signer_unconfirmed_flow() {
let alice_provider = &Provider::default();
let bob_provider = &Provider::default();
let (mut alice_group, alice_signer, mut bob_group, _bob_signer) =
setup_alice_bob_group_with_policy(
ciphersuite,
alice_provider,
bob_provider,
PURE_CIPHERTEXT_WIRE_FORMAT_POLICY,
);
let epoch = alice_group.epoch();
let (new_credential, new_signer_kp) =
new_credential(alice_provider, b"Alice", ciphersuite.signature_algorithm());
let new_signer = NewSignerBundle {
signer: &new_signer_kp,
credential_with_key: new_credential,
};
let (proposal, _pref, confirmation) = alice_group
.propose_self_update_with_new_signer_unconfirmed(
alice_provider,
&alice_signer,
new_signer,
LeafNodeParameters::builder().build(),
)
.expect("propose_self_update_with_new_signer_unconfirmed");
let confirmation =
confirmation.expect("ciphertext-framed update proposal carries confirmation");
assert_eq!(confirmation.epoch, epoch);
let processed = bob_group
.process_message(bob_provider, proposal.into_protocol_message().unwrap())
.expect("bob processes the update proposal");
assert!(matches!(
processed.into_content(),
ProcessedMessageContent::ProposalMessage(_)
));
alice_group
.confirm_handshake_message(
alice_provider.storage(),
confirmation.epoch,
confirmation.generation,
)
.expect("confirm update proposal");
}