diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs
index 1fd7de79e9d..b6d032e3722 100644
--- a/crates/matrix-sdk-crypto/src/identities/manager.rs
+++ b/crates/matrix-sdk-crypto/src/identities/manager.rs
@@ -512,6 +512,7 @@ impl IdentityManager {
             *changed_private_identity = self.check_private_identity(&identity).await;
             Ok(identity.into())
         } else {
+            // First time seen, create the identity. The current MSK will be pinned.
             let identity = OtherUserIdentityData::new(master_key, self_signing)?;
             Ok(identity.into())
         }
@@ -1338,7 +1339,7 @@ pub(crate) mod tests {
     use std::ops::Deref;
 
     use futures_util::pin_mut;
-    use matrix_sdk_test::{async_test, response_from_file};
+    use matrix_sdk_test::{async_test, response_from_file, test_json};
     use ruma::{
         api::{client::keys::get_keys::v3::Response as KeysQueryResponse, IncomingResponse},
         device_id, user_id, TransactionId,
@@ -1897,4 +1898,129 @@ pub(crate) mod tests {
 
         manager.store.get_device_data(other_user, device_id!("OBEBOSKTBE")).await.unwrap().unwrap();
     }
+
+    #[async_test]
+    async fn test_manager_identity_updates() {
+        use test_json::keys_query_sets::IdentityChangeDataSet as DataSet;
+
+        let manager = manager_test_helper(user_id(), device_id()).await;
+        let other_user = DataSet::user_id();
+        let devices = manager.store.get_user_devices(other_user).await.unwrap();
+        assert_eq!(devices.devices().count(), 0);
+
+        let identity = manager.store.get_user_identity(other_user).await.unwrap();
+        assert!(identity.is_none());
+
+        manager
+            .receive_keys_query_response(
+                &TransactionId::new(),
+                &DataSet::key_query_with_identity_a(),
+            )
+            .await
+            .unwrap();
+
+        let identity = manager.store.get_user_identity(other_user).await.unwrap().unwrap();
+        let other_identity = identity.other().unwrap();
+
+        // We should now have an identity for the user but no pin violation
+        // (pinned master key is the current one)
+        assert!(!other_identity.has_pin_violation());
+        let first_device = manager
+            .store
+            .get_device_data(other_user, DataSet::first_device_id())
+            .await
+            .unwrap()
+            .unwrap();
+        assert!(first_device.is_cross_signed_by_owner(&identity));
+
+        // We receive a new keys update for that user, with a new identity
+        manager
+            .receive_keys_query_response(
+                &TransactionId::new(),
+                &DataSet::key_query_with_identity_b(),
+            )
+            .await
+            .unwrap();
+
+        let identity = manager.store.get_user_identity(other_user).await.unwrap().unwrap();
+        let other_identity = identity.other().unwrap();
+
+        // The previous known identity has been replaced, there should be a pin
+        // violation
+        assert!(other_identity.has_pin_violation());
+
+        let second_device = manager
+            .store
+            .get_device_data(other_user, DataSet::second_device_id())
+            .await
+            .unwrap()
+            .unwrap();
+
+        // There is a new device signed by the new identity
+        assert!(second_device.is_cross_signed_by_owner(&identity));
+
+        // The first device should not be signed by the new identity
+        let first_device = manager
+            .store
+            .get_device_data(other_user, DataSet::first_device_id())
+            .await
+            .unwrap()
+            .unwrap();
+        assert!(!first_device.is_cross_signed_by_owner(&identity));
+
+        let remember_previous_identity = other_identity.clone();
+        // We receive updated keys for that user, with no identity anymore.
+        // Notice that there is no server API to delete identity, but we want to
+        // test here that a home server cannot clear the identity and
+        // subsequently serve a new one which would get automatically approved.
+        manager
+            .receive_keys_query_response(
+                &TransactionId::new(),
+                &DataSet::key_query_with_identity_no_identity(),
+            )
+            .await
+            .unwrap();
+
+        let identity = manager.store.get_user_identity(other_user).await.unwrap().unwrap();
+        let other_identity = identity.other().unwrap();
+
+        assert_eq!(other_identity, &remember_previous_identity);
+        assert!(other_identity.has_pin_violation());
+    }
+
+    #[async_test]
+    async fn test_manager_resolve_identity_pin_violation() {
+        use test_json::keys_query_sets::IdentityChangeDataSet as DataSet;
+
+        let manager = manager_test_helper(user_id(), device_id()).await;
+        let other_user = DataSet::user_id();
+
+        manager
+            .receive_keys_query_response(
+                &TransactionId::new(),
+                &DataSet::key_query_with_identity_a(),
+            )
+            .await
+            .unwrap();
+
+        // We receive a new keys update for that user, with a new identity
+        manager
+            .receive_keys_query_response(
+                &TransactionId::new(),
+                &DataSet::key_query_with_identity_b(),
+            )
+            .await
+            .unwrap();
+
+        let identity = manager.store.get_user_identity(other_user).await.unwrap().unwrap();
+        let other_identity = identity.other().unwrap();
+
+        // We have a new identity now, so there should be a pin violation
+        assert!(other_identity.has_pin_violation());
+
+        // Resolve the violation by pinning the new identity
+        other_identity.pin();
+
+        assert!(!other_identity.has_pin_violation());
+    }
 }
diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs
index 93b7f4bcffc..88f6ce878a6 100644
--- a/crates/matrix-sdk-crypto/src/identities/user.rs
+++ b/crates/matrix-sdk-crypto/src/identities/user.rs
@@ -17,7 +17,7 @@ use std::{
     ops::Deref,
     sync::{
         atomic::{AtomicBool, Ordering},
-        Arc,
+        Arc, RwLock,
     },
 };
 
@@ -30,6 +30,7 @@ use ruma::{
     DeviceId, EventId, OwnedDeviceId, OwnedUserId, RoomId, UserId,
 };
 use serde::{Deserialize, Serialize};
+use serde_json::Value;
 use tracing::error;
 
 use super::{atomic_bool_deserializer, atomic_bool_serializer};
@@ -295,6 +296,42 @@ impl UserIdentity {
             methods,
         )
     }
