From 421bdc7badc914f5e4b8f444bd767e4efcc2af81 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Jul 2024 09:33:18 +0200 Subject: [PATCH] feature(crypto): Add support for master key local pinning --- .../src/identities/manager.rs | 128 +++++++- .../matrix-sdk-crypto/src/identities/user.rs | 287 +++++++++++++++++- .../src/test_json/keys_query_sets.rs | 216 ++++++++++++- 3 files changed, 619 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index dfbb4a47d55..699dfcade88 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -542,6 +542,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 = ReadOnlyUserIdentity::new(master_key, self_signing)?; Ok(identity.into()) } @@ -1329,7 +1330,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, @@ -1893,4 +1894,129 @@ pub(crate) mod tests { .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(); + + // There should be now an identity and no pin violation (pinned msk is the + // current one) + assert!(!other_identity.has_pin_violation()); + let first_device = manager + .store + .get_readonly_device(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_readonly_device(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_readonly_device(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 a new keys update 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 serve a new one + // after that 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_mismatch() { + 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 misatch 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 b719f4bfa86..0a6c5ebdf6e 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, Mutex, }, }; @@ -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}; @@ -291,6 +292,36 @@ impl UserIdentity { methods, ) } + + /// Pin the current identity (Master public key). + pub async fn pin_current_master_key(&self) -> Result<(), CryptoStoreError> { + self.inner.pin(); + let to_save = ReadOnlyUserIdentities::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(()) + } + + /// An identity mismatch is detected when there is a trust problem with the + /// user identity. There is an identity mismatch if the current identity + /// is not verified and there is a pinning violation. An identity + /// mismatch must be reported to the user, and can be resolved by: + /// - Verifying the new identity (see + /// [`UserIdentity::request_verification`]) + /// - Or by updating the pinned key + /// ([`UserIdentity::pin_current_master_key`]). + pub fn has_identity_mismatch(&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. @@ -371,10 +402,87 @@ impl ReadOnlyUserIdentities { /// only contain a master key and a self signing key, meaning that only device /// signatures can be checked with this identity. #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(from = "ReadOnlyUserIdentitySerializer", into = "ReadOnlyUserIdentitySerializer")] pub struct ReadOnlyUserIdentity { user_id: OwnedUserId, pub(crate) master_key: Arc, self_signing_key: Arc, + /// The first time a cryptographic identity is seen for a given user, it + /// will be associated to that user (i.e pinned). Future interaction + /// will expect this user crypto identity to stay the same, + /// this will help prevent some MITM attacks. + /// In case of identity change, it will be possible to pin the new identity + /// is the user wants. + pinned_msk: Arc>, +} + +/// Intermediate struct to help serialize ReadOnlyUserIdentity and support +/// versioning and migration. +/// Version v1 is adding support for identity pinning (`pinned_msk`), as part +/// of migration we just pin the currently known msk. +#[derive(Deserialize, Serialize)] +struct ReadOnlyUserIdentitySerializer { + version: Option, + #[serde(flatten)] + other: Value, +} + +#[derive(Debug, Deserialize, Serialize)] +struct ReadOnlyUserIdentityV0 { + user_id: OwnedUserId, + master_key: MasterPubkey, + self_signing_key: SelfSigningPubkey, +} + +#[derive(Debug, Deserialize, Serialize)] +struct ReadOnlyUserIdentityV1 { + user_id: OwnedUserId, + master_key: MasterPubkey, + self_signing_key: SelfSigningPubkey, + pinned_msk: MasterPubkey, +} + +impl From for ReadOnlyUserIdentity { + fn from(value: ReadOnlyUserIdentitySerializer) -> Self { + match value.version { + None => { + // Old format, migrate the pinned identity + let v0: ReadOnlyUserIdentityV0 = serde_json::from_value(value.other).unwrap(); + ReadOnlyUserIdentity { + 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 msk + pinned_msk: Arc::new(Mutex::new(v0.master_key.clone())), + } + } + _ => { + // v1 format + let v1: ReadOnlyUserIdentityV1 = serde_json::from_value(value.other).unwrap(); + ReadOnlyUserIdentity { + user_id: v1.user_id, + master_key: Arc::new(v1.master_key.clone()), + self_signing_key: Arc::new(v1.self_signing_key), + pinned_msk: Arc::new(Mutex::new(v1.pinned_msk)), + } + } + } + } +} + +impl From for ReadOnlyUserIdentitySerializer { + fn from(value: ReadOnlyUserIdentity) -> Self { + let v1 = ReadOnlyUserIdentityV1 { + user_id: value.user_id.clone(), + master_key: value.master_key().to_owned(), + self_signing_key: value.self_signing_key().to_owned(), + pinned_msk: value.pinned_msk.lock().unwrap().clone(), + }; + ReadOnlyUserIdentitySerializer { + version: Some("1".to_owned()), + other: serde_json::to_value(v1).unwrap(), + } + } } impl PartialEq for ReadOnlyUserIdentity { @@ -417,19 +525,24 @@ impl ReadOnlyUserIdentity { 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_msk: Mutex::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_msk: Arc::new(Mutex::new(master_key.clone())), + } } /// Get the user id of this identity. @@ -447,6 +560,24 @@ impl ReadOnlyUserIdentity { &self.self_signing_key } + /// Pin the current identity + pub(crate) fn pin(&self) { + let mut m = self.pinned_msk.lock().unwrap(); + *m = self.master_key.as_ref().clone() + } + + /// 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_msk = self.pinned_msk.lock().unwrap().clone(); + pinned_msk.get_first_key() != self.master_key().get_first_key() + } + /// Update the identity with a new master key and self signing key. /// /// # Arguments @@ -465,7 +596,15 @@ impl ReadOnlyUserIdentity { ) -> Result { master_key.verify_subkey(&self_signing_key)?; - let new = Self::new(master_key, self_signing_key)?; + // The pin is maintained. + let pinned_msk = self.pinned_msk.lock().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_msk: Mutex::new(pinned_msk).into(), + }; let changed = new != *self; *self = new; @@ -786,8 +925,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}; + 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; @@ -796,11 +938,16 @@ pub(crate) mod tests { ReadOnlyOwnUserIdentity, ReadOnlyUserIdentities, }; use crate::{ - identities::{manager::testing::own_key_query, Device}, + identities::{ + manager::testing::own_key_query, + user::{ReadOnlyUserIdentitySerializer, ReadOnlyUserIdentityV1}, + Device, + }, olm::{Account, PrivateCrossSigningIdentity}, store::{CryptoStoreWrapper, MemoryStore}, types::{CrossSigningKey, MasterPubkey, SelfSigningPubkey, Signatures, UserSigningPubkey}, verification::VerificationMachine, + OlmMachine, ReadOnlyUserIdentity, }; #[test] @@ -861,6 +1008,58 @@ 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: ReadOnlyUserIdentity = serde_json::from_value(serialized_value).unwrap(); + + { + let pinned_msk = migrated.clone().pinned_msk; + assert_eq!(pinned_msk.lock().unwrap().clone(), migrated.master_key.as_ref().clone()); + } + + // Serialize back + let value = serde_json::to_value(migrated.clone()).unwrap(); + + // Should be serialized with latest version + let _: ReadOnlyUserIdentityV1 = + serde_json::from_value(value.clone()).expect("Should deserialize as version 1"); + + let with_serializer: ReadOnlyUserIdentitySerializer = + serde_json::from_value(value).unwrap(); + assert_eq!("1", with_serializer.version.unwrap()); + } + #[test] fn own_identity_check_signatures() { let response = own_key_query(); @@ -1015,4 +1214,74 @@ pub(crate) mod tests { [second_device_id] ); } + + #[async_test] + async fn resolve_identity_mismacth_with_verification() { + use test_json::keys_query_sets::IdentityChangeDataSet as DataSet; + + 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(); + + 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; + + println!("USK ID: {}", usk_key_id); + 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(); + + // Simulate an identity hange + 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(); + + let other_user_id = DataSet::user_id(); + + let other_identity = + machine.get_identity(other_user_id, None).await.unwrap().unwrap().other().unwrap(); + + // There should be an identity mismatch + assert!(other_identity.has_identity_mismatch()); + + // Manually verify for the purpose of this test + let sig_upload = other_identity.verify().await.unwrap(); + + let raw_extracted = + sig_upload.signed_keys.get(other_user_id).unwrap().iter().next().unwrap().1.get(); + + let new_signature: CrossSigningKey = serde_json::from_str(raw_extracted).unwrap(); + + let mut msk_to_update: CrossSigningKey = + serde_json::from_value(DataSet::msk_b().get("@bob:localhost").unwrap().clone()) + .unwrap(); + + 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(); + + // There should not be an identity mismatch anymore + let other_identity = + machine.get_identity(other_user_id, None).await.unwrap().unwrap().other().unwrap(); + assert!(!other_identity.has_identity_mismatch()); + // 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..755369902bf 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,215 @@ 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 + 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 a new identity (Ib) and a new device `ATWKQFSFRN`. + /// `ATWKQFSFRN` is signed with the new identity but + 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", + "unsigned": { + "device_display_name": "develop.element.io: Chrome on macOS" + } + } + } + }, + "failures": {}, + })); + KeyQueryResponse::try_from_http_response(data) + .expect("Can't parse the `/keys/upload` response") + } +}