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