+
+    /// Pin the current identity (public part of the master signing key).
+    pub async fn pin_current_master_key(&self) -> Result<(), CryptoStoreError> {
+        self.inner.pin();
+        let to_save = UserIdentityData::Other(self.inner.clone());
+        let changes = Changes {
+            identities: IdentityChanges { changed: vec![to_save], ..Default::default() },
+            ..Default::default()
+        };
+        self.verification_machine.store.inner().save_changes(changes).await?;
+        Ok(())
+    }
+
+    /// Did the identity change after an initial observation in a way that
+    /// requires approval from the user?
+    ///
+    /// A user identity needs approval if it changed after the crypto machine
+    /// has already observed ("pinned") a different identity for that user *and*
+    /// it is not an explicitly verified identity (using for example interactive
+    /// verification).
+    ///
+    /// Such a change is to be considered a pinning violation which the
+    /// application should report to the local user, and can be resolved by:
+    ///
+    /// - Verifying the new identity with [`UserIdentity::request_verification`]
+    /// - Or by updating the pin to the new identity with
+    ///   [`UserIdentity::pin_current_master_key`].
+    pub fn identity_needs_user_approval(&self) -> bool {
+        // First check if the current identity is verified.
+        if self.is_verified() {
+            return false;
+        }
+        // If not we can check the pinned identity. Verification always have
+        // higher priority than pinning.
+        self.inner.has_pin_violation()
+    }
 }
 
 /// Enum over the different user identity types we can have.
