diff --git a/services/commtest/src/identity/olm_account_infos.rs b/services/commtest/src/identity/olm_account_infos.rs --- a/services/commtest/src/identity/olm_account_infos.rs +++ b/services/commtest/src/identity/olm_account_infos.rs @@ -15,6 +15,12 @@ pub notification_identity_public_keys: IdentityPublicKeys, } +impl ClientPublicKeys { + pub fn device_id(&self) -> String { + self.primary_identity_public_keys.ed25519.clone() + } +} + lazy_static! { pub static ref DEFAULT_CLIENT_KEYS: ClientPublicKeys = ClientPublicKeys { primary_identity_public_keys: IdentityPublicKeys { diff --git a/services/commtest/tests/identity_device_list_tests.rs b/services/commtest/tests/identity_device_list_tests.rs --- a/services/commtest/tests/identity_device_list_tests.rs +++ b/services/commtest/tests/identity_device_list_tests.rs @@ -6,6 +6,7 @@ login_user_device, logout_user_device, register_user_device, DEVICE_TYPE, PLACEHOLDER_CODE_VERSION, }; +use commtest::identity::SigningCapableAccount; use commtest::service_addr; use grpc_clients::identity::authenticated::ChainedInterceptedAuthClient; use grpc_clients::identity::get_auth_client; @@ -14,8 +15,7 @@ }; use grpc_clients::identity::protos::authenticated::GetDeviceListRequest; use grpc_clients::identity::DeviceType; -use serde::Deserialize; -use serde_json::json; +use serde::{Deserialize, Serialize}; // 1. register user with android device // 2. register a web device @@ -82,7 +82,7 @@ // Get device list updates for the user let device_lists_response: Vec> = - get_device_list_history(&mut auth_client, &user_id) + get_raw_device_list_history(&mut auth_client, &user_id) .await .into_iter() .map(|device_list| device_list.devices) @@ -115,7 +115,7 @@ // Initial device list check let initial_device_list = - get_device_list_history(&mut auth_client, &primary_device.user_id) + get_raw_device_list_history(&mut auth_client, &primary_device.user_id) .await .into_iter() .map(|device_list| device_list.devices) @@ -127,17 +127,13 @@ // perform update by adding a new device let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); - let raw_update_payload = json!({ - "devices": [primary_device_id, "device2"], - "timestamp": now.as_millis(), - }); - let update_payload = json!({ - "rawDeviceList": serde_json::to_string(&raw_update_payload).unwrap(), + let devices_payload = vec![primary_device_id, "device2".to_string()]; + + let update_payload = SignedDeviceList::from_raw_unsigned(&RawDeviceList { + devices: devices_payload.clone(), + timestamp: now.as_millis() as i64, }); - let update_request = UpdateDeviceListRequest { - new_device_list: serde_json::to_string(&update_payload) - .expect("failed to serialize payload"), - }; + let update_request = UpdateDeviceListRequest::from(&update_payload); auth_client .update_device_list(update_request) .await @@ -145,16 +141,123 @@ // get device list again let last_device_list = - get_device_list_history(&mut auth_client, &primary_device.user_id).await; + get_raw_device_list_history(&mut auth_client, &primary_device.user_id) + .await; let last_device_list = last_device_list .last() .expect("Failed to get last device list update"); // check that the device list is properly updated - assert_eq!( - last_device_list.devices, - vec![primary_device_id, "device2".into()] - ); + assert_eq!(last_device_list.devices, devices_payload); + assert_eq!(last_device_list.timestamp, now.as_millis() as i64); +} + +#[tokio::test] +async fn test_device_list_signatures() { + // device list history as list of tuples: (signature, devices) + type DeviceListHistoryItem = (Option, Vec); + + // Register user with primary device + let mut primary_account = SigningCapableAccount::new(); + let primary_device_keys = primary_account.public_keys(); + let primary_device_id = primary_device_keys.device_id(); + let user = + register_user_device(Some(&primary_device_keys), Some(DeviceType::Ios)) + .await; + + let mut auth_client = get_auth_client( + &service_addr::IDENTITY_GRPC.to_string(), + user.user_id.clone(), + user.device_id, + user.access_token, + PLACEHOLDER_CODE_VERSION, + DEVICE_TYPE.to_string(), + ) + .await + .expect("Couldn't connect to identity service"); + + // Perform unsigned update (add a new device) + let first_update: DeviceListHistoryItem = { + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let update_payload = SignedDeviceList::from_raw_unsigned(&RawDeviceList { + devices: vec![primary_device_id.clone(), "device2".to_string()], + timestamp: now.as_millis() as i64, + }); + let update_request = UpdateDeviceListRequest::from(&update_payload); + auth_client + .update_device_list(update_request) + .await + .expect("Unsigned device list update failed"); + + ( + update_payload.cur_primary_signature.clone(), + update_payload.into_raw().devices, + ) + }; + + // now perform a update (remove a device), but sign the device list + let second_update: DeviceListHistoryItem = { + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let update_payload = SignedDeviceList::create_signed( + &RawDeviceList { + devices: vec![primary_device_id.clone()], + timestamp: now.as_millis() as i64, + }, + &mut primary_account, + None, + ); + let update_request = UpdateDeviceListRequest::from(&update_payload); + auth_client + .update_device_list(update_request) + .await + .expect("Signed device list update failed"); + + ( + update_payload.cur_primary_signature.clone(), + update_payload.into_raw().devices, + ) + }; + + // now perform a signed update (add a device), but with invalid signature + { + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let mut update_payload = SignedDeviceList::create_signed( + &RawDeviceList { + devices: vec![primary_device_id.clone(), "device3".to_string()], + timestamp: now.as_millis() as i64, + }, + &mut primary_account, + None, + ); + // malfolm signature by replacing first characters + update_payload + .cur_primary_signature + .as_mut() + .expect("signature should be present") + .replace_range(0..3, "foo"); + + let update_request = UpdateDeviceListRequest::from(&update_payload); + auth_client + .update_device_list(update_request) + .await + .expect_err("RPC should fail for invalid signature"); + } + + // check the history to make sure our updates are correct + let device_list_history = + get_device_list_history(&mut auth_client, &user.user_id).await; + + let expected_devices_lists: Vec = vec![ + (None, vec![primary_device_id.clone()]), // auto-generated during registration + first_update, + second_update, + ]; + let actual_device_lists: Vec = device_list_history + .into_iter() + .map(|list| (list.cur_primary_signature.clone(), list.into_raw().devices)) + .collect(); + + assert_eq!(actual_device_lists, expected_devices_lists); } #[tokio::test] @@ -212,7 +315,7 @@ // Get device list updates for the user let device_lists_response: Vec> = - get_device_list_history(&mut auth_client, &user_id) + get_raw_device_list_history(&mut auth_client, &user_id) .await .into_iter() .map(|device_list| device_list.devices) @@ -289,26 +392,67 @@ // See GetDeviceListResponse in identity_authenticated.proto // for details on the response format. -#[derive(Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[allow(unused)] struct RawDeviceList { devices: Vec, timestamp: i64, } -#[derive(Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct SignedDeviceList { raw_device_list: String, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + cur_primary_signature: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + last_primary_signature: Option, +} + +impl RawDeviceList { + fn as_json_string(&self) -> String { + serde_json::to_string(self).expect("Failed to serialize RawDeviceList") + } } impl SignedDeviceList { + fn from_raw_unsigned(raw: &RawDeviceList) -> Self { + Self { + raw_device_list: raw.as_json_string(), + cur_primary_signature: None, + last_primary_signature: None, + } + } + + fn create_signed( + raw: &RawDeviceList, + cur_primary_account: &mut SigningCapableAccount, + last_primary_account: Option<&mut SigningCapableAccount>, + ) -> Self { + let raw_device_list = raw.as_json_string(); + let cur_primary_signature = + cur_primary_account.sign_message(&raw_device_list); + let last_primary_signature = last_primary_account + .map(|account| account.sign_message(&raw_device_list)); + Self { + raw_device_list, + cur_primary_signature: Some(cur_primary_signature), + last_primary_signature, + } + } + fn into_raw(self) -> RawDeviceList { self .raw_device_list .parse() .expect("Failed to parse raw device list") } + + fn as_json_string(&self) -> String { + serde_json::to_string(self).expect("Failed to serialize SignedDeviceList") + } } impl FromStr for SignedDeviceList { @@ -327,10 +471,18 @@ } } +impl From<&SignedDeviceList> for UpdateDeviceListRequest { + fn from(value: &SignedDeviceList) -> Self { + Self { + new_device_list: value.as_json_string(), + } + } +} + async fn get_device_list_history( client: &mut ChainedInterceptedAuthClient, user_id: &str, -) -> Vec { +) -> Vec { let request = GetDeviceListRequest { user_id: user_id.to_string(), since_timestamp: None, @@ -348,7 +500,17 @@ .map(|update| { SignedDeviceList::from_str(&update) .expect("Failed to parse device list update") - .into_raw() }) .collect() } + +async fn get_raw_device_list_history( + client: &mut ChainedInterceptedAuthClient, + user_id: &str, +) -> Vec { + get_device_list_history(client, user_id) + .await + .into_iter() + .map(|signed| signed.into_raw()) + .collect() +}