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, self_signing_key: Arc, + pinned_master_key: Arc>, +} + +/// 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, + #[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 for OtherUserIdentityData { + type Error = serde_json::Error; + fn try_from( + value: OtherUserIdentityDataSerializer, + ) -> Result { + 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 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 { 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") + } +}