diff --git a/Cargo.lock b/Cargo.lock index 9ae5eb6e07c..9b88c01de76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3468,6 +3468,7 @@ dependencies = [ "ctor", "getrandom", "http", + "insta", "matrix-sdk-common", "matrix-sdk-test-macros", "once_cell", @@ -3476,6 +3477,7 @@ dependencies = [ "serde_json", "tokio", "tracing-subscriber", + "vodozemac", "wasm-bindgen-test", "wiremock", ] diff --git a/testing/matrix-sdk-test/Cargo.toml b/testing/matrix-sdk-test/Cargo.toml index 75b8e2953f8..7e4dc493f16 100644 --- a/testing/matrix-sdk-test/Cargo.toml +++ b/testing/matrix-sdk-test/Cargo.toml @@ -18,13 +18,15 @@ doctest = false [dependencies] as_variant = { workspace = true } http = { workspace = true } +insta = { workspace = true } matrix-sdk-common = { path = "../../crates/matrix-sdk-common" } matrix-sdk-test-macros = { version = "0.7.0", path = "../matrix-sdk-test-macros" } once_cell = { workspace = true } # Enabling the unstable feature for polls support. -ruma = { workspace = true, features = ["rand", "unstable-msc3381"] } +ruma = { workspace = true, features = ["canonical-json", "rand", "unstable-msc3381"] } serde = { workspace = true } serde_json = { workspace = true } +vodozemac = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ctor = "0.2.9" 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 1ee8591162c..9c56dd1526b 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 @@ -1,8 +1,13 @@ +use std::default::Default; + +use insta::{assert_json_snapshot, with_settings}; use ruma::{ api::client::keys::get_keys::v3::Response as KeyQueryResponse, device_id, - encryption::DeviceKeys, serde::Raw, user_id, DeviceId, OwnedDeviceId, UserId, + encryption::DeviceKeys, serde::Raw, user_id, CanonicalJsonValue, DeviceId, OwnedDeviceId, + OwnedUserId, UserId, }; use serde_json::{json, Value}; +use vodozemac::{Curve25519PublicKey, Ed25519SecretKey, Ed25519Signature}; use super::keys_query::{keys_query, master_keys, KeysQueryUser}; use crate::{ @@ -10,9 +15,216 @@ use crate::{ test_json::keys_query::{device_keys_payload, self_signing_keys}, }; +/// A test helper for building sets of test user data. +/// +/// Currently, its main purpose is to build `/keys/query` response objects +/// ([`KeyQueryResponse`]), but that may evolve in future. +pub struct TestUserDataBuilder { + /// The User ID of the user that this test data is about. + pub user_id: OwnedUserId, + + /// The user's private master cross-signing key, once it has been set via + /// [`TestUserDataBuilder::with_cross_signing_keys`]. + pub master_cross_signing_key: Option, + + /// The user's private self-signing key, once it has been set via + /// [`TestUserDataBuilder::with_cross_signing_keys`]. + pub self_signing_key: Option, + + /// The user's private user-signing key, once it has been set via + /// [`TestUserDataBuilder::with_cross_signing_keys`]. + pub user_signing_key: Option, + + /// The JSON representation of the user's public master cross-signing key, + /// ready for return in the `/keys/query` response. + /// + /// This starts off as `null`, but is populated with the correct JSON object + /// when the master key is set. It accumulates additional signatures + /// when the key is cross-signed + /// via [`TestUserDataBuilder::with_user_verification_signature`]. + master_cross_signing_key_json: Value, + + /// The JSON object containing the public, signed, device keys, added via + /// [`TestUserDataBuilder::with_device`]. + device_keys: serde_json::Map, +} + +impl TestUserDataBuilder { + /// Create a new [`TestUserDataBuilder`] for the given user. + pub fn new(user_id: OwnedUserId) -> Self { + TestUserDataBuilder { + user_id, + master_cross_signing_key: None, + self_signing_key: None, + user_signing_key: None, + master_cross_signing_key_json: Value::Null, + device_keys: Default::default(), + } + } + + /// Add a set of cross-signing keys to the data to be returned. + /// + /// The private keys must be provided here so that signatures can be + /// correctly calculated. + pub fn with_cross_signing_keys( + mut self, + master_cross_signing_key: Ed25519SecretKey, + self_signing_key: Ed25519SecretKey, + user_signing_key: Ed25519SecretKey, + ) -> Self { + // For the master key, we build the JSON representation upfront, so that we can + // start to accumulate signatures. For the other keys, we generate the + // JSON representation on-demand. + let public_master_key_base64 = master_cross_signing_key.public_key().to_base64(); + let mut master_key_json = json!({ + "keys": { + format!("ed25519:{}", public_master_key_base64): public_master_key_base64, + }, + "signatures": {}, + "usage": [ "master" ], + "user_id": self.user_id.clone(), + }); + + // Sign the master key with itself + sign_json( + &mut master_key_json, + &master_cross_signing_key, + &self.user_id, + &public_master_key_base64, + ); + + self.master_cross_signing_key_json = master_key_json; + self.master_cross_signing_key = Some(master_cross_signing_key); + self.self_signing_key = Some(self_signing_key); + self.user_signing_key = Some(user_signing_key); + + self + } + + /// Add a device to the data to be returned. + /// + /// As well as a device ID and public curve25519 device key, the *private* + /// ed25519 device key must be provided so that the signature can be + /// calculated. + /// + /// The device can optionally be signed by the self-signing key by setting + /// `cross_signed` to `true`. + pub fn with_device( + mut self, + device_id: &DeviceId, + curve25519_public_key: &Curve25519PublicKey, + ed25519_secret_key: &Ed25519SecretKey, + cross_signed: bool, + ) -> Self { + let mut device_keys = json!({ + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": device_id.to_owned(), + "keys": { + format!("curve25519:{}", device_id): curve25519_public_key.to_base64(), + format!("ed25519:{}", device_id): ed25519_secret_key.public_key().to_base64(), + }, + "signatures": {}, + "user_id": self.user_id.clone(), + }); + + sign_json(&mut device_keys, ed25519_secret_key, &self.user_id, device_id.as_str()); + if cross_signed { + let ssk = self + .self_signing_key + .as_ref() + .expect("must call with_cross_signing_keys() before creating cross-signed device"); + sign_json(&mut device_keys, ssk, &self.user_id, &ssk.public_key().to_base64()); + } + + self.device_keys.insert(device_id.to_string(), device_keys); + self + } + + /// Add the signature from another user to our master key, as would happen + /// if that user had verified us. + pub fn with_user_verification_signature( + mut self, + signing_user_id: &UserId, + signing_user_user_signing_key: &Ed25519SecretKey, + ) -> Self { + sign_json( + &mut self.master_cross_signing_key_json, + signing_user_user_signing_key, + signing_user_id, + &signing_user_user_signing_key.public_key().to_base64(), + ); + self + } + + /// Build the JSON for a `/keys/query` response containing this user's data. + pub fn build_keys_query_response_json(&self) -> Value { + let mut data: Value = json!({}); + + if !self.device_keys.is_empty() { + data["device_keys"] = json!({ + self.user_id.as_str(): self.device_keys.clone(), + }); + } + + if self.master_cross_signing_key_json.is_object() { + data["master_keys"] = + json!({self.user_id.as_str(): self.master_cross_signing_key_json.clone()}); + } + + if let Some(self_signing_key) = &self.self_signing_key { + let public_key_base64 = self_signing_key.public_key().to_base64(); + let mut self_signing_key_json = json!({ + "keys": { + format!("ed25519:{}", public_key_base64): public_key_base64, + }, + "signatures": {}, + "usage": [ "self_signing" ], + "user_id": self.user_id.clone(), + }); + + if let Some(master_key) = &self.master_cross_signing_key { + // Sign self-signing key with master key + sign_json( + &mut self_signing_key_json, + master_key, + &self.user_id, + &master_key.public_key().to_base64(), + ); + } + data["self_signing_keys"] = json!({self.user_id.as_str(): self_signing_key_json}); + } + + if let Some(user_signing_key) = &self.user_signing_key { + let public_key_base64 = user_signing_key.public_key().to_base64(); + let mut user_signing_key_json = json!({ + "keys": { + format!("ed25519:{}", public_key_base64): public_key_base64, + }, + "signatures": {}, + "usage": [ "user_signing" ], + "user_id": self.user_id.clone(), + }); + + if let Some(master_key) = &self.master_cross_signing_key { + // Sign user-signing key with master key + sign_json( + &mut user_signing_key_json, + master_key, + &self.user_id, + &master_key.public_key().to_base64(), + ); + } + data["user_signing_keys"] = json!({self.user_id.as_str(): user_signing_key_json}); + } + + data + } +} + /// This set of keys/query response was generated using a local synapse. -/// Each users was created, device added according to needs and the payload -/// of the keys query have been copy/pasted here. /// /// The current user is `@me:localhost`, the private part of the /// cross-signing keys have been exported using the console with the @@ -45,248 +257,102 @@ impl KeyDistributionTestData { pub const USER_SIGNING_KEY_PRIVATE_EXPORT: &'static str = "zQSosK46giUFs2ACsaf32bA7drcIXbmViyEt+TLfloI"; + /// Current user's private user-signing key, as an [`Ed25519SecretKey`]. + pub fn me_private_user_signing_key() -> Ed25519SecretKey { + Ed25519SecretKey::from_base64(Self::USER_SIGNING_KEY_PRIVATE_EXPORT).unwrap() + } + /// Current user keys query response containing the cross-signing keys pub fn me_keys_query_response() -> KeyQueryResponse { - let data = json!({ - "master_keys": { - "@me:localhost": { - "keys": { - "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4" - }, - "signatures": { - "@me:localhost": { - "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "5G9+Ns28rzNd+2DvP73Y0orr8sxduRQcrJj0YB7ZygH7oeXshvGLeQn6mcNs7q7ZrMR5bYlXxopufKSWWoKpCg", - "ed25519:YVKUSVBKWX": "ih1Kmj4dTB1AjjkwrLA2qIL3e/oPUFisP5Ic8kGp29wrpoHokasKKnkRl1zS7zq6iBcOL6aOZLPPX/ZHYCX5BQ" - } - }, - "usage": [ - "master" - ], - "user_id": "@me:localhost" - } - }, - "self_signing_keys": { - "@me:localhost": { - "keys": { - "ed25519:9gXJQzvqZ+KQunfBTd0g9AkrulwEeFfspyWTSQFqqrw": "9gXJQzvqZ+KQunfBTd0g9AkrulwEeFfspyWTSQFqqrw" - }, - "signatures": { - "@me:localhost": { - "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "amiKDLpWIwUQPzq+eov6KJsoskkWA1YzrGNb7HF3OcGV0nm4t7df0tUdZB/OpREtT5D78BKtzOPUipde2DxUAw" - } - }, - "usage": [ - "self_signing" - ], - "user_id": "@me:localhost" - } - }, - "user_signing_keys": { - "@me:localhost": { - "keys": { - "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY" - }, - "signatures": { - "@me:localhost": { - "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "Cv56vTHAzRkvdcELleOlhECZQP0pXcikCdEZrnXbkjXQ/k0ZvVOJ1beG/SiH8xc6zh1bCIMYv96C9p8o+7VZCQ" - } - }, - "usage": [ - "user_signing" - ], - "user_id": "@me:localhost" - } - } + let builder = TestUserDataBuilder::new(Self::me_id().to_owned()).with_cross_signing_keys( + Ed25519SecretKey::from_base64(Self::MASTER_KEY_PRIVATE_EXPORT).unwrap(), + Ed25519SecretKey::from_base64(Self::SELF_SIGNING_KEY_PRIVATE_EXPORT).unwrap(), + Self::me_private_user_signing_key(), + ); + + let data = builder.build_keys_query_response_json(); + with_settings!({sort_maps => true}, { + assert_json_snapshot!("KeyDistributionTestData::me_keys_query_response", data); }); - ruma_response_from_json(&data) } - /// Dan has cross-signing setup, one device is cross signed `JHPUERYQUW`, - /// but not the other one `FRGNMZVOKA`. - /// `@dan` identity is signed by `@me` identity (alice trust dan) + /// Dan's (base64-encoded) private master cross-signing key. + const DAN_PRIVATE_MASTER_CROSS_SIGNING_KEY: &'static str = + "QGZo39k199RM0NYvPvFNXBspc5llftHWKKHqEi25q0U"; + + /// Dan's (base64-encoded) private self-signing key. + const DAN_PRIVATE_SELF_SIGNING_KEY: &'static str = + "0ES1HO5VXpy/BsXxadwsk6QcwH/ci99KkV9ZlPakHlU"; + + /// Dan's (base64-encoded) private user-signing key. + const DAN_PRIVATE_USER_SIGNING_KEY: &'static str = + "vSdfrHJO8sZH/54r1uCg8BE0CdcDVGkPQNOu7Ej8BBs"; + + /// Dan has cross-signing set up; one device is cross-signed (`JHPUERYQUW`), + /// but not the other one (`FRGNMZVOKA`). + /// + /// `@dan`'s identity is signed by `@me`'s identity (Alice trusts Dan). pub fn dan_keys_query_response() -> KeyQueryResponse { - let data: Value = json!({ - "device_keys": { - "@dan:localhost": { - "JHPUERYQUW": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "JHPUERYQUW", - "keys": { - "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", - "ed25519:JHPUERYQUW": "jZ5Ca/J5RXn3qnNWIHFz9EQBZ4637QI/9ExSiEcGC7I" - }, - "signatures": { - "@dan:localhost": { - "ed25519:JHPUERYQUW": "PaVfCE9QODgluq0gYMpjCarfDbraRXU71uRcUN5MoqtiJYlB0bjzY6bD5/qxugrsgcx4DZOgCLgiyoEZ/vW4DQ", - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "2sZcF5aSyEuryTfWgsw3rNDevnZisH2Df6fCO5pmGwweiaD+n6+pyrzB75mvA1sOwzm9jfTsjv/2+Uj1CNOTBA" - } - }, - "user_id": "@dan:localhost", - }, - "FRGNMZVOKA": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "FRGNMZVOKA", - "keys": { - "curve25519:FRGNMZVOKA": "Hc/BC/xyQIEnScyZkEk+ilDMfOARxHMFoEcggPqqRw4", - "ed25519:FRGNMZVOKA": "jVroR0JoRemjF0vJslY3HirJgwfX5gm5DCM64hZgkI0" - }, - "signatures": { - "@dan:localhost": { - "ed25519:FRGNMZVOKA": "+row23EcWR2D8EKgwzZmy3dWz/l5DHvEHR6jHKnBohphEIsBl0o3Cp9rIztFpStFGRPSAa3xEqfMVW2dIaKkCg" - } - }, - "user_id": "@dan:localhost", - }, - } - }, - "failures": {}, - "master_keys": { - "@dan:localhost": { - "keys": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k" - }, - "signatures": { - "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "DI/zpWA/wG1tdK9aLof1TGBHtihtQZQ+7e62QRSBbo+RAHlQ+akGcaVskLbtLdEKbcJEt61F+Auol+XVGlCEBA", - "ed25519:SNEBMNPLHN": "5Y8byBteGZo1SvPf8QM88pvThJu+2mJ4020YsTLPhCQ4DfdalHWTPOvE7gw09cCONhX/cKY7YHMyH8R26Yd9DA" - }, - "@me:localhost": { - "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "vg2MLJx36Usti4NfsbOfk0ipW7koOoTlBibZkQNrPTMX88V+geTgDjvIMEU/OAyEsgsDHjg3C+2t/yUUDE7hBA" - } - }, - "usage": [ - "master" - ], - "user_id": "@dan:localhost" - } - }, - "self_signing_keys": { - "@dan:localhost": { - "keys": { - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak" - }, - "signatures": { - "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "vxUCzOO4EGwLp+tzfoFbPOVicynvmWgxVx/bv/3fG/Xfl7piJVmeHP+1qDstOewiREuO4W+ti/tYkOXd7GgoAw" - } - }, - "usage": [ - "self_signing" - ], - "user_id": "@dan:localhost" - } - }, - "user_signing_keys": { - "@dan:localhost": { - "keys": { - "ed25519:N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU": "N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU" - }, - "signatures": { - "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "gbcD579EGVDRePnKV9j6YNwGhssgFeJWhF1NRJhFNAcpbGL8911cW54jyiFKFCev89QemfqyFFljldFLfyN9DA" - } - }, - "usage": [ - "user_signing" - ], - "user_id": "@dan:localhost" - } - } + let data = Self::dan_keys_query_response_json(); + with_settings!({sort_maps => true}, { + assert_json_snapshot!("KeyDistributionTestData::dan_keys_query_response", data); }); - ruma_response_from_json(&data) } - /// Same as `dan_keys_query_response` but `FRGNMZVOKA` was removed. + /// Same as [`Self::dan_keys_query_response`] but `FRGNMZVOKA` was removed. pub fn dan_keys_query_response_device_loggedout() -> KeyQueryResponse { - let data = json!({ - "device_keys": { - "@dan:localhost": { - "JHPUERYQUW": { - "algorithms": [ - "m.olm.v1.curve25519-aes-sha2", - "m.megolm.v1.aes-sha2" - ], - "device_id": "JHPUERYQUW", - "keys": { - "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", - "ed25519:JHPUERYQUW": "jZ5Ca/J5RXn3qnNWIHFz9EQBZ4637QI/9ExSiEcGC7I" - }, - "signatures": { - "@dan:localhost": { - "ed25519:JHPUERYQUW": "PaVfCE9QODgluq0gYMpjCarfDbraRXU71uRcUN5MoqtiJYlB0bjzY6bD5/qxugrsgcx4DZOgCLgiyoEZ/vW4DQ", - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "2sZcF5aSyEuryTfWgsw3rNDevnZisH2Df6fCO5pmGwweiaD+n6+pyrzB75mvA1sOwzm9jfTsjv/2+Uj1CNOTBA" - } - }, - "user_id": "@dan:localhost", - }, - } - }, - "failures": {}, - "master_keys": { - "@dan:localhost": { - "keys": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k" - }, - "signatures": { - "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "DI/zpWA/wG1tdK9aLof1TGBHtihtQZQ+7e62QRSBbo+RAHlQ+akGcaVskLbtLdEKbcJEt61F+Auol+XVGlCEBA", - "ed25519:SNEBMNPLHN": "5Y8byBteGZo1SvPf8QM88pvThJu+2mJ4020YsTLPhCQ4DfdalHWTPOvE7gw09cCONhX/cKY7YHMyH8R26Yd9DA" - }, - "@me:localhost": { - "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "vg2MLJx36Usti4NfsbOfk0ipW7koOoTlBibZkQNrPTMX88V+geTgDjvIMEU/OAyEsgsDHjg3C+2t/yUUDE7hBA" - } - }, - "usage": [ - "master" - ], - "user_id": "@dan:localhost" - } - }, - "self_signing_keys": { - "@dan:localhost": { - "keys": { - "ed25519:aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak": "aX+O6rO/RxzkygPd7XXilKM07aSFK4gSPK1Zxenr6ak" - }, - "signatures": { - "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "vxUCzOO4EGwLp+tzfoFbPOVicynvmWgxVx/bv/3fG/Xfl7piJVmeHP+1qDstOewiREuO4W+ti/tYkOXd7GgoAw" - } - }, - "usage": [ - "self_signing" - ], - "user_id": "@dan:localhost" - } - }, - "user_signing_keys": { - "@dan:localhost": { - "keys": { - "ed25519:N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU": "N4y+jN6GctRXyNDa1CFRdjofTTxHkNK9t430jE9DxrU" - }, - "signatures": { - "@dan:localhost": { - "ed25519:Nj4qZEmWplA8tofkjcR+YOvRCYMRLDKY71BT9GFO32k": "gbcD579EGVDRePnKV9j6YNwGhssgFeJWhF1NRJhFNAcpbGL8911cW54jyiFKFCev89QemfqyFFljldFLfyN9DA" - } - }, - "usage": [ - "user_signing" - ], - "user_id": "@dan:localhost" - } - } + let mut data = Self::dan_keys_query_response_json(); + data["device_keys"][Self::dan_id().to_string()] + .as_object_mut() + .unwrap() + .remove(&Self::dan_unsigned_device_id().to_string()); + + with_settings!({sort_maps => true}, { + assert_json_snapshot!( + "KeyDistributionTestData::dan_keys_query_response_device_loggedout", + data + ); }); ruma_response_from_json(&data) } + /// Common helper for [`Self::dan_keys_query_response`] and + /// [`Self::dan_keys_query_response_device_loggedout`]. + /// + /// Returns the JSON for the full response, including both devices. + pub fn dan_keys_query_response_json() -> Value { + let builder = TestUserDataBuilder::new(Self::dan_id().to_owned()) + .with_cross_signing_keys( + Ed25519SecretKey::from_base64(Self::DAN_PRIVATE_MASTER_CROSS_SIGNING_KEY).unwrap(), + Ed25519SecretKey::from_base64(Self::DAN_PRIVATE_SELF_SIGNING_KEY).unwrap(), + Ed25519SecretKey::from_base64(Self::DAN_PRIVATE_USER_SIGNING_KEY).unwrap(), + ) + .with_user_verification_signature(Self::me_id(), &Self::me_private_user_signing_key()); + + // Add signed device JHPUERYQUW + let builder = builder.with_device( + Self::dan_signed_device_id(), + &Curve25519PublicKey::from_base64("PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4") + .unwrap(), + &Ed25519SecretKey::from_base64("yzj53Kccfqx2yx9lcTwaRfPZX+7jU19harsDWWu5YnM").unwrap(), + true, + ); + + // Add unsigned device FRGNMZVOKA + let builder = builder.with_device( + Self::dan_unsigned_device_id(), + &Curve25519PublicKey::from_base64("Hc/BC/xyQIEnScyZkEk+ilDMfOARxHMFoEcggPqqRw4") + .unwrap(), + &Ed25519SecretKey::from_base64("/SlFtNKxTPN+i4pHzSPWZ1Oc6ymMB33sS32GXZkaLos").unwrap(), + false, + ); + + builder.build_keys_query_response_json() + } + /// Dave is a user that has not enabled cross-signing pub fn dave_keys_query_response() -> KeyQueryResponse { let data = json!({ @@ -581,58 +647,16 @@ impl VerificationViolationTestData { /// `/keys/query` response for Alice, containing the public cross-signing /// keys. pub fn own_keys_query_response_1() -> KeyQueryResponse { - let data = json!({ - "master_keys": { - "@alice:localhost": { - "keys": { - "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk" - }, - "signatures": { - "@alice:localhost": { - "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "FX+srrw9SRmi12fexYHH1jrlEIWgOfre1aPNzDZWcAlaP9WKRdhcQGh70/3F9hk/PGr51I+ux62YgU4xnRTqAA", - "ed25519:PWVCNMMGCT": "teLq0rCYKX9h8WXu6kH8UE6HPKAtkF/DwCncxJGvVBCyZRtLHD8W1yYEzJXjTNynn+4fibQZBhR3th1RGLn4Ag" - } - }, - "usage": [ - "master" - ], - "user_id": "@alice:localhost" - } - }, - "self_signing_keys": { - "@alice:localhost": { - "keys": { - "ed25519:WXLer0esHUanp8DCeu2Be0xB5ms9aKFFBrCFl50COjw": "WXLer0esHUanp8DCeu2Be0xB5ms9aKFFBrCFl50COjw" - }, - "signatures": { - "@alice:localhost": { - "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "lCV9R1xjD34arzq/CAuej1XBv+Ip4dFfAGHfe7znbW7rnwKDaX5PaX3MHk+EIC7nXvUYEAn502WcUFme5c0cCQ" - } - }, - "usage": [ - "self_signing" - ], - "user_id": "@alice:localhost" - } - }, - "user_signing_keys": { - "@alice:localhost": { - "keys": { - "ed25519:MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0": "MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0" - }, - "signatures": { - "@alice:localhost": { - "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "A73QfZ5Dzhh7abdal/sEaq1bfgxzPFU8Bvwa9Y5TIe/a5jTmLVubNmsMSsO5tOT+b6aVJg1G4FtId0Q/cb1aAA" - } - }, - "usage": [ - "user_signing" - ], - "user_id": "@alice:localhost" - } - } + let builder = TestUserDataBuilder::new(Self::own_id().to_owned()).with_cross_signing_keys( + Ed25519SecretKey::from_base64(Self::MASTER_KEY_PRIVATE_EXPORT).unwrap(), + Ed25519SecretKey::from_base64(Self::SELF_SIGNING_KEY_PRIVATE_EXPORT).unwrap(), + Ed25519SecretKey::from_base64(Self::USER_SIGNING_KEY_PRIVATE_EXPORT).unwrap(), + ); + + let data = builder.build_keys_query_response_json(); + with_settings!({sort_maps => true}, { + assert_json_snapshot!("VerificationViolationTestData::own_keys_query_response_1", data); }); - ruma_response_from_json(&data) } @@ -1273,3 +1297,63 @@ impl MaloIdentityChangeDataSet { ruma_response_from_json(&data) } } + +/// Calculate the signature for a JSON object, without adding that signature to +/// the object. +/// +/// # Arguments +/// +/// * `value` - the JSON object to be signed. +/// * `signing_key` - the Ed25519 key to sign with. +fn calculate_json_signature(mut value: Value, signing_key: &Ed25519SecretKey) -> Ed25519Signature { + // strip `unsigned` and any existing signatures + let json_object = value.as_object_mut().expect("value must be object"); + json_object.remove("signatures"); + json_object.remove("unsigned"); + + let canonical_json: CanonicalJsonValue = + value.try_into().expect("could not convert to canonicaljson"); + + // do the signing + signing_key.sign(canonical_json.to_string().as_ref()) +} + +/// Add a signature to a JSON object, following the Matrix JSON-signing spec (https://spec.matrix.org/v1.12/appendices/#signing-details). +/// +/// # Arguments +/// +/// * `value` - the JSON object to be signed. +/// * `signing_key` - the Ed25519 key to sign with. +/// * `user_id` - the user doing the signing. This will be used to add the +/// signature to the object. +/// * `key_identifier` - the name of the key being used to sign with, +/// *excluding* the `ed25519` prefix. +/// +/// # Panics +/// +/// If the JSON value passed in is not an object, or contains a non-object +/// `signatures` property. +fn sign_json( + value: &mut Value, + signing_key: &Ed25519SecretKey, + user_id: &UserId, + key_identifier: &str, +) { + let signature = calculate_json_signature(value.clone(), signing_key); + + let value_obj = value.as_object_mut().expect("value must be object"); + + let signatures_obj = value_obj + .entry("signatures") + .or_insert_with(|| serde_json::Map::new().into()) + .as_object_mut() + .expect("signatures key must be object"); + + let user_signatures_obj = signatures_obj + .entry(user_id.to_string()) + .or_insert_with(|| serde_json::Map::new().into()) + .as_object_mut() + .expect("signatures keys must be object"); + + user_signatures_obj.insert(format!("ed25519:{}", key_identifier), signature.to_base64().into()); +} diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap new file mode 100644 index 00000000000..67ed28374f0 --- /dev/null +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response.snap @@ -0,0 +1,96 @@ +--- +source: testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +expression: data +--- +{ + "device_keys": { + "@dan:localhost": { + "FRGNMZVOKA": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "FRGNMZVOKA", + "keys": { + "curve25519:FRGNMZVOKA": "Hc/BC/xyQIEnScyZkEk+ilDMfOARxHMFoEcggPqqRw4", + "ed25519:FRGNMZVOKA": "xp/IW9Sh8Jw/buUYlARWD20EV2TdUG/SZ+Pa4iEtcew" + }, + "signatures": { + "@dan:localhost": { + "ed25519:FRGNMZVOKA": "G6f8s4a3rXOnODPdSQTUOjJ0YtxlxwDSTPNkzbAMHEwdmnmUuFhTdEYNP/dzDDGd8kViWpMteaPqvlAKIlvlBg" + } + }, + "user_id": "@dan:localhost" + }, + "JHPUERYQUW": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "JHPUERYQUW", + "keys": { + "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", + "ed25519:JHPUERYQUW": "+qMutlCB/eWCbI3bIskWhjYVGrRX8hF+F48sNsmg1YE" + }, + "signatures": { + "@dan:localhost": { + "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "msmrAwToOpBmSLeWyThuV7vS7fXGB291C3KUrM+2a0L6CS6pqpK12nBZG/dLeDzVSAamyWjB3OuwhTl0kE7UCA", + "ed25519:JHPUERYQUW": "I9mcfT2BIwbWfga1P85rdbhEDh5qc/3pLgY8jsqlzXbjl4AfHKBZUvcgQ54kKFOf/jK9pTK2Ed35cDQ1QZ44Cw" + } + }, + "user_id": "@dan:localhost" + } + } + }, + "master_keys": { + "@dan:localhost": { + "keys": { + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo" + }, + "signatures": { + "@dan:localhost": { + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "WiSxmg/RpT8BTHcLvNS7vgKyonsRpX/K6E9EzEfLpNOxXfisPClbpuVtxEb3te/Cx/1UKaio1MJcDxqKFMVmDw" + }, + "@me:localhost": { + "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "lSa34sDcviLPgk8yECGxwdSt2074sf2R8uSmxpTuK5NBqv9dpu0NwTJkO4PLohQTvleIYZSH+uXc3mPZpFy8DQ" + } + }, + "usage": [ + "master" + ], + "user_id": "@dan:localhost" + } + }, + "self_signing_keys": { + "@dan:localhost": { + "keys": { + "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA" + }, + "signatures": { + "@dan:localhost": { + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZKjShHHjXbnkhAUk6P2c03FsUr4wB+3xH7llKV/7QD5wuOapgM7OucABCixivA6xiUVWPGjzZ76y6DFG6YloAA" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@dan:localhost" + } + }, + "user_signing_keys": { + "@dan:localhost": { + "keys": { + "ed25519:2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk": "2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk" + }, + "signatures": { + "@dan:localhost": { + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "bsjJnBMMYcRwcNysPhW9r8jOXuui7otHJH1/clnf7jhEHzj2v8ei4IjZaKLvodFdNLyl/DQE5eOKlhCgt5ekDQ" + } + }, + "usage": [ + "user_signing" + ], + "user_id": "@dan:localhost" + } + } +} diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap new file mode 100644 index 00000000000..06f231100ce --- /dev/null +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::dan_keys_query_response_device_loggedout.snap @@ -0,0 +1,79 @@ +--- +source: testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +expression: data +--- +{ + "device_keys": { + "@dan:localhost": { + "JHPUERYQUW": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "JHPUERYQUW", + "keys": { + "curve25519:JHPUERYQUW": "PBo2nKbink/HxgzMrBftGPogsD0d47LlIMsViTpCRn4", + "ed25519:JHPUERYQUW": "+qMutlCB/eWCbI3bIskWhjYVGrRX8hF+F48sNsmg1YE" + }, + "signatures": { + "@dan:localhost": { + "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "msmrAwToOpBmSLeWyThuV7vS7fXGB291C3KUrM+2a0L6CS6pqpK12nBZG/dLeDzVSAamyWjB3OuwhTl0kE7UCA", + "ed25519:JHPUERYQUW": "I9mcfT2BIwbWfga1P85rdbhEDh5qc/3pLgY8jsqlzXbjl4AfHKBZUvcgQ54kKFOf/jK9pTK2Ed35cDQ1QZ44Cw" + } + }, + "user_id": "@dan:localhost" + } + } + }, + "master_keys": { + "@dan:localhost": { + "keys": { + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo" + }, + "signatures": { + "@dan:localhost": { + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "WiSxmg/RpT8BTHcLvNS7vgKyonsRpX/K6E9EzEfLpNOxXfisPClbpuVtxEb3te/Cx/1UKaio1MJcDxqKFMVmDw" + }, + "@me:localhost": { + "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "lSa34sDcviLPgk8yECGxwdSt2074sf2R8uSmxpTuK5NBqv9dpu0NwTJkO4PLohQTvleIYZSH+uXc3mPZpFy8DQ" + } + }, + "usage": [ + "master" + ], + "user_id": "@dan:localhost" + } + }, + "self_signing_keys": { + "@dan:localhost": { + "keys": { + "ed25519:FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA": "FZSpCr3lGMCfjgRtlLxxHkGJ8dMw5YSYvSYaf7bJ2QA" + }, + "signatures": { + "@dan:localhost": { + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "ZKjShHHjXbnkhAUk6P2c03FsUr4wB+3xH7llKV/7QD5wuOapgM7OucABCixivA6xiUVWPGjzZ76y6DFG6YloAA" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@dan:localhost" + } + }, + "user_signing_keys": { + "@dan:localhost": { + "keys": { + "ed25519:2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk": "2MZakmHIYdjIvYEmUsoNJoAx8Rx2hviO2hq8q5jQ4Rk" + }, + "signatures": { + "@dan:localhost": { + "ed25519:ZzirLovLjBbJ9KkCl/w1+WevTSQdn0ngS4xl36bAuZo": "bsjJnBMMYcRwcNysPhW9r8jOXuui7otHJH1/clnf7jhEHzj2v8ei4IjZaKLvodFdNLyl/DQE5eOKlhCgt5ekDQ" + } + }, + "usage": [ + "user_signing" + ], + "user_id": "@dan:localhost" + } + } +} diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap new file mode 100644 index 00000000000..4a0330b121c --- /dev/null +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__KeyDistributionTestData::me_keys_query_response.snap @@ -0,0 +1,54 @@ +--- +source: testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +expression: data +--- +{ + "master_keys": { + "@me:localhost": { + "keys": { + "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4" + }, + "signatures": { + "@me:localhost": { + "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "5G9+Ns28rzNd+2DvP73Y0orr8sxduRQcrJj0YB7ZygH7oeXshvGLeQn6mcNs7q7ZrMR5bYlXxopufKSWWoKpCg" + } + }, + "usage": [ + "master" + ], + "user_id": "@me:localhost" + } + }, + "self_signing_keys": { + "@me:localhost": { + "keys": { + "ed25519:9gXJQzvqZ+KQunfBTd0g9AkrulwEeFfspyWTSQFqqrw": "9gXJQzvqZ+KQunfBTd0g9AkrulwEeFfspyWTSQFqqrw" + }, + "signatures": { + "@me:localhost": { + "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "amiKDLpWIwUQPzq+eov6KJsoskkWA1YzrGNb7HF3OcGV0nm4t7df0tUdZB/OpREtT5D78BKtzOPUipde2DxUAw" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@me:localhost" + } + }, + "user_signing_keys": { + "@me:localhost": { + "keys": { + "ed25519:mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY": "mvzOc2EuHoVfZTk1hX3y0hyjUs4MrfPv2V/PUFzMQJY" + }, + "signatures": { + "@me:localhost": { + "ed25519:KOS8zz9SJnMOxpfPOx9LO2+abuEcnZP/lxDo5RsXao4": "Cv56vTHAzRkvdcELleOlhECZQP0pXcikCdEZrnXbkjXQ/k0ZvVOJ1beG/SiH8xc6zh1bCIMYv96C9p8o+7VZCQ" + } + }, + "usage": [ + "user_signing" + ], + "user_id": "@me:localhost" + } + } +} diff --git a/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap new file mode 100644 index 00000000000..44e7d4852a7 --- /dev/null +++ b/testing/matrix-sdk-test/src/test_json/snapshots/matrix_sdk_test__test_json__keys_query_sets__VerificationViolationTestData::own_keys_query_response_1.snap @@ -0,0 +1,54 @@ +--- +source: testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +expression: data +--- +{ + "master_keys": { + "@alice:localhost": { + "keys": { + "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk" + }, + "signatures": { + "@alice:localhost": { + "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "FX+srrw9SRmi12fexYHH1jrlEIWgOfre1aPNzDZWcAlaP9WKRdhcQGh70/3F9hk/PGr51I+ux62YgU4xnRTqAA" + } + }, + "usage": [ + "master" + ], + "user_id": "@alice:localhost" + } + }, + "self_signing_keys": { + "@alice:localhost": { + "keys": { + "ed25519:WXLer0esHUanp8DCeu2Be0xB5ms9aKFFBrCFl50COjw": "WXLer0esHUanp8DCeu2Be0xB5ms9aKFFBrCFl50COjw" + }, + "signatures": { + "@alice:localhost": { + "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "lCV9R1xjD34arzq/CAuej1XBv+Ip4dFfAGHfe7znbW7rnwKDaX5PaX3MHk+EIC7nXvUYEAn502WcUFme5c0cCQ" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@alice:localhost" + } + }, + "user_signing_keys": { + "@alice:localhost": { + "keys": { + "ed25519:MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0": "MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0" + }, + "signatures": { + "@alice:localhost": { + "ed25519:EPVg/QLG9+FmNvKjNXfycZEpQLtfHDaTN+rENAURZSk": "A73QfZ5Dzhh7abdal/sEaq1bfgxzPFU8Bvwa9Y5TIe/a5jTmLVubNmsMSsO5tOT+b6aVJg1G4FtId0Q/cb1aAA" + } + }, + "usage": [ + "user_signing" + ], + "user_id": "@alice:localhost" + } + } +}