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