core_crypto/e2e_identity/
rotate.rs

1use super::{Error, Result};
2#[cfg(not(target_family = "wasm"))]
3use crate::e2e_identity::refresh_token::RefreshToken;
4use crate::{
5    MlsError, RecursiveError,
6    context::CentralContext,
7    e2e_identity::init_certificates::NewCrlDistributionPoint,
8    mls::credential::{ext::CredentialExt, x509::CertificatePrivateKey},
9    prelude::{CertificateBundle, E2eiEnrollment, MlsCiphersuite, MlsCredentialType},
10};
11use core_crypto_keystore::{CryptoKeystoreMls, connection::FetchFromDatabase, entities::MlsKeyPackage};
12use openmls::prelude::KeyPackage;
13use openmls_traits::OpenMlsCryptoProvider;
14
15impl CentralContext {
16    /// Generates an E2EI enrollment instance for a "regular" client (with a Basic credential)
17    /// willing to migrate to E2EI. As a consequence, this method does not support changing the
18    /// ClientId which should remain the same as the Basic one.
19    /// Once the enrollment is finished, use the instance in [CentralContext::save_x509_credential]
20    /// to save the new credential.
21    pub async fn e2ei_new_activation_enrollment(
22        &self,
23        display_name: String,
24        handle: String,
25        team: Option<String>,
26        expiry_sec: u32,
27        ciphersuite: MlsCiphersuite,
28    ) -> Result<E2eiEnrollment> {
29        let mls_provider = self
30            .mls_provider()
31            .await
32            .map_err(RecursiveError::root("getting mls provider"))?;
33        // look for existing credential of type basic. If there isn't, then this method has been misused
34        let cb = self
35            .mls_client()
36            .await
37            .map_err(RecursiveError::root("getting mls client"))?
38            .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), MlsCredentialType::Basic)
39            .await
40            .map_err(|_| Error::MissingExistingClient(MlsCredentialType::Basic))?;
41        let client_id = cb.credential().identity().into();
42
43        let sign_keypair = Some((&cb.signature_key).try_into()?);
44
45        E2eiEnrollment::try_new(
46            client_id,
47            display_name,
48            handle,
49            team,
50            expiry_sec,
51            &mls_provider,
52            ciphersuite,
53            sign_keypair,
54            #[cfg(not(target_family = "wasm"))]
55            None, // no x509 credential yet at this point so no OIDC authn yet so no refresh token to restore
56        )
57    }
58
59    /// Generates an E2EI enrollment instance for a E2EI client (with a X509 certificate credential)
60    /// having to change/rotate their credential, either because the former one is expired or it
61    /// has been revoked. As a consequence, this method does not support changing neither ClientId which
62    /// should remain the same as the previous one. It lets you change the DisplayName or the handle
63    /// if you need to. Once the enrollment is finished, use the instance in [CentralContext::save_x509_credential] to do the rotation.
64    pub async fn e2ei_new_rotate_enrollment(
65        &self,
66        display_name: Option<String>,
67        handle: Option<String>,
68        team: Option<String>,
69        expiry_sec: u32,
70        ciphersuite: MlsCiphersuite,
71    ) -> Result<E2eiEnrollment> {
72        let mls_provider = self
73            .mls_provider()
74            .await
75            .map_err(RecursiveError::root("getting mls provider"))?;
76        // look for existing credential of type x509. If there isn't, then this method has been misused
77        let cb = self
78            .mls_client()
79            .await
80            .map_err(RecursiveError::root("getting mls client"))?
81            .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), MlsCredentialType::X509)
82            .await
83            .map_err(|_| Error::MissingExistingClient(MlsCredentialType::X509))?;
84        let client_id = cb.credential().identity().into();
85        let sign_keypair = Some((&cb.signature_key).try_into()?);
86        let existing_identity = cb
87            .to_mls_credential_with_key()
88            .extract_identity(ciphersuite, None)
89            .map_err(RecursiveError::mls_credential("extracting identity"))?
90            .x509_identity
91            .ok_or(Error::ImplementationError)?;
92
93        let display_name = display_name.unwrap_or(existing_identity.display_name);
94        let handle = handle.unwrap_or(existing_identity.handle);
95
96        E2eiEnrollment::try_new(
97            client_id,
98            display_name,
99            handle,
100            team,
101            expiry_sec,
102            &mls_provider,
103            ciphersuite,
104            sign_keypair,
105            #[cfg(not(target_family = "wasm"))]
106            Some(RefreshToken::find(&mls_provider.keystore()).await?), // Since we are renewing an e2ei certificate we MUST have already generated one hence we MUST already have done an OIDC authn and gotten a refresh token from it we also MUST have stored in CoreCrypto
107        )
108    }
109
110    /// Saves a new X509 credential. Requires first
111    /// having enrolled a new X509 certificate with either [CentralContext::e2ei_new_activation_enrollment]
112    /// or [CentralContext::e2ei_new_rotate_enrollment].
113    ///
114    /// # Expected actions to perform after this function (in this order)
115    /// 1. Rotate credentials for each conversation in [crate::mls::conversation::ConversationGuard::e2ei_rotate]
116    /// 2. Generate new key packages with [Client::generate_new_keypackages]
117    /// 3. Use these to replace the stale ones the in the backend
118    /// 4. Delete the stale ones locally using [Self::delete_stale_key_packages]
119    ///     * This is the last step because you might still need the old key packages to avoid
120    ///       an orphan welcome message
121    pub async fn save_x509_credential(
122        &self,
123        enrollment: &mut E2eiEnrollment,
124        certificate_chain: String,
125    ) -> Result<NewCrlDistributionPoint> {
126        let sk = enrollment.get_sign_key_for_mls()?;
127        let cs = enrollment.ciphersuite;
128        let certificate_chain = enrollment
129            .certificate_response(
130                certificate_chain,
131                self.mls_provider()
132                    .await
133                    .map_err(RecursiveError::root("getting mls provider"))?
134                    .authentication_service()
135                    .borrow()
136                    .await
137                    .as_ref()
138                    .ok_or(Error::PkiEnvironmentUnset)?,
139            )
140            .await?;
141
142        let private_key = CertificatePrivateKey {
143            value: sk,
144            signature_scheme: cs.signature_algorithm(),
145        };
146
147        let crl_new_distribution_points = self
148            .extract_dp_on_init(&certificate_chain[..])
149            .await
150            .map_err(RecursiveError::mls_credential("extracting dp on init"))?;
151
152        let cert_bundle = CertificateBundle {
153            certificate_chain,
154            private_key,
155        };
156        let client = &self
157            .mls_client()
158            .await
159            .map_err(RecursiveError::root("getting mls client"))?;
160
161        client
162            .save_new_x509_credential_bundle(
163                &self
164                    .mls_provider()
165                    .await
166                    .map_err(RecursiveError::root("getting mls provider"))?
167                    .keystore(),
168                cs.signature_algorithm(),
169                cert_bundle,
170            )
171            .await
172            .map_err(RecursiveError::mls_client("saving new x509 credential bundle"))?;
173
174        Ok(crl_new_distribution_points)
175    }
176
177    /// Deletes all key packages whose leaf node's credential does not match the most recently
178    /// saved x509 credential with the provided signature scheme.
179    pub async fn delete_stale_key_packages(&self, cipher_suite: MlsCiphersuite) -> Result<()> {
180        let signature_scheme = cipher_suite.signature_algorithm();
181        let keystore = self
182            .keystore()
183            .await
184            .map_err(RecursiveError::root("getting keystore"))?;
185        let nb_kp = keystore.count::<MlsKeyPackage>().await?;
186        let kps: Vec<KeyPackage> = keystore.mls_fetch_keypackages(nb_kp as u32).await?;
187        let client = self
188            .mls_client()
189            .await
190            .map_err(RecursiveError::root("getting mls client"))?;
191
192        let cb = client
193            .find_most_recent_credential_bundle(signature_scheme, MlsCredentialType::X509)
194            .await
195            .map_err(RecursiveError::mls_client("finding most recent credential bundle"))?;
196
197        let mut kp_refs = vec![];
198
199        let provider = self
200            .mls_provider()
201            .await
202            .map_err(RecursiveError::root("getting mls provider"))?;
203        for kp in kps {
204            let kp_cred = kp.leaf_node().credential().mls_credential();
205            let local_cred = cb.credential().mls_credential();
206            if kp_cred != local_cred {
207                let kpr = kp
208                    .hash_ref(provider.crypto())
209                    .map_err(MlsError::wrap("computing keypackage hashref"))?;
210                kp_refs.push(kpr);
211            };
212        }
213        self.delete_keypackages(&kp_refs)
214            .await
215            .map_err(RecursiveError::mls_client("deleting keypackages"))?;
216        Ok(())
217    }
218}
219
220#[cfg(test)]
221// This is pub(crate) because failsafe_ctx() is used in other modules
222pub(crate) mod tests {
223    use std::collections::HashSet;
224
225    use openmls::prelude::SignaturePublicKey;
226    use tls_codec::Deserialize;
227    use wasm_bindgen_test::*;
228
229    use core_crypto_keystore::entities::{EntityFindParams, MlsCredential};
230
231    use crate::{
232        e2e_identity::tests::*,
233        mls::credential::ext::CredentialExt,
234        prelude::key_package::INITIAL_KEYING_MATERIAL_COUNT,
235        test_utils::{x509::X509TestChain, *},
236    };
237
238    use super::*;
239
240    wasm_bindgen_test_configure!(run_in_browser);
241
242    pub(crate) mod all {
243        use crate::prelude::Client;
244        use crate::test_utils::context::TEAM;
245        use openmls_traits::types::SignatureScheme;
246
247        use super::*;
248
249        pub(crate) async fn failsafe_ctx(
250            ctxs: &mut [&mut ClientContext],
251            sc: SignatureScheme,
252        ) -> std::sync::Arc<Option<X509TestChain>> {
253            let mut found_test_chain = None;
254            for ctx in ctxs.iter() {
255                if ctx.x509_test_chain.is_some() {
256                    found_test_chain.replace(ctx.x509_test_chain.clone());
257                    break;
258                }
259            }
260
261            let found_test_chain = found_test_chain.unwrap_or_else(|| Some(X509TestChain::init_empty(sc)).into());
262
263            // Propagate the chain
264            for ctx in ctxs.iter_mut() {
265                if ctx.x509_test_chain.is_none() {
266                    ctx.replace_x509_chain(found_test_chain.clone());
267                }
268            }
269
270            let x509_test_chain = found_test_chain.as_ref().as_ref().unwrap();
271
272            for ctx in ctxs {
273                let _ = x509_test_chain.register_with_central(&ctx.context).await;
274            }
275
276            found_test_chain
277        }
278
279        #[apply(all_cred_cipher)]
280        #[wasm_bindgen_test]
281        async fn enrollment_should_rotate_all(case: TestCase) {
282            run_test_with_client_ids(
283                case.clone(),
284                ["alice", "bob", "charlie"],
285                move |[mut alice_central, mut bob_central, mut charlie_central]| {
286                    Box::pin(async move {
287                        const N: usize = 50;
288                        const NB_KEY_PACKAGE: usize = 50;
289
290                        let mut ids = vec![];
291
292                        let x509_test_chain_arc = failsafe_ctx(
293                            &mut [&mut alice_central, &mut bob_central, &mut charlie_central],
294                            case.signature_scheme(),
295                        )
296                        .await;
297
298                        let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
299
300                        for _ in 0..N {
301                            let id = conversation_id();
302                            alice_central
303                                .context
304                                .new_conversation(&id, case.credential_type, case.cfg.clone())
305                                .await
306                                .unwrap();
307                            alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
308                            ids.push(id)
309                        }
310
311                        // Count the key material before the rotation to compare it later
312                        let before_rotate = alice_central.context.count_entities().await;
313                        assert_eq!(before_rotate.key_package, INITIAL_KEYING_MATERIAL_COUNT);
314
315                        assert_eq!(before_rotate.hpke_private_key, INITIAL_KEYING_MATERIAL_COUNT);
316
317                        // 1 is created per new KeyPackage
318                        assert_eq!(before_rotate.encryption_keypair, INITIAL_KEYING_MATERIAL_COUNT);
319
320                        assert_eq!(before_rotate.credential, 1);
321                        let old_credential = alice_central
322                            .find_most_recent_credential_bundle(case.signature_scheme(), case.credential_type)
323                            .await
324                            .unwrap()
325                            .clone();
326
327                        let is_renewal = case.credential_type == MlsCredentialType::X509;
328
329                        let (mut enrollment, cert) = e2ei_enrollment(
330                            &mut alice_central,
331                            &case,
332                            x509_test_chain,
333                            None,
334                            is_renewal,
335                            init_activation_or_rotation,
336                            noop_restore,
337                        )
338                        .await
339                        .unwrap();
340
341                        alice_central
342                            .context
343                            .save_x509_credential(&mut enrollment, cert)
344                            .await
345                            .unwrap();
346
347                        let cb = alice_central
348                            .find_most_recent_credential_bundle(case.signature_scheme(), MlsCredentialType::X509)
349                            .await
350                            .unwrap();
351
352                        let result = alice_central
353                            .create_key_packages_and_update_credential_in_all_conversations(
354                                &cb,
355                                enrollment.ciphersuite,
356                                NB_KEY_PACKAGE,
357                            )
358                            .await
359                            .unwrap();
360
361                        let after_rotate = alice_central.context.count_entities().await;
362                        // verify we have indeed created the right amount of new X509 KeyPackages
363                        assert_eq!(after_rotate.key_package - before_rotate.key_package, NB_KEY_PACKAGE);
364
365                        // and a new Credential has been persisted in the keystore
366                        assert_eq!(after_rotate.credential - before_rotate.credential, 1);
367
368                        for (id, commit) in result.conversation_ids_and_commits.into_iter() {
369                            let decrypted = bob_central
370                                .context
371                                .conversation(&id)
372                                .await
373                                .unwrap()
374                                .decrypt_message(commit.commit.to_bytes().unwrap())
375                                .await
376                                .unwrap();
377                            alice_central.verify_sender_identity(&case, &decrypted).await;
378
379                            alice_central
380                                .verify_local_credential_rotated(&id, NEW_HANDLE, NEW_DISPLAY_NAME)
381                                .await;
382                        }
383
384                        // Verify that all the new KeyPackages contain the new identity
385                        let new_credentials = result
386                            .new_key_packages
387                            .iter()
388                            .map(|kp| kp.leaf_node().to_credential_with_key());
389                        for c in new_credentials {
390                            assert_eq!(c.credential.credential_type(), openmls::prelude::CredentialType::X509);
391                            let identity = c.extract_identity(case.ciphersuite(), None).unwrap();
392                            assert_eq!(identity.x509_identity.as_ref().unwrap().display_name, NEW_DISPLAY_NAME);
393                            assert_eq!(
394                                identity.x509_identity.as_ref().unwrap().handle,
395                                format!("wireapp://%40{NEW_HANDLE}@world.com")
396                            );
397                        }
398
399                        // Alice has to delete her old KeyPackages
400
401                        // But first let's verify the previous credential material is present
402                        assert!(
403                            alice_central
404                                .find_credential_bundle(
405                                    case.signature_scheme(),
406                                    case.credential_type,
407                                    &old_credential.signature_key.public().into()
408                                )
409                                .await
410                                .is_some()
411                        );
412
413                        // we also have generated the right amount of private encryption keys
414                        let before_delete = alice_central.context.count_entities().await;
415                        assert_eq!(
416                            before_delete.hpke_private_key - before_rotate.hpke_private_key,
417                            NB_KEY_PACKAGE
418                        );
419
420                        // 1 has been created per new KeyPackage created in the rotation
421                        assert_eq!(before_delete.key_package - before_rotate.key_package, NB_KEY_PACKAGE);
422
423                        // and the signature keypair is still present
424                        assert!(
425                            alice_central
426                                .find_signature_keypair_from_keystore(old_credential.signature_key.public())
427                                .await
428                                .is_some()
429                        );
430
431                        // Checks are done, now let's delete ALL the deprecated KeyPackages.
432                        // This should have the consequence to purge the previous credential material as well.
433                        alice_central
434                            .context
435                            .delete_stale_key_packages(case.ciphersuite())
436                            .await
437                            .unwrap();
438
439                        // Alice should just have the number of X509 KeyPackages she requested
440                        let nb_x509_kp = alice_central
441                            .count_key_package(case.ciphersuite(), Some(MlsCredentialType::X509))
442                            .await;
443                        assert_eq!(nb_x509_kp, NB_KEY_PACKAGE);
444                        // in both cases, Alice should not anymore have any Basic KeyPackage
445                        let nb_basic_kp = alice_central
446                            .count_key_package(case.ciphersuite(), Some(MlsCredentialType::Basic))
447                            .await;
448                        assert_eq!(nb_basic_kp, 0);
449
450                        // and since all of Alice's unclaimed KeyPackages have been purged, so should be her old Credential
451
452                        // Also the old Credential has been removed from the keystore
453                        let after_delete = alice_central.context.count_entities().await;
454                        assert_eq!(after_delete.credential, 1);
455                        assert!(
456                            alice_central
457                                .find_credential_from_keystore(&old_credential)
458                                .await
459                                .is_none()
460                        );
461
462                        // and all her Private HPKE keys...
463                        assert_eq!(after_delete.hpke_private_key, NB_KEY_PACKAGE);
464
465                        // ...and encryption keypairs
466                        assert_eq!(
467                            after_rotate.encryption_keypair - after_delete.encryption_keypair,
468                            INITIAL_KEYING_MATERIAL_COUNT
469                        );
470
471                        // Now charlie tries to add Alice to a conversation with her new KeyPackages
472                        let id = conversation_id();
473                        charlie_central
474                            .context
475                            .new_conversation(&id, case.credential_type, case.cfg.clone())
476                            .await
477                            .unwrap();
478                        // required because now Alice does not anymore have a Basic credential
479                        let alice = alice_central
480                            .rand_key_package_of_type(&case, MlsCredentialType::X509)
481                            .await;
482                        charlie_central
483                            .invite_all_members(&case, &id, [(&alice_central, alice)])
484                            .await
485                            .unwrap();
486                    })
487                },
488            )
489            .await
490        }
491
492        #[apply(all_cred_cipher)]
493        #[wasm_bindgen_test]
494        async fn should_restore_credentials_in_order(case: TestCase) {
495            run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
496                Box::pin(async move {
497                    let x509_test_chain_arc = failsafe_ctx(&mut [&mut alice_central], case.signature_scheme()).await;
498
499                    let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
500
501                    let id = conversation_id();
502                    alice_central
503                        .context
504                        .new_conversation(&id, case.credential_type, case.cfg.clone())
505                        .await
506                        .unwrap();
507
508                    let old_cb = alice_central
509                        .find_most_recent_credential_bundle(case.signature_scheme(), case.credential_type)
510                        .await
511                        .unwrap()
512                        .clone();
513
514                    // simulate a real rotation where both credential are not created within the same second
515                    // we only have a precision of 1 second for the `created_at` field of the Credential
516                    async_std::task::sleep(core::time::Duration::from_secs(1)).await;
517
518                    let is_renewal = case.credential_type == MlsCredentialType::X509;
519
520                    let (mut enrollment, cert) = e2ei_enrollment(
521                        &mut alice_central,
522                        &case,
523                        x509_test_chain,
524                        None,
525                        is_renewal,
526                        init_activation_or_rotation,
527                        noop_restore,
528                    )
529                    .await
530                    .unwrap();
531
532                    alice_central
533                        .context
534                        .save_x509_credential(&mut enrollment, cert)
535                        .await
536                        .unwrap();
537
538                    // So alice has a new Credential as expected
539                    let cb = alice_central
540                        .find_most_recent_credential_bundle(case.signature_scheme(), MlsCredentialType::X509)
541                        .await
542                        .unwrap();
543                    let identity = cb
544                        .to_mls_credential_with_key()
545                        .extract_identity(case.ciphersuite(), None)
546                        .unwrap();
547                    assert_eq!(identity.x509_identity.as_ref().unwrap().display_name, NEW_DISPLAY_NAME);
548                    assert_eq!(
549                        identity.x509_identity.as_ref().unwrap().handle,
550                        format!("wireapp://%40{NEW_HANDLE}@world.com")
551                    );
552
553                    // but keeps her old one since it's referenced from some KeyPackages
554                    let old_spk = SignaturePublicKey::from(old_cb.signature_key.public());
555                    let old_cb_found = alice_central
556                        .find_credential_bundle(case.signature_scheme(), case.credential_type, &old_spk)
557                        .await
558                        .unwrap();
559                    assert_eq!(old_cb, old_cb_found);
560                    let (cid, all_credentials, scs, old_nb_identities) = {
561                        let alice_client = alice_central.client().await;
562                        let old_nb_identities = alice_client.identities_count().await.unwrap();
563
564                        // Let's simulate an app crash, client gets deleted and restored from keystore
565                        let cid = alice_client.id().await.unwrap();
566                        let scs = HashSet::from([case.signature_scheme()]);
567                        let all_credentials = alice_central
568                            .context
569                            .keystore()
570                            .await
571                            .unwrap()
572                            .find_all::<MlsCredential>(EntityFindParams::default())
573                            .await
574                            .unwrap()
575                            .into_iter()
576                            .map(|c| {
577                                let credential =
578                                    openmls::prelude::Credential::tls_deserialize(&mut c.credential.as_slice())
579                                        .unwrap();
580                                (credential, c.created_at)
581                            })
582                            .collect::<Vec<_>>();
583                        assert_eq!(all_credentials.len(), 2);
584                        (cid, all_credentials, scs, old_nb_identities)
585                    };
586                    let backend = &alice_central.context.mls_provider().await.unwrap();
587                    backend.keystore().commit_transaction().await.unwrap();
588                    backend.keystore().new_transaction().await.unwrap();
589
590                    let new_client = Client::default();
591
592                    new_client.load(backend, &cid, all_credentials, scs).await.unwrap();
593
594                    // Verify that Alice has the same credentials
595                    let cb = new_client
596                        .find_most_recent_credential_bundle(case.signature_scheme(), MlsCredentialType::X509)
597                        .await
598                        .unwrap();
599                    let identity = cb
600                        .to_mls_credential_with_key()
601                        .extract_identity(case.ciphersuite(), None)
602                        .unwrap();
603
604                    assert_eq!(identity.x509_identity.as_ref().unwrap().display_name, NEW_DISPLAY_NAME);
605                    assert_eq!(
606                        identity.x509_identity.as_ref().unwrap().handle,
607                        format!("wireapp://%40{NEW_HANDLE}@world.com")
608                    );
609
610                    assert_eq!(new_client.identities_count().await.unwrap(), old_nb_identities);
611                })
612            })
613            .await
614        }
615
616        #[apply(all_cred_cipher)]
617        #[wasm_bindgen_test]
618        async fn rotate_should_roundtrip(case: TestCase) {
619            run_test_with_client_ids(
620                case.clone(),
621                ["alice", "bob"],
622                move |[mut alice_central, mut bob_central]| {
623                    Box::pin(async move {
624                        let x509_test_chain_arc =
625                            failsafe_ctx(&mut [&mut alice_central, &mut bob_central], case.signature_scheme()).await;
626
627                        let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
628
629                        let id = conversation_id();
630                        alice_central
631                            .context
632                            .new_conversation(&id, case.credential_type, case.cfg.clone())
633                            .await
634                            .unwrap();
635
636                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
637                        // Alice's turn
638                        const ALICE_NEW_HANDLE: &str = "new_alice_wire";
639                        const ALICE_NEW_DISPLAY_NAME: &str = "New Alice Smith";
640
641                        fn init_alice(wrapper: E2eiInitWrapper) -> InitFnReturn<'_> {
642                            Box::pin(async move {
643                                let E2eiInitWrapper { context: cc, case } = wrapper;
644                                let cs = case.ciphersuite();
645                                match case.credential_type {
646                                    MlsCredentialType::Basic => {
647                                        cc.e2ei_new_activation_enrollment(
648                                            ALICE_NEW_DISPLAY_NAME.to_string(),
649                                            ALICE_NEW_HANDLE.to_string(),
650                                            Some(TEAM.to_string()),
651                                            E2EI_EXPIRY,
652                                            cs,
653                                        )
654                                        .await
655                                    }
656                                    MlsCredentialType::X509 => {
657                                        cc.e2ei_new_rotate_enrollment(
658                                            Some(ALICE_NEW_DISPLAY_NAME.to_string()),
659                                            Some(ALICE_NEW_HANDLE.to_string()),
660                                            Some(TEAM.to_string()),
661                                            E2EI_EXPIRY,
662                                            cs,
663                                        )
664                                        .await
665                                    }
666                                }
667                            })
668                        }
669
670                        let is_renewal = case.credential_type == MlsCredentialType::X509;
671
672                        let (mut enrollment, cert) = e2ei_enrollment(
673                            &mut alice_central,
674                            &case,
675                            x509_test_chain,
676                            None,
677                            is_renewal,
678                            init_alice,
679                            noop_restore,
680                        )
681                        .await
682                        .unwrap();
683
684                        alice_central
685                            .context
686                            .save_x509_credential(&mut enrollment, cert)
687                            .await
688                            .unwrap();
689                        alice_central
690                            .context
691                            .conversation(&id)
692                            .await
693                            .unwrap()
694                            .e2ei_rotate(None)
695                            .await
696                            .unwrap();
697
698                        let commit = alice_central.mls_transport.latest_commit().await;
699
700                        let decrypted = bob_central
701                            .context
702                            .conversation(&id)
703                            .await
704                            .unwrap()
705                            .decrypt_message(commit.to_bytes().unwrap())
706                            .await
707                            .unwrap();
708                        alice_central.verify_sender_identity(&case, &decrypted).await;
709
710                        alice_central
711                            .verify_local_credential_rotated(&id, ALICE_NEW_HANDLE, ALICE_NEW_DISPLAY_NAME)
712                            .await;
713
714                        // Bob's turn
715                        const BOB_NEW_HANDLE: &str = "new_bob_wire";
716                        const BOB_NEW_DISPLAY_NAME: &str = "New Bob Smith";
717
718                        fn init_bob(wrapper: E2eiInitWrapper) -> InitFnReturn<'_> {
719                            Box::pin(async move {
720                                let E2eiInitWrapper { context: cc, case } = wrapper;
721                                let cs = case.ciphersuite();
722                                match case.credential_type {
723                                    MlsCredentialType::Basic => {
724                                        cc.e2ei_new_activation_enrollment(
725                                            BOB_NEW_DISPLAY_NAME.to_string(),
726                                            BOB_NEW_HANDLE.to_string(),
727                                            Some(TEAM.to_string()),
728                                            E2EI_EXPIRY,
729                                            cs,
730                                        )
731                                        .await
732                                    }
733                                    MlsCredentialType::X509 => {
734                                        cc.e2ei_new_rotate_enrollment(
735                                            Some(BOB_NEW_DISPLAY_NAME.to_string()),
736                                            Some(BOB_NEW_HANDLE.to_string()),
737                                            Some(TEAM.to_string()),
738                                            E2EI_EXPIRY,
739                                            cs,
740                                        )
741                                        .await
742                                    }
743                                }
744                            })
745                        }
746                        let is_renewal = case.credential_type == MlsCredentialType::X509;
747
748                        let (mut enrollment, cert) = e2ei_enrollment(
749                            &mut bob_central,
750                            &case,
751                            x509_test_chain,
752                            None,
753                            is_renewal,
754                            init_bob,
755                            noop_restore,
756                        )
757                        .await
758                        .unwrap();
759
760                        bob_central
761                            .context
762                            .save_x509_credential(&mut enrollment, cert)
763                            .await
764                            .unwrap();
765
766                        bob_central
767                            .context
768                            .conversation(&id)
769                            .await
770                            .unwrap()
771                            .e2ei_rotate(None)
772                            .await
773                            .unwrap();
774
775                        let commit = bob_central.mls_transport.latest_commit().await;
776
777                        let decrypted = alice_central
778                            .context
779                            .conversation(&id)
780                            .await
781                            .unwrap()
782                            .decrypt_message(commit.to_bytes().unwrap())
783                            .await
784                            .unwrap();
785                        bob_central.verify_sender_identity(&case, &decrypted).await;
786
787                        bob_central
788                            .verify_local_credential_rotated(&id, BOB_NEW_HANDLE, BOB_NEW_DISPLAY_NAME)
789                            .await;
790                    })
791                },
792            )
793            .await
794        }
795    }
796
797    mod one {
798        use super::*;
799        use crate::mls::conversation::Conversation as _;
800
801        #[apply(all_cred_cipher)]
802        #[wasm_bindgen_test]
803        pub async fn should_rotate_one_conversations_credential(case: TestCase) {
804            if case.is_x509() {
805                run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
806                    Box::pin(async move {
807                        let id = conversation_id();
808                        alice_central
809                            .context
810                            .new_conversation(&id, case.credential_type, case.cfg.clone())
811                            .await
812                            .unwrap();
813
814                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
815
816                        let init_count = alice_central.context.count_entities().await;
817                        let x509_test_chain = alice_central.x509_test_chain.as_ref().as_ref().unwrap();
818
819                        let intermediate_ca = x509_test_chain.find_local_intermediate_ca();
820                        let alice_og_cert = &x509_test_chain
821                            .actors
822                            .iter()
823                            .find(|actor| actor.name == "alice")
824                            .unwrap()
825                            .certificate;
826
827                        // Alice creates a new Credential, updating her handle/display_name
828                        let alice_cid = alice_central.get_client_id().await;
829                        let (new_handle, new_display_name) = ("new_alice_wire", "New Alice Smith");
830                        let cb = alice_central
831                            .save_new_credential(&case, new_handle, new_display_name, alice_og_cert, intermediate_ca)
832                            .await;
833
834                        // Verify old identity is still there in the MLS group
835                        let alice_old_identities = alice_central
836                            .context
837                            .conversation(&id)
838                            .await
839                            .unwrap()
840                            .get_device_identities(&[alice_cid])
841                            .await
842                            .unwrap();
843                        let alice_old_identity = alice_old_identities.first().unwrap();
844                        assert_ne!(
845                            alice_old_identity.x509_identity.as_ref().unwrap().display_name,
846                            new_display_name
847                        );
848                        assert_ne!(
849                            alice_old_identity.x509_identity.as_ref().unwrap().handle,
850                            format!("{new_handle}@world.com")
851                        );
852
853                        // Alice issues an Update commit to replace her current identity
854                        alice_central
855                            .context
856                            .conversation(&id)
857                            .await
858                            .unwrap()
859                            .e2ei_rotate(Some(&cb))
860                            .await
861                            .unwrap();
862                        let commit = alice_central.mls_transport.latest_commit().await;
863
864                        // Bob decrypts the commit...
865                        let decrypted = bob_central
866                            .context
867                            .conversation(&id)
868                            .await
869                            .unwrap()
870                            .decrypt_message(commit.to_bytes().unwrap())
871                            .await
872                            .unwrap();
873                        // ...and verifies that now Alice is represented with her new identity
874                        alice_central.verify_sender_identity(&case, &decrypted).await;
875
876                        // Finally, Alice merges her commit and verifies her new identity gets applied
877                        alice_central
878                            .verify_local_credential_rotated(&id, new_handle, new_display_name)
879                            .await;
880
881                        let final_count = alice_central.context.count_entities().await;
882                        assert_eq!(init_count.encryption_keypair, final_count.encryption_keypair);
883                        assert_eq!(
884                            init_count.epoch_encryption_keypair,
885                            final_count.epoch_encryption_keypair
886                        );
887                        assert_eq!(init_count.key_package, final_count.key_package);
888                    })
889                })
890                .await
891            }
892        }
893
894        #[apply(all_cred_cipher)]
895        #[wasm_bindgen_test]
896        pub async fn rotate_should_be_renewable_when_commit_denied(case: TestCase) {
897            if !case.is_x509() {
898                return;
899            }
900            run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
901                Box::pin(async move {
902                    let id = conversation_id();
903                    alice_central
904                        .context
905                        .new_conversation(&id, case.credential_type, case.cfg.clone())
906                        .await
907                        .unwrap();
908
909                    alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
910
911                    let init_count = alice_central.context.count_entities().await;
912
913                    let x509_test_chain = alice_central.x509_test_chain.as_ref().as_ref().unwrap();
914
915                    let intermediate_ca = x509_test_chain.find_local_intermediate_ca();
916
917                    // In this case Alice will try to rotate her credential but her commit will be denied
918                    // by the backend (because another commit from Bob had precedence)
919
920                    // Alice creates a new Credential, updating her handle/display_name
921                    let (new_handle, new_display_name) = ("new_alice_wire", "New Alice Smith");
922                    let cb = alice_central
923                        .save_new_credential(
924                            &case,
925                            new_handle,
926                            new_display_name,
927                            x509_test_chain.find_certificate_for_actor("alice").unwrap(),
928                            intermediate_ca,
929                        )
930                        .await;
931
932                    // Alice issues an Update commit to replace her current identity
933                    let _rotate_commit = alice_central.create_unmerged_e2ei_rotate_commit(&id, &cb).await;
934
935                    // Meanwhile, Bob creates a simple commit
936                    bob_central
937                        .context
938                        .conversation(&id)
939                        .await
940                        .unwrap()
941                        .update_key_material()
942                        .await
943                        .unwrap();
944                    // accepted by the backend
945                    let bob_commit = bob_central.mls_transport.latest_commit().await;
946
947                    // Alice decrypts the commit...
948                    let decrypted = alice_central
949                        .context
950                        .conversation(&id)
951                        .await
952                        .unwrap()
953                        .decrypt_message(bob_commit.to_bytes().unwrap())
954                        .await
955                        .unwrap();
956
957                    // Alice's previous rotate commit should have been renewed so that she can re-commit it
958                    assert_eq!(decrypted.proposals.len(), 1);
959                    let renewed_proposal = decrypted.proposals.first().unwrap();
960                    bob_central
961                        .context
962                        .conversation(&id)
963                        .await
964                        .unwrap()
965                        .decrypt_message(renewed_proposal.proposal.to_bytes().unwrap())
966                        .await
967                        .unwrap();
968
969                    alice_central
970                        .context
971                        .conversation(&id)
972                        .await
973                        .unwrap()
974                        .commit_pending_proposals()
975                        .await
976                        .unwrap();
977
978                    // Finally, Alice merges her commit and verifies her new identity gets applied
979                    alice_central
980                        .verify_local_credential_rotated(&id, new_handle, new_display_name)
981                        .await;
982
983                    let rotate_commit = alice_central.mls_transport.latest_commit().await;
984                    // Bob verifies that now Alice is represented with her new identity
985                    let decrypted = bob_central
986                        .context
987                        .conversation(&id)
988                        .await
989                        .unwrap()
990                        .decrypt_message(rotate_commit.to_bytes().unwrap())
991                        .await
992                        .unwrap();
993                    alice_central.verify_sender_identity(&case, &decrypted).await;
994
995                    let final_count = alice_central.context.count_entities().await;
996                    assert_eq!(init_count.encryption_keypair, final_count.encryption_keypair);
997                    // TODO: there is no efficient way to clean a credential when alice merges her pending commit. Tracking issue: WPB-9594
998                    // One option would be to fetch all conversations and see if Alice is never represented with the said Credential
999                    // but let's be honest this is not very efficient.
1000                    // The other option would be to get rid of having an implicit KeyPackage for the creator of a conversation
1001                    // assert_eq!(init_count.credential, final_count.credential);
1002                })
1003            })
1004            .await
1005        }
1006
1007        #[apply(all_cred_cipher)]
1008        #[wasm_bindgen_test]
1009        pub async fn rotate_should_replace_existing_basic_credentials(case: TestCase) {
1010            if case.is_x509() {
1011                run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1012                    Box::pin(async move {
1013                        let id = conversation_id();
1014                        alice_central
1015                            .context
1016                            .new_conversation(&id, MlsCredentialType::Basic, case.cfg.clone())
1017                            .await
1018                            .unwrap();
1019
1020                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1021
1022                        let x509_test_chain = alice_central.x509_test_chain.as_ref().as_ref().unwrap();
1023                        let intermediate_ca = x509_test_chain.find_local_intermediate_ca();
1024                        let alice_og_cert = &x509_test_chain
1025                            .actors
1026                            .iter()
1027                            .find(|actor| actor.name == "alice")
1028                            .unwrap()
1029                            .certificate;
1030
1031                        // Alice creates a new Credential, updating her handle/display_name
1032                        let alice_cid = alice_central.get_client_id().await;
1033                        let (new_handle, new_display_name) = ("new_alice_wire", "New Alice Smith");
1034                        alice_central
1035                            .save_new_credential(&case, new_handle, new_display_name, alice_og_cert, intermediate_ca)
1036                            .await;
1037
1038                        // Verify old identity is a basic identity in the MLS group
1039                        let alice_old_identities = alice_central
1040                            .context
1041                            .conversation(&id)
1042                            .await
1043                            .unwrap()
1044                            .get_device_identities(&[alice_cid])
1045                            .await
1046                            .unwrap();
1047                        let alice_old_identity = alice_old_identities.first().unwrap();
1048                        assert_eq!(alice_old_identity.credential_type, MlsCredentialType::Basic);
1049                        assert_eq!(alice_old_identity.x509_identity, None);
1050
1051                        // Alice issues an Update commit to replace her current identity
1052                        alice_central
1053                            .context
1054                            .conversation(&id)
1055                            .await
1056                            .unwrap()
1057                            .e2ei_rotate(None)
1058                            .await
1059                            .unwrap();
1060                        let commit = alice_central.mls_transport.latest_commit().await;
1061
1062                        // Bob decrypts the commit...
1063                        let decrypted = bob_central
1064                            .context
1065                            .conversation(&id)
1066                            .await
1067                            .unwrap()
1068                            .decrypt_message(commit.to_bytes().unwrap())
1069                            .await
1070                            .unwrap();
1071                        // ...and verifies that now Alice is represented with her new identity
1072                        alice_central.verify_sender_identity(&case, &decrypted).await;
1073
1074                        // Finally, Alice merges her commit and verifies her new identity gets applied
1075                        alice_central
1076                            .verify_local_credential_rotated(&id, new_handle, new_display_name)
1077                            .await;
1078                    })
1079                })
1080                .await
1081            }
1082        }
1083    }
1084}