core_crypto/transaction_context/e2e_identity/
rotate.rs

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