@@ -376,11 +413,97 @@ impl UserIdentityData {
 /// This is the user identity of a user that isn't our own. Other users will
 /// only contain a master key and a self signing key, meaning that only device
 /// signatures can be checked with this identity.
+///
+/// This struct also contains the currently pinned user identity (public master
+/// key) for that user.
+///
+/// The first time a cryptographic user identity is seen for a given user, it
+/// will be associated with that user ("pinned"). Future interactions
+/// will expect this identity to stay the same, to avoid MITM attacks from the
+/// homeserver.
+///
+/// The user can explicitly pin the new identity to allow for legitimate
+/// identity changes (for example, in case of key material or device loss).
 #[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(try_from = "OtherUserIdentityDataSerializer", into = "OtherUserIdentityDataSerializer")]
 pub struct OtherUserIdentityData {
     user_id: OwnedUserId,
     pub(crate) master_key: Arc<MasterPubkey>,
     self_signing_key: Arc<SelfSigningPubkey>,
+    pinned_master_key: Arc<RwLock<MasterPubkey>>,
+}
+
+/// Intermediate struct to help serialize OtherUserIdentityData and support
+/// versioning and migration.
+///
+/// Version v1 is adding support for identity pinning (`pinned_master_key`), as
+/// part of migration we just pin the currently known public master key.
+#[derive(Deserialize, Serialize)]
+struct OtherUserIdentityDataSerializer {
+    version: Option<String>,
+    #[serde(flatten)]
+    other: Value,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+struct OtherUserIdentityDataSerializerV0 {
+    user_id: OwnedUserId,
+    master_key: MasterPubkey,
+    self_signing_key: SelfSigningPubkey,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+struct OtherUserIdentityDataSerializerV1 {
+    user_id: OwnedUserId,
+    master_key: MasterPubkey,
+    self_signing_key: SelfSigningPubkey,
+    pinned_master_key: MasterPubkey,
+}
+
+impl TryFrom<OtherUserIdentityDataSerializer> for OtherUserIdentityData {
+    type Error = serde_json::Error;
+    fn try_from(
+        value: OtherUserIdentityDataSerializer,
+    ) -> Result<OtherUserIdentityData, Self::Error> {
+        match value.version {
+            None => {
+                // Old format, migrate the pinned identity
+                let v0: OtherUserIdentityDataSerializerV0 = serde_json::from_value(value.other)?;
+                Ok(OtherUserIdentityData {
+                    user_id: v0.user_id,
+                    master_key: Arc::new(v0.master_key.clone()),
+                    self_signing_key: Arc::new(v0.self_signing_key),
+                    // We migrate by pinning the current master key
+                    pinned_master_key: Arc::new(RwLock::new(v0.master_key)),
+                })
+            }
+            Some(v) if v == "1" => {
+                let v1: OtherUserIdentityDataSerializerV1 = serde_json::from_value(value.other)?;
+                Ok(OtherUserIdentityData {
+                    user_id: v1.user_id,
+                    master_key: Arc::new(v1.master_key.clone()),
+                    self_signing_key: Arc::new(v1.self_signing_key),
+                    pinned_master_key: Arc::new(RwLock::new(v1.pinned_master_key)),
+                })
+            }
+            _ => Err(serde::de::Error::custom(format!("Unsupported Version {:?}", value.version))),
+        }
+    }
+}
+
+impl From<OtherUserIdentityData> for OtherUserIdentityDataSerializer {
+    fn from(value: OtherUserIdentityData) -> Self {
+        let v1 = OtherUserIdentityDataSerializerV1 {
+            user_id: value.user_id.clone(),
+            master_key: value.master_key().to_owned(),
+            self_signing_key: value.self_signing_key().to_owned(),
+            pinned_master_key: value.pinned_master_key.read().unwrap().clone(),
+        };
+        OtherUserIdentityDataSerializer {
+            version: Some("1".to_owned()),
+            other: serde_json::to_value(v1).unwrap(),
+        }
+    }
 }
 
 impl PartialEq for OtherUserIdentityData {
@@ -423,19 +546,24 @@ impl OtherUserIdentityData {
 
         Ok(Self {
             user_id: master_key.user_id().into(),
-            master_key: master_key.into(),
+            master_key: master_key.clone().into(),
             self_signing_key: self_signing_key.into(),
+            pinned_master_key: RwLock::new(master_key).into(),
         })
     }
 
     #[cfg(test)]
     pub(crate) async fn from_private(identity: &crate::olm::PrivateCrossSigningIdentity) -> Self {
-        let master_key =
-            identity.master_key.lock().await.as_ref().unwrap().public_key().clone().into();
+        let master_key = identity.master_key.lock().await.as_ref().unwrap().public_key().clone();
         let self_signing_key =
             identity.self_signing_key.lock().await.as_ref().unwrap().public_key().clone().into();
 
-        Self { user_id: identity.user_id().into(), master_key, self_signing_key }
+        Self {
+            user_id: identity.user_id().into(),
+            master_key: Arc::new(master_key.clone()),
+            self_signing_key,
+            pinned_master_key: Arc::new(RwLock::new(master_key.clone())),
+        }
     }
 
     /// Get the user id of this identity.
@@ -453,6 +581,26 @@ impl OtherUserIdentityData {
         &self.self_signing_key
     }
 
+    /// Pin the current identity
+    pub(crate) fn pin(&self) {
+        let mut m = self.pinned_master_key.write().unwrap();
+        *m = self.master_key.as_ref().clone()
+    }
+
+    /// Returns true if the identity has changed since we last pinned it.
+    ///
+    /// Key pinning acts as a trust on first use mechanism, the first time an
+    /// identity is known for a user it will be pinned.
+    /// For future interaction with a user, the identity is expected to be the
+    /// one that was pinned. In case of identity change the UI client should
+    /// receive reports of pinning violation and decide to act accordingly;
+    /// that is accept and pin the new identity, perform a verification or
+    /// stop communications.
+    pub(crate) fn has_pin_violation(&self) -> bool {
+        let pinned_master_key = self.pinned_master_key.read().unwrap();
+        pinned_master_key.get_first_key() != self.master_key().get_first_key()
+    }
+
     /// Update the identity with a new master key and self signing key.
     ///
     /// # Arguments
@@ -471,7 +619,18 @@ impl OtherUserIdentityData {
     ) -> Result<bool, SignatureError> {
         master_key.verify_subkey(&self_signing_key)?;
 
-        let new = Self::new(master_key, self_signing_key)?;
+        // We update the identity with the new master and self signing key, but we keep
+        // the previous pinned master key.
+        // This identity will have a pin violation until the new master key is pinned
+        // (see `has_pin_violation()`).
+        let pinned_master_key = self.pinned_master_key.read().unwrap().clone();
+
+        let new = Self {
+            user_id: master_key.user_id().into(),
+            master_key: master_key.clone().into(),
+            self_signing_key: self_signing_key.into(),
+            pinned_master_key: RwLock::new(pinned_master_key).into(),
+        };
         let changed = new != *self;
 
         *self = new;
@@ -792,8 +951,11 @@ pub(crate) mod tests {
     use std::{collections::HashMap, sync::Arc};
 
     use assert_matches::assert_matches;
-    use matrix_sdk_test::async_test;
-    use ruma::{device_id, user_id, UserId};
+    use matrix_sdk_test::{async_test, response_from_file, test_json};
+    use ruma::{
+        api::{client::keys::get_keys::v3::Response as KeyQueryResponse, IncomingResponse},
+        device_id, user_id, TransactionId,
+    };
     use serde_json::{json, Value};
     use tokio::sync::Mutex;
 
@@ -802,16 +964,16 @@ pub(crate) mod tests {
         OwnUserIdentityData, UserIdentityData,
     };
     use crate::{
-        identities::{manager::testing::own_key_query, Device},
-        machine::tests::{
-            get_machine_pair, mark_alice_identity_as_verified_test_helper,
-            setup_cross_signing_for_machine_test_helper,
+        identities::{
+            manager::testing::own_key_query,
+            user::{OtherUserIdentityDataSerializer, OtherUserIdentityDataSerializerV1},
+            Device,
         },
         olm::{Account, PrivateCrossSigningIdentity},
-        store::{Changes, CryptoStoreWrapper, MemoryStore},
+        store::{CryptoStoreWrapper, MemoryStore},
         types::{CrossSigningKey, MasterPubkey, SelfSigningPubkey, Signatures, UserSigningPubkey},
         verification::VerificationMachine,
-        OlmMachine,
+        OlmMachine, OtherUserIdentityData,
     };
 
     #[test]
@@ -872,6 +1034,56 @@ pub(crate) mod tests {
         get_other_identity();
     }
 
+    #[test]
+    fn deserialization_migration_test() {
+        let serialized_value = json!({
+                "user_id":"@example2:localhost",
+                "master_key":{
+                   "user_id":"@example2:localhost",
+                   "usage":[
+                      "master"
+                   ],
+                   "keys":{
+                      "ed25519:kC/HmRYw4HNqUp/i4BkwYENrf+hd9tvdB7A1YOf5+Do":"kC/HmRYw4HNqUp/i4BkwYENrf+hd9tvdB7A1YOf5+Do"
+                   },
+                   "signatures":{
+                      "@example2:localhost":{
+                         "ed25519:SKISMLNIMH":"KdUZqzt8VScGNtufuQ8lOf25byYLWIhmUYpPENdmM8nsldexD7vj+Sxoo7PknnTX/BL9h2N7uBq0JuykjunCAw"
+                      }
+                   }
+                },
+                "self_signing_key":{
+                   "user_id":"@example2:localhost",
+                   "usage":[
+                      "self_signing"
+                   ],
+                   "keys":{
+                      "ed25519:ZtFrSkJ1qB8Jph/ql9Eo/lKpIYCzwvKAKXfkaS4XZNc":"ZtFrSkJ1qB8Jph/ql9Eo/lKpIYCzwvKAKXfkaS4XZNc"
+                   },
+                   "signatures":{
+                      "@example2:localhost":{
+                         "ed25519:kC/HmRYw4HNqUp/i4BkwYENrf+hd9tvdB7A1YOf5+Do":"W/O8BnmiUETPpH02mwYaBgvvgF/atXnusmpSTJZeUSH/vHg66xiZOhveQDG4cwaW8iMa+t9N4h1DWnRoHB4mCQ"
+                      }
+                   }
+                }
+        });
+        let migrated: OtherUserIdentityData = serde_json::from_value(serialized_value).unwrap();
+
+        let pinned_master_key = migrated.pinned_master_key.read().unwrap();
+        assert_eq!(*pinned_master_key, migrated.master_key().clone());
+
+        // Serialize back
+        let value = serde_json::to_value(migrated.clone()).unwrap();
+
+        // Should be serialized with latest version
+        let _: OtherUserIdentityDataSerializerV1 =
+            serde_json::from_value(value.clone()).expect("Should deserialize as version 1");
+
+        let with_serializer: OtherUserIdentityDataSerializer =
+            serde_json::from_value(value).unwrap();
+        assert_eq!("1", with_serializer.version.unwrap());
+    }
+
     #[test]
     fn own_identity_check_signatures() {
         let response = own_key_query();
@@ -1027,86 +1239,72 @@ pub(crate) mod tests {
         );
     }
 
-    async fn get_machine_pair_with_signed_identities(
-        alice: &UserId,
-        bob: &UserId,
-    ) -> (OlmMachine, OlmMachine) {
-        let (alice, bob, _) = get_machine_pair(alice, bob, false).await;
-        setup_cross_signing_for_machine_test_helper(&alice, &bob).await;
-        mark_alice_identity_as_verified_test_helper(&alice, &bob).await;
+    #[async_test]
+    async fn resolve_identity_pin_violation_with_verification() {
+        use test_json::keys_query_sets::IdentityChangeDataSet as DataSet;
 
-        (alice, bob)
-    }
+        let my_user_id = user_id!("@me:localhost");
+        let machine = OlmMachine::new(my_user_id, device_id!("ABCDEFGH")).await;
+        machine.bootstrap_cross_signing(false).await.unwrap();
 
-    #[async_test]
-    async fn test_other_user_is_verified_if_my_identity_is_verified_and_they_are_cross_signed() {
-        let alice_user_id = user_id!("@alice:localhost");
-        let bob_user_id = user_id!("@bob:localhost");
-        let (alice, bob) =
-            get_machine_pair_with_signed_identities(alice_user_id, bob_user_id).await;
+        let my_id = machine.get_identity(my_user_id, None).await.unwrap().unwrap().own().unwrap();
+        let usk_key_id = my_id.inner.user_signing_key().keys().iter().next().unwrap().0;
 
-        let bobs_own_identity =
-            bob.get_identity(bob.user_id(), None).await.unwrap().unwrap().own().unwrap();
-        let bobs_alice_identity =
-            bob.get_identity(alice.user_id(), None).await.unwrap().unwrap().other().unwrap();
+        let keys_query = DataSet::key_query_with_identity_a();
+        let txn_id = TransactionId::new();
+        machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
 
-        assert!(bobs_own_identity.is_verified(), "Bob's identity should be verified.");
-        assert!(bobs_alice_identity.is_verified(), "Alice's identity should be verified as well.");
-    }
+        // Simulate an identity change
+        let keys_query = DataSet::key_query_with_identity_b();
+        let txn_id = TransactionId::new();
+        machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
 
-    #[async_test]
-    async fn test_other_user_is_not_verified_if_they_are_not_cross_signed() {
-        let alice_user_id = user_id!("@alice:localhost");
-        let bob_user_id = user_id!("@bob:localhost");
-        let (alice, bob, _) = get_machine_pair(alice_user_id, bob_user_id, false).await;
-        setup_cross_signing_for_machine_test_helper(&alice, &bob).await;
-
-        let bobs_own_identity =
-            bob.get_identity(bob.user_id(), None).await.unwrap().unwrap().own().unwrap();
-        let bobs_alice_identity =
-            bob.get_identity(alice.user_id(), None).await.unwrap().unwrap().other().unwrap();
-
-        assert!(bobs_own_identity.is_verified(), "Bob's identity should be verified.");
-        assert!(
-            !bobs_alice_identity.is_verified(),
-            "Alice's identity should not be considered verified since Bob has not signed it."
-        );
-    }
+        let other_user_id = DataSet::user_id();
 
-    #[async_test]
-    async fn test_other_user_is_not_verified_if_my_identity_is_not_verified() {
-        let alice_user_id = user_id!("@alice:localhost");
-        let bob_user_id = user_id!("@bob:localhost");
+        let other_identity =
+            machine.get_identity(other_user_id, None).await.unwrap().unwrap().other().unwrap();
 
-        let (alice, bob, _) = get_machine_pair(alice_user_id, bob_user_id, false).await;
-        setup_cross_signing_for_machine_test_helper(&alice, &bob).await;
-        mark_alice_identity_as_verified_test_helper(&alice, &bob).await;
+        // The identity should need user approval now
+        assert!(other_identity.identity_needs_user_approval());
 
-        let bobs_own_identity =
-            bob.get_identity(bob.user_id(), None).await.unwrap().unwrap().own().unwrap();
-        let bobs_alice_identity =
-            bob.get_identity(alice.user_id(), None).await.unwrap().unwrap().other().unwrap();
+        // Manually verify for the purpose of this test
+        let sig_upload = other_identity.verify().await.unwrap();
 
-        assert!(bobs_own_identity.is_verified(), "Bob's identity should be verified.");
-        assert!(bobs_alice_identity.is_verified(), "Alice's identity should be verified as well.");
+        let raw_extracted =
+            sig_upload.signed_keys.get(other_user_id).unwrap().iter().next().unwrap().1.get();
 
-        bobs_own_identity.mark_as_unverified();
+        let new_signature: CrossSigningKey = serde_json::from_str(raw_extracted).unwrap();
 
-        bob.store()
-            .save_changes(Changes {
-                identities: crate::store::IdentityChanges {
-                    changed: vec![bobs_own_identity.inner.clone().into()],
-                    ..Default::default()
-                },
-                ..Default::default()
-            })
-            .await
-            .unwrap();
+        let mut msk_to_update: CrossSigningKey =
+            serde_json::from_value(DataSet::msk_b().get("@bob:localhost").unwrap().clone())
+                .unwrap();
 
-        assert!(!bobs_own_identity.is_verified(), "Bob's identity should not be verified anymore.");
-        assert!(
-            !bobs_alice_identity.is_verified(),
-            "Alice's identity should not be verified either."
+        msk_to_update.signatures.add_signature(
+            my_user_id.to_owned(),
+            usk_key_id.to_owned(),
+            new_signature.signatures.get_signature(my_user_id, usk_key_id).unwrap(),
         );
+
+        // we want to update bob device keys with the new signature
+        let data = json!({
+                "device_keys": {}, // For the purpose of this test we don't need devices here
+                "failures": {},
+                "master_keys": {
+                    DataSet::user_id(): msk_to_update
+        ,
+                },
+                "self_signing_keys": DataSet::ssk_b(),
+            });
+
+        let kq_response = KeyQueryResponse::try_from_http_response(response_from_file(&data))
+            .expect("Can't parse the `/keys/upload` response");
+        machine.mark_request_as_sent(&TransactionId::new(), &kq_response).await.unwrap();
+
+        // The identity should not need any user approval now
+        let other_identity =
+            machine.get_identity(other_user_id, None).await.unwrap().unwrap().other().unwrap();
+        assert!(!other_identity.identity_needs_user_approval());
+        // But there is still a pin violation
+        assert!(other_identity.inner.has_pin_violation());
     }
 }
diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs
index c4e06e190fc..bf5c1a3321e 100644
--- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs
+++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs
@@ -2,7 +2,7 @@ use ruma::{
     api::{client::keys::get_keys::v3::Response as KeyQueryResponse, IncomingResponse},
     device_id, user_id, DeviceId, UserId,
 };
-use serde_json::json;
+use serde_json::{json, Value};
 
 use crate::response_from_file;
 
@@ -105,7 +105,7 @@ impl KeyDistributionTestData {
     /// but not the other one `FRGNMZVOKA`.
     /// `@dan` identity is signed by `@me` identity (alice trust dan)
     pub fn dan_keys_query_response() -> KeyQueryResponse {
-        let data: serde_json::Value = json!({
+        let data: Value = json!({
                 "device_keys": {
                     "@dan:localhost": {
                         "JHPUERYQUW": {
@@ -457,3 +457,213 @@ impl KeyDistributionTestData {
         user_id!("@good:localhost")
     }
 }
+
+/// A set of keys query to test identity changes,
+/// For user @bob, several payloads with no identities then identity A and B.
+pub struct IdentityChangeDataSet {}
+
+#[allow(dead_code)]
+impl IdentityChangeDataSet {
+    pub fn user_id() -> &'static UserId {
+        user_id!("@bob:localhost")
+    }
+
+    pub fn first_device_id() -> &'static DeviceId {
+        device_id!("GYKSNAWLVK")
+    }
+
+    pub fn second_device_id() -> &'static DeviceId {
+        device_id!("ATWKQFSFRN")
+    }
+
+    pub fn third_device_id() -> &'static DeviceId {
+        device_id!("OPABMDDXGX")
+    }
+
+    fn device_keys_payload_1_signed_by_a() -> Value {
+        json!({
+            "algorithms": [
+                "m.olm.v1.curve25519-aes-sha2",
+                "m.megolm.v1.aes-sha2"
+            ],
+            "device_id": "GYKSNAWLVK",
+            "keys": {
+                "curve25519:GYKSNAWLVK": "dBcZBzQaiQYWf6rBPh2QypIOB/dxSoTeyaFaxNNbeHs",
+                "ed25519:GYKSNAWLVK": "6melQNnhoI9sT2b4VzNPAwa8aB179ym45fON8Yo7kVk"
+            },
+            "signatures": {
+                "@bob:localhost": {
+                    "ed25519:GYKSNAWLVK": "Fk45zHAbrd+1j9wZXLjL2Y/+DU/Mnz9yuvlfYBOOT7qExN2Jdud+5BAuNs8nZ/caS4wTF39Kg3zQpzaGERoCBg",
+                    "ed25519:dO4gmBNW7WC0bXBK81j8uh4me6085fP+keoOm0pH3gw": "md0Pa1MYlneFb1fp6KCsvZpi2ySb6/G+ULoCbQDWBeDxNEcoNMzf7PEKY04UToCZKUU4LifvRWmiWFDanOlkCQ"
+                }
+            },
+            "user_id": "@bob:localhost",
+        })
+    }
+
+    fn msk_a() -> Value {
+        json!({
+            "@bob:localhost": {
+                "keys": {
+                    "ed25519:/mULSzYNTdHJOBWnBmsvDHhqdHQcWnXRHHmqwzwC7DY": "/mULSzYNTdHJOBWnBmsvDHhqdHQcWnXRHHmqwzwC7DY"
+                },
+                "signatures": {
+                    "@bob:localhost": {
+                        "ed25519:/mULSzYNTdHJOBWnBmsvDHhqdHQcWnXRHHmqwzwC7DY": "6vGDbPO5XzlcwbU3aV+kcck+iHHEBtX85ow2gW5U05/DZdtda/JNVa5Nn7B9lQHNnnrMqt1sX00y/JrIkSS1Aw",
+                        "ed25519:GYKSNAWLVK": "jLxmUPr0Ny2Ai9+NGKGhed9BAuKikOc7r6gr7MQVawePYS95w8NJ8Tzaq9zFFOmIiojACNdQ/ksy3QAdwD6vBQ"
+                    }
+                },
+                "usage": [
+                    "master"
+                ],
+                "user_id": "@bob:localhost"
+            }
+        })
+    }
+    fn ssk_a() -> Value {
+        json!({
+            "@bob:localhost": {
+                "keys": {
+                    "ed25519:dO4gmBNW7WC0bXBK81j8uh4me6085fP+keoOm0pH3gw": "dO4gmBNW7WC0bXBK81j8uh4me6085fP+keoOm0pH3gw"
+                },
+                "signatures": {
+                    "@bob:localhost": {
+                        "ed25519:/mULSzYNTdHJOBWnBmsvDHhqdHQcWnXRHHmqwzwC7DY": "7md6mwjUK8zjintmffJ0+kImC59/Y8PdySy99EZz5Neu+VMX3LT7txhKO2gC/hmDduRw+JGfGXIiDxR7GmQqDw"
+                    }
+                },
+                "usage": [
+                    "self_signing"
+                ],
+                "user_id": "@bob:localhost"
+            }
+        })
+    }
+    /// A key query with an identity (Ia), and a first device `GYKSNAWLVK`
+    /// signed by Ia.
+    pub fn key_query_with_identity_a() -> KeyQueryResponse {
+        let data = response_from_file(&json!({
+            "device_keys": {
+                "@bob:localhost": {
+                    "GYKSNAWLVK": Self::device_keys_payload_1_signed_by_a()
+                }
+            },
+            "failures": {},
+            "master_keys": Self::msk_a(),
+            "self_signing_keys": Self::ssk_a(),
+            "user_signing_keys": {}
+        }));
+        KeyQueryResponse::try_from_http_response(data)
+            .expect("Can't parse the `/keys/upload` response")
+    }
+
+    pub fn msk_b() -> Value {
+        json!({
+            "@bob:localhost": {
+                "keys": {
+                    "ed25519:NmI78hY54kE7OZsIjbRE/iCox59t4nzScCNEO6fvtY4": "NmI78hY54kE7OZsIjbRE/iCox59t4nzScCNEO6fvtY4"
+                },
+                "signatures": {
+                    "@bob:localhost": {
+                        "ed25519:ATWKQFSFRN": "MBOzCKYPQLQMpBY2lFZJ4c8451xJfQCdhPBb1AHlTUSxKFiWi6V+k1oRRnhQein/PjkIY7ZO+HoOrIeOtbRMAw",
+                        "ed25519:NmI78hY54kE7OZsIjbRE/iCox59t4nzScCNEO6fvtY4": "xqLhC3sIUci1W2CNVW7HZWXreQApgjv2RDwB0WPiMd1P4vbZ/qJM0KWqK2piGPWliPi8YVREMrg216KXM3IhCA"
+                    }
+                },
+                "usage": [
+                    "master"
+                ],
+                "user_id": "@bob:localhost"
+            }
+        })
+    }
+
+    pub fn ssk_b() -> Value {
+        json!({
+            "@bob:localhost": {
+                "keys": {
+                    "ed25519:At1ai1VUZrCncCI7V7fEAJmBShfpqZ30xRzqcEjTjdc": "At1ai1VUZrCncCI7V7fEAJmBShfpqZ30xRzqcEjTjdc"
+                },
+                "signatures": {
+                    "@bob:localhost": {
+                        "ed25519:NmI78hY54kE7OZsIjbRE/iCox59t4nzScCNEO6fvtY4": "Ls6CeoA4LoPCHuSwG96kbhd1dEV09TgdMROIZi6vFz/MT9Wtik6joQi/tQ3zCwIZCSR53ksLO4jG1DD31AiBAA"
+                    }
+                },
+                "usage": [
+                    "self_signing"
+                ],
+                "user_id": "@bob:localhost"
+            }
+        })
+    }
+
+    pub fn device_keys_payload_2_signed_by_b() -> Value {
+        json!({
+            "algorithms": [
+                "m.olm.v1.curve25519-aes-sha2",
+                "m.megolm.v1.aes-sha2"
+            ],
+            "device_id": "ATWKQFSFRN",
+            "keys": {
+                "curve25519:ATWKQFSFRN": "CY0TWVK1/Kj3ZADuBcGe3UKvpT+IKAPMUsMeJhSDqno",
+                "ed25519:ATWKQFSFRN": "TyTQqd6j2JlWZh97r+kTYuCbvqnPoNwO6EGovYsjY00"
+            },
+            "signatures": {
+                "@bob:localhost": {
+                    "ed25519:ATWKQFSFRN": "BQ9Gp0p+6srF+c8OyruqKKd9R4yaub3THYAyyBB/7X/rG8BwcAqFynzl1aGyFYun4Q+087a5OSiglCXI+/kQAA",
+                    "ed25519:At1ai1VUZrCncCI7V7fEAJmBShfpqZ30xRzqcEjTjdc": "TWmDPaG7t0rZ6luauonELD3dmBDTIRryqXhgsIQRiGint2rJdic8RVyZ6a61bgu6mtBjfvU3prqMNp6sVi16Cg"
+                }
+            },
+            "user_id": "@bob:localhost",
+        })
+    }
+    /// A key query with a new identity (Ib) and a new device `ATWKQFSFRN`.
+    /// `ATWKQFSFRN` is signed with the new identity but `GYKSNAWLVK` is still
+    /// signed by the old identity (Ia).
+    pub fn key_query_with_identity_b() -> KeyQueryResponse {
+        let data = response_from_file(&json!({
+            "device_keys": {
+                "@bob:localhost": {
+                    "ATWKQFSFRN": Self::device_keys_payload_2_signed_by_b(),
+                    "GYKSNAWLVK": Self::device_keys_payload_1_signed_by_a(),
+                }
+            },
+            "failures": {},
+            "master_keys": Self::msk_b(),
+            "self_signing_keys": Self::ssk_b(),
+        }));
+        KeyQueryResponse::try_from_http_response(data)
+            .expect("Can't parse the `/keys/upload` response")
+    }
+
+    /// A key query with no identity and a new device `OPABMDDXGX` (not
+    /// cross-signed).
+    pub fn key_query_with_identity_no_identity() -> KeyQueryResponse {
+        let data = response_from_file(&json!({
+            "device_keys": {
+                "@bob:localhost": {
+                    "ATWKQFSFRN": Self::device_keys_payload_2_signed_by_b(),
+                    "GYKSNAWLVK": Self::device_keys_payload_1_signed_by_a(),
+                    "OPABMDDXGX": {
+                        "algorithms": [
+                            "m.olm.v1.curve25519-aes-sha2",
+                            "m.megolm.v1.aes-sha2"
+                        ],
+                        "device_id": "OPABMDDXGX",
+                        "keys": {
+                            "curve25519:OPABMDDXGX": "O6bwa9Op0E+PQPCrbTOfdYwU+j95RRPhXIHuNpe94ns",
+                            "ed25519:OPABMDDXGX": "DvjkSNOM9XrR1gWrr2YSDvTnwnLIgKDMRr5v8HgMKak"
+                        },
+                        "signatures": {
+                            "@bob:localhost": {
+                                "ed25519:OPABMDDXGX": "o+BBnw/SIJWxSf799Adq6jEl9X3lwCg5MJkS8GlfId+pW3ReEETK0l+9bhCAgBsNSKRtB/fmZQBhjMx4FJr+BA"
+                            }
+                        },
+                        "user_id": "@bob:localhost",
+                    }
+                }
+            },
+            "failures": {},
+        }));
+        KeyQueryResponse::try_from_http_response(data)
+            .expect("Can't parse the `/keys/upload` response")
+    }
